diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4b674120644..e7bff9f470d0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -73,6 +73,8 @@ Changelog :class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and :class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to allow encrypting directly into a pre-allocated buffer. +* Added support for PKCS1v15 signing without DigestInfo using + :class:`~cryptography.hazmat.primitives.asymmetric.utils.NoDigestInfo`. .. _v46-0-3: diff --git a/docs/hazmat/primitives/asymmetric/rsa.rst b/docs/hazmat/primitives/asymmetric/rsa.rst index 54190ae2dd38..2112c7d2a0aa 100644 --- a/docs/hazmat/primitives/asymmetric/rsa.rst +++ b/docs/hazmat/primitives/asymmetric/rsa.rst @@ -784,9 +784,10 @@ Key interfaces ``algorithm`` parameters must match the ones used when the signature was created for the recovery to succeed. - The ``algorithm`` parameter can also be set to ``None`` to recover all + The ``algorithm`` parameter can also be set to ``NoDigestInfo`` to recover all the data present in the signature, without regard to its format or the - hash algorithm used for its creation. + hash algorithm used for its creation. (Note that setting ``algorithm`` to ``None` + is deprecated and have the same semantic as setting ``NoDigestInfo``.) For :class:`~cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15` diff --git a/docs/hazmat/primitives/asymmetric/utils.rst b/docs/hazmat/primitives/asymmetric/utils.rst index 487926e91256..9dc3b1098098 100644 --- a/docs/hazmat/primitives/asymmetric/utils.rst +++ b/docs/hazmat/primitives/asymmetric/utils.rst @@ -29,6 +29,19 @@ Asymmetric Utilities :return bytes: The encoded signature. +.. class:: NoDigestInfo() + + .. versionadded:: 47.0.0 + + Use a non-standard RSA signature formats where the PKCS #1-padded data is without DigestInfo. + + ``NoDigestInfo`` can be passed as the ``algorithm`` in the RSA + :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.sign`, + :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey.verify` + and + :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey.recover_data_from_signature` + methods. + .. class:: Prehashed(algorithm) .. versionadded:: 1.6 diff --git a/src/cryptography/hazmat/primitives/asymmetric/rsa.py b/src/cryptography/hazmat/primitives/asymmetric/rsa.py index 5cdbef32b733..a85a02bb92e4 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -40,7 +40,9 @@ def sign( self, data: bytes, padding: AsymmetricPadding, - algorithm: asym_utils.Prehashed | hashes.HashAlgorithm, + algorithm: asym_utils.Prehashed + | hashes.HashAlgorithm + | asym_utils.NoDigestInfo, ) -> bytes: """ Signs the data. @@ -127,7 +129,7 @@ def recover_data_from_signature( self, signature: bytes, padding: AsymmetricPadding, - algorithm: hashes.HashAlgorithm | None, + algorithm: hashes.HashAlgorithm | asym_utils.NoDigestInfo | None, ) -> bytes: """ Recovers the original data from the signature. diff --git a/src/cryptography/hazmat/primitives/asymmetric/utils.py b/src/cryptography/hazmat/primitives/asymmetric/utils.py index 826b9567b47b..c01c34274be5 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/utils.py +++ b/src/cryptography/hazmat/primitives/asymmetric/utils.py @@ -11,6 +11,10 @@ encode_dss_signature = asn1.encode_dss_signature +class NoDigestInfo: + pass + + class Prehashed: def __init__(self, algorithm: hashes.HashAlgorithm): if not isinstance(algorithm, hashes.HashAlgorithm): diff --git a/src/rust/src/backend/rsa.rs b/src/rust/src/backend/rsa.rs index 9c5ccc26a7cb..21aaaf9861e4 100644 --- a/src/rust/src/backend/rsa.rs +++ b/src/rust/src/backend/rsa.rs @@ -290,8 +290,16 @@ impl RsaPrivateKey { padding: &pyo3::Bound<'p, pyo3::PyAny>, algorithm: &pyo3::Bound<'p, pyo3::PyAny>, ) -> CryptographyResult> { - let (data, algorithm) = - utils::calculate_digest_and_algorithm(py, data.as_bytes(), algorithm)?; + let (data, algorithm) = { + if algorithm.is_instance(&types::NO_DIGEST_INFO.get(py)?)? { + ( + utils::BytesOrPyBytes::Bytes(data.as_bytes()), + pyo3::types::PyNone::get(py).to_owned().into_any(), + ) + } else { + utils::calculate_digest_and_algorithm(py, data.as_bytes(), algorithm)? + } + }; let mut ctx = openssl::pkey_ctx::PkeyCtx::new(&self.pkey)?; ctx.sign_init().map_err(|_| { @@ -441,8 +449,16 @@ impl RsaPublicKey { padding: &pyo3::Bound<'_, pyo3::PyAny>, algorithm: &pyo3::Bound<'_, pyo3::PyAny>, ) -> CryptographyResult<()> { - let (data, algorithm) = - utils::calculate_digest_and_algorithm(py, data.as_bytes(), algorithm)?; + let (data, algorithm) = { + if algorithm.is_instance(&types::NO_DIGEST_INFO.get(py)?)? { + ( + utils::BytesOrPyBytes::Bytes(data.as_bytes()), + pyo3::types::PyNone::get(py).to_owned().into_any(), + ) + } else { + utils::calculate_digest_and_algorithm(py, data.as_bytes(), algorithm)? + } + }; let mut ctx = openssl::pkey_ctx::PkeyCtx::new(&self.pkey)?; ctx.verify_init()?; @@ -488,6 +504,11 @@ impl RsaPublicKey { padding: &pyo3::Bound<'_, pyo3::PyAny>, algorithm: &pyo3::Bound<'_, pyo3::PyAny>, ) -> CryptographyResult> { + let algorithm = if algorithm.is_instance(&types::NO_DIGEST_INFO.get(py)?)? { + &pyo3::types::PyNone::get(py).to_owned().into_any() + } else { + algorithm + }; if algorithm.is_instance(&types::PREHASHED.get(py)?)? { return Err(CryptographyError::from( pyo3::exceptions::PyTypeError::new_err( diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index 30c5376f0f2e..3a7b02c6d69e 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -393,6 +393,10 @@ pub static SHA1: LazyPyImport = pub static SHA256: LazyPyImport = LazyPyImport::new("cryptography.hazmat.primitives.hashes", &["SHA256"]); +pub static NO_DIGEST_INFO: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.utils", + &["NoDigestInfo"], +); pub static PREHASHED: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.asymmetric.utils", &["Prehashed"], diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index 04472fb2394d..c8412dba7549 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -51,7 +51,9 @@ ) from .utils import ( _check_rsa_private_numbers, + compute_rsa_hash_digest_sha256, generate_rsa_verification_test, + generate_rsa_verification_without_digest_test, skip_fips_traditional_openssl, ) @@ -442,6 +444,57 @@ def test_pkcs1v15_signing(self, backend, subtests): ) assert binascii.hexlify(signature) == example["signature"] + @pytest.mark.supported( + only_if=lambda backend: backend.rsa_padding_supported( + padding.PKCS1v15() + ), + skip_message="Does not support PKCS1v1.5.", + ) + @pytest.mark.supported( + only_if=lambda backend: backend.signature_hash_supported( + hashes.SHA256() + ), + skip_message="Does not support SHA256 signature.", + ) + def test_pkcs1v15_signing_without_digest(self, backend, subtests): + vectors = load_vectors_from_file( + os.path.join( + "asymmetric", "RSA", "FIPS_186-2", "SigVer15_186-3.rsp" + ), + load_rsa_nist_vectors, + ) + for params in vectors: + if params["fail"] or params["algorithm"] != "SHA256": + continue + with subtests.test(): + dmp1 = rsa.rsa_crt_dmp1( + params["private_exponent"], params["p"] + ) + dmq1 = rsa.rsa_crt_dmq1( + params["private_exponent"], params["q"] + ) + iqmp = rsa.rsa_crt_iqmp(params["p"], params["q"]) + + private_key = rsa.RSAPrivateNumbers( + p=params["p"], + q=params["q"], + d=params["private_exponent"], + dmp1=dmp1, + dmq1=dmq1, + iqmp=iqmp, + public_numbers=rsa.RSAPublicNumbers( + e=params["public_exponent"], n=params["modulus"] + ), + ).private_key(backend, unsafe_skip_rsa_key_validation=True) + signature = private_key.sign( + binascii.unhexlify( + compute_rsa_hash_digest_sha256(backend, params["msg"]) + ), + padding.PKCS1v15(), + asym_utils.NoDigestInfo(), + ) + assert binascii.hexlify(signature) == params["s"] + @pytest.mark.supported( only_if=lambda backend: backend.rsa_padding_supported( padding.PSS( @@ -910,7 +963,7 @@ def test_pkcs1v15_verification(self, backend, subtests): # Test recovery of all data (full DigestInfo) with hash alg. as # None rec_sig_data = public_key.recover_data_from_signature( - signature, padding.PKCS1v15(), None + signature, padding.PKCS1v15(), asym_utils.NoDigestInfo() ) assert len(rec_sig_data) > len(msg_digest) assert msg_digest == rec_sig_data[-len(msg_digest) :] @@ -1522,6 +1575,26 @@ class TestRSAPKCS1Verification: ) ) + test_rsa_pkcs1v15_verify_sha256_without_digest = pytest.mark.supported( + only_if=lambda backend: ( + backend.signature_hash_supported(hashes.SHA256()) + and backend.rsa_padding_supported(padding.PKCS1v15()) + ), + skip_message="Does not support SHA256 and PKCS1v1.5.", + )( + generate_rsa_verification_without_digest_test( + load_rsa_nist_vectors, + os.path.join("asymmetric", "RSA", "FIPS_186-2"), + [ + "SigGen15_186-2.rsp", + "SigGen15_186-3.rsp", + "SigVer15_186-3.rsp", + ], + hashes.SHA256(), + lambda params, hash_alg: padding.PKCS1v15(), + ) + ) + test_rsa_pkcs1v15_verify_sha224 = pytest.mark.supported( only_if=lambda backend: ( backend.signature_hash_supported(hashes.SHA224()) diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index 0557694c96e9..c746e48676e2 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -21,6 +21,7 @@ ) from cryptography.hazmat.primitives import hashes, hmac, serialization from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from cryptography.hazmat.primitives.ciphers import ( BlockCipherAlgorithm, Cipher, @@ -37,6 +38,14 @@ from ...utils import load_vectors_from_file +_hash_alg_oids = { + "sha1": binascii.unhexlify(b"3021300906052b0e03021a05000414"), + "sha224": binascii.unhexlify(b"302d300d06096086480165030402040500041c"), + "sha256": binascii.unhexlify(b"3031300d060960864801650304020105000420"), + "sha384": binascii.unhexlify(b"3041300d060960864801650304020205000430"), + "sha512": binascii.unhexlify(b"3051300d060960864801650304020305000440"), +} + def _load_all_params(path, file_names, param_loader): all_params = [] @@ -47,6 +56,13 @@ def _load_all_params(path, file_names, param_loader): return all_params +def compute_rsa_hash_digest_sha256(backend, msg): + oid = binascii.unhexlify(b"3031300d060960864801650304020105000420") + h = hashes.Hash(hashes.SHA256(), backend=backend) + h.update(binascii.unhexlify(msg)) + return binascii.hexlify(oid) + binascii.hexlify(h.finalize()) + + def generate_encrypt_test( param_loader, path, file_names, cipher_factory, mode_factory ): @@ -497,6 +513,26 @@ def test_rsa_verification(self, backend, subtests): return test_rsa_verification +def generate_rsa_verification_without_digest_test( + param_loader, path, file_names, hash_alg, pad_factory +): + def test_rsa_verification(self, backend, subtests): + all_params = _load_all_params(path, file_names, param_loader) + all_params = [ + i for i in all_params if i["algorithm"] == hash_alg.name.upper() + ] + for params in all_params: + with subtests.test(): + params["msg"] = compute_rsa_hash_digest_sha256( + backend, params["msg"] + ) + rsa_verification_test( + backend, params, asym_utils.NoDigestInfo(), pad_factory + ) + + return test_rsa_verification + + def rsa_verification_test(backend, params, hash_alg, pad_factory): public_numbers = rsa.RSAPublicNumbers( e=params["public_exponent"], n=params["modulus"]