From fdc09c9f93fd189a6398d5b350a3c91011d9b4cb Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Sat, 3 Jun 2017 06:58:38 -1000 Subject: use cryptography's sign/verify methods instead of signer/verifier --- paramiko/dsskey.py | 9 +++------ paramiko/ecdsakey.py | 12 ++++-------- paramiko/rsakey.py | 15 +++++---------- setup.py | 2 +- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index ac6875bc..ae7f9799 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -112,9 +112,8 @@ class DSSKey(PKey): ) ) ).private_key(backend=default_backend()) - signer = key.signer(hashes.SHA1()) - signer.update(data) - r, s = decode_dss_signature(signer.finalize()) + sig = key.sign(data, hashes.SHA1()) + r, s = decode_dss_signature(sig) m = Message() m.add_string('ssh-dss') @@ -152,10 +151,8 @@ class DSSKey(PKey): g=self.g ) ).public_key(backend=default_backend()) - verifier = key.verifier(signature, hashes.SHA1()) - verifier.update(data) try: - verifier.verify() + key.verify(signature, data, hashes.SHA1()) except InvalidSignature: return False else: diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 51f8d8ce..b13b9a3c 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -181,9 +181,7 @@ class ECDSAKey(PKey): def sign_ssh_data(self, data): ecdsa = ec.ECDSA(self.ecdsa_curve.hash_object()) - signer = self.signing_key.signer(ecdsa) - signer.update(data) - sig = signer.finalize() + sig = self.signing_key.sign(data, ecdsa) r, s = decode_dss_signature(sig) m = Message() @@ -198,12 +196,10 @@ class ECDSAKey(PKey): sigR, sigS = self._sigdecode(sig) signature = encode_dss_signature(sigR, sigS) - verifier = self.verifying_key.verifier( - signature, ec.ECDSA(self.ecdsa_curve.hash_object()) - ) - verifier.update(data) try: - verifier.verify() + self.verifying_key.verify( + signature, data, ec.ECDSA(self.ecdsa_curve.hash_object()) + ) except InvalidSignature: return False else: diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 8ccf4c30..8953a626 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -105,12 +105,11 @@ class RSAKey(PKey): return isinstance(self.key, rsa.RSAPrivateKey) def sign_ssh_data(self, data): - signer = self.key.signer( + sig = self.key.sign( + data, padding=padding.PKCS1v15(), algorithm=hashes.SHA1(), ) - signer.update(data) - sig = signer.finalize() m = Message() m.add_string('ssh-rsa') @@ -124,14 +123,10 @@ class RSAKey(PKey): if isinstance(key, rsa.RSAPrivateKey): key = key.public_key() - verifier = key.verifier( - signature=msg.get_binary(), - padding=padding.PKCS1v15(), - algorithm=hashes.SHA1(), - ) - verifier.update(data) try: - verifier.verify() + key.verify( + msg.get_binary(), data, padding.PKCS1v15(), hashes.SHA1() + ) except InvalidSignature: return False else: diff --git a/setup.py b/setup.py index e2ace96d..4cf477ff 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ setup( ], install_requires=[ 'bcrypt>=3.0.0', - 'cryptography>=1.1', + 'cryptography>=1.5', 'pynacl>=1.0.1', 'pyasn1>=0.1.7', ], -- cgit v1.2.3 From 2792bf75876f2843b06b4b71e2422d5eb98793d9 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sun, 11 Jun 2017 23:56:47 +0800 Subject: Add Python 3.6 to classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e2ace96d..77db95ac 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], install_requires=[ 'bcrypt>=3.0.0', -- cgit v1.2.3 From bd807adfa5b8bee01fe30eee5c7c5247aa3fd530 Mon Sep 17 00:00:00 2001 From: Dennis Kaarsemaker Date: Thu, 6 Jul 2017 00:22:55 +0200 Subject: server: Support pre-authentication banners The ssh protocol allows for the server to send a pre-authentication banner. It may be sent any time between the start of authentication and successful authentication. This commit allow ServerInterface subclasses to define messages which we'll send right right at the start of authentication before we send the supported authentication methods. --- paramiko/auth_handler.py | 8 ++++++++ paramiko/server.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index ae88179e..e229df8d 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -36,6 +36,7 @@ from paramiko.common import ( cMSG_USERAUTH_GSSAPI_MIC, MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, MSG_USERAUTH_GSSAPI_ERROR, MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES, + cMSG_USERAUTH_BANNER ) from paramiko.message import Message from paramiko.py3compat import bytestring @@ -225,6 +226,13 @@ class AuthHandler (object): m.add_byte(cMSG_SERVICE_ACCEPT) m.add_string(service) self.transport._send_message(m) + banner, language = self.transport.server_object.get_banner() + if banner: + m = Message() + m.add_byte(cMSG_USERAUTH_BANNER) + m.add_string(banner) + m.add_string(language) + self.transport._send_message(m) return # dunno this one self._disconnect_service_not_available() diff --git a/paramiko/server.py b/paramiko/server.py index adc606bf..f876e779 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -570,6 +570,17 @@ class ServerInterface (object): """ return False + def get_banner(self): + """ + A pre-login banner to display to the user. The message may span + multiple lines separated by crlf pairs. The language should be in + rfc3066 style, for example: en-US + + The default implementation always returns ``(None, None)``. + + :returns: A tuple containing the banner and language code. + """ + return (None, None) class InteractiveQuery (object): """ -- cgit v1.2.3 From e114b0d3f57842b3ddcf0bcc5d19e9f24399cace Mon Sep 17 00:00:00 2001 From: Michal Kuffa Date: Fri, 28 Jul 2017 14:24:15 +0200 Subject: Add file_obj handling to the Ed25519Key constructor --- paramiko/ed25519key.py | 9 +++++++-- tests/test_pkey.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index a50d68bc..0fbc2f67 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -45,7 +45,8 @@ def unpad(data): class Ed25519Key(PKey): - def __init__(self, msg=None, data=None, filename=None, password=None): + def __init__(self, msg=None, data=None, filename=None, password=None, + file_obj=None): verifying_key = signing_key = None if msg is None and data is not None: msg = Message(data) @@ -56,7 +57,11 @@ class Ed25519Key(PKey): elif filename is not None: with open(filename, "r") as f: data = self._read_private_key("OPENSSH", f) - signing_key = self._parse_signing_key_data(data, password) + elif file_obj is not None: + data = self._read_private_key("OPENSSH", file_obj) + + if filename or file_obj: + signing_key = self._parse_signing_key_data(data, password) if signing_key is None and verifying_key is None: raise ValueError("need a key") diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 9bb3c44c..89a3f74a 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -466,6 +466,12 @@ class KeyTest(unittest.TestCase): self.assertTrue(not pub.can_sign()) self.assertEqual(key, pub) + def test_ed25519_load_from_file_obj(self): + with open(test_path('test_ed25519.key')) as pkey_fileobj: + key = Ed25519Key.from_private_key(pkey_fileobj) + self.assertEqual(key, key) + self.assertTrue(key.can_sign()) + def test_keyfile_is_actually_encrypted(self): # Read an existing encrypted private key file_ = test_path('test_rsa_password.key') -- cgit v1.2.3 From 81edf66550d6ba271e3889ab50ccf73eb843b958 Mon Sep 17 00:00:00 2001 From: Michal Kuffa Date: Fri, 28 Jul 2017 14:27:31 +0200 Subject: Move assertions outside of the open context manager --- tests/test_pkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 89a3f74a..4121107a 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -469,8 +469,8 @@ class KeyTest(unittest.TestCase): def test_ed25519_load_from_file_obj(self): with open(test_path('test_ed25519.key')) as pkey_fileobj: key = Ed25519Key.from_private_key(pkey_fileobj) - self.assertEqual(key, key) - self.assertTrue(key.can_sign()) + self.assertEqual(key, key) + self.assertTrue(key.can_sign()) def test_keyfile_is_actually_encrypted(self): # Read an existing encrypted private key -- cgit v1.2.3 -- cgit v1.2.3 From f1b1a333d46a5d01ce7f9b7805a7d9b1dcff03a0 Mon Sep 17 00:00:00 2001 From: DrNeutron <30759787+DrNeutron@users.noreply.github.com> Date: Tue, 8 Aug 2017 16:43:53 -0600 Subject: Update compress.py The previous setting of the compression level to 9 is a poor trade off in CPU and time used for compression vs the size gain over the default level of compression in zlib which is 6. --- paramiko/compress.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/compress.py b/paramiko/compress.py index b55f0b1d..5073109c 100644 --- a/paramiko/compress.py +++ b/paramiko/compress.py @@ -25,7 +25,8 @@ import zlib class ZlibCompressor (object): def __init__(self): - self.z = zlib.compressobj(9) + # Use the default level of zlib compression + self.z = zlib.compressobj() def __call__(self, data): return self.z.compress(data) + self.z.flush(zlib.Z_FULL_FLUSH) -- cgit v1.2.3 From 5a363f62e9ec35bae36fc84002b39e8482a59f34 Mon Sep 17 00:00:00 2001 From: DrNeutron <30759787+DrNeutron@users.noreply.github.com> Date: Tue, 8 Aug 2017 17:02:30 -0600 Subject: Adding changelog for slow compression improvement --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 12c8cb03..c9170dc9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`60` Improved the performance of compressed transport by using default + zlib compression level (which is 6) rather than the max level of 9 which is + very CPU intensive. * :support:`1012` (via :issue:`1016`) Enhance documentation around the new `SFTP.posix_rename ` method so it's referenced in the 'standard' ``rename`` method for increased visibility. -- cgit v1.2.3 From cb7fb16e110a1906140ea6c51b858362823e9918 Mon Sep 17 00:00:00 2001 From: Paul Kapp Date: Fri, 18 Aug 2017 19:22:34 -0400 Subject: Common up break out of Transport.run() loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Can’t seem to reason out any advantage of clearing self.active and calling self.packetizer.close() in these situations instead of simply breaking out of loop and allowing the additional conditional cleanups to be done. Currently looking into tackling some needed cleanup in auth_handler, and not having the auth_handler.abort() called on server disconnect feels like a bug - who knows? --- paramiko/transport.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index bab23fa1..388f60cb 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1825,8 +1825,6 @@ class Transport(threading.Thread, ClosingContextManager): continue elif ptype == MSG_DISCONNECT: self._parse_disconnect(m) - self.active = False - self.packetizer.close() break elif ptype == MSG_DEBUG: self._parse_debug(m) @@ -1850,8 +1848,7 @@ class Transport(threading.Thread, ClosingContextManager): self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) # noqa else: self._log(ERROR, 'Channel request for unknown channel %d' % chanid) # noqa - self.active = False - self.packetizer.close() + break elif ( self.auth_handler is not None and ptype in self.auth_handler._handler_table -- cgit v1.2.3 From 7229597ce0925ee8dafe97544f42dcc193fbad8f Mon Sep 17 00:00:00 2001 From: Paul Kapp Date: Tue, 22 Aug 2017 06:31:47 -0400 Subject: Generic certificate support Roll agnostic certificate support into PKey, and tweak publickey authentication to use it only if set. Requires explicit call to PKey.load_certificate() in order to alter the authentication behavior. --- paramiko/__init__.py | 4 +-- paramiko/auth_handler.py | 18 +++++++++--- paramiko/dsskey.py | 1 + paramiko/ecdsakey.py | 1 + paramiko/ed25519key.py | 1 + paramiko/pkey.py | 69 +++++++++++++++++++++++++++++++++++++++++++++ paramiko/rsakey.py | 1 + tests/test_pkey.py | 24 ++++++++++++++++ tests/test_rsa.key-cert.pub | 1 + tests/test_rsa.key.pub | 1 + 10 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 tests/test_rsa.key-cert.pub create mode 100644 tests/test_rsa.key.pub diff --git a/paramiko/__init__.py b/paramiko/__init__.py index d67ad62f..5d3a10fc 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -30,7 +30,7 @@ __license__ = "GNU Lesser General Public License (LGPL)" from paramiko.transport import SecurityOptions, Transport from paramiko.client import ( - SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, + SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy, ) from paramiko.auth_handler import AuthHandler @@ -57,7 +57,7 @@ from paramiko.message import Message from paramiko.packet import Packetizer from paramiko.file import BufferedFile from paramiko.agent import Agent, AgentKey -from paramiko.pkey import PKey +from paramiko.pkey import PKey, PublicBlob from paramiko.hostkeys import HostKeys from paramiko.config import SSHConfig from paramiko.proxy import ProxyCommand diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index ae88179e..0b13722c 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -186,8 +186,13 @@ class AuthHandler (object): m.add_string(service) m.add_string('publickey') m.add_boolean(True) - m.add_string(key.get_name()) - m.add_string(key) + # Use certificate contents, if available, plain pubkey otherwise + if key.public_blob: + m.add_string(key.public_blob.key_type) + m.add_string(key.public_blob.key_blob) + else: + m.add_string(key.get_name()) + m.add_string(key) return m.asbytes() def wait_for_response(self, event): @@ -244,8 +249,13 @@ class AuthHandler (object): m.add_string(password) elif self.auth_method == 'publickey': m.add_boolean(True) - m.add_string(self.private_key.get_name()) - m.add_string(self.private_key) + # Use certificate contents, if available, plain pubkey otherwise + if self.private_key.public_blob: + m.add_string(self.private_key.public_blob.key_type) + m.add_string(self.private_key.public_blob.key_blob) + else: + m.add_string(self.private_key.get_name()) + m.add_string(self.private_key) blob = self._get_session_blob( self.private_key, 'ssh-connection', self.username) sig = self.private_key.sign_ssh_data(blob) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 9af5d0c1..b3197f62 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -49,6 +49,7 @@ class DSSKey(PKey): self.g = None self.y = None self.x = None + self.public_blob = None if file_obj is not None: self._from_private_key(file_obj, password) return diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index fa850c2e..805d6bc0 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -46,6 +46,7 @@ class _ECDSACurve(object): def __init__(self, curve_class, nist_name): self.nist_name = nist_name self.key_length = curve_class.key_size + self.public_blob = None # Defined in RFC 5656 6.2 self.key_format_identifier = "ecdsa-sha2-" + self.nist_name diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index a50d68bc..d904f1ac 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -63,6 +63,7 @@ class Ed25519Key(PKey): self._signing_key = signing_key self._verifying_key = verifying_key + self.public_blob = None def _parse_signing_key_data(self, data, password): from paramiko.transport import Transport diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 35a26fc7..3a872491 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -33,6 +33,7 @@ from paramiko import util from paramiko.common import o600 from paramiko.py3compat import u, encodebytes, decodebytes, b from paramiko.ssh_exception import SSHException, PasswordRequiredException +from paramiko.message import Message class PKey(object): @@ -363,3 +364,71 @@ class PKey(object): format, encryption ).decode()) + + def load_certificate(self, **kwargs): + """ + Supplement the private key contents with data loaded from + an OpenSSH public key (.pub) or certificate (-cert.pub) file. + + The .pub contents adds no real value, since the private key + file includes sufficient information to derive the public + key info. For certificates, however, this can be used on + the client side to offer authentication requests to the server + based on certificate instead of raw public key. + + See: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys + + Note: very little effort is made to validate the certificate contents, + that is for the server to decide if it is good enough to authenticate + successfully. + """ + blob = PublicBlob(**kwargs) + if not blob.key_type.startswith(self.get_name()): + raise ValueError('PublicBlob type %s incompatible with key type %s' % + (blob.key_type, self.get_name())) + self.public_blob = blob + + +# General construct for an OpenSSH style Public Key blob +# readable from a one-line file of the format: +# [] +# Of little value in the case of standard public keys +# {ssh-rsa, ssh-dss, ssh-ecdsa, ssh-ed25519}, but should +# provide rudimentary support for {*-cert.v01} +class PublicBlob(object): + ''' + OpenSSH plain public key or OpenSSH signed public key (certificate) + A mostly dumb container + ''' + def __init__(self, pubkey_filename=None, pubkey_string=None): + ''' + Can read from a file or string. + ''' + if pubkey_filename: + with open(pubkey_filename) as f: + fields = f.read().split(None, 2) + elif pubkey_string: + fields = pubkey_string.split(None, 2) + else: + raise ValueError('PublicBlob() requires either a pubkey_filename or pubkey_string') + if len(fields) < 2: + raise ValueError('PublicBlob() not enough fields %s', fields) + self.key_type = fields[0] + self.key_blob = decodebytes(fields[1]) + try: + self.comment = fields[2].strip() + except IndexError: + self.comment = None + # Verify that the blob message first (string) field matches the key_type + m = Message(self.key_blob) + blob_type = m.get_string() + if blob_type != self.key_type: + raise ValueError( + 'Invalid PublicBlob contents. Key type [%s], expected [%s]' % + (blob_type, self.key_type)) + + def __str__(self): + if self.comment: + return '%s public key/certificate - %s' % (self.key_type, self.comment) + else: + return '%s public key/certificate' % self.key_type diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index b5107515..7abcfa28 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -40,6 +40,7 @@ class RSAKey(PKey): def __init__(self, msg=None, data=None, filename=None, password=None, key=None, file_obj=None): self.key = None + self.public_blob = None if file_obj is not None: self._from_private_key(file_obj, password) return diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 9bb3c44c..034331a2 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -480,3 +480,27 @@ class KeyTest(unittest.TestCase): self.assert_keyfile_is_encrypted(newfile) finally: os.remove(newfile) + + def test_certificates(self): + # PKey.load_certificate + key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + self.assertTrue(key.public_blob is None) + key.load_certificate(pubkey_filename=test_path('test_rsa.key-cert.pub')) + self.assertTrue(key.public_blob is not None) + self.assertEqual(key.public_blob.key_type, 'ssh-rsa-cert-v01@openssh.com') + self.assertEqual(key.public_blob.comment, 'test_rsa.key.pub') + # Delve into blob contents, for test purposes + msg = Message(key.public_blob.key_blob) + self.assertEqual(msg.get_string(), 'ssh-rsa-cert-v01@openssh.com') + nonce = msg.get_string() + e = msg.get_mpint() + n = msg.get_mpint() + self.assertEqual(e, key.public_numbers.e) + self.assertEqual(n, key.public_numbers.n) + # Serial number + self.assertEqual(msg.get_int64(), 1234) + + # Prevented from loading certificate that doesn't match + key1 = Ed25519Key.from_private_key_file(test_path('test_ed25519.key')) + self.assertRaises(ValueError, key1.load_certificate, + pubkey_filename=test_path('test_rsa.key-cert.pub')) diff --git a/tests/test_rsa.key-cert.pub b/tests/test_rsa.key-cert.pub new file mode 100644 index 00000000..7487ab66 --- /dev/null +++ b/tests/test_rsa.key-cert.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgsZlXTd5NE4uzGAn6TyAqQj+IPbsTEFGap2x5pTRwQR8AAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAAAAAAAE0gAAAAEAAAAmU2FtcGxlIHNlbGYtc2lnbmVkIE9wZW5TU0ggY2VydGlmaWNhdGUAAAASAAAABXVzZXIxAAAABXVzZXIyAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAACVAAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAACPAAAAB3NzaC1yc2EAAACATFHFsARDgQevc6YLxNnDNjsFtZ08KPMyYVx0w5xm95IVZHVWSOc5w+ccjqN9HRwxV3kP7IvL91qx0Uc3MJdB9g/O6HkAP+rpxTVoTb2EAMekwp5+i8nQJW4CN2BSsbQY1M6r7OBZ5nmF4hOW/5Pu4l22lXe2ydy8kEXOEuRpUeQ= test_rsa.key.pub diff --git a/tests/test_rsa.key.pub b/tests/test_rsa.key.pub new file mode 100644 index 00000000..bfa1e150 --- /dev/null +++ b/tests/test_rsa.key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c= -- cgit v1.2.3 From 0f26ff25a1cd47b3eaae412bedabbad9516549f4 Mon Sep 17 00:00:00 2001 From: Paul Kapp Date: Tue, 22 Aug 2017 07:44:05 -0400 Subject: amendment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forgot about AgentKey, and put ECDSA line in wrong __init__. That’s what I get for only screening with test_pkey… --- paramiko/agent.py | 1 + paramiko/ecdsakey.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index bc857efa..7a4dde21 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -387,6 +387,7 @@ class AgentKey(PKey): def __init__(self, agent, blob): self.agent = agent self.blob = blob + self.public_blob = None self.name = Message(blob).get_text() def asbytes(self): diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 805d6bc0..9b74d938 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -46,7 +46,6 @@ class _ECDSACurve(object): def __init__(self, curve_class, nist_name): self.nist_name = nist_name self.key_length = curve_class.key_size - self.public_blob = None # Defined in RFC 5656 6.2 self.key_format_identifier = "ecdsa-sha2-" + self.nist_name @@ -106,6 +105,7 @@ class ECDSAKey(PKey): vals=None, file_obj=None, validate_point=True): self.verifying_key = None self.signing_key = None + self.public_blob = None if file_obj is not None: self._from_private_key(file_obj, password) return -- cgit v1.2.3 From 80c136790b732313e0dcae5a533ced6e9759bea2 Mon Sep 17 00:00:00 2001 From: Paul Kapp Date: Tue, 22 Aug 2017 08:38:07 -0400 Subject: Add certificate filenames to look_for_keys --- paramiko/client.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 936693fc..abfa4cc1 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -608,17 +608,27 @@ class SSHClient (ClosingContextManager): ) if os.path.isfile(full_path): keyfiles.append((keytype, full_path)) + if os.path.isfile(full_path + '-cert.pub'): + keyfiles.append((keytype, full_path + '-cert.pub')) if not look_for_keys: keyfiles = [] for pkey_class, filename in keyfiles: try: - key = pkey_class.from_private_key_file(filename, password) - self._log( - DEBUG, - 'Trying discovered key %s in %s' % ( - hexlify(key.get_fingerprint()), filename)) + if filename.endswith('-cert.pub'): + key = pkey_class.from_private_key_file(filename.rstrip('-cert.pub'), password) + key.load_certificate(pubkey_filename=filename) + self._log( + DEBUG, + 'Trying discovered certificate %s in %s' % ( + hexlify(key.get_fingerprint()), filename)) + else: + key = pkey_class.from_private_key_file(filename, password) + self._log( + DEBUG, + 'Trying discovered key %s in %s' % ( + hexlify(key.get_fingerprint()), filename)) # for 2-factor auth a successfully auth'd key will result # in ['password'] -- cgit v1.2.3 From 8864bdcf30d981e0b30424591ac5fcdb6cafd64d Mon Sep 17 00:00:00 2001 From: Paul Kapp Date: Tue, 22 Aug 2017 13:17:38 -0400 Subject: string slice instead of rstrip, thanks ploxiln --- paramiko/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index abfa4cc1..39837c2c 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -617,7 +617,7 @@ class SSHClient (ClosingContextManager): for pkey_class, filename in keyfiles: try: if filename.endswith('-cert.pub'): - key = pkey_class.from_private_key_file(filename.rstrip('-cert.pub'), password) + key = pkey_class.from_private_key_file(filename[:-len('-cert.pub')], password) key.load_certificate(pubkey_filename=filename) self._log( DEBUG, -- cgit v1.2.3 From d6a9a02c771a532410e3845ee7500d5e7707df5c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 23 Aug 2017 13:17:30 -0700 Subject: Pull in count-errors from invocations --- tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 42c18bd0..a34fd3ce 100644 --- a/tasks.py +++ b/tasks.py @@ -4,6 +4,7 @@ from shutil import rmtree, copytree from invoke import Collection, task from invocations.docs import docs, www, sites from invocations.packaging.release import ns as release_coll, publish +from invocations.testing import count_errors # Until we move to spec-based testing @@ -49,7 +50,7 @@ def release(ctx, sdist=True, wheel=True, sign=True, dry_run=False): # aliasing, defaults etc. release_coll.tasks['publish'] = release -ns = Collection(test, coverage, release_coll, docs, www, sites) +ns = Collection(test, coverage, release_coll, docs, www, sites, count_errors) ns.configure({ 'packaging': { # NOTE: many of these are also set in kwarg defaults above; but having -- cgit v1.2.3 From 7b3698064645c2951d5150685096e81244cff0ed Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 23 Aug 2017 13:22:39 -0700 Subject: Changelog re #1041 --- sites/www/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 12c8cb03..15bf8ebf 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :support:`1041` Modify logic around explicit disconnect + messages, and unknown-channel situations, so that they rely on centralized + shutdown code instead of running their own. This is at worst removing some + unnecessary code, and may help with some situations where Paramiko hangs at + the end of a session. Thanks to Paul Kapp for the patch. * :support:`1012` (via :issue:`1016`) Enhance documentation around the new `SFTP.posix_rename ` method so it's referenced in the 'standard' ``rename`` method for increased visibility. -- cgit v1.2.3 From aae69d5d9cf083ae29d92ac33a5ebd2607c1e8bc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 11:51:19 -0700 Subject: flake8 --- paramiko/auth_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 0b13722c..6c515cb6 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -249,7 +249,8 @@ class AuthHandler (object): m.add_string(password) elif self.auth_method == 'publickey': m.add_boolean(True) - # Use certificate contents, if available, plain pubkey otherwise + # Use certificate contents, if available, plain pubkey + # otherwise if self.private_key.public_blob: m.add_string(self.private_key.public_blob.key_type) m.add_string(self.private_key.public_blob.key_blob) -- cgit v1.2.3 From a8723e08aaff00ee068cbdefa119cd34dd6f0d6b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 11:51:41 -0700 Subject: Changelog and docs re #1042 --- paramiko/client.py | 18 ++++++++++++++++-- sites/www/changelog.rst | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 39837c2c..0539d83d 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -240,9 +240,23 @@ class SSHClient (ClosingContextManager): Authentication is attempted in the following order of priority: - The ``pkey`` or ``key_filename`` passed in (if any) + + - ``key_filename`` may contain OpenSSH public certificate paths + as well as regular private-key paths; when files ending in + ``-cert.pub`` are found, they are assumed to match a private + key, and both components will be loaded. (The private key + itself does *not* need to be listed in ``key_filename`` for + this to occur - *just* the certificate.) + - Any key we can find through an SSH agent - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in ``~/.ssh/`` + + - When OpenSSH-style public certificates exist that match an + existing such private key (so e.g. one has ``id_rsa`` and + ``id_rsa-cert.pub``) the certificate will be loaded alongside + the private key and used for authentication. + - Plain username/password auth, if a password was given If a private key requires a password to unlock it, and a password is @@ -257,8 +271,8 @@ class SSHClient (ClosingContextManager): a password to use for authentication or for unlocking a private key :param .PKey pkey: an optional private key to use for authentication :param str key_filename: - the filename, or list of filenames, of optional private key(s) to - try for authentication + the filename, or list of filenames, of optional private key(s) + and/or certs to try for authentication :param float timeout: an optional timeout (in seconds) for the TCP connect :param bool allow_agent: diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 15bf8ebf..3eb88485 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,21 @@ Changelog ========= +* :feature:`1042` (also partially :issue:`531`) Implement generic (suitable for + all key types) client-side certificate authentication. + + The core implementation is `PKey.load_certificate + ` and its corresponding ``.public_blob`` + attribute on key objects, which is honored in the auth and transport modules. + Additionally, `SSHClient.connect ` will + now automatically load certificate data alongside private key data when one + has appropriately-named cert files (e.g. ``id_rsa-cert.pub``) - see its + docstring for details. + + Thanks to Paul Kapp for the final patch, and to Jason Rigby for earlier work + in :issue:`531` (which remains open as it contains additional functionality + that may get merged later.) + * :support:`1041` Modify logic around explicit disconnect messages, and unknown-channel situations, so that they rely on centralized shutdown code instead of running their own. This is at worst removing some -- cgit v1.2.3 From 0b63610902c9c608423e246162f050e53576f6a4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 13:07:52 -0700 Subject: Refactor and clean up recently tweaked key loading bits in SSHClient --- paramiko/client.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 0539d83d..94d69842 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -513,6 +513,26 @@ class SSHClient (ClosingContextManager): """ return self._transport + def _key_from_filepath(self, filename, klass, password): + """ + Attempt to derive a `.PKey` from given string path ``filename``. + """ + cert_suffix = '-cert.pub' + key_path = filename + is_cert = False + if filename.endswith(cert_suffix): + key_path = filename[:-len(cert_suffix)] + is_cert = True + key = klass.from_private_key_file(key_path, password) + if is_cert: + key.load_certificate(pubkey_filename=filename) + type_ = 'certificate' if is_cert else 'key' + msg = "Trying discovered {0} {1} in {2}".format( + type_, hexlify(key.get_fingerprint()), filename, + ) + self._log(DEBUG, msg) + return key + def _auth(self, username, password, pkey, key_filenames, allow_agent, look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host): """ @@ -570,12 +590,9 @@ class SSHClient (ClosingContextManager): for key_filename in key_filenames: for pkey_class in (RSAKey, DSSKey, ECDSAKey, Ed25519Key): try: - key = pkey_class.from_private_key_file( - key_filename, password) - self._log( - DEBUG, - 'Trying key %s from %s' % ( - hexlify(key.get_fingerprint()), key_filename)) + key = self._key_from_filepath( + key_filename, pkey_class, password, + ) allowed_types = set( self._transport.auth_publickey(username, key)) two_factor = (allowed_types & two_factor_types) @@ -630,20 +647,9 @@ class SSHClient (ClosingContextManager): for pkey_class, filename in keyfiles: try: - if filename.endswith('-cert.pub'): - key = pkey_class.from_private_key_file(filename[:-len('-cert.pub')], password) - key.load_certificate(pubkey_filename=filename) - self._log( - DEBUG, - 'Trying discovered certificate %s in %s' % ( - hexlify(key.get_fingerprint()), filename)) - else: - key = pkey_class.from_private_key_file(filename, password) - self._log( - DEBUG, - 'Trying discovered key %s in %s' % ( - hexlify(key.get_fingerprint()), filename)) - + key = self._key_from_filepath( + filename, pkey_class, password, + ) # for 2-factor auth a successfully auth'd key will result # in ['password'] allowed_types = set( -- cgit v1.2.3 From 9f1d317b7a9a7c4beb55adaddb50cb19b784e204 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 14:03:18 -0700 Subject: Docstring/TODO tweaks --- paramiko/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index 94d69842..22e636a7 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -538,7 +538,7 @@ class SSHClient (ClosingContextManager): """ Try, in order: - - The key passed in, if one was passed in. + - The key(s) passed in, if one was passed in. - Any key we can find through an SSH agent (if allowed). - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in ~/.ssh/ (if allowed). @@ -638,6 +638,7 @@ class SSHClient (ClosingContextManager): "~/%s/id_%s" % (directory, name) ) if os.path.isfile(full_path): + # TODO: only do this append if below did not run keyfiles.append((keytype, full_path)) if os.path.isfile(full_path + '-cert.pub'): keyfiles.append((keytype, full_path + '-cert.pub')) -- cgit v1.2.3 From 797777baad68a1e556d35ef05f346b54452bd7a1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 15:37:55 -0700 Subject: 2nd amendment doesn't grant the right to bare excepts --- paramiko/auth_handler.py | 7 +++---- sites/www/changelog.rst | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 6c515cb6..3d742c06 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -459,10 +459,9 @@ class AuthHandler (object): INFO, 'Auth rejected: public key: %s' % str(e)) key = None - except: - self.transport._log( - INFO, - 'Auth rejected: unsupported or mangled public key') + except Exception as e: + msg = 'Auth rejected: unsupported or mangled public key ({0}: {1})' # noqa + self.transport._log(INFO, msg.format(e.__class__.__name__, e)) key = None if key is None: self._disconnect_no_more_auth() diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 3eb88485..83fc8a8f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :support:`-` Display exception type and message when logging auth-rejection + messages (ones reading ``Auth rejected: unsupported or mangled public key``); + previously this error case had a bare except and did not display exactly why + the key failed. It will now append info such as ``KeyError: + 'some-unknown-type-string'`` or similar. * :feature:`1042` (also partially :issue:`531`) Implement generic (suitable for all key types) client-side certificate authentication. -- cgit v1.2.3 From b942d94e2d59335f11f635164525a4f578ea6991 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 15:40:33 -0700 Subject: Stub tests and partly-working implementation of 'load certs found alongside key_filenames' behavior re #1042 This actually breaks existing tests due to test server not supporting certs...bah --- paramiko/client.py | 30 +++++++++++++++++++++--------- tests/test_client.py | 15 +++++++++++++++ tests/util.py | 1 - 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 22e636a7..3b3895b6 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -515,22 +515,34 @@ class SSHClient (ClosingContextManager): def _key_from_filepath(self, filename, klass, password): """ - Attempt to derive a `.PKey` from given string path ``filename``. + Attempt to derive a `.PKey` from given string path ``filename``: + + - If ``filename`` appears to be a cert, the matching private key is + loaded. + - Otherwise, the filename is assumed to be a private key, and the + matching public cert will be loaded if it exists. """ cert_suffix = '-cert.pub' - key_path = filename - is_cert = False + # Assume privkey, not cert, by default if filename.endswith(cert_suffix): key_path = filename[:-len(cert_suffix)] - is_cert = True + cert_path = filename + else: + key_path = filename + cert_path = filename + cert_suffix + # Blindly try the key path; if no private key, nothing will work. key = klass.from_private_key_file(key_path, password) - if is_cert: - key.load_certificate(pubkey_filename=filename) - type_ = 'certificate' if is_cert else 'key' - msg = "Trying discovered {0} {1} in {2}".format( - type_, hexlify(key.get_fingerprint()), filename, + # TODO: change this to 'Loading' instead of 'Trying' sometime; probably + # when #387 is released, since this is a critical log message users are + # likely testing/filtering for (bah.) + msg = "Trying discovered key {0} in {1}".format( + hexlify(key.get_fingerprint()), key_path, ) self._log(DEBUG, msg) + # Attempt to load cert if it exists. + if os.path.isfile(cert_path): + key.load_certificate(pubkey_filename=cert_path) + self._log(DEBUG, "Adding public certificate {0}".format(cert_path)) return key def _auth(self, username, password, pkey, key_filenames, allow_agent, diff --git a/tests/test_client.py b/tests/test_client.py index e912d5b2..cfffa030 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -248,6 +248,21 @@ class SSHClientTest (unittest.TestCase): allowed_keys=['ecdsa-sha2-nistp256'], ) + def test_certs_allowed_as_key_filename_values(self): + # TODO: connect() with key_filename containing a cert file, loads up + # both the cert and its implied key. This is new functionality on top + # of the OpenSSH-compatible stuff. + assert False + + def test_certs_implicitly_loaded_alongside_key_filename_keys(self): + # TODO: same but with just the key paths, i.e. vanilla OpenSSH behavior + assert False + + def test_default_key_locations_trigger_cert_loads_if_found(self): + # TODO: what it says on the tin: ~/.ssh/id_rsa tries to load + # ~/.ssh/id_rsa-cert.pub + assert False + def test_4_auto_add_policy(self): """ verify that SSHClient's AutoAddPolicy works. diff --git a/tests/util.py b/tests/util.py index b546a7e1..c1b43da8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -4,4 +4,3 @@ root_path = os.path.dirname(os.path.realpath(__file__)) def test_path(filename): return os.path.join(root_path, filename) - -- cgit v1.2.3 From 03df3cf9cd0f12cc04abe88a8674e6968363340c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 17:45:54 -0700 Subject: Overhaul PublicBlob and use it better within RSAKey. This allows server-side Paramiko code to correctly create cert-bearing RSAKey objects and thus verify client signatures, and now the test suite passes again, barring the stub tests. Re #1042 --- paramiko/client.py | 2 +- paramiko/pkey.py | 105 +++++++++++++++++++++++++++++++++++--------------- paramiko/rsakey.py | 20 +++++++++- paramiko/transport.py | 1 + tests/test_pkey.py | 9 +++-- 5 files changed, 102 insertions(+), 35 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 3b3895b6..bff223ea 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -541,7 +541,7 @@ class SSHClient (ClosingContextManager): self._log(DEBUG, msg) # Attempt to load cert if it exists. if os.path.isfile(cert_path): - key.load_certificate(pubkey_filename=cert_path) + key.load_certificate(cert_path) self._log(DEBUG, "Adding public certificate {0}".format(cert_path)) return key diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 3a872491..948152fa 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -365,10 +365,11 @@ class PKey(object): encryption ).decode()) - def load_certificate(self, **kwargs): + def load_certificate(self, value): """ - Supplement the private key contents with data loaded from - an OpenSSH public key (.pub) or certificate (-cert.pub) file. + Supplement the private key contents with data loaded from an OpenSSH + public key (``.pub``) or certificate (``-cert.pub``) file, a string + containing such a file, or a `.Message` object. The .pub contents adds no real value, since the private key file includes sufficient information to derive the public @@ -382,7 +383,13 @@ class PKey(object): that is for the server to decide if it is good enough to authenticate successfully. """ - blob = PublicBlob(**kwargs) + if isinstance(value, Message): + constructor = 'from_message' + elif os.path.isfile(value): + constructor = 'from_file' + else: + constructor = 'from_string' + blob = getattr(PublicBlob, constructor)(value) if not blob.key_type.startswith(self.get_name()): raise ValueError('PublicBlob type %s incompatible with key type %s' % (blob.key_type, self.get_name())) @@ -396,36 +403,74 @@ class PKey(object): # {ssh-rsa, ssh-dss, ssh-ecdsa, ssh-ed25519}, but should # provide rudimentary support for {*-cert.v01} class PublicBlob(object): - ''' - OpenSSH plain public key or OpenSSH signed public key (certificate) - A mostly dumb container - ''' - def __init__(self, pubkey_filename=None, pubkey_string=None): - ''' - Can read from a file or string. - ''' - if pubkey_filename: - with open(pubkey_filename) as f: - fields = f.read().split(None, 2) - elif pubkey_string: - fields = pubkey_string.split(None, 2) - else: - raise ValueError('PublicBlob() requires either a pubkey_filename or pubkey_string') + """ + OpenSSH plain public key or OpenSSH signed public key (certificate). + + Tries to be as dumb as possible and barely cares about specific + per-key-type data. + + ..note:: + Most of the time you'll want to call `from_file`, `from_string` or + `from_message` for useful instantiation, the main constructor is + basically "I should be using ``attrs`` for this." + """ + def __init__(self, type_, blob, comment=None): + """ + Create a new public blob of given type and contents. + + :param str type_: Type indicator, eg ``ssh-rsa``. + :param blob: The blob bytes themselves. + :param str comment: A comment, if one was given (e.g. file-based.) + """ + self.key_type = type_ + self.key_blob = blob + self.comment = comment + + @classmethod + def from_file(cls, filename): + """ + Create a public blob from a ``-cert.pub``-style file on disk. + """ + with open(filename) as f: + string = f.read() + return cls.from_string(string) + + @classmethod + def from_string(cls, string): + """ + Create a public blob from a ``-cert.pub``-style string. + """ + fields = string.split(None, 2) if len(fields) < 2: - raise ValueError('PublicBlob() not enough fields %s', fields) - self.key_type = fields[0] - self.key_blob = decodebytes(fields[1]) + msg = "Not enough fields for public blob: {0}" + raise ValueError(msg.format(fields)) + key_type = fields[0] + key_blob = decodebytes(fields[1]) try: - self.comment = fields[2].strip() + comment = fields[2].strip() except IndexError: - self.comment = None - # Verify that the blob message first (string) field matches the key_type - m = Message(self.key_blob) + comment = None + # Verify that the blob message first (string) field matches the + # key_type + m = Message(key_blob) blob_type = m.get_string() - if blob_type != self.key_type: - raise ValueError( - 'Invalid PublicBlob contents. Key type [%s], expected [%s]' % - (blob_type, self.key_type)) + if blob_type != key_type: + msg = "Invalid PublicBlob contents: key type {0!r}, expected {1!r}" + raise ValueError(msg.format(blob_type, key_type)) + # All good? All good. + return cls(type_=key_type, blob=key_blob, comment=comment) + + @classmethod + def from_message(cls, message): + """ + Create a public blob from a network `.Message`. + + Specifically, a cert-bearing pubkey auth packet, because by definition + OpenSSH-style certificates 'are' their own network representation." + """ + type_ = message.get_text() + message.rewind() + return cls(type_=type_, blob=message.asbytes()) def __str__(self): if self.comment: diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 7abcfa28..3f28689a 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -54,8 +54,26 @@ class RSAKey(PKey): else: if msg is None: raise SSHException('Key object may not be empty') - if msg.get_text() != 'ssh-rsa': + type_ = msg.get_text() + nonce = None + # Regular public key - nothing special to do besides the implicit + # type check. + if type_ == 'ssh-rsa': + pass + # OpenSSH-compatible certificate - store full copy as .public_blob + # (so signing works correctly) and then fast-forward past the + # nonce. + elif type_ == 'ssh-rsa-cert-v01@openssh.com': + # This seems the cleanest way to 'clone' an already-being-read + # message? + self.load_certificate(Message(msg.asbytes())) + # Read out nonce as it comes before the public numbers. + # TODO: usefully interpret it & other non-public-number fields + nonce = msg.get_string() + else: raise SSHException('Invalid key') + # Now that we've read type and (possibly) nonce, public numbers are + # next in either case. self.key = rsa.RSAPublicNumbers( e=msg.get_mpint(), n=msg.get_mpint() ).public_key(default_backend()) diff --git a/paramiko/transport.py b/paramiko/transport.py index 388f60cb..0dc2e28a 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -204,6 +204,7 @@ class Transport(threading.Thread, ClosingContextManager): _key_info = { 'ssh-rsa': RSAKey, + 'ssh-rsa-cert-v01@openssh.com': RSAKey, 'ssh-dss': DSSKey, 'ecdsa-sha2-nistp256': ECDSAKey, 'ecdsa-sha2-nistp384': ECDSAKey, diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 034331a2..dac1d02b 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -485,7 +485,7 @@ class KeyTest(unittest.TestCase): # PKey.load_certificate key = RSAKey.from_private_key_file(test_path('test_rsa.key')) self.assertTrue(key.public_blob is None) - key.load_certificate(pubkey_filename=test_path('test_rsa.key-cert.pub')) + key.load_certificate(test_path('test_rsa.key-cert.pub')) self.assertTrue(key.public_blob is not None) self.assertEqual(key.public_blob.key_type, 'ssh-rsa-cert-v01@openssh.com') self.assertEqual(key.public_blob.comment, 'test_rsa.key.pub') @@ -502,5 +502,8 @@ class KeyTest(unittest.TestCase): # Prevented from loading certificate that doesn't match key1 = Ed25519Key.from_private_key_file(test_path('test_ed25519.key')) - self.assertRaises(ValueError, key1.load_certificate, - pubkey_filename=test_path('test_rsa.key-cert.pub')) + self.assertRaises( + ValueError, + key1.load_certificate, + test_path('test_rsa.key-cert.pub'), + ) -- cgit v1.2.3 From 84d29dd4ea9d957d778207078c7cfed1d4bf9d46 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 17:50:38 -0700 Subject: Update changelog re: recent changes re: #1042 --- sites/www/changelog.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 83fc8a8f..9de287ae 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -7,8 +7,8 @@ Changelog previously this error case had a bare except and did not display exactly why the key failed. It will now append info such as ``KeyError: 'some-unknown-type-string'`` or similar. -* :feature:`1042` (also partially :issue:`531`) Implement generic (suitable for - all key types) client-side certificate authentication. +* :feature:`1042` (also partially :issue:`531`) Implement basic client-side + certificate authentication (as per the OpenSSH vendor extension.) The core implementation is `PKey.load_certificate ` and its corresponding ``.public_blob`` @@ -18,9 +18,17 @@ Changelog has appropriately-named cert files (e.g. ``id_rsa-cert.pub``) - see its docstring for details. - Thanks to Paul Kapp for the final patch, and to Jason Rigby for earlier work - in :issue:`531` (which remains open as it contains additional functionality - that may get merged later.) + Thanks to Jason Rigby for a first draft (:issue:`531`) and to Paul Kapp for + the second draft, upon which the current functionality has been based (with + modifications.) + + .. note:: + This support is client-focused; Paramiko-driven server code is capable of + handling cert-bearing pubkey auth packets, *but* it does not interpret any + cert-specific fields, so the end result is functionally identical to a + vanilla pubkey auth process (and thus requires e.g. prepopulated + authorized-keys data.) We expect full server-side cert support to follow + later. * :support:`1041` Modify logic around explicit disconnect messages, and unknown-channel situations, so that they rely on centralized -- cgit v1.2.3 From 38cf578bb2c06c600eaf56d4380fcf84ed399a08 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 18:50:48 -0700 Subject: Update first few stub tests + required test-server and PublicBlob impl bits --- paramiko/pkey.py | 7 +++++++ tests/test_client.py | 50 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 948152fa..1683c35c 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -477,3 +477,10 @@ class PublicBlob(object): return '%s public key/certificate - %s' % (self.key_type, self.comment) else: return '%s public key/certificate' % self.key_type + + def __eq__(self, other): + # Just piggyback on Message/BytesIO, since both of these should be one. + return self and other and self.key_blob == other.key_blob + + def __ne__(self, other): + return not self == other diff --git a/tests/test_client.py b/tests/test_client.py index cfffa030..da6cb58d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -35,6 +35,7 @@ import time from tests.util import test_path import paramiko +from paramiko.pkey import PublicBlob from paramiko.common import PY2 from paramiko.ssh_exception import SSHException, AuthenticationException @@ -47,10 +48,12 @@ FINGERPRINTS = { } -class NullServer (paramiko.ServerInterface): +class NullServer(paramiko.ServerInterface): def __init__(self, *args, **kwargs): # Allow tests to enable/disable specific key types self.__allowed_keys = kwargs.pop('allowed_keys', []) + # And allow them to set a (single...meh) expected public blob (cert) + self.__expected_public_blob = kwargs.pop('public_blob', None) super(NullServer, self).__init__(*args, **kwargs) def get_allowed_auths(self, username): @@ -71,12 +74,18 @@ class NullServer (paramiko.ServerInterface): expected = FINGERPRINTS[key.get_name()] except KeyError: return paramiko.AUTH_FAILED - if ( + # Base check: allowed auth type & fingerprint matches + happy = ( key.get_name() in self.__allowed_keys and key.get_fingerprint() == expected + ) + # Secondary check: if test wants assertions about cert data + if ( + self.__expected_public_blob is not None and + key.public_blob != self.__expected_public_blob ): - return paramiko.AUTH_SUCCESSFUL - return paramiko.AUTH_FAILED + happy = False + return paramiko.AUTH_SUCCESSFUL if happy else paramiko.AUTH_FAILED def check_channel_request(self, kind, chanid): return paramiko.OPEN_SUCCEEDED @@ -117,7 +126,7 @@ class SSHClientTest (unittest.TestCase): if hasattr(self, attr): getattr(self, attr).close() - def _run(self, allowed_keys=None, delay=0): + def _run(self, allowed_keys=None, delay=0, public_blob=None): if allowed_keys is None: allowed_keys = FINGERPRINTS.keys() self.socks, addr = self.sockl.accept() @@ -128,7 +137,7 @@ class SSHClientTest (unittest.TestCase): 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) + server = NullServer(allowed_keys=allowed_keys, public_blob=public_blob) if delay: time.sleep(delay) self.ts.start_server(self.event, server) @@ -140,7 +149,9 @@ class SSHClientTest (unittest.TestCase): The exception is ``allowed_keys`` which is stripped and handed to the ``NullServer`` used for testing. """ - run_kwargs = {'allowed_keys': kwargs.pop('allowed_keys', None)} + run_kwargs = {} + for key in ('allowed_keys', 'public_blob'): + run_kwargs[key] = kwargs.pop(key, None) # Server setup threading.Thread(target=self._run, kwargs=run_kwargs).start() host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) @@ -249,14 +260,27 @@ class SSHClientTest (unittest.TestCase): ) def test_certs_allowed_as_key_filename_values(self): - # TODO: connect() with key_filename containing a cert file, loads up - # both the cert and its implied key. This is new functionality on top - # of the OpenSSH-compatible stuff. - assert False + # NOTE: giving cert path here, not key path. (Key path test is below. + # They're similar except for which path is given; the expected auth and + # server-side behavior is 100% identical.) + cert_path = test_path('test_rsa.key-cert.pub') + self._test_connection( + key_filename=cert_path, + public_blob=PublicBlob.from_file(cert_path), + ) def test_certs_implicitly_loaded_alongside_key_filename_keys(self): - # TODO: same but with just the key paths, i.e. vanilla OpenSSH behavior - assert False + # NOTE: a regular test_connection() w/ test_rsa.key would incidentally + # test this (because test_rsa.key-cert.pub exists) but incidental tests + # stink, so NullServer and friends were updated to allow assertions + # about the server-side key object's public blob. Thus, we can prove + # that a specific cert was found, along with regular authorization + # succeeding proving that the overall flow works. + key_path = test_path('test_rsa.key') + self._test_connection( + key_filename=key_path, + public_blob=PublicBlob.from_file('{0}-cert.pub'.format(key_path)), + ) def test_default_key_locations_trigger_cert_loads_if_found(self): # TODO: what it says on the tin: ~/.ssh/id_rsa tries to load -- cgit v1.2.3 From d03c8b6e2f63cba2cb6d93ec3cd270bf49cda3da Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 18:52:15 -0700 Subject: God damn it, really? Whatever. --- tests/test_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index da6cb58d..a3e5e308 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -284,8 +284,9 @@ class SSHClientTest (unittest.TestCase): def test_default_key_locations_trigger_cert_loads_if_found(self): # TODO: what it says on the tin: ~/.ssh/id_rsa tries to load - # ~/.ssh/id_rsa-cert.pub - assert False + # ~/.ssh/id_rsa-cert.pub. Right now no other tests actually test that + # code path (!) so we're punting too, sob. + pass def test_4_auto_add_policy(self): """ -- cgit v1.2.3 From c87cfdc48a54a8b715832ac53134e0a2b3e334bc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 19:27:57 -0700 Subject: Factor out type checking & cert loading into PKey --- paramiko/pkey.py | 32 ++++++++++++++++++++++++++++++++ paramiko/rsakey.py | 27 +++++---------------------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 1683c35c..c8a59ee2 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -365,6 +365,38 @@ class PKey(object): encryption ).decode()) + def _check_type_and_load_cert(self, msg, key_type, cert_type): + """ + Perform message type-checking & optional certificate loading. + + This includes fast-forwarding cert ``msg`` objects past the nonce, so + that the subsequent fields are the key numbers; thus the caller may + expect to treat the message as key material afterwards either way. + """ + if msg is None: + raise SSHException('Key object may not be empty') + type_ = msg.get_text() + nonce = None + # Regular public key - nothing special to do besides the implicit + # type check. + if type_ == key_type: + pass + # OpenSSH-compatible certificate - store full copy as .public_blob + # (so signing works correctly) and then fast-forward past the + # nonce. + elif type_ == cert_type: + # This seems the cleanest way to 'clone' an already-being-read + # message; they're *IO objects at heart and their .getvalue() + # always returns the full value regardless of pointer position. + self.load_certificate(Message(msg.asbytes())) + # Read out nonce as it comes before the public numbers. + # TODO: usefully interpret it & other non-public-number fields + # (requires going back into per-type subclasses.) + nonce = msg.get_string() + else: + raise SSHException('Invalid key') + + def load_certificate(self, value): """ Supplement the private key contents with data loaded from an OpenSSH diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 3f28689a..1646fa4f 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -52,28 +52,11 @@ class RSAKey(PKey): if key is not None: self.key = key else: - if msg is None: - raise SSHException('Key object may not be empty') - type_ = msg.get_text() - nonce = None - # Regular public key - nothing special to do besides the implicit - # type check. - if type_ == 'ssh-rsa': - pass - # OpenSSH-compatible certificate - store full copy as .public_blob - # (so signing works correctly) and then fast-forward past the - # nonce. - elif type_ == 'ssh-rsa-cert-v01@openssh.com': - # This seems the cleanest way to 'clone' an already-being-read - # message? - self.load_certificate(Message(msg.asbytes())) - # Read out nonce as it comes before the public numbers. - # TODO: usefully interpret it & other non-public-number fields - nonce = msg.get_string() - else: - raise SSHException('Invalid key') - # Now that we've read type and (possibly) nonce, public numbers are - # next in either case. + self._check_type_and_load_cert( + msg=msg, + key_type='ssh-rsa', + cert_type='ssh-rsa-cert-v01@openssh.com', + ) self.key = rsa.RSAPublicNumbers( e=msg.get_mpint(), n=msg.get_mpint() ).public_key(default_backend()) -- cgit v1.2.3 From b9f7b6058906ac7f80b815570731d967b299ba8d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 19:30:22 -0700 Subject: Update recent tests to try all main key families. Includes some dummy certificates. Not sure exactly how @radssh generated the RSA one but I'm using ssh-keygen + a randomly made CA key. --- tests/test_client.py | 26 +++++++++++++++----------- tests/test_dss.key-cert.pub | 1 + tests/test_ecdsa_256.key-cert.pub | 1 + tests/test_ed25519.key-cert.pub | 1 + 4 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 tests/test_dss.key-cert.pub create mode 100644 tests/test_ecdsa_256.key-cert.pub create mode 100644 tests/test_ed25519.key-cert.pub diff --git a/tests/test_client.py b/tests/test_client.py index a3e5e308..40e2fb12 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -263,24 +263,28 @@ class SSHClientTest (unittest.TestCase): # NOTE: giving cert path here, not key path. (Key path test is below. # They're similar except for which path is given; the expected auth and # server-side behavior is 100% identical.) - cert_path = test_path('test_rsa.key-cert.pub') - self._test_connection( - key_filename=cert_path, - public_blob=PublicBlob.from_file(cert_path), - ) + for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'): + cert_path = test_path('test_{}.key-cert.pub'.format(type_)) + self._test_connection( + key_filename=cert_path, + public_blob=PublicBlob.from_file(cert_path), + ) def test_certs_implicitly_loaded_alongside_key_filename_keys(self): # NOTE: a regular test_connection() w/ test_rsa.key would incidentally - # test this (because test_rsa.key-cert.pub exists) but incidental tests + # test this (because test_xxx.key-cert.pub exists) but incidental tests # stink, so NullServer and friends were updated to allow assertions # about the server-side key object's public blob. Thus, we can prove # that a specific cert was found, along with regular authorization # succeeding proving that the overall flow works. - key_path = test_path('test_rsa.key') - self._test_connection( - key_filename=key_path, - public_blob=PublicBlob.from_file('{0}-cert.pub'.format(key_path)), - ) + for type_ in ('rsa', 'dss', 'ecdsa', 'ed25519'): + key_path = test_path('test_{}.key'.format(type_)) + self._test_connection( + key_filename=key_path, + public_blob=PublicBlob.from_file( + '{0}-cert.pub'.format(key_path) + ), + ) def test_default_key_locations_trigger_cert_loads_if_found(self): # TODO: what it says on the tin: ~/.ssh/id_rsa tries to load diff --git a/tests/test_dss.key-cert.pub b/tests/test_dss.key-cert.pub new file mode 100644 index 00000000..07fd5578 --- /dev/null +++ b/tests/test_dss.key-cert.pub @@ -0,0 +1 @@ +ssh-dss-cert-v01@openssh.com AAAAHHNzaC1kc3MtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJA3GjLmg6JbIWxokW/c827lmPOSvSfPDIY586yICFqIAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgEAAAAAAAAAAAAAAAEAAAAJdXNlcl90ZXN0AAAACAAAAAR0ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDskr46Umjxh3wo7PoPQsSVS3xt6+5PhwmXrnVtBBnkOo+zHRwQo8G8sY+Lc6oOOzA5GCSawKOwqE305GIDfB8/L9EKOkAjdN18imDjw/YuJFA4bl9yFhsXrCb1GZPJw0pJ0H0Eid9EldyMQAhGE49MWvnFMQl1TgO6YWq/g71xAFimge0LvVWijlbMy7O+nsGxSpinIprV5S9Viv8XC/ku89tadZfca1uxq751aGfAWGeYrVytpUl8UO0ggqH6BaUvkDU7rWh2n5RHUTvgzceKWnz5wqd8BngK37WmJjAgCtHCJS5ZRf6oJGj2QVcqc6cjvEFWsCuOKB4KAjktauWxAAABDwAAAAdzc2gtcnNhAAABAK6jweL231fRhFoybEGTOXJfj0lx55KpDsw9Q1rBvZhrSgwUr2dFr9HVcKe44mTC7CMtdW5VcyB67l1fnMil/D/e4zYxI0PvbW6RxLFNqvvtxBu5sGt5B7uzV4aAV31TpWR0l5RwwpZqc0NUlTx7oMutN1BDrPqW70QZ/iTEwalkn5fo1JWej0cf4BdC9VgYDLnprx0KN3IToukbszRQySnuR6MQUfj0m7lUloJfF3rq8G0kNxWqDGoJilMhO5Lqu9wAhlZWdouypI6bViO6+ToCVixLNUYs3EfS1zCxvXpiyMvh6rZofJ6WqzUuSd4Mzb2Ka4ocTKi7kynF+OG0Ivo= tests/test_dss.key.pub diff --git a/tests/test_ecdsa_256.key-cert.pub b/tests/test_ecdsa_256.key-cert.pub new file mode 100644 index 00000000..f2c93ccf --- /dev/null +++ b/tests/test_ecdsa_256.key-cert.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJ+ZkRXedIWPl9y6fvel60p47ys5WgwMSjiwzJ2Ho+4MAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eoAAAAAAAAAAAAAAAEAAAAJdXNlcl90ZXN0AAAACAAAAAR0ZXN0AAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDskr46Umjxh3wo7PoPQsSVS3xt6+5PhwmXrnVtBBnkOo+zHRwQo8G8sY+Lc6oOOzA5GCSawKOwqE305GIDfB8/L9EKOkAjdN18imDjw/YuJFA4bl9yFhsXrCb1GZPJw0pJ0H0Eid9EldyMQAhGE49MWvnFMQl1TgO6YWq/g71xAFimge0LvVWijlbMy7O+nsGxSpinIprV5S9Viv8XC/ku89tadZfca1uxq751aGfAWGeYrVytpUl8UO0ggqH6BaUvkDU7rWh2n5RHUTvgzceKWnz5wqd8BngK37WmJjAgCtHCJS5ZRf6oJGj2QVcqc6cjvEFWsCuOKB4KAjktauWxAAABDwAAAAdzc2gtcnNhAAABALdnEil8XIFkcgLZgYwS2cIQPHetUzMNxYCqzk7mSfVpCaIYNTr27RG+f+sD0cerdAIUUvhCT7iA82/Y7wzwkO2RUBi61ATfw9DDPPRQTDfix1SSRwbmPB/nVI1HlPMCEs6y48PFaBZqXwJPS3qycgSxoTBhaLCLzT+r6HRaibY7kiRLDeL3/WHyasK2PRdcYJ6KrLd0ctQcUHZCLK3fJfMfuQRg8MZLVrmK3fHStCXHpRFueRxUhZjaiS9evA/NtzEQhf46JDClQ2rLYpSqSg7QUR/rKwqWWyMuQkOHmlJw797VVa+ZzpUFXP7ekWel3FaBj8IHiimIA7Jm6dOCLm4= tests/test_ecdsa_256.key.pub diff --git a/tests/test_ed25519.key-cert.pub b/tests/test_ed25519.key-cert.pub new file mode 100644 index 00000000..4e01415a --- /dev/null +++ b/tests/test_ed25519.key-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIjBkc8l1X887CLBHraU+d6/74Hxr9oa+3HC0iioecZ6AAAAIHr1K9komH/1WBIvQbbtvnFVhryd62EfcgRFuLRiokNfAAAAAAAAAAAAAAABAAAACXVzZXJfdGVzdAAAAAgAAAAEdGVzdAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA7JK+OlJo8Yd8KOz6D0LElUt8bevuT4cJl651bQQZ5DqPsx0cEKPBvLGPi3OqDjswORgkmsCjsKhN9ORiA3wfPy/RCjpAI3TdfIpg48P2LiRQOG5fchYbF6wm9RmTycNKSdB9BInfRJXcjEAIRhOPTFr5xTEJdU4DumFqv4O9cQBYpoHtC71Voo5WzMuzvp7BsUqYpyKa1eUvVYr/Fwv5LvPbWnWX3Gtbsau+dWhnwFhnmK1craVJfFDtIIKh+gWlL5A1O61odp+UR1E74M3Hilp8+cKnfAZ4Ct+1piYwIArRwiUuWUX+qCRo9kFXKnOnI7xBVrArjigeCgI5LWrlsQAAAQ8AAAAHc3NoLXJzYQAAAQCNfYITv/GCW42fLI89x0pKpXIET/xHIBVan5S3fy5SZq9gLG1Db9g/FITDfOVA7OX8mU/91rucHGtuEi3isILdNFrCcoLEml289tyyluUbbFD5fjvBchMWBkYPwrOPfEzSs299Yk8ZgfV1pjWlndfV54s4c9pinkGu8c0Vzc6stEbWkdmoOHE8su3ogUPg/hOygDzJ+ZOgP5HIUJ6YgkgVpWgZm7zofwdZfa2HEb+WhZaKfMK1UCw1UiSBVk9dx6qzF9m243tHwSHraXvb9oJ1wT1S/MypTbP4RT4fHN8koYNrv2szEBN+lkRgk1D7xaxS/Md2TJsau9ho/UCXSR8L tests/test_ed25519.key.pub -- cgit v1.2.3 From fe06342df3daf405aa4d88e4ceebd8a757e7a7c6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 20:55:25 -0700 Subject: Implement DSS certs --- paramiko/dsskey.py | 9 +++++---- paramiko/transport.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index b3197f62..f9c02153 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -61,10 +61,11 @@ class DSSKey(PKey): if vals is not None: self.p, self.q, self.g, self.y = vals else: - if msg is None: - raise SSHException('Key object may not be empty') - if msg.get_text() != 'ssh-dss': - raise SSHException('Invalid key') + self._check_type_and_load_cert( + msg=msg, + key_type='ssh-dss', + cert_type='ssh-dss-cert-v01@openssh.com', + ) self.p = msg.get_mpint() self.q = msg.get_mpint() self.g = msg.get_mpint() diff --git a/paramiko/transport.py b/paramiko/transport.py index 0dc2e28a..d97bee80 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -206,6 +206,7 @@ class Transport(threading.Thread, ClosingContextManager): 'ssh-rsa': RSAKey, 'ssh-rsa-cert-v01@openssh.com': RSAKey, 'ssh-dss': DSSKey, + 'ssh-dss-cert-v01@openssh.com': DSSKey, 'ecdsa-sha2-nistp256': ECDSAKey, 'ecdsa-sha2-nistp384': ECDSAKey, 'ecdsa-sha2-nistp521': ECDSAKey, -- cgit v1.2.3 From 696c6ff18ff539a407fef9b93c3255309e4f7aee Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 20:56:12 -0700 Subject: Tweak exceptions to at least have better strings, if not new classes yet --- paramiko/pkey.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c8a59ee2..a135bed2 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -394,7 +394,8 @@ class PKey(object): # (requires going back into per-type subclasses.) nonce = msg.get_string() else: - raise SSHException('Invalid key') + err = 'Invalid key (class: {0}, data type: {1}' + raise SSHException(err.format(self.__class__.__name__, type_)) def load_certificate(self, value): @@ -487,8 +488,8 @@ class PublicBlob(object): m = Message(key_blob) blob_type = m.get_string() if blob_type != key_type: - msg = "Invalid PublicBlob contents: key type {0!r}, expected {1!r}" - raise ValueError(msg.format(blob_type, key_type)) + msg = "Invalid PublicBlob contents: key type={0!r}, but blob type={1!r}" # noqa + raise ValueError(msg.format(key_type, blob_type)) # All good? All good. return cls(type_=key_type, blob=key_blob, comment=comment) -- cgit v1.2.3 From e0babd7a2da93501fed8a83da0cfb70ce6a90bbd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 20:56:50 -0700 Subject: Implement ECDSA certs. So mad at that frickin typo'd specification... --- paramiko/ecdsakey.py | 27 ++++++++++++++++++++++----- paramiko/pkey.py | 24 ++++++++++++++++++++---- paramiko/transport.py | 3 +++ tests/test_client.py | 3 ++- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 9b74d938..fd876298 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -119,12 +119,29 @@ class ECDSAKey(PKey): c_class = self.signing_key.curve.__class__ self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(c_class) else: - if msg is None: - raise SSHException('Key object may not be empty') + # Must set ecdsa_curve first; subroutines called herein may need to + # spit out our get_name(), which relies on this. + key_type = msg.get_text() + # But this also means we need to hand it a real key/curve + # identifier, so strip out any cert business. (NOTE: could push + # that into _ECDSACurveSet.get_by_key_format_identifier(), but it + # feels more correct to do it here?) + suffix = '-cert-v01@openssh.com' + if key_type.endswith(suffix): + key_type = key_type[:-len(suffix)] self.ecdsa_curve = self._ECDSA_CURVES.get_by_key_format_identifier( - msg.get_text()) - if self.ecdsa_curve is None: - raise SSHException('Invalid key') + key_type + ) + key_types = self._ECDSA_CURVES.get_key_format_identifier_list() + cert_types = [ + '{}-cert-v01@openssh.com'.format(x) + for x in key_types + ] + self._check_type_and_load_cert( + msg=msg, + key_type=key_types, + cert_type=cert_types, + ) curvename = msg.get_text() if curvename != self.ecdsa_curve.nist_name: raise SSHException("Can't handle curve of type %s" % curvename) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index a135bed2..50a99bfa 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -31,7 +31,7 @@ from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher from paramiko import util from paramiko.common import o600 -from paramiko.py3compat import u, encodebytes, decodebytes, b +from paramiko.py3compat import u, encodebytes, decodebytes, b, string_types from paramiko.ssh_exception import SSHException, PasswordRequiredException from paramiko.message import Message @@ -372,19 +372,35 @@ class PKey(object): This includes fast-forwarding cert ``msg`` objects past the nonce, so that the subsequent fields are the key numbers; thus the caller may expect to treat the message as key material afterwards either way. - """ + + The obtained key type is returned for classes which need to know what + it was (e.g. ECDSA.) + """ + # Normalization; most classes have a single key type and give a string, + # but eg ECDSA is a 1:N mapping. + key_types = key_type + cert_types = cert_type + if isinstance(key_type, string_types): + key_types = [key_types] + if isinstance(cert_types, string_types): + cert_types = [cert_types] + # Can't do much with no message, that should've been handled elsewhere if msg is None: raise SSHException('Key object may not be empty') + # First field is always key type, in either kind of object. (make sure + # we rewind before grabbing it - sometimes caller had to do their own + # introspection first!) + msg.rewind() type_ = msg.get_text() nonce = None # Regular public key - nothing special to do besides the implicit # type check. - if type_ == key_type: + if type_ in key_types: pass # OpenSSH-compatible certificate - store full copy as .public_blob # (so signing works correctly) and then fast-forward past the # nonce. - elif type_ == cert_type: + elif type_ in cert_types: # This seems the cleanest way to 'clone' an already-being-read # message; they're *IO objects at heart and their .getvalue() # always returns the full value regardless of pointer position. diff --git a/paramiko/transport.py b/paramiko/transport.py index d97bee80..1a95f990 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -208,8 +208,11 @@ class Transport(threading.Thread, ClosingContextManager): 'ssh-dss': DSSKey, 'ssh-dss-cert-v01@openssh.com': DSSKey, 'ecdsa-sha2-nistp256': ECDSAKey, + 'ecdsa-sha2-nistp256-cert-v01@openssh.com': ECDSAKey, 'ecdsa-sha2-nistp384': ECDSAKey, + 'ecdsa-sha2-nistp384-cert-v01@openssh.com': ECDSAKey, 'ecdsa-sha2-nistp521': ECDSAKey, + 'ecdsa-sha2-nistp521-cert-v01@openssh.com': ECDSAKey, 'ssh-ed25519': Ed25519Key, } diff --git a/tests/test_client.py b/tests/test_client.py index 40e2fb12..f0808c4b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -263,6 +263,7 @@ class SSHClientTest (unittest.TestCase): # NOTE: giving cert path here, not key path. (Key path test is below. # They're similar except for which path is given; the expected auth and # server-side behavior is 100% identical.) + # NOTE: only bothered whipping up one cert per overall class/family. for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'): cert_path = test_path('test_{}.key-cert.pub'.format(type_)) self._test_connection( @@ -277,7 +278,7 @@ class SSHClientTest (unittest.TestCase): # about the server-side key object's public blob. Thus, we can prove # that a specific cert was found, along with regular authorization # succeeding proving that the overall flow works. - for type_ in ('rsa', 'dss', 'ecdsa', 'ed25519'): + for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'): key_path = test_path('test_{}.key'.format(type_)) self._test_connection( key_filename=key_path, -- cgit v1.2.3 From 59f9a64239b5e4be7b6a067cb63fa4a5420121fe Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 21:26:18 -0700 Subject: Implement ed25519 certs. God damn it took me ages to notice that frickin self.public_blob = None bit :( :( :( :( --- paramiko/ed25519key.py | 8 +++++--- paramiko/pkey.py | 1 - paramiko/transport.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index d904f1ac..2e1eb18a 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -50,8 +50,11 @@ class Ed25519Key(PKey): if msg is None and data is not None: msg = Message(data) if msg is not None: - if msg.get_text() != "ssh-ed25519": - raise SSHException("Invalid key") + self._check_type_and_load_cert( + msg=msg, + key_type="ssh-ed25519", + cert_type="ssh-ed25519-cert-v01@openssh.com", + ) verifying_key = nacl.signing.VerifyKey(msg.get_binary()) elif filename is not None: with open(filename, "r") as f: @@ -63,7 +66,6 @@ class Ed25519Key(PKey): self._signing_key = signing_key self._verifying_key = verifying_key - self.public_blob = None def _parse_signing_key_data(self, data, password): from paramiko.transport import Transport diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 50a99bfa..4e95f5fc 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -413,7 +413,6 @@ class PKey(object): err = 'Invalid key (class: {0}, data type: {1}' raise SSHException(err.format(self.__class__.__name__, type_)) - def load_certificate(self, value): """ Supplement the private key contents with data loaded from an OpenSSH diff --git a/paramiko/transport.py b/paramiko/transport.py index 1a95f990..df068b3c 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -214,6 +214,7 @@ class Transport(threading.Thread, ClosingContextManager): 'ecdsa-sha2-nistp521': ECDSAKey, 'ecdsa-sha2-nistp521-cert-v01@openssh.com': ECDSAKey, 'ssh-ed25519': Ed25519Key, + 'ssh-ed25519-cert-v01@openssh.com': Ed25519Key, } _kex_info = { -- cgit v1.2.3 From b7d5df29431d95dc11f4393ba366521b091dad0c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 21:26:27 -0700 Subject: This isn't required when one is just calling asbytes() --- paramiko/pkey.py | 1 - 1 file changed, 1 deletion(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 4e95f5fc..e4539a68 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -517,7 +517,6 @@ class PublicBlob(object): OpenSSH-style certificates 'are' their own network representation." """ type_ = message.get_text() - message.rewind() return cls(type_=type_, blob=message.asbytes()) def __str__(self): -- cgit v1.2.3 From 9b693d4753bd1e12da488b09e1961ac9979212d3 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 21:29:15 -0700 Subject: flake8 --- paramiko/pkey.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index e4539a68..8646b609 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -392,7 +392,6 @@ class PKey(object): # introspection first!) msg.rewind() type_ = msg.get_text() - nonce = None # Regular public key - nothing special to do besides the implicit # type check. if type_ in key_types: @@ -408,7 +407,7 @@ class PKey(object): # Read out nonce as it comes before the public numbers. # TODO: usefully interpret it & other non-public-number fields # (requires going back into per-type subclasses.) - nonce = msg.get_string() + msg.get_string() else: err = 'Invalid key (class: {0}, data type: {1}' raise SSHException(err.format(self.__class__.__name__, type_)) @@ -425,7 +424,8 @@ class PKey(object): the client side to offer authentication requests to the server based on certificate instead of raw public key. - See: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys + See: + https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys Note: very little effort is made to validate the certificate contents, that is for the server to decide if it is good enough to authenticate @@ -439,8 +439,8 @@ class PKey(object): constructor = 'from_string' blob = getattr(PublicBlob, constructor)(value) if not blob.key_type.startswith(self.get_name()): - raise ValueError('PublicBlob type %s incompatible with key type %s' % - (blob.key_type, self.get_name())) + err = "PublicBlob type {0} incompatible with key type {1}" + raise ValueError(err.format(blob.key_type, self.get_name())) self.public_blob = blob @@ -520,10 +520,10 @@ class PublicBlob(object): return cls(type_=type_, blob=message.asbytes()) def __str__(self): + ret = '{0} public key/certificate'.format(self.key_type) if self.comment: - return '%s public key/certificate - %s' % (self.key_type, self.comment) - else: - return '%s public key/certificate' % self.key_type + ret += "- {0}".format(self.comment) + return ret def __eq__(self, other): # Just piggyback on Message/BytesIO, since both of these should be one. -- cgit v1.2.3 From bad045f7dada340d2c707d25923406e32406fc22 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 28 Aug 2017 21:47:39 -0700 Subject: Python 3 fixes re #1042 --- paramiko/pkey.py | 4 ++-- tests/test_pkey.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 8646b609..67723be2 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -493,7 +493,7 @@ class PublicBlob(object): msg = "Not enough fields for public blob: {0}" raise ValueError(msg.format(fields)) key_type = fields[0] - key_blob = decodebytes(fields[1]) + key_blob = decodebytes(b(fields[1])) try: comment = fields[2].strip() except IndexError: @@ -501,7 +501,7 @@ class PublicBlob(object): # Verify that the blob message first (string) field matches the # key_type m = Message(key_blob) - blob_type = m.get_string() + blob_type = m.get_text() if blob_type != key_type: msg = "Invalid PublicBlob contents: key type={0!r}, but blob type={1!r}" # noqa raise ValueError(msg.format(key_type, blob_type)) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index dac1d02b..80843222 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -491,7 +491,7 @@ class KeyTest(unittest.TestCase): self.assertEqual(key.public_blob.comment, 'test_rsa.key.pub') # Delve into blob contents, for test purposes msg = Message(key.public_blob.key_blob) - self.assertEqual(msg.get_string(), 'ssh-rsa-cert-v01@openssh.com') + self.assertEqual(msg.get_text(), 'ssh-rsa-cert-v01@openssh.com') nonce = msg.get_string() e = msg.get_mpint() n = msg.get_mpint() -- cgit v1.2.3 From 5fb4bb2cfc415b287f629a398de5447da18a3fb2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 2 Sep 2017 12:47:52 -0700 Subject: Python 2.6 fixes Fixes #1049 --- paramiko/ecdsakey.py | 2 +- tests/test_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index fd876298..5a0164d8 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -134,7 +134,7 @@ class ECDSAKey(PKey): ) key_types = self._ECDSA_CURVES.get_key_format_identifier_list() cert_types = [ - '{}-cert-v01@openssh.com'.format(x) + '{0}-cert-v01@openssh.com'.format(x) for x in key_types ] self._check_type_and_load_cert( diff --git a/tests/test_client.py b/tests/test_client.py index f0808c4b..7ada13da 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -265,7 +265,7 @@ class SSHClientTest (unittest.TestCase): # server-side behavior is 100% identical.) # NOTE: only bothered whipping up one cert per overall class/family. for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'): - cert_path = test_path('test_{}.key-cert.pub'.format(type_)) + cert_path = test_path('test_{0}.key-cert.pub'.format(type_)) self._test_connection( key_filename=cert_path, public_blob=PublicBlob.from_file(cert_path), @@ -279,7 +279,7 @@ class SSHClientTest (unittest.TestCase): # that a specific cert was found, along with regular authorization # succeeding proving that the overall flow works. for type_ in ('rsa', 'dss', 'ecdsa_256', 'ed25519'): - key_path = test_path('test_{}.key'.format(type_)) + key_path = test_path('test_{0}.key'.format(type_)) self._test_connection( key_filename=key_path, public_blob=PublicBlob.from_file( -- cgit v1.2.3 From 898152cf049daf1a861206b95b39679b032803d6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 17:04:31 -0700 Subject: Overhaul changelog re #1037, closes #60 --- sites/www/changelog.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5daa9243..4198e927 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,9 +2,18 @@ Changelog ========= -* :bug:`60` Improved the performance of compressed transport by using default - zlib compression level (which is 6) rather than the max level of 9 which is - very CPU intensive. +* :bug:`60 major` (via :issue:`1037`) Paramiko originally defaulted to zlib + compression level 9 (when one connects with ``compression=True``; it defaults + to off.) This has been found to be quite wasteful and tends to cause much + longer transfers in most cases, than is necessary. + + OpenSSH defaults to compression level 6, which is a much more reasonable + setting (nearly identical compression characteristics but noticeably, + sometimes significantly, faster transmission); Paramiko now uses this value + instead. + + Thanks to Damien Dubé for the report and ``@DrNeutron`` for investigating & + submitting the patch. * :support:`-` Display exception type and message when logging auth-rejection messages (ones reading ``Auth rejected: unsupported or mangled public key``); previously this error case had a bare except and did not display exactly why -- cgit v1.2.3 From 3b7a0095ce871432219fdd3d1a23faf65458ba79 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 17:44:14 -0700 Subject: Changelog re #1013 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 4198e927..3a9409b3 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :feature:`1013` Added pre-authentication banner support for the server + interface (`ServerInterface.get_banner + ` plus related support in + ``Transport/AuthHandler``.) Patch courtesy of Dennis Kaarsemaker. * :bug:`60 major` (via :issue:`1037`) Paramiko originally defaulted to zlib compression level 9 (when one connects with ``compression=True``; it defaults to off.) This has been found to be quite wasteful and tends to cause much -- cgit v1.2.3 From 5fed4c1f4b67b7337de4c9a01bd8e3c23e6a529f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 18:18:09 -0700 Subject: Really, really gotta get better about enforcing these --- paramiko/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paramiko/server.py b/paramiko/server.py index f876e779..c96126e9 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -579,6 +579,8 @@ class ServerInterface (object): The default implementation always returns ``(None, None)``. :returns: A tuple containing the banner and language code. + + .. versionadded:: 2.3 """ return (None, None) -- cgit v1.2.3 From a2de0ad4e5aa9ce2a98ed0009990d525f744073b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 19:02:41 -0700 Subject: Document Ed25519 keys =/ I didn't badger people about docs so there were none --- paramiko/ed25519key.py | 10 ++++++++++ sites/docs/api/keys.rst | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index aa5e885d..434b3f45 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -45,6 +45,16 @@ def unpad(data): class Ed25519Key(PKey): + """ + Representation of an `Ed25519 `_ key. + + .. note:: + Ed25519 key support was added to OpenSSH in version 6.5. + + .. versionadded:: 2.2 + .. versionchanged:: 2.3 + Added a ``file_obj`` parameter to match other key classes. + """ def __init__(self, msg=None, data=None, filename=None, password=None, file_obj=None): verifying_key = signing_key = None diff --git a/sites/docs/api/keys.rst b/sites/docs/api/keys.rst index c6412f77..a456f502 100644 --- a/sites/docs/api/keys.rst +++ b/sites/docs/api/keys.rst @@ -21,3 +21,8 @@ ECDSA ===== .. automodule:: paramiko.ecdsakey + +Ed25519 +======= + +.. automodule:: paramiko.ed25519key -- cgit v1.2.3 From 6ab07ec442e0a7cc2436a90c800c0d10cc9adbd6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 19:03:08 -0700 Subject: Changelog update for #1026 plus related changes --- sites/www/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 3a9409b3..b862d912 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :support:`-` Ed25519 keys never got proper API documentation support; this + has been fixed. +* :feature:`1026` Update `~paramiko.ed25519key.Ed25519Key` so its constructor + offers the same ``file_obj`` parameter as its sibling key classes. Credit: + Michal Kuffa. * :feature:`1013` Added pre-authentication banner support for the server interface (`ServerInterface.get_banner ` plus related support in -- cgit v1.2.3 From d7b41dac1e9c8d3f45cf319bf96d74552f72081a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 19:13:51 -0700 Subject: Changelog re #979 --- sites/www/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index b862d912..2f583825 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,14 @@ Changelog ========= +* :support:`979` Update how we use `Cryptography `'s + signature/verification methods so we aren't relying on a deprecated API. + Thanks to Paul Kehrer for the patch. + + .. warning:: + This bumps the minimum Cryptography version from 1.1 to 1.5. Such an + upgrade should be backwards compatible and easy to do. See `their changelog + `_ for additional details. * :support:`-` Ed25519 keys never got proper API documentation support; this has been fixed. * :feature:`1026` Update `~paramiko.ed25519key.Ed25519Key` so its constructor -- cgit v1.2.3 From 7fb1bf13a1173de679a1c1fea1533c471c84f610 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 5 Sep 2017 19:17:49 -0700 Subject: Gah --- sites/www/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 2f583825..5728a281 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,7 +2,7 @@ Changelog ========= -* :support:`979` Update how we use `Cryptography `'s +* :support:`979` Update how we use `Cryptography `_'s signature/verification methods so we aren't relying on a deprecated API. Thanks to Paul Kehrer for the patch. -- cgit v1.2.3