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