summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README2
-rw-r--r--paramiko/__init__.py6
-rw-r--r--paramiko/hostkeys.py187
-rw-r--r--paramiko/util.py30
-rwxr-xr-xtest.py2
-rw-r--r--tests/test_hostkeys.py73
-rw-r--r--tests/test_util.py28
7 files changed, 298 insertions, 30 deletions
diff --git a/README b/README
index 30c27953..0ff4d813 100644
--- a/README
+++ b/README
@@ -257,6 +257,4 @@ v1.0 JIGGLYPUFF
* SFTPClient.set_size
* remove @since that predate 1.0
* put examples in examples/ folder
-* support .ssh/known_hosts files made with HashKnownHosts
* sftp server mode should convert all paths to unicode before calling into sftp_si
-
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index b13f5bdc..00045cb0 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net>
+# Copyright (C) 2003-2006 Robey Pointer <robey@lag.net>
#
# This file is part of paramiko.
#
@@ -84,6 +84,7 @@ from packet import Packetizer
from file import BufferedFile
from agent import Agent, AgentKey
from pkey import PKey
+from hostkeys import HostKeys
# fix module names for epydoc
for x in [Transport, SecurityOptions, Channel, SFTPServer, SSHException, \
@@ -91,7 +92,7 @@ for x in [Transport, SecurityOptions, Channel, SFTPServer, SSHException, \
SubsystemHandler, AuthHandler, RSAKey, DSSKey, SFTPError, \
SFTP, SFTPClient, SFTPServer, Message, Packetizer, SFTPAttributes, \
SFTPHandle, SFTPServerInterface, BufferedFile, Agent, AgentKey, \
- PKey, BaseSFTP, SFTPFile, ServerInterface]:
+ PKey, BaseSFTP, SFTPFile, ServerInterface, HostKeys]:
x.__module__ = 'paramiko'
from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \
@@ -124,4 +125,5 @@ __all__ = [ 'Transport',
'BufferedFile',
'Agent',
'AgentKey',
+ 'HostKeys',
'util' ]
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
new file mode 100644
index 00000000..5ef27160
--- /dev/null
+++ b/paramiko/hostkeys.py
@@ -0,0 +1,187 @@
+# Copyright (C) 2006 Robey Pointer <robey@lag.net>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+L{HostKeys}
+"""
+
+import base64
+from Crypto.Hash import SHA, HMAC
+
+from paramiko.common import *
+from paramiko.dsskey import DSSKey
+from paramiko.rsakey import RSAKey
+
+
+class HostKeys (object):
+ """
+ Representation of an openssh-style "known hosts" file. Host keys can be
+ read from one or more files, and then individual hosts can be looked up to
+ verify server keys during SSH negotiation.
+
+ A HostKeys object can be treated like a dict; any dict lookup is equivalent
+ to calling L{lookup}.
+
+ @since: 1.5.3
+ """
+
+ def __init__(self, filename=None):
+ """
+ Create a new HostKeys object, optionally loading keys from an openssh
+ style host-key file.
+
+ @param filename: filename to load host keys from, or C{None}
+ @type filename: str
+ """
+ # hostname -> keytype -> PKey
+ self.keys = {}
+ self.contains_hashes = False
+ if filename is not None:
+ self.load(filename)
+
+ def add(self, hostname, keytype, key):
+ """
+ Add a host key entry to the table. Any existing entry for a
+ C{(hostname, keytype)} pair will be replaced.
+
+ @param hostname:
+ @type hostname: str
+ @param keytype: key type (C{"ssh-rsa"} or C{"ssh-dss"})
+ @type keytype: str
+ @param key: the key to add
+ @type key: L{PKey}
+ """
+ if not hostname in self.keys:
+ self.keys[hostname] = {}
+ if hostname.startswith('|1|'):
+ self.contains_hashes = True
+ self.keys[hostname][keytype] = key
+
+ def load(self, filename):
+ """
+ Read a file of known SSH host keys, in the format used by openssh.
+ This type of file unfortunately doesn't exist on Windows, but on
+ posix, it will usually be stored in
+ C{os.path.expanduser("~/.ssh/known_hosts")}.
+
+ @param filename: name of the file to read host keys from
+ @type filename: str
+ """
+ f = file(filename, 'r')
+ for line in f:
+ line = line.strip()
+ if (len(line) == 0) or (line[0] == '#'):
+ continue
+ keylist = line.split(' ')
+ if len(keylist) != 3:
+ # don't understand this line
+ continue
+ hostlist, keytype, key = keylist
+ for host in hostlist.split(','):
+ if keytype == 'ssh-rsa':
+ self.add(host, keytype, RSAKey(data=base64.decodestring(key)))
+ elif keytype == 'ssh-dss':
+ self.add(host, keytype, DSSKey(data=base64.decodestring(key)))
+ f.close()
+
+ def lookup(self, hostname):
+ """
+ Find a hostkey entry for a given hostname or IP. If no entry is found,
+ C{None} is returned. Otherwise a dictionary of keytype to key is
+ returned.
+
+ @param hostname: the hostname to lookup
+ @type hostname: str
+ @return: keys associated with this host (or C{None})
+ @rtype: dict(str, L{PKey})
+ """
+ if hostname in self.keys:
+ return self.keys[hostname]
+ if not self.contains_hashes:
+ return None
+ for h in self.keys.keys():
+ if h.startswith('|1|'):
+ hmac = self.hash_host(hostname, h)
+ if hmac == h:
+ return self.keys[h]
+ return None
+
+ def check(self, hostname, key):
+ """
+ Return True if the given key is associated with the given hostname
+ in this dictionary.
+
+ @param hostname: hostname (or IP) of the SSH server
+ @type hostname: str
+ @param key: the key to check
+ @type key: L{PKey}
+ @return: C{True} if the key is associated with the hostname; C{False}
+ if not
+ @rtype: bool
+ """
+ k = self.lookup(hostname)
+ if k is None:
+ return False
+ host_key = k.get(key.get_name(), None)
+ if host_key is None:
+ return False
+ return str(host_key) == str(key)
+
+ def clear(self):
+ """
+ Remove all host keys from the dictionary.
+ """
+ self.keys = {}
+ self.contains_hashes = False
+
+ def values(self):
+ return self.keys.values();
+
+ def __getitem__(self, key):
+ ret = self.lookup(key)
+ if ret is None:
+ raise KeyError(key)
+ return ret
+
+ def __len__(self):
+ return len(self.keys)
+
+ def hash_host(hostname, salt=None):
+ """
+ Return a "hashed" form of the hostname, as used by openssh when storing
+ hashed hostnames in the known_hosts file.
+
+ @param hostname: the hostname to hash
+ @type hostname: str
+ @param salt: optional salt to use when hashing (must be 20 bytes long)
+ @type salt: str
+ @return: the hashed hostname
+ @rtype: str
+ """
+ if salt is None:
+ salt = randpool.get_bytes(SHA.digest_size)
+ else:
+ if salt.startswith('|1|'):
+ salt = salt.split('|')[2]
+ salt = base64.decodestring(salt)
+ assert len(salt) == SHA.digest_size
+ hmac = HMAC.HMAC(salt, hostname, SHA).digest()
+ hostkey = '|1|%s|%s' % (base64.encodestring(salt), base64.encodestring(hmac))
+ return hostkey.replace('\n', '')
+ hash_host = staticmethod(hash_host)
+
diff --git a/paramiko/util.py b/paramiko/util.py
index fad12eeb..2ec963ec 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -189,35 +189,15 @@ def load_host_keys(filename):
This type of file unfortunately doesn't exist on Windows, but on posix,
it will usually be stored in C{os.path.expanduser("~/.ssh/known_hosts")}.
+ Since 1.5.3, this is just a wrapper around L{HostKeys}.
+
@param filename: name of the file to read host keys from
@type filename: str
@return: dict of host keys, indexed by hostname and then keytype
@rtype: dict(hostname, dict(keytype, L{PKey <paramiko.pkey.PKey>}))
"""
- import base64
- from rsakey import RSAKey
- from dsskey import DSSKey
-
- keys = {}
- f = file(filename, 'r')
- for line in f:
- line = line.strip()
- if (len(line) == 0) or (line[0] == '#'):
- continue
- keylist = line.split(' ')
- if len(keylist) != 3:
- continue
- hostlist, keytype, key = keylist
- hosts = hostlist.split(',')
- for host in hosts:
- if not keys.has_key(host):
- keys[host] = {}
- if keytype == 'ssh-rsa':
- keys[host][keytype] = RSAKey(data=base64.decodestring(key))
- elif keytype == 'ssh-dss':
- keys[host][keytype] = DSSKey(data=base64.decodestring(key))
- f.close()
- return keys
+ from paramiko.hostkeys import HostKeys
+ return HostKeys(filename)
def parse_ssh_config(file_obj):
"""
@@ -355,3 +335,5 @@ def get_logger(name):
l = logging.getLogger(name)
l.addFilter(_pfilter)
return l
+
+
diff --git a/test.py b/test.py
index 17b62949..ca5a4aef 100755
--- a/test.py
+++ b/test.py
@@ -31,6 +31,7 @@ sys.path.append('tests/')
from test_message import MessageTest
from test_file import BufferedFileTest
from test_util import UtilTest
+from test_hostkeys import HostKeysTest
from test_pkey import KeyTest
from test_kex import KexTest
from test_packetizer import PacketizerTest
@@ -89,6 +90,7 @@ suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MessageTest))
suite.addTest(unittest.makeSuite(BufferedFileTest))
suite.addTest(unittest.makeSuite(UtilTest))
+suite.addTest(unittest.makeSuite(HostKeysTest))
if options.use_pkey:
suite.addTest(unittest.makeSuite(KeyTest))
suite.addTest(unittest.makeSuite(KexTest))
diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py
new file mode 100644
index 00000000..13426387
--- /dev/null
+++ b/tests/test_hostkeys.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+
+# Copyright (C) 2006 Robey Pointer <robey@lag.net>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Some unit tests for HostKeys.
+"""
+
+import base64
+import os
+import unittest
+import paramiko
+
+
+test_hosts_file = """\
+secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
+9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
+D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
+BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
+5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
+"""
+
+keyblob = """\
+AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31MBGQ3GQ/Fc7SX6gkpXkwcZryoi4k\
+NFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW5ymME3bQ4J/k1IKxCtz/bAlAqFgK\
+oc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M="""
+
+
+class HostKeysTest (unittest.TestCase):
+
+ def setUp(self):
+ f = open('hostfile.temp', 'w')
+ f.write(test_hosts_file)
+ f.close()
+
+ def tearDown(self):
+ os.unlink('hostfile.temp')
+
+ def test_1_load(self):
+ hostdict = paramiko.HostKeys('hostfile.temp')
+ self.assertEquals(2, len(hostdict))
+ self.assertEquals(1, len(hostdict.values()[0]))
+ self.assertEquals(1, len(hostdict.values()[1]))
+ fp = paramiko.util.hexify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint())
+ self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp)
+
+ def test_2_add(self):
+ hostdict = paramiko.HostKeys('hostfile.temp')
+ hh = '|1|BMsIC6cUIP2zBuXR3t2LRcJYjzM=|hpkJMysjTk/+zzUUzxQEa2ieq6c='
+ key = paramiko.RSAKey(data=base64.decodestring(keyblob))
+ hostdict.add(hh, 'ssh-rsa', key)
+ self.assertEquals(3, len(hostdict))
+ x = hostdict['foo.example.com']
+ fp = paramiko.util.hexify(x['ssh-rsa'].get_fingerprint())
+ self.assertEquals('7EC91BB336CB6D810B124B1353C32396', fp)
+ self.assertTrue(hostdict.check('foo.example.com', key))
diff --git a/tests/test_util.py b/tests/test_util.py
index fa8c029b..83852b06 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -23,6 +23,7 @@ Some unit tests for utility functions.
"""
import cStringIO
+import os
import unittest
from Crypto.Hash import SHA
import paramiko.util
@@ -43,10 +44,17 @@ Host spoo.example.com
Crazy something else
"""
+test_hosts_file = """\
+secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
+9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
+D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
+BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
+5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
+"""
-class UtilTest (unittest.TestCase):
- K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504L
+class UtilTest (unittest.TestCase):
def setUp(self):
pass
@@ -78,3 +86,19 @@ class UtilTest (unittest.TestCase):
x = paramiko.util.generate_key_bytes(SHA, 'ABCDEFGH', 'This is my secret passphrase.', 64)
hex = ''.join(['%02x' % ord(c) for c in x])
self.assertEquals(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b')
+
+ def test_4_host_keys(self):
+ f = open('hostfile.temp', 'w')
+ f.write(test_hosts_file)
+ f.close()
+ try:
+ hostdict = paramiko.util.load_host_keys('hostfile.temp')
+ self.assertEquals(2, len(hostdict))
+ self.assertEquals(1, len(hostdict.values()[0]))
+ self.assertEquals(1, len(hostdict.values()[1]))
+ fp = paramiko.util.hexify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint())
+ self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp)
+ finally:
+ os.unlink('hostfile.temp')
+
+