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 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
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
227 e.key = val
228 break
229 else:
230
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
269 """
270 Remove all host keys from the dictionary.
271 """
272 self._entries = []
273
275 ret = self.lookup(key)
276 if ret is None:
277 raise KeyError(key)
278 return ret
279
281
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
290 e.key = entry[key_type]
291 found = True
292 if not found:
293 self._entries.append(HostKeyEntry([hostname], entry[key_type]))
294
296
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
305 ret = []
306 for k in self.keys():
307 ret.append(self.lookup(k))
308 return ret
309
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