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, hash_hostname=True):
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 """ 146 for e in self._entries: 147 if (hostname in e.hostnames) and (e.key.get_name() == keytype): 148 e.key = key 149 return 150 if not hostname.startswith('|1|') and hash_hostname: 151 hostname = self.hash_host(hostname) 152 self._entries.append(HostKeyEntry([hostname], key))
153
154 - def load(self, filename):
155 """ 156 Read a file of known SSH host keys, in the format used by openssh. 157 This type of file unfortunately doesn't exist on Windows, but on 158 posix, it will usually be stored in 159 C{os.path.expanduser("~/.ssh/known_hosts")}. 160 161 If this method is called multiple times, the host keys are merged, 162 not cleared. So multiple calls to C{load} will just call L{add}, 163 replacing any existing entries and adding new ones. 164 165 @param filename: name of the file to read host keys from 166 @type filename: str 167 168 @raise IOError: if there was an error reading the file 169 """ 170 f = open(filename, 'r') 171 for lineno, line in enumerate(f): 172 line = line.strip() 173 if (len(line) == 0) or (line[0] == '#'): 174 continue 175 e = HostKeyEntry.from_line(line, lineno) 176 if e is not None: 177 _hostnames = e.hostnames 178 for h in _hostnames: 179 if self.check(h, e.key): 180 e.hostnames.remove(h) 181 if len(e.hostnames): 182 self._entries.append(e) 183 f.close()
184
185 - def save(self, filename):
186 """ 187 Save host keys into a file, in the format used by openssh. The order of 188 keys in the file will be preserved when possible (if these keys were 189 loaded from a file originally). The single exception is that combined 190 lines will be split into individual key lines, which is arguably a bug. 191 192 @param filename: name of the file to write 193 @type filename: str 194 195 @raise IOError: if there was an error writing the file 196 197 @since: 1.6.1 198 """ 199 f = open(filename, 'w') 200 for e in self._entries: 201 line = e.to_line() 202 if line: 203 f.write(line) 204 f.close()
205
206 - def lookup(self, hostname):
207 """ 208 Find a hostkey entry for a given hostname or IP. If no entry is found, 209 C{None} is returned. Otherwise a dictionary of keytype to key is 210 returned. The keytype will be either C{"ssh-rsa"} or C{"ssh-dss"}. 211 212 @param hostname: the hostname (or IP) to lookup 213 @type hostname: str 214 @return: keys associated with this host (or C{None}) 215 @rtype: dict(str, L{PKey}) 216 """ 217 class SubDict (UserDict.DictMixin): 218 def __init__(self, hostname, entries, hostkeys): 219 self._hostname = hostname 220 self._entries = entries 221 self._hostkeys = hostkeys
222 223 def __getitem__(self, key): 224 for e in self._entries: 225 if e.key.get_name() == key: 226 return e.key 227 raise KeyError(key)
228 229 def __setitem__(self, key, val): 230 for e in self._entries: 231 if e.key is None: 232 continue 233 if e.key.get_name() == key: 234 # replace 235 e.key = val 236 break 237 else: 238 # add a new one 239 e = HostKeyEntry([hostname], val) 240 self._entries.append(e) 241 self._hostkeys._entries.append(e) 242 243 def keys(self): 244 return [e.key.get_name() for e in self._entries if e.key is not None] 245 246 entries = [] 247 for e in self._entries: 248 for h in e.hostnames: 249 if (h.startswith('|1|') and (self.hash_host(hostname, h) == h)) or (h == hostname): 250 entries.append(e) 251 if len(entries) == 0: 252 return None 253 return SubDict(hostname, entries, self) 254
255 - def check(self, hostname, key):
256 """ 257 Return True if the given key is associated with the given hostname 258 in this dictionary. 259 260 @param hostname: hostname (or IP) of the SSH server 261 @type hostname: str 262 @param key: the key to check 263 @type key: L{PKey} 264 @return: C{True} if the key is associated with the hostname; C{False} 265 if not 266 @rtype: bool 267 """ 268 k = self.lookup(hostname) 269 if k is None: 270 return False 271 host_key = k.get(key.get_name(), None) 272 if host_key is None: 273 return False 274 return str(host_key) == str(key)
275
276 - def clear(self):
277 """ 278 Remove all host keys from the dictionary. 279 """ 280 self._entries = []
281
282 - def __getitem__(self, key):
283 ret = self.lookup(key) 284 if ret is None: 285 raise KeyError(key) 286 return ret
287
288 - def __setitem__(self, hostname, entry):
289 # don't use this please. 290 if len(entry) == 0: 291 self._entries.append(HostKeyEntry([hostname], None)) 292 return 293 for key_type in entry.keys(): 294 found = False 295 for e in self._entries: 296 if (hostname in e.hostnames) and (e.key.get_name() == key_type): 297 # replace 298 e.key = entry[key_type] 299 found = True 300 if not found: 301 self._entries.append(HostKeyEntry([hostname], entry[key_type]))
302
303 - def keys(self):
304 # python 2.4 sets would be nice here. 305 ret = [] 306 for e in self._entries: 307 for h in e.hostnames: 308 if h not in ret: 309 ret.append(h) 310 return ret
311
312 - def values(self):
313 ret = [] 314 for k in self.keys(): 315 ret.append(self.lookup(k)) 316 return ret
317
318 - def hash_host(hostname, salt=None):
319 """ 320 Return a "hashed" form of the hostname, as used by openssh when storing 321 hashed hostnames in the known_hosts file. 322 323 @param hostname: the hostname to hash 324 @type hostname: str 325 @param salt: optional salt to use when hashing (must be 20 bytes long) 326 @type salt: str 327 @return: the hashed hostname 328 @rtype: str 329 """ 330 if salt is None: 331 salt = rng.read(SHA.digest_size) 332 else: 333 if salt.startswith('|1|'): 334 salt = salt.split('|')[2] 335 salt = base64.decodestring(salt) 336 assert len(salt) == SHA.digest_size 337 hmac = HMAC.HMAC(salt, hostname, SHA).digest() 338 hostkey = '|1|%s|%s' % (base64.encodestring(salt), base64.encodestring(hmac)) 339 return hostkey.replace('\n', '')
340 hash_host = staticmethod(hash_host) 341