1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
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
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
168 e.key = val
169 break
170 else:
171
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 not hostname.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
207 """
208 Remove all host keys from the dictionary.
209 """
210 self._entries = []
211
213 for k in self.keys():
214 yield k
215
217 return len(self.keys())
218
221
223 ret = self.lookup(key)
224 if ret is None:
225 raise KeyError(key)
226 return ret
227
229
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
238 e.key = entry[key_type]
239 found = True
240 if not found:
241 self._entries.append(HostKeyEntry([hostname], entry[key_type]))
242
244
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
253 ret = []
254 for k in self.keys():
255 ret.append(self.lookup(k))
256 return ret
257
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
282 self.line = line
283 self.exc = exc
284 self.args = (line, exc)
285
286
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
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
322
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
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