diff options
-rw-r--r-- | paramiko/__init__.py | 4 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 18 | ||||
-rw-r--r-- | paramiko/dsskey.py | 1 | ||||
-rw-r--r-- | paramiko/ecdsakey.py | 1 | ||||
-rw-r--r-- | paramiko/ed25519key.py | 1 | ||||
-rw-r--r-- | paramiko/pkey.py | 69 | ||||
-rw-r--r-- | paramiko/rsakey.py | 1 | ||||
-rw-r--r-- | tests/test_pkey.py | 24 | ||||
-rw-r--r-- | tests/test_rsa.key-cert.pub | 1 | ||||
-rw-r--r-- | tests/test_rsa.key.pub | 1 |
10 files changed, 115 insertions, 6 deletions
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: +# <key-name> <base64-blob> [<comment>] +# 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= |