Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/hazmat/primitives/asymmetric/rsa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
13 changes: 13 additions & 0 deletions docs/hazmat/primitives/asymmetric/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ Asymmetric Utilities

:return bytes: The encoded signature.

.. class:: NoDigestInfo()

.. versionadded:: 47.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: 47.0
.. 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
Expand Down
6 changes: 4 additions & 2 deletions src/cryptography/hazmat/primitives/asymmetric/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/cryptography/hazmat/primitives/asymmetric/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
29 changes: 25 additions & 4 deletions src/rust/src/backend/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,16 @@ impl RsaPrivateKey {
padding: &pyo3::Bound<'p, pyo3::PyAny>,
algorithm: &pyo3::Bound<'p, pyo3::PyAny>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyAny>> {
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(|_| {
Expand Down Expand Up @@ -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()?;
Expand Down Expand Up @@ -488,6 +504,11 @@ impl RsaPublicKey {
padding: &pyo3::Bound<'_, pyo3::PyAny>,
algorithm: &pyo3::Bound<'_, pyo3::PyAny>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
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(
Expand Down
4 changes: 4 additions & 0 deletions src/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
67 changes: 66 additions & 1 deletion tests/hazmat/primitives/test_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
)
from .utils import (
_check_rsa_private_numbers,
compute_rsa_hash_digest,
generate_rsa_verification_test,
generate_rsa_verification_without_digest_test,
skip_fips_traditional_openssl,
)

Expand Down Expand Up @@ -442,6 +444,49 @@ 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.SHA1()
),
skip_message="Does not support SHA1 signature.",
)
def test_pkcs1v15_signing_without_digest(self, backend, subtests):
vectors = _flatten_pkcs1_examples(
load_vectors_from_file(
os.path.join("asymmetric", "RSA", "pkcs1v15sign-vectors.txt"),
load_pkcs1_vectors,
)
)
for private, public, example in vectors:
with subtests.test():
private_key = rsa.RSAPrivateNumbers(
p=private["p"],
q=private["q"],
d=private["private_exponent"],
dmp1=private["dmp1"],
dmq1=private["dmq1"],
iqmp=private["iqmp"],
public_numbers=rsa.RSAPublicNumbers(
e=private["public_exponent"], n=private["modulus"]
),
).private_key(backend, unsafe_skip_rsa_key_validation=True)
signature = private_key.sign(
binascii.unhexlify(
compute_rsa_hash_digest(
backend, hashes.SHA1(), example["message"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer we used SHA256 for the tests here because otherwise we'll increasingly have coverage challenges as more and more things disable SHA1.

Copy link
Contributor Author

@jahkosha jahkosha Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with that change is that the test vectors we use here (pkcs1v15sign-vectors.txt) do use SHA1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to use pkcs1v15sign-vectors.txt here, we can use SigVer15_186-3.rsp and filter to passing non-SHA1 tests.

)
),
padding.PKCS1v15(),
asym_utils.NoDigestInfo(),
)
assert binascii.hexlify(signature) == example["signature"]

@pytest.mark.supported(
only_if=lambda backend: backend.rsa_padding_supported(
padding.PSS(
Expand Down Expand Up @@ -910,7 +955,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) :]
Expand Down Expand Up @@ -1522,6 +1567,26 @@ class TestRSAPKCS1Verification:
)
)

test_rsa_pkcs1v15_verify_sha1_without_digest = pytest.mark.supported(
only_if=lambda backend: (
backend.signature_hash_supported(hashes.SHA1())
and backend.rsa_padding_supported(padding.PKCS1v15())
),
skip_message="Does not support SHA1 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())
Expand Down
36 changes: 36 additions & 0 deletions tests/hazmat/primitives/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = []
Expand All @@ -47,6 +56,13 @@ def _load_all_params(path, file_names, param_loader):
return all_params


def compute_rsa_hash_digest(backend, hash_alg, msg):
oid = _hash_alg_oids[hash_alg.name]
h = hashes.Hash(hash_alg, 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
):
Expand Down Expand Up @@ -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(
backend, hash_alg, 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"]
Expand Down