Merge pull request #739 from skeuomorf/dsa-backend

DSA backend
diff --git a/cryptography/hazmat/backends/interfaces.py b/cryptography/hazmat/backends/interfaces.py
index 27b609e..20c2111 100644
--- a/cryptography/hazmat/backends/interfaces.py
+++ b/cryptography/hazmat/backends/interfaces.py
@@ -113,6 +113,21 @@
         """
 
 
+class DSABackend(six.with_metaclass(abc.ABCMeta)):
+    @abc.abstractmethod
+    def generate_dsa_parameters(self, key_size):
+        """
+        Generate a DSAParameters instance with a modulus of key_size bits.
+        """
+
+    @abc.abstractmethod
+    def generate_dsa_private_key(self, parameters):
+        """
+        Generate an DSAPrivateKey instance with parameters as
+        a DSAParameters object.
+        """
+
+
 class OpenSSLSerializationBackend(six.with_metaclass(abc.ABCMeta)):
     @abc.abstractmethod
     def load_openssl_pem_private_key(self, data, password):
diff --git a/cryptography/hazmat/backends/multibackend.py b/cryptography/hazmat/backends/multibackend.py
index aa649dd..86cded8 100644
--- a/cryptography/hazmat/backends/multibackend.py
+++ b/cryptography/hazmat/backends/multibackend.py
@@ -16,7 +16,8 @@
 from cryptography import utils
 from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
 from cryptography.hazmat.backends.interfaces import (
-    CipherBackend, HMACBackend, HashBackend, PBKDF2HMACBackend, RSABackend
+    CipherBackend, DSABackend, HMACBackend, HashBackend, PBKDF2HMACBackend,
+    RSABackend
 )
 
 
@@ -25,6 +26,7 @@
 @utils.register_interface(HMACBackend)
 @utils.register_interface(PBKDF2HMACBackend)
 @utils.register_interface(RSABackend)
+@utils.register_interface(DSABackend)
 class MultiBackend(object):
     name = "multibackend"
 
@@ -142,3 +144,15 @@
                                                  padding, algorithm)
         raise UnsupportedAlgorithm("RSA is not supported by the backend",
                                    _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM)
+
+    def generate_dsa_parameters(self, key_size):
+        for b in self._filtered_backends(DSABackend):
+            return b.generate_dsa_parameters(key_size)
+        raise UnsupportedAlgorithm("DSA is not supported by the backend",
+                                   _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM)
+
+    def generate_dsa_private_key(self, parameters):
+        for b in self._filtered_backends(DSABackend):
+            return b.generate_dsa_private_key(parameters)
+        raise UnsupportedAlgorithm("DSA is not supported by the backend",
+                                   _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM)
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py
index 0c632ae..900d25c 100644
--- a/cryptography/hazmat/backends/openssl/backend.py
+++ b/cryptography/hazmat/backends/openssl/backend.py
@@ -25,11 +25,12 @@
     UnsupportedAlgorithm, _Reasons
 )
 from cryptography.hazmat.backends.interfaces import (
-    CipherBackend, HMACBackend, HashBackend, PBKDF2HMACBackend, RSABackend
+    CipherBackend, DSABackend, HMACBackend, HashBackend, PBKDF2HMACBackend,
+    RSABackend
 )
 from cryptography.hazmat.bindings.openssl.binding import Binding
 from cryptography.hazmat.primitives import hashes, interfaces
-from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives.asymmetric import dsa, rsa
 from cryptography.hazmat.primitives.asymmetric.padding import (
     MGF1, PKCS1v15, PSS
 )
@@ -46,6 +47,7 @@
 
 
 @utils.register_interface(CipherBackend)
+@utils.register_interface(DSABackend)
 @utils.register_interface(HashBackend)
 @utils.register_interface(HMACBackend)
 @utils.register_interface(PBKDF2HMACBackend)
@@ -415,6 +417,52 @@
         else:
             return isinstance(algorithm, hashes.SHA1)
 
+    def generate_dsa_parameters(self, key_size):
+        if key_size not in (1024, 2048, 3072):
+            raise ValueError(
+                "Key size must be 1024 or 2048 or 3072 bits")
+
+        if (self._lib.OPENSSL_VERSION_NUMBER < 0x1000000f and
+                key_size > 1024):
+            raise ValueError(
+                "Key size must be 1024 because OpenSSL < 1.0.0 doesn't "
+                "support larger key sizes")
+
+        ctx = self._lib.DSA_new()
+        assert ctx != self._ffi.NULL
+        ctx = self._ffi.gc(ctx, self._lib.DSA_free)
+
+        res = self._lib.DSA_generate_parameters_ex(
+            ctx, key_size, self._ffi.NULL, 0,
+            self._ffi.NULL, self._ffi.NULL, self._ffi.NULL
+        )
+
+        assert res == 1
+
+        return dsa.DSAParameters(
+            modulus=self._bn_to_int(ctx.p),
+            subgroup_order=self._bn_to_int(ctx.q),
+            generator=self._bn_to_int(ctx.g)
+        )
+
+    def generate_dsa_private_key(self, parameters):
+        ctx = self._lib.DSA_new()
+        assert ctx != self._ffi.NULL
+        ctx = self._ffi.gc(ctx, self._lib.DSA_free)
+        ctx.p = self._int_to_bn(parameters.p)
+        ctx.q = self._int_to_bn(parameters.q)
+        ctx.g = self._int_to_bn(parameters.g)
+
+        self._lib.DSA_generate_key(ctx)
+
+        return dsa.DSAPrivateKey(
+            modulus=self._bn_to_int(ctx.p),
+            subgroup_order=self._bn_to_int(ctx.q),
+            generator=self._bn_to_int(ctx.g),
+            x=self._bn_to_int(ctx.priv_key),
+            y=self._bn_to_int(ctx.pub_key)
+        )
+
 
 class GetCipherByName(object):
     def __init__(self, fmt):
diff --git a/cryptography/hazmat/primitives/asymmetric/dsa.py b/cryptography/hazmat/primitives/asymmetric/dsa.py
index 974db0a..4c2de36 100644
--- a/cryptography/hazmat/primitives/asymmetric/dsa.py
+++ b/cryptography/hazmat/primitives/asymmetric/dsa.py
@@ -16,6 +16,8 @@
 import six
 
 from cryptography import utils
+from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
+from cryptography.hazmat.backends.interfaces import DSABackend
 from cryptography.hazmat.primitives import interfaces
 
 
@@ -49,6 +51,16 @@
         self._subgroup_order = subgroup_order
         self._generator = generator
 
+    @classmethod
+    def generate(cls, key_size, backend):
+        if not isinstance(backend, DSABackend):
+            raise UnsupportedAlgorithm(
+                "Backend object does not implement DSABackend",
+                _Reasons.BACKEND_MISSING_INTERFACE
+            )
+
+        return backend.generate_dsa_parameters(key_size)
+
     @property
     def modulus(self):
         return self._modulus
@@ -96,6 +108,16 @@
         self._x = x
         self._y = y
 
+    @classmethod
+    def generate(cls, parameters, backend):
+        if not isinstance(backend, DSABackend):
+            raise UnsupportedAlgorithm(
+                "Backend object does not implement DSABackend",
+                _Reasons.BACKEND_MISSING_INTERFACE
+            )
+
+        return backend.generate_dsa_private_key(parameters)
+
     @property
     def key_size(self):
         return utils.bit_length(self._modulus)
diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst
index c38f818..9c401d2 100644
--- a/docs/hazmat/backends/interfaces.rst
+++ b/docs/hazmat/backends/interfaces.rst
@@ -285,3 +285,37 @@
 
         :raises cryptography.exceptions.UnsupportedAlgorithm: If the data is
             encrypted with an unsupported algorithm.
+
+
+.. class:: DSABackend
+
+    .. versionadded:: 0.4
+
+    A backend with methods for using DSA.
+
+    .. method:: generate_dsa_parameters(key_size)
+
+        :param int key_size: The length of the modulus in bits. It should be 
+            either "1024, 2048 or 3072". For keys generated in 2014 this should 
+            be at least 2048.
+            Note that some applications (such as SSH) have not yet gained support 
+            for larger key sizes specified in FIPS 186-3 and are still restricted
+            to only the 1024-bit keys specified in FIPS 186-2.
+
+        :return: A new instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.DSAParameters`
+            provider.
+
+    .. method:: generate_dsa_private_key(parameters)
+
+        :param parameters: A
+            :class:`~cryptography.hazmat.primitives.interfaces.DSAParameters`
+            provider.
+
+        :return: A new instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.DSAPrivateKey`
+            provider.
+
+        :raises ValueError: This is raised if the key size is not (1024 or 2048 or 3072)
+            or if the OpenSSL version is older than 1.0.0 and the key size is larger than 1024
+            because older OpenSSL versions don't support a key size larger than 1024.
diff --git a/docs/hazmat/primitives/asymmetric/dsa.rst b/docs/hazmat/primitives/asymmetric/dsa.rst
index 69e8d58..1a6a6e0 100644
--- a/docs/hazmat/primitives/asymmetric/dsa.rst
+++ b/docs/hazmat/primitives/asymmetric/dsa.rst
@@ -13,6 +13,16 @@
 
     DSA Parameters are required for generating a DSA private key.
 
+    You should use :meth:`~generate` to generate new parameters.
+
+    .. warning::
+        This method only checks a limited set of properties of its arguments.
+        Using DSA parameters that you do not trust or with incorrect arguments
+        may lead to insecure operation, crashes, and other undefined behavior.
+        We recommend that you only ever load parameters that were generated
+        with software you trust.
+
+
     This class conforms to the
     :class:`~cryptography.hazmat.primitives.interfaces.DSAParameters`
     interface.
@@ -23,6 +33,23 @@
                         ``subgroup_order``, or ``generator`` do
                         not match the bounds specified in `FIPS 186-4`_.
 
+    .. classmethod:: generate(key_size, backend)
+
+        Generate a new ``DSAParameters`` instance using ``backend``.
+
+        :param int key_size: The length of the modulus in bits. It should be 
+            either "1024, 2048 or 3072". For keys generated in 2014 this should 
+            be `at least 2048`_ (See page 41).
+            Note that some applications (such as SSH) have not yet gained support 
+            for larger key sizes specified in FIPS 186-3 and are still restricted
+            to only the 1024-bit keys specified in FIPS 186-2.
+            
+        :return: A new instance of ``DSAParameters``
+
+        :raises cryptography.exceptions.UnsupportedAlgorithm: This is raised if
+            the provided ``backend`` does not implement
+            :class:`~cryptography.hazmat.backends.interfaces.DSABackend`
+
 
 .. class:: DSAPrivateKey(modulus, subgroup_order, generator, x, y)
 
@@ -30,6 +57,16 @@
 
     A DSA private key is required for signing messages.
 
+    You should use :meth:`~generate` to generate new keys.
+
+    .. warning::
+        This method only checks a limited set of properties of its arguments.
+        Using a DSA private key that you do not trust or with incorrect
+        parameters may lead to insecure operation, crashes, and other undefined
+        behavior. We recommend that you only ever load private keys that were
+        generated with software you trust.
+
+
     This class conforms to the
     :class:`~cryptography.hazmat.primitives.interfaces.DSAPrivateKey`
     interface.
@@ -40,6 +77,26 @@
                         ``subgroup_order``, or ``generator`` do
                         not match the bounds specified in `FIPS 186-4`_.
 
+    .. classmethod:: generate(parameters, backend)
+
+        Generate a new ``DSAPrivateKey`` instance using ``backend``.
+
+        :param parameters: A
+            :class:`~cryptography.hazmat.primitives.interfaces.DSAParameters`
+            provider.
+        :param backend: A
+            :class:`~cryptography.hazmat.backends.interfaces.DSABackend`
+            provider.
+        :return: A new instance of ``DSAPrivateKey``.
+
+        :raises cryptography.exceptions.UnsupportedAlgorithm: This is raised if
+            the provided ``backend`` does not implement
+            :class:`~cryptography.hazmat.backends.interfaces.DSABackend`
+        
+        :raises ValueError: This is raised if the key size is not (1024 or 2048 or 3072)
+            or if the OpenSSL version is older than 1.0.0 and the key size is larger than 1024
+            because older OpenSSL versions don't support a key size larger than 1024.
+
 
 .. class:: DSAPublicKey(modulus, subgroup_order, generator, y)
 
@@ -65,4 +122,4 @@
 .. _`DSA`: https://en.wikipedia.org/wiki/Digital_Signature_Algorithm 
 .. _`public-key`: https://en.wikipedia.org/wiki/Public-key_cryptography
 .. _`FIPS 186-4`: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf 
-
+.. _`at least 2048`: http://www.ecrypt.eu.org/documents/D.SPA.20.pdf
diff --git a/pytest.ini b/pytest.ini
index 3f65e30..b590d0b 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -2,6 +2,7 @@
 addopts = -r s
 markers =
     cipher: this test requires a backend providing CipherBackend
+    dsa: this test requires a backend providing DSABackend
     hash: this test requires a backend providing HashBackend
     hmac: this test requires a backend providing HMACBackend
     pbkdf2hmac: this test requires a backend providing PBKDF2HMACBackend
diff --git a/tests/conftest.py b/tests/conftest.py
index 8e89af5..1ee2a99 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -17,7 +17,8 @@
 
 from cryptography.hazmat.backends import _available_backends
 from cryptography.hazmat.backends.interfaces import (
-    CipherBackend, HMACBackend, HashBackend, PBKDF2HMACBackend, RSABackend
+    CipherBackend, DSABackend, HMACBackend, HashBackend, PBKDF2HMACBackend,
+    RSABackend
 )
 
 from .utils import check_backend_support, check_for_iface, select_backends
@@ -37,6 +38,7 @@
     check_for_iface("cipher", CipherBackend, item)
     check_for_iface("hash", HashBackend, item)
     check_for_iface("pbkdf2hmac", PBKDF2HMACBackend, item)
+    check_for_iface("dsa", DSABackend, item)
     check_for_iface("rsa", RSABackend, item)
     check_backend_support(item)
 
diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py
index f0be72b..f46009d 100644
--- a/tests/hazmat/backends/test_multibackend.py
+++ b/tests/hazmat/backends/test_multibackend.py
@@ -18,7 +18,8 @@
     UnsupportedAlgorithm, _Reasons
 )
 from cryptography.hazmat.backends.interfaces import (
-    CipherBackend, HMACBackend, HashBackend, PBKDF2HMACBackend, RSABackend
+    CipherBackend, DSABackend, HMACBackend, HashBackend, PBKDF2HMACBackend,
+    RSABackend
 )
 from cryptography.hazmat.backends.multibackend import MultiBackend
 from cryptography.hazmat.primitives import hashes, hmac
@@ -98,6 +99,15 @@
         pass
 
 
+@utils.register_interface(DSABackend)
+class DummyDSABackend(object):
+    def generate_dsa_parameters(self, key_size):
+        pass
+
+    def generate_dsa_private_key(self, parameters):
+        pass
+
+
 class TestMultiBackend(object):
     def test_ciphers(self):
         backend = MultiBackend([
@@ -193,3 +203,24 @@
         ):
             backend.create_rsa_verification_ctx(
                 "public_key", "sig", padding.PKCS1v15(), hashes.MD5())
+
+    def test_dsa(self):
+        backend = MultiBackend([
+            DummyDSABackend()
+        ])
+
+        backend.generate_dsa_parameters(key_size=1024)
+
+        parameters = object()
+        backend.generate_dsa_private_key(parameters)
+
+        backend = MultiBackend([])
+        with raises_unsupported_algorithm(
+            _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
+        ):
+            backend.generate_dsa_parameters(key_size=1024)
+
+        with raises_unsupported_algorithm(
+            _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM
+        ):
+            backend.generate_dsa_private_key(parameters)
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index 016da0f..6ab1662 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -21,13 +21,15 @@
 )
 from cryptography.hazmat.backends.openssl.backend import Backend, backend
 from cryptography.hazmat.primitives import hashes, interfaces
-from cryptography.hazmat.primitives.asymmetric import padding, rsa
+from cryptography.hazmat.primitives.asymmetric import dsa, padding, rsa
 from cryptography.hazmat.primitives.ciphers import Cipher
 from cryptography.hazmat.primitives.ciphers.algorithms import AES
 from cryptography.hazmat.primitives.ciphers.modes import CBC
 
 from ...utils import raises_unsupported_algorithm
 
+from cryptography.utils import bit_length
+
 
 @utils.register_interface(interfaces.Mode)
 class DummyMode(object):
@@ -192,6 +194,27 @@
         res = backend._lib.ENGINE_free(e)
         assert res == 1
 
+    @pytest.mark.skipif(
+        backend._lib.OPENSSL_VERSION_NUMBER >= 0x1000000f,
+        reason="Requires an older OpenSSL. Must be < 1.0.0"
+    )
+    def test_large_key_size_on_old_openssl(self):
+        with pytest.raises(ValueError):
+            dsa.DSAParameters.generate(2048, backend=backend)
+
+        with pytest.raises(ValueError):
+            dsa.DSAParameters.generate(3072, backend=backend)
+
+    @pytest.mark.skipif(
+        backend._lib.OPENSSL_VERSION_NUMBER < 0x1000000f,
+        reason="Requires a newer OpenSSL. Must be >= 1.0.0"
+    )
+    def test_large_key_size_on_new_openssl(self):
+        parameters = dsa.DSAParameters.generate(2048, backend)
+        assert bit_length(parameters.p) == 2048
+        parameters = dsa.DSAParameters.generate(3072, backend)
+        assert bit_length(parameters.p) == 3072
+
 
 class TestOpenSSLRandomEngine(object):
     def teardown_method(self, method):
diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py
index 2a2b9bd..2b5d4bb 100644
--- a/tests/hazmat/primitives/test_dsa.py
+++ b/tests/hazmat/primitives/test_dsa.py
@@ -14,9 +14,18 @@
 
 from __future__ import absolute_import, division, print_function
 
+import os
+
 import pytest
 
+from cryptography.exceptions import _Reasons
 from cryptography.hazmat.primitives.asymmetric import dsa
+from cryptography.utils import bit_length
+
+from ...utils import (
+    load_vectors_from_file, load_fips_dsa_key_pair_vectors,
+    raises_unsupported_algorithm
+)
 
 
 def _check_dsa_private_key(skey):
@@ -53,6 +62,7 @@
     assert skey_parameters.generator == pkey_parameters.generator
 
 
+@pytest.mark.dsa
 class TestDSA(object):
     _parameters_1024 = {
         'p': 'd38311e2cd388c3ed698e82fdf88eb92b5a9a483dc88005d4b725ef341eabb47'
@@ -157,6 +167,35 @@
         'f90f7dff6d2bae'
     }
 
+    def test_generate_dsa_parameters(self, backend):
+        parameters = dsa.DSAParameters.generate(1024, backend)
+        assert bit_length(parameters.p) == 1024
+
+    def test_generate_invalid_dsa_parameters(self, backend):
+        with pytest.raises(ValueError):
+            dsa.DSAParameters.generate(1, backend)
+
+    @pytest.mark.parametrize(
+        "vector",
+        load_vectors_from_file(
+            os.path.join(
+                "asymmetric", "DSA", "FIPS_186-3", "KeyPair.rsp"),
+            load_fips_dsa_key_pair_vectors
+        )
+    )
+    def test_generate_dsa_keys(self, vector, backend):
+        parameters = dsa.DSAParameters(modulus=vector['p'],
+                                       subgroup_order=vector['q'],
+                                       generator=vector['g'])
+        skey = dsa.DSAPrivateKey.generate(parameters, backend)
+
+        skey_parameters = skey.parameters()
+        assert skey_parameters.p == vector['p']
+        assert skey_parameters.q == vector['q']
+        assert skey_parameters.g == vector['g']
+        assert skey.key_size == bit_length(vector['p'])
+        assert skey.y == pow(skey_parameters.g, skey.x, skey_parameters.p)
+
     def test_invalid_parameters_argument_types(self):
         with pytest.raises(TypeError):
             dsa.DSAParameters(None, None, None)
@@ -679,3 +718,14 @@
                 generator=int(self._parameters_1024['g'], 16),
                 y=None
             )
+
+
+def test_dsa_generate_invalid_backend():
+    pretend_backend = object()
+
+    with raises_unsupported_algorithm(_Reasons.BACKEND_MISSING_INTERFACE):
+        dsa.DSAParameters.generate(1024, pretend_backend)
+
+    pretend_parameters = object()
+    with raises_unsupported_algorithm(_Reasons.BACKEND_MISSING_INTERFACE):
+        dsa.DSAPrivateKey.generate(pretend_parameters, pretend_backend)