paramiko.hostkeys

Package paramiko :: Module hostkeys |
[frames] | no frames]

# Source Code for Module paramiko.hostkeys

      1  # Copyright (C) 2006-2007  Robey Pointer <robey@lag.net>
      2  #
      3  # This file is part of paramiko.
      4  #
      5  # Paramiko is free software; you can redistribute it and/or modify it under the
      6  # terms of the GNU Lesser General Public License as published by the Free
      7  # Software Foundation; either version 2.1 of the License, or (at your option)
      8  # any later version.
      9  #
     10  # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
     11  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     12  # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
     13  # details.
     14  #
     15  # You should have received a copy of the GNU Lesser General Public License
     16  # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
     17  # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
     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
     32 -class HostKeyEntry:
     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              # Bad number of fields
     58              return None
     59
     60          names, keytype, key = fields
     61          names = names.split(',')
     62
     63          # Decide what kind of key we're looking at and create an object
     64          # to hold it accordingly.
     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
     75 -    def to_line(self):
     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
     86 -    def __repr__(self):
     87          return '<HostKeyEntry %r: %r>' % (self.hostnames, self.key)
     88
     89
     90 -class HostKeys (UserDict.DictMixin):
     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
    102 -    def __init__(self, filename=None):
    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          # emulate a dict of { hostname: { keytype: PKey } }
    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
    180 -    def lookup(self, hostname):
    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                          # replace
    209                          e.key = val
    210                          break
    211                  else:
    212                      # add a new one
    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
    250 -    def clear(self):
    251          """
    252          Remove all host keys from the dictionary.
    253          """
    254          self._entries = []
    255
    256 -    def __getitem__(self, key):
    257          ret = self.lookup(key)
    258          if ret is None:
    259              raise KeyError(key)
    260          return ret
    261
    262 -    def __setitem__(self, hostname, entry):
    263          # don't use this please.
    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                      # replace
    272                      e.key = entry[key_type]
    273                      found = True
    274              if not found:
    275                  self._entries.append(HostKeyEntry([hostname], entry[key_type]))
    276
    277 -    def keys(self):
    278          # python 2.4 sets would be nice here.
    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
    286 -    def values(self):
    287          ret = []
    288          for k in self.keys():
    289              ret.append(self.lookup(k))
    290          return ret
    291
    292 -    def hash_host(hostname, salt=None):
    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