Skip to content
Open
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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:

Expand Down
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.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) = {
Copy link
Member

Choose a reason for hiding this comment

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

What happens if I pass NoDigestInfo and also PSS instead of PKCS1v15 for padding? Do we properly handle that case (it should be a ValueError that NoDigestInfo is only allowed for PKCS1v15)? I don't see logic for it. Assuming it's missing, we'll need a test for that once the check is added..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently If you pass NoDigestInfo and also PSS you get a TypeError: Expected instance of hashes.HashAlgorithm..

I think this is even better than a ValueErrorbut your mileage might vary.

I added a test case for this code path in 712d5e2

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
124 changes: 123 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_sha256,
generate_rsa_verification_test,
generate_rsa_verification_without_digest_test,
skip_fips_traditional_openssl,
)

Expand Down Expand Up @@ -442,6 +444,60 @@ 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)

_check_fips_key_length(backend, private_key)

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(
Expand Down Expand Up @@ -503,6 +559,52 @@ def test_pss_signing(self, subtests, backend):
hashes.SHA1(),
)

@pytest.mark.supported(
only_if=lambda backend: backend.rsa_padding_supported(
padding.PSS(
mgf=padding.MGF1(hashes.SHA1()),
salt_length=padding.PSS.MAX_LENGTH,
)
),
skip_message="Does not support PSS.",
)
@pytest.mark.supported(
only_if=lambda backend: backend.signature_hash_supported(
hashes.SHA1()
),
skip_message="Does not support SHA1 signature.",
)
def test_pss_signing_without_digest(self, backend, subtests):
for private, public, example in _flatten_pkcs1_examples(
load_vectors_from_file(
os.path.join(
"asymmetric", "RSA", "pkcs-1v2-1d2-vec", "pss-vect.txt"
),
load_pkcs1_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)
with pytest.raises(TypeError):
private_key.sign(
binascii.unhexlify(example["message"]),
padding.PSS(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
salt_length=padding.PSS.MAX_LENGTH,
),
asym_utils.NoDigestInfo(),
)

@pytest.mark.supported(
only_if=lambda backend: backend.rsa_padding_supported(
padding.PSS(
Expand Down Expand Up @@ -910,7 +1012,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 +1624,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())
Expand Down
28 changes: 28 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 @@ -47,6 +48,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
):
Expand Down Expand Up @@ -501,6 +509,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"]
Expand Down