Repository: pyca/cryptography

Issue:
Clearer error message for unsupported hashes in x509 signing
Hey!
I am one of the maintainers of [pymobiledevice3](https://github.com/doronz88/pymobiledevice3) - a pure python package to interact with iDevices.

First of all, I want to thank you for your amazing package.

Second, we recently faced an issue with creating certificates for communication with devices (a generic ValueError arises). After some debugging, we understood that it is related to the drop of SHA1 hash when building a certificate - https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/ca.py#L20

We would really appreciate your help with the following:
- Is there any way to keep using your package with SHA1 hashes?
- Could you please raise a more specific exception than ValueError?
- Do you have any suggestion on the subject?

(Could you please revert the changes? 😅)

Thanks a lot!


Discussion:
No, we no longer support SHA1 signatures because they cannot be used securely. SHA1 is broken for cryptographic purposes.

Our exception contains a pretty explicit message: `"Hash algorithm sha1 not supported for signatures"`

We will not be reverting the change.
I understand that SHA1 is not secure, but some old devices requires it so I thought it might be a good idea to ask you for suggestions ¯\_(ツ)_/¯
Anyway, catching an error by its message is quite inconvenient - will you accept a PR that raises more specific error?

Thanks for your fast response!
Do you have an idea of what sort of exception you'd prefer?

If we changed the type signature from `typing.Optional[hashes.HashAlgorithm]` to `typing.Union[SHA256, SHA384, ...]` would that help, do you use mypy?
I would prefer something like the `UnsupportedAlgorithm` or any exception class that subclasses ValueError to something more explicit.

We don't use mypy so changing the typing wouldn't help (and in my humble opinion it goes against the open-closed principle so I don't think its a good idea)
Well, signing can only be implemented for known types, because it requires mapping to OIDs and parameters, so changing the type doesn't restrict what's possible, it just makes it clearer.
This issue has been waiting for a reporter response for 5 days. It will be auto-closed if no activity occurs in the next week.
This issue has not received a reporter response and has been auto-closed. If the issue is still relevant please leave a comment and we can reopen it.
Reopening this because I think we haven't made a call on raising a different error here and that doesn't require reporter input.

Resolving patch:
diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py
--- a/src/cryptography/x509/base.py
+++ b/src/cryptography/x509/base.py
@@ -36,6 +36,19 @@
 
 _EARLIEST_UTC_TIME = datetime.datetime(1950, 1, 1)
 
+# This must be kept in sync with sign.rs's list of allowable types in
+# identify_hash_type
+_AllowedHashTypes = typing.Union[
+    hashes.SHA224,
+    hashes.SHA256,
+    hashes.SHA384,
+    hashes.SHA512,
+    hashes.SHA3_224,
+    hashes.SHA3_256,
+    hashes.SHA3_384,
+    hashes.SHA3_512,
+]
+
 
 class AttributeNotFound(Exception):
     def __init__(self, msg: str, oid: ObjectIdentifier) -> None:
@@ -679,7 +692,7 @@ def add_attribute(
     def sign(
         self,
         private_key: CERTIFICATE_PRIVATE_KEY_TYPES,
-        algorithm: typing.Optional[hashes.HashAlgorithm],
+        algorithm: typing.Optional[_AllowedHashTypes],
         backend: typing.Any = None,
     ) -> CertificateSigningRequest:
         """
@@ -900,7 +913,7 @@ def add_extension(
     def sign(
         self,
         private_key: CERTIFICATE_PRIVATE_KEY_TYPES,
-        algorithm: typing.Optional[hashes.HashAlgorithm],
+        algorithm: typing.Optional[_AllowedHashTypes],
         backend: typing.Any = None,
     ) -> Certificate:
         """
@@ -1047,7 +1060,7 @@ def add_revoked_certificate(
     def sign(
         self,
         private_key: CERTIFICATE_PRIVATE_KEY_TYPES,
-        algorithm: typing.Optional[hashes.HashAlgorithm],
+        algorithm: typing.Optional[_AllowedHashTypes],
         backend: typing.Any = None,
     ) -> CertificateRevocationList:
         if self._issuer_name is None:


Test patch:
diff --git a/tests/x509/test_ocsp.py b/tests/x509/test_ocsp.py
--- a/tests/x509/test_ocsp.py
+++ b/tests/x509/test_ocsp.py
@@ -10,6 +10,7 @@
 import pytest
 
 from cryptography import x509
+from cryptography.exceptions import UnsupportedAlgorithm
 from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa
 from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
@@ -917,7 +918,7 @@ def test_sign_unrecognized_hash_algorithm(self, backend):
             None,
         )
 
-        with pytest.raises(ValueError):
+        with pytest.raises(UnsupportedAlgorithm):
             builder.sign(private_key, hashes.BLAKE2b(digest_size=64))
 
     def test_sign_none_hash_not_eddsa(self):
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -14,7 +14,7 @@
 import pytz
 
 from cryptography import utils, x509
-from cryptography.exceptions import InvalidSignature
+from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
 from cryptography.hazmat.bindings._rust import asn1
 from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import (
@@ -2853,7 +2853,7 @@ def test_sign_dsa_with_unsupported_hash(self, hash_algorithm, backend):
             .not_valid_before(datetime.datetime(2002, 1, 1, 12, 1))
             .not_valid_after(datetime.datetime(2032, 1, 1, 12, 1))
         )
-        with pytest.raises(ValueError):
+        with pytest.raises(UnsupportedAlgorithm):
             builder.sign(private_key, hash_algorithm, backend)
 
     @pytest.mark.supported(
@@ -2876,8 +2876,10 @@ def test_sign_ec_with_md5(self, backend):
             .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)
+        with pytest.raises(UnsupportedAlgorithm):
+            builder.sign(
+                private_key, hashes.MD5(), backend  # type: ignore[arg-type]
+            )
 
     @pytest.mark.supported(
         only_if=lambda backend: backend.dsa_supported(),
diff --git a/tests/x509/test_x509_crlbuilder.py b/tests/x509/test_x509_crlbuilder.py
--- a/tests/x509/test_x509_crlbuilder.py
+++ b/tests/x509/test_x509_crlbuilder.py
@@ -9,6 +9,7 @@
 import pytz
 
 from cryptography import x509
+from cryptography.exceptions import UnsupportedAlgorithm
 from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519
 from cryptography.x509.oid import (
@@ -732,8 +733,10 @@ def test_dsa_key_sign_md5(self, backend):
             .next_update(next_time)
         )
 
-        with pytest.raises(ValueError):
-            builder.sign(private_key, hashes.MD5(), backend)
+        with pytest.raises(UnsupportedAlgorithm):
+            builder.sign(
+                private_key, hashes.MD5(), backend  # type: ignore[arg-type]
+            )
 
     def test_ec_key_sign_md5(self, backend):
         _skip_curve_unsupported(backend, ec.SECP256R1())
@@ -755,8 +758,10 @@ def test_ec_key_sign_md5(self, backend):
             .next_update(next_time)
         )
 
-        with pytest.raises(ValueError):
-            builder.sign(private_key, hashes.MD5(), backend)
+        with pytest.raises(UnsupportedAlgorithm):
+            builder.sign(
+                private_key, hashes.MD5(), backend  # type: ignore[arg-type]
+            )
 
     def test_sign_with_revoked_certificates(self, backend):
         private_key = RSA_KEY_2048.private_key(backend)