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, 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
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
235 e.key = val
236 break
237 else:
238
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
277 """
278 Remove all host keys from the dictionary.
279 """
280 self._entries = []
281
283 ret = self.lookup(key)
284 if ret is None:
285 raise KeyError(key)
286 return ret
287
289
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
298 e.key = entry[key_type]
299 found = True
300 if not found:
301 self._entries.append(HostKeyEntry([hostname], entry[key_type]))
302
304
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
313 ret = []
314 for k in self.keys():
315 ret.append(self.lookup(k))
316 return ret
317
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