Repository: pyca/cryptography

Issue:
Add support for DSA signature as (r, s)
From the mailing list:

```
I recently began looking at adding support for PyCA to my AsyncSSH 
package (http://asyncssh.timeheart.net). It currently uses PyCrypto, 
but I see a lot of potential in being able to directly use OpenSSL and 
other more carefully scrutinized crypto libraries from a security perspective.

In the process of adding this support, I ran into an issue with DSA signing 
and verification. The current implementation in PyCA seems to ASN.1 
encode the signature it produces before returning it, while the SSH 
protocol requires a different encoding to be used of the raw ‘r’ and ’s’ 
values. I’ve worked around this for now by doing my own ASN.1 code of 
the return result before re-encoding it in the form SSH needs, but this is 
somewhat wasteful. I was wondering if it might be possible to provide an 
option or alternate API which would return the raw r & s values as 
integers, for protocols that want to use an encoding other than ASN.1.
```

I agree with this sentiment, as these convenience functions would be helpful for Paramiko and Conch as well. Should we implement this ourselves or consider adding a pyasn1 dependency?



Discussion:
What's the thoughts on the API, some utility that parses the asn.1 and
returns `(r, s)`, or a new method on the `_AsymmetricSignatureContext` that
returns `(r, s)` directly? If it's the latter, I don't even think we need
an ASN.1 parser?

On Sat, Jul 26, 2014 at 4:35 PM, Paul Kehrer notifications@github.com
wrote:

> From the mailing list:
> 
> I recently began looking at adding support for PyCA to my AsyncSSH
> package (http://asyncssh.timeheart.net). It currently uses PyCrypto,
> but I see a lot of potential in being able to directly use OpenSSL and
> other more carefully scrutinized crypto libraries from a security perspective.
> 
> In the process of adding this support, I ran into an issue with DSA signing
> and verification. The current implementation in PyCA seems to ASN.1
> encode the signature it produces before returning it, while the SSH
> protocol requires a different encoding to be used of the raw ‘r’ and ’s’
> values. I’ve worked around this for now by doing my own ASN.1 code of
> the return result before re-encoding it in the form SSH needs, but this is
> somewhat wasteful. I was wondering if it might be possible to provide an
> option or alternate API which would return the raw r & s values as
> integers, for protocols that want to use an encoding other than ASN.1.
> 
> I agree with this sentiment, as these convenience functions would be
> helpful for Paramiko and Conch as well. Should we implement this ourselves
> or consider adding a pyasn1 dependency?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/1285.

## 

"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 was hoping this new call could avoid the ASN.1 encoding step in the first place, so an ASN.1 parser would not be needed in this case and the extra work of encoding & then decoding ASN.1 would be saved.

The OpenSSL backend always returns the signature as asn.1, but we could do the decoding to integer in C if it's a new method on the context.

On Sat, Jul 26, 2014 at 9:26 PM, Ron Frederick notifications@github.com
wrote:

> ## I was hoping this new call could avoid the ASN.1 encoding step in the first place, so an ASN.1 parser would not be needed in this case and the extra work of encoding & then decoding ASN.1 would be saved.
> 
> Reply to this email directly or view it on GitHub:
> https://github.com/pyca/cryptography/issues/1285#issuecomment-50253814

Decoding this tuple is relatively easy. We already have code in the test suite that _encodes_ this tuple to ASN.1. I thought it was originally written without using pyasn1 too @reaperhulk ?

> The OpenSSL backend always returns the signature as asn.1,

It's true that the _backend_ does but OpenSSL will hand you a struct containing r and s as BNs if you ask it properly IIRC so really we might not even need to parse the ASN.1.

As far as how to expose this as an API goes I guess there are a few options.
1. Return str-like instance with extra `r` and `s` attributes.
2. Add a new alternative `finalize` method that returned the tuple.
3. Add a parameter to `finalize`. e.g. `asn1=False` that returned the tuple.
4. A utility method that took the ASN.1 output and split the tuple into `int`s for you.

I guess CommonCrypto probably only has ASN.1 encoded output? That would mean we'd need to write the ASN.1 parsing code ourselves anyway at some point.

I like either option 1 or 3 best here. To avoid problems with things that introspect the type later, it may be safer to go with option 3, though.

Also, I don't think this option will apply in all cases. For instance, the signature returned by RSA is just a raw binary string matching the length of the key without any encoding.

@public The code in the current test suite does use pyasn1, but I have code in the commoncrypto branch that does not require it. I have not, however, written anything to parse, only encode.

It seems to me that if there are limited ways the ASN.1 encoding can apply to cryptographic operations, then using an additional library introduces dependencies and requirements of updates etc. I think that a helper method that returns the two ints as a tuple would be most useful, coupled with an automatic use of a finalize parameter.
TL:DR; Options 3&4 combined IMO.

If the ASN.1 version is staying, then I agree 3+4 seem nicest to me.

Similarly on the verify side, I currently have to DER encode the r and s that I have before I can pass them to the verifier, which is unfortunate.

To accomplish this we can change the `DSAPublicKey` `verifier` method to take r, s or the ASN.1 bytes. For signing I'd prefer to modify `AsymmetricSignatureContext`, possibly by subclassing it to make a `DSASignatureContext`. We can then add a new method that returns (r, s). If we don't add a new method we would need to make `finalize` default to ASN.1 output (as @public suggested). I'm not a big fan of having a default value for a new argument as it starts us down the path of having implicit defaults.


Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@
 
 requirements = [
     CFFI_DEPENDENCY,
+    "pyasn1",
     SIX_DEPENDENCY,
     SETUPTOOLS_DEPENDENCY
 ]
@@ -43,7 +44,6 @@
 # If you add a new dep here you probably need to add it in the tox.ini as well
 test_requirements = [
     "pytest",
-    "pyasn1",
     "pretend",
     "iso8601",
 ]
diff --git a/src/cryptography/hazmat/primitives/asymmetric/utils.py b/src/cryptography/hazmat/primitives/asymmetric/utils.py
new file mode 100644
--- /dev/null
+++ b/src/cryptography/hazmat/primitives/asymmetric/utils.py
@@ -0,0 +1,46 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+from pyasn1.codec.der import decoder, encoder
+from pyasn1.error import PyAsn1Error
+from pyasn1.type import namedtype, univ
+
+import six
+
+
+class _DSSSigValue(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('r', univ.Integer()),
+        namedtype.NamedType('s', univ.Integer())
+    )
+
+
+def decode_rfc6979_signature(signature):
+    try:
+        data, remaining = decoder.decode(signature, asn1Spec=_DSSSigValue())
+    except PyAsn1Error:
+        raise ValueError("Invalid signature data. Unable to decode ASN.1")
+
+    if remaining:
+        raise ValueError(
+            "The signature contains bytes after the end of the ASN.1 sequence."
+        )
+    r = int(data.getComponentByName('r'))
+    s = int(data.getComponentByName('s'))
+    return (r, s)
+
+
+def encode_rfc6979_signature(r, s):
+    if (
+        not isinstance(r, six.integer_types) or
+        not isinstance(s, six.integer_types)
+    ):
+        raise ValueError("Both r and s must be integers")
+
+    sig = _DSSSigValue()
+    sig.setComponentByName('r', r)
+    sig.setComponentByName('s', s)
+    return encoder.encode(sig)


Test patch:
diff --git a/tests/hazmat/primitives/test_asym_utils.py b/tests/hazmat/primitives/test_asym_utils.py
new file mode 100644
--- /dev/null
+++ b/tests/hazmat/primitives/test_asym_utils.py
@@ -0,0 +1,65 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+from cryptography.hazmat.primitives.asymmetric.utils import (
+    decode_rfc6979_signature, encode_rfc6979_signature
+)
+
+
+def test_rfc6979_signature():
+    sig = encode_rfc6979_signature(1, 1)
+    assert sig == b"0\x06\x02\x01\x01\x02\x01\x01"
+    assert decode_rfc6979_signature(sig) == (1, 1)
+
+    r_s1 = (
+        1037234182290683143945502320610861668562885151617,
+        559776156650501990899426031439030258256861634312
+    )
+    sig2 = encode_rfc6979_signature(*r_s1)
+    assert sig2 == (
+        b'0-\x02\x15\x00\xb5\xaf0xg\xfb\x8bT9\x00\x13\xccg\x02\r\xdf\x1f,\x0b'
+        b'\x81\x02\x14b\r;"\xabP1D\x0c>5\xea\xb6\xf4\x81)\x8f\x9e\x9f\x08'
+    )
+    assert decode_rfc6979_signature(sig2) == r_s1
+
+    sig3 = encode_rfc6979_signature(0, 0)
+    assert sig3 == b"0\x06\x02\x01\x00\x02\x01\x00"
+    assert decode_rfc6979_signature(sig3) == (0, 0)
+
+    sig4 = encode_rfc6979_signature(-1, 0)
+    assert sig4 == b"0\x06\x02\x01\xFF\x02\x01\x00"
+    assert decode_rfc6979_signature(sig4) == (-1, 0)
+
+
+def test_encode_rfc6979_non_integer():
+    with pytest.raises(ValueError):
+        encode_rfc6979_signature("h", 3)
+
+    with pytest.raises(ValueError):
+        encode_rfc6979_signature("3", "2")
+
+    with pytest.raises(ValueError):
+        encode_rfc6979_signature(3, "h")
+
+    with pytest.raises(ValueError):
+        encode_rfc6979_signature(3.3, 1.2)
+
+    with pytest.raises(ValueError):
+        encode_rfc6979_signature("hello", "world")
+
+
+def test_decode_rfc6979_trailing_bytes():
+    with pytest.raises(ValueError):
+        decode_rfc6979_signature(b"0\x06\x02\x01\x01\x02\x01\x01\x00\x00\x00")
+
+
+def test_decode_rfc6979_invalid_asn1():
+    with pytest.raises(ValueError):
+        # This byte sequence has an invalid ASN.1 sequence length as well as
+        # an invalid integer length for the second integer.
+        decode_rfc6979_signature(b"0\x07\x02\x01\x01\x02\x02\x01")
diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py
--- a/tests/hazmat/primitives/test_dsa.py
+++ b/tests/hazmat/primitives/test_dsa.py
@@ -12,14 +12,17 @@
 from cryptography.hazmat.backends.interfaces import DSABackend
 from cryptography.hazmat.primitives import hashes, interfaces
 from cryptography.hazmat.primitives.asymmetric import dsa
+from cryptography.hazmat.primitives.asymmetric.utils import (
+    encode_rfc6979_signature
+)
 from cryptography.utils import bit_length
 
 from .fixtures_dsa import (
     DSA_KEY_1024, DSA_KEY_2048, DSA_KEY_3072
 )
 from ...utils import (
-    der_encode_dsa_signature, load_fips_dsa_key_pair_vectors,
-    load_fips_dsa_sig_vectors, load_vectors_from_file,
+    load_fips_dsa_key_pair_vectors, load_fips_dsa_sig_vectors,
+    load_vectors_from_file,
 )
 
 
@@ -557,7 +560,7 @@ def test_dsa_verification(self, vector, backend):
             ),
             y=vector['y']
         ).public_key(backend)
