diff options
-rw-r--r-- | docs/hazmat/backends/interfaces.rst | 19 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/dh.rst | 19 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 91 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/interfaces.py | 12 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 63 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/dh.py | 22 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/serialization.py | 12 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl.py | 4 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_dh.py | 136 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_serialization.py | 19 | ||||
-rw-r--r-- | vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der | bin | 526 -> 557 bytes |
11 files changed, 395 insertions, 2 deletions
diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst index 4d0520fa..8ea1cf99 100644 --- a/docs/hazmat/backends/interfaces.rst +++ b/docs/hazmat/backends/interfaces.rst @@ -452,6 +452,15 @@ A specific ``backend`` may provide one or more of these interfaces. serialized data contains. :raises ValueError: If the data could not be deserialized. + .. method:: load_pem_parameters(data) + + .. versionadded:: 2.0 + + :param bytes data: PEM data to load. + :return: A new instance of the appropriate type of encryption + parameters the serialized data contains. + :raises ValueError: If the data could not be deserialized. + .. class:: DERSerializationBackend .. versionadded:: 0.8 @@ -476,6 +485,16 @@ A specific ``backend`` may provide one or more of these interfaces. serialized data contains. :raises ValueError: If the data could not be deserialized. + .. method:: load_der_parameters(data) + + .. versionadded:: 2.0 + + :param bytes data: DER data to load. + :return: A new instance of the appropriate type of encryption + parameters the serialized data contains. + :raises ValueError: If the data could not be deserialized. + + .. class:: X509Backend .. versionadded:: 0.7 diff --git a/docs/hazmat/primitives/asymmetric/dh.rst b/docs/hazmat/primitives/asymmetric/dh.rst index f4cae1c3..971d3452 100644 --- a/docs/hazmat/primitives/asymmetric/dh.rst +++ b/docs/hazmat/primitives/asymmetric/dh.rst @@ -115,6 +115,25 @@ Group parameters :return: A :class:`~cryptography.hazmat.primitives.asymmetric.dh.DHParameterNumbers`. + .. method:: parameter_bytes(encoding, format) + + .. versionadded:: 2.0 + + Allows serialization of the parameters to bytes. Encoding ( + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM` or + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`) and + format ( + :attr:`~cryptography.hazmat.primitives.serialization.ParameterFormat.PKCS3`) + are chosen to define the exact serialization. + + :param encoding: A value from the + :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum. + + :param format: A value from the + :class:`~cryptography.hazmat.primitives.serialization.ParameterFormat` enum. + + :return bytes: Serialized parameters. + Key interfaces ~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index b745332e..b0cfbd0e 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -63,6 +63,20 @@ Key Serialization def sign_with_dsa_key(key, message): return b"" + parameters_pem_data = b""" + -----BEGIN DH PARAMETERS----- + MIGHAoGBALsrWt44U1ojqTy88o0wfjysBE51V6Vtarjm2+5BslQK/RtlndHde3gx + +ccNs+InANszcuJFI8AHt4743kGRzy5XSlul4q4dDJENOHoyqYxueFuFVJELEwLQ + XrX/McKw+hS6GPVQnw6tZhgGo9apdNdYgeLQeQded8Bum8jqzP3rAgEC + -----END DH PARAMETERS----- + """.strip() + + parameters_der_data = base64.b64decode( + b"MIGHAoGBALsrWt44U1ojqTy88o0wfjysBE51V6Vtarjm2+5BslQK/RtlndHde3gx+ccNs+In" + b"ANsz\ncuJFI8AHt4743kGRzy5XSlul4q4dDJENOHoyqYxueFuFVJELEwLQXrX/McKw+hS6GP" + b"VQnw6tZhgG\no9apdNdYgeLQeQded8Bum8jqzP3rAgEC" + ) + There are several common schemes for serializing asymmetric private and public keys to bytes. They generally support encryption of private keys and additional key metadata. @@ -181,6 +195,37 @@ 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. +.. function:: load_pem_parameters(data, backend) + + .. versionadded:: 2.0 + + Deserialize encryption parameters from PEM encoded data to one of the supported + asymmetric encryption parameters types. + + .. doctest:: + + >>> from cryptography.hazmat.primitives.serialization import load_pem_parameters + >>> from cryptography.hazmat.primitives.asymmetric import dh + >>> parameters = load_pem_parameters(parameters_pem_data, backend=default_backend()) + >>> isinstance(parameters, dh.DHParameters) + True + + :param bytes data: The PEM encoded parameters data. + + :param backend: An instance of + :class:`~cryptography.hazmat.backends.interfaces.PEMSerializationBackend`. + + + :returns: Currently only + :class:`~cryptography.hazmat.primitives.asymmetric.dh.DHParameters` + supported. + + :raises ValueError: If the PEM data's structure could not be decoded + successfully. + + :raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized parameters + is of a type that is not supported by the backend. + DER ~~~ @@ -268,6 +313,37 @@ the rest. >>> isinstance(key, rsa.RSAPublicKey) True +.. function:: load_der_parameters(data, backend) + + .. versionadded:: 2.0 + + Deserialize encryption parameters from DER encoded data to one of the supported + asymmetric encryption parameters types. + + :param bytes data: The DER encoded parameters data. + + :param backend: An instance of + :class:`~cryptography.hazmat.backends.interfaces.DERSerializationBackend`. + + :returns: Currently only + :class:`~cryptography.hazmat.primitives.asymmetric.dh.DHParameters` + supported. + + :raises ValueError: If the DER data's structure could not be decoded + successfully. + + :raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized key is of a type that + is not supported by the backend. + + .. doctest:: + + >>> from cryptography.hazmat.backends import default_backend + >>> from cryptography.hazmat.primitives.asymmetric import dh + >>> from cryptography.hazmat.primitives.serialization import load_der_parameters + >>> parameters = load_der_parameters(parameters_der_data, backend=default_backend()) + >>> isinstance(parameters, dh.DHParameters) + True + OpenSSH Public Key ~~~~~~~~~~~~~~~~~~ @@ -379,6 +455,18 @@ Serialization Formats The public key format used by OpenSSH (e.g. as found in ``~/.ssh/id_rsa.pub`` or ``~/.ssh/authorized_keys``). +.. class:: ParameterFormat + + .. versionadded:: 2.0 + + An enumeration for parameters formats. Used with the ``parameter_bytes`` + method available on + :class:`~cryptography.hazmat.primitives.asymmetric.dh.DHParametersWithSerialization`. + + .. attribute:: PKCS3 + + ASN1 DH parameters sequence as defined in `PKCS3`_. + Serialization Encodings ~~~~~~~~~~~~~~~~~~~~~~~ @@ -445,3 +533,6 @@ Serialization Encryption Types .. class:: NoEncryption Do not encrypt. + + +.. _`PKCS3`: https://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-3-diffie-hellman-key-agreement-standar.htm diff --git a/src/cryptography/hazmat/backends/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py index 9ed50cc4..0a476b99 100644 --- a/src/cryptography/hazmat/backends/interfaces.py +++ b/src/cryptography/hazmat/backends/interfaces.py @@ -243,6 +243,12 @@ class PEMSerializationBackend(object): Loads a public key from PEM encoded data. """ + @abc.abstractmethod + def load_pem_parameters(self, data): + """ + Load encryption parameters from PEM encoded data. + """ + @six.add_metaclass(abc.ABCMeta) class DERSerializationBackend(object): @@ -259,6 +265,12 @@ class DERSerializationBackend(object): Loads a public key from DER encoded data. """ + @abc.abstractmethod + def load_der_parameters(self, data): + """ + Load encryption parameters from DER encoded data. + """ + @six.add_metaclass(abc.ABCMeta) class X509Backend(object): diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index d17b38ca..5458a0f8 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -1007,6 +1007,17 @@ class Backend(object): else: self._handle_key_loading_error() + def load_pem_parameters(self, data): + mem_bio = self._bytes_to_bio(data) + # only DH is supported currently + dh_cdata = self._lib.PEM_read_bio_DHparams( + mem_bio.bio, self._ffi.NULL, self._ffi.NULL, self._ffi.NULL) + if dh_cdata != self._ffi.NULL: + dh_cdata = self._ffi.gc(dh_cdata, self._lib.DH_free) + return _DHParameters(self, dh_cdata) + else: + self._handle_key_loading_error() + def load_der_private_key(self, data, password): # OpenSSL has a function called d2i_AutoPrivateKey that in theory # handles this automatically, however it doesn't handle encrypted @@ -1063,6 +1074,28 @@ class Backend(object): else: self._handle_key_loading_error() + def load_der_parameters(self, data): + mem_bio = self._bytes_to_bio(data) + dh_cdata = self._lib.d2i_DHparams_bio( + mem_bio.bio, self._ffi.NULL + ) + if dh_cdata != self._ffi.NULL: + dh_cdata = self._ffi.gc(dh_cdata, self._lib.DH_free) + return _DHParameters(self, dh_cdata) + elif self._lib.Cryptography_HAS_EVP_PKEY_DHX: + # We check to see if the is dhx. + self._consume_errors() + res = self._lib.BIO_reset(mem_bio.bio) + self.openssl_assert(res == 1) + dh_cdata = self._lib.Cryptography_d2i_DHxparams_bio( + mem_bio.bio, self._ffi.NULL + ) + if dh_cdata != self._ffi.NULL: + dh_cdata = self._ffi.gc(dh_cdata, self._lib.DH_free) + return _DHParameters(self, dh_cdata) + + self._handle_key_loading_error() + def load_pem_x509_certificate(self, data): mem_bio = self._bytes_to_bio(data) x509 = self._lib.PEM_read_bio_X509( @@ -1618,6 +1651,36 @@ class Backend(object): serialization._ssh_write_string(public_numbers.encode_point()) ) + def _parameter_bytes(self, encoding, format, cdata): + if encoding is serialization.Encoding.OpenSSH: + raise TypeError( + "OpenSSH encoding is not supported" + ) + + # Only DH is supported here currently. + q = self._ffi.new("BIGNUM **") + self._lib.DH_get0_pqg(cdata, + self._ffi.NULL, + q, + self._ffi.NULL) + if encoding is serialization.Encoding.PEM: + if q[0] != self._ffi.NULL: + write_bio = self._lib.PEM_write_bio_DHxparams + else: + write_bio = self._lib.PEM_write_bio_DHparams + elif encoding is serialization.Encoding.DER: + if q[0] != self._ffi.NULL: + write_bio = self._lib.Cryptography_i2d_DHxparams_bio + else: + write_bio = self._lib.i2d_DHparams_bio + else: + raise TypeError("encoding must be an item from the Encoding enum") + + bio = self._create_mem_bio_gc() + res = write_bio(bio, cdata) + self.openssl_assert(res == 1) + return self._read_mem_bio(bio) + def generate_dh_parameters(self, generator, key_size): if key_size < 512: raise ValueError("DH key_size must be at least 512 bits") diff --git a/src/cryptography/hazmat/backends/openssl/dh.py b/src/cryptography/hazmat/backends/openssl/dh.py index 456e9bea..e5f76447 100644 --- a/src/cryptography/hazmat/backends/openssl/dh.py +++ b/src/cryptography/hazmat/backends/openssl/dh.py @@ -59,6 +59,28 @@ class _DHParameters(object): def generate_private_key(self): return self._backend.generate_dh_private_key(self) + def parameter_bytes(self, encoding, format): + if format is not serialization.ParameterFormat.PKCS3: + raise ValueError( + "Only PKCS3 serialization is supported" + ) + if not self._backend._lib.Cryptography_HAS_EVP_PKEY_DHX: + q = self._backend._ffi.new("BIGNUM **") + self._backend._lib.DH_get0_pqg(self._dh_cdata, + self._backend._ffi.NULL, + q, + self._backend._ffi.NULL) + if q[0] != self._backend._ffi.NULL: + raise UnsupportedAlgorithm( + "DH X9.42 serialization is not supported", + _Reasons.UNSUPPORTED_SERIALIZATION) + + return self._backend._parameter_bytes( + encoding, + format, + self._dh_cdata + ) + def _handle_dh_compute_key_error(errors, backend): lib = backend._lib diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index 992fd42f..bd09e6e3 100644 --- a/src/cryptography/hazmat/primitives/serialization.py +++ b/src/cryptography/hazmat/primitives/serialization.py @@ -24,6 +24,10 @@ def load_pem_public_key(data, backend): return backend.load_pem_public_key(data) +def load_pem_parameters(data, backend): + return backend.load_pem_parameters(data) + + def load_der_private_key(data, password, backend): return backend.load_der_private_key(data, password) @@ -32,6 +36,10 @@ def load_der_public_key(data, backend): return backend.load_der_public_key(data) +def load_der_parameters(data, backend): + return backend.load_der_parameters(data) + + def load_ssh_public_key(data, backend): key_parts = data.split(b' ', 2) @@ -178,6 +186,10 @@ class PublicFormat(Enum): OpenSSH = "OpenSSH" +class ParameterFormat(Enum): + PKCS3 = "PKCS3" + + @six.add_metaclass(abc.ABCMeta) class KeySerializationEncryption(object): pass diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index e857ff61..d8e7fe47 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -614,6 +614,10 @@ class TestOpenSSLDHSerialization(object): public_key.public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_SERIALIZATION): + parameters.parameters(backend).parameter_bytes( + serialization.Encoding.PEM, + serialization.ParameterFormat.PKCS3) @pytest.mark.parametrize( ("key_path", "loader_func"), diff --git a/tests/hazmat/primitives/test_dh.py b/tests/hazmat/primitives/test_dh.py index c351e5df..1fdabe57 100644 --- a/tests/hazmat/primitives/test_dh.py +++ b/tests/hazmat/primitives/test_dh.py @@ -669,3 +669,139 @@ class TestDHPublicKeySerialization(object): key.public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.PKCS1 ) + + +@pytest.mark.requires_backend_interface(interface=DHBackend) +@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend) +@pytest.mark.requires_backend_interface(interface=DERSerializationBackend) +class TestDHParameterSerialization(object): + + @pytest.mark.parametrize( + ("encoding", "loader_func"), + [ + [ + serialization.Encoding.PEM, + serialization.load_pem_parameters + ], + [ + serialization.Encoding.DER, + serialization.load_der_parameters + ], + ] + ) + def test_parameter_bytes(self, backend, encoding, + loader_func): + parameters = dh.generate_parameters(2, 512, backend) + serialized = parameters.parameter_bytes( + encoding, serialization.ParameterFormat.PKCS3 + ) + loaded_key = loader_func(serialized, backend) + loaded_param_num = loaded_key.parameter_numbers() + assert loaded_param_num == parameters.parameter_numbers() + + @pytest.mark.parametrize( + ("param_path", "loader_func", "encoding", "is_dhx"), + [ + ( + os.path.join("asymmetric", "DH", "dhp.pem"), + serialization.load_pem_parameters, + serialization.Encoding.PEM, + False, + ), ( + os.path.join("asymmetric", "DH", "dhp.der"), + serialization.load_der_parameters, + serialization.Encoding.DER, + False, + ), ( + os.path.join("asymmetric", "DH", "dhp_rfc5114_2.pem"), + serialization.load_pem_parameters, + serialization.Encoding.PEM, + True, + ), ( + os.path.join("asymmetric", "DH", "dhp_rfc5114_2.der"), + serialization.load_der_parameters, + serialization.Encoding.DER, + True, + ) + ] + ) + def test_parameter_bytes_match(self, param_path, loader_func, + encoding, backend, is_dhx): + _skip_dhx_unsupported(backend, is_dhx) + param_bytes = load_vectors_from_file( + param_path, + lambda pemfile: pemfile.read(), mode="rb" + ) + parameters = loader_func(param_bytes, backend) + serialized = parameters.parameter_bytes( + encoding, + serialization.ParameterFormat.PKCS3, + ) + assert serialized == param_bytes + + @pytest.mark.parametrize( + ("param_path", "loader_func", "vec_path", "is_dhx"), + [ + ( + os.path.join("asymmetric", "DH", "dhp.pem"), + serialization.load_pem_parameters, + os.path.join("asymmetric", "DH", "dhkey.txt"), + False, + ), ( + os.path.join("asymmetric", "DH", "dhp.der"), + serialization.load_der_parameters, + os.path.join("asymmetric", "DH", "dhkey.txt"), + False, + ), ( + os.path.join("asymmetric", "DH", "dhp_rfc5114_2.pem"), + serialization.load_pem_parameters, + os.path.join("asymmetric", "DH", "dhkey_rfc5114_2.txt"), + True, + ), ( + os.path.join("asymmetric", "DH", "dhp_rfc5114_2.der"), + serialization.load_der_parameters, + os.path.join("asymmetric", "DH", "dhkey_rfc5114_2.txt"), + True, + ) + ] + ) + def test_public_bytes_values(self, param_path, loader_func, + vec_path, backend, is_dhx): + _skip_dhx_unsupported(backend, is_dhx) + key_bytes = load_vectors_from_file( + param_path, + lambda pemfile: pemfile.read(), mode="rb" + ) + vec = load_vectors_from_file(vec_path, load_nist_vectors)[0] + parameters = loader_func(key_bytes, backend) + parameter_numbers = parameters.parameter_numbers() + assert parameter_numbers.g == int(vec["g"], 16) + assert parameter_numbers.p == int(vec["p"], 16) + if "q" in vec: + assert parameter_numbers.q == int(vec["q"], 16) + else: + assert parameter_numbers.q is None + + def test_parameter_bytes_invalid_encoding(self, backend): + parameters = dh.generate_parameters(2, 512, backend) + with pytest.raises(TypeError): + parameters.parameter_bytes( + "notencoding", + serialization.ParameterFormat.PKCS3 + ) + + def test_parameter_bytes_invalid_format(self, backend): + parameters = dh.generate_parameters(2, 512, backend) + with pytest.raises(ValueError): + parameters.parameter_bytes( + serialization.Encoding.PEM, + "notformat" + ) + + def test_parameter_bytes_openssh_unsupported(self, backend): + parameters = dh.generate_parameters(2, 512, backend) + with pytest.raises(TypeError): + parameters.parameter_bytes( + serialization.Encoding.OpenSSH, + serialization.ParameterFormat.PKCS3 + ) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index f4b953e6..a7355221 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -18,8 +18,9 @@ from cryptography.hazmat.backends.interfaces import ( ) from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa from cryptography.hazmat.primitives.serialization import ( - BestAvailableEncryption, load_der_private_key, load_der_public_key, - load_pem_private_key, load_pem_public_key, load_ssh_public_key + BestAvailableEncryption, load_der_parameters, load_der_private_key, + load_der_public_key, load_pem_parameters, load_pem_private_key, + load_pem_public_key, load_ssh_public_key ) @@ -310,6 +311,14 @@ class TestDERSerialization(object): assert key.curve.name == "secp256r1" assert key.curve.key_size == 256 + def test_wrong_parameters_format(self, backend): + param_data = b"---- NOT A KEY ----\n" + + with pytest.raises(ValueError): + load_der_parameters( + param_data, backend + ) + @pytest.mark.requires_backend_interface(interface=PEMSerializationBackend) class TestPEMSerialization(object): @@ -591,6 +600,12 @@ class TestPEMSerialization(object): with pytest.raises(ValueError): load_pem_public_key(key_data, backend) + def test_wrong_parameters_format(self, backend): + param_data = b"---- NOT A KEY ----\n" + + with pytest.raises(ValueError): + load_pem_parameters(param_data, backend) + def test_corrupt_traditional_format(self, backend): # privkey.pem with a bunch of data missing. key_data = textwrap.dedent("""\ diff --git a/vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der b/vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der Binary files differindex 666eb9a0..f00c443a 100644 --- a/vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der +++ b/vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der |