Skip to content

Commit 317b64e

Browse files
facutuescaalex
authored andcommitted
Fix ASN.1 issues in PKCS#7 and S/MIME signing (pyca#10373)
* Fix ASN.1 for S/MIME capabilities. The current implementation defines the SMIMECapabilities attribute so that its value is a SEQUENCE of all the algorithm OIDs that are supported. However, the S/MIME v3 spec (RFC 2633) specifies that each algorithm should be specified in its own SEQUENCE: SMIMECapabilities ::= SEQUENCE OF SMIMECapability SMIMECapability ::= SEQUENCE { capabilityID OBJECT IDENTIFIER, parameters ANY DEFINED BY capabilityID OPTIONAL } (RFC 2633, Appendix A) This commit changes the implementation so that each algorithm is inside its own SEQUENCE. This also matches the OpenSSL implementation. * Fix the RSA OID used for signing PKCS#7/SMIME The current implementation computes the algorithm identifier used in the `digest_encryption_algorithm` PKCS#7 field (or `SignatureAlgorithmIdentifier` in S/MIME) based on both the algorithm used to sign (e.g. RSA) and the digest algorithm (e.g. SHA512). This is correct for ECDSA signatures, where the OIDs used include the digest algorithm (e.g: ecdsa-with-SHA512). However, due to historical reasons, when signing with RSA the OID specified should be the one corresponding to just RSA ("1.2.840.113549.1.1.1" rsaEncryption), rather than OIDs which also include the digest algorithm (such as "1.2.840.113549.1.1.13", sha512WithRSAEncryption). This means that the logic to compute the algorithm identifier is the same except when signing with RSA, in which case the OID will always be `rsaEncryption`. This is consistent with the OpenSSL implementation, and the RFCs that define PKCS#7 and S/MIME. See RFC 3851 (section 2.2), and RFC 3370 (section 3.2) for more details. * Add tests for the changes in PKCS7 signing * PKCS7 fixes from code review * Update CHANGELOG
1 parent 7a4d012 commit 317b64e

File tree

4 files changed

+89
-7
lines changed

4 files changed

+89
-7
lines changed

CHANGELOG.rst

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
=========
33

4+
.. _v42-0-4:
5+
6+
42.0.4 - 2024-02-20
7+
~~~~~~~~~~~~~~~~~~~
8+
9+
* Fixed ASN.1 encoding for PKCS7/SMIME signed messages. The fields ``SMIMECapabilities``
10+
and ``SignatureAlgorithmIdentifier`` should now be correctly encoded according to the
11+
definitions in :rfc:`2633` :rfc:`3370`.
12+
413
.. _v42-0-3:
514

615
42.0.3 - 2024-02-15

src/rust/src/pkcs7.rs

+24-4
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ fn sign_and_serialize<'p>(
104104
// Subset of values OpenSSL provides:
105105
// https://github.com/openssl/openssl/blob/667a8501f0b6e5705fd611d5bb3ca24848b07154/crypto/pkcs7/pk7_smime.c#L150
106106
// removing all the ones that are bad cryptography
107-
AES_256_CBC_OID,
108-
AES_192_CBC_OID,
109-
AES_128_CBC_OID,
107+
&asn1::SequenceOfWriter::new([AES_256_CBC_OID]),
108+
&asn1::SequenceOfWriter::new([AES_192_CBC_OID]),
109+
&asn1::SequenceOfWriter::new([AES_128_CBC_OID]),
110110
]))?;
111111

112112
let py_signers: Vec<(
@@ -205,7 +205,7 @@ fn sign_and_serialize<'p>(
205205
},
206206
digest_algorithm: digest_alg,
207207
authenticated_attributes: authenticated_attrs,
208-
digest_encryption_algorithm: x509::sign::compute_signature_algorithm(
208+
digest_encryption_algorithm: compute_pkcs7_signature_algorithm(
209209
py,
210210
py_private_key,
211211
py_hash_alg,
@@ -262,6 +262,26 @@ fn sign_and_serialize<'p>(
262262
}
263263
}
264264

