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