paramiko.hostkeys

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

# Source Code for Module paramiko.hostkeys

      1  # Copyright (C) 2006-2007  Robey Pointer <robeypointer@gmail.com>
      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          fields = fields[:3]
     60
     61          names, keytype, key = fields
     62          names = names.split(',')
     63
     64          # Decide what kind of key we're looking at and create an object
     65          # to hold it accordingly.
     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
     76 -    def to_line(self):
     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
     87 -    def __repr__(self):
     88          return '<HostKeyEntry %r: %r>' % (self.hostnames, self.key)
     89
     90
     91 -class HostKeys (UserDict.DictMixin):
     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
    103 -    def __init__(self, filename=None):
    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          # emulate a dict of { hostname: { keytype: PKey } }
    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
    181 -    def lookup(self, hostname):
    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                          # replace
    210                          e.key = val
    211                          break
    212                  else:
    213                      # add a new one
    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
    251 -    def clear(self):
    252          """
    253          Remove all host keys from the dictionary.
    254          """
    255          self._entries = []
    256
    257 -    def __getitem__(self, key):
    258          ret = self.lookup(key)
    259          if ret is None:
    260              raise KeyError(key)
    261          return ret
    262
    263 -    def __setitem__(self, hostname, entry):
    264          # don't use this please.
    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                      # replace
    273                      e.key = entry[key_type]
    274                      found = True
    275              if not found:
    276                  self._entries.append(HostKeyEntry([hostname], entry[key_type]))
    277
    278 -    def keys(self):
    279          # python 2.4 sets would be nice here.
    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
    287 -    def values(self):
    288          ret = []
    289          for k in self.keys():
    290              ret.append(self.lookup(k))
    291          return ret
    292
    293 -    def hash_host(hostname, salt=None):
    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