From 78a7d1c4c63737c4eae0c22207a00141a44402d3 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Fri, 12 Dec 2014 23:13:12 -0600 Subject: Added load_ssh_rsa_public_key to hazmat.primitives.serialization to allow for loading of OpenSSH RSA public keys Also added load_ssh_public_key as a generic method that can be later extended to support more public key algorithms. --- AUTHORS.rst | 1 + CHANGELOG.rst | 4 + .../hazmat/primitives/asymmetric/serialization.rst | 55 +++++++++++ .../hazmat/primitives/serialization.py | 47 ++++++++++ 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 (2F04 3DCC D6E6 D5AC D262 2E0B C046 E8A8 7452 2973) * Mohammed Attia (854A F9C5 9FF5 6E38 B17D 9587 2D70 E1ED 5290 D357) * Michael Hart +* Mark Adams (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 -- cgit v1.2.3 From b7b91179a5b1d4c28643f9e59bb46e52144a7de3 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sat, 13 Dec 2014 10:43:01 -0600 Subject: Privatized the load_ssh_rsa_public_key function and fixed some coverage issues on test_serialization. --- CHANGELOG.rst | 6 +- .../hazmat/primitives/asymmetric/serialization.rst | 18 ----- .../hazmat/primitives/serialization.py | 4 +- tests/hazmat/primitives/test_serialization.py | 82 ++++++++++------------ 4 files changed, 44 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e0c71a7b..5b1f48e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,9 +19,9 @@ Changelog * 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). + :func:`~cryptography.hazmat.primitives.serialization.load_ssh_public_key` + to support the loading of OpenSSH public keys (RFC 4253). Currently, only RSA + is supported. 0.6.1 - 2014-10-15 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 52960ec0..ec35c3cf 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -232,21 +232,3 @@ Example RSA key in OpenSSH format (line breaks added for formatting purposes):: :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 0f07e41f..e1ffab9e 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -52,10 +52,10 @@ def load_ssh_public_key(data, backend): if not data.startswith(b'ssh-rsa'): raise UnsupportedAlgorithm('Only RSA keys are currently supported.') - return load_ssh_rsa_public_key(data, backend) + return _load_ssh_rsa_public_key(data, backend) -def 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') diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 63ec6c4c..8dbe8344 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -18,9 +18,9 @@ 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_ssh_public_key, - load_ssh_rsa_public_key + _load_ssh_rsa_public_key, load_pem_pkcs8_private_key, load_pem_private_key, + load_pem_public_key, load_pem_traditional_openssl_private_key, + load_ssh_public_key ) @@ -713,33 +713,31 @@ class TestSSHSerialization(object): assert key is not None assert isinstance(key, interfaces.RSAPublicKey) - if not isinstance(key, interfaces.RSAPublicKeyWithNumbers): - return - - numbers = key.public_numbers() + if isinstance(key, interfaces.RSAPublicKeyWithNumbers): + numbers = key.public_numbers() - expected_e = 0x10001 - expected_n = int( - '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D' - '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691' - 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF' - '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142' - '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF' - 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3' - '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B' - 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0' - '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31' - '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16) + expected_e = 0x10001 + expected_n = int( + '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D' + '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691' + 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF' + '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142' + '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF' + 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3' + '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B' + 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0' + '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31' + '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16) - expected = RSAPublicNumbers(expected_e, expected_n) + expected = RSAPublicNumbers(expected_e, expected_n) - assert numbers == expected + 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) + _load_ssh_rsa_public_key(str_key, backend) def test_load_ssh_rsa_public_key(self, backend): str_key = ( @@ -751,29 +749,27 @@ class TestSSHSerialization(object): '///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX' '2MzHvnbv testkey@localhost').encode() - key = load_ssh_public_key(str_key, backend) + key = _load_ssh_rsa_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) + if isinstance(key, interfaces.RSAPublicKeyWithNumbers): + numbers = key.public_numbers() - assert numbers == expected + 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 -- cgit v1.2.3 From 3aeac8c132f53f07393f85e260e135b64f204482 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sat, 13 Dec 2014 22:17:03 -0600 Subject: Standardized the assignment statement for str_key in TestSSHSerialization. --- tests/hazmat/primitives/test_serialization.py | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 8dbe8344..f6710505 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -699,14 +699,14 @@ class TestSSHSerialization(object): 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() + str_key = textwrap.dedent("""\ + 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) @@ -740,14 +740,14 @@ class TestSSHSerialization(object): _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() + str_key = textwrap.dedent("""\ + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk + FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll + PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK + vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f + sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy + ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX + 2MzHvnbv testkey@localhost""").encode() key = _load_ssh_rsa_public_key(str_key, backend) -- cgit v1.2.3 From 1832e24256c5984cfe534a24839657609522b7a4 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sat, 13 Dec 2014 22:54:34 -0600 Subject: Minor documentation corrections for load_ssh_public_key --- CHANGELOG.rst | 2 +- docs/hazmat/primitives/asymmetric/serialization.rst | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5b1f48e6..0394ebd9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,7 +20,7 @@ Changelog :doc:`/hazmat/primitives/asymmetric/utils`. * Added :func:`~cryptography.hazmat.primitives.serialization.load_ssh_public_key` - to support the loading of OpenSSH public keys (RFC 4253). Currently, only RSA + to support the loading of OpenSSH public keys (:rfc:`4253`). Currently, only RSA is supported. 0.6.1 - 2014-10-15 diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index ec35c3cf..45c7a5bc 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -199,12 +199,13 @@ KEY-----`` or ``-----BEGIN DSA PRIVATE KEY-----``. OpenSSH Public Key ~~~~~~~~~~~~~~~~~~ -The format used by OpenSSH to store public keys as specified in :rfc:`4253` +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):: +An example RSA key in OpenSSH format (line breaks added for formatting +purposes):: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll -- cgit v1.2.3 From dfa57bf7821a63c65ef0f83234c79f611fab46db Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sat, 13 Dec 2014 23:13:52 -0600 Subject: Removed redundant tests for _load_ssh_rsa_public_key since it is now a non-public part of the API and made a number of minor changes to tests and documentation --- .../hazmat/primitives/serialization.py | 3 +- tests/hazmat/primitives/test_serialization.py | 91 ++++++---------------- 2 files changed, 25 insertions(+), 69 deletions(-) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index e1ffab9e..38c541cb 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -56,8 +56,7 @@ def load_ssh_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') + assert data.startswith(b'ssh-rsa ') parts = data.split(b' ') data = base64.b64decode(parts[1]) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index f6710505..ffe3d7df 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -18,9 +18,8 @@ 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_ssh_rsa_public_key, load_pem_pkcs8_private_key, load_pem_private_key, - load_pem_public_key, load_pem_traditional_openssl_private_key, - load_ssh_public_key + load_pem_pkcs8_private_key, load_pem_private_key, load_pem_public_key, + load_pem_traditional_openssl_private_key, load_ssh_public_key ) @@ -687,19 +686,19 @@ class TestPKCS8Serialization(object): @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...' + ssh_key = b'ssh-dss AAAAB3NzaC1kc3MAAACBAO7q0a7VsQZcdRTCqFentQt...' with pytest.raises(UnsupportedAlgorithm): - load_ssh_public_key(str_key, backend) + load_ssh_public_key(ssh_key, backend) def test_load_ssh_public_key_bad_format(self, backend): - str_key = b'not-a-real-key' + ssh_key = b'not-a-real-key' with pytest.raises(ValueError): - load_ssh_public_key(str_key, backend) + load_ssh_public_key(ssh_key, backend) - def test_load_ssh_public_key(self, backend): - str_key = textwrap.dedent("""\ + def test_load_ssh_public_key_rsa(self, backend): + ssh_key = textwrap.dedent("""\ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK @@ -708,68 +707,26 @@ class TestSSHSerialization(object): ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX 2MzHvnbv testkey@localhost""").encode() - key = load_ssh_public_key(str_key, backend) + key = load_ssh_public_key(ssh_key, backend) assert key is not None assert isinstance(key, interfaces.RSAPublicKey) - if isinstance(key, interfaces.RSAPublicKeyWithNumbers): - numbers = key.public_numbers() - - expected_e = 0x10001 - expected_n = int( - '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D' - '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691' - 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF' - '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142' - '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF' - 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3' - '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B' - 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0' - '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31' - '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16) + numbers = key.public_numbers() - expected = RSAPublicNumbers(expected_e, expected_n) + expected_e = 0x10001 + expected_n = int( + '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D' + '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691' + 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF' + '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142' + '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF' + 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3' + '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B' + 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0' + '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31' + '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16) - 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 = textwrap.dedent("""\ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk - FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll - PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK - vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f - sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy - ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX - 2MzHvnbv testkey@localhost""").encode() - - key = _load_ssh_rsa_public_key(str_key, backend) - - assert key is not None - assert isinstance(key, interfaces.RSAPublicKey) - - if isinstance(key, interfaces.RSAPublicKeyWithNumbers): - numbers = key.public_numbers() + expected = RSAPublicNumbers(expected_e, expected_n) - 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 + assert numbers == expected -- cgit v1.2.3 From 4724d61be546f900298c7594d3bdb942b39a919f Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 14 Dec 2014 00:16:03 -0600 Subject: Added better parsing for RFC 4251 string and mpint values. Also moved several of the SSH key splitting and validation checks up into the load_ssh_public_key method since they will apply to more than just RSA. Added additional checks to make sure the key doesn't contain extraneous data --- .../hazmat/primitives/serialization.py | 62 ++++++++++++++-------- tests/hazmat/primitives/test_serialization.py | 45 ++++++++++++++++ 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index 38c541cb..f20d9f56 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -46,42 +46,60 @@ def load_pem_public_key(data, backend): def load_ssh_public_key(data, backend): - if not data.startswith(b'ssh-'): - raise ValueError('SSH-formatted keys must begin with ssh-') + key_parts = data.split(b' ') - if not data.startswith(b'ssh-rsa'): + if len(key_parts) < 2 or len(key_parts) > 3: + raise ValueError( + 'Key is not in the proper format or contains extra data.') + + key_type = key_parts[0] + key_body = key_parts[1] + + if not key_type.startswith(b'ssh-'): + raise ValueError('SSH-formatted keys must begin with \'ssh-\'.') + + if not key_type.startswith(b'ssh-rsa'): raise UnsupportedAlgorithm('Only RSA keys are currently supported.') - return _load_ssh_rsa_public_key(data, backend) + return _load_ssh_rsa_public_key(key_type, key_body, backend) + +def _load_ssh_rsa_public_key(key_type, key_body, backend): + assert key_type == b'ssh-rsa' -def _load_ssh_rsa_public_key(data, backend): - assert data.startswith(b'ssh-rsa ') + data = base64.b64decode(key_body) - parts = data.split(b' ') - data = base64.b64decode(parts[1]) + key_body_type, rest = _read_next_string(data) + e, rest = _read_next_mpint(rest) + n, rest = _read_next_mpint(rest) - cert_data = [] + if key_type != key_body_type: + raise ValueError( + 'Key header and key body contain different key type values.') - 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:] + if len(rest) != 0: + raise ValueError('Key body contains extra bytes.') - 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: +def _read_next_string(data): + """Retrieves the next RFC 4251 string value from the data.""" + str_len = struct.unpack('>I', data[0:4])[0] + return data[4:4 + str_len], data[4 + str_len:] + + +def _read_next_mpint(data): + mpint_data, rest = _read_next_string(data) + + if len(mpint_data) % 4 != 0: # Pad the bytes with 0x00 to a block size of 4 - data = (b'\x00' * (4 - (len(data) % 4))) + data + mpint_data = (b'\x00' * (4 - (len(mpint_data) % 4))) + mpint_data result = 0 - while len(data) > 0: - result = (result << 32) + struct.unpack('>I', data[0:4])[0] - data = data[4:] + while len(mpint_data) > 0: + result = (result << 32) + struct.unpack('>I', mpint_data[0:4])[0] + mpint_data = mpint_data[4:] - return result + return result, rest diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index ffe3d7df..9180b9aa 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -697,6 +697,51 @@ class TestSSHSerialization(object): with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) + def test_load_ssh_public_key_rsa_too_short(self, backend): + ssh_key = b'ssh-rsa' + + with pytest.raises(ValueError): + load_ssh_public_key(ssh_key, backend) + + def test_load_ssh_public_key_rsa_key_types_dont_match(self, backend): + ssh_key = textwrap.dedent("""\ + ssh-bad AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk + FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll + PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK + vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f + sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy + ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX + 2MzHvnbv testkey@localhost extra""").encode() # ssh-bad + + with pytest.raises(ValueError): + load_ssh_public_key(ssh_key, backend) + + def test_load_ssh_public_key_rsa_extra_string_after_comment(self, backend): + ssh_key = textwrap.dedent("""\ + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk + FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll + PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK + vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f + sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy + ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX + 2MzHvnbv testkey@localhost extra""").encode() # Extra appended + + with pytest.raises(ValueError): + load_ssh_public_key(ssh_key, backend) + + def test_load_ssh_public_key_rsa_extra_data_after_modulo(self, backend): + ssh_key = textwrap.dedent("""\ + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk + FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll + PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK + vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f + sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy + ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX + 2MzHvnbvAQ== testkey@localhost""").encode() # Extra 0x01 appended + + with pytest.raises(ValueError): + load_ssh_public_key(ssh_key, backend) + def test_load_ssh_public_key_rsa(self, backend): ssh_key = textwrap.dedent("""\ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk -- cgit v1.2.3 From c3e8b8890585d82bf19ac642756c5c4baac74237 Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 14 Dec 2014 08:11:48 -0600 Subject: Made a couple of minor tweaks to clean up _read_next_string and _read_next_mpint --- src/cryptography/hazmat/primitives/serialization.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index f20d9f56..455c8a91 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -65,8 +65,6 @@ def load_ssh_public_key(data, backend): def _load_ssh_rsa_public_key(key_type, key_body, backend): - assert key_type == b'ssh-rsa' - data = base64.b64decode(key_body) key_body_type, rest = _read_next_string(data) @@ -85,7 +83,7 @@ def _load_ssh_rsa_public_key(key_type, key_body, backend): def _read_next_string(data): """Retrieves the next RFC 4251 string value from the data.""" - str_len = struct.unpack('>I', data[0:4])[0] + str_len, = struct.unpack('>I', data[0:4]) return data[4:4 + str_len], data[4 + str_len:] -- cgit v1.2.3 From cbddc9897b579f1b44e0c4cd5cd868fa7c6dd06d Mon Sep 17 00:00:00 2001 From: Mark Adams Date: Sun, 14 Dec 2014 22:31:29 -0600 Subject: Added optimization for Python 3 to use int.from_bytes instead of Python code --- src/cryptography/hazmat/primitives/serialization.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index 455c8a91..8a4c8bd8 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function import base64 import struct +import sys import warnings from cryptography import utils @@ -88,10 +89,15 @@ def _read_next_string(data): def _read_next_mpint(data): + """Reads the next mpint from the data. Currently, all mpints are + interpreted as unsigned.""" mpint_data, rest = _read_next_string(data) + if sys.version_info >= (3, 2): + # If we're using >= 3.2, use int.from_bytes for identical results. + return int.from_bytes(mpint_data, byteorder='big', signed=False), rest + if len(mpint_data) % 4 != 0: - # Pad the bytes with 0x00 to a block size of 4 mpint_data = (b'\x00' * (4 - (len(mpint_data) % 4))) + mpint_data result = 0 -- cgit v1.2.3 From 993b85ad6f3ebe5db6a24c1649d28f8cf45095ea Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 15 Dec 2014 10:42:45 -0800 Subject: A handful of tiny fixes --- CHANGELOG.rst | 6 +- .../hazmat/primitives/asymmetric/serialization.rst | 5 +- .../hazmat/primitives/serialization.py | 49 +++++++++------ src/cryptography/utils.py | 7 ++- tests/hazmat/primitives/test_serialization.py | 69 ++++++++++++---------- 5 files changed, 77 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0394ebd9..cf6d2252 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,9 +19,9 @@ Changelog * 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` - to support the loading of OpenSSH public keys (:rfc:`4253`). Currently, only RSA - is supported. + :func:`~cryptography.hazmat.primitives.serialization.load_ssh_public_key` to + support the loading of OpenSSH public keys (:rfc:`4253`). Currently, only RSA + keys are supported. 0.6.1 - 2014-10-15 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 45c7a5bc..a9392c7b 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -199,7 +199,7 @@ KEY-----`` or ``-----BEGIN DSA PRIVATE KEY-----``. OpenSSH Public Key ~~~~~~~~~~~~~~~~~~ -The format used by OpenSSH to store public keys as specified in :rfc:`4253`. +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. @@ -224,7 +224,8 @@ purposes):: :param bytes data: The OpenSSH encoded key data. - :param backend: A backend provider. + :param backend: An + :class:`~cryptography.hazmat.backends.interfaces.RSABackend` provider. :returns: A new instance of a public key type. diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index 8a4c8bd8..858ec043 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -49,7 +49,7 @@ def load_pem_public_key(data, backend): def load_ssh_public_key(data, backend): key_parts = data.split(b' ') - if len(key_parts) < 2 or len(key_parts) > 3: + if len(key_parts) != 2 and len(key_parts) != 3: raise ValueError( 'Key is not in the proper format or contains extra data.') @@ -62,21 +62,21 @@ def load_ssh_public_key(data, backend): if not key_type.startswith(b'ssh-rsa'): raise UnsupportedAlgorithm('Only RSA keys are currently supported.') - return _load_ssh_rsa_public_key(key_type, key_body, backend) + return _load_ssh_rsa_public_key(key_body, backend) -def _load_ssh_rsa_public_key(key_type, key_body, backend): +def _load_ssh_rsa_public_key(key_body, backend): data = base64.b64decode(key_body) - key_body_type, rest = _read_next_string(data) + key_type, rest = _read_next_string(data) e, rest = _read_next_mpint(rest) n, rest = _read_next_mpint(rest) - if key_type != key_body_type: + if key_type != b'ssh-rsa': raise ValueError( 'Key header and key body contain different key type values.') - if len(rest) != 0: + if rest: raise ValueError('Key body contains extra bytes.') return backend.load_rsa_public_numbers(RSAPublicNumbers(e, n)) @@ -84,26 +84,37 @@ def _load_ssh_rsa_public_key(key_type, key_body, backend): def _read_next_string(data): """Retrieves the next RFC 4251 string value from the data.""" - str_len, = struct.unpack('>I', data[0:4]) + str_len, = struct.unpack('>I', data[:4]) return data[4:4 + str_len], data[4 + str_len:] def _read_next_mpint(data): - """Reads the next mpint from the data. Currently, all mpints are - interpreted as unsigned.""" + """ + Reads the next mpint from the data. + + Currently, all mpints are interpreted as unsigned. + """ mpint_data, rest = _read_next_string(data) - if sys.version_info >= (3, 2): - # If we're using >= 3.2, use int.from_bytes for identical results. - return int.from_bytes(mpint_data, byteorder='big', signed=False), rest + return _int_from_bytes(mpint_data, byteorder='big', signed=False), rest + + + +if hasattr(int, "from_bytes"): + _int_from_bytes = int.from_bytes +else: + def _int_from_bytes(data, byteorder, signed=False): + assert byteorder == 'big' + assert not signed - if len(mpint_data) % 4 != 0: - mpint_data = (b'\x00' * (4 - (len(mpint_data) % 4))) + mpint_data + if len(data) % 4 != 0: + data = (b'\x00' * (4 - (len(data) % 4))) + data - result = 0 + result = 0 - while len(mpint_data) > 0: - result = (result << 32) + struct.unpack('>I', mpint_data[0:4])[0] - mpint_data = mpint_data[4:] + while len(data) > 0: + digit, = struct.unpack('>I', data[:4]) + result = (result << 32) + digit + data = data[4:] - return result, rest + return result diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py index 63464dfa..78f73464 100644 --- a/src/cryptography/utils.py +++ b/src/cryptography/utils.py @@ -48,8 +48,9 @@ def verify_interface(iface, klass): ) -def bit_length(x): - if sys.version_info >= (2, 7): +if sys.version_info >= (2, 7): + def bit_length(x): return x.bit_length() - else: +else: + def bit_length(x): return len(bin(x)) - (2 + (x <= 0)) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 9180b9aa..2b9d05a7 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -704,53 +704,58 @@ class TestSSHSerialization(object): load_ssh_public_key(ssh_key, backend) def test_load_ssh_public_key_rsa_key_types_dont_match(self, backend): - ssh_key = textwrap.dedent("""\ - ssh-bad AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk - FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll - PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK - vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f - sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy - ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX - 2MzHvnbv testkey@localhost extra""").encode() # ssh-bad + ssh_key = ( + b"ssh-bad AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk" + b"FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll" + b"PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK" + b"vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f" + b"sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy" + b"///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX" + b"2MzHvnbv testkey@localhost extra" + ) with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) def test_load_ssh_public_key_rsa_extra_string_after_comment(self, backend): - ssh_key = textwrap.dedent("""\ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk - FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll - PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK - vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f - sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy - ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX - 2MzHvnbv testkey@localhost extra""").encode() # Extra appended + ssh_key = ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk" + b"FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll" + b"PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK" + b"vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f" + b"sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy" + b"///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX" + # Extra section appended + b"2MzHvnbv testkey@localhost extra" + ) with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) def test_load_ssh_public_key_rsa_extra_data_after_modulo(self, backend): - ssh_key = textwrap.dedent("""\ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk - FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll - PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK - vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f - sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy - ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX - 2MzHvnbvAQ== testkey@localhost""").encode() # Extra 0x01 appended + ssh_key = ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk" + b"FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll" + b"PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK" + b"vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f" + b"sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy" + b"///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX" + b"2MzHvnbvAQ== testkey@localhost" + ) with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) def test_load_ssh_public_key_rsa(self, backend): - ssh_key = textwrap.dedent("""\ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk - FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll - PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK - vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f - sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy - ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX - 2MzHvnbv testkey@localhost""").encode() + ssh_key = ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk" + b"FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll" + b"PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK" + b"vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f" + b"sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy" + b"///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX" + b"2MzHvnbv testkey@localhost" + ) key = load_ssh_public_key(ssh_key, backend) -- cgit v1.2.3 From 004b3ad14d691311d6dbb48aa05901e01d681ea2 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 15 Dec 2014 11:19:05 -0800 Subject: two flake8 fixes --- src/cryptography/hazmat/primitives/serialization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index 858ec043..0dbbc85c 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function import base64 import struct -import sys import warnings from cryptography import utils @@ -99,7 +98,6 @@ def _read_next_mpint(data): return _int_from_bytes(mpint_data, byteorder='big', signed=False), rest - if hasattr(int, "from_bytes"): _int_from_bytes = int.from_bytes else: -- cgit v1.2.3 From 0c9e8af1f43a1d37ccf46640250186e0fb42fb06 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 15 Dec 2014 12:30:50 -0800 Subject: Added two more tests to get coverage back up --- tests/hazmat/primitives/test_serialization.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 2b9d05a7..abb55751 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -692,7 +692,7 @@ class TestSSHSerialization(object): load_ssh_public_key(ssh_key, backend) def test_load_ssh_public_key_bad_format(self, backend): - ssh_key = b'not-a-real-key' + ssh_key = b'not-a-real-key text' with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) @@ -746,6 +746,22 @@ class TestSSHSerialization(object): with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) + def test_load_ssh_public_key_rsa_different_string(self, backend): + ssh_key = ( + # "AAAAB3NzA" the final A is capitalized here to cause the string + # ssh-rsa inside the base64 encoded blob to be incorrect. It should + # be a lower case 'a'. + b"ssh-rsa AAAAB3NzAC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk" + b"FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll" + b"PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK" + b"vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f" + b"sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy" + b"///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX" + b"2MzHvnbvAQ== testkey@localhost" + ) + with pytest.raises(ValueError): + load_ssh_public_key(ssh_key, backend) + def test_load_ssh_public_key_rsa(self, backend): ssh_key = ( b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk" -- cgit v1.2.3