add ec.private_key_from_secret_and_curve (#3225)

* finish https://github.com/pyca/cryptography/pull/1973

* change API & add test

Function will now return an instance of EllipticCurvePrivateKey, as that
is the users' ultimate goal anyway.

* fix test

* improve coverage

* complete coverage

* final fix

* centos fix

* retry

* cleanup asserts

* use openssl_assert

* skip unsupported platforms

* change API name to derive_private_key

* change version added

* improve description of `secret` param

* separate successful and failure test cases

* simplify successful case

* add docs for derive_elliptic_curve_public_point

* add period
diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst
index b79bb23..0a0d145 100644
--- a/docs/hazmat/backends/interfaces.rst
+++ b/docs/hazmat/backends/interfaces.rst
@@ -422,6 +422,15 @@
         :returns: An instance of
             :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`.
 
+    .. method:: derive_elliptic_curve_public_point(private_value, curve)
+
+        :param private_value: A secret scalar value.
+
+        :param curve: An instance of
+            :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve`.
+
+        :returns: A tuple (x, y).
+
 .. class:: PEMSerializationBackend
 
     .. versionadded:: 0.6
diff --git a/docs/hazmat/primitives/asymmetric/ec.rst b/docs/hazmat/primitives/asymmetric/ec.rst
index 2421d92..33ebee0 100644
--- a/docs/hazmat/primitives/asymmetric/ec.rst
+++ b/docs/hazmat/primitives/asymmetric/ec.rst
@@ -20,6 +20,22 @@
     :returns: A new instance of :class:`EllipticCurvePrivateKey`.
 
 
+.. function:: derive_private_key(secret, curve, backend)
+
+    .. versionadded:: 1.6
+
+    Derive a private key from ``secret`` on ``curve`` for use with ``backend``.
+
+    :param int secret: The secret scalar value.
+
+    :param curve: An instance of :class:`EllipticCurve`.
+
+    :param backend: An instance of
+        :class:`~cryptography.hazmat.backends.interfaces.EllipticCurveBackend`.
+
+    :returns: A new instance of :class:`EllipticCurvePrivateKey`.
+
+
 Elliptic Curve Signature Algorithms
 -----------------------------------
 
diff --git a/src/cryptography/hazmat/backends/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py
index 9a1d704..ad4a436 100644
--- a/src/cryptography/hazmat/backends/interfaces.py
+++ b/src/cryptography/hazmat/backends/interfaces.py
@@ -221,6 +221,12 @@
         Returns whether the exchange algorithm is supported by this backend.
         """
 
+    @abc.abstractmethod
+    def derive_elliptic_curve_public_point(self, private_value, curve):
+        """
+        Compute the public key point (x, y) given the private value and curve.
+        """
+
 
 @six.add_metaclass(abc.ABCMeta)
 class PEMSerializationBackend(object):
diff --git a/src/cryptography/hazmat/backends/multibackend.py b/src/cryptography/hazmat/backends/multibackend.py
index deca020..ab9127f 100644
--- a/src/cryptography/hazmat/backends/multibackend.py
+++ b/src/cryptography/hazmat/backends/multibackend.py
@@ -279,6 +279,19 @@
             _Reasons.UNSUPPORTED_ELLIPTIC_CURVE
         )
 
+    def derive_elliptic_curve_public_point(self, private_value, curve):
+        for b in self._filtered_backends(EllipticCurveBackend):
+            try:
+                return b.derive_elliptic_curve_public_point(private_value,
+                                                            curve)
+            except UnsupportedAlgorithm:
+                continue
+
+        raise UnsupportedAlgorithm(
+            "This backend does not support this elliptic curve.",
+            _Reasons.UNSUPPORTED_ELLIPTIC_CURVE
+        )
+
     def elliptic_curve_exchange_algorithm_supported(self, algorithm, curve):
         return any(
             b.elliptic_curve_exchange_algorithm_supported(algorithm, curve)
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 41e7e77..7991429 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -1386,6 +1386,40 @@
 
         return _EllipticCurvePublicKey(self, ec_cdata, evp_pkey)
 
+    def derive_elliptic_curve_public_point(self, private_value, curve):
+        curve_nid = self._elliptic_curve_to_nid(curve)
+
+        ec_cdata = self._lib.EC_KEY_new_by_curve_name(curve_nid)
+        self.openssl_assert(ec_cdata != self._ffi.NULL)
+        ec_cdata = self._ffi.gc(ec_cdata, self._lib.EC_KEY_free)
+
+        set_func, get_func, group = (
+            self._ec_key_determine_group_get_set_funcs(ec_cdata)
+        )
+
+        point = self._lib.EC_POINT_new(group)
+        self.openssl_assert(point != self._ffi.NULL)
+        point = self._ffi.gc(point, self._lib.EC_POINT_free)
+
+        value = self._int_to_bn(private_value)
+        value = self._ffi.gc(value, self._lib.BN_free)
+
+        with self._tmp_bn_ctx() as bn_ctx:
+            res = self._lib.EC_POINT_mul(group, point, value, self._ffi.NULL,
+                                         self._ffi.NULL, bn_ctx)
+            self.openssl_assert(res == 1)
+
+            bn_x = self._lib.BN_CTX_get(bn_ctx)
+            bn_y = self._lib.BN_CTX_get(bn_ctx)
+
+            res = get_func(group, point, bn_x, bn_y, bn_ctx)
+            self.openssl_assert(res == 1)
+
+            point_x = self._bn_to_int(bn_x)
+            point_y = self._bn_to_int(bn_y)
+
+        return point_x, point_y
+
     def elliptic_curve_exchange_algorithm_supported(self, algorithm, curve):
         return (
             self.elliptic_curve_supported(curve) and
diff --git a/src/cryptography/hazmat/primitives/asymmetric/ec.py b/src/cryptography/hazmat/primitives/asymmetric/ec.py
index 1c576c6..1005ccd 100644
--- a/src/cryptography/hazmat/primitives/asymmetric/ec.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/ec.py
@@ -253,6 +253,19 @@
     return backend.generate_elliptic_curve_private_key(curve)
 
 
+def derive_private_key(secret, curve, backend):
+    if not isinstance(secret, six.integer_types):
+        raise TypeError("secret must be an integer type.")
+
+    if not isinstance(curve, EllipticCurve):
+        raise TypeError("curve must provide the EllipticCurve interface.")
+
+    x, y = backend.derive_elliptic_curve_public_point(secret, curve)
+    public_numbers = EllipticCurvePublicNumbers(x, y, curve)
+    private_numbers = EllipticCurvePrivateNumbers(secret, public_numbers)
+    return private_numbers.private_key(backend)
+
+
 class EllipticCurvePublicNumbers(object):
     def __init__(self, x, y, curve):
         if (
diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py
index 1cd8733..319edf7 100644
--- a/tests/hazmat/backends/test_multibackend.py
+++ b/tests/hazmat/backends/test_multibackend.py
@@ -27,6 +27,12 @@
     pass
 
 
+@utils.register_interface(ec.EllipticCurve)
+class DummyCurve(object):
+    name = "dummy-curve"
+    key_size = 1
+
+
 @utils.register_interface(CipherBackend)
 class DummyCipherBackend(object):
     def __init__(self, supported_ciphers):
@@ -179,6 +185,10 @@
             self.elliptic_curve_supported(curve)
         )
 
+    def derive_elliptic_curve_public_point(self, private_value, curve):
+        if not self.elliptic_curve_supported(curve):
+            raise UnsupportedAlgorithm(_Reasons.UNSUPPORTED_ELLIPTIC_CURVE)
+
 
 @utils.register_interface(PEMSerializationBackend)
 class DummyPEMSerializationBackend(object):
@@ -501,6 +511,12 @@
             ec.ECDH(), ec.SECT163K1()
         )
 
+        with pytest.raises(UnsupportedAlgorithm):
+            backend.derive_elliptic_curve_public_point(123, DummyCurve())
+
+        assert backend.derive_elliptic_curve_public_point(
+            123, ec.SECT283K1()) is None
+
     def test_pem_serialization_backend(self):
         backend = MultiBackend([DummyPEMSerializationBackend()])
 
diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py
index dff2f3e..523f3f4 100644
--- a/tests/hazmat/primitives/test_ec.py
+++ b/tests/hazmat/primitives/test_ec.py
@@ -100,6 +100,32 @@
         _skip_ecdsa_vector(backend, DummyCurve, hashes.SHA256)
 
 
+@pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+def test_derive_private_key_success(backend):
+    curve = ec.SECP256K1()
+    _skip_curve_unsupported(backend, curve)
+
+    private_numbers = ec.generate_private_key(curve, backend).private_numbers()
+
+    derived_key = ec.derive_private_key(
+        private_numbers.private_value, curve, backend
+    )
+
+    assert private_numbers == derived_key.private_numbers()
+
+
+@pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+def test_derive_private_key_errors(backend):
+    curve = ec.SECP256K1()
+    _skip_curve_unsupported(backend, curve)
+
+    with pytest.raises(TypeError):
+        ec.derive_private_key('one', curve, backend)
+
+    with pytest.raises(TypeError):
+        ec.derive_private_key(10, 'five', backend)
+
+
 def test_ec_numbers():
     numbers = ec.EllipticCurvePrivateNumbers(
         1,