diff options
author | Alex Gaynor <alex.gaynor@gmail.com> | 2016-06-03 13:04:26 -0700 |
---|---|---|
committer | Paul Kehrer <paul.l.kehrer@gmail.com> | 2016-06-03 13:04:26 -0700 |
commit | 949892938735c0cf14a6689d68779c2ce2410585 (patch) | |
tree | 17077680d9c0583458d21662904fdc48dc3f4f5b | |
parent | 6eeaf0bd76f5d40e9fbd9bc17b1b2fd08df186c4 (diff) | |
download | cryptography-949892938735c0cf14a6689d68779c2ce2410585.tar.gz cryptography-949892938735c0cf14a6689d68779c2ce2410585.tar.bz2 cryptography-949892938735c0cf14a6689d68779c2ce2410585.zip |
SSH serialization for public keys (#2957)
* SSH serialization for public keys
* name errors ahoy!
* id, ego, superego
* dsa support
* EC support
* Don't keyerror
* Documentation OpenSSH
* flake8
* fix
* bytes bytes bytes
* skip curve unsupported
* bytes!
* Move a function
* reorganize code for coverage
-rw-r--r-- | CHANGELOG.rst | 1 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 13 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 55 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/dsa.py | 1 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/ec.py | 1 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/rsa.py | 1 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/serialization.py | 37 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_dsa.py | 23 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_ec.py | 28 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_rsa.py | 36 |
10 files changed, 182 insertions, 14 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 22764e53..f8602e90 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Changelog * Support for OpenSSL 0.9.8 has been removed. Users on older version of OpenSSL will need to upgrade. * Added :class:`~cryptography.hazmat.primitives.kdf.kbkdf.KBKDFHMAC`. +* Added support for ``OpenSSH`` public key serialization. 1.3.4 - 2016-06-03 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index b94c0e10..7cef77fd 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -360,6 +360,13 @@ Serialization Formats Just the public key elements (without the algorithm identifier). This format is RSA only, but is used by some older systems. + .. attribute:: OpenSSH + + .. versionadded:: 1.4 + + The public key format used by OpenSSH (e.g. as found in + ``~/.ssh/id_rsa.pub`` or ``~/.ssh/authorized_keys``). + Serialization Encodings ~~~~~~~~~~~~~~~~~~~~~~~ @@ -389,6 +396,12 @@ Serialization Encodings For DER format. This is a binary format. + .. attribute:: OpenSSH + + .. versionadded:: 1.4 + + The format used by OpenSSH public keys. This is a text format. + Serialization Encryption Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index d8a681e6..126a881a 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function +import base64 import calendar import collections import itertools @@ -1698,11 +1699,23 @@ class Backend(object): self.openssl_assert(res == 1) return self._read_mem_bio(bio) - def _public_key_bytes(self, encoding, format, evp_pkey, cdata): + def _public_key_bytes(self, encoding, format, key, evp_pkey, cdata): if not isinstance(encoding, serialization.Encoding): raise TypeError("encoding must be an item from the Encoding enum") - if format is serialization.PublicFormat.SubjectPublicKeyInfo: + if ( + format is serialization.PublicFormat.OpenSSH or + encoding is serialization.Encoding.OpenSSH + ): + if ( + format is not serialization.PublicFormat.OpenSSH or + encoding is not serialization.Encoding.OpenSSH + ): + raise ValueError( + "OpenSSH format must be used with OpenSSH encoding" + ) + return self._openssh_public_key_bytes(key) + elif format is serialization.PublicFormat.SubjectPublicKeyInfo: if encoding is serialization.Encoding.PEM: write_bio = self._lib.PEM_write_bio_PUBKEY else: @@ -1732,6 +1745,44 @@ class Backend(object): self.openssl_assert(res == 1) return self._read_mem_bio(bio) + def _openssh_public_key_bytes(self, key): + if isinstance(key, rsa.RSAPublicKey): + public_numbers = key.public_numbers() + return b"ssh-rsa " + base64.b64encode( + serialization._ssh_write_string(b"ssh-rsa") + + serialization._ssh_write_mpint(public_numbers.e) + + serialization._ssh_write_mpint(public_numbers.n) + ) + elif isinstance(key, dsa.DSAPublicKey): + public_numbers = key.public_numbers() + parameter_numbers = public_numbers.parameter_numbers + return b"ssh-dss " + base64.b64encode( + serialization._ssh_write_string(b"ssh-dss") + + serialization._ssh_write_mpint(parameter_numbers.p) + + serialization._ssh_write_mpint(parameter_numbers.q) + + serialization._ssh_write_mpint(parameter_numbers.g) + + serialization._ssh_write_mpint(public_numbers.y) + ) + else: + assert isinstance(key, ec.EllipticCurvePublicKey) + public_numbers = key.public_numbers() + try: + curve_name = { + ec.SECP256R1: b"nistp256", + ec.SECP384R1: b"nistp384", + ec.SECP521R1: b"nistp521", + }[type(public_numbers.curve)] + except KeyError: + raise ValueError( + "Only SECP256R1, SECP384R1, and SECP521R1 curves are " + "supported by the SSH public key format" + ) + return b"ecdsa-sha2-" + curve_name + b" " + base64.b64encode( + serialization._ssh_write_string(b"ecdsa-sha2-" + curve_name) + + serialization._ssh_write_string(curve_name) + + serialization._ssh_write_string(public_numbers.encode_point()) + ) + class GetCipherByName(object): def __init__(self, fmt): diff --git a/src/cryptography/hazmat/backends/openssl/dsa.py b/src/cryptography/hazmat/backends/openssl/dsa.py index 5abc3da9..1608df04 100644 --- a/src/cryptography/hazmat/backends/openssl/dsa.py +++ b/src/cryptography/hazmat/backends/openssl/dsa.py @@ -297,6 +297,7 @@ class _DSAPublicKey(object): return self._backend._public_key_bytes( encoding, format, + self, self._evp_pkey, None ) diff --git a/src/cryptography/hazmat/backends/openssl/ec.py b/src/cryptography/hazmat/backends/openssl/ec.py index aa4a7a35..2f476031 100644 --- a/src/cryptography/hazmat/backends/openssl/ec.py +++ b/src/cryptography/hazmat/backends/openssl/ec.py @@ -299,6 +299,7 @@ class _EllipticCurvePublicKey(object): return self._backend._public_key_bytes( encoding, format, + self, self._evp_pkey, None ) diff --git a/src/cryptography/hazmat/backends/openssl/rsa.py b/src/cryptography/hazmat/backends/openssl/rsa.py index d8458ccc..920bae06 100644 --- a/src/cryptography/hazmat/backends/openssl/rsa.py +++ b/src/cryptography/hazmat/backends/openssl/rsa.py @@ -642,6 +642,7 @@ class _RSAPublicKey(object): return self._backend._public_key_bytes( encoding, format, + self, self._evp_pkey, self._rsa_cdata ) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index d848e5d4..992fd42f 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -59,7 +59,7 @@ def load_ssh_public_key(data, backend): except TypeError: raise ValueError('Key is not in the proper format.') - inner_key_type, rest = _read_next_string(decoded_data) + inner_key_type, rest = _ssh_read_next_string(decoded_data) if inner_key_type != key_type: raise ValueError( @@ -70,8 +70,8 @@ def load_ssh_public_key(data, backend): def _load_ssh_rsa_public_key(key_type, decoded_data, backend): - e, rest = _read_next_mpint(decoded_data) - n, rest = _read_next_mpint(rest) + e, rest = _ssh_read_next_mpint(decoded_data) + n, rest = _ssh_read_next_mpint(rest) if rest: raise ValueError('Key body contains extra bytes.') @@ -80,10 +80,10 @@ def _load_ssh_rsa_public_key(key_type, decoded_data, backend): def _load_ssh_dss_public_key(key_type, decoded_data, backend): - p, rest = _read_next_mpint(decoded_data) - q, rest = _read_next_mpint(rest) - g, rest = _read_next_mpint(rest) - y, rest = _read_next_mpint(rest) + p, rest = _ssh_read_next_mpint(decoded_data) + q, rest = _ssh_read_next_mpint(rest) + g, rest = _ssh_read_next_mpint(rest) + y, rest = _ssh_read_next_mpint(rest) if rest: raise ValueError('Key body contains extra bytes.') @@ -95,8 +95,8 @@ def _load_ssh_dss_public_key(key_type, decoded_data, backend): def _load_ssh_ecdsa_public_key(expected_key_type, decoded_data, backend): - curve_name, rest = _read_next_string(decoded_data) - data, rest = _read_next_string(rest) + curve_name, rest = _ssh_read_next_string(decoded_data) + data, rest = _ssh_read_next_string(rest) if expected_key_type != b"ecdsa-sha2-" + curve_name: raise ValueError( @@ -121,7 +121,7 @@ def _load_ssh_ecdsa_public_key(expected_key_type, decoded_data, backend): return numbers.public_key(backend) -def _read_next_string(data): +def _ssh_read_next_string(data): """ Retrieves the next RFC 4251 string value from the data. @@ -137,22 +137,34 @@ def _read_next_string(data): return data[4:4 + str_len], data[4 + str_len:] -def _read_next_mpint(data): +def _ssh_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) + mpint_data, rest = _ssh_read_next_string(data) return ( utils.int_from_bytes(mpint_data, byteorder='big', signed=False), rest ) +def _ssh_write_string(data): + return struct.pack(">I", len(data)) + data + + +def _ssh_write_mpint(value): + data = utils.int_to_bytes(value) + if six.indexbytes(data, 0) & 0x80: + data = b"\x00" + data + return _ssh_write_string(data) + + class Encoding(Enum): PEM = "PEM" DER = "DER" + OpenSSH = "OpenSSH" class PrivateFormat(Enum): @@ -163,6 +175,7 @@ class PrivateFormat(Enum): class PublicFormat(Enum): SubjectPublicKeyInfo = "X.509 subjectPublicKeyInfo with PKCS#1" PKCS1 = "Raw PKCS#1" + OpenSSH = "OpenSSH" @six.add_metaclass(abc.ABCMeta) diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py index b02cadc8..6ad9762a 100644 --- a/tests/hazmat/primitives/test_dsa.py +++ b/tests/hazmat/primitives/test_dsa.py @@ -1018,6 +1018,29 @@ class TestDSAPEMPublicKeySerialization(object): ) assert serialized == key_bytes + def test_public_bytes_openssh(self, backend): + key_bytes = load_vectors_from_file( + os.path.join("asymmetric", "PKCS8", "unenc-dsa-pkcs8.pub.pem"), + lambda pemfile: pemfile.read(), mode="rb" + ) + key = serialization.load_pem_public_key(key_bytes, backend) + + ssh_bytes = key.public_bytes( + serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH + ) + assert ssh_bytes == ( + b"ssh-dss AAAAB3NzaC1kc3MAAACBAKoJMMwUWCUiHK/6KKwolBlqJ4M95ewhJweR" + b"aJQgd3Si57I4sNNvGySZosJYUIPrAUMpJEGNhn+qIS3RBx1NzrJ4J5StOTzAik1K" + b"2n9o1ug5pfzTS05ALYLLioy0D+wxkRv5vTYLA0yqy0xelHmSVzyekAmcGw8FlAyr" + b"5dLeSaFnAAAAFQCtwOhps28KwBOmgf301ImdaYIEUQAAAIEAjGtFia+lOk0QSL/D" + b"RtHzhsp1UhzPct2qJRKGiA7hMgH/SIkLv8M9ebrK7HHnp3hQe9XxpmQi45QVvgPn" + b"EUG6Mk9bkxMZKRgsiKn6QGKDYGbOvnS1xmkMfRARBsJAq369VOTjMB/Qhs5q2ski" + b"+ycTorCIfLoTubxozlz/8kHNMkYAAACAKyYOqX3GoSrpMsZA5989j/BKigWgMk+N" + b"Xxsj8V+hcP8/QgYRJO/yWGyxG0moLc3BuQ/GqE+xAQnLZ9tdLalxrq8Xvl43KEVj" + b"5MZNnl/ISAJYsxnw3inVTYNQcNnih5FNd9+BSR9EI7YtqYTrP0XrKin86l2uUlrG" + b"q2vM4Ev99bY=" + ) + def test_public_bytes_invalid_encoding(self, backend): key = DSA_KEY_2048.private_key(backend).public_key() with pytest.raises(TypeError): diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py index 8747ea4f..8705f79c 100644 --- a/tests/hazmat/primitives/test_ec.py +++ b/tests/hazmat/primitives/test_ec.py @@ -815,6 +815,34 @@ class TestEllipticCurvePEMPublicKeySerialization(object): ) assert serialized == key_bytes + def test_public_bytes_openssh(self, backend): + _skip_curve_unsupported(backend, ec.SECP192R1()) + _skip_curve_unsupported(backend, ec.SECP256R1()) + + key_bytes = load_vectors_from_file( + os.path.join( + "asymmetric", "PEM_Serialization", "ec_public_key.pem" + ), + lambda pemfile: pemfile.read(), mode="rb" + ) + key = serialization.load_pem_public_key(key_bytes, backend) + + ssh_bytes = key.public_bytes( + serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH + ) + assert ssh_bytes == ( + b"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy" + b"NTYAAABBBCS8827s9rUZyxZTi/um01+oIlWrwLHOjQxRU9CDAndom00zVAw5BRrI" + b"KtHB+SWD4P+sVJTARSq1mHt8kOIWrPc=" + ) + + key = ec.generate_private_key(ec.SECP192R1(), backend).public_key() + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH + ) + def test_public_bytes_invalid_encoding(self, backend): _skip_curve_unsupported(backend, ec.SECP256R1()) key = load_vectors_from_file( diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index 6a8bb95d..320a96e5 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -2066,6 +2066,42 @@ class TestRSAPEMPublicKeySerialization(object): serialized = key.public_bytes(encoding, format) assert serialized == key_bytes + def test_public_bytes_openssh(self, backend): + key_bytes = load_vectors_from_file( + os.path.join("asymmetric", "public", "PKCS1", "rsa.pub.pem"), + lambda pemfile: pemfile.read(), mode="rb" + ) + key = serialization.load_pem_public_key(key_bytes, backend) + + ssh_bytes = key.public_bytes( + serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH + ) + assert ssh_bytes == ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7JHoJfg6yNzLMOWet8Z49a4KD" + b"0dCspMAYvo2YAMB7/wdEycocujbhJ2n/seONi+5XqTqqFkM5VBl8rmkkFPZk/7x0" + b"xmdsTPECSWnHK+HhoaNDFPR3j8jQhVo1laxiqcEhAHegi5cwtFosuJAvSKAFKEvy" + b"D43si00DQnXWrYHAEQ==" + ) + + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.OpenSSH + ) + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.DER, serialization.PublicFormat.OpenSSH + ) + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.PKCS1, + ) + with pytest.raises(ValueError): + key.public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + def test_public_bytes_invalid_encoding(self, backend): key = RSA_KEY_2048.private_key(backend).public_key() with pytest.raises(TypeError): |