Merge pull request #982 from reaperhulk/dsa-verify

DSA verification
diff --git a/cryptography/hazmat/backends/interfaces.py b/cryptography/hazmat/backends/interfaces.py
index aaaca5e..e63b079 100644
--- a/cryptography/hazmat/backends/interfaces.py
+++ b/cryptography/hazmat/backends/interfaces.py
@@ -145,6 +145,25 @@
         a DSAParameters object.
         """
 
+    @abc.abstractmethod
+    def create_dsa_verification_ctx(self, public_key, signature, algorithm):
+        """
+        Returns an object conforming to the AsymmetricVerificationContext
+        interface.
+        """
+
+    @abc.abstractmethod
+    def dsa_hash_supported(self, algorithm):
+        """
+        Return True if the hash algorithm is supported by the backend for DSA.
+        """
+
+    @abc.abstractmethod
+    def dsa_parameters_supported(self, p, q, g):
+        """
+        Return True if the parameters are supported by the backend for DSA.
+        """
+
 
 @six.add_metaclass(abc.ABCMeta)
 class TraditionalOpenSSLSerializationBackend(object):
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py
index f9154f3..37deb2a 100644
--- a/cryptography/hazmat/backends/openssl/backend.py
+++ b/cryptography/hazmat/backends/openssl/backend.py
@@ -474,6 +474,35 @@
             y=self._bn_to_int(ctx.pub_key)
         )
 
+    def create_dsa_verification_ctx(self, public_key, signature,
+                                    algorithm):
+        return _DSAVerificationContext(self, public_key, signature,
+                                       algorithm)
+
+    def _dsa_cdata_from_public_key(self, public_key):
+        # Does not GC the DSA cdata. You *must* make sure it's freed
+        # correctly yourself!
+        ctx = self._lib.DSA_new()
+        assert ctx != self._ffi.NULL
+        parameters = public_key.parameters()
+        ctx.p = self._int_to_bn(parameters.p)
+        ctx.q = self._int_to_bn(parameters.q)
+        ctx.g = self._int_to_bn(parameters.g)
+        ctx.pub_key = self._int_to_bn(public_key.y)
+        return ctx
+
+    def dsa_hash_supported(self, algorithm):
+        if self._lib.OPENSSL_VERSION_NUMBER < 0x1000000f:
+            return isinstance(algorithm, hashes.SHA1)
+        else:
+            return self.hash_supported(algorithm)
+
+    def dsa_parameters_supported(self, p, q, g):
+        if self._lib.OPENSSL_VERSION_NUMBER < 0x1000000f:
+            return (utils.bit_length(p) <= 1024 and utils.bit_length(q) <= 160)
+        else:
+            return True
+
     def decrypt_rsa(self, private_key, ciphertext, padding):
         key_size_bytes = int(math.ceil(private_key.key_size / 8.0))
         if key_size_bytes != len(ciphertext):
@@ -1297,6 +1326,49 @@
             raise InvalidSignature
 
 
+@utils.register_interface(interfaces.AsymmetricVerificationContext)
+class _DSAVerificationContext(object):
+    def __init__(self, backend, public_key, signature, algorithm):
+        self._backend = backend
+        self._public_key = public_key
+        self._signature = signature
+        self._algorithm = algorithm
+
+        self._hash_ctx = _HashContext(backend, self._algorithm)
+
+    def update(self, data):
+        if self._hash_ctx is None:
+            raise AlreadyFinalized("Context has already been finalized")
+
+        self._hash_ctx.update(data)
+
+    def verify(self):
+        if self._hash_ctx is None:
+            raise AlreadyFinalized("Context has already been finalized")
+
+        self._dsa_cdata = self._backend._dsa_cdata_from_public_key(
+            self._public_key)
+        self._dsa_cdata = self._backend._ffi.gc(self._dsa_cdata,
+                                                self._backend._lib.DSA_free)
+
+        data_to_verify = self._hash_ctx.finalize()
+        self._hash_ctx = None
+
+        # The first parameter passed to DSA_verify is unused by OpenSSL but
+        # must be an integer.
+        res = self._backend._lib.DSA_verify(
+            0, data_to_verify, len(data_to_verify), self._signature,
+            len(self._signature), self._dsa_cdata)
+
+        if res != 1:
+            errors = self._backend._consume_errors()
+            assert errors
+            if res == -1:
+                assert errors[0].lib == self._backend._lib.ERR_LIB_ASN1
+
+            raise InvalidSignature
+
+
 @utils.register_interface(interfaces.CMACContext)
 class _CMACContext(object):
     def __init__(self, backend, algorithm, ctx=None):
diff --git a/cryptography/hazmat/primitives/asymmetric/dsa.py b/cryptography/hazmat/primitives/asymmetric/dsa.py
index 4c2de36..57a7ef3 100644
--- a/cryptography/hazmat/primitives/asymmetric/dsa.py
+++ b/cryptography/hazmat/primitives/asymmetric/dsa.py
@@ -151,6 +151,16 @@
         self._generator = generator
         self._y = y
 
+    def verifier(self, signature, algorithm, backend):
+        if not isinstance(backend, DSABackend):
+            raise UnsupportedAlgorithm(
+                "Backend object does not implement DSABackend",
+                _Reasons.BACKEND_MISSING_INTERFACE
+            )
+
+        return backend.create_dsa_verification_ctx(self, signature,
+                                                   algorithm)
+
     @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 2f63f3e..6833f22 100644
--- a/docs/hazmat/backends/interfaces.rst
+++ b/docs/hazmat/backends/interfaces.rst
@@ -345,6 +345,42 @@
             1.0.0 and the key size is larger than 1024; older OpenSSL versions
             do not support keys larger than 1024 bits.
 
+    .. method:: create_dsa_verification_ctx(public_key, signature, algorithm)
+
+        :param public_key: An instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.DSAPublicKey`
+            provider.
+
+        :param bytes signature: The signature to verify. DER encoded as
+            specified in :rfc:`6979`.
+
+        :param algorithm: An instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm`
+            provider.
+
+        :returns:
+            :class:`~cryptography.hazmat.primitives.interfaces.AsymmetricVerificationContext`
+
+    .. method:: dsa_hash_supported(algorithm):
+
+        :param algorithm: An instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm`
+            provider.
+
+        :returns: ``True`` if the specified ``algorithm`` is supported by this
+            backend, otherwise ``False``.
+
+    .. method:: dsa_parameters_supported(p, q, g):
+
+        :param int p: The p value of a DSA key.
+
+        :param int q: The q value of a DSA key.
+
+        :param int g: The g value of a DSA key.
+
+        :returns: ``True`` if the given values of ``p``, ``q``, and ``g`` are
+            supported by this backend, otherwise ``False``.
+
 
 .. class:: CMACBackend
 
diff --git a/docs/hazmat/backends/openssl.rst b/docs/hazmat/backends/openssl.rst
index 43e5d8f..f15bc28 100644
--- a/docs/hazmat/backends/openssl.rst
+++ b/docs/hazmat/backends/openssl.rst
@@ -14,6 +14,7 @@
     It implements the following interfaces:
 
     * :class:`~cryptography.hazmat.backends.interfaces.CipherBackend`
+    * :class:`~cryptography.hazmat.backends.interfaces.DSABackend`
     * :class:`~cryptography.hazmat.backends.interfaces.HashBackend`
     * :class:`~cryptography.hazmat.backends.interfaces.HMACBackend`
     * :class:`~cryptography.hazmat.backends.interfaces.PBKDF2HMACBackend`
diff --git a/docs/hazmat/primitives/asymmetric/dsa.rst b/docs/hazmat/primitives/asymmetric/dsa.rst
index 2819bbd..03e476b 100644
--- a/docs/hazmat/primitives/asymmetric/dsa.rst
+++ b/docs/hazmat/primitives/asymmetric/dsa.rst
@@ -118,6 +118,55 @@
                         ``subgroup_order``, ``generator``, or ``y``
                         do not match the bounds specified in `FIPS 186-4`_.
 
+    .. method:: verifier(signature, algorithm, backend)
+
+        .. versionadded:: 0.4
+
+        Verify data was signed by the private key associated with this public
+        key.
+
+        .. code-block:: pycon
+
+            >>> from cryptography.hazmat.backends import default_backend
+            >>> from cryptography.hazmat.primitives import hashes
+            >>> from cryptography.hazmat.primitives.asymmetric import dsa
+            >>> parameters = dsa.DSAParameters.generate(
+            ...     key_size=1024,
+            ...     backend=default_backend()
+            ... )
+            >>> private_key = dsa.DSAPrivateKey.generate(
+            ...     parameters=parameters,
+            ...     backend=default_backend()
+            ... )
+            >>> signer = private_key.signer(
+            ...     hashes.SHA256(),
+            ...     default_backend()
+            ... )
+            >>> data = b"this is some data I'd like to sign"
+            >>> signer.update(data)
+            >>> signature = signer.finalize()
+            >>> public_key = private_key.public_key()
+            >>> verifier = public_key.verifier(
+            ...     signature,
+            ...     hashes.SHA256(),
+            ...     default_backend()
+            ... )
+            >>> verifier.update(data)
+            >>> verifier.verify()
+
+        :param bytes signature: The signature to verify. DER encoded as
+            specified in :rfc:`6979`.
+
+        :param algorithm: An instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm`
+            provider.
+
+        :param backend: A
+            :class:`~cryptography.hazmat.backends.interfaces.DSABackend`
+            provider.
+
+        :returns:
+            :class:`~cryptography.hazmat.primitives.interfaces.AsymmetricVerificationContext`
 
 .. _`DSA`: https://en.wikipedia.org/wiki/Digital_Signature_Algorithm
 .. _`public-key`: https://en.wikipedia.org/wiki/Public-key_cryptography
diff --git a/docs/hazmat/primitives/interfaces.rst b/docs/hazmat/primitives/interfaces.rst
index c76582c..feafe94 100644
--- a/docs/hazmat/primitives/interfaces.rst
+++ b/docs/hazmat/primitives/interfaces.rst
@@ -412,17 +412,38 @@
 
         The bit length of the modulus.
 
+    .. attribute:: y
+
+        :type: int
+
+        The public key.
+
     .. method:: parameters()
 
         :return: :class:`~cryptography.hazmat.primitives.interfaces.DSAParameters`
 
         The DSAParameters object associated with this public key.
 
-    .. attribute:: y
+    .. method:: verifier(signature, algorithm, backend)
 
-        :type: int
+        .. versionadded:: 0.4
 
-        The public key.
+        Verify data was signed by the private key associated with this public
+        key.
+
+        :param bytes signature: The signature to verify. DER encoded as
+            specified in :rfc:`6979`.
+
+        :param algorithm: An instance of a
+            :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm`
+            provider.
+
+        :param backend: A
+            :class:`~cryptography.hazmat.backends.interfaces.DSABackend`
+            provider.
+
+        :returns:
+            :class:`~cryptography.hazmat.primitives.interfaces.AsymmetricVerificationContext`
 
 
 .. class:: AsymmetricSignatureContext
diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py
index bc3b1db..4c3cd58 100644
--- a/tests/hazmat/primitives/test_dsa.py
+++ b/tests/hazmat/primitives/test_dsa.py
@@ -18,12 +18,15 @@
 
 import pytest
 
-from cryptography.exceptions import _Reasons
+from cryptography.exceptions import (
+    AlreadyFinalized, InvalidSignature, _Reasons)
+from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.asymmetric import dsa
 from cryptography.utils import bit_length
 
 from ...utils import (
-    load_fips_dsa_key_pair_vectors, load_vectors_from_file,
+    der_encode_dsa_signature, load_fips_dsa_key_pair_vectors,
+    load_fips_dsa_sig_vectors, load_vectors_from_file,
     raises_unsupported_algorithm
 )
 
@@ -720,12 +723,89 @@
             )
 
 
+@pytest.mark.dsa
+class TestDSAVerification(object):
+    _algorithms_dict = {
+        'SHA1': hashes.SHA1,
+        'SHA224': hashes.SHA224,
+        'SHA256': hashes.SHA256,
+        'SHA384': hashes.SHA384,
+        'SHA512': hashes.SHA512
+    }
+
+    @pytest.mark.parametrize(
+        "vector",
+        load_vectors_from_file(
+            os.path.join(
+                "asymmetric", "DSA", "FIPS_186-3", "SigVer.rsp"),
+            load_fips_dsa_sig_vectors
+        )
+    )
+    def test_dsa_verification(self, vector, backend):
+        digest_algorithm = vector['digest_algorithm'].replace("-", "")
+        algorithm = self._algorithms_dict[digest_algorithm]
+        if (
+            not backend.dsa_parameters_supported(
+                vector['p'], vector['q'], vector['g']
+            ) or not backend.dsa_hash_supported(algorithm)
+        ):
+            pytest.skip(
+                "{0} does not support the provided parameters".format(backend)
+            )
+
+        public_key = dsa.DSAPublicKey(
+            vector['p'], vector['q'], vector['g'], vector['y']
+        )
+        sig = der_encode_dsa_signature(vector['r'], vector['s'])
+        verifier = public_key.verifier(sig, algorithm(), backend)
+        verifier.update(vector['msg'])
+        if vector['result'] == "F":
+            with pytest.raises(InvalidSignature):
+                verifier.verify()
+        else:
+            verifier.verify()
+
+    def test_dsa_verify_invalid_asn1(self, backend):
+        parameters = dsa.DSAParameters.generate(1024, backend)
+        private_key = dsa.DSAPrivateKey.generate(parameters, backend)
+        public_key = private_key.public_key()
+        verifier = public_key.verifier(b'fakesig', hashes.SHA1(), backend)
+        verifier.update(b'fakesig')
+        with pytest.raises(InvalidSignature):
+            verifier.verify()
+
+    def test_use_after_finalize(self, backend):
+        parameters = dsa.DSAParameters.generate(1024, backend)
+        private_key = dsa.DSAPrivateKey.generate(parameters, backend)
+        public_key = private_key.public_key()
+        verifier = public_key.verifier(b'fakesig', hashes.SHA1(), backend)
+        verifier.update(b'irrelevant')
+        with pytest.raises(InvalidSignature):
+            verifier.verify()
+        with pytest.raises(AlreadyFinalized):
+            verifier.verify()
+        with pytest.raises(AlreadyFinalized):
+            verifier.update(b"more data")
+
+    def test_dsa_verifier_invalid_backend(self, backend):
+        pretend_backend = object()
+        params = dsa.DSAParameters.generate(1024, backend)
+        private_key = dsa.DSAPrivateKey.generate(params, backend)
+        public_key = private_key.public_key()
+
+        with raises_unsupported_algorithm(
+                _Reasons.BACKEND_MISSING_INTERFACE):
+            public_key.verifier(b"sig", hashes.SHA1(), pretend_backend)
+
+
 def test_dsa_generate_invalid_backend():
     pretend_backend = object()
 
-    with raises_unsupported_algorithm(_Reasons.BACKEND_MISSING_INTERFACE):
+    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):
+    with raises_unsupported_algorithm(
+            _Reasons.BACKEND_MISSING_INTERFACE):
         dsa.DSAPrivateKey.generate(pretend_parameters, pretend_backend)