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