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