Repository: pyca/cryptography

Issue:
Numerous test failures in test_dsa.py
Hi,

While trying to run the test suite for cryptography, I see many errors in test_dsa (180), e.g.:

```
_________ TestDSAVerification.test_dsa_verification[backend0-vector45] _________

self = <tests.hazmat.primitives.test_dsa.TestDSAVerification object at 0xab3ba10>
vector = {'digest_algorithm': 'SHA-384', 'g': 654006831184636975...002302089630032617L, 'msg': '\x07\xb8\x89g%`D-8\x17w\r\xdf\x...x17\x8c\x88Q\xbe\x18\xb3-\xc1\xd8;\xb7\xb5\xf5\xaam9[\x1f\xd7\xa7', 'p': 128486996923988273...380007425585661467L, ...}
backend = <cryptography.hazmat.backends.openssl.backend.Backend object at 0xa2b7910>

    @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.DSAPublicNumbers(
            parameter_numbers=dsa.DSAParameterNumbers(
                vector['p'], vector['q'], vector['g']
            ),
            y=vector['y']
        ).public_key(backend)
        sig = der_encode_dsa_signature(vector['r'], vector['s'])
        verifier = public_key.verifier(sig, algorithm())
        verifier.update(vector['msg'])
        if vector['result'] == "F":
            with pytest.raises(InvalidSignature):
                verifier.verify()
        else:
>           verifier.verify()

recipes/cryptography-0.6/tests/tests/hazmat/primitives/test_dsa.py:698: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cryptography.hazmat.backends.openssl.dsa._DSAVerificationContext object at 0xab3bb90>

    def verify(self):
        self._dsa_cdata = self._backend._ffi.gc(self._public_key._dsa_cdata,
                                                self._backend._lib.DSA_free)

        data_to_verify = self._hash_ctx.finalize()

        # 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._public_key._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
E               assert 10 == 13
E                +  where 10 = 10
E                +    where 10 = _OpenSSLError(code=168235108L, lib=10, func=113, reason=100).lib
E                +  and   13 = 13
E                +    where 13 = <cffi.vengine_cpy.FFILibrary object at 0xb616690>.ERR_LIB_ASN1
E                +      where <cffi.vengine_cpy.FFILibrary object at 0xb616690> = <cffi.vengine_cpy.FFILibrary object at 0xb616690>
E                +        where <cffi.vengine_cpy.FFILibrary object at 0xb616690> = <cryptography.hazmat.backends.openssl.backend.Backend object at 0xa2b7910>._lib
E                +          where <cryptography.hazmat.backends.openssl.backend.Backend object at 0xa2b7910> = <cryptography.hazmat.backends.openssl.backend.Backend object at 0xa2b7910>
E                +            where <cryptography.hazmat.backends.openssl.backend.Backend object at 0xa2b7910> = <cryptography.hazmat.backends.openssl.dsa._DSAVerificationContext object at 0xab3bb90>._backend

../master-env/lib/python2.7/site-packages/cryptography/hazmat/backends/openssl/dsa.py:54: AssertionError
```

I do not see errors running the full testsuite outside of test_dsa.

I am using python 2.7.3, py 1.4.25, pytest 2.6.3, pyasn1 0.1.7. The SSL version is 1.0.0.b:

```
>>> ssl.OPENSSL_VERSION_INFO
(1, 0, 0, 2, 15)
>>> ssl.OPENSSL_VERSION
'OpenSSL 1.0.0b 16 Nov 2010'
```

I cannot easily upgrade the SSL version.



Discussion:
What's your OS and OpenSSL package version?

It is rhel 5.8, using openssl 1.0.0b. The latter was built as follows:

```
  ./config shared && make && make install
```

I cannot easily change the openssl version (but can do as many custom builds/tests as needed on my own VM).

168235108 corresponds to "error:0A071064:dsa routines:DSA_do_verify:data too large for key size".

Not sure why that'd be happening, but I'll look into this more.

I suspect this is due to the size of the signature (a SHA384 signature of 48 bytes is too big for a 1024-bit DSA key). In OpenSSL 0.9.8 we don't support this combo, but our code assumes anything >=1.0.0 can do it. In the signer in 1.0.1 here's what we see:

```
    if (dlen > BN_num_bytes(dsa->q))
        /* if the digest length is greater than the size of q use the
         * BN_num_bits(dsa->q) leftmost bits of the digest, see
         * fips 186-3, 4.2 */
```

This automatic truncation is not present in 1.0.0b (it is present in 1.0.0m but I haven't looked to see exactly where it was added). Instead this is present

```
    /* reject a excessive digest length (currently at most
     * dsa-with-SHA256 is supported) */
    if (dlen > SHA256_DIGEST_LENGTH)
```

DSA*

On Thu, Oct 2, 2014 at 9:53 AM, Alex Gaynor alex.gaynor@gmail.com wrote:

> For ECDSA we do the truncation ourselves, IIRC, should we be doing it for
> RSA?
> 
> On Thu, Oct 2, 2014 at 9:51 AM, Paul Kehrer notifications@github.com
> wrote:
> 
> > I suspect this is due to the size of the signature (a SHA384 signature of
> > 48 bytes is too big for a 1024-bit DSA key). In OpenSSL 0.9.8 we don't
> > support this combo, but our code assumes anything >=1.0.0 can do it. In the
> > signer in 1.0.1 here's what we see:
> > 
> > ```
> > if (dlen > BN_num_bytes(dsa->q))
> >     /* if the digest length is greater than the size of q use the
> >      * BN_num_bits(dsa->q) leftmost bits of the digest, see
> >      * fips 186-3, 4.2 */
> > ```
> > 
> > This automatic truncation is not present in 1.0.0b (it is present in
> > 1.0.0m but I haven't looked to see exactly where it was added). Instead
> > this is present
> > 
> > ```
> > /* reject a excessive digest length (currently at most
> >  * dsa-with-SHA256 is supported) */
> > if (dlen > SHA256_DIGEST_LENGTH)
> > ```
> > 
> > —
> > Reply to this email directly or view it on GitHub
> > https://github.com/pyca/cryptography/issues/1382#issuecomment-57660371.
> 
> ## 
> 
> "I disapprove of what you say, but I will defend to the death your right
> to say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
> "The people's good is the highest law." -- Cicero
> GPG Key fingerprint: 125F 5C67 DFE9 4084

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

For ECDSA we do the truncation ourselves, IIRC, should we be doing it for
RSA?

On Thu, Oct 2, 2014 at 9:51 AM, Paul Kehrer notifications@github.com
wrote:

> I suspect this is due to the size of the signature (a SHA384 signature of
> 48 bytes is too big for a 1024-bit DSA key). In OpenSSL 0.9.8 we don't
> support this combo, but our code assumes anything >=1.0.0 can do it. In the
> signer in 1.0.1 here's what we see:
> 
> ```
> if (dlen > BN_num_bytes(dsa->q))
>     /* if the digest length is greater than the size of q use the
>      * BN_num_bits(dsa->q) leftmost bits of the digest, see
>      * fips 186-3, 4.2 */
> ```
> 
> This automatic truncation is not present in 1.0.0b (it is present in
> 1.0.0m but I haven't looked to see exactly where it was added). Instead
> this is present
> 
> ```
> /* reject a excessive digest length (currently at most
>  * dsa-with-SHA256 is supported) */
> if (dlen > SHA256_DIGEST_LENGTH)
> ```
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/1382#issuecomment-57660371.

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

I think we probably will need to do so, yes.

Let me know if there anything I can do on my end. I would be happy to do some testing, or making my environment available.

Support for digest truncation per FIPS 186-3 was added in 1.0.0c (https://github.com/openssl/openssl/blob/OpenSSL_1_0_0c/crypto/dsa/dsa_ossl.c#L177). So there are 3 releases (1.0.0, 1.0.0a, and 1.0.0b) subject to this issue. :angry:


Resolving patch:
diff --git a/cryptography/hazmat/backends/openssl/dsa.py b/cryptography/hazmat/backends/openssl/dsa.py
--- a/cryptography/hazmat/backends/openssl/dsa.py
+++ b/cryptography/hazmat/backends/openssl/dsa.py
@@ -15,6 +15,7 @@
 
 from cryptography import utils
 from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.backends.openssl.utils import _truncate_digest
 from cryptography.hazmat.primitives import hashes, interfaces
 from cryptography.hazmat.primitives.asymmetric import dsa
 from cryptography.hazmat.primitives.interfaces import (
@@ -22,6 +23,19 @@
 )
 
 
+def _truncate_digest_for_dsa(dsa_cdata, digest, backend):
+    """
+    This function truncates digests that are longer than a given DS
+    key's length so they can be signed. OpenSSL does this for us in
+    1.0.0c+ and it isn't needed in 0.9.8, but that leaves us with three
+    releases (1.0.0, 1.0.0a, and 1.0.0b) where this is a problem. This
+    truncation is not required in 0.9.8 because DSA is limited to SHA-1.
+    """
+
+    order_bits = backend._lib.BN_num_bits(dsa_cdata.q)
+    return _truncate_digest(digest, order_bits)
+
+
 @utils.register_interface(interfaces.AsymmetricVerificationContext)
 class _DSAVerificationContext(object):
     def __init__(self, backend, public_key, signature, algorithm):
@@ -41,6 +55,10 @@ def verify(self):
 
         data_to_verify = self._hash_ctx.finalize()
 
+        data_to_verify = _truncate_digest_for_dsa(
+            self._dsa_cdata, data_to_verify, self._backend
+        )
+
         # The first parameter passed to DSA_verify is unused by OpenSSL but
         # must be an integer.
         res = self._backend._lib.DSA_verify(
@@ -69,6 +87,9 @@ def update(self, data):
 
     def finalize(self):
         data_to_sign = self._hash_ctx.finalize()
+        data_to_sign = _truncate_digest_for_dsa(
+            self._private_key._dsa_cdata, data_to_sign, self._backend
+        )
         sig_buf_len = self._backend._lib.DSA_size(self._private_key._dsa_cdata)
         sig_buf = self._backend._ffi.new("unsigned char[]", sig_buf_len)
         buflen = self._backend._ffi.new("unsigned int *")
diff --git a/cryptography/hazmat/backends/openssl/ec.py b/cryptography/hazmat/backends/openssl/ec.py
--- a/cryptography/hazmat/backends/openssl/ec.py
+++ b/cryptography/hazmat/backends/openssl/ec.py
@@ -13,12 +13,11 @@
 
 from __future__ import absolute_import, division, print_function
 
-import six
-
 from cryptography import utils
 from cryptography.exceptions import (
     InvalidSignature, UnsupportedAlgorithm, _Reasons
 )
+from cryptography.hazmat.backends.openssl.utils import _truncate_digest
 from cryptography.hazmat.primitives import hashes, interfaces
 from cryptography.hazmat.primitives.asymmetric import ec
 
@@ -34,8 +33,6 @@ def _truncate_digest_for_ecdsa(ec_key_cdata, digest, backend):
     _lib = backend._lib
     _ffi = backend._ffi
 
-    digest_len = len(digest)
-
     group = _lib.EC_KEY_get0_group(ec_key_cdata)
 
     with backend._tmp_bn_ctx() as bn_ctx:
@@ -47,20 +44,7 @@ def _truncate_digest_for_ecdsa(ec_key_cdata, digest, backend):
 
         order_bits = _lib.BN_num_bits(order)
 
-    if 8 * digest_len > order_bits:
-        digest_len = (order_bits + 7) // 8
-        digest = digest[:digest_len]
-
-    if 8 * digest_len > order_bits:
-        rshift = 8 - (order_bits & 0x7)
-        assert rshift > 0 and rshift < 8
-
-        mask = 0xFF >> rshift << rshift
-
-        # Set the bottom rshift bits to 0
-        digest = digest[:-1] + six.int2byte(six.indexbytes(digest, -1) & mask)
-
-    return digest
+    return _truncate_digest(digest, order_bits)
 
 
 def _ec_key_curve_sn(backend, ec_key):
diff --git a/cryptography/hazmat/backends/openssl/utils.py b/cryptography/hazmat/backends/openssl/utils.py
new file mode 100644
--- /dev/null
+++ b/cryptography/hazmat/backends/openssl/utils.py
@@ -0,0 +1,35 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import, division, print_function
+
+import six
+
+
+def _truncate_digest(digest, order_bits):
+    digest_len = len(digest)
+
+    if 8 * digest_len > order_bits:
+        digest_len = (order_bits + 7) // 8
+        digest = digest[:digest_len]
+
+    if 8 * digest_len > order_bits:
+        rshift = 8 - (order_bits & 0x7)
+        assert rshift > 0 and rshift < 8
+
+        mask = 0xFF >> rshift << rshift
+
+        # Set the bottom rshift bits to 0
+        digest = digest[:-1] + six.int2byte(six.indexbytes(digest, -1) & mask)
+
+    return digest


Test patch: