Repository: pyca/cryptography

Issue:
x509_builder.sign(ec_key, hashes.MD5()) produces an assertion error from OpenSSL
`'error:100C508A:elliptic curve routines:pkey_ec_ctrl:invalid digest type'`

It should error with a clean python exception, not an `InternalError`.


Discussion:
Does MD5 signing fail on RSA/DSA as well?
Worked fine with RSA. Don't know about DSA, since the code I'm writing has no plans to support it :-)
Confirmed this happens with CSRs too for both ECDSA and DSA. Maybe we should consider just disallowing MD5 entirely.
In fact, PR coming :)
PR disallowing MD5?
yep.

```

        if (isinstance(algorithm, hashes.MD5)):
            raise ValueError("MD5 is not a supported hash algorithm for certs/CSRs")
```
This will break my tests which ensure my x509 validator doesn't accept MD5, but I'll live :-)
(Hot take: maybe we shouldn't support MD5 at all?)
I think it might be reasonable to deprecate it. Users who need MD5 for something like a splaying algorithm can just do `hashlib.md5(data).digest()` after all...
"splaying"? (But yes, the only thing us removing support for really means is removing it as a marker in APIs like x.509, signatures, etc.)

Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -662,6 +662,14 @@ def create_x509_csr(self, builder, private_key, algorithm):
         if not isinstance(algorithm, hashes.HashAlgorithm):
             raise TypeError('Algorithm must be a registered hash algorithm.')
 
+        if (
+            isinstance(algorithm, hashes.MD5) and not
+            isinstance(private_key, rsa.RSAPrivateKey)
+        ):
+            raise ValueError(
+                "MD5 is not a supported hash algorithm for EC/DSA CSRs"
+            )
+
         # Resolve the signature algorithm.
         evp_md = self._lib.EVP_get_digestbyname(
             algorithm.name.encode('ascii')
@@ -731,6 +739,14 @@ def create_x509_certificate(self, builder, private_key, algorithm):
         if not isinstance(algorithm, hashes.HashAlgorithm):
             raise TypeError('Algorithm must be a registered hash algorithm.')
 
+        if (
+            isinstance(algorithm, hashes.MD5) and not
+            isinstance(private_key, rsa.RSAPrivateKey)
+        ):
+            raise ValueError(
+                "MD5 is not a supported hash algorithm for EC/DSA certificates"
+            )
+
         # Resolve the signature algorithm.
         evp_md = self._lib.EVP_get_digestbyname(
             algorithm.name.encode('ascii')
@@ -828,6 +844,14 @@ def create_x509_crl(self, builder, private_key, algorithm):
         if not isinstance(algorithm, hashes.HashAlgorithm):
             raise TypeError('Algorithm must be a registered hash algorithm.')
 
+        if (
+            isinstance(algorithm, hashes.MD5) and not
+            isinstance(private_key, rsa.RSAPrivateKey)
+        ):
+            raise ValueError(
+                "MD5 is not a supported hash algorithm for EC/DSA CRLs"
+            )
+
         evp_md = self._lib.EVP_get_digestbyname(
             algorithm.name.encode('ascii')
         )


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -34,6 +34,7 @@
 )
 
 from .hazmat.primitives.fixtures_dsa import DSA_KEY_2048
+from .hazmat.primitives.fixtures_ec import EC_KEY_SECP256R1
 from .hazmat.primitives.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512
 from .hazmat.primitives.test_ec import _skip_curve_unsupported
 from .utils import load_vectors_from_file
@@ -1933,6 +1934,70 @@ def test_sign_with_unsupported_hash(self, backend):
         with pytest.raises(TypeError):
             builder.sign(private_key, object(), backend)
 
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_sign_rsa_with_md5(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+        builder = x509.CertificateBuilder()
+        builder = builder.subject_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).issuer_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).serial_number(
+            1
+        ).public_key(
+            private_key.public_key()
+        ).not_valid_before(
+            datetime.datetime(2002, 1, 1, 12, 1)
+        ).not_valid_after(
+            datetime.datetime(2032, 1, 1, 12, 1)
+        )
+        cert = builder.sign(private_key, hashes.MD5(), backend)
+        assert isinstance(cert.signature_hash_algorithm, hashes.MD5)
+
+    @pytest.mark.requires_backend_interface(interface=DSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_sign_dsa_with_md5(self, backend):
+        private_key = DSA_KEY_2048.private_key(backend)
+        builder = x509.CertificateBuilder()
+        builder = builder.subject_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).issuer_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).serial_number(
+            1
+        ).public_key(
+            private_key.public_key()
+        ).not_valid_before(
+            datetime.datetime(2002, 1, 1, 12, 1)
+        ).not_valid_after(
+            datetime.datetime(2032, 1, 1, 12, 1)
+        )
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.MD5(), backend)
+
+    @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_sign_ec_with_md5(self, backend):
+        _skip_curve_unsupported(backend, ec.SECP256R1())
+        private_key = EC_KEY_SECP256R1.private_key(backend)
+        builder = x509.CertificateBuilder()
+        builder = builder.subject_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).issuer_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).serial_number(
+            1
+        ).public_key(
+            private_key.public_key()
+        ).not_valid_before(
+            datetime.datetime(2002, 1, 1, 12, 1)
+        ).not_valid_after(
+            datetime.datetime(2032, 1, 1, 12, 1)
+        )
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.MD5(), backend)
+
     @pytest.mark.parametrize(
         "cdp",
         [
@@ -2622,6 +2687,41 @@ def test_sign_invalid_hash_algorithm(self, backend):
         with pytest.raises(TypeError):
             builder.sign(private_key, 'NotAHash', backend)
 
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    def test_sign_rsa_with_md5(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        builder = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA'),
+            ])
+        )
+        request = builder.sign(private_key, hashes.MD5(), backend)
+        assert isinstance(request.signature_hash_algorithm, hashes.MD5)
+
+    @pytest.mark.requires_backend_interface(interface=DSABackend)
+    def test_sign_dsa_with_md5(self, backend):
+        private_key = DSA_KEY_2048.private_key(backend)
+        builder = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA'),
+            ])
+        )
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.MD5(), backend)
+
+    @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+    def test_sign_ec_with_md5(self, backend):
+        _skip_curve_unsupported(backend, ec.SECP256R1())
+        private_key = EC_KEY_SECP256R1.private_key(backend)
+        builder = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA'),
+            ])
+        )
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.MD5(), backend)
+
     @pytest.mark.requires_backend_interface(interface=RSABackend)
     def test_no_subject_name(self, backend):
         private_key = RSA_KEY_2048.private_key(backend)
