diff options
-rw-r--r-- | AUTHORS.rst | 1 | ||||
-rw-r--r-- | CHANGELOG.rst | 4 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 55 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/serialization.py | 47 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_serialization.py | 103 |
5 files changed, 207 insertions, 3 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst index 250b717b..c233bc86 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,3 +18,4 @@ PGP key fingerprints are enclosed in parentheses. * Matthew Iversen <matt@notevencode.com> (2F04 3DCC D6E6 D5AC D262 2E0B C046 E8A8 7452 2973) * Mohammed Attia <skeuomorf@gmail.com> (854A F9C5 9FF5 6E38 B17D 9587 2D70 E1ED 5290 D357) * Michael Hart <michael.hart1994@gmail.com> +* Mark Adams <mark@markadams.me> (A18A 7DD3 283C CF2A B0CE FE0E C7A0 5E3F C972 098C) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b8a799a2..e0c71a7b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,10 @@ Changelog :class:`~cryptography.hazmat.primitives.interfaces.CMACContext`. * Added support for encoding and decoding :rfc:`6979` signatures in :doc:`/hazmat/primitives/asymmetric/utils`. +* Added + :func:`~cryptography.hazmat.primitives.serialization.load_ssh_public_key` and + :func:`~cryptography.hazmat.primitives.serialization.load_ssh_rsa_public_key` + to support the loading of OpenSSH RSA public keys (RFC 4253). 0.6.1 - 2014-10-15 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index b0b37b80..52960ec0 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -195,3 +195,58 @@ KEY-----`` or ``-----BEGIN DSA PRIVATE KEY-----``. :raises UnsupportedAlgorithm: If the serialized key is of a type that is not supported by the backend or if the key is encrypted with a symmetric cipher that is not supported by the backend. + +OpenSSH Public Key +~~~~~~~~~~~~~~~~~~ + +The format used by OpenSSH to store public keys as specified in :rfc:`4253` + +Currently, only RSA public keys are supported. Any other type of key will +result in an exception being thrown. + +Example RSA key in OpenSSH format (line breaks added for formatting purposes):: + + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk + FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll + PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK + vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f + sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy + ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX + 2MzHvnbv testkey@localhost + +.. function:: load_ssh_public_key(data, backend) + + .. versionadded:: 0.7 + + Deserialize a public key from OpenSSH (:rfc:`4253`) encoded data to an + instance of the public key type for the specified backend. + + :param bytes data: The OpenSSH encoded key data. + + :param backend: A backend provider. + + :returns: A new instance of a public key type. + + :raises ValueError: If the OpenSSH data could not be properly decoded or + if the key is not in the proper format. + + :raises UnsupportedAlgorithm: If the serialized key is of a type that is + not supported. + +.. function:: load_ssh_rsa_public_key(data, backend) + + .. versionadded:: 0.7 + + Deserialize a RSA public key from OpenSSH (:rfc:`4253`) encoded data to an + instance of the RSA Public Key type for the specified backend. + + :param bytes data: The OpenSSH encoded key data. + + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.RSABackend` + provider. + + :returns: A new instance of a public key type. + + :raises ValueError: If the OpenSSH data could not be properly decoded or + if the key is not in the proper format. diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index b9cf5967..0f07e41f 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -4,9 +4,13 @@ from __future__ import absolute_import, division, print_function +import base64 +import struct import warnings from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers def load_pem_traditional_openssl_private_key(data, password, backend): @@ -39,3 +43,46 @@ def load_pem_private_key(data, password, backend): def load_pem_public_key(data, backend): return backend.load_pem_public_key(data) + + +def load_ssh_public_key(data, backend): + if not data.startswith(b'ssh-'): + raise ValueError('SSH-formatted keys must begin with ssh-') + + if not data.startswith(b'ssh-rsa'): + raise UnsupportedAlgorithm('Only RSA keys are currently supported.') + + return load_ssh_rsa_public_key(data, backend) + + +def load_ssh_rsa_public_key(data, backend): + if not data.startswith(b'ssh-rsa '): + raise ValueError('SSH-formatted RSA keys must begin with ssh-rsa') + + parts = data.split(b' ') + data = base64.b64decode(parts[1]) + + cert_data = [] + + while len(data) > 0: + str_len = struct.unpack('>I', data[0:4])[0] + cert_data.append(data[4:4 + str_len]) + data = data[4 + str_len:] + + e = _bytes_to_int(cert_data[1]) + n = _bytes_to_int(cert_data[2]) + return backend.load_rsa_public_numbers(RSAPublicNumbers(e, n)) + + +def _bytes_to_int(data): + if len(data) % 4 != 0: + # Pad the bytes with 0x00 to a block size of 4 + data = (b'\x00' * (4 - (len(data) % 4))) + data + + result = 0 + + while len(data) > 0: + result = (result << 32) + struct.unpack('>I', data[0:4])[0] + data = data[4:] + + return result diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 726e73dd..63ec6c4c 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -9,16 +9,18 @@ import textwrap import pytest -from cryptography.exceptions import _Reasons +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat.backends.interfaces import ( EllipticCurveBackend, PEMSerializationBackend, PKCS8SerializationBackend, - TraditionalOpenSSLSerializationBackend + RSABackend, TraditionalOpenSSLSerializationBackend ) from cryptography.hazmat.primitives import interfaces from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers from cryptography.hazmat.primitives.serialization import ( load_pem_pkcs8_private_key, load_pem_private_key, load_pem_public_key, - load_pem_traditional_openssl_private_key + load_pem_traditional_openssl_private_key, load_ssh_public_key, + load_ssh_rsa_public_key ) @@ -680,3 +682,98 @@ class TestPKCS8Serialization(object): pemfile.read().encode(), password, backend ) ) + + +@pytest.mark.requires_backend_interface(interface=RSABackend) +class TestSSHSerialization(object): + def test_load_ssh_public_key_unsupported(self, backend): + str_key = b'ssh-dss AAAAB3NzaC1kc3MAAACBAO7q0a7VsQZcdRTCqFentQt...' + + with pytest.raises(UnsupportedAlgorithm): + load_ssh_public_key(str_key, backend) + + def test_load_ssh_public_key_bad_format(self, backend): + str_key = b'not-a-real-key' + + with pytest.raises(ValueError): + load_ssh_public_key(str_key, backend) + + def test_load_ssh_public_key(self, backend): + str_key = ( + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk' + 'FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll' + 'PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK' + 'vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f' + 'sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy' + '///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX' + '2MzHvnbv testkey@localhost').encode() + + key = load_ssh_public_key(str_key, backend) + + assert key is not None + assert isinstance(key, interfaces.RSAPublicKey) + + if not isinstance(key, interfaces.RSAPublicKeyWithNumbers): + return + + numbers = key.public_numbers() + + expected_e = 0x10001 + expected_n = int( + '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D' + '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691' + 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF' + '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142' + '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF' + 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3' + '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B' + 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0' + '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31' + '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16) + + expected = RSAPublicNumbers(expected_e, expected_n) + + assert numbers == expected + + def test_load_ssh_rsa_public_key_bad_format(self, backend): + str_key = b'ssh-rsa-not-a-key' + + with pytest.raises(ValueError): + load_ssh_rsa_public_key(str_key, backend) + + def test_load_ssh_rsa_public_key(self, backend): + str_key = ( + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk' + 'FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll' + 'PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK' + 'vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f' + 'sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy' + '///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX' + '2MzHvnbv testkey@localhost').encode() + + key = load_ssh_public_key(str_key, backend) + + assert key is not None + assert isinstance(key, interfaces.RSAPublicKey) + + if not isinstance(key, interfaces.RSAPublicKeyWithNumbers): + return + + numbers = key.public_numbers() + + expected_e = 0x10001 + expected_n = int( + '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D' + '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691' + 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF' + '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142' + '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF' + 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3' + '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B' + 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0' + '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31' + '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16) + + expected = RSAPublicNumbers(expected_e, expected_n) + + assert numbers == expected |