265+
fn compute_pkcs7_signature_algorithm<'p>(
266+
py: pyo3::Python<'p>,
267+
private_key: &'p pyo3::PyAny,
268+
hash_algorithm: &'p pyo3::PyAny,
269+
rsa_padding: &'p pyo3::PyAny,
270+
) -> pyo3::PyResult<common::AlgorithmIdentifier<'static>> {
271+
let key_type = x509::sign::identify_key_type(py, private_key)?;
272+
let has_pss_padding = rsa_padding.is_instance(types::PSS.get(py)?)?;
273+
// For RSA signatures (with no PSS padding), the OID is always the same no matter the
274+
// digest algorithm. See RFC 3370 (section 3.2).
275+
if key_type == x509::sign::KeyType::Rsa && !has_pss_padding {
276+
Ok(common::AlgorithmIdentifier {
277+
oid: asn1::DefinedByMarker::marker(),
278+
params: common::AlgorithmParameters::Rsa(Some(())),
279+
})
280+
} else {
281+
x509::sign::compute_signature_algorithm(py, private_key, hash_algorithm, rsa_padding)
282+
}
283+
}
284+
265285
fn smime_canonicalize(data: &[u8], text_mode: bool) -> (Cow<'_, [u8]>, Cow<'_, [u8]>) {
266286
let mut new_data_with_header = vec![];
267287
let mut new_data_without_header = vec![];

src/rust/src/x509/sign.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ enum HashType {
4848
Sha3_512,
4949
}
5050

51-
fn identify_key_type(py: pyo3::Python<'_>, private_key: &pyo3::PyAny) -> pyo3::PyResult<KeyType> {
51+
pub(crate) fn identify_key_type(
52+
py: pyo3::Python<'_>,
53+
private_key: &pyo3::PyAny,
54+
) -> pyo3::PyResult<KeyType> {
5255
if private_key.is_instance(types::RSA_PRIVATE_KEY.get(py)?)? {
5356
Ok(KeyType::Rsa)
5457
} else if private_key.is_instance(types::DSA_PRIVATE_KEY.get(py)?)? {

tests/hazmat/primitives/test_pkcs7.py

+52-2
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,50 @@ def test_sign_text(self, backend):
557557
backend,
558558
)
559559

560+
def test_smime_capabilities(self, backend):
561+
data = b"hello world"
562+
cert, key = _load_cert_key()
563+
builder = (
564+
pkcs7.PKCS7SignatureBuilder()
565+
.set_data(data)
566+
.add_signer(cert, key, hashes.SHA256())
567+
)
568+
569+
sig_binary = builder.sign(serialization.Encoding.DER, [])
570+
571+
# 1.2.840.113549.1.9.15 (SMIMECapabilities) as an ASN.1 DER encoded OID
572+
assert b"\x06\t*\x86H\x86\xf7\r\x01\t\x0f" in sig_binary
573+
574+
# 2.16.840.1.101.3.4.1.42 (aes256-CBC-PAD) as an ASN.1 DER encoded OID
575+
aes256_cbc_pad_oid = b"\x06\x09\x60\x86\x48\x01\x65\x03\x04\x01\x2A"
576+
# 2.16.840.1.101.3.4.1.22 (aes192-CBC-PAD) as an ASN.1 DER encoded OID
577+
aes192_cbc_pad_oid = b"\x06\x09\x60\x86\x48\x01\x65\x03\x04\x01\x16"
578+
# 2.16.840.1.101.3.4.1.2 (aes128-CBC-PAD) as an ASN.1 DER encoded OID
579+
aes128_cbc_pad_oid = b"\x06\x09\x60\x86\x48\x01\x65\x03\x04\x01\x02"
580+
581+
# Each algorithm in SMIMECapabilities should be inside its own
582+
# SEQUENCE.
583+
# This is encoded as SEQUENCE_IDENTIFIER + LENGTH + ALGORITHM_OID.
584+
# This tests that each algorithm is indeed encoded inside its own
585+
# sequence. See RFC 2633, Appendix A for more details.
586+
sequence_identifier = b"\x30"
587+
for oid in [
588+
aes256_cbc_pad_oid,
589+
aes192_cbc_pad_oid,
590+
aes128_cbc_pad_oid,
591+
]:
592+
len_oid = len(oid).to_bytes(length=1, byteorder="big")
593+
assert sequence_identifier + len_oid + oid in sig_binary
594+
595+
_pkcs7_verify(
596+
serialization.Encoding.DER,
597+
sig_binary,
598+
None,
599+
[cert],
600+
[],
601+
backend,
602+
)
603+
560604
def test_sign_no_capabilities(self, backend):
561605
data = b"hello world"
562606
cert, key = _load_cert_key()
@@ -677,9 +721,15 @@ def test_rsa_pkcs_padding_options(self, pad, backend):
677721
sig.count(b"\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x08") == 1
678722
)
679723
else:
680-
# This should be a pkcs1 sha512 signature
724+
# This should be a pkcs1 RSA signature, which uses the
725+
# `rsaEncryption` OID (1.2.840.113549.1.1.1) no matter which
726+
# digest algorithm is used.
727+
# See RFC 3370 section 3.2 for more details.
728+
# This OID appears twice, once in the certificate itself and
729+
# another in the SignerInfo data structure in the
730+
# `digest_encryption_algorithm` field.
681731
assert (
682-
sig.count(b"\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0D") == 1
732+
sig.count(b"\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") == 2
683733
)
684734
_pkcs7_verify(
685735
serialization.Encoding.DER,

0 commit comments

Comments
 (0)