diff --git a/tests/test_x509_crlbuilder.py b/tests/test_x509_crlbuilder.py
--- a/tests/test_x509_crlbuilder.py
+++ b/tests/test_x509_crlbuilder.py
@@ -19,6 +19,7 @@
 from cryptography.x509.oid import AuthorityInformationAccessOID, NameOID
 
 from .hazmat.primitives.fixtures_dsa import DSA_KEY_2048
+from .hazmat.primitives.fixtures_ec import EC_KEY_SECP256R1
 from .hazmat.primitives.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512
 from .hazmat.primitives.test_ec import _skip_curve_unsupported
 
@@ -390,7 +391,7 @@ def test_sign_dsa_key(self, backend):
 
     @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
     @pytest.mark.requires_backend_interface(interface=X509Backend)
-    def test_sign_ec_key_unsupported(self, backend):
+    def test_sign_ec_key(self, backend):
         _skip_curve_unsupported(backend, ec.SECP256R1())
         private_key = ec.generate_private_key(ec.SECP256R1(), backend)
         invalidity_date = x509.InvalidityDate(
@@ -433,6 +434,37 @@ def test_sign_ec_key_unsupported(self, backend):
         assert ext.critical is False
         assert ext.value == invalidity_date
 
+    @pytest.mark.requires_backend_interface(interface=DSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_dsa_key_sign_md5(self, backend):
+        private_key = DSA_KEY_2048.private_key(backend)
+        last_time = datetime.datetime(2012, 1, 16, 22, 43)
+        next_time = datetime.datetime(2022, 1, 17, 6, 43)
+        builder = x509.CertificateRevocationListBuilder().issuer_name(
+            x509.Name([
+                x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA")
+            ])
+        ).last_update(last_time).next_update(next_time)
+
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.MD5(), backend)
+
+    @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_ec_key_sign_md5(self, backend):
+        _skip_curve_unsupported(backend, ec.SECP256R1())
+        private_key = EC_KEY_SECP256R1.private_key(backend)
+        last_time = datetime.datetime(2012, 1, 16, 22, 43)
+        next_time = datetime.datetime(2022, 1, 17, 6, 43)
+        builder = x509.CertificateRevocationListBuilder().issuer_name(
+            x509.Name([
+                x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA")
+            ])
+        ).last_update(last_time).next_update(next_time)
+
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.MD5(), backend)
+
     @pytest.mark.requires_backend_interface(interface=RSABackend)
     @pytest.mark.requires_backend_interface(interface=X509Backend)
     def test_sign_with_revoked_certificates(self, backend):