diff options
-rw-r--r-- | CHANGELOG.rst | 5 | ||||
-rw-r--r-- | docs/hazmat/backends/openssl.rst | 1 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 58 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 74 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/serialization.py | 8 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_serialization.py | 269 |
6 files changed, 410 insertions, 5 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 85c0f581..e106ff44 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -68,6 +68,11 @@ Changelog :mod:`~cryptography.hazmat.primitives.asymmetric.rsa`. * Added support for parsing X.509 names. See the :doc:`X.509 documentation</x509>` for more information. +* Added + :func:`~cryptography.hazmat.primitives.serialization.load_der_private_key` to + support loading of DER encoded private keys and + :func:`~cryptography.hazmat.primitives.serialization.load_der_public_key` to + support loading DER encoded public keys. * Fixed building against LibreSSL, a compile-time substitute for OpenSSL. 0.7.2 - 2015-01-16 diff --git a/docs/hazmat/backends/openssl.rst b/docs/hazmat/backends/openssl.rst index 4e0f6282..26ffea6a 100644 --- a/docs/hazmat/backends/openssl.rst +++ b/docs/hazmat/backends/openssl.rst @@ -15,6 +15,7 @@ Red Hat Enterprise Linux 5) and greater. Earlier versions may work but are * :class:`~cryptography.hazmat.backends.interfaces.CipherBackend` * :class:`~cryptography.hazmat.backends.interfaces.CMACBackend` + * :class:`~cryptography.hazmat.backends.interfaces.DERSerializationBackend` * :class:`~cryptography.hazmat.backends.interfaces.DSABackend` * :class:`~cryptography.hazmat.backends.interfaces.EllipticCurveBackend` * :class:`~cryptography.hazmat.backends.interfaces.HashBackend` diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 8155e6f4..3bf39151 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -136,6 +136,64 @@ all begin with ``-----BEGIN {format}-----`` and end with ``-----END :raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized key is of a type that is not supported by the backend. +DER +~~~ + +DER is an ASN.1 encoding type. There are no encapsulation boundaries and the +data is binary. DER keys may be in a variety of formats, but as long as you +know whether it is a public or private key the loading functions will handle +the rest. + +.. function:: load_der_private_key(data, password, backend) + + .. versionadded:: 0.8 + + Deserialize a private key from DER encoded data to one of the supported + asymmetric private key types. + + :param bytes data: The DER encoded key data. + + :param bytes password: The password to use to decrypt the data. Should + be ``None`` if the private key is not encrypted. + + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.DERSerializationBackend` + provider. + + :returns: A new instance of a private key. + + :raises ValueError: If the DER data could not be decrypted or if its + structure could not be decoded successfully. + + :raises TypeError: If a ``password`` was given and the private key was + not encrypted. Or if the key was encrypted but no + password was supplied. + + :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. + +.. function:: load_der_public_key(data, backend) + + .. versionadded:: 0.8 + + Deserialize a public key from DER encoded data to one of the supported + asymmetric public key types. + + :param bytes data: The DER encoded key data. + + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.DERSerializationBackend` + provider. + + :returns: A new instance of a public key. + + :raises ValueError: If the DER data's structure could not be decoded + successfully. + + :raises UnsupportedAlgorithm: If the serialized key is of a type that + is not supported by the backend. + OpenSSH Public Key ~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 8441e891..3e95c88b 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -15,9 +15,9 @@ from cryptography.exceptions import ( InternalError, UnsupportedAlgorithm, _Reasons ) from cryptography.hazmat.backends.interfaces import ( - CMACBackend, CipherBackend, DSABackend, EllipticCurveBackend, HMACBackend, - HashBackend, PBKDF2HMACBackend, PEMSerializationBackend, RSABackend, - X509Backend + CMACBackend, CipherBackend, DERSerializationBackend, DSABackend, + EllipticCurveBackend, HMACBackend, HashBackend, PBKDF2HMACBackend, + PEMSerializationBackend, RSABackend, X509Backend ) from cryptography.hazmat.backends.openssl.ciphers import ( _AESCTRCipherContext, _CipherContext @@ -56,6 +56,7 @@ _OpenSSLError = collections.namedtuple("_OpenSSLError", @utils.register_interface(CipherBackend) @utils.register_interface(CMACBackend) +@utils.register_interface(DERSerializationBackend) @utils.register_interface(DSABackend) @utils.register_interface(EllipticCurveBackend) @utils.register_interface(HashBackend) @@ -696,6 +697,73 @@ class Backend(object): None, ) + def load_der_private_key(self, data, password): + # OpenSSL has a function called d2i_AutoPrivateKey that can simplify + # this. Unfortunately it doesn't properly support PKCS8 on OpenSSL + # 0.9.8 so we can't use it. Instead we sequentially try to load it 3 + # different ways. First we'll try to load it as a traditional key + key = self._evp_pkey_from_der_traditional_key(data, password) + if not key: + # Okay so it's not a traditional key. Let's try + # PKCS8 unencrypted. OpenSSL 0.9.8 can't load unencrypted + # PKCS8 keys using d2i_PKCS8PrivateKey_bio so we do this instead. + key = self._evp_pkey_from_der_unencrypted_pkcs8(data, password) + + if key: + return self._evp_pkey_to_private_key(key) + else: + # Finally we try to load it with the method that handles encrypted + # PKCS8 properly. + return self._load_key( + self._lib.d2i_PKCS8PrivateKey_bio, + self._evp_pkey_to_private_key, + data, + password, + ) + + def _evp_pkey_from_der_traditional_key(self, data, password): + mem_bio = self._bytes_to_bio(data) + key = self._lib.d2i_PrivateKey_bio(mem_bio.bio, self._ffi.NULL) + if key != self._ffi.NULL: + if password is not None: + raise TypeError( + "Password was given but private key is not encrypted." + ) + + key = self._ffi.gc(key, self._lib.EVP_PKEY_free) + return key + else: + self._consume_errors() + return None + + def _evp_pkey_from_der_unencrypted_pkcs8(self, data, password): + mem_bio = self._bytes_to_bio(data) + info = self._lib.d2i_PKCS8_PRIV_KEY_INFO_bio( + mem_bio.bio, self._ffi.NULL + ) + if info != self._ffi.NULL: + key = self._lib.EVP_PKCS82PKEY(info) + assert key != self._ffi.NULL + if password is not None: + raise TypeError( + "Password was given but private key is not encrypted." + ) + key = self._ffi.gc(key, self._lib.EVP_PKEY_free) + return key + else: + self._consume_errors() + return None + + def load_der_public_key(self, data): + mem_bio = self._bytes_to_bio(data) + evp_pkey = self._lib.d2i_PUBKEY_bio(mem_bio.bio, self._ffi.NULL) + if evp_pkey == self._ffi.NULL: + self._consume_errors() + raise ValueError("Could not unserialize key data.") + + evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free) + return self._evp_pkey_to_public_key(evp_pkey) + def load_pem_x509_certificate(self, data): mem_bio = self._bytes_to_bio(data) x509 = self._lib.PEM_read_bio_X509( diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index dad419fe..0f9506e1 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -21,6 +21,14 @@ def load_pem_public_key(data, backend): return backend.load_pem_public_key(data) +def load_der_private_key(data, password, backend): + return backend.load_der_private_key(data, password) + + +def load_der_public_key(data, backend): + return backend.load_der_public_key(data) + + def load_ssh_public_key(data, backend): key_parts = data.split(b' ') diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 8c79f640..2ec8f254 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function +import base64 import itertools import os import textwrap @@ -12,7 +13,8 @@ import pytest from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat.backends.interfaces import ( - DSABackend, EllipticCurveBackend, PEMSerializationBackend, RSABackend + DERSerializationBackend, DSABackend, EllipticCurveBackend, + PEMSerializationBackend, RSABackend ) from cryptography.hazmat.primitives import interfaces from cryptography.hazmat.primitives.asymmetric import ec @@ -21,7 +23,8 @@ from cryptography.hazmat.primitives.asymmetric.dsa import ( ) from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, load_pem_public_key, load_ssh_public_key + load_der_private_key, load_der_public_key, load_pem_private_key, + load_pem_public_key, load_ssh_public_key ) @@ -33,6 +36,268 @@ from .utils import ( from ...utils import raises_unsupported_algorithm +@pytest.mark.requires_backend_interface(interface=DERSerializationBackend) +class TestDERSerialization(object): + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.parametrize( + ("key_path", "password"), + [ + (["DER_Serialization", "enc-rsa-pkcs8.der"], b"foobar"), + (["DER_Serialization", "enc2-rsa-pkcs8.der"], b"baz"), + (["DER_Serialization", "unenc-rsa-pkcs8.der"], None), + (["DER_Serialization", "testrsa.der"], None), + ] + ) + def test_load_der_rsa_private_key(self, key_path, password, backend): + key = load_vectors_from_file( + os.path.join("asymmetric", *key_path), + lambda derfile: load_der_private_key( + derfile.read(), password, backend + ), + mode="rb" + ) + assert key + assert isinstance(key, interfaces.RSAPrivateKey) + if isinstance(key, interfaces.RSAPrivateKeyWithNumbers): + _check_rsa_private_numbers(key.private_numbers()) + + @pytest.mark.requires_backend_interface(interface=DSABackend) + @pytest.mark.parametrize( + ("key_path", "password"), + [ + (["DER_Serialization", "unenc-dsa-pkcs8.der"], None), + (["DER_Serialization", "dsa.1024.der"], None), + (["DER_Serialization", "dsa.2048.der"], None), + (["DER_Serialization", "dsa.3072.der"], None), + ] + ) + def test_load_der_dsa_private_key(self, key_path, password, backend): + key = load_vectors_from_file( + os.path.join("asymmetric", *key_path), + lambda derfile: load_der_private_key( + derfile.read(), password, backend + ), + mode="rb" + ) + assert key + assert isinstance(key, interfaces.DSAPrivateKey) + if isinstance(key, interfaces.DSAPrivateKeyWithNumbers): + _check_dsa_private_numbers(key.private_numbers()) + + @pytest.mark.parametrize( + ("key_path", "password"), + [ + (["DER_Serialization", "ec_private_key.der"], None), + (["DER_Serialization", "ec_private_key_encrypted.der"], b"123456"), + ] + ) + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) + def test_load_der_ec_private_key(self, key_path, password, backend): + _skip_curve_unsupported(backend, ec.SECP256R1()) + key = load_vectors_from_file( + os.path.join("asymmetric", *key_path), + lambda derfile: load_der_private_key( + derfile.read(), password, backend + ), + mode="rb" + ) + + assert key + assert isinstance(key, interfaces.EllipticCurvePrivateKey) + assert key.curve.name == "secp256r1" + assert key.curve.key_size == 256 + + @pytest.mark.parametrize( + "key_path", + [ + ["DER_Serialization", "enc-rsa-pkcs8.der"], + ] + ) + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_wrong_password(self, key_path, backend): + key_file = os.path.join("asymmetric", *key_path) + password = b"this password is wrong" + + with pytest.raises(ValueError): + load_vectors_from_file( + key_file, + lambda derfile: load_der_private_key( + derfile.read(), password, backend + ), + mode="rb" + ) + + @pytest.mark.parametrize( + "key_path", + [ + ["DER_Serialization", "unenc-rsa-pkcs8.der"] + ] + ) + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_unused_password(self, key_path, backend): + key_file = os.path.join("asymmetric", *key_path) + password = b"this password will not be used" + + with pytest.raises(TypeError): + load_vectors_from_file( + key_file, + lambda derfile: load_der_private_key( + derfile.read(), password, backend + ), + mode="rb" + ) + + @pytest.mark.parametrize( + ("key_path", "password"), + itertools.product( + [ + ["DER_Serialization", "enc-rsa-pkcs8.der"], + ], + [b"", None] + ) + ) + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_missing_password(self, key_path, password, backend): + key_file = os.path.join("asymmetric", *key_path) + + with pytest.raises(TypeError): + load_vectors_from_file( + key_file, + lambda derfile: load_der_private_key( + derfile.read(), password, backend + ), + mode="rb" + ) + + def test_wrong_format(self, backend): + key_data = b"---- NOT A KEY ----\n" + + with pytest.raises(ValueError): + load_der_private_key( + key_data, None, backend + ) + + with pytest.raises(ValueError): + load_der_private_key( + key_data, b"this password will not be used", backend + ) + + def test_corrupt_der_pkcs8(self, backend): + # unenc-rsa-pkcs8 with a bunch of data missing. + key_data = textwrap.dedent("""\ + MIICdQIBADALBgkqhkiG9w0BAQEEggJhMIICXQIBAAKBgQC7JHoJfg6yNzLMOWet + 8Z49a4KD0dCspMAYvo2YAMB7/wdEycocujbhJ2n/seONi+5XqTqqFkM5VBl8rmkk + FPZk/7x0xmdsTPECSWnHK+HhoaNDFPR3j8jQhVo1laxiqcEhAHegi5cwtFosuJAv + FiRC0Cgz+frQPFQEBsAV9RuasyQxqzxrR0Ow0qncBeGBWbYE6WZhqtcLAI895b+i + +F4lbB4iD7T9QeIDMU/aIMXA81UO4cns1z4qDAHKeyLLrPQrJ/B4X7XC+egUWm5+ + hr1qmyAMusyXIBECQQDJWZ8piluf4yrYfsJAn6hF5T4RjTztbqvO0GVG2McHY7Uj + NPSffhzHx/ll0fQEQji+OgydCCX8o3HZrgw5YfSJAkEA7e+rqdU5nO5ZG//PSEQb + tjLnRiTzBH/elQhtdZ5nF7pcpNTi4k13zutmKcWW4GK75azcRGJUhu1kDM7QYAOd + SQJAVNkYcifkvna7GmooL5VYEsQsqLbM4v0NF2TIGNfG3z1MGp75KrC5LhL97MNR + we2p/bd2k0HYyCKUGnf2nMPDiQJBAI75pwittSoE240EobUGIDTSz8CJsXIxuDmL + z+KOpdpPRR5TQmbEMEspjsFpFymMiuYPgmihQbO2cJl1qScY5OkCQQCJ6m5tcN8l + Xxg/SNpjEIv+qAyUD96XVlOJlOIeLHQ8kYE0C6ZA+MsqYIzgAreJk88Yn0lU/X0/ + mu/UpE/BRZmR + """).encode() + bad_der = base64.b64decode(b"".join(key_data.splitlines())) + + with pytest.raises(ValueError): + load_der_private_key( + bad_der, None, backend + ) + + with pytest.raises(ValueError): + load_der_private_key( + bad_der, b"this password will not be used", backend + ) + + def test_corrupt_traditional_format_der(self, backend): + # privkey with a bunch of data missing. + key_data = textwrap.dedent("""\ + MIIBPAIBAAJBAKrbeqkuRk8VcRmWFmtP+LviMB3+6dizWW3DwaffznyHGAFwUJ/I + Tv0XtbsCyl3QoyKGhrOAy3RvPK5M38iuXT0CAwEAAQJAZ3cnzaHXM/bxGaR5CR1R + rD1qFBAVfoQFiOH9uPJgMaoAuoQEisPHVcZDKcOv4wEg6/TInAIXBnEigtqvRzuy + mvcpHZwQJdmdHHkGKAs37Dfxi67HbkUCIQCeZGliHXFa071Fp06ZeWlR2ADonTZz + rJBhdTe0v5pCeQIhAIZfkiGgGBX4cIuuckzEm43g9WMUjxP/0GlK39vIyihxAiEA + mymehFRT0MvqW5xAKAx7Pgkt8HVKwVhc2LwGKHE0DZM= + """).encode() + bad_der = base64.b64decode(b"".join(key_data.splitlines())) + + with pytest.raises(ValueError): + load_pem_private_key(bad_der, None, backend) + + with pytest.raises(ValueError): + load_pem_private_key( + bad_der, b"this password will not be used", backend + ) + + @pytest.mark.parametrize( + ("key_file"), + [ + os.path.join( + "asymmetric", "DER_Serialization", "unenc-rsa-pkcs8.pub.der"), + os.path.join( + "asymmetric", "DER_Serialization", "rsa_public_key.der"), + ] + ) + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_load_der_rsa_public_key(self, key_file, backend): + key = load_vectors_from_file( + key_file, + lambda derfile: load_der_public_key( + derfile.read(), backend + ), + mode="rb" + ) + assert key + assert isinstance(key, interfaces.RSAPublicKey) + if isinstance(key, interfaces.RSAPublicKeyWithNumbers): + numbers = key.public_numbers() + assert numbers.e == 65537 + + def test_load_der_invalid_public_key(self, backend): + with pytest.raises(ValueError): + load_der_public_key(b"invalid data", backend) + + @pytest.mark.parametrize( + ("key_file"), + [ + os.path.join( + "asymmetric", "DER_Serialization", "unenc-dsa-pkcs8.pub.der"), + os.path.join( + "asymmetric", "DER_Serialization", "dsa_public_key.der"), + ] + ) + @pytest.mark.requires_backend_interface(interface=DSABackend) + def test_load_der_dsa_public_key(self, key_file, backend): + key = load_vectors_from_file( + key_file, + lambda derfile: load_der_public_key( + derfile.read(), backend + ), + mode="rb" + ) + assert key + assert isinstance(key, interfaces.DSAPublicKey) + + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) + def test_load_ec_public_key(self, backend): + _skip_curve_unsupported(backend, ec.SECP256R1()) + key = load_vectors_from_file( + os.path.join( + "asymmetric", "DER_Serialization", + "ec_public_key.der"), + lambda derfile: load_der_public_key( + derfile.read(), backend + ), + mode="rb" + ) + assert key + assert isinstance(key, interfaces.EllipticCurvePublicKey) + assert key.curve.name == "secp256r1" + assert key.curve.key_size == 256 + + @pytest.mark.requires_backend_interface(interface=PEMSerializationBackend) class TestPEMSerialization(object): @pytest.mark.parametrize( |