summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/client.py44
-rw-r--r--paramiko/transport.py3
-rw-r--r--sites/www/changelog.rst4
-rw-r--r--tests/test_client.py60
4 files changed, 88 insertions, 23 deletions
diff --git a/paramiko/client.py b/paramiko/client.py
index b2c21798..936693fc 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -343,35 +343,41 @@ class SSHClient (ClosingContextManager):
t.banner_timeout = banner_timeout
if auth_timeout is not None:
t.auth_timeout = auth_timeout
- t.start_client(timeout=timeout)
-
- server_key = t.get_remote_server_key()
- keytype = server_key.get_name()
if port == SSH_PORT:
server_hostkey_name = hostname
else:
server_hostkey_name = "[%s]:%d" % (hostname, port)
+ our_server_keys = None
# If GSS-API Key Exchange is performed we are not required to check the
# host key, because the host is authenticated via GSS-API / SSPI as
# well as our client.
if not self._transport.use_gss_kex:
- our_server_key = self._system_host_keys.get(
- server_hostkey_name, {}).get(keytype)
- if our_server_key is None:
- our_server_key = self._host_keys.get(server_hostkey_name,
- {}).get(keytype, None)
- if our_server_key is None:
- # will raise exception if the key is rejected;
- # let that fall out
- self._policy.missing_host_key(self, server_hostkey_name,
- server_key)
- # if the callback returns, assume the key is ok
- our_server_key = server_key
-
- if server_key != our_server_key:
- raise BadHostKeyException(hostname, server_key, our_server_key)
+ our_server_keys = self._system_host_keys.get(server_hostkey_name)
+ if our_server_keys is None:
+ our_server_keys = self._host_keys.get(server_hostkey_name)
+ if our_server_keys is not None:
+ keytype = our_server_keys.keys()[0]
+ sec_opts = t.get_security_options()
+ other_types = [x for x in sec_opts.key_types if x != keytype]
+ sec_opts.key_types = [keytype] + other_types
+
+ t.start_client(timeout=timeout)
+
+ if not self._transport.use_gss_kex:
+ server_key = t.get_remote_server_key()
+ if our_server_keys is None:
+ # will raise exception if the key is rejected
+ self._policy.missing_host_key(
+ self, server_hostkey_name, server_key
+ )
+ else:
+ our_key = our_server_keys.get(server_key.get_name())
+ if our_key != server_key:
+ if our_key is None:
+ our_key = list(our_server_keys.values())[0]
+ raise BadHostKeyException(hostname, server_key, our_key)
if username is None:
username = getpass.getuser()
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 174e0bf4..bab23fa1 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -2100,6 +2100,9 @@ class Transport(threading.Thread, ClosingContextManager):
self.host_key_type = agreed_keys[0]
if self.server_mode and (self.get_server_key() is None):
raise SSHException('Incompatible ssh peer (can\'t match requested host key type)') # noqa
+ self._log_agreement(
+ 'HostKey', agreed_keys[0], agreed_keys[0]
+ )
if self.server_mode:
agreed_local_ciphers = list(filter(
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 1d299119..2fa80c0a 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,10 @@
Changelog
=========
+* :bug:`865` SSHClient requests the type of host key it has (e.g. from
+ known_hosts) and does not consider a different type to be a "Missing" host
+ key. This fixes a common case where an ECDSA key is in known_hosts and the
+ server also has an RSA host key. Thanks to Pierce Lopez.
* :support:`906 (1.18+)` Clean up a handful of outdated imports and related
tweaks. Thanks to Pierce Lopez.
* :bug:`984` Enhance default cipher preference order such that
diff --git a/tests/test_client.py b/tests/test_client.py
index bddaf4bc..e912d5b2 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -122,7 +122,11 @@ class SSHClientTest (unittest.TestCase):
allowed_keys = FINGERPRINTS.keys()
self.socks, addr = self.sockl.accept()
self.ts = paramiko.Transport(self.socks)
- host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ keypath = test_path('test_rsa.key')
+ host_key = paramiko.RSAKey.from_private_key_file(keypath)
+ self.ts.add_server_key(host_key)
+ keypath = test_path('test_ecdsa_256.key')
+ host_key = paramiko.ECDSAKey.from_private_key_file(keypath)
self.ts.add_server_key(host_key)
server = NullServer(allowed_keys=allowed_keys)
if delay:
@@ -249,8 +253,9 @@ class SSHClientTest (unittest.TestCase):
verify that SSHClient's AutoAddPolicy works.
"""
threading.Thread(target=self._run).start()
- host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
- public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+ hostname = '[%s]:%d' % (self.addr, self.port)
+ key_file = test_path('test_ecdsa_256.key')
+ public_host_key = paramiko.ECDSAKey.from_private_key_file(key_file)
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -263,7 +268,8 @@ class SSHClientTest (unittest.TestCase):
self.assertEqual('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated())
self.assertEqual(1, len(self.tc.get_host_keys()))
- self.assertEqual(public_host_key, self.tc.get_host_keys()['[%s]:%d' % (self.addr, self.port)]['ssh-rsa'])
+ new_host_key = list(self.tc.get_host_keys()[hostname].values())[0]
+ self.assertEqual(public_host_key, new_host_key)
def test_5_save_host_keys(self):
"""
@@ -396,6 +402,52 @@ class SSHClientTest (unittest.TestCase):
auth_timeout=0.5,
)
+ def _client_host_key_bad(self, host_key):
+ threading.Thread(target=self._run).start()
+ hostname = '[%s]:%d' % (self.addr, self.port)
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.WarningPolicy())
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.assertRaises(
+ paramiko.BadHostKeyException,
+ self.tc.connect,
+ password='pygmalion',
+ **self.connect_kwargs
+ )
+
+ def _client_host_key_good(self, ktype, kfile):
+ threading.Thread(target=self._run).start()
+ hostname = '[%s]:%d' % (self.addr, self.port)
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ host_key = ktype.from_private_key_file(test_path(kfile))
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.tc.connect(password='pygmalion', **self.connect_kwargs)
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual(True, self.ts.is_authenticated())
+
+ def test_host_key_negotiation_1(self):
+ host_key = paramiko.ECDSAKey.generate()
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_2(self):
+ host_key = paramiko.RSAKey.generate(2048)
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_3(self):
+ self._client_host_key_good(paramiko.ECDSAKey, 'test_ecdsa_256.key')
+
+ def test_host_key_negotiation_4(self):
+ self._client_host_key_good(paramiko.RSAKey, 'test_rsa.key')
+
def test_update_environment(self):
"""
Verify that environment variables can be set by the client.