From 42e5b827e93f8300dc3462e59923d513a1771d65 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 9 Mar 2024 20:58:08 -0500 Subject: [PATCH] Begin migrating PKCS#12 serialization to Rust For now, only handle unencrypted cert-only PKCS#12. --- .../hazmat/bindings/_rust/pkcs12.pyi | 5 + .../hazmat/primitives/serialization/pkcs12.py | 5 + src/rust/cryptography-x509/src/common.rs | 19 +++ src/rust/cryptography-x509/src/pkcs12.rs | 37 ++-- src/rust/cryptography-x509/src/pkcs7.rs | 2 +- src/rust/src/pkcs12.rs | 160 +++++++++++++++++- src/rust/src/types.rs | 2 + 7 files changed, 208 insertions(+), 22 deletions(-) diff --git a/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi b/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi index 109ae4fce5d8..76dd0194c40a 100644 --- a/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi +++ b/src/cryptography/hazmat/bindings/_rust/pkcs12.pyi @@ -33,3 +33,8 @@ def load_pkcs12( password: bytes | None, backend: typing.Any = None, ) -> PKCS12KeyAndCertificates: ... +def serialize_key_and_certificates( + name: bytes | None, + cert: x509.Certificate | None, + cas: typing.Iterable[x509.Certificate | PKCS12Certificate] | None, +) -> bytes: ... diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs12.py b/src/cryptography/hazmat/primitives/serialization/pkcs12.py index 8ed5f1e0872b..0d37145eb943 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs12.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs12.py @@ -167,6 +167,11 @@ def serialize_key_and_certificates( if key is None and cert is None and not cas: raise ValueError("You must supply at least one of key, cert, or cas") + if key is None and isinstance( + encryption_algorithm, serialization.NoEncryption + ): + return rust_pkcs12.serialize_key_and_certificates(name, cert, cas) + from cryptography.hazmat.backends.openssl.backend import backend return backend.serialize_key_and_certificates_to_pkcs12( diff --git a/src/rust/cryptography-x509/src/common.rs b/src/rust/cryptography-x509/src/common.rs index 77cebc30464e..9eea5ff7bca8 100644 --- a/src/rust/cryptography-x509/src/common.rs +++ b/src/rust/cryptography-x509/src/common.rs @@ -414,6 +414,25 @@ impl<'a> asn1::SimpleAsn1Writable for UnvalidatedVisibleString<'a> { } } +/// A BMPString ASN.1 element, where it is stored as a UTF-8 string in memory. +pub struct Utf8StoredBMPString<'a>(pub &'a str); + +impl<'a> Utf8StoredBMPString<'a> { + pub fn new(s: &'a str) -> Self { + Utf8StoredBMPString(s) + } +} + +impl<'a> asn1::SimpleAsn1Writable for Utf8StoredBMPString<'a> { + const TAG: asn1::Tag = asn1::BMPString::TAG; + fn write_data(&self, writer: &mut asn1::WriteBuf) -> asn1::WriteResult { + for ch in self.0.encode_utf16() { + writer.push_slice(&ch.to_be_bytes())?; + } + Ok(()) + } +} + #[derive(Clone)] pub struct WithTlv<'a, T> { tlv: asn1::Tlv<'a>, diff --git a/src/rust/cryptography-x509/src/pkcs12.rs b/src/rust/cryptography-x509/src/pkcs12.rs index 328961fce053..4fea62179846 100644 --- a/src/rust/cryptography-x509/src/pkcs12.rs +++ b/src/rust/cryptography-x509/src/pkcs12.rs @@ -2,6 +2,7 @@ // 2.0, and the BSD License. See the LICENSE file in the root of this repository // for complete details. +use crate::common::Utf8StoredBMPString; use crate::pkcs7; pub const CERT_BAG_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 12, 10, 1, 3); @@ -9,60 +10,60 @@ pub const KEY_BAG_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 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); -// #[derive(asn1::Asn1Write)] +#[derive(asn1::Asn1Write)] pub struct Pfx<'a> { pub version: u8, pub auth_safe: pkcs7::ContentInfo<'a>, pub mac_data: Option>, } -// #[derive(asn1::Asn1Write)] +#[derive(asn1::Asn1Write)] pub struct MacData<'a> { pub mac: pkcs7::DigestInfo<'a>, pub salt: &'a [u8], - // #[default(1u64)] + #[default(1u64)] pub iterations: u64, } -// #[derive(asn1::Asn1Write)] +#[derive(asn1::Asn1Write)] pub struct SafeBag<'a> { pub _bag_id: asn1::DefinedByMarker, - // #[defined_by(_bag_id)] + #[defined_by(_bag_id)] pub bag_value: asn1::Explicit, 0>, - // pub attributes: Option>>, + pub attributes: Option, Vec>>>, } -// #[derive(asn1::Asn1Write)] +#[derive(asn1::Asn1Write)] pub struct Attribute<'a> { pub _attr_id: asn1::DefinedByMarker, - // #[defined_by(_attr_id)] + #[defined_by(_attr_id)] pub attr_values: AttributeSet<'a>, } -// #[derive(asn1::Asn1DefinedByWrite)] +#[derive(asn1::Asn1DefinedByWrite)] pub enum AttributeSet<'a> { - // #[defined_by(FRIENDLY_NAME_OID)] - FriendlyName(asn1::SetOfWriter<'a, asn1::BMPString<'a>>), + #[defined_by(FRIENDLY_NAME_OID)] + FriendlyName(asn1::SetOfWriter<'a, Utf8StoredBMPString<'a>, [Utf8StoredBMPString<'a>; 1]>), } -// #[derive(asn1::Asn1DefinedByWrite)] +#[derive(asn1::Asn1DefinedByWrite)] pub enum BagValue<'a> { - // #[defined_by(CERT_BAG_OID)] + #[defined_by(CERT_BAG_OID)] CertBag(CertBag<'a>), - // #[defined_by(KEY_BAG_OID)] + #[defined_by(KEY_BAG_OID)] KeyBag(asn1::Tlv<'a>), } -// #[derive(asn1::Asn1Write)] +#[derive(asn1::Asn1Write)] pub struct CertBag<'a> { pub _cert_id: asn1::DefinedByMarker, - // #[defined_by(_cert_id)] + #[defined_by(_cert_id)] pub cert_value: asn1::Explicit, 0>, } -// #[derive(asn1::Asn1DefinedByWrite)] +#[derive(asn1::Asn1DefinedByWrite)] pub enum CertType<'a> { - // #[defined_by(X509_CERTIFICATE_OID)] + #[defined_by(X509_CERTIFICATE_OID)] X509(asn1::OctetStringEncoded>), } diff --git a/src/rust/cryptography-x509/src/pkcs7.rs b/src/rust/cryptography-x509/src/pkcs7.rs index e1581a0e069a..9df323696ac3 100644 --- a/src/rust/cryptography-x509/src/pkcs7.rs +++ b/src/rust/cryptography-x509/src/pkcs7.rs @@ -59,7 +59,7 @@ pub struct IssuerAndSerialNumber<'a> { pub serial_number: asn1::BigInt<'a>, } -// #[derive(asn1::Asn1Write)] +#[derive(asn1::Asn1Write)] pub struct DigestInfo<'a> { pub algorithm: common::AlgorithmIdentifier<'a>, pub digest: &'a [u8], diff --git a/src/rust/src/pkcs12.rs b/src/rust/src/pkcs12.rs index 1df4d51ae2e8..88af09fb8ce8 100644 --- a/src/rust/src/pkcs12.rs +++ b/src/rust/src/pkcs12.rs @@ -2,11 +2,12 @@ // 2.0, and the BSD License. See the LICENSE file in the root of this repository // for complete details. -use crate::backend::keys; +use crate::backend::{hashes, hmac, keys}; use crate::buf::CffiBuf; use crate::error::CryptographyResult; use crate::x509::certificate::Certificate; use crate::{types, x509}; +use cryptography_x509::common::Utf8StoredBMPString; use pyo3::IntoPy; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -76,9 +77,8 @@ impl PKCS12Certificate { const KDF_ENCRYPTION_KEY_ID: u8 = 1; #[allow(dead_code)] const KDF_IV_ID: u8 = 2; -#[allow(dead_code)] const KDF_MAC_KEY_ID: u8 = 3; -#[allow(dead_code)] + fn pkcs12_kdf( pass: &[u8], salt: &[u8], @@ -179,6 +179,156 @@ fn pkcs12_kdf( Ok(result) } +fn friendly_name_attributes( + friendly_name: Option<&[u8]>, +) -> CryptographyResult< + Option< + asn1::SetOfWriter< + '_, + cryptography_x509::pkcs12::Attribute<'_>, + Vec>, + >, + >, +> { + if let Some(name) = friendly_name { + let name_str = std::str::from_utf8(name).map_err(|_| { + pyo3::exceptions::PyValueError::new_err("friendly_name must be valid UTF-8") + })?; + + Ok(Some(asn1::SetOfWriter::new(vec![ + cryptography_x509::pkcs12::Attribute { + _attr_id: asn1::DefinedByMarker::marker(), + attr_values: cryptography_x509::pkcs12::AttributeSet::FriendlyName( + asn1::SetOfWriter::new([Utf8StoredBMPString::new(name_str)]), + ), + }, + ]))) + } else { + Ok(None) + } +} + +fn cert_to_bag<'a>( + cert: &'a Certificate, + friendly_name: Option<&'a [u8]>, +) -> CryptographyResult> { + Ok(cryptography_x509::pkcs12::SafeBag { + _bag_id: asn1::DefinedByMarker::marker(), + bag_value: asn1::Explicit::new(cryptography_x509::pkcs12::BagValue::CertBag( + cryptography_x509::pkcs12::CertBag { + _cert_id: asn1::DefinedByMarker::marker(), + cert_value: asn1::Explicit::new(cryptography_x509::pkcs12::CertType::X509( + asn1::OctetStringEncoded::new(cert.raw.borrow_dependent().clone()), + )), + }, + )), + attributes: friendly_name_attributes(friendly_name)?, + }) +} + +fn decode_encryption_algorithm( + py: pyo3::Python<'_>, +) -> CryptographyResult<(&[u8], &pyo3::PyAny, u64)> { + let default_hmac_alg = types::SHA256.get(py)?.call0()?; + let default_hmac_kdf_iter = 2048; + + Ok((b"", default_hmac_alg, default_hmac_kdf_iter)) +} + +#[derive(pyo3::FromPyObject)] +enum CertificateOrPKCS12Certificate { + Certificate(pyo3::Py), + PKCS12Certificate(pyo3::Py), +} + +#[pyo3::prelude::pyfunction] +#[pyo3(signature = (name, cert, cas))] +fn serialize_key_and_certificates<'p>( + py: pyo3::Python<'p>, + name: Option<&[u8]>, + cert: Option<&Certificate>, + cas: Option<&pyo3::PyAny>, +) -> CryptographyResult<&'p pyo3::types::PyBytes> { + let (password, mac_algorithm, mac_kdf_iter) = decode_encryption_algorithm(py)?; + + let mut auth_safe_contents = vec![]; + let cert_bag_contents; + let mut ca_certs = vec![]; + if cert.is_some() || cas.is_some() { + let mut cert_bags = vec![]; + + if let Some(cert) = cert { + cert_bags.push(cert_to_bag(cert, name)?); + } + + if let Some(cas) = cas { + for cert in cas.iter()? { + ca_certs.push(cert?.extract::()?); + } + + for cert in &ca_certs { + let bag = match cert { + CertificateOrPKCS12Certificate::Certificate(c) => cert_to_bag(c.get(), None)?, + CertificateOrPKCS12Certificate::PKCS12Certificate(c) => cert_to_bag( + c.get().certificate.get(), + c.get().friendly_name.as_ref().map(|v| v.as_bytes(py)), + )?, + }; + cert_bags.push(bag); + } + } + + cert_bag_contents = asn1::write_single(&asn1::SequenceOfWriter::new(cert_bags))?; + auth_safe_contents.push(cryptography_x509::pkcs7::ContentInfo { + _content_type: asn1::DefinedByMarker::marker(), + content: cryptography_x509::pkcs7::Content::Data(Some(asn1::Explicit::new( + &cert_bag_contents, + ))), + }); + } + let auth_safe_content = asn1::write_single(&asn1::SequenceOfWriter::new(auth_safe_contents))?; + + let salt = types::OS_URANDOM.get(py)?.call1((8,))?.extract::<&[u8]>()?; + let mac_algorithm_md = hashes::message_digest_from_algorithm(py, mac_algorithm)?; + let mac_key = pkcs12_kdf( + password, + salt, + KDF_MAC_KEY_ID, + mac_kdf_iter, + mac_algorithm_md.size(), + mac_algorithm_md, + )?; + let mac_digest = { + let mut h = hmac::Hmac::new_bytes(py, &mac_key, mac_algorithm)?; + h.update_bytes(&auth_safe_content)?; + h.finalize(py)? + }; + let mac_algorithm_identifier = crate::x509::ocsp::HASH_NAME_TO_ALGORITHM_IDENTIFIERS + [mac_algorithm + .getattr(pyo3::intern!(py, "name"))? + .extract::<&str>()?] + .clone(); + + let p12 = cryptography_x509::pkcs12::Pfx { + version: 3, + auth_safe: cryptography_x509::pkcs7::ContentInfo { + _content_type: asn1::DefinedByMarker::marker(), + content: cryptography_x509::pkcs7::Content::Data(Some(asn1::Explicit::new( + &auth_safe_content, + ))), + }, + mac_data: Some(cryptography_x509::pkcs12::MacData { + mac: cryptography_x509::pkcs7::DigestInfo { + algorithm: mac_algorithm_identifier, + digest: mac_digest.as_bytes(), + }, + salt, + iterations: mac_kdf_iter, + }), + }; + Ok(pyo3::types::PyBytes::new(py, &asn1::write_single(&p12)?)) +} + fn decode_p12( data: CffiBuf<'_>, password: Option>, @@ -314,6 +464,10 @@ pub(crate) fn create_submodule(py: pyo3::Python<'_>) -> pyo3::PyResult<&pyo3::pr submod.add_function(pyo3::wrap_pyfunction!(load_key_and_certificates, submod)?)?; submod.add_function(pyo3::wrap_pyfunction!(load_pkcs12, submod)?)?; + submod.add_function(pyo3::wrap_pyfunction!( + serialize_key_and_certificates, + submod + )?)?; submod.add_class::()?; diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index 55250a0b0b58..45b6dcaf5c09 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -343,6 +343,8 @@ pub static EXTENDABLE_OUTPUT_FUNCTION: LazyPyImport = LazyPyImport::new( ); pub static SHA1: LazyPyImport = LazyPyImport::new("cryptography.hazmat.primitives.hashes", &["SHA1"]); +pub static SHA256: LazyPyImport = + LazyPyImport::new("cryptography.hazmat.primitives.hashes", &["SHA256"]); pub static PREHASHED: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.asymmetric.utils",