1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
35
37 self.line = line
38 self.exc = exc
39 self.args = (line, exc)
40
41
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
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
78
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
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
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
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
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
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
232 e.key = val
233 break
234 else:
235
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
274 """
275 Remove all host keys from the dictionary.
276 """
277 self._entries = []
278
280 ret = self.lookup(key)
281 if ret is None:
282 raise KeyError(key)
283 return ret
284
286
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
295 e.key = entry[key_type]
296 found = True
297 if not found:
298 self._entries.append(HostKeyEntry([hostname], entry[key_type]))
299
301
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
310 ret = []
311 for k in self.keys():
312 ret.append(self.lookup(k))
313 return ret
314
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