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