Package paramiko :: Module hostkeys
[frames] | no frames]

Source Code for Module paramiko.hostkeys

  1  # Copyright (C) 2006-2007  Robey Pointer <robeypointer@gmail.com> 
  2  # 
  3  # This file is part of paramiko. 
  4  # 
  5  # Paramiko is free software; you can redistribute it and/or modify it under the 
  6  # terms of the GNU Lesser General Public License as published by the Free 
  7  # Software Foundation; either version 2.1 of the License, or (at your option) 
  8  # any later version. 
  9  # 
 10  # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY 
 11  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 
 12  # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
 13  # details. 
 14  # 
 15  # You should have received a copy of the GNU Lesser General Public License 
 16  # along with Paramiko; if not, write to the Free Software Foundation, Inc., 
 17  # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA. 
 18   
 19  """ 
 20  L{HostKeys} 
 21  """ 
 22   
 23  import base64 
 24  from Crypto.Hash import SHA, HMAC 
 25  import UserDict 
 26   
 27  from paramiko.common import * 
 28  from paramiko.dsskey import DSSKey 
 29  from paramiko.rsakey import RSAKey 
 30   
 31   
32 -class HostKeyEntry:
33 """ 34 Representation of a line in an OpenSSH-style "known hosts" file. 35 """ 36
37 - def __init__(self, hostnames=None, key=None):
38 self.valid = (hostnames is not None) and (key is not None) 39 self.hostnames = hostnames 40 self.key = key
41
42 - def from_line(cls, line):
43 """ 44 Parses the given line of text to find the names for the host, 45 the type of key, and the key data. The line is expected to be in the 46 format used by the openssh known_hosts file. 47 48 Lines are expected to not have leading or trailing whitespace. 49 We don't bother to check for comments or empty lines. All of 50 that should be taken care of before sending the line to us. 51 52 @param line: a line from an OpenSSH known_hosts file 53 @type line: str 54 """ 55 fields = line.split(' ') 56 if len(fields) < 3: 57 # Bad number of fields 58 return None 59 fields = fields[:3] 60 61 names, keytype, key = fields 62 names = names.split(',') 63 64 # Decide what kind of key we're looking at and create an object 65 # to hold it accordingly. 66 if keytype == 'ssh-rsa': 67 key = RSAKey(data=base64.decodestring(key)) 68 elif keytype == 'ssh-dss': 69 key = DSSKey(data=base64.decodestring(key)) 70 else: 71 return None 72 73 return cls(names, key)
74 from_line = classmethod(from_line) 75
76 - def to_line(self):
77 """ 78 Returns a string in OpenSSH known_hosts file format, or None if 79 the object is not in a valid state. A trailing newline is 80 included. 81 """ 82 if self.valid: 83 return '%s %s %s\n' % (','.join(self.hostnames), self.key.get_name(), 84 self.key.get_base64()) 85 return None
86
87 - def __repr__(self):
88 return '<HostKeyEntry %r: %r>' % (self.hostnames, self.key)
89 90
91 -class HostKeys (UserDict.DictMixin):
92 """ 93 Representation of an openssh-style "known hosts" file. Host keys can be 94 read from one or more files, and then individual hosts can be looked up to 95 verify server keys during SSH negotiation. 96 97 A HostKeys object can be treated like a dict; any dict lookup is equivalent 98 to calling L{lookup}. 99 100 @since: 1.5.3 101 """ 102
103 - def __init__(self, filename=None):
104 """ 105 Create a new HostKeys object, optionally loading keys from an openssh 106 style host-key file. 107 108 @param filename: filename to load host keys from, or C{None} 109 @type filename: str 110 """ 111 # emulate a dict of { hostname: { keytype: PKey } } 112 self._entries = [] 113 if filename is not None: 114 self.load(filename)
115
116 - def add(self, hostname, keytype, key):
117 """ 118 Add a host key entry to the table. Any existing entry for a 119 C{(hostname, keytype)} pair will be replaced. 120 121 @param hostname: the hostname (or IP) to add 122 @type hostname: str 123 @param keytype: key type (C{"ssh-rsa"} or C{"ssh-dss"}) 124 @type keytype: str 125 @param key: the key to add 126 @type key: L{PKey} 127 """ 128 for e in self._entries: 129 if (hostname in e.hostnames) and (e.key.get_name() == keytype): 130 e.key = key 131 return 132 self._entries.append(HostKeyEntry([hostname], key))
133
134 - def load(self, filename):
135 """ 136 Read a file of known SSH host keys, in the format used by openssh. 137 This type of file unfortunately doesn't exist on Windows, but on 138 posix, it will usually be stored in 139 C{os.path.expanduser("~/.ssh/known_hosts")}. 140 141 If this method is called multiple times, the host keys are merged, 142 not cleared. So multiple calls to C{load} will just call L{add}, 143 replacing any existing entries and adding new ones. 144 145 @param filename: name of the file to read host keys from 146 @type filename: str 147 148 @raise IOError: if there was an error reading the file 149 """ 150 f = open(filename, 'r') 151 for line in f: 152 line = line.strip() 153 if (len(line) == 0) or (line[0] == '#'): 154 continue 155 e = HostKeyEntry.from_line(line) 156 if e is not None: 157 self._entries.append(e) 158 f.close()
159
160 - def save(self, filename):
161 """ 162 Save host keys into a file, in the format used by openssh. The order of 163 keys in the file will be preserved when possible (if these keys were 164 loaded from a file originally). The single exception is that combined 165 lines will be split into individual key lines, which is arguably a bug. 166 167 @param filename: name of the file to write 168 @type filename: str 169 170 @raise IOError: if there was an error writing the file 171 172 @since: 1.6.1 173 """ 174 f = open(filename, 'w') 175 for e in self._entries: 176 line = e.to_line() 177 if line: 178 f.write(line) 179 f.close()
180
181 - def lookup(self, hostname):
182 """ 183 Find a hostkey entry for a given hostname or IP. If no entry is found, 184 C{None} is returned. Otherwise a dictionary of keytype to key is 185 returned. The keytype will be either C{"ssh-rsa"} or C{"ssh-dss"}. 186 187 @param hostname: the hostname (or IP) to lookup 188 @type hostname: str 189 @return: keys associated with this host (or C{None}) 190 @rtype: dict(str, L{PKey}) 191 """ 192 class SubDict (UserDict.DictMixin): 193 def __init__(self, hostname, entries, hostkeys): 194 self._hostname = hostname 195 self._entries = entries 196 self._hostkeys = hostkeys
197 198 def __getitem__(self, key): 199 for e in self._entries: 200 if e.key.get_name() == key: 201 return e.key 202 raise KeyError(key)
203 204 def __setitem__(self, key, val): 205 for e in self._entries: 206 if e.key is None: 207 continue 208 if e.key.get_name() == key: 209 # replace 210 e.key = val 211 break 212 else: 213 # add a new one 214 e = HostKeyEntry([hostname], val) 215 self._entries.append(e) 216 self._hostkeys._entries.append(e) 217 218 def keys(self): 219 return [e.key.get_name() for e in self._entries if e.key is not None] 220 221 entries = [] 222 for e in self._entries: 223 for h in e.hostnames: 224 if (h.startswith('|1|') and (self.hash_host(hostname, h) == h)) or (h == hostname): 225 entries.append(e) 226 if len(entries) == 0: 227 return None 228 return SubDict(hostname, entries, self) 229
230 - def check(self, hostname, key):
231 """ 232 Return True if the given key is associated with the given hostname 233 in this dictionary. 234 235 @param hostname: hostname (or IP) of the SSH server 236 @type hostname: str 237 @param key: the key to check 238 @type key: L{PKey} 239 @return: C{True} if the key is associated with the hostname; C{False} 240 if not 241 @rtype: bool 242 """ 243 k = self.lookup(hostname) 244 if k is None: 245 return False 246 host_key = k.get(key.get_name(), None) 247 if host_key is None: 248 return False 249 return str(host_key) == str(key)
250
251 - def clear(self):
252 """ 253 Remove all host keys from the dictionary. 254 """ 255 self._entries = []
256
257 - def __getitem__(self, key):
258 ret = self.lookup(key) 259 if ret is None: 260 raise KeyError(key) 261 return ret
262
263 - def __setitem__(self, hostname, entry):
264 # don't use this please. 265 if len(entry) == 0: 266 self._entries.append(HostKeyEntry([hostname], None)) 267 return 268 for key_type in entry.keys(): 269 found = False 270 for e in self._entries: 271 if (hostname in e.hostnames) and (e.key.get_name() == key_type): 272 # replace 273 e.key = entry[key_type] 274 found = True 275 if not found: 276 self._entries.append(HostKeyEntry([hostname], entry[key_type]))
277
278 - def keys(self):
279 # python 2.4 sets would be nice here. 280 ret = [] 281 for e in self._entries: 282 for h in e.hostnames: 283 if h not in ret: 284 ret.append(h) 285 return ret
286
287 - def values(self):
288 ret = [] 289 for k in self.keys(): 290 ret.append(self.lookup(k)) 291 return ret
292
293 - def hash_host(hostname, salt=None):
294 """ 295 Return a "hashed" form of the hostname, as used by openssh when storing 296 hashed hostnames in the known_hosts file. 297 298 @param hostname: the hostname to hash 299 @type hostname: str 300 @param salt: optional salt to use when hashing (must be 20 bytes long) 301 @type salt: str 302 @return: the hashed hostname 303 @rtype: str 304 """ 305 if salt is None: 306 salt = randpool.get_bytes(SHA.digest_size) 307 else: 308 if salt.startswith('|1|'): 309 salt = salt.split('|')[2] 310 salt = base64.decodestring(salt) 311 assert len(salt) == SHA.digest_size 312 hmac = HMAC.HMAC(salt, hostname, SHA).digest() 313 hostkey = '|1|%s|%s' % (base64.encodestring(salt), base64.encodestring(hmac)) 314 return hostkey.replace('\n', '')
315 hash_host = staticmethod(hash_host) 316