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