diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 02a6405050fa..de1b4242ec5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ Changelog .. note:: This version is not yet released and is under active development. +* Added support for serialization of PKCS#12 Java truststores in + :func:`~cryptography.hazmat.primitives.serialization.pkcs12.serialize_java_truststore` * Support for Python 3.7 is deprecated and will be removed in the next ``cryptography`` release. * Added support for PKCS7 decryption and encryption using AES-256 as the diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 45f850fe17d8..41c6b5d111f8 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -929,6 +929,10 @@ Custom PKCS12 Test Vectors certs (``x509/cryptography.io.pem`` and ``x509/letsencryptx3.pem``) with friendly names ``☹`` and ``ï``, respectively, encrypted via AES 256 CBC with the password ``cryptography``. +* ``pkcs12/java-truststore.p12`` - A PKCS12 file containing two certs + (``x509/custom/dsa_selfsigned_ca.pem`` and ``x509/letsencryptx3.pem``) with + the first having a friendly name of `cert1`. Both have Java truststore + attributes with ANY_EXTENDED_KEY_USAGE. Custom PKCS7 Test Vectors ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index b832dd032722..f414df569d15 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -946,6 +946,42 @@ file suffix. ... b"friendlyname", key, cert, None, encryption ... ) +.. function:: serialize_java_truststore(certs, encryption_algorithm) + + .. versionadded:: 45.0.0 + + .. warning:: + + PKCS12 encryption is typically not secure and should not be used as a + security mechanism. Wrap a PKCS12 blob in a more secure envelope if you + need to store or send it safely. + + Serialize a PKCS12 blob containing provided certificates. Java expects an + internal flag to denote truststore usage, which this function adds. + + :param certs: A set of certificates to also include in the structure. + :type certs: + + A list of :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12Certificate` + instances. + + :param encryption_algorithm: The encryption algorithm that should be used + for the key and certificate. An instance of an object conforming to the + :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption` + interface. PKCS12 encryption is typically **very weak** and should not + be used as a security boundary. + + :return bytes: Serialized PKCS12. + + .. doctest:: + + >>> from cryptography import x509 + >>> from cryptography.hazmat.primitives.serialization import BestAvailableEncryption, pkcs12 + >>> cert = x509.load_pem_x509_certificate(ca_cert) + >>> p12 = pkcs12.serialize_java_truststore( + ... [pkcs12.PKCS12Certificate(cert, b"friendlyname")], BestAvailableEncryption(b"password") + ... ) + .. class:: PKCS12Certificate .. versionadded:: 36.0.0 diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 001f8f5f7c2a..80160dc57156 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -143,6 +143,8 @@ TLS toolchain totient Trixie +truststore +truststores tunable Ubuntu unencrypted diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi index 1ad1479e4472..b25becb6bdef 100644 --- a/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi +++ b/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi @@ -39,6 +39,10 @@ def load_pkcs12( password: bytes | None, backend: typing.Any = None, ) -> PKCS12KeyAndCertificates: ... +def serialize_java_truststore( + certs: Iterable[PKCS12Certificate], + encryption_algorithm: KeySerializationEncryption, +) -> bytes: ... def serialize_key_and_certificates( name: bytes | None, key: PKCS12PrivateKeyTypes | None, diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs12.py b/src/cryptography/hazmat/primitives/serialization/pkcs12.py index bab4d1eabf14..58884ff61a79 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs12.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs12.py @@ -27,6 +27,7 @@ "PKCS12PrivateKeyTypes", "load_key_and_certificates", "load_pkcs12", + "serialize_java_truststore", "serialize_key_and_certificates", ] @@ -119,6 +120,24 @@ def __repr__(self) -> str: ] +def serialize_java_truststore( + certs: Iterable[PKCS12Certificate], + encryption_algorithm: serialization.KeySerializationEncryption, +) -> bytes: + if not certs: + raise ValueError("You must supply at least one cert") + + if not isinstance( + encryption_algorithm, serialization.KeySerializationEncryption + ): + raise TypeError( + "Key encryption algorithm must be a " + "KeySerializationEncryption instance" + ) + + return rust_pkcs12.serialize_java_truststore(certs, encryption_algorithm) + + def serialize_key_and_certificates( name: bytes | None, key: PKCS12PrivateKeyTypes | None, diff --git a/src/rust/cryptography-x509/src/pkcs12.rs b/src/rust/cryptography-x509/src/pkcs12.rs index b4ce81c0a345..9935d3bd9830 100644 --- a/src/rust/cryptography-x509/src/pkcs12.rs +++ b/src/rust/cryptography-x509/src/pkcs12.rs @@ -2,6 +2,8 @@ // 2.0, and the BSD License. See the LICENSE file in the root of this repository // for complete details. +use asn1::ObjectIdentifier; + use crate::common::Utf8StoredBMPString; use crate::{pkcs7, pkcs8}; @@ -12,6 +14,8 @@ pub const SHROUDED_KEY_BAG_OID: asn1::ObjectIdentifier = pub const X509_CERTIFICATE_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 9, 22, 1); pub const FRIENDLY_NAME_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 9, 20); pub const LOCAL_KEY_ID_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 9, 21); +pub const JDK_TRUSTSTORE_USAGE: asn1::ObjectIdentifier = + asn1::oid!(2, 16, 840, 1, 113894, 746875, 1, 1); #[derive(asn1::Asn1Write, asn1::Asn1Read)] pub struct Pfx<'a> { @@ -50,6 +54,9 @@ pub enum AttributeSet<'a> { #[defined_by(LOCAL_KEY_ID_OID)] LocalKeyId(asn1::SetOfWriter<'a, &'a [u8], [&'a [u8]; 1]>), + + #[defined_by(JDK_TRUSTSTORE_USAGE)] + JDKTruststoreUsage(asn1::SetOfWriter<'a, ObjectIdentifier, [ObjectIdentifier; 1]>), } #[derive(asn1::Asn1DefinedByWrite)] diff --git a/src/rust/src/pkcs12.rs b/src/rust/src/pkcs12.rs index 00e6a759e2a2..e2ba412a6899 100644 --- a/src/rust/src/pkcs12.rs +++ b/src/rust/src/pkcs12.rs @@ -6,6 +6,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use cryptography_x509::common::Utf8StoredBMPString; +use cryptography_x509::oid::EKU_ANY_KEY_USAGE_OID; use pyo3::types::{PyAnyMethods, PyBytesMethods, PyListMethods}; use pyo3::{IntoPyObject, PyTypeInfo}; @@ -240,6 +241,7 @@ impl EncryptionAlgorithm { fn pkcs12_attributes<'a>( friendly_name: Option<&'a [u8]>, local_key_id: Option<&'a [u8]>, + is_java_trusted_cert: bool, ) -> CryptographyResult< Option< asn1::SetOfWriter< @@ -270,6 +272,14 @@ fn pkcs12_attributes<'a>( ), }); } + if is_java_trusted_cert { + attrs.push(cryptography_x509::pkcs12::Attribute { + _attr_id: asn1::DefinedByMarker::marker(), + attr_values: cryptography_x509::pkcs12::AttributeSet::JDKTruststoreUsage( + asn1::SetOfWriter::new([EKU_ANY_KEY_USAGE_OID]), + ), + }); + } if attrs.is_empty() { Ok(None) @@ -282,6 +292,7 @@ fn cert_to_bag<'a>( cert: &'a Certificate, friendly_name: Option<&'a [u8]>, local_key_id: Option<&'a [u8]>, + is_java_trusted_cert: bool, ) -> CryptographyResult> { Ok(cryptography_x509::pkcs12::SafeBag { _bag_id: asn1::DefinedByMarker::marker(), @@ -293,7 +304,7 @@ fn cert_to_bag<'a>( )), }, ))), - attributes: pkcs12_attributes(friendly_name, local_key_id)?, + attributes: pkcs12_attributes(friendly_name, local_key_id, is_java_trusted_cert)?, }) } @@ -388,7 +399,7 @@ enum CertificateOrPKCS12Certificate { PKCS12Certificate(pyo3::Py), } -fn serialize_bags<'p>( +fn serialize_safebags<'p>( py: pyo3::Python<'p>, safebags: &[cryptography_x509::pkcs12::SafeBag<'_>], encryption_details: &KeySerializationEncryption<'_>, @@ -405,6 +416,9 @@ fn serialize_bags<'p>( ); if let Some(e) = &encryption_details.encryption_algorithm { + // When encryption is applied, safebags that have already been encrypted (ShroudedKeyBag) + // should not be encrypted again, so they are placed in their own ContentInfo. + // See RFC 7292 4.1 let mut shrouded_safebags = vec![]; let mut plain_safebags = vec![]; for safebag in safebags { @@ -521,6 +535,28 @@ fn serialize_bags<'p>( Ok(pyo3::types::PyBytes::new(py, &asn1::write_single(&p12)?)) } +#[pyo3::pyfunction] +#[pyo3(signature = (certs, encryption_algorithm))] +fn serialize_java_truststore<'p>( + py: pyo3::Python<'p>, + certs: Vec>, + encryption_algorithm: pyo3::Bound<'_, pyo3::PyAny>, +) -> CryptographyResult> { + let encryption_details = decode_encryption_algorithm(py, encryption_algorithm)?; + let mut safebags = vec![]; + + for cert in &certs { + safebags.push(cert_to_bag( + cert.get().certificate.get(), + cert.get().friendly_name.as_ref().map(|v| v.as_bytes(py)), + None, + true, + )?); + } + + serialize_safebags(py, &safebags, &encryption_details) +} + #[pyo3::pyfunction] #[pyo3(signature = (name, key, cert, cas, encryption_algorithm))] fn serialize_key_and_certificates<'p>( @@ -559,6 +595,7 @@ fn serialize_key_and_certificates<'p>( cert, name, key_id.as_ref().map(|v| v.as_bytes()), + false, )?); } @@ -570,12 +607,13 @@ fn serialize_key_and_certificates<'p>( for cert in &ca_certs { let bag = match cert { CertificateOrPKCS12Certificate::Certificate(c) => { - cert_to_bag(c.get(), None, None)? + cert_to_bag(c.get(), None, None, false)? } CertificateOrPKCS12Certificate::PKCS12Certificate(c) => cert_to_bag( c.get().certificate.get(), c.get().friendly_name.as_ref().map(|v| v.as_bytes(py)), None, + false, )?, }; safebags.push(bag); @@ -627,7 +665,7 @@ fn serialize_key_and_certificates<'p>( }, ), ), - attributes: pkcs12_attributes(name, key_id.as_ref().map(|v| v.as_bytes()))?, + attributes: pkcs12_attributes(name, key_id.as_ref().map(|v| v.as_bytes()), false)?, } } else { let pkcs8_tlv = asn1::parse_single(&pkcs8_bytes)?; @@ -637,14 +675,14 @@ fn serialize_key_and_certificates<'p>( bag_value: asn1::Explicit::new(cryptography_x509::pkcs12::BagValue::KeyBag( pkcs8_tlv, )), - attributes: pkcs12_attributes(name, key_id.as_ref().map(|v| v.as_bytes()))?, + attributes: pkcs12_attributes(name, key_id.as_ref().map(|v| v.as_bytes()), false)?, } }; safebags.push(key_bag); } - serialize_bags(py, &safebags, &encryption_details) + serialize_safebags(py, &safebags, &encryption_details) } fn decode_p12( @@ -795,6 +833,7 @@ fn load_pkcs12<'p>( pub(crate) mod pkcs12 { #[pymodule_export] use super::{ - load_key_and_certificates, load_pkcs12, serialize_key_and_certificates, PKCS12Certificate, + load_key_and_certificates, load_pkcs12, serialize_java_truststore, + serialize_key_and_certificates, PKCS12Certificate, }; } diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py index 0490a543587e..96b4d59ebc55 100644 --- a/tests/hazmat/primitives/test_pkcs12.py +++ b/tests/hazmat/primitives/test_pkcs12.py @@ -32,6 +32,7 @@ PKCS12KeyAndCertificates, load_key_and_certificates, load_pkcs12, + serialize_java_truststore, serialize_key_and_certificates, ) @@ -737,6 +738,193 @@ def test_generate_localkeyid(self, backend, encryption_algorithm): assert p12.count(cert.fingerprint(hashes.SHA1())) == count +@pytest.mark.skip_fips( + reason="PKCS12 unsupported in FIPS mode. So much bad crypto in it." +) +class TestPKCS12TrustStoreCreation: + def test_generate_valid_truststore(self, backend): + # serialize_java_truststore adds a special attribute to each + # certificate's safebag. As we cannot read this back currently, + # comparison against a pre-verified file is necessary. + cert1 = _load_cert( + backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem") + ) + cert2 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem")) + encryption = serialization.NoEncryption() + p12 = serialize_java_truststore( + [ + PKCS12Certificate(cert1, b"cert1"), + PKCS12Certificate(cert2, None), + ], + encryption, + ) + + # The golden file was verified with: + # keytool -list -keystore java-truststore.p12 + # Ensuring both entries are listed with "trustedCertEntry" + golden_bytes = load_vectors_from_file( + os.path.join("pkcs12", "java-truststore.p12"), + lambda data: data.read(), + mode="rb", + ) + + # The last 49 bytes are the MAC digest, and will vary each call, so we + # can ignore them. + mac_digest_size = 49 + assert p12[:-mac_digest_size] == golden_bytes[:-mac_digest_size] + + def test_generate_certs_friendly_names(self, backend): + cert1 = _load_cert( + backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem") + ) + cert2 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem")) + encryption = serialization.NoEncryption() + p12 = serialize_java_truststore( + [ + PKCS12Certificate(cert1, b"cert1"), + PKCS12Certificate(cert2, None), + ], + encryption, + ) + + p12_cert = load_pkcs12(p12, None, backend) + cas = p12_cert.additional_certs + assert cas[0].certificate == cert1 + assert cas[0].friendly_name == b"cert1" + assert cas[1].certificate == cert2 + assert cas[1].friendly_name is None + + @pytest.mark.parametrize( + ("enc_alg", "enc_alg_der"), + [ + ( + PBES.PBESv2SHA256AndAES256CBC, + [ + b"\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x05\x0d", # PBESv2 + b"\x06\x09\x60\x86\x48\x01\x65\x03\x04\x01\x2a", # AES + ], + ), + ( + PBES.PBESv1SHA1And3KeyTripleDESCBC, + [b"\x06\x0a\x2a\x86\x48\x86\xf7\x0d\x01\x0c\x01\x03"], + ), + ( + None, + [], + ), + ], + ) + @pytest.mark.parametrize( + ("mac_alg", "mac_alg_der"), + [ + (hashes.SHA1(), b"\x06\x05\x2b\x0e\x03\x02\x1a"), + (hashes.SHA256(), b"\x06\t`\x86H\x01e\x03\x04\x02\x01"), + (None, None), + ], + ) + @pytest.mark.parametrize( + ("iters", "iter_der"), + [ + (420, b"\x02\x02\x01\xa4"), + (22222, b"\x02\x02\x56\xce"), + (None, None), + ], + ) + def test_key_serialization_encryption( + self, + backend, + enc_alg, + enc_alg_der, + mac_alg, + mac_alg_der, + iters, + iter_der, + ): + builder = serialization.PrivateFormat.PKCS12.encryption_builder() + if enc_alg is not None: + builder = builder.key_cert_algorithm(enc_alg) + if mac_alg is not None: + builder = builder.hmac_hash(mac_alg) + if iters is not None: + builder = builder.kdf_rounds(iters) + + encryption = builder.build(b"password") + cert = _load_cert( + backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem") + ) + assert isinstance(cert, x509.Certificate) + p12 = serialize_java_truststore( + [PKCS12Certificate(cert, b"name")], encryption + ) + # We want to know if we've serialized something that has the parameters + # we expect, so we match on specific byte strings of OIDs & DER values. + for der in enc_alg_der: + assert der in p12 + if mac_alg_der is not None: + assert mac_alg_der in p12 + if iter_der is not None: + assert iter_der in p12 + _, _, parsed_more_certs = load_key_and_certificates( + p12, b"password", backend + ) + assert parsed_more_certs == [cert] + + def test_invalid_utf8_friendly_name(self, backend): + cert, _ = _load_ca(backend) + with pytest.raises(ValueError): + serialize_java_truststore( + [PKCS12Certificate(cert, b"\xc9")], + serialization.NoEncryption(), + ) + + def test_generate_empty_certs(self): + with pytest.raises(ValueError) as exc: + serialize_java_truststore([], serialization.NoEncryption()) + assert str(exc.value) == ("You must supply at least one cert") + + def test_generate_unsupported_encryption_type(self, backend): + cert, _ = _load_ca(backend) + with pytest.raises(ValueError) as exc: + serialize_java_truststore( + [PKCS12Certificate(cert, None)], + DummyKeySerializationEncryption(), + ) + assert str(exc.value) == "Unsupported key encryption type" + + def test_generate_wrong_types(self, backend): + cert, key = _load_ca(backend) + encryption = serialization.NoEncryption() + with pytest.raises(TypeError) as exc: + serialize_java_truststore(cert, encryption) + + with pytest.raises(TypeError) as exc: + serialize_java_truststore([cert], encryption) + assert "object cannot be converted to 'PKCS12Certificate'" in str( + exc.value + ) + + with pytest.raises(TypeError) as exc: + serialize_java_truststore( + [PKCS12Certificate(cert, None), key], + encryption, + ) + assert "object cannot be converted to 'PKCS12Certificate'" in str( + exc.value + ) + + with pytest.raises(TypeError) as exc: + serialize_java_truststore([PKCS12Certificate(cert, None)], cert) + assert str(exc.value) == ( + "Key encryption algorithm must be a " + "KeySerializationEncryption instance" + ) + with pytest.raises(TypeError) as exc: + serialize_java_truststore([key], encryption) + assert "object cannot be converted to 'PKCS12Certificate'" in str( + exc.value + ) + + @pytest.mark.skip_fips( reason="PKCS12 unsupported in FIPS mode. So much bad crypto in it." ) diff --git a/vectors/cryptography_vectors/pkcs12/java-truststore.p12 b/vectors/cryptography_vectors/pkcs12/java-truststore.p12 new file mode 100644 index 000000000000..02d8e7220f2a Binary files /dev/null and b/vectors/cryptography_vectors/pkcs12/java-truststore.p12 differ