Dh parameters serialization (#3504)

* Support DH parameter serizalization - no X9.42

* Support X9.42 serialization - DER not working

* Fix dhp_rfc5114_2.der

Changing the DER parameters serialization after the
fix in openssl commit a292c9f1b835

* DH parameters X9.42 DER serialization fixed

* fix _skip_dhx_unsupported

* document DH parameter_bytes

* PEP8 fixes

* Document load_pem_parameters

* Document load_der_parameters

* document ParameterFormat

* Increase test coverage

* Increase test covrage

* Remove unneeded check

* Fix typo

* Fix error in load_der_parameters

* Add load_pem_parameters and load_der_parameters to interfaces

* CR fixes

* Removed unverified phrase

* Update version to 2.0

* Fix pep8

* Rename ParameterFormat.ASN1 to ParameterFormat.DHParameter

* link pkcs3

* Add new line at end of file to serialization.rst

* Rename DHparameters to PKCS3

* doc CR fix
diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst
index 4d0520f..8ea1cf9 100644
--- a/docs/hazmat/backends/interfaces.rst
+++ b/docs/hazmat/backends/interfaces.rst
@@ -452,6 +452,15 @@
             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 @@
             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 f4cae1c..971d345 100644
--- a/docs/hazmat/primitives/asymmetric/dh.rst
+++ b/docs/hazmat/primitives/asymmetric/dh.rst
@@ -115,6 +115,25 @@
 
         :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 b745332..b0cfbd0 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -63,6 +63,20 @@
     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 @@
     :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 @@
         >>> 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 @@
         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 @@
 .. 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 9ed50cc..0a476b9 100644
--- a/src/cryptography/hazmat/backends/interfaces.py
+++ b/src/cryptography/hazmat/backends/interfaces.py
@@ -243,6 +243,12 @@
         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 @@
         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 d17b38c..5458a0f 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -1007,6 +1007,17 @@
             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 @@
             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 @@
                 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 456e9be..e5f7644 100644
--- a/src/cryptography/hazmat/backends/openssl/dh.py
+++ b/src/cryptography/hazmat/backends/openssl/dh.py
@@ -59,6 +59,28 @@
     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 992fd42..bd09e6e 100644
--- a/src/cryptography/hazmat/primitives/serialization.py
+++ b/src/cryptography/hazmat/primitives/serialization.py
@@ -24,6 +24,10 @@
     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 @@
     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 @@
     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 e857ff6..d8e7fe4 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -614,6 +614,10 @@
             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 c351e5d..1fdabe5 100644
--- a/tests/hazmat/primitives/test_dh.py
+++ b/tests/hazmat/primitives/test_dh.py
@@ -669,3 +669,139 @@
             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 f4b953e..a735522 100644
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -18,8 +18,9 @@
 )
 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 @@
         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 @@
         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
index 666eb9a..f00c443 100644
--- a/vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der
+++ b/vectors/cryptography_vectors/asymmetric/DH/dhp_rfc5114_2.der
Binary files differ