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 = randpool.get_bytes(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