-        sig = der_encode_dsa_signature(vector['r'], vector['s'])
+        sig = encode_rfc6979_signature(vector['r'], vector['s'])
         verifier = public_key.verifier(sig, algorithm())
         verifier.update(vector['msg'])
         if vector['result'] == "F":
diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py
--- a/tests/hazmat/primitives/test_ec.py
+++ b/tests/hazmat/primitives/test_ec.py
@@ -13,11 +13,13 @@
 from cryptography.hazmat.backends.interfaces import EllipticCurveBackend
 from cryptography.hazmat.primitives import hashes, interfaces
 from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric.utils import (
+    encode_rfc6979_signature
+)
 
 from ...utils import (
-    der_encode_dsa_signature, load_fips_ecdsa_key_pair_vectors,
-    load_fips_ecdsa_signing_vectors, load_vectors_from_file,
-    raises_unsupported_algorithm
+    load_fips_ecdsa_key_pair_vectors, load_fips_ecdsa_signing_vectors,
+    load_vectors_from_file, raises_unsupported_algorithm
 )
 
 _HASH_TYPES = {
@@ -305,10 +307,7 @@ def test_signatures(self, backend, vector):
             curve_type()
         ).public_key(backend)
 
-        signature = der_encode_dsa_signature(
-            vector['r'],
-            vector['s']
-        )
+        signature = encode_rfc6979_signature(vector['r'], vector['s'])
 
         verifier = key.verifier(
             signature,
@@ -337,10 +336,7 @@ def test_signature_failures(self, backend, vector):
             curve_type()
         ).public_key(backend)
 
-        signature = der_encode_dsa_signature(
-            vector['r'],
-            vector['s']
-        )
+        signature = encode_rfc6979_signature(vector['r'], vector['s'])
 
         verifier = key.verifier(
             signature,
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -18,7 +18,7 @@
 import cryptography_vectors
 
 from .utils import (
-    check_backend_support, der_encode_dsa_signature, load_cryptrec_vectors,
+    check_backend_support, load_cryptrec_vectors,
     load_fips_dsa_key_pair_vectors, load_fips_dsa_sig_vectors,
     load_fips_ecdsa_key_pair_vectors, load_fips_ecdsa_signing_vectors,
     load_hash_vectors, load_kasvs_dh_vectors, load_nist_vectors,
@@ -110,26 +110,6 @@ def test_check_backend_support_no_backend():
         check_backend_support(item)
 
 
-def test_der_encode_dsa_signature_values():
-    sig = der_encode_dsa_signature(1, 1)
-    assert sig == b"0\x06\x02\x01\x01\x02\x01\x01"
-
-    sig2 = der_encode_dsa_signature(
-        1037234182290683143945502320610861668562885151617,
-        559776156650501990899426031439030258256861634312
-    )
-    assert sig2 == (
-        b'0-\x02\x15\x00\xb5\xaf0xg\xfb\x8bT9\x00\x13\xccg\x02\r\xdf\x1f,\x0b'
-        b'\x81\x02\x14b\r;"\xabP1D\x0c>5\xea\xb6\xf4\x81)\x8f\x9e\x9f\x08'
-    )
-
-    sig3 = der_encode_dsa_signature(0, 0)
-    assert sig3 == b"0\x06\x02\x01\x00\x02\x01\x00"
-
-    sig4 = der_encode_dsa_signature(-1, 0)
-    assert sig4 == b"0\x06\x02\x01\xFF\x02\x01\x00"
-
-
 def test_load_nist_vectors():
     vector_data = textwrap.dedent("""
     # CAVS 11.1
diff --git a/tests/utils.py b/tests/utils.py
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -9,9 +9,6 @@
 import re
 from contextlib import contextmanager
 
-from pyasn1.codec.der import encoder
-from pyasn1.type import namedtype, univ
-
 import pytest
 
 import six
@@ -73,20 +70,6 @@ def raises_unsupported_algorithm(reason):
     assert exc_info.value._reason is reason
 
 
-class _DSSSigValue(univ.Sequence):
-    componentType = namedtype.NamedTypes(
-        namedtype.NamedType('r', univ.Integer()),
-        namedtype.NamedType('s', univ.Integer())
-    )
-
-
-def der_encode_dsa_signature(r, s):
-    sig = _DSSSigValue()
-    sig.setComponentByName('r', r)
-    sig.setComponentByName('s', s)
-    return encoder.encode(sig)
-
-
 def load_vectors_from_file(filename, loader, mode="r"):
     with cryptography_vectors.open_vector_file(filename, mode) as vector_file:
         return loader(vector_file)