diff --git a/Cargo.lock b/Cargo.lock index feee9050e..3834ac9ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,16 @@ dependencies = [ "cipher", ] +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -594,6 +604,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flagset" version = "0.4.7" @@ -612,6 +628,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures-core" version = "0.3.32" @@ -953,6 +984,54 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "p256" version = "0.14.0-rc.9" @@ -1014,15 +1093,28 @@ dependencies = [ name = "pkcs12" version = "0.2.0-pre.0" dependencies = [ + "cbc", "cms", "const-oid", + "crypto-common", "der", + "des", "digest", "hex-literal", + "hmac", + "log", + "openssl", "pkcs5", "pkcs8", + "rand 0.10.1", + "rand_core 0.10.1", + "rc2", + "sha1", "sha2", "spki", + "subtle", + "subtle-encoding", + "tempfile", "whirlpool", "x509-cert", "zeroize", @@ -1059,6 +1151,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "postcard" version = "1.1.3" @@ -1239,6 +1337,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rc2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceda21af1ae61033b63175653a1af86cae399d79cd03ca80ba347eb3a6c4a7fe" +dependencies = [ + "cipher", +] + [[package]] name = "regex" version = "1.12.3" @@ -1591,6 +1698,12 @@ dependencies = [ "keccak", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "3.0.0-rc.10" @@ -1641,6 +1754,15 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + [[package]] name = "syn" version = "2.0.117" @@ -1895,6 +2017,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/pkcs12/Cargo.toml b/pkcs12/Cargo.toml index d2afabe8e..01e0f7bad 100644 --- a/pkcs12/Cargo.toml +++ b/pkcs12/Cargo.toml @@ -22,21 +22,63 @@ x509-cert = { version = "0.3.0-rc.4", default-features = false } const-oid = { version = "0.10", features = ["db"], default-features = false } cms = { version = "=0.3.0-pre.2", default-features = false } -# optional dependencies +# optional dependencies (kdf) digest = { version = "0.11", features = ["alloc"], optional = true } zeroize = { version = "1.8.1", optional = true, default-features = false } +# optional dependencies (builder) +pkcs5 = { version = "0.8.0-rc.13", optional = true } +pkcs8 = { version = "0.11.0-rc.11", features = ["encryption"], optional = true } +crypto-common = { version = "0.2.0", optional = true } +hmac = { version = "0.13.0-rc.5", optional = true } +sha2 = { version = "0.11.0-rc.5", optional = true } +log = { version = "0.4.29", optional = true } +rand = { version = "0.10.0", optional = true } +rand_core = { version = "0.10.0", optional = true } +subtle = { version = "2.6.1", optional = true } + +# optional dependencies (legacy) +sha1 = { version = "0.11.0", optional = true } +cbc = { version = "0.2.0", optional = true } +des = { version = "0.9.0", optional = true } +rc2 = { version = "0.9.0", optional = true } + [dev-dependencies] hex-literal = "1" -pkcs8 = { version = "0.11.0-rc.10", features = ["pkcs5"] } +pkcs8 = { version = "0.11.0-rc.11", features = ["pkcs5"] } pkcs5 = { version = "0.8.0-rc.13", features = ["pbes2", "3des"] } sha2 = "0.11" whirlpool = "0.11" +openssl = { version = "0.10.75", features = ["vendored"] } +subtle-encoding = "0.5.1" +tempfile = "3.26.0" [features] default = ["pem"] kdf = ["dep:digest", "zeroize/alloc"] pem = ["der/pem", "x509-cert/pem"] +builder = [ + "kdf", + "dep:pkcs5", + "dep:pkcs8", + "dep:crypto-common", + "dep:hmac", + "dep:sha2", + "dep:log", + "dep:rand", + "dep:rand_core", + "dep:subtle", + "zeroize/alloc", +] +legacy = [ + "builder", + "dep:sha1", + "dep:des", + "dep:rc2", + "dep:cbc", + "pkcs5/sha1-insecure", + "pkcs5/3des", +] [package.metadata.docs.rs] all-features = true diff --git a/pkcs12/README.md b/pkcs12/README.md index 0efc760f3..56bb11293 100644 --- a/pkcs12/README.md +++ b/pkcs12/README.md @@ -12,6 +12,40 @@ Personal Information Exchange Syntax v1.1 ([RFC7292]). [Documentation][docs-link] +## Builder (`builder` feature) + +The `builder` feature provides `Pkcs12Builder` for creating a `Pfx` containing one private key +and one certificate, plus optional additional certificates (e.g. CA/intermediate chain +certificates), all protected with password-based encryption (PBES2 / PBKDF2 by default) and a +password-based MAC. + +### Features + +- PBES2 encryption with configurable PBKDF2 PRF and AES-CBC cipher +- Configurable MAC algorithm and iteration count +- Optional additional certificates (CA/intermediate chain) +- `localKeyID` and `friendlyName` attribute helpers +- Parsing and decryption of existing PKCS #12 files via `parse_pkcs12` +- Per-bag attribute preservation (key ID, friendly name, and arbitrary attributes) +- Legacy PKCS #12 PBE support (SHA-1/3DES-CBC, SHA-1/RC2-CBC) with the `legacy` feature + +### Quick start + +```toml +[dependencies] +pkcs12 = { version = "0.2", features = ["builder"] } +``` + +### `legacy` feature + +Enable the `legacy` feature to support legacy PKCS #12 PBE algorithms (SHA-1/3DES-CBC, +SHA-1/RC2-CBC) and HMAC-SHA-1 MAC. + +```toml +[dependencies] +pkcs12 = { version = "0.2", features = ["legacy"] } +``` + ## Minimum Supported Rust Version (MSRV) Policy MSRV increases are not considered breaking changes and can happen in patch releases. diff --git a/pkcs12/src/builder/asn1_utils.rs b/pkcs12/src/builder/asn1_utils.rs new file mode 100644 index 000000000..566f02090 --- /dev/null +++ b/pkcs12/src/builder/asn1_utils.rs @@ -0,0 +1,609 @@ +//! Utility functions for interacting with ASN.1 structures associated with [PKCS #12 objects](crate::pfx::Pfx) + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use log::{error, warn}; + +#[cfg(feature = "legacy")] +use sha1::Sha1; +use sha2::{Sha256, Sha384, Sha512}; + +use crate::{ + AuthenticatedSafe, CertBag, MacData, + kdf::{Pkcs12KeyType, derive_key_utf8}, + pfx::Pfx, + safe_bag::SafeContents, +}; +use cms::encrypted_data::EncryptedData; +use const_oid::ObjectIdentifier; +use const_oid::db::rfc2985::PKCS_9_AT_LOCAL_KEY_ID; +use const_oid::db::rfc5911::{ID_DATA, ID_ENCRYPTED_DATA}; +use der::{ + Any, Decode, Encode, + asn1::{ContextSpecific, OctetString}, +}; +use pkcs8::EncryptedPrivateKeyInfo; +use subtle::ConstantTimeEq; +use x509_cert::attr::{Attribute, Attributes}; +use zeroize::Zeroizing; + +use super::{ + MAX_ITERATION_COUNT, + error::{Error, Result}, + supported_algs::MacAlgorithm, +}; + +/// DER-encoded certificates extracted from a PKCS #12 safe contents. +pub struct CertContents { + /// DER-encoded main (end-entity) certificate. + pub cert: CertAndAttributes, + /// DER-encoded additional certificates (CA / intermediate chain). + pub additional_certs: Vec, +} + +/// A DER-encoded certificate together with its PKCS #12 bag attributes. +#[derive(Debug, PartialEq)] +pub struct CertAndAttributes { + /// DER-encoded certificate. + pub der: Vec, + /// Optional `localKeyID` attribute value. + pub local_key_id: Option>, + /// Optional `friendlyName` attribute value. + pub friendly_name: Option, + /// Any additional bag attributes beyond `localKeyID` and `friendlyName`. + pub other_attributes: Option>, +} + +/// Return type for [`get_key`]: the decrypted key bytes (zeroized on drop) and parsed bag attributes. +pub type KeyContents = (Zeroizing>, ParsedAttributes); + +/// Fully decoded contents of a PKCS #12 object. +pub struct Pkcs12Contents { + /// DER-encoded private key (zeroized on drop). + pub key_der: Zeroizing>, + /// Optional `localKeyID` attribute from the key bag. + pub key_id: Option>, + /// Optional `friendlyName` attribute from the key bag. + pub friendly_name: Option, + /// Any additional key bag attributes beyond `localKeyID` and `friendlyName`. + pub other_key_attributes: Option>, + /// End-entity certificate and attributes. + pub certificate: CertAndAttributes, + /// Additional certificates and attributes (CA / intermediate chain). + pub additional_certificates: Vec, +} + +/// Returns `true` if the OID identifies a known PKCS#12 legacy PBE algorithm. +/// Used without the `legacy` feature to produce a clear error message. +#[cfg(not(feature = "legacy"))] +fn is_known_legacy_pbe_oid(oid: &ObjectIdentifier) -> bool { + matches!( + *oid, + crate::PKCS_12_PBE_WITH_SHAAND3_KEY_TRIPLE_DES_CBC + | crate::PKCS_12_PBEWITH_SHAAND40_BIT_RC2_CBC + | crate::PKCS_12_PBE_WITH_SHAAND128_BIT_RC2_CBC + ) +} + +/// Returns `true` if the OID identifies a PKCS#12 legacy PBE algorithm. +#[cfg(feature = "legacy")] +fn is_pkcs12_pbe_oid(oid: &ObjectIdentifier) -> bool { + matches!( + *oid, + crate::PKCS_12_PBE_WITH_SHAAND3_KEY_TRIPLE_DES_CBC + | crate::PKCS_12_PBEWITH_SHAAND40_BIT_RC2_CBC + | crate::PKCS_12_PBE_WITH_SHAAND128_BIT_RC2_CBC + ) +} + +/// Decrypt data encrypted with a PKCS#12 legacy PBE scheme (SHA-1 based KDF with 3DES-CBC or RC2-CBC). +#[cfg(feature = "legacy")] +fn pkcs12_pbe_decrypt<'a>( + alg_oid: &ObjectIdentifier, + params_der: &[u8], + password: &str, + buffer: &'a mut [u8], +) -> Result<&'a [u8]> { + use crate::pbe_params::Pkcs12PbeParams; + use cbc::cipher::{BlockModeDecrypt, InnerIvInit, KeyIvInit, block_padding::Pkcs7}; + + let params = Pkcs12PbeParams::from_der(params_der)?; + + if params.iterations as u32 > MAX_ITERATION_COUNT { + return Err(Error::Pkcs12Builder(format!( + "The iterations limit exceeded. {} is greater than {}", + params.iterations, MAX_ITERATION_COUNT + ))); + } + + let salt = params.salt.as_bytes(); + let iterations = params.iterations; + + let (key_len, iv_len) = match *alg_oid { + crate::PKCS_12_PBE_WITH_SHAAND3_KEY_TRIPLE_DES_CBC => (24, 8), + crate::PKCS_12_PBEWITH_SHAAND40_BIT_RC2_CBC => (5, 8), + crate::PKCS_12_PBE_WITH_SHAAND128_BIT_RC2_CBC => (16, 8), + _ => { + return Err(Error::Pkcs12Builder(format!( + "Unsupported PKCS#12 PBE algorithm: {alg_oid}" + ))); + } + }; + + let key = Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::EncryptionKey, + iterations, + key_len, + )?); + let iv = Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::Iv, + iterations, + iv_len, + )?); + + match *alg_oid { + crate::PKCS_12_PBE_WITH_SHAAND3_KEY_TRIPLE_DES_CBC => { + cbc::Decryptor::::new_from_slices(&key, &iv) + .map_err(|e| { + Error::Pkcs12Builder(format!("Failed to init 3DES-CBC decryptor: {e}")) + })? + .decrypt_padded::(buffer) + .map_err(|e| Error::Pkcs12Builder(format!("3DES-CBC decryption failed: {e}"))) + } + crate::PKCS_12_PBEWITH_SHAAND40_BIT_RC2_CBC => { + let cipher = rc2::Rc2::new_with_eff_key_len(&key, 40); + cbc::Decryptor::::inner_iv_slice_init(cipher, &iv) + .map_err(|e| { + Error::Pkcs12Builder(format!("Failed to init RC2-40-CBC decryptor: {e}")) + })? + .decrypt_padded::(buffer) + .map_err(|e| Error::Pkcs12Builder(format!("RC2-40-CBC decryption failed: {e}"))) + } + crate::PKCS_12_PBE_WITH_SHAAND128_BIT_RC2_CBC => { + let cipher = rc2::Rc2::new_with_eff_key_len(&key, 128); + cbc::Decryptor::::inner_iv_slice_init(cipher, &iv) + .map_err(|e| { + Error::Pkcs12Builder(format!("Failed to init RC2-128-CBC decryptor: {e}")) + })? + .decrypt_padded::(buffer) + .map_err(|e| Error::Pkcs12Builder(format!("RC2-128-CBC decryption failed: {e}"))) + } + _ => unreachable!(), + } +} + +/// Extract certificates and optional key ID from decrypted SafeContents bytes. +/// +/// Returns a [CertContents]. The main certificate is the first `CertBag` that carries a +/// `localKeyID` attribute, or simply the first `CertBag` if none do. All remaining `CertBag` +/// entries are returned as additional certificates. +fn extract_certs_from_safe_contents(plaintext: &[u8]) -> Result { + let safe_bags = SafeContents::from_der(plaintext)?; + let mut main_cert: Option = None; + let mut additional_certs: Vec = Vec::new(); + + for safe_bag in safe_bags { + match safe_bag.bag_id { + crate::PKCS_12_CERT_BAG_OID => { + let attrs = parse_attributes(safe_bag.bag_attributes); + let cs: ContextSpecific = ContextSpecific::from_der(&safe_bag.bag_value)?; + let der = cs.value.cert_value.as_bytes().to_vec(); + + if main_cert + .as_ref() + .is_none_or(|mc| mc.cert.local_key_id.is_none() && attrs.local_key_id.is_some()) + { + // Promote this to main cert; demote any previous main to additional + if let Some(prev) = main_cert.take() { + additional_certs.push(prev.cert); + } + main_cert = Some(CertContents { + cert: CertAndAttributes { + der, + local_key_id: attrs.local_key_id, + friendly_name: attrs.friendly_name, + other_attributes: attrs.other, + }, + additional_certs: Vec::new(), + }); + } else { + additional_certs.push(CertAndAttributes { + der, + local_key_id: attrs.local_key_id, + friendly_name: attrs.friendly_name, + other_attributes: attrs.other, + }); + } + } + _ => { + warn!("Unexpected SafeBag type. Ignoring and continuing."); + } + }; + } + + match main_cert { + Some(mut cc) => { + cc.additional_certs = additional_certs; + Ok(cc) + } + None => { + error!("Failed to find certificate bag"); + Err(Error::NotFound) + } + } +} + +/// Takes an [`Any`] that notionally contains an [`OctetString`] and returns an [`AuthenticatedSafe`] +/// object or error. +/// +/// The [`Any`] value is typically the value from the [`ContentInfo`](cms::content_info::ContentInfo) included in the `auth_safe` field +/// of a [`Pfx`] object. The resulting [`AuthenticatedSafe`] contains a vector of +/// [`ContentInfo`](cms::content_info::ContentInfo) objects +pub fn get_auth_safes(content: &Any) -> Result> { + let auth_safes_os = OctetString::from_der(&content.to_der()?)?; + Ok(AuthenticatedSafe::from_der(auth_safes_os.as_bytes())?) +} + +/// Takes an [`Any`] that notionally contains an [`OctetString`] and returns a [`SafeContents`] +/// object or error. +/// +/// The [`Any`] value is typically the value from the [`ContentInfo`](cms::content_info::ContentInfo) included in an [`AuthenticatedSafe`] +/// read from the `auth_safe` field of a [`Pfx`] object. The resulting [`SafeContents`] contains a vector of +/// [`SafeBag`](crate::safe_bag::SafeBag) objects +pub fn get_safe_bags(content: &Any) -> Result { + let safe_bags_os = OctetString::from_der(&content.to_der()?)?; + Ok(SafeContents::from_der(safe_bags_os.as_bytes())?) +} + +/// Takes an [`Any`] that notionally contains an [`OctetString`] wrapping a [`SafeContents`] object. +/// Iterates over the [`SafeBag`](crate::safe_bag::SafeBag) list and decrypts the first bag of type +/// [`PKCS_12_PKCS8_KEY_BAG_OID`](crate::PKCS_12_PKCS8_KEY_BAG_OID) using the provided password, +/// returning a tuple containing the plaintext key bytes (zeroized on drop) and an optional key +/// identifier. Returns an error if no key bag is found or decryption fails. +pub fn get_key(content: &Any, password: &str) -> Result { + let safe_bags = get_safe_bags(content)?; + for safe_bag in safe_bags { + match safe_bag.bag_id { + crate::PKCS_12_PKCS8_KEY_BAG_OID => { + let key_attrs = parse_attributes(safe_bag.bag_attributes); + + // Try PKCS#12 legacy PBE first (requires legacy feature) + #[cfg(feature = "legacy")] + { + let cs_generic: ContextSpecific = + ContextSpecific::from_der(&safe_bag.bag_value)?; + if is_pkcs12_pbe_oid(&cs_generic.value.encryption_algorithm.oid) { + let params_der = cs_generic + .value + .encryption_algorithm + .parameters + .as_ref() + .ok_or_else(|| { + Error::Pkcs12Builder("Missing PKCS#12 PBE parameters".to_string()) + })? + .to_der()?; + let mut ciphertext = + Zeroizing::new(cs_generic.value.encrypted_data.as_bytes().to_vec()); + let plaintext = pkcs12_pbe_decrypt( + &cs_generic.value.encryption_algorithm.oid, + ¶ms_der, + password, + &mut ciphertext, + )?; + return Ok((Zeroizing::new(plaintext.to_vec()), key_attrs)); + } + } + + #[cfg(not(feature = "legacy"))] + { + let cs_generic: ContextSpecific = + ContextSpecific::from_der(&safe_bag.bag_value)?; + if is_known_legacy_pbe_oid(&cs_generic.value.encryption_algorithm.oid) { + return Err(Error::Pkcs12Builder( + "This P12 uses legacy PBE encryption. \ + Enable the `legacy` feature to parse it." + .to_string(), + )); + } + } + + // PBES2 path + let cs: ContextSpecific> = + ContextSpecific::from_der(&safe_bag.bag_value)?; + + if let Some(pbes2) = cs.value.encryption_algorithm.pbes2() { + if let Some(params) = pbes2.kdf.pbkdf2() { + if params.iteration_count > MAX_ITERATION_COUNT { + return Err(Error::Pkcs12Builder(format!( + "The iterations limit exceeded. {} is greater than {}", + params.iteration_count, MAX_ITERATION_COUNT + ))); + } + } + } + + let mut ciphertext = Zeroizing::new(cs.value.encrypted_data.as_bytes().to_vec()); + let plaintext = cs + .value + .encryption_algorithm + .decrypt_in_place(password, &mut ciphertext)?; + return Ok((Zeroizing::new(plaintext.to_vec()), key_attrs)); + } + _ => { + warn!("Unexpected SafeBag type. Ignoring and continuing..."); + } + }; + } + Err(Error::Pkcs12Builder(String::from( + "Failed to find SafeBag containing key", + ))) +} + +/// Takes an [`Any`] that notionally contains an [`EncryptedData`] whose payload is an encrypted +/// [`SafeContents`]. Attempts to decrypt the content using the provided password, then extracts and +/// returns a [`CertContents`] containing the DER-encoded end-entity certificate, any additional +/// certificate DER blobs, and an optional key identifier. +pub fn get_cert(content: &Any, password: &str) -> Result { + let enc_data = EncryptedData::from_der(&content.to_der()?)?; + + let Some(ciphertext_os) = enc_data.enc_content_info.encrypted_content else { + return Err(Error::Pkcs12Builder(String::from( + "Failed to read encrypted content", + ))); + }; + let mut ciphertext = Zeroizing::new(ciphertext_os.as_bytes().to_vec()); + + // Try PKCS#12 legacy PBE first (requires legacy feature) + #[cfg(feature = "legacy")] + if is_pkcs12_pbe_oid(&enc_data.enc_content_info.content_enc_alg.oid) { + let params_der = enc_data + .enc_content_info + .content_enc_alg + .parameters + .as_ref() + .ok_or_else(|| Error::Pkcs12Builder("Missing PKCS#12 PBE parameters".to_string()))? + .to_der()?; + let plaintext = pkcs12_pbe_decrypt( + &enc_data.enc_content_info.content_enc_alg.oid, + ¶ms_der, + password, + &mut ciphertext, + )?; + return extract_certs_from_safe_contents(plaintext); + } + + #[cfg(not(feature = "legacy"))] + if is_known_legacy_pbe_oid(&enc_data.enc_content_info.content_enc_alg.oid) { + return Err(Error::Pkcs12Builder( + "This P12 uses legacy PBE encryption. \ + Enable the `legacy` feature to parse it." + .to_string(), + )); + } + + // PBES2 path + let enc_params = match enc_data + .enc_content_info + .content_enc_alg + .parameters + .as_ref() + { + Some(r) => r.to_der()?, + None => { + return Err(Error::Pkcs12Builder(String::from( + "Failed to obtain reference to parameters", + ))); + } + }; + + let params = pkcs5::pbes2::Parameters::from_der(&enc_params)?; + if let Some(kdf_params) = params.kdf.pbkdf2() { + if kdf_params.iteration_count > MAX_ITERATION_COUNT { + return Err(Error::Pkcs12Builder(format!( + "The iterations limit exceeded. {} is greater than {}", + kdf_params.iteration_count, MAX_ITERATION_COUNT + ))); + } + } + + let scheme = pkcs5::EncryptionScheme::from(params.clone()); + let plaintext = scheme.decrypt_in_place(password, &mut ciphertext)?; + extract_certs_from_safe_contents(plaintext) +} + +/// Parsed bag attributes: the well-known `localKeyID` and `friendlyName` values plus any +/// remaining attributes. +pub struct ParsedAttributes { + /// Optional `localKeyID` attribute value. + pub local_key_id: Option>, + /// Optional `friendlyName` attribute value. + pub friendly_name: Option, + /// Any additional bag attributes beyond `localKeyID` and `friendlyName`. + pub other: Option>, +} + +/// Extract `localKeyID`, `friendlyName`, and any remaining attributes from an optional attribute set. +fn parse_attributes(attributes: Option) -> ParsedAttributes { + use const_oid::db::rfc2985::PKCS_9_AT_FRIENDLY_NAME; + use der::asn1::BmpString; + + let mut local_key_id = None; + let mut friendly_name = None; + let mut other = Vec::new(); + + if let Some(attributes) = attributes { + for attribute in attributes.iter() { + if attribute.oid == PKCS_9_AT_LOCAL_KEY_ID { + if let Some(value) = attribute.values.iter().next() { + local_key_id = Some(value.value().to_vec()); + } else { + warn!( + "Found a key ID attribute but it had no value. Ignoring and continuing..." + ); + } + } else if attribute.oid == PKCS_9_AT_FRIENDLY_NAME { + if let Some(value) = attribute.values.iter().next() { + if let Ok(bmp) = BmpString::from_der(&value.to_der().unwrap_or_default()) { + friendly_name = Some(bmp.to_string()); + } else { + warn!( + "Found a friendlyName attribute but could not decode the BMP string. Ignoring and continuing..." + ); + } + } else { + warn!( + "Found a friendlyName attribute but it had no value. Ignoring and continuing..." + ); + } + } else { + other.push(attribute.clone()); + } + } + } + + ParsedAttributes { + local_key_id, + friendly_name, + other: if other.is_empty() { None } else { Some(other) }, + } +} + +/// Takes a DER-encoded [PKCS #12 object](crate::pfx::Pfx) and password, attempts to decrypt it and, if successful, returns +/// a [`Pkcs12Contents`] containing the private key, the end-entity certificate, an optional key +/// identifier, and any additional certificates (e.g. CA/intermediate chain certificates). +/// +/// This method assumes this basic high-level representation of the structure (though the order of +/// the AuthenticatedSafe elements is unimportant). +/// +/// ```text +/// SEQUENCE { -- PFX +/// SEQUENCE { -- AuthSafe +/// [0] { +/// SEQUENCE { -- AuthenticatedSafes +/// SEQUENCE { -- AuthenticatedSafe +/// contentType: ID_ENCRYPTED_DATA +/// content: SafeContents (including SafeBag of type PKCS_12_CERT_BAG_OID) +/// } +/// SEQUENCE { -- AuthenticatedSafe +/// contentType: ID_DATA +/// content: SafeContents (including SafeBag of type PKCS_12_PKCS8_KEY_BAG_OID) +/// } +/// } +/// } +/// } +/// SEQUENCE { -- MacData +/// SEQUENCE { +/// SEQUENCE { +/// } +/// } +/// } +/// } +/// ``` +pub fn parse_pkcs12(der_p12: &[u8], password: &str) -> Result { + let mut recovered_cert_data = None; + let mut recovered_key_and_key_id = None; + let pfx = Pfx::from_der(der_p12)?; + let auth_safes_os = OctetString::from_der(&pfx.auth_safe.content.to_der()?)?; + if let Some(mac_data) = &pfx.mac_data { + check_mac(password, mac_data, auth_safes_os.as_bytes())?; + } else { + warn!( + "MacData was absent. While this is permitted by the specification, it may indicate a stripping attack." + ); + } + let auth_safes = get_auth_safes(&pfx.auth_safe.content)?; + for auth_safe in auth_safes { + if ID_ENCRYPTED_DATA == auth_safe.content_type { + recovered_cert_data = Some(get_cert(&auth_safe.content, password)?); + } else if ID_DATA == auth_safe.content_type { + recovered_key_and_key_id = Some(get_key(&auth_safe.content, password)?); + } + } + if let Some(cert_contents) = recovered_cert_data + && let Some((recovered_key, key_attrs)) = recovered_key_and_key_id + { + let key_id = if key_attrs.local_key_id.is_some() { + key_attrs.local_key_id + } else { + cert_contents.cert.local_key_id.clone() + }; + return Ok(Pkcs12Contents { + key_der: recovered_key, + key_id, + friendly_name: key_attrs.friendly_name, + other_key_attributes: key_attrs.other, + certificate: cert_contents.cert, + additional_certificates: cert_contents.additional_certs, + }); + } + Err(Error::NotFound) +} + +/// Check MAC given a password, an optional MacData and the content to authenticate. +fn check_mac(password: &str, mac_data: &MacData, content: &[u8]) -> Result<()> { + if mac_data.iterations < 1 { + return Err(Error::Pkcs12Builder(format!( + "Invalid MAC iteration count: {}", + mac_data.iterations + ))); + } + if mac_data.iterations as u32 > MAX_ITERATION_COUNT { + return Err(Error::Pkcs12Builder(format!( + "The iterations limit exceeded. {} is greater than {}", + mac_data.iterations, MAX_ITERATION_COUNT + ))); + } + + let md = MacAlgorithm::try_from(mac_data.mac.algorithm.oid)?; + + let mac_key = Zeroizing::new(match md { + #[cfg(feature = "legacy")] + MacAlgorithm::HmacSha1 => derive_key_utf8::( + password, + mac_data.mac_salt.as_bytes(), + Pkcs12KeyType::Mac, + mac_data.iterations, + md.output_size(), + )?, + MacAlgorithm::HmacSha256 => derive_key_utf8::( + password, + mac_data.mac_salt.as_bytes(), + Pkcs12KeyType::Mac, + mac_data.iterations, + md.output_size(), + )?, + MacAlgorithm::HmacSha384 => derive_key_utf8::( + password, + mac_data.mac_salt.as_bytes(), + Pkcs12KeyType::Mac, + mac_data.iterations, + md.output_size(), + )?, + MacAlgorithm::HmacSha512 => derive_key_utf8::( + password, + mac_data.mac_salt.as_bytes(), + Pkcs12KeyType::Mac, + mac_data.iterations, + md.output_size(), + )?, + }); + let mac = generate_mac(md, &mac_key, content)?; + + match mac.ct_eq(mac_data.mac.digest.as_bytes()).unwrap_u8() { + 1 => Ok(()), + _ => Err(Error::Pkcs12Builder(String::from( + "MAC verification failed", + ))), + } +} + +/// Generate a MAC given a MAC key and content +fn generate_mac(md: MacAlgorithm, mac_key: &[u8], content: &[u8]) -> Result> { + Ok(md.compute_hmac(mac_key, content)?) +} diff --git a/pkcs12/src/builder/error.rs b/pkcs12/src/builder/error.rs new file mode 100644 index 000000000..abb93d2a6 --- /dev/null +++ b/pkcs12/src/builder/error.rs @@ -0,0 +1,53 @@ +//! Error and Result types for the `builder` module + +use alloc::string::String; + +/// Result type for the `builder` module +pub type Result = core::result::Result; + +/// Error type for the `builder` module +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Error { + /// ASN.1 encoding/decoding error + Asn1(der::Error), + /// Invalid key length + InvalidLength(crypto_common::InvalidLength), + /// Something that was sought was not found + NotFound, + /// PKCS5-related error + Pkcs5(pkcs5::Error), + /// String-based error originating from PKCS #12 structure construction or parsing logic + Pkcs12Builder(String), +} + +impl From for Error { + fn from(err: der::Error) -> Error { + Error::Asn1(err) + } +} + +impl From for Error { + fn from(err: pkcs5::Error) -> Error { + Error::Pkcs5(err) + } +} + +impl From for Error { + fn from(err: crypto_common::InvalidLength) -> Error { + Error::InvalidLength(err) + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Asn1(e) => write!(f, "ASN.1 error: {e}"), + Error::InvalidLength(e) => write!(f, "invalid length: {e}"), + Error::NotFound => write!(f, "not found"), + Error::Pkcs5(e) => write!(f, "PKCS#5 error: {e}"), + Error::Pkcs12Builder(msg) => write!(f, "{msg}"), + } + } +} + +impl core::error::Error for Error {} diff --git a/pkcs12/src/builder/mac_data_builder.rs b/pkcs12/src/builder/mac_data_builder.rs new file mode 100644 index 000000000..82538d43c --- /dev/null +++ b/pkcs12/src/builder/mac_data_builder.rs @@ -0,0 +1,172 @@ +//! Structure to help with generating [`MacData`] objects + +use alloc::format; +use alloc::string::String; +use alloc::vec::Vec; + +#[cfg(feature = "legacy")] +use sha1::Sha1; +use sha2::{Sha256, Sha384, Sha512}; +use spki::AlgorithmIdentifier; +use zeroize::Zeroizing; + +use crate::{ + DigestInfo, MacData, + kdf::{Pkcs12KeyType, derive_key_utf8}, +}; +use der::{Any, AnyRef, Decode, asn1::OctetString}; +use log::{error, warn}; + +use super::error::{Error, Result}; + +use super::supported_algs::MacAlgorithm; + +/// Helper for building password-based [`MacData`] objects for inclusion in a [PKCS #12 object](crate::pfx::Pfx) +pub struct MacDataBuilder { + digest_algorithm: MacAlgorithm, + salt: Option>, + iterations: Option, +} +impl MacDataBuilder { + /// Creates a new MacDataBuilder instance with no salt, suitable for use with build_with_rng as-is + /// or for further customization prior to invoking build. By default, iterations will be set to 600,000. + pub fn new(digest_algorithm: MacAlgorithm) -> MacDataBuilder { + MacDataBuilder { + digest_algorithm, + salt: None, + iterations: None, + } + } + + /// Creates a new MacDataBuilder instance with the provided salt suitable for further + /// customization prior to invoking build. By default, iterations will be set to 600,000. + pub fn new_with_salt(digest_algorithm: MacAlgorithm, salt: Vec) -> MacDataBuilder { + if salt.len() < 16 { + warn!( + "The salt value passed to new_with_salt is shorter than the recommended 16 bytes." + ); + } + MacDataBuilder { + digest_algorithm, + salt: Some(salt), + iterations: None, + } + } + + /// Specify a salt value for use on the subsequent [`build`](MacDataBuilder::build) invocation. + pub fn salt(&mut self, salt: Option>) -> &mut Self { + if let Some(salt) = &salt { + if salt.len() < 16 { + warn!("The provided salt value is shorter than the recommended 16 bytes."); + } + } + self.salt = salt; + self + } + + /// Returns true if a `salt` value has been specified and false if not. + pub fn has_salt(&self) -> bool { + self.salt.is_some() + } + + /// Specify an iteration count for use on the subsequent [`build`](MacDataBuilder::build) + /// invocation. If not set, a default of 600,000 iterations is used. + pub fn iterations(&mut self, iterations: Option) -> Result<&mut Self> { + if let Some(iterations) = iterations { + if iterations > i32::MAX as u32 { + return Err(Error::Pkcs12Builder(format!( + "Invalid number of iterations provided ({iterations})" + ))); + } + } + self.iterations = iterations; + Ok(self) + } + + /// Casts the u32 to an i32 if possible, else returns 600000 as a default. + fn get_iterations(&self) -> i32 { + if let Some(iterations) = self.iterations { + if iterations <= i32::MAX as u32 { + return iterations as i32; + } else { + error!("Invalid number of iterations provided ({iterations})"); + } + } + 600000 + } + + /// Generate MAC key given a password and a salt. The returned key is zeroized on drop. + fn generate_mac_key(&self, password: &str, salt: &[u8]) -> Result>> { + let iterations = self.get_iterations(); + + match self.digest_algorithm { + MacAlgorithm::HmacSha256 => Ok(Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::Mac, + iterations, + self.digest_algorithm.output_size(), + )?)), + MacAlgorithm::HmacSha384 => Ok(Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::Mac, + iterations, + self.digest_algorithm.output_size(), + )?)), + MacAlgorithm::HmacSha512 => Ok(Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::Mac, + iterations, + self.digest_algorithm.output_size(), + )?)), + #[cfg(feature = "legacy")] + MacAlgorithm::HmacSha1 => Ok(Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::Mac, + iterations, + self.digest_algorithm.output_size(), + )?)), + } + } + + /// Generate a MAC given a MAC key and content + fn generate_mac(&self, mac_key: &[u8], content: &[u8]) -> Result> { + Ok(self.digest_algorithm.compute_hmac(mac_key, content)?) + } + + /// Builds a MacData instance using a previously specified salt value and a previously specified + /// (or default) iterations value. If no iterations value has been specified, a default of 600,000 + /// is used. + pub fn build(&self, password: &str, content: &[u8]) -> Result { + let salt = match &self.salt { + Some(salt) => salt, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No salt provided for MacData", + ))); + } + }; + + let mac_key = self.generate_mac_key(password, salt)?; + let result = self.generate_mac(&mac_key, content)?; + let mac_os = OctetString::new(&result[..])?; + let mac_salt = OctetString::new(&salt[..])?; + let params_bytes = self.digest_algorithm.parameters(); + let params_ref = Some(Any::from(AnyRef::from_der(¶ms_bytes)?)); + + Ok(MacData { + mac: DigestInfo { + algorithm: AlgorithmIdentifier { + oid: self.digest_algorithm.oid(), + parameters: params_ref, + }, + digest: mac_os, + }, + mac_salt, + iterations: self.get_iterations(), + }) + } +} diff --git a/pkcs12/src/builder/mod.rs b/pkcs12/src/builder/mod.rs new file mode 100644 index 000000000..8cd378c82 --- /dev/null +++ b/pkcs12/src/builder/mod.rs @@ -0,0 +1,955 @@ +//! Builder for PKCS #12 objects. +//! +//! This module provides [`Pkcs12Builder`] for creating a `Pfx` containing one private key and one +//! certificate, plus optional additional certificates (e.g. CA/intermediate chain certificates), +//! all protected with password-based encryption (PBES2 / PBKDF2 by default) and a password-based MAC. +//! +//! [`MacDataBuilder`] is provided for creating the `MacData` structure included in a `Pfx`. +//! +//! Helper functions [`add_key_id_attr`] and [`add_friendly_name_attr`] are provided for setting the +//! `localKeyID` and `friendlyName` PKCS #9 attributes on certificate and key bags. +//! +//! # Features +//! +//! - PBES2 encryption with configurable PBKDF2 PRF and AES-CBC cipher +//! - Configurable MAC algorithm and iteration count +//! - Optional additional certificates (CA/intermediate chain) +//! - `localKeyID` and `friendlyName` attribute helpers +//! - Parsing and decryption of existing PKCS #12 files via [`parse_pkcs12`](asn1_utils::parse_pkcs12) +//! - Per-bag attribute preservation (key ID, friendly name, and arbitrary attributes are retained for each certificate and the key bag) +//! - Legacy PKCS #12 PBE support (SHA-1/3DES-CBC, SHA-1/RC2-CBC) with the `legacy` feature + +use alloc::format; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +pub mod asn1_utils; +pub mod error; +pub mod mac_data_builder; + +pub mod supported_algs; + +#[doc(inline)] +pub use asn1_utils::{ + CertAndAttributes, CertContents, ParsedAttributes, Pkcs12Contents, parse_pkcs12, +}; +#[doc(inline)] +pub use error::{Error, Result}; +#[doc(inline)] +pub use mac_data_builder::MacDataBuilder; +#[cfg(feature = "legacy")] +#[doc(inline)] +pub use supported_algs::LegacyPbeAlgorithm; +#[doc(inline)] +pub use supported_algs::{EncryptionAlgorithm, MacAlgorithm}; + +use rand_core::CryptoRng; + +use pkcs5::{ + pbes2, + pbes2::{AES_256_CBC_OID, Kdf, PBES2_OID, Pbkdf2Params, Pbkdf2Prf}, +}; +use pkcs8::EncryptedPrivateKeyInfo; +use spki::AlgorithmIdentifier; + +#[cfg(doc)] +use crate::MacData; +use crate::{ + CertBag, PKCS_12_CERT_BAG_OID, PKCS_12_PKCS8_KEY_BAG_OID, PKCS_12_X509_CERT_OID, + pfx::{Pfx, Version}, + safe_bag::SafeBag, +}; +use cms::{ + content_info::{CmsVersion, ContentInfo}, + encrypted_data::EncryptedData, + enveloped_data::EncryptedContentInfo, +}; +use const_oid::db::{ + rfc2985::PKCS_9_AT_LOCAL_KEY_ID, + rfc5911::{ID_DATA, ID_ENCRYPTED_DATA}, +}; +use der::{ + Any, AnyRef, Decode, Encode, + asn1::{OctetString, SetOfVec}, +}; +use log::{error, warn}; +use pkcs5::pbes2::{PBKDF2_OID, Salt}; +use x509_cert::{Certificate, attr::Attribute, spki::AlgorithmIdentifierOwned}; +use zeroize::Zeroizing; + +/// Maximum number of iterations that will be performed when parsing a PKCS #12 object +pub const MAX_ITERATION_COUNT: u32 = 100_000_000; + +/// Helper for building [PKCS #12 objects](crate::pfx::Pfx) that contain one private key and one +/// certificate, plus optional additional certificates (e.g. CA/intermediate chain certificates). +/// No pairwise consistency check between the key and certificate is performed; the caller is +/// responsible for ensuring they correspond. +/// +/// For each of the key and certificate a KDF algorithm, an encryption algorithm, and a set of +/// bag attributes may be specified independently. By default, PBKDF2 with HMAC-SHA-256 is used as +/// the KDF algorithm and AES-256-CBC is used as the encryption algorithm. +/// +/// Though not recommended, if omitting [`MacData`] is desired, invoke the `omit_mac` method. +/// +/// Use [`build_with_rng`](Pkcs12Builder::build_with_rng) to let the builder generate all required +/// random material (salts and IVs) automatically. Use [`build`](Pkcs12Builder::build) only when +/// all algorithm identifiers have been fully populated beforehand. +/// +/// A builder instance should not be reused after a failed call to `build_with_rng`, as +/// internal state may be partially populated. Create a new `Pkcs12Builder` instead. +pub struct Pkcs12Builder { + cert_attributes: Option>, + cert_kdf_algorithm: Option, + cert_enc_algorithm: Option, + cert_kdf_algorithm_identifier: Option, + cert_enc_algorithm_identifier: Option, + key_attributes: Option>, + key_kdf_algorithm: Option, + key_enc_algorithm: Option, + key_kdf_algorithm_identifier: Option, + key_enc_algorithm_identifier: Option, + mac_data_builder: Option, + iterations: Option, + omit_mac: bool, + additional_certs: Vec, + #[cfg(feature = "legacy")] + cert_legacy_pbe: Option, + #[cfg(feature = "legacy")] + key_legacy_pbe: Option, + #[cfg(feature = "legacy")] + cert_legacy_pbe_salt: Option>, + #[cfg(feature = "legacy")] + key_legacy_pbe_salt: Option>, +} + +impl Default for Pkcs12Builder { + /// Generate a new Pkcs12Builder instance with all default values. + fn default() -> Self { + Pkcs12Builder::new() + } +} + +impl Pkcs12Builder { + /// Generate a new Pkcs12Builder instance with all default values. + pub fn new() -> Pkcs12Builder { + Pkcs12Builder { + cert_attributes: None, + cert_kdf_algorithm: None, + cert_enc_algorithm: None, + cert_kdf_algorithm_identifier: None, + cert_enc_algorithm_identifier: None, + key_attributes: None, + key_kdf_algorithm: None, + key_enc_algorithm: None, + key_kdf_algorithm_identifier: None, + key_enc_algorithm_identifier: None, + mac_data_builder: None, + iterations: None, + omit_mac: false, + additional_certs: vec![], + #[cfg(feature = "legacy")] + cert_legacy_pbe: None, + #[cfg(feature = "legacy")] + key_legacy_pbe: None, + #[cfg(feature = "legacy")] + cert_legacy_pbe_salt: None, + #[cfg(feature = "legacy")] + key_legacy_pbe_salt: None, + } + } + + /// Set attributes to associated with the certificate included in the generated [PKCS #12 object](crate::pfx::Pfx). + pub fn cert_attributes(&mut self, attrs: Option>) -> &mut Self { + self.cert_attributes = attrs; + self + } + /// Add an additional certificate (e.g. a CA or intermediate certificate) to include in the + /// generated [PKCS #12 object](crate::pfx::Pfx). Additional certificates are included as + /// `CertBag` entries without `localKeyID` attributes. May be called multiple times. + pub fn additional_cert(&mut self, cert: Certificate) -> &mut Self { + self.additional_certs.push(cert); + self + } + /// Set the PBKDF2 PRF to use as the KDF when protecting the certificate in the generated + /// [PKCS #12 object](crate::pfx::Pfx). A random salt is generated by + /// [`build_with_rng`](Pkcs12Builder::build_with_rng). Calling this clears any previously set + /// [`cert_kdf_algorithm_identifier`](Pkcs12Builder::cert_kdf_algorithm_identifier). + pub fn cert_kdf_algorithm(&mut self, alg: Option) -> &mut Self { + self.cert_kdf_algorithm_identifier = None; + self.cert_kdf_algorithm = alg; + self + } + /// Set the encryption algorithm to use when protecting the certificate in the generated + /// [PKCS #12 object](crate::pfx::Pfx). A random IV is generated by + /// [`build_with_rng`](Pkcs12Builder::build_with_rng). Calling this clears any previously set + /// [`cert_enc_algorithm_identifier`](Pkcs12Builder::cert_enc_algorithm_identifier). + pub fn cert_enc_algorithm(&mut self, alg: Option) -> &mut Self { + self.cert_enc_algorithm_identifier = None; + self.cert_enc_algorithm = alg; + self + } + /// Set the KDF algorithm to use when protecting the certificate in the generated PKCS #12 + /// object using a fully populated [`AlgorithmIdentifier`] (including salt and iteration count). + /// This takes precedence over [`cert_kdf_algorithm`](Pkcs12Builder::cert_kdf_algorithm) and + /// calling this clears any previously set enum value. + pub fn cert_kdf_algorithm_identifier( + &mut self, + alg: Option, + ) -> &mut Self { + if let Some(alg) = &alg + && let Some(params) = &alg.parameters + { + match params.to_der() { + Ok(der_params) => match Pbkdf2Params::from_der(&der_params) { + Ok(kdf_params) => { + if kdf_params.salt.as_bytes().len() < 16 { + warn!("Provided salt length is shorter than the recommended length"); + } + } + Err(e) => { + warn!( + "Failed to encode parameters passed into cert_kdf_algorithm_identifier as part of a salt length check with: ({e:?}). Ignoring and continuing..." + ); + } + }, + Err(e) => { + warn!( + "Failed to encode parameters passed into cert_kdf_algorithm_identifier as part of a salt length check with: ({e:?}). Ignoring and continuing..." + ); + } + } + } + + self.cert_kdf_algorithm = None; + self.cert_kdf_algorithm_identifier = alg; + self + } + /// Set the encryption algorithm to use when protecting the certificate in the generated PKCS + /// #12 object using a fully populated [`AlgorithmIdentifier`] (including IV). This takes + /// precedence over [`cert_enc_algorithm`](Pkcs12Builder::cert_enc_algorithm) and calling this + /// clears any previously set enum value. + pub fn cert_enc_algorithm_identifier( + &mut self, + alg: Option, + ) -> &mut Self { + self.cert_enc_algorithm = None; + self.cert_enc_algorithm_identifier = alg; + self + } + /// Set attributes to associated with the key included in the generated [PKCS #12 object](crate::pfx::Pfx). + pub fn key_attributes(&mut self, attrs: Option>) -> &mut Self { + self.key_attributes = attrs; + self + } + /// Set the PBKDF2 PRF to use as the KDF when protecting the key in the generated + /// [PKCS #12 object](crate::pfx::Pfx). A random salt is generated by + /// [`build_with_rng`](Pkcs12Builder::build_with_rng). Calling this clears any previously set + /// [`key_kdf_algorithm_identifier`](Pkcs12Builder::key_kdf_algorithm_identifier). + pub fn key_kdf_algorithm(&mut self, alg: Option) -> &mut Self { + self.key_kdf_algorithm_identifier = None; + self.key_kdf_algorithm = alg; + self + } + /// Set the encryption algorithm to use when protecting the key in the generated + /// [PKCS #12 object](crate::pfx::Pfx). A random IV is generated by + /// [`build_with_rng`](Pkcs12Builder::build_with_rng). Calling this clears any previously set + /// [`key_enc_algorithm_identifier`](Pkcs12Builder::key_enc_algorithm_identifier). + pub fn key_enc_algorithm(&mut self, alg: Option) -> &mut Self { + self.key_enc_algorithm_identifier = None; + self.key_enc_algorithm = alg; + self + } + /// Set the KDF algorithm to use when protecting the key in the generated PKCS #12 object + /// using a fully populated [`AlgorithmIdentifier`] (including salt and iteration count). This + /// takes precedence over [`key_kdf_algorithm`](Pkcs12Builder::key_kdf_algorithm) and calling + /// this clears any previously set enum value. + pub fn key_kdf_algorithm_identifier( + &mut self, + alg: Option, + ) -> &mut Self { + if let Some(alg) = &alg + && let Some(params) = &alg.parameters + { + match params.to_der() { + Ok(der_params) => match Pbkdf2Params::from_der(&der_params) { + Ok(kdf_params) => { + if kdf_params.salt.as_bytes().len() < 16 { + warn!("Provided salt length is shorter than the recommended length"); + } + } + Err(e) => { + warn!( + "Failed to encode parameters passed into key_kdf_algorithm_identifier as part of a salt length check with: ({e:?}). Ignoring and continuing..." + ); + } + }, + Err(e) => { + warn!( + "Failed to encode parameters passed into key_kdf_algorithm_identifier as part of a salt length check with: ({e:?}). Ignoring and continuing..." + ); + } + } + } + self.key_kdf_algorithm = None; + self.key_kdf_algorithm_identifier = alg; + self + } + /// Set the encryption algorithm to use when protecting the key in the generated PKCS #12 + /// object using a fully populated [`AlgorithmIdentifier`] (including IV). This takes + /// precedence over [`key_enc_algorithm`](Pkcs12Builder::key_enc_algorithm) and calling this + /// clears any previously set enum value. + pub fn key_enc_algorithm_identifier( + &mut self, + alg: Option, + ) -> &mut Self { + self.key_enc_algorithm = None; + self.key_enc_algorithm_identifier = alg; + self + } + /// Set a MacDataBuilder instance for use in generating a MAC for the [PKCS #12 object](crate::pfx::Pfx). + /// This sets `omit_mac` to false. + pub fn mac_data_builder(&mut self, mdb: Option) -> &mut Self { + self.omit_mac = false; + self.mac_data_builder = mdb; + self + } + + /// Clears any previously set `mac_data_builder` and sets `omit_mac` to true, which will cause + /// the generated PKCS #12 object to omit the optional [`MacData`]. + pub fn omit_mac(&mut self) -> &mut Self { + self.omit_mac = true; + self.mac_data_builder = None; + self + } + + /// Set a legacy PKCS#12 PBE algorithm for the certificate bag. When set, the certificate + /// will be encrypted using the specified legacy PBE algorithm instead of PBES2. Clears any + /// previously set PBES2 cert KDF/encryption algorithm settings. + #[cfg(feature = "legacy")] + pub fn cert_legacy_pbe_algorithm(&mut self, alg: Option) -> &mut Self { + if alg.is_some() { + self.cert_kdf_algorithm = None; + self.cert_enc_algorithm = None; + self.cert_kdf_algorithm_identifier = None; + self.cert_enc_algorithm_identifier = None; + } + self.cert_legacy_pbe = alg; + self + } + + /// Set the salt for legacy PBE encryption of the certificate bag. Only used when a legacy PBE + /// algorithm is set via [`cert_legacy_pbe_algorithm`](Pkcs12Builder::cert_legacy_pbe_algorithm). + /// When using [`build_with_rng`](Pkcs12Builder::build_with_rng), a random salt is generated + /// automatically if none is set. + #[cfg(feature = "legacy")] + pub fn cert_legacy_pbe_salt(&mut self, salt: Option>) -> &mut Self { + self.cert_legacy_pbe_salt = salt; + self + } + + /// Set the salt for legacy PBE encryption of the key bag. Only used when a legacy PBE + /// algorithm is set via [`key_legacy_pbe_algorithm`](Pkcs12Builder::key_legacy_pbe_algorithm). + /// When using [`build_with_rng`](Pkcs12Builder::build_with_rng), a random salt is generated + /// automatically if none is set. + #[cfg(feature = "legacy")] + pub fn key_legacy_pbe_salt(&mut self, salt: Option>) -> &mut Self { + self.key_legacy_pbe_salt = salt; + self + } + + /// Set a legacy PKCS#12 PBE algorithm for the key bag. When set, the key will be encrypted + /// using the specified legacy PBE algorithm instead of PBES2. Clears any previously set + /// PBES2 key KDF/encryption algorithm settings. + #[cfg(feature = "legacy")] + pub fn key_legacy_pbe_algorithm(&mut self, alg: Option) -> &mut Self { + if alg.is_some() { + self.key_kdf_algorithm = None; + self.key_enc_algorithm = None; + self.key_kdf_algorithm_identifier = None; + self.key_enc_algorithm_identifier = None; + } + self.key_legacy_pbe = alg; + self + } + + fn default_mac_data(rng: &mut R, iterations: u32) -> Result + where + R: CryptoRng, + { + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let mut md_builder = MacDataBuilder::new_with_salt(MacAlgorithm::HmacSha256, salt); + md_builder.iterations(Some(iterations))?; + Ok(md_builder) + } + fn default_kdf_alg(rng: &mut R, iteration_count: u32) -> Result + where + R: CryptoRng, + { + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let cert_kdf_params = Pbkdf2Params { + salt: Salt::new(salt)?, + iteration_count, + key_length: None, + prf: Pbkdf2Prf::HmacWithSha256, + }; + let enc_cert_kdf_params = cert_kdf_params.to_der()?; + let enc_cert_kdf_params_ref = AnyRef::try_from(enc_cert_kdf_params.as_slice())?; + Ok(AlgorithmIdentifierOwned { + oid: PBKDF2_OID, + parameters: Some(Any::from(enc_cert_kdf_params_ref)), + }) + } + + fn default_enc_alg(rng: &mut R) -> Result + where + R: CryptoRng, + { + let mut iv = vec![0_u8; 16]; + rng.fill_bytes(iv.as_mut_slice()); + + let cert_iv = OctetString::new(iv)?.to_der()?; + let cert_iv_ref = AnyRef::try_from(cert_iv.as_slice())?; + Ok(AlgorithmIdentifier { + oid: AES_256_CBC_OID, + parameters: Some(Any::from(cert_iv_ref)), + }) + } + + /// Set the PBKDF2 iteration count used for both the key and certificate encryption KDFs as + /// well as the MAC KDF (except where a custom MacDataBuilder is supplied). If not set, a + /// default of 600,000 iterations is used. Has no effect when algorithm identifiers are supplied + /// via [`cert_kdf_algorithm_identifier`](Pkcs12Builder::cert_kdf_algorithm_identifier) or + /// [`key_kdf_algorithm_identifier`](Pkcs12Builder::key_kdf_algorithm_identifier), since those + /// already encode the iteration count. + pub fn iterations(&mut self, iterations: Option) -> Result<&mut Self> { + if let Some(iterations) = iterations { + if iterations > i32::MAX as u32 { + return Err(Error::Pkcs12Builder(format!( + "Invalid number of iterations provided ({iterations})" + ))); + } + } + self.iterations = iterations; + Ok(self) + } + + /// Casts the u32 to an i32 if possible, else returns 600000 as a default. + fn get_iterations(&self) -> i32 { + if let Some(iterations) = self.iterations { + if iterations <= i32::MAX as u32 { + return iterations as i32; + } else { + error!("Invalid number of iterations provided ({iterations})"); + } + } + 600000 + } + + /// Builds a [PKCS #12 object](crate::pfx::Pfx) containing the provided certificate and key protected using password-based + /// encryption and MAC. Where KDF, encryption or MAC details have not been previously specified + /// default values are used with the provided RNG used to generate any necessary random values. + /// A default iteration count of 600,000 is used if none is specified. + pub fn build_with_rng( + &mut self, + certificate: &Certificate, + key: &[u8], + password: &str, + rng: &mut R, + ) -> Result> + where + R: CryptoRng, + { + if let Some(prf) = self.cert_kdf_algorithm { + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let cert_kdf_params = Pbkdf2Params { + salt: Salt::new(salt)?, + iteration_count: self.iterations.unwrap_or(600000), + key_length: None, + prf, + }; + let enc_cert_kdf_params = cert_kdf_params.to_der()?; + let enc_cert_kdf_params_ref = AnyRef::try_from(enc_cert_kdf_params.as_slice())?; + self.cert_kdf_algorithm_identifier = Some(AlgorithmIdentifierOwned { + oid: PBKDF2_OID, + parameters: Some(Any::from(enc_cert_kdf_params_ref)), + }); + } + + if let Some(enc_alg) = &self.cert_enc_algorithm { + let mut iv = vec![0_u8; 16]; + rng.fill_bytes(iv.as_mut_slice()); + + let cert_iv = OctetString::new(iv)?.to_der()?; + let cert_iv_ref = AnyRef::try_from(cert_iv.as_slice())?; + self.cert_enc_algorithm_identifier = Some(AlgorithmIdentifier { + oid: enc_alg.oid(), + parameters: Some(Any::from(cert_iv_ref)), + }); + } + + if let Some(prf) = self.key_kdf_algorithm { + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let key_kdf_params = Pbkdf2Params { + salt: Salt::new(salt)?, + iteration_count: self.iterations.unwrap_or(600000), + key_length: None, + prf, + }; + let enc_key_kdf_params = key_kdf_params.to_der()?; + let enc_key_kdf_params_ref = AnyRef::try_from(enc_key_kdf_params.as_slice())?; + self.key_kdf_algorithm_identifier = Some(AlgorithmIdentifierOwned { + oid: PBKDF2_OID, + parameters: Some(Any::from(enc_key_kdf_params_ref)), + }); + } + + if let Some(enc_alg) = &self.key_enc_algorithm { + let mut iv = vec![0_u8; 16]; + rng.fill_bytes(iv.as_mut_slice()); + + let key_iv = OctetString::new(iv)?.to_der()?; + let key_iv_ref = AnyRef::try_from(key_iv.as_slice())?; + self.key_enc_algorithm_identifier = Some(AlgorithmIdentifier { + oid: enc_alg.oid(), + parameters: Some(Any::from(key_iv_ref)), + }); + } + + #[cfg(feature = "legacy")] + let use_cert_legacy = self.cert_legacy_pbe.is_some(); + #[cfg(not(feature = "legacy"))] + let use_cert_legacy = false; + + #[cfg(feature = "legacy")] + let use_key_legacy = self.key_legacy_pbe.is_some(); + #[cfg(not(feature = "legacy"))] + let use_key_legacy = false; + + if !use_cert_legacy && self.cert_kdf_algorithm_identifier.is_none() { + self.cert_kdf_algorithm_identifier = + Some(Self::default_kdf_alg(rng, self.get_iterations() as u32)?); + } + if !use_cert_legacy && self.cert_enc_algorithm_identifier.is_none() { + self.cert_enc_algorithm_identifier = Some(Self::default_enc_alg(rng)?); + } + if !use_key_legacy && self.key_kdf_algorithm_identifier.is_none() { + self.key_kdf_algorithm_identifier = + Some(Self::default_kdf_alg(rng, self.get_iterations() as u32)?); + } + if !use_key_legacy && self.key_enc_algorithm_identifier.is_none() { + self.key_enc_algorithm_identifier = Some(Self::default_enc_alg(rng)?); + } + if self.mac_data_builder.is_none() && !self.omit_mac { + self.mac_data_builder = Some(Self::default_mac_data( + rng, + self.iterations.unwrap_or(600000), + )?); + } + if let Some(mdb) = &mut self.mac_data_builder + && !mdb.has_salt() + { + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + mdb.salt(Some(salt)); + } + #[cfg(feature = "legacy")] + { + if self.cert_legacy_pbe.is_some() && self.cert_legacy_pbe_salt.is_none() { + let mut salt = vec![0u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + self.cert_legacy_pbe_salt = Some(salt); + } + if self.key_legacy_pbe.is_some() && self.key_legacy_pbe_salt.is_none() { + let mut salt = vec![0u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + self.key_legacy_pbe_salt = Some(salt); + } + } + let result = self.build(certificate, key, password); + self.cert_kdf_algorithm_identifier = None; + self.cert_enc_algorithm_identifier = None; + self.key_kdf_algorithm_identifier = None; + self.key_enc_algorithm_identifier = None; + #[cfg(feature = "legacy")] + { + self.cert_legacy_pbe_salt = None; + self.key_legacy_pbe_salt = None; + } + if let Some(mdb) = &mut self.mac_data_builder { + mdb.salt(None); + } + + result + } + + /// Build PBES2 EncryptedData for the certificate bag. + fn build_pbes2_cert_encrypted_data( + &self, + der_cert_safe_bags: &[u8], + password: &str, + ) -> Result { + let der_cert_kdf_alg = match &self.cert_kdf_algorithm_identifier { + Some(cert_kdf_alg) => match &cert_kdf_alg.parameters { + Some(params) => params.to_der()?, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No parameters provided for certificate KDF algorithm", + ))); + } + }, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No certificate KDF algorithm provided", + ))); + } + }; + + let cert_kdf = Kdf::from(Pbkdf2Params::from_der(&der_cert_kdf_alg)?); + + let der_cert_enc_alg = match &self.cert_enc_algorithm_identifier { + Some(cert_enc_alg) => cert_enc_alg.to_der()?, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No certificate encryption algorithm provided", + ))); + } + }; + let cert_encryption = pbes2::EncryptionScheme::from_der(&der_cert_enc_alg)?; + + let cert_params = pbes2::Parameters { + kdf: cert_kdf, + encryption: cert_encryption, + }; + let cert_scheme = pkcs5::EncryptionScheme::from(cert_params.clone()); + let mut enc_buf = Zeroizing::new(vec![]); + enc_buf.extend_from_slice(der_cert_safe_bags); + enc_buf.extend_from_slice(&[0u8; 16]); + let cert_ciphertext = + match cert_scheme.encrypt_in_place(password, &mut enc_buf, der_cert_safe_bags.len()) { + Ok(ct) => ct, + Err(e) => { + return Err(Error::Pkcs12Builder(format!( + "Failed to encrypt certificate: {e:?}" + ))); + } + }; + + let der_cert_params = cert_params.to_der()?; + let der_cert_params_ref = AnyRef::try_from(der_cert_params.as_slice())?; + + Ok(EncryptedData { + version: CmsVersion::V0, + enc_content_info: EncryptedContentInfo { + content_type: ID_DATA, + content_enc_alg: AlgorithmIdentifier { + oid: PBES2_OID, + parameters: Some(Any::from(der_cert_params_ref)), + }, + encrypted_content: Some(OctetString::new(cert_ciphertext)?), + }, + unprotected_attrs: None, + }) + } + + /// Build PBES2 encrypted DER for the key bag. + fn build_pbes2_key_encrypted_data(&self, key: &[u8], password: &str) -> Result> { + let der_key_kdf_alg = match &self.key_kdf_algorithm_identifier { + Some(key_kdf_alg) => match &key_kdf_alg.parameters { + Some(params) => params.to_der()?, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No parameters provided for key KDF algorithm", + ))); + } + }, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No key KDF algorithm provided", + ))); + } + }; + let key_kdf = Kdf::from(Pbkdf2Params::from_der(&der_key_kdf_alg)?); + + let der_key_enc_alg = match &self.key_enc_algorithm_identifier { + Some(key_enc_alg) => key_enc_alg.to_der()?, + None => { + return Err(Error::Pkcs12Builder(String::from( + "No key encryption algorithm provided", + ))); + } + }; + let key_encryption = pbes2::EncryptionScheme::from_der(&der_key_enc_alg)?; + + let key_params = pbes2::Parameters { + kdf: key_kdf, + encryption: key_encryption, + }; + let key_scheme = pkcs5::EncryptionScheme::from(key_params.clone()); + let mut enc_buf = Zeroizing::new(key.to_vec()); + enc_buf.extend_from_slice(&[0u8; 16]); + let key_ciphertext = match key_scheme.encrypt_in_place(password, &mut enc_buf, key.len()) { + Ok(ct) => ct, + Err(e) => { + return Err(Error::Pkcs12Builder(format!( + "Failed to encrypt key: {e:?}" + ))); + } + }; + + let enc_epki = EncryptedPrivateKeyInfo { + encryption_algorithm: key_scheme, + encrypted_data: OctetString::new(key_ciphertext)?, + }; + Ok(enc_epki.to_der()?) + } + + /// Builds a [PKCS #12 object](crate::pfx::Pfx) containing the provided certificate and key protected using password-based + /// encryption and MAC. KDF, encryption and, except where `omit_mac` is used, MAC information must have been previously provided to + /// successfully use this function. To use default values, use the build_with_rng function. + pub fn build(&self, certificate: &Certificate, key: &[u8], password: &str) -> Result> { + let der_cert = certificate.to_der()?; + let cert_bag = CertBag { + cert_id: PKCS_12_X509_CERT_OID, + cert_value: OctetString::new(der_cert.clone())?, + }; + let der_cert_bag_inner = cert_bag.to_der()?; + let cert_safe_bag = SafeBag { + bag_id: PKCS_12_CERT_BAG_OID, + bag_value: der_cert_bag_inner, + bag_attributes: self.cert_attributes.clone(), + }; + let mut cert_safe_bags = vec![cert_safe_bag]; + for additional_cert in &self.additional_certs { + let der_additional = additional_cert.to_der()?; + let additional_bag = CertBag { + cert_id: PKCS_12_X509_CERT_OID, + cert_value: OctetString::new(der_additional)?, + }; + cert_safe_bags.push(SafeBag { + bag_id: PKCS_12_CERT_BAG_OID, + bag_value: additional_bag.to_der()?, + bag_attributes: None, + }); + } + let der_cert_safe_bags = cert_safe_bags.to_der()?; + + // --- Cert bag encryption --- + #[cfg(feature = "legacy")] + let enc_data1 = if let Some(legacy_alg) = &self.cert_legacy_pbe { + let iterations = self.get_iterations(); + let salt = self.cert_legacy_pbe_salt.as_ref().ok_or_else(|| { + Error::Pkcs12Builder(String::from( + "No salt provided for certificate legacy PBE. Use build_with_rng to generate salts automatically.", + )) + })?; + let cert_ciphertext = + pkcs12_pbe_encrypt(legacy_alg, password, salt, iterations, &der_cert_safe_bags)?; + + let pbe_params = crate::pbe_params::Pkcs12PbeParams { + salt: OctetString::new(salt.clone())?, + iterations, + }; + let der_pbe_params = pbe_params.to_der()?; + let der_pbe_params_ref = AnyRef::try_from(der_pbe_params.as_slice())?; + + EncryptedData { + version: CmsVersion::V0, + enc_content_info: EncryptedContentInfo { + content_type: ID_DATA, + content_enc_alg: AlgorithmIdentifier { + oid: legacy_alg.oid(), + parameters: Some(Any::from(der_pbe_params_ref)), + }, + encrypted_content: Some(OctetString::new(cert_ciphertext)?), + }, + unprotected_attrs: None, + } + } else { + self.build_pbes2_cert_encrypted_data(&der_cert_safe_bags, password)? + }; + + #[cfg(not(feature = "legacy"))] + let enc_data1 = self.build_pbes2_cert_encrypted_data(&der_cert_safe_bags, password)?; + + let der_enc_data1 = enc_data1.to_der()?; + let der_data_ref1 = AnyRef::try_from(der_enc_data1.as_slice())?; + + // --- Key bag encryption --- + #[cfg(feature = "legacy")] + let der_enc_epki = if let Some(legacy_alg) = &self.key_legacy_pbe { + let iterations = self.get_iterations(); + let salt = self.key_legacy_pbe_salt.as_ref().ok_or_else(|| { + Error::Pkcs12Builder(String::from( + "No salt provided for key legacy PBE. Use build_with_rng to generate salts automatically.", + )) + })?; + let key_ciphertext = pkcs12_pbe_encrypt(legacy_alg, password, salt, iterations, key)?; + + let pbe_params = crate::pbe_params::Pkcs12PbeParams { + salt: OctetString::new(salt.clone())?, + iterations, + }; + let der_pbe_params = pbe_params.to_der()?; + let der_pbe_params_ref = AnyRef::try_from(der_pbe_params.as_slice())?; + + let epki = crate::pbe_params::EncryptedPrivateKeyInfo { + encryption_algorithm: AlgorithmIdentifierOwned { + oid: legacy_alg.oid(), + parameters: Some(Any::from(der_pbe_params_ref)), + }, + encrypted_data: OctetString::new(key_ciphertext)?, + }; + epki.to_der()? + } else { + self.build_pbes2_key_encrypted_data(key, password)? + }; + + #[cfg(not(feature = "legacy"))] + let der_enc_epki = self.build_pbes2_key_encrypted_data(key, password)?; + + let shrouded_key_bag = SafeBag { + bag_id: PKCS_12_PKCS8_KEY_BAG_OID, + bag_value: der_enc_epki, + bag_attributes: self.key_attributes.clone(), + }; + let sb = vec![shrouded_key_bag]; + let der_enc_data2 = sb.to_der()?; + let os2 = OctetString::new(der_enc_data2)?.to_der()?; + let der_data_ref2 = AnyRef::try_from(os2.as_slice())?; + + let auth_safes = vec![ + ContentInfo { + content_type: ID_ENCRYPTED_DATA, + content: Any::from(der_data_ref1), + }, + ContentInfo { + content_type: ID_DATA, + content: Any::from(der_data_ref2), + }, + ]; + + let content_bytes = auth_safes.to_der()?; + let os = OctetString::new(content_bytes.clone())?.to_der()?; + let content = AnyRef::try_from(os.as_slice())?; + + let auth_safe = ContentInfo { + content_type: ID_DATA, + content: Any::from(content), + }; + + let mac_data = match &self.mac_data_builder { + Some(md_build) => Some(md_build.build(password, &content_bytes)?), + None => { + if !self.omit_mac { + return Err(Error::Pkcs12Builder(String::from( + "No MacData builder was found but one was expected. This is a bug.", + ))); + } + None + } + }; + + let pfx = Pfx { + version: Version::V3, + auth_safe, + mac_data, + }; + Ok(pfx.to_der()?) + } +} + +/// Encrypt data using a PKCS#12 legacy PBE scheme (SHA-1 based KDF with 3DES-CBC or RC2-CBC). +#[cfg(feature = "legacy")] +fn pkcs12_pbe_encrypt( + alg: &LegacyPbeAlgorithm, + password: &str, + salt: &[u8], + iterations: i32, + plaintext: &[u8], +) -> Result> { + use crate::kdf::{Pkcs12KeyType, derive_key_utf8}; + use cbc::cipher::{BlockModeEncrypt, InnerIvInit, KeyIvInit, block_padding::Pkcs7}; + use sha1::Sha1; + + let key = Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::EncryptionKey, + iterations, + alg.key_len(), + )?); + let iv = Zeroizing::new(derive_key_utf8::( + password, + salt, + Pkcs12KeyType::Iv, + iterations, + alg.iv_len(), + )?); + + // Allocate buffer with room for padding (up to one block = 8 bytes) + let mut buf = vec![0u8; plaintext.len() + 8]; + buf[..plaintext.len()].copy_from_slice(plaintext); + + match alg { + LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc => { + let ct = cbc::Encryptor::::new_from_slices(&key, &iv) + .map_err(|e| { + Error::Pkcs12Builder(format!("Failed to init 3DES-CBC encryptor: {e}")) + })? + .encrypt_padded::(&mut buf, plaintext.len()) + .map_err(|e| Error::Pkcs12Builder(format!("3DES-CBC encryption failed: {e}")))?; + Ok(ct.to_vec()) + } + LegacyPbeAlgorithm::ShaAnd128BitRc2Cbc => { + let cipher = rc2::Rc2::new_with_eff_key_len(&key, 128); + let ct = cbc::Encryptor::::inner_iv_slice_init(cipher, &iv) + .map_err(|e| { + Error::Pkcs12Builder(format!("Failed to init RC2-128-CBC encryptor: {e}")) + })? + .encrypt_padded::(&mut buf, plaintext.len()) + .map_err(|e| Error::Pkcs12Builder(format!("RC2-128-CBC encryption failed: {e}")))?; + Ok(ct.to_vec()) + } + } +} + +/// Adds an [`Attribute`] containing the provided key ID to the provided set of attributes. +pub fn add_key_id_attr(attrs: &mut SetOfVec, key_id: &[u8]) -> Result<()> { + let attr_bytes = OctetString::new(key_id)?.to_der()?; + let attr_bytes_ref = AnyRef::try_from(attr_bytes.as_slice())?; + let mut values = SetOfVec::new(); + values.insert(Any::from(attr_bytes_ref))?; + let attr = Attribute { + oid: PKCS_9_AT_LOCAL_KEY_ID, + values, + }; + Ok(attrs.insert(attr)?) +} + +/// Adds an [`Attribute`] containing the provided friendly name to the provided set of attributes. +/// +/// The friendly name is encoded as a BMP string (UCS-2) per PKCS #9. +pub fn add_friendly_name_attr(attrs: &mut SetOfVec, name: &str) -> Result<()> { + use const_oid::db::rfc2985::PKCS_9_AT_FRIENDLY_NAME; + use der::asn1::BmpString; + + let bmp = BmpString::from_utf8(name)?; + let attr_bytes = bmp.to_der()?; + let attr_bytes_ref = AnyRef::try_from(attr_bytes.as_slice())?; + let mut values = SetOfVec::new(); + values.insert(Any::from(attr_bytes_ref))?; + let attr = Attribute { + oid: PKCS_9_AT_FRIENDLY_NAME, + values, + }; + Ok(attrs.insert(attr)?) +} diff --git a/pkcs12/src/builder/supported_algs.rs b/pkcs12/src/builder/supported_algs.rs new file mode 100644 index 000000000..5ef59657e --- /dev/null +++ b/pkcs12/src/builder/supported_algs.rs @@ -0,0 +1,172 @@ +//! Supported MAC and encryption algorithms + +use alloc::format; +use alloc::vec; +use alloc::vec::Vec; + +use super::Error; +#[cfg(feature = "legacy")] +use const_oid::db::rfc5912::ID_SHA_1; +use const_oid::{ + ObjectIdentifier, + db::rfc5912::{ID_SHA_256, ID_SHA_384, ID_SHA_512}, +}; +use hmac::{Hmac, KeyInit, Mac}; +#[cfg(feature = "legacy")] +use sha1::Sha1; +use sha2::{Sha256, Sha384, Sha512}; + +/// Supported MAC algorithms. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum MacAlgorithm { + /// HMAC SHA1 (verification only, legacy/interoperability) + #[cfg(feature = "legacy")] + HmacSha1, + /// HMAC SHA256 + HmacSha256, + /// HMAC SHA384 + HmacSha384, + /// HMAC SHA512 + HmacSha512, +} + +impl TryFrom for MacAlgorithm { + type Error = Error; + + /// Attempt to map an OID to a [`MacAlgorithm`] variant. Returns an error if the OID is not + /// one of the supported HMAC-SHA-2 algorithms. + fn try_from(value: ObjectIdentifier) -> Result { + match value { + #[cfg(feature = "legacy")] + ID_SHA_1 => Ok(Self::HmacSha1), + ID_SHA_256 => Ok(Self::HmacSha256), + ID_SHA_384 => Ok(Self::HmacSha384), + ID_SHA_512 => Ok(Self::HmacSha512), + _ => Err(Error::Pkcs12Builder(format!( + "{} is not a recognized MAC algorithm", + value + ))), + } + } +} +impl MacAlgorithm { + /// Return the OID of the algorithm. + pub fn oid(&self) -> ObjectIdentifier { + match self { + #[cfg(feature = "legacy")] + MacAlgorithm::HmacSha1 => ID_SHA_1, + MacAlgorithm::HmacSha256 => ID_SHA_256, + MacAlgorithm::HmacSha384 => ID_SHA_384, + MacAlgorithm::HmacSha512 => ID_SHA_512, + } + } + + /// Return the output size of the associated digest algorithm. + pub fn output_size(&self) -> usize { + match self { + #[cfg(feature = "legacy")] + MacAlgorithm::HmacSha1 => 20, + MacAlgorithm::HmacSha256 => 32, + MacAlgorithm::HmacSha384 => 48, + MacAlgorithm::HmacSha512 => 64, + } + } + + /// Return DER-encoded parameters for inclusion in an `AlgorithmIdentifier`. For all supported + /// HMAC-SHA-2 algorithms this is a DER-encoded NULL (`0x05 0x00`). + pub fn parameters(&self) -> Vec { + vec![0x05, 0x00] + } + + /// Compute an HMAC over `content` using the given `key`. + pub fn compute_hmac( + &self, + key: &[u8], + content: &[u8], + ) -> Result, digest::InvalidLength> { + match self { + #[cfg(feature = "legacy")] + MacAlgorithm::HmacSha1 => { + let mut mac = Hmac::::new_from_slice(key)?; + mac.update(content); + Ok(mac.finalize().into_bytes().to_vec()) + } + MacAlgorithm::HmacSha256 => { + let mut mac = Hmac::::new_from_slice(key)?; + mac.update(content); + Ok(mac.finalize().into_bytes().to_vec()) + } + MacAlgorithm::HmacSha384 => { + let mut mac = Hmac::::new_from_slice(key)?; + mac.update(content); + Ok(mac.finalize().into_bytes().to_vec()) + } + MacAlgorithm::HmacSha512 => { + let mut mac = Hmac::::new_from_slice(key)?; + mac.update(content); + Ok(mac.finalize().into_bytes().to_vec()) + } + } + } +} + +/// Supported encryption algorithms. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EncryptionAlgorithm { + /// AES128 CBC + Aes128Cbc, + /// AES-192 CBC + Aes192Cbc, + /// AES-256 CBC + Aes256Cbc, +} + +impl EncryptionAlgorithm { + /// Return the OID of the algorithm. + pub fn oid(&self) -> ObjectIdentifier { + match self { + EncryptionAlgorithm::Aes128Cbc => const_oid::db::rfc5911::ID_AES_128_CBC, + EncryptionAlgorithm::Aes192Cbc => const_oid::db::rfc5911::ID_AES_192_CBC, + EncryptionAlgorithm::Aes256Cbc => const_oid::db::rfc5911::ID_AES_256_CBC, + } + } +} + +/// Legacy PKCS#12 PBE algorithms (SHA-1 based KDF with 3DES-CBC or RC2-CBC). +/// +/// These algorithms are required for interoperability with iOS `SecPKCS12Import`, +/// which does not support PBES2 (PBKDF2 + AES-CBC). +#[cfg(feature = "legacy")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LegacyPbeAlgorithm { + /// pbeWithSHAAnd3-KeyTripleDES-CBC (OID 1.2.840.113549.1.12.1.3) + ShaAnd3KeyTripleDesCbc, + /// pbeWithSHAAnd128BitRC2-CBC (OID 1.2.840.113549.1.12.1.5) + ShaAnd128BitRc2Cbc, +} + +#[cfg(feature = "legacy")] +impl LegacyPbeAlgorithm { + /// Return the OID of the algorithm. + pub fn oid(&self) -> ObjectIdentifier { + match self { + LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc => { + crate::PKCS_12_PBE_WITH_SHAAND3_KEY_TRIPLE_DES_CBC + } + LegacyPbeAlgorithm::ShaAnd128BitRc2Cbc => crate::PKCS_12_PBE_WITH_SHAAND128_BIT_RC2_CBC, + } + } + + /// Return the encryption key length in bytes. + pub fn key_len(&self) -> usize { + match self { + LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc => 24, + LegacyPbeAlgorithm::ShaAnd128BitRc2Cbc => 16, + } + } + + /// Return the IV length in bytes. + pub fn iv_len(&self) -> usize { + 8 + } +} diff --git a/pkcs12/src/lib.rs b/pkcs12/src/lib.rs index ac8a29436..d3b29c957 100644 --- a/pkcs12/src/lib.rs +++ b/pkcs12/src/lib.rs @@ -23,6 +23,9 @@ pub mod safe_bag; #[cfg(feature = "kdf")] pub mod kdf; +#[cfg(feature = "builder")] +pub mod builder; + mod authenticated_safe; mod bag_type; mod cert_type; @@ -103,9 +106,4 @@ pub const PKCS_12_X509_CERT_OID: ObjectIdentifier = pub const PKCS_12_SDSI_CERT_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.22.2"); -// todo: return the friendly name if present? (minimally, defer until BMPString support is available) // todo: support separate mac and encryption passwords? -// todo: add decryption support -// todo: add more encryption tests -// todo: add a builder -// todo: add RC2 support diff --git a/pkcs12/tests/builder.rs b/pkcs12/tests/builder.rs new file mode 100644 index 000000000..5deba0705 --- /dev/null +++ b/pkcs12/tests/builder.rs @@ -0,0 +1,16 @@ +#![cfg(feature = "builder")] + +#[path = "builder/openssl_interop.rs"] +mod openssl_interop; +#[path = "builder/pkcs12_builder.rs"] +mod pkcs12_builder; + +#[cfg(feature = "legacy")] +#[path = "builder/decrypt_3des.rs"] +mod decrypt_3des; +#[cfg(feature = "legacy")] +#[path = "builder/legacy_pbe.rs"] +mod legacy_pbe; +#[cfg(feature = "legacy")] +#[path = "builder/sha1_mac.rs"] +mod sha1_mac; diff --git a/pkcs12/tests/builder/data/README.md b/pkcs12/tests/builder/data/README.md new file mode 100644 index 000000000..3cd1140d6 --- /dev/null +++ b/pkcs12/tests/builder/data/README.md @@ -0,0 +1,32 @@ +# Test Fixture Files + +## Generated fixtures (password: `hunter2`) + +All three files contain the same RSA-2048 key and self-signed certificate, encrypted +with `pbeWithSHA1And3-KeyTripleDES-CBC`. They differ only in iteration count. + +Public key fingerprint (SHA-256 of DER pubkey, oracle for decryption tests): +`adbe3a3bb8f734eed65bb2c841ebce69c3d0fac37722c8d794de5b37399411fd` + +Generated with: +```bash +openssl genrsa -out key.pem 2048 +openssl req -x509 -new -key key.pem -out cert.pem -days 3650 -nodes -subj "/CN=pkcs12-test/O=test/C=US" +openssl pkcs12 -export -legacy -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \ + -inkey key.pem -in cert.pem -out test-3des-iter.p12 -passout pass:hunter2 [-iter ] +``` + +| File | Iterations | Cipher | +|------|-----------|--------| +| `test-3des-iter1.p12` | 1 | pbeWithSHA1And3-KeyTripleDES-CBC | +| `test-3des-iter2048.p12` | 2048 | pbeWithSHA1And3-KeyTripleDES-CBC | +| `test-3des-iter100000.p12` | 100000 | pbeWithSHA1And3-KeyTripleDES-CBC | + +## pyca/cryptography fixture (password: `cryptography`) + +`pyca-cert-rc2-key-3des.p12` — from the pyca/cryptography test vectors. +- Certificate: pbeWithSHA1And40BitRC2-CBC (not tested here) +- Private key: EC (`id-ecPublicKey`), pbeWithSHA1And3-KeyTripleDES-CBC, 2048 iterations + +Public key fingerprint (SHA-256 of DER pubkey): +`c5eacb73dd8324007d050afcc807fccd09c1f752634eeafaffc0872b35da4383` diff --git a/pkcs12/tests/builder/data/pyca-cert-rc2-key-3des.p12 b/pkcs12/tests/builder/data/pyca-cert-rc2-key-3des.p12 new file mode 100644 index 000000000..9041671be Binary files /dev/null and b/pkcs12/tests/builder/data/pyca-cert-rc2-key-3des.p12 differ diff --git a/pkcs12/tests/builder/data/test-3des-iter1.p12 b/pkcs12/tests/builder/data/test-3des-iter1.p12 new file mode 100644 index 000000000..6b3df06be Binary files /dev/null and b/pkcs12/tests/builder/data/test-3des-iter1.p12 differ diff --git a/pkcs12/tests/builder/data/test-3des-iter100000.p12 b/pkcs12/tests/builder/data/test-3des-iter100000.p12 new file mode 100644 index 000000000..36799d5ed Binary files /dev/null and b/pkcs12/tests/builder/data/test-3des-iter100000.p12 differ diff --git a/pkcs12/tests/builder/data/test-3des-iter2048.p12 b/pkcs12/tests/builder/data/test-3des-iter2048.p12 new file mode 100644 index 000000000..47ad409e8 Binary files /dev/null and b/pkcs12/tests/builder/data/test-3des-iter2048.p12 differ diff --git a/pkcs12/tests/builder/decrypt_3des.rs b/pkcs12/tests/builder/decrypt_3des.rs new file mode 100644 index 000000000..10ec68e92 --- /dev/null +++ b/pkcs12/tests/builder/decrypt_3des.rs @@ -0,0 +1,54 @@ +//! Uses test data from PR2280 from RustCrypto/formats + +use pkcs12::builder::parse_pkcs12; +use sha2::{Digest, Sha256}; + +#[test] +fn decrypt_3des() { + let p12_iter1 = include_bytes!("data/test-3des-iter1.p12"); + let p12_iter2048 = include_bytes!("data/test-3des-iter2048.p12"); + let p12_iter100000 = include_bytes!("data/test-3des-iter100000.p12"); + let password = "hunter2"; + let contents1 = parse_pkcs12(p12_iter1, password).unwrap(); + let contents2048 = parse_pkcs12(p12_iter2048, password).unwrap(); + let contents100000 = parse_pkcs12(p12_iter100000, password).unwrap(); + let rsa_key_der_sha256 = + hex_literal::hex!("ccdf40f8d0881c5aa3cb9c563399f5fb590f7615ef7da4d057031bc809c9190d"); + let rsa_digest = Sha256::digest(contents1.key_der.clone()); + assert_eq!(rsa_digest.as_slice(), rsa_key_der_sha256); + assert_eq!(contents1.key_id, contents2048.key_id); + assert_eq!(contents1.key_der, contents2048.key_der); + assert_eq!(contents1.certificate.der, contents2048.certificate.der); + assert_eq!( + contents1.certificate.local_key_id, + contents2048.certificate.local_key_id + ); + assert!( + contents1 + .additional_certificates + .iter() + .zip(contents2048.additional_certificates.iter()) + .all(|(a, b)| a.der == b.der) + ); + assert_eq!(contents1.key_id, contents100000.key_id); + assert_eq!(contents1.key_der, contents100000.key_der); + assert_eq!(contents1.certificate.der, contents100000.certificate.der); + assert_eq!( + contents1.certificate.local_key_id, + contents100000.certificate.local_key_id + ); + assert!( + contents1 + .additional_certificates + .iter() + .zip(contents100000.additional_certificates.iter()) + .all(|(a, b)| a.der == b.der) + ); + + let p12_pyca = include_bytes!("data/pyca-cert-rc2-key-3des.p12"); + let contents_pyca = parse_pkcs12(p12_pyca, "cryptography").unwrap(); + let pyca_ec_key_der_sha256 = + hex_literal::hex!("956890dd43249260db8b4a7edf87541070086c186f6a5e39e2eba2eec28f634c"); + let ec_digest = Sha256::digest(contents_pyca.key_der.clone()); + assert_eq!(ec_digest.as_slice(), pyca_ec_key_der_sha256); +} diff --git a/pkcs12/tests/builder/examples/cert.der b/pkcs12/tests/builder/examples/cert.der new file mode 100644 index 000000000..214457402 Binary files /dev/null and b/pkcs12/tests/builder/examples/cert.der differ diff --git a/pkcs12/tests/builder/examples/example.pfx b/pkcs12/tests/builder/examples/example.pfx new file mode 100644 index 000000000..d591096d8 Binary files /dev/null and b/pkcs12/tests/builder/examples/example.pfx differ diff --git a/pkcs12/tests/builder/examples/key.der b/pkcs12/tests/builder/examples/key.der new file mode 100644 index 000000000..ed0701158 Binary files /dev/null and b/pkcs12/tests/builder/examples/key.der differ diff --git a/pkcs12/tests/builder/legacy_pbe.rs b/pkcs12/tests/builder/legacy_pbe.rs new file mode 100644 index 000000000..b324f696b --- /dev/null +++ b/pkcs12/tests/builder/legacy_pbe.rs @@ -0,0 +1,566 @@ +//! Tests for PKCS#12 legacy PBE decryption support (pbeWithSHAAnd3-KeyTripleDES-CBC, etc.) + +use std::process::Command; +use std::sync::LazyLock; + +use tempfile::TempDir; + +use pkcs12::builder::asn1_utils::parse_pkcs12; + +const PASSWORD: &str = "legacy-pbe-test"; + +/// Cached detection of whether the system OpenSSL has the legacy provider. +/// On OpenSSL 1.x the legacy algorithms are always built in (returns `true`). +/// On OpenSSL 3.x we probe with `openssl list -providers -provider legacy`. +static HAS_LEGACY_PROVIDER: LazyLock = LazyLock::new(|| { + let version_out = Command::new("openssl") + .args(["version"]) + .output() + .expect("openssl version"); + let version = String::from_utf8_lossy(&version_out.stdout); + + // OpenSSL 1.x has legacy algorithms built in + if version.starts_with("OpenSSL 1.") || version.starts_with("LibreSSL") { + return true; + } + + // OpenSSL 3.x: probe the legacy provider + Command::new("openssl") + .args(["list", "-providers", "-provider", "legacy"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +}); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Generate a fresh RSA key + self-signed certificate via OpenSSL. +/// Returns `(key_pem_path, cert_pem_path, key_der, cert_der, TempDir)`. +fn generate_credentials() -> (String, String, Vec, Vec, TempDir) { + let dir = TempDir::new().expect("temp dir"); + let key_pem = dir.path().join("key.pem"); + let cert_pem = dir.path().join("cert.pem"); + + let ok = Command::new("openssl") + .args([ + "genpkey", + "-quiet", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + "-out", + key_pem.to_str().unwrap(), + ]) + .status() + .expect("spawn openssl genpkey"); + assert!(ok.success(), "openssl genpkey failed"); + + let ok = Command::new("openssl") + .args([ + "req", + "-quiet", + "-new", + "-x509", + "-key", + key_pem.to_str().unwrap(), + "-out", + cert_pem.to_str().unwrap(), + "-days", + "365", + "-subj", + "/CN=legacy-pbe-test/O=Test", + ]) + .status() + .expect("spawn openssl req"); + assert!(ok.success(), "openssl req -x509 failed"); + + let key_der = Command::new("openssl") + .args(["pkey", "-in", key_pem.to_str().unwrap(), "-outform", "DER"]) + .output() + .expect("openssl pkey"); + assert!(key_der.status.success()); + + let cert_der = Command::new("openssl") + .args(["x509", "-in", cert_pem.to_str().unwrap(), "-outform", "DER"]) + .output() + .expect("openssl x509"); + assert!(cert_der.status.success()); + + let key_pem_str = key_pem.to_str().unwrap().to_string(); + let cert_pem_str = cert_pem.to_str().unwrap().to_string(); + ( + key_pem_str, + cert_pem_str, + key_der.stdout, + cert_der.stdout, + dir, + ) +} + +/// Build a PKCS#12 file with OpenSSL using legacy PBE for both key and cert bags. +fn openssl_export_legacy_pbe( + cert_pem_path: &str, + key_pem_path: &str, + keypbe: &str, + certpbe: &str, +) -> Vec { + let dir = TempDir::new().expect("temp dir"); + let p12_path = dir.path().join("out.p12"); + + let out = Command::new("openssl") + .args([ + "pkcs12", + "-export", + "-in", + cert_pem_path, + "-inkey", + key_pem_path, + "-out", + p12_path.to_str().unwrap(), + "-passout", + &format!("pass:{PASSWORD}"), + "-keypbe", + keypbe, + "-certpbe", + certpbe, + "-macalg", + "SHA1", + "-iter", + "2048", + ]) + .output() + .expect("spawn openssl pkcs12 -export"); + + if !out.status.success() { + eprintln!( + "[openssl pkcs12 -export stderr]\n{}", + String::from_utf8_lossy(&out.stderr) + ); + } + assert!( + out.status.success(), + "openssl pkcs12 -export with legacy PBE failed (keypbe={keypbe}, certpbe={certpbe})" + ); + + std::fs::read(&p12_path).expect("read exported p12") +} + +// --------------------------------------------------------------------------- +// Tests --- pbeWithSHAAnd3-KeyTripleDES-CBC (both key and cert bags) +// --------------------------------------------------------------------------- + +/// Parse a PKCS#12 file that uses 3DES PBE for both key and cert. +#[test] +fn legacy_3des_both_bags() { + let (key_pem, cert_pem, key_der, cert_der, _dir) = generate_credentials(); + + let p12_bytes = + openssl_export_legacy_pbe(&cert_pem, &key_pem, "PBE-SHA1-3DES", "PBE-SHA1-3DES"); + + let contents = + parse_pkcs12(&p12_bytes, PASSWORD).expect("get_key_and_cert with 3DES PBE failed"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +/// Wrong password should fail MAC verification or decryption. +#[test] +fn legacy_3des_wrong_password() { + let (key_pem, cert_pem, _, _, _dir) = generate_credentials(); + + let p12_bytes = + openssl_export_legacy_pbe(&cert_pem, &key_pem, "PBE-SHA1-3DES", "PBE-SHA1-3DES"); + + let result = parse_pkcs12(&p12_bytes, "wrong-password"); + assert!( + result.is_err(), + "should fail with wrong password on 3DES PBE P12" + ); +} + +// --------------------------------------------------------------------------- +// Tests --- mixed: legacy PBE cert bag + PBES2 key bag (and vice versa) +// --------------------------------------------------------------------------- + +/// Legacy PBE for the cert bag, PBES2 (AES-256-CBC) for the key bag. +#[test] +fn legacy_cert_pbes2_key() { + let (key_pem, cert_pem, key_der, cert_der, _dir) = generate_credentials(); + + let p12_bytes = openssl_export_legacy_pbe(&cert_pem, &key_pem, "AES-256-CBC", "PBE-SHA1-3DES"); + + let contents = + parse_pkcs12(&p12_bytes, PASSWORD).expect("get_key_and_cert with mixed PBE/PBES2 failed"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +/// PBES2 (AES-256-CBC) for the cert bag, legacy PBE for the key bag. +#[test] +fn pbes2_cert_legacy_key() { + let (key_pem, cert_pem, key_der, cert_der, _dir) = generate_credentials(); + + let p12_bytes = openssl_export_legacy_pbe(&cert_pem, &key_pem, "PBE-SHA1-3DES", "AES-256-CBC"); + + let contents = + parse_pkcs12(&p12_bytes, PASSWORD).expect("get_key_and_cert with mixed PBES2/PBE failed"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +// --------------------------------------------------------------------------- +// Tests --- RC2-CBC variants +// --------------------------------------------------------------------------- + +/// Build a PKCS#12 file using the OpenSSL legacy provider (needed for RC2-40). +/// Requires `HAS_LEGACY_PROVIDER` to be checked by the caller before invoking. +fn openssl_export_legacy_provider( + cert_pem_path: &str, + key_pem_path: &str, + keypbe: &str, + certpbe: &str, +) -> Vec { + let dir = TempDir::new().expect("temp dir"); + let p12_path = dir.path().join("out.p12"); + + let out = Command::new("openssl") + .args([ + "pkcs12", + "-export", + "-in", + cert_pem_path, + "-inkey", + key_pem_path, + "-out", + p12_path.to_str().unwrap(), + "-passout", + &format!("pass:{PASSWORD}"), + "-keypbe", + keypbe, + "-certpbe", + certpbe, + "-macalg", + "SHA1", + "-iter", + "2048", + "-legacy", + ]) + .output() + .expect("spawn openssl pkcs12 -export"); + + assert!( + out.status.success(), + "openssl pkcs12 -export with legacy provider failed (keypbe={keypbe}, certpbe={certpbe}): {}", + String::from_utf8_lossy(&out.stderr) + ); + + std::fs::read(&p12_path).expect("read exported p12") +} + +/// Parse a PKCS#12 file that uses RC2-40-CBC for the cert bag and 3DES for the key bag. +/// This was the historical OpenSSL default. Skipped if OpenSSL doesn't support legacy provider. +#[test] +fn legacy_rc2_40_cert_3des_key() { + if !*HAS_LEGACY_PROVIDER { + println!("Skipping: OpenSSL legacy provider not available"); + return; + } + let (key_pem, cert_pem, key_der, cert_der, _dir) = generate_credentials(); + + let p12_bytes = + openssl_export_legacy_provider(&cert_pem, &key_pem, "PBE-SHA1-3DES", "PBE-SHA1-RC2-40"); + + let contents = parse_pkcs12(&p12_bytes, PASSWORD) + .expect("get_key_and_cert with RC2-40 cert / 3DES key failed"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +// --------------------------------------------------------------------------- +// Tests --- PKCS#12 KDF unit tests +// --------------------------------------------------------------------------- + +/// Verify that the PKCS#12 KDF produces expected output for a known test vector. +/// This uses the RFC 7292 Appendix B KDF with SHA-1. +#[test] +fn pkcs12_kdf_known_answer() { + use pkcs12::kdf::{Pkcs12KeyType, derive_key_utf8}; + use sha1::Sha1; + + // Derive a 24-byte encryption key from a known password and salt + let password = "test"; + let salt = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let iterations = 2048; + let key_len = 24; + + let key = derive_key_utf8::( + password, + &salt, + Pkcs12KeyType::EncryptionKey, + iterations, + key_len, + ) + .expect("KDF should succeed"); + assert_eq!(key.len(), key_len); + + // Derive again with same inputs --- must be deterministic + let key2 = derive_key_utf8::( + password, + &salt, + Pkcs12KeyType::EncryptionKey, + iterations, + key_len, + ) + .expect("KDF should succeed"); + assert_eq!(key, key2); + + // Derive IV with same password/salt --- must differ from encryption key + let iv = derive_key_utf8::(password, &salt, Pkcs12KeyType::Iv, iterations, 8) + .expect("IV derivation should succeed"); + assert_eq!(iv.len(), 8); + assert_ne!(&key[..8], iv.as_slice(), "IV must differ from key prefix"); +} + +/// Verify that different passwords produce different keys. +#[test] +fn pkcs12_kdf_different_passwords() { + use pkcs12::kdf::{Pkcs12KeyType, derive_key_utf8}; + use sha1::Sha1; + + let salt = [0xAA; 8]; + let key1 = derive_key_utf8::("password1", &salt, Pkcs12KeyType::EncryptionKey, 1000, 24) + .expect("KDF should succeed"); + let key2 = derive_key_utf8::("password2", &salt, Pkcs12KeyType::EncryptionKey, 1000, 24) + .expect("KDF should succeed"); + assert_ne!(key1, key2); +} + +/// Verify 3DES-CBC round-trip via PKCS#12 KDF-derived key and IV. +#[test] +fn pkcs12_3des_cbc_round_trip() { + use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7}; + use pkcs12::kdf::{Pkcs12KeyType, derive_key_utf8}; + use sha1::Sha1; + + let password = "round-trip-test"; + let salt = [0x55; 8]; + let iterations = 1000; + + let key = derive_key_utf8::( + password, + &salt, + Pkcs12KeyType::EncryptionKey, + iterations, + 24, + ) + .expect("key derivation"); + let iv = derive_key_utf8::(password, &salt, Pkcs12KeyType::Iv, iterations, 8) + .expect("IV derivation"); + + let plaintext = b"Hello, PKCS#12 legacy PBE world!"; + + // Encrypt + let mut buf = vec![0u8; plaintext.len() + 8]; // room for padding + buf[..plaintext.len()].copy_from_slice(plaintext); + let ciphertext = cbc::Encryptor::::new_from_slices(&key, &iv) + .expect("encryptor init") + .encrypt_padded::(&mut buf, plaintext.len()) + .expect("encrypt"); + let ct_vec = ciphertext.to_vec(); + + // Decrypt + let mut dec_buf = ct_vec.clone(); + let decrypted = cbc::Decryptor::::new_from_slices(&key, &iv) + .expect("decryptor init") + .decrypt_padded::(&mut dec_buf) + .expect("decrypt"); + + assert_eq!(decrypted, plaintext); +} + +/// Verify RC2-CBC round-trip via PKCS#12 KDF-derived key and IV using InnerIvInit. +#[test] +fn pkcs12_rc2_40_cbc_round_trip() { + use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt, InnerIvInit, block_padding::Pkcs7}; + use pkcs12::kdf::{Pkcs12KeyType, derive_key_utf8}; + use sha1::Sha1; + + let password = "rc2-round-trip"; + let salt = [0x77; 8]; + let iterations = 1000; + + let key = derive_key_utf8::(password, &salt, Pkcs12KeyType::EncryptionKey, iterations, 5) + .expect("key derivation"); + let iv = derive_key_utf8::(password, &salt, Pkcs12KeyType::Iv, iterations, 8) + .expect("IV derivation"); + + let plaintext = b"RC2-40 test data padding!!!!!!!!"; // 32 bytes, multiple of block size + + // Encrypt with RC2-40 + let enc_cipher = rc2::Rc2::new_with_eff_key_len(&key, 40); + let mut buf = vec![0u8; plaintext.len() + 8]; + buf[..plaintext.len()].copy_from_slice(plaintext); + let ciphertext = cbc::Encryptor::::inner_iv_slice_init(enc_cipher, &iv) + .expect("encryptor init") + .encrypt_padded::(&mut buf, plaintext.len()) + .expect("encrypt"); + let ct_vec = ciphertext.to_vec(); + + // Decrypt with RC2-40 + let dec_cipher = rc2::Rc2::new_with_eff_key_len(&key, 40); + let mut dec_buf = ct_vec.clone(); + let decrypted = cbc::Decryptor::::inner_iv_slice_init(dec_cipher, &iv) + .expect("decryptor init") + .decrypt_padded::(&mut dec_buf) + .expect("decrypt"); + + assert_eq!(decrypted, plaintext); +} + +// --------------------------------------------------------------------------- +// Tests --- Legacy PBE generation (Rust builds, Rust reads back) +// --------------------------------------------------------------------------- + +/// Round-trip: generate a P12 with legacy 3DES PBE for both bags, then parse it back. +#[test] +fn generate_legacy_3des_round_trip() { + use der::Decode; + use pkcs12::builder::{LegacyPbeAlgorithm, MacAlgorithm, MacDataBuilder, Pkcs12Builder}; + + let (_key_pem, _cert_pem, key_der, cert_der, _dir) = generate_credentials(); + let cert = x509_cert::Certificate::from_der(&cert_der).expect("parse cert"); + + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.iterations(Some(2048)).expect("set iterations"); + p12_builder + .cert_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc)) + .key_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc)); + + // Use SHA-1 MAC for full legacy compatibility + let mut mdb = MacDataBuilder::new(MacAlgorithm::HmacSha1); + mdb.iterations(Some(2048)).expect("set mac iterations"); + p12_builder.mac_data_builder(Some(mdb)); + + let p12 = p12_builder + .build_with_rng(&cert, &key_der, PASSWORD, &mut rand::rng()) + .expect("build legacy 3DES P12"); + + let contents = parse_pkcs12(&p12, PASSWORD).expect("parse back legacy 3DES P12"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +/// Round-trip: generate a P12 with legacy RC2-128 cert bag + 3DES key bag. +#[test] +fn generate_legacy_rc2_cert_3des_key_round_trip() { + use der::Decode; + use pkcs12::builder::{LegacyPbeAlgorithm, Pkcs12Builder}; + + let (_key_pem, _cert_pem, key_der, cert_der, _dir) = generate_credentials(); + let cert = x509_cert::Certificate::from_der(&cert_der).expect("parse cert"); + + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.iterations(Some(2048)).expect("set iterations"); + p12_builder + .cert_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd128BitRc2Cbc)) + .key_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc)); + + let p12 = p12_builder + .build_with_rng(&cert, &key_der, PASSWORD, &mut rand::rng()) + .expect("build mixed legacy P12"); + + let contents = parse_pkcs12(&p12, PASSWORD).expect("parse back mixed legacy P12"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +/// Mixed: legacy PBE cert bag + PBES2 key bag (Rust generates, Rust reads). +#[test] +fn generate_legacy_cert_pbes2_key_round_trip() { + use der::Decode; + use pkcs12::builder::{LegacyPbeAlgorithm, Pkcs12Builder}; + + let (_key_pem, _cert_pem, key_der, cert_der, _dir) = generate_credentials(); + let cert = x509_cert::Certificate::from_der(&cert_der).expect("parse cert"); + + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.iterations(Some(2048)).expect("set iterations"); + p12_builder.cert_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc)); + // key uses default PBES2 + + let p12 = p12_builder + .build_with_rng(&cert, &key_der, PASSWORD, &mut rand::rng()) + .expect("build legacy-cert/pbes2-key P12"); + + let contents = parse_pkcs12(&p12, PASSWORD).expect("parse back legacy-cert/pbes2-key P12"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +/// OpenSSL interop: Rust generates legacy PBE P12 with 3DES, OpenSSL reads it. +#[test] +fn generate_legacy_openssl_reads() { + use der::Decode; + use pkcs12::builder::{LegacyPbeAlgorithm, MacAlgorithm, MacDataBuilder, Pkcs12Builder}; + + let (_key_pem, _cert_pem, key_der, cert_der, _dir) = generate_credentials(); + let cert = x509_cert::Certificate::from_der(&cert_der).expect("parse cert"); + + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.iterations(Some(2048)).expect("set iterations"); + p12_builder + .cert_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc)) + .key_legacy_pbe_algorithm(Some(LegacyPbeAlgorithm::ShaAnd3KeyTripleDesCbc)); + + let mut mdb = MacDataBuilder::new(MacAlgorithm::HmacSha1); + mdb.iterations(Some(2048)).expect("set mac iterations"); + p12_builder.mac_data_builder(Some(mdb)); + + let p12 = p12_builder + .build_with_rng(&cert, &key_der, PASSWORD, &mut rand::rng()) + .expect("build legacy P12 for OpenSSL"); + + let dir = TempDir::new().expect("temp dir"); + let p12_path = dir.path().join("rust_legacy.p12"); + std::fs::write(&p12_path, &p12).expect("write p12"); + + let out = Command::new("openssl") + .args([ + "pkcs12", + "-info", + "-in", + p12_path.to_str().unwrap(), + "-passin", + &format!("pass:{PASSWORD}"), + "-passout", + &format!("pass:{PASSWORD}"), + "-nokeys", + ]) + .output() + .expect("openssl pkcs12 -info"); + + assert!( + out.status.success(), + "OpenSSL failed to read Rust-generated legacy P12: {}", + String::from_utf8_lossy(&out.stderr) + ); +} + +#[test] +fn process_p12_test() { + let enc_p12 = hex_literal::hex!( + "30820B2002010330820AE606092A864886F70D010701A0820AD704820AD330820ACF3082056706092A864886F70D010706A0820558308205540201003082054D06092A864886F70D010701301C060A2A864886F70D010C0103300E0408DA45CC37D1079B1F0202080080820520CC0F46556D09D47BD184504F4D123AA5A702B6EEBA68C009907EAFC5851B1A08AFC0A064612D69D548CFBBB7219A2438448BFE2A272E4B43CDD620B3224367C57E4AEF6EF20A87E2F31C1515FF22906833DFD170B8BEE4F845757274B5798ABFFC1BFDBCA6A2D8FBCEACE50BF5B20CB09A264F6EC0FBD779D0A6972FC375A5DCEB7DF109AB30E113C82B02C5980A900AE1DA212F4519E4BCE000A5265C3FB54FF5DF6C5AD8054AB1631600620E00D2F88FB74721938B520E67AB65FC49C78BF25D5BBC6475838D412DEF40DF2F544AFCAEA45B11A5C28771777FD31349A0BBC40CC089D900F68009689B754A0E97611A0667C9C615414D41D2ACF8B93B9B357636E2DFEFF6113FB2B22C34F736A3AE107C3D0A1874B6499773490F8049318753C577AB9312A6A80F9E318DF370F0F6BD36418BB1766A91A121221333444BD542AFA75DE31550EA5EDE46127D87779238D6CF4C9C5520B08D749FA7BC56923A0696AC8733AEACDCA3CB9611C69711C56C586B1270B45D40E6C78C10D1E734F8EF6F3B476623D378C4F509101778EDB70EEC005E9893BCE418521B5CBC326A8A8A9C77C46E4B04FA6D13326AB666A646031F82DA7BC2BF592510CDE7FE62E2627506F44D1C2379E8F2B25AE19D9D699A5ABDB9D7BF27E60366A9AFBD923C02E21F4EEF2C288A43D28DA41064FF5B1D54E242CAE7B47E82CF3212A23AA0AABA52FAF1AE9F74C284543220D0CE0DD671EED99F40D010A72F46F4BF62D1C5159689E408DF975E28B2DD4615AA21A7581BDA244EAB3203873BD160A62D17BDEA212AC9231B7C6A7E7ABC86E78AE1FCB238B597651A2933F87DAAE175C038AF555DCB0C3ED0322A23B368A2C2ABA878A4CCCE47F5B82C7041004411BC0810F0D5A72EB9BA9061AB6D43B0721A7354652E538A645BF59B3FC6C868D30D455FFFE8322066C3D4FFCF424B323344C79BFFAC19D9B14D13AA85BC8A49B8E9E83F8F29E16538F1E341383C9B98649F611146780CA8D7BF17B069E753A6A6677229F24D5F7D1B627CAE81C6F050476DFF8F26DBAF11C0A2C71C898D021D7B5C5B32A9D9060F1ED8CFB3AC7B349CD4CD99142638CC20778416F35D61F27E3CC5ED61209826DA328D8CD515C77DB0FB397DCD6F86F6D101CEDE28F7CC4CE0C9051DEFCF8E3CB9D4AD78CE8B2F344C98F09CC9B43945AEB97DDE05C8A7E8BF27CDE49871ECA874627BF1B09EBE4D18B23400E3CA5C177E33123085AD1F2E609483FAE950A085A9EB4BE878CCBE41CF16F17161F04313853EE7CE09280F6B835B193BB34E2AF440A890D0AC42C2B2DBFE74607617D3C0A5E5D0A46289E8526F294A3D3EAA1617D900D0600EE444C38AD2E2136F108861D3ED2049AFEF334A2A2D963D990509830CD2F5F59104AA44774A65035844A235C58C070F4051EBAB2E002A5BDB618264E1CD1998720D700B1F73F3BF2512A18FF20187BE0E8EB5C5C2429AF299526EDEFF03DB72B90147ED8E3B250E31845016355CA98836C7889F9F6D7A6A0D9038019B27AD8ADEEA456FA79044C12E8E41BBA96B1F9F0AF534DF9C732C67CBB74008998812F797AD5718BEF0E3CEF14C9BDD74FFD4DBAD07B35C09DD9B1D9ECD09695B8C61EACFA6E6FB10BA32BA3B3FC6DED93DEFE8D3BDE925BEF6204EC2426E2E903AB54CBA69F35785E08F3DB487E9FF16896129F2051A8959883088AAA62C866B362B23F2E6E63B774B6D5F0B878BE56BD9DEC71D8860882B02CBC78F0B770AE7D84028087BBEE6CD51BC28239F46F5A974F63523ED9D363095DA886C36AE44CEF6A6374394D3991CBF39BD16748BD89E5DBD29AF3760C1055B834CB520F119C4283082056006092A864886F70D010701A08205510482054D3082054930820545060B2A864886F70D010C0A0102A08204EE308204EA301C060A2A864886F70D010C0103300E04084C1BBDCDA98501B102020800048204C849E4283055357FCE518F77476290EB288F608332811C61BD906EB2342DBFC0866925CA95642D17FF7833543F5BCF523BEFA11486059E74CA6BA3DE402F8229038815F6458EE09B88F0E6140FC5A7D15251EF6EC36C852133269729BB514B0E344550F8E5FA1ACCCA6A32CB2668B2DAE7C6D46D470633DD002800FA0702D71739A2C0B7E9008551383B243B7C00A95C936B033C5F2C57BF43DCD1F2A41ED5A14F3833E124847DB29D1F88B2B0AB03BFFDE23CF7DF65692AA1F6A32A7C258E7B35708536ED081A7898EBC3A64DB85086F10F315C3B2867E55BE65A6CA938D3B08FAD6BA63326170950F25FAEFC468D34C74C4CACBF1D9C61E44417D8F1A8BAF17A564514C3B53FEE30724EE55B67EBF0CD489356F0DC964CB7E998D1029E24565486C839464C464447EC2DEDE32C9C787628F35D2F50452D85FC084E14C8739BCDF51EBD34EB49E777937E7E2135D9AB836749C904EE8E21538CDBDDF1E3665DC7F07900627BC78B44C6A95EC482A9B8D337BAA4C80433AEA62ADC3F7EEFB4DD0EB0B0936BBBEA543ED9F1407FC55CBBEA72D42EA147FCFEFDCF27D4A3556F4677AFA012E426B2924FF564CE1DD2CA6B8009A77EEEE84D7C639D39F2E7551BAA6ACE03EFC133E41C2D06337C69F5F2FBA6CFBEF97AF650518FE4AF87CC29B68EDA5B5AB87E4021C6B8E0BC1C4FA00FB5FF9B6211B5DCCDC0AF8638E285B2833ACA4E5071BA92E7AA49483A54BB118F3835F1497BBA4355D235A4A8C83D3EEA8D0EEAE5BEE35872009E3E3F389DC1475AD4CF0D601F5170C1068670F476C40A0EF1A0CB9EB290D33C66DAD76B50B8FC1A9CCE1398CA7367B89FA7935A01DC1BF061CF7E439E98E9A8494D863EEBAB4070AC043CBA40DE8F0644327AEC1B1A2F9E10924B068F974532935B884081D198B26EF130AFAEA19E5253218018C4A8DC3D08303FA81313A78CEEA3DC883A22DA15414FE6A8BF5B7E925FA483962F2F7EE534083E2BD21CD76AEB4E3D08B86E57A48FA9AB5D2B27A75D37806625938600CBE2B6709FB63A4F04E44117E4E9D3274409CB16E6D009D2E459225E29AAD9A5340A03F9BDE1775603EA0BCA02603394534308E5C468059877CCDB8F88B1BB15E61BDAC950D1C8439E92FC890DD1E158B6FF7AA842FE2D25C95439D2F7C9F47F686A5083B0DCC59444478B54A5E5BCFF6649E604D04C8FF306165E7BD11C1F4967CA479D9A4164137F0A063E4D51238EABA608F9C9F4E2DBA2E8437A24E4C15EFA51F9E36BDC7DA42B3CC29431575074F06291F5FE4C3B82ED3996B07FB182FF3AAE28EA045E81C63451B37B6EF810E83E6C26D9E93693FFB79539AE8B1503E8F48A70AFE5DED4CCD782177DDD81CC2187BDE9ED49C593925FD77E47FE0F06713F36BB438FBBD6028D80C9707C1E64EACC6119C8AF5482E0EA413128C4D299F18BDDBFC1A9B505026420D33D187A21E42FEC090C61B1B01B03D092C7DD97AA6DCB2797EEE4F14A14D83A31D787E75ACF06D4312E01EA7D81CE837EF4E9EDEFE26E968F9B9D55A4BF0B77D757ECF78F96B365BC3932FB6CEE9F094E9C115A229CEC0C079A35358F713A1450F74D8FE9F173FEF0CADA79435815B4D5EA689DAD7C48ADBC93C71C2674D0A300B0946BA8247E16D75CA4501B8B3DA4DB572C6853E3C46282FDB299C3905468CC80A1755CB676CB37001C1DE4B3414490C5F9667FD9ED1161042F38F21432EA3144301D06092A864886F70D01091431101E0E007300690067006E006500720032302306092A864886F70D01091531160414198F0B8F39F377C7063D27614886B35C59FD495230313021300906052B0E03021A050004142A4B88194EBF14006DF11653BCB7B42D4420AD9204088AEC2E8F2178CE6F02020800" + ); + let password = "////////"; + assert!(pkcs12::builder::parse_pkcs12(&enc_p12, password).is_ok()) +} diff --git a/pkcs12/tests/builder/openssl_interop.rs b/pkcs12/tests/builder/openssl_interop.rs new file mode 100644 index 000000000..5882cb4f3 --- /dev/null +++ b/pkcs12/tests/builder/openssl_interop.rs @@ -0,0 +1,553 @@ +//! Integration tests verifying PKCS #12 interoperability with OpenSSL. +//! +//! Two directions are tested: +//! 1. Rust builds a PFX -> OpenSSL parses it (`openssl pkcs12 -noout`) +//! 2. OpenSSL builds a PFX -> Rust parses it (`parse_pkcs12`) + +use std::process::Command; +use std::sync::OnceLock; + +use der::{Decode, Encode}; +use pkcs5::pbes2::Pbkdf2Prf; +use tempfile::TempDir; +use x509_cert::Certificate; + +use pkcs12::builder::{ + Pkcs12Builder, + asn1_utils::parse_pkcs12, + mac_data_builder::MacDataBuilder, + supported_algs::{EncryptionAlgorithm, MacAlgorithm}, +}; + +const PASSWORD: &str = "test-p@ss!123"; + +// --------------------------------------------------------------------------- +// Shared test credentials (generated once per test run) +// --------------------------------------------------------------------------- + +/// Returns `(key_der, cert_der)` created once and reused across all tests. +fn creds() -> &'static (Vec, Vec) { + static CREDS: OnceLock<(Vec, Vec)> = OnceLock::new(); + CREDS.get_or_init(generate_credentials) +} + +/// Generate a 2048-bit RSA key and a self-signed certificate via OpenSSL. +/// Returns `(key_der, cert_der)`. +fn generate_credentials() -> (Vec, Vec) { + let dir = TempDir::new().expect("temp dir"); + let key_pem = dir.path().join("key.pem"); + let cert_pem = dir.path().join("cert.pem"); + + let ok = Command::new("openssl") + .args([ + "genpkey", + "-quiet", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + "-out", + key_pem.to_str().unwrap(), + ]) + .status() + .expect("spawn openssl genpkey"); + assert!(ok.success(), "openssl genpkey failed"); + + let ok = Command::new("openssl") + .args([ + "req", + "-quiet", + "-new", + "-x509", + "-key", + key_pem.to_str().unwrap(), + "-out", + cert_pem.to_str().unwrap(), + "-days", + "365", + "-subj", + "/CN=pkcs12-builder-test/O=Test", + ]) + .status() + .expect("spawn openssl req"); + assert!(ok.success(), "openssl req -x509 failed"); + + // Convert key to PKCS #8 DER + let key_out = Command::new("openssl") + .args(["pkey", "-in", key_pem.to_str().unwrap(), "-outform", "DER"]) + .output() + .expect("spawn openssl pkey"); + assert!(key_out.status.success(), "openssl pkey -outform DER failed"); + + // Convert cert to DER + let cert_out = Command::new("openssl") + .args(["x509", "-in", cert_pem.to_str().unwrap(), "-outform", "DER"]) + .output() + .expect("spawn openssl x509"); + assert!( + cert_out.status.success(), + "openssl x509 -outform DER failed" + ); + + (key_out.stdout, cert_out.stdout) +} + +fn cert_from_der(der: &[u8]) -> Certificate { + Certificate::from_der(der).expect("parse Certificate from DER") +} + +// --------------------------------------------------------------------------- +// Direction 1 helpers: Rust builds PFX, OpenSSL verifies it +// --------------------------------------------------------------------------- + +/// Ask OpenSSL to parse `p12_bytes` with the given password. +/// Returns `true` if OpenSSL exits successfully. +fn openssl_verify(p12_bytes: &[u8], password: &str) -> bool { + let dir = TempDir::new().expect("temp dir"); + let path = dir.path().join("test.p12"); + std::fs::write(&path, p12_bytes).expect("write p12"); + + let out = Command::new("openssl") + .args([ + "pkcs12", + "-in", + path.to_str().unwrap(), + "-noout", + "-passin", + &format!("pass:{password}"), + ]) + .output() + .expect("spawn openssl pkcs12"); + + if !out.status.success() { + eprintln!( + "[openssl pkcs12 stderr]\n{}", + String::from_utf8_lossy(&out.stderr) + ); + } + out.status.success() +} + +/// Build a PFX with the given algorithm choices. +fn build_rust_pfx( + cert: &Certificate, + key_der: &[u8], + enc: EncryptionAlgorithm, + kdf: Pbkdf2Prf, + mac: MacAlgorithm, +) -> Vec { + let mut mac_builder = MacDataBuilder::new(mac); + mac_builder.iterations(Some(2048)).unwrap(); + + let mut builder = Pkcs12Builder::new(); + builder + .iterations(Some(2048)) + .unwrap() + .cert_kdf_algorithm(Some(kdf)) + .cert_enc_algorithm(Some(enc.clone())) + .key_kdf_algorithm(Some(kdf)) + .key_enc_algorithm(Some(enc)) + .mac_data_builder(Some(mac_builder)); + + let mut rng = rand::rng(); + builder + .build_with_rng(cert, key_der, PASSWORD, &mut rng) + .expect("Pkcs12Builder::build_with_rng") +} + +// --------------------------------------------------------------------------- +// Direction 1 tests: Rust builds -> OpenSSL reads +// --------------------------------------------------------------------------- + +#[test] +fn rust_default_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes256Cbc, + Pbkdf2Prf::HmacWithSha256, + MacAlgorithm::HmacSha256, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +#[test] +fn rust_aes128_cbc_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes128Cbc, + Pbkdf2Prf::HmacWithSha256, + MacAlgorithm::HmacSha256, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +#[test] +fn rust_aes192_cbc_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes192Cbc, + Pbkdf2Prf::HmacWithSha256, + MacAlgorithm::HmacSha256, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +#[test] +fn rust_pbkdf2_sha384_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes256Cbc, + Pbkdf2Prf::HmacWithSha384, + MacAlgorithm::HmacSha256, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +#[test] +fn rust_pbkdf2_sha512_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes256Cbc, + Pbkdf2Prf::HmacWithSha512, + MacAlgorithm::HmacSha256, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +#[test] +fn rust_hmac_sha384_mac_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes256Cbc, + Pbkdf2Prf::HmacWithSha256, + MacAlgorithm::HmacSha384, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +#[test] +fn rust_hmac_sha512_mac_openssl_reads() { + let (key_der, cert_der) = creds(); + let cert = cert_from_der(cert_der); + let p12 = build_rust_pfx( + &cert, + key_der, + EncryptionAlgorithm::Aes256Cbc, + Pbkdf2Prf::HmacWithSha256, + MacAlgorithm::HmacSha512, + ); + assert!(openssl_verify(&p12, PASSWORD)); +} + +// --------------------------------------------------------------------------- +// Direction 1b: Rust builds PFX with certificate chain -> OpenSSL reads +// --------------------------------------------------------------------------- + +#[test] +fn rust_chain_openssl_reads() { + let dir = TempDir::new().expect("temp dir"); + let ca_key_pem = dir.path().join("ca-key.pem"); + let ca_cert_pem = dir.path().join("ca-cert.pem"); + let ee_key_pem = dir.path().join("ee-key.pem"); + let ee_csr_pem = dir.path().join("ee.csr"); + let ee_cert_pem = dir.path().join("ee-cert.pem"); + + // Generate CA key and self-signed CA cert + let ok = Command::new("openssl") + .args([ + "genpkey", + "-quiet", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + "-out", + ca_key_pem.to_str().unwrap(), + ]) + .status() + .expect("spawn openssl genpkey (CA)"); + assert!(ok.success(), "openssl genpkey (CA) failed"); + + let ok = Command::new("openssl") + .args([ + "req", + "-quiet", + "-new", + "-x509", + "-key", + ca_key_pem.to_str().unwrap(), + "-out", + ca_cert_pem.to_str().unwrap(), + "-days", + "365", + "-subj", + "/CN=Test CA/O=pkcs12-builder-test", + ]) + .status() + .expect("spawn openssl req (CA)"); + assert!(ok.success(), "openssl req -x509 (CA) failed"); + + // Generate EE key and CSR + let ok = Command::new("openssl") + .args([ + "genpkey", + "-quiet", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + "-out", + ee_key_pem.to_str().unwrap(), + ]) + .status() + .expect("spawn openssl genpkey (EE)"); + assert!(ok.success(), "openssl genpkey (EE) failed"); + + let ok = Command::new("openssl") + .args([ + "req", + "-quiet", + "-new", + "-key", + ee_key_pem.to_str().unwrap(), + "-out", + ee_csr_pem.to_str().unwrap(), + "-subj", + "/CN=Test EE/O=pkcs12-builder-test", + ]) + .status() + .expect("spawn openssl req (EE CSR)"); + assert!(ok.success(), "openssl req (EE CSR) failed"); + + // Sign EE cert with CA + let ok = Command::new("openssl") + .args([ + "x509", + "-req", + "-in", + ee_csr_pem.to_str().unwrap(), + "-CA", + ca_cert_pem.to_str().unwrap(), + "-CAkey", + ca_key_pem.to_str().unwrap(), + "-CAcreateserial", + "-out", + ee_cert_pem.to_str().unwrap(), + "-days", + "365", + ]) + .output() + .expect("spawn openssl x509 -req"); + assert!(ok.status.success(), "openssl x509 -req (sign EE) failed"); + + // Convert EE key to PKCS #8 DER + let key_out = Command::new("openssl") + .args([ + "pkey", + "-in", + ee_key_pem.to_str().unwrap(), + "-outform", + "DER", + ]) + .output() + .expect("spawn openssl pkey (EE)"); + assert!(key_out.status.success()); + + // Convert EE cert to DER + let ee_cert_out = Command::new("openssl") + .args([ + "x509", + "-in", + ee_cert_pem.to_str().unwrap(), + "-outform", + "DER", + ]) + .output() + .expect("spawn openssl x509 (EE)"); + assert!(ee_cert_out.status.success()); + + // Convert CA cert to DER + let ca_cert_out = Command::new("openssl") + .args([ + "x509", + "-in", + ca_cert_pem.to_str().unwrap(), + "-outform", + "DER", + ]) + .output() + .expect("spawn openssl x509 (CA)"); + assert!(ca_cert_out.status.success()); + + let ee_cert = cert_from_der(&ee_cert_out.stdout); + let ca_cert = cert_from_der(&ca_cert_out.stdout); + + let mut mac_builder = MacDataBuilder::new(MacAlgorithm::HmacSha256); + mac_builder.iterations(Some(2048)).unwrap(); + + let mut builder = Pkcs12Builder::new(); + builder + .iterations(Some(2048)) + .unwrap() + .cert_kdf_algorithm(Some(Pbkdf2Prf::HmacWithSha256)) + .cert_enc_algorithm(Some(EncryptionAlgorithm::Aes256Cbc)) + .key_kdf_algorithm(Some(Pbkdf2Prf::HmacWithSha256)) + .key_enc_algorithm(Some(EncryptionAlgorithm::Aes256Cbc)) + .mac_data_builder(Some(mac_builder)) + .additional_cert(ca_cert.clone()); + + let mut rng = rand::rng(); + let p12 = builder + .build_with_rng(&ee_cert, &key_out.stdout, PASSWORD, &mut rng) + .expect("build_with_rng with chain"); + + assert!( + openssl_verify(&p12, PASSWORD), + "OpenSSL failed to read PFX with certificate chain" + ); + + // Verify the Rust parser recovers the additional certificate + let contents = parse_pkcs12(&p12, PASSWORD).expect("get_key_and_cert with chain"); + assert_eq!( + contents.additional_certificates.len(), + 1, + "expected one additional certificate" + ); + assert_eq!( + contents.additional_certificates[0].der, + ca_cert.to_der().unwrap(), + "additional certificate should match the CA cert" + ); +} + +// --------------------------------------------------------------------------- +// Direction 2: OpenSSL builds PFX -> Rust parses it +// --------------------------------------------------------------------------- + +/// Export a PKCS #12 file via `openssl pkcs12 -export` using PBES2 algorithms +/// and return the raw DER bytes. +fn openssl_export(cert_pem: &str, key_pem: &str) -> Vec { + let dir = TempDir::new().expect("temp dir"); + let cert_path = dir.path().join("cert.pem"); + let key_path = dir.path().join("key.pem"); + let p12_path = dir.path().join("out.p12"); + + std::fs::write(&cert_path, cert_pem).expect("write cert pem"); + std::fs::write(&key_path, key_pem).expect("write key pem"); + + let ok = Command::new("openssl") + .args([ + "pkcs12", + "-export", + "-in", + cert_path.to_str().unwrap(), + "-inkey", + key_path.to_str().unwrap(), + "-out", + p12_path.to_str().unwrap(), + "-passout", + &format!("pass:{PASSWORD}"), + // Use PBES2 (AES-256-CBC + PBKDF2) so our library can decrypt + "-keypbe", + "AES-256-CBC", + "-certpbe", + "AES-256-CBC", + "-macalg", + "SHA256", + "-iter", + "2048", + ]) + .output() + .expect("spawn openssl pkcs12 -export"); + + if !ok.status.success() { + eprintln!( + "[openssl pkcs12 -export stderr]\n{}", + String::from_utf8_lossy(&ok.stderr) + ); + } + assert!(ok.status.success(), "openssl pkcs12 -export failed"); + + std::fs::read(&p12_path).expect("read exported p12") +} + +/// OpenSSL generates a PFX using AES-256-CBC / PBKDF2-SHA256; Rust must be +/// able to decrypt and recover the original key and certificate. +#[test] +fn openssl_builds_rust_reads() { + let dir = TempDir::new().expect("temp dir"); + let key_pem_path = dir.path().join("key.pem"); + let cert_pem_path = dir.path().join("cert.pem"); + + // Generate fresh PEM files (we need the PEM text for the export command) + let ok = Command::new("openssl") + .args([ + "genpkey", + "-quiet", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + "-out", + key_pem_path.to_str().unwrap(), + ]) + .status() + .expect("spawn openssl genpkey"); + assert!(ok.success(), "openssl genpkey failed"); + + let ok = Command::new("openssl") + .args([ + "req", + "-new", + "-x509", + "-key", + key_pem_path.to_str().unwrap(), + "-out", + cert_pem_path.to_str().unwrap(), + "-days", + "365", + "-subj", + "/CN=openssl-builds-rust-reads/O=Test", + ]) + .status() + .expect("spawn openssl req"); + assert!(ok.success(), "openssl req -x509 failed"); + + let cert_pem = std::fs::read_to_string(&cert_pem_path).expect("read cert pem"); + let key_pem = std::fs::read_to_string(&key_pem_path).expect("read key pem"); + + let p12_bytes = openssl_export(&cert_pem, &key_pem); + + let contents = parse_pkcs12(&p12_bytes, PASSWORD).expect("get_key_and_cert failed"); + + assert!( + !contents.key_der.is_empty(), + "recovered key must not be empty" + ); + + // The subject should contain the CN we encoded above + let recovered_cert = x509_cert::Certificate::from_der(&contents.certificate.der).unwrap(); + let subject = recovered_cert.tbs_certificate().subject().to_string(); + assert!( + subject.contains("openssl-builds-rust-reads"), + "unexpected subject: {subject}" + ); +} diff --git a/pkcs12/tests/builder/pkcs12_builder.rs b/pkcs12/tests/builder/pkcs12_builder.rs new file mode 100644 index 000000000..752e6bad7 --- /dev/null +++ b/pkcs12/tests/builder/pkcs12_builder.rs @@ -0,0 +1,847 @@ +use pkcs5::{ + pbes2, + pbes2::{AES_256_CBC_OID, PBES2_OID, PBKDF2_OID, Pbkdf2Params, Pbkdf2Prf}, +}; +use pkcs8::{ + EncryptedPrivateKeyInfo, + spki::{AlgorithmIdentifier, AlgorithmIdentifierOwned}, +}; + +use cms::encrypted_data::EncryptedData; +use const_oid::db::rfc5911::{ID_DATA, ID_ENCRYPTED_DATA}; +use der::{ + Any, AnyRef, Decode, Encode, + asn1::{ContextSpecific, OctetString, SetOfVec}, +}; +use pkcs5::pbes2::Salt; +use pkcs12::{PKCS_12_PKCS8_KEY_BAG_OID, pfx::Pfx}; +use rand_core::Rng; +use x509_cert::Certificate; + +use pkcs12::builder::{ + EncryptionAlgorithm, MacAlgorithm, MacDataBuilder, Pkcs12Builder, add_friendly_name_attr, + add_key_id_attr, + asn1_utils::{get_auth_safes, get_cert, get_key, get_safe_bags}, + parse_pkcs12, +}; + +#[cfg(test)] +fn check_key_and_cert( + der_p12: &[u8], + password: &str, + key: &[u8], + cert: &[u8], + cert_id: &Option>, + key_id: &Option>, +) { + let pfx = Pfx::from_der(der_p12).unwrap(); + let auth_safes = get_auth_safes(&pfx.auth_safe.content).unwrap(); + for auth_safe in auth_safes { + if ID_ENCRYPTED_DATA == auth_safe.content_type { + // certificate + let recovered_cert = get_cert(&auth_safe.content, password).unwrap(); + assert_eq!(recovered_cert.cert.der, cert); + assert_eq!(&recovered_cert.cert.local_key_id, cert_id); + } else if ID_DATA == auth_safe.content_type { + // key + let recovered_key = get_key(&auth_safe.content, password).unwrap(); + assert_eq!(*recovered_key.0, key); + assert_eq!(&recovered_key.1.local_key_id, key_id); + } + } + + let contents = parse_pkcs12(der_p12, password).unwrap(); + assert_eq!(contents.certificate.der, cert); + assert_eq!(*contents.key_der, key); + if key_id.is_some() { + assert_eq!(&contents.key_id, key_id); + } else { + assert_eq!(&contents.key_id, cert_id); + } + + assert!(parse_pkcs12(der_p12, &format!("{password}X")).is_err()); +} +#[cfg(test)] +fn check_algs( + mac: &MacAlgorithm, + enc: &EncryptionAlgorithm, + kdf: &Pbkdf2Prf, + der_p12: &[u8], + p12_iterations: u32, + mac_iterations: u32, +) { + let pfx = Pfx::from_der(der_p12).unwrap(); + let auth_safes = get_auth_safes(&pfx.auth_safe.content).unwrap(); + + for auth_safe in auth_safes { + if ID_ENCRYPTED_DATA == auth_safe.content_type { + // certificate + let enc_data = EncryptedData::from_der(&auth_safe.content.to_der().unwrap()).unwrap(); + assert_eq!(PBES2_OID, enc_data.enc_content_info.content_enc_alg.oid); + + let enc_params = enc_data + .enc_content_info + .content_enc_alg + .parameters + .as_ref() + .unwrap() + .to_der() + .unwrap(); + let params = pbes2::Parameters::from_der(&enc_params).unwrap(); + assert_eq!(PBKDF2_OID, params.kdf.oid()); + assert_eq!(kdf.oid(), params.kdf.pbkdf2().unwrap().prf.oid()); + assert_eq!(enc.oid(), params.encryption.oid()); + assert_eq!(p12_iterations, params.kdf.pbkdf2().unwrap().iteration_count); + } else if ID_DATA == auth_safe.content_type { + // key + let safe_bags = get_safe_bags(&auth_safe.content).unwrap(); + for safe_bag in safe_bags { + match safe_bag.bag_id { + PKCS_12_PKCS8_KEY_BAG_OID => { + let cs: ContextSpecific> = + ContextSpecific::from_der(&safe_bag.bag_value).unwrap(); + assert_eq!(PBES2_OID, cs.value.encryption_algorithm.oid()); + assert_eq!( + p12_iterations, + cs.value + .encryption_algorithm + .pbes2() + .unwrap() + .kdf + .pbkdf2() + .unwrap() + .iteration_count + ); + + assert_eq!( + kdf.oid(), + cs.value + .encryption_algorithm + .pbes2() + .unwrap() + .kdf + .pbkdf2() + .unwrap() + .prf + .oid() + ); + assert_eq!( + enc.oid(), + cs.value + .encryption_algorithm + .pbes2() + .unwrap() + .encryption + .oid() + ); + } + _ => { + panic!("Unexpected bag type"); + } + } + } + } else { + panic!("Unexpected bag type"); + } + } + + match pfx.mac_data { + Some(mac_data) => { + assert_eq!(mac_iterations as i32, mac_data.iterations); + assert_eq!(mac.oid(), mac_data.mac.algorithm.oid); + } + None => { + panic!("Missing MAC"); + } + } +} + +#[cfg(test)] +fn check_with_openssl(password: &str, der_p12: &[u8], key: &[u8], cert: &[u8]) { + use openssl::pkcs12::Pkcs12; + openssl::init(); + let pkcs12 = Pkcs12::from_der(der_p12).unwrap(); + let p12 = pkcs12.as_ref().parse2(password).unwrap(); + let ossl_cert = p12.cert.unwrap(); + let recovered_cert = ossl_cert.to_der().unwrap(); + let ossl_pkey = p12.pkey.unwrap(); + let recovered_key = ossl_pkey.private_key_to_pkcs8().unwrap(); + assert_eq!(recovered_cert, cert); + assert_eq!(recovered_key, key); +} + +#[allow(clippy::unwrap_used)] +#[test] +fn p12_simple() { + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + // read this from SubjectAltName + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + let der_pfx = Pkcs12Builder::new() + .iterations(Some(2048)) + .unwrap() + .key_attributes(Some(key_attrs.clone())) + .cert_attributes(Some(cert_attrs.clone())) + .build_with_rng(&cert.clone(), key, "password", &mut rand::rng()) + .unwrap(); + let contents = parse_pkcs12(&der_pfx, "password").unwrap(); + assert_eq!(*contents.key_der, key); + assert_eq!(contents.certificate.der, cert_bytes); + assert_eq!(contents.key_id, Some(key_id.to_vec())); +} + +#[test] +fn p12_builder_combinations() { + let mac_algs = [ + MacAlgorithm::HmacSha256, + MacAlgorithm::HmacSha384, + MacAlgorithm::HmacSha512, + ]; + let enc_algs = [ + EncryptionAlgorithm::Aes128Cbc, + EncryptionAlgorithm::Aes192Cbc, + EncryptionAlgorithm::Aes256Cbc, + ]; + let kdf_algs = [ + Pbkdf2Prf::HmacWithSha256, + Pbkdf2Prf::HmacWithSha384, + Pbkdf2Prf::HmacWithSha512, + ]; + + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + let password = "password"; + let rng = &mut rand::rng(); + + // Spin over various combinations of algorithms... + for mac in &mac_algs { + for enc in &enc_algs { + for kdf in &kdf_algs { + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let mut md = MacDataBuilder::new_with_salt(mac.clone(), salt); + md.iterations(Some(2048)).unwrap(); + let der_pfx = Pkcs12Builder::new() + .iterations(Some(2048)) + .unwrap() + .cert_enc_algorithm(Some(enc.clone())) + .key_enc_algorithm(Some(enc.clone())) + .cert_kdf_algorithm(Some(*kdf)) + .key_kdf_algorithm(Some(*kdf)) + .mac_data_builder(Some(md)) + .key_attributes(Some(key_attrs.clone())) + .cert_attributes(Some(cert_attrs.clone())) + .build_with_rng(&cert.clone(), key, password, &mut rand::rng()) + .unwrap(); + println!("{mac:?}-{enc:?}-{kdf:?}: {}", buffer_to_hex(&der_pfx)); + + // Parse with pkcs12 crate and make sure algorithms match expectations + check_algs(mac, enc, kdf, &der_pfx, 2048, 2048); + + // Make sure openssl can parse the results + check_with_openssl(password, &der_pfx, key, cert_bytes); + + check_key_and_cert( + &der_pfx, + password, + key, + cert_bytes, + &Some(key_id.to_vec()), + &Some(key_id.to_vec()), + ); + } + } + } +} + +#[cfg(test)] +pub fn buffer_to_hex(buffer: &[u8]) -> String { + std::str::from_utf8(&subtle_encoding::hex::encode_upper(buffer)) + .unwrap_or_default() + .to_string() +} + +#[test] +fn p12_builder_with_defaults_test() { + let mut p12_builder = Pkcs12Builder::new(); + // This test intentionally uses defaults (600k iterations) to verify default behavior. + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + p12_builder.key_attributes(Some(key_attrs)); + p12_builder.cert_attributes(Some(cert_attrs)); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "", &mut rand::rng()) + .unwrap(); + check_key_and_cert( + &der_pfx, + "", + key, + cert_bytes, + &Some(key_id.to_vec()), + &Some(key_id.to_vec()), + ); + check_algs( + &MacAlgorithm::HmacSha256, + &EncryptionAlgorithm::Aes256Cbc, + &Pbkdf2Prf::HmacWithSha256, + &der_pfx, + 600000, + 600000, + ); +} + +#[test] +fn p12_builder_test() { + use hex_literal::hex; + + let mut p12_builder = Pkcs12Builder::new(); + let key_id = hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + // Cert bag + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + p12_builder.cert_attributes(Some(cert_attrs)); + + let cert_kdf_params = Pbkdf2Params { + salt: Salt::new(hex!("9A A2 77 B5 F0 51 B4 50")).unwrap(), + iteration_count: 2048, + key_length: None, + prf: Pbkdf2Prf::HmacWithSha256, + }; + let enc_cert_kdf_params = cert_kdf_params.to_der().unwrap(); + let enc_cert_kdf_params_ref = AnyRef::try_from(enc_cert_kdf_params.as_slice()).unwrap(); + let cert_kdf_alg = AlgorithmIdentifierOwned { + oid: PBKDF2_OID, + parameters: Some(Any::from(enc_cert_kdf_params_ref)), + }; + p12_builder.cert_kdf_algorithm_identifier(Some(cert_kdf_alg)); + + let cert_iv = OctetString::new(hex!("2E 23 6C 8C 7A 44 0C 3E 0F 4E 0D 32 C9 90 E9 97")) + .unwrap() + .to_der() + .unwrap(); + let cert_iv_ref = AnyRef::try_from(cert_iv.as_slice()).unwrap(); + p12_builder.cert_enc_algorithm_identifier(Some(AlgorithmIdentifier { + oid: AES_256_CBC_OID, + parameters: Some(Any::from(cert_iv_ref)), + })); + + // Key bag + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + p12_builder.key_attributes(Some(key_attrs)); + + let key_kdf_params = Pbkdf2Params { + salt: Salt::new(hex!("10 AF 41 1E 77 84 BA CD")).unwrap(), + iteration_count: 2048, + key_length: None, + prf: Pbkdf2Prf::HmacWithSha256, + }; + let enc_key_kdf_params = key_kdf_params.to_der().unwrap(); + let enc_key_kdf_params_ref = AnyRef::try_from(enc_key_kdf_params.as_slice()).unwrap(); + let key_kdf_alg = AlgorithmIdentifierOwned { + oid: PBKDF2_OID, + parameters: Some(Any::from(enc_key_kdf_params_ref)), + }; + p12_builder.key_kdf_algorithm_identifier(Some(key_kdf_alg)); + + let key_iv = OctetString::new(hex!("46 21 13 61 4C 99 4D 1F DA 70 B4 71 16 5A AE 4A")) + .unwrap() + .to_der() + .unwrap(); + let key_iv_ref = AnyRef::try_from(key_iv.as_slice()).unwrap(); + p12_builder.key_enc_algorithm_identifier(Some(AlgorithmIdentifier { + oid: AES_256_CBC_OID, + parameters: Some(Any::from(key_iv_ref)), + })); + + // Mac + let mut md_builder = MacDataBuilder::new(MacAlgorithm::HmacSha256); + md_builder.iterations(Some(2048)).unwrap(); + md_builder.salt(Some(hex!("FF 08 ED 21 81 C8 A8 E3").to_vec())); + p12_builder.mac_data_builder(Some(md_builder)); + + let orig_p12 = include_bytes!("examples/example.pfx"); + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let der_pfx = p12_builder.build(&cert, key, "").unwrap(); + assert_eq!(der_pfx, orig_p12); + + let contents = parse_pkcs12(&der_pfx, "").unwrap(); + assert_eq!(contents.certificate.der, cert_bytes); + assert_eq!(*contents.key_der, key); + assert_eq!(contents.key_id, Some(key_id.to_vec())); +} + +#[test] +fn invalid_iterations() { + let mut p12_builder = Pkcs12Builder::new(); + let oversized: u32 = i32::MAX as u32 + 1; + assert!(p12_builder.iterations(Some(oversized)).is_err()); + + let mut mac_builder = MacDataBuilder::new(MacAlgorithm::HmacSha256); + assert!(mac_builder.iterations(Some(oversized)).is_err()); +} + +#[test] +fn no_mac_data_and_no_key_identifier() { + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.omit_mac(); + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "", &mut rand::rng()) + .unwrap(); + check_key_and_cert(&der_pfx, "", key, cert_bytes, &None, &None); + let pfx = Pfx::from_der(&der_pfx).unwrap(); + assert!(pfx.mac_data.is_none()); +} + +#[test] +fn p12_builder_iterations_test() { + let mut p12_builder = Pkcs12Builder::new(); + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + p12_builder.key_attributes(Some(key_attrs)); + p12_builder.cert_attributes(Some(cert_attrs)); + p12_builder.iterations(Some(2048)).unwrap(); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "", &mut rand::rng()) + .unwrap(); + check_key_and_cert( + &der_pfx, + "", + key, + cert_bytes, + &Some(key_id.to_vec()), + &Some(key_id.to_vec()), + ); + check_algs( + &MacAlgorithm::HmacSha256, + &EncryptionAlgorithm::Aes256Cbc, + &Pbkdf2Prf::HmacWithSha256, + &der_pfx, + 2048, + 2048, + ); +} + +#[test] +fn different_iterations_test() { + let mut p12_builder = Pkcs12Builder::new(); + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + p12_builder.key_attributes(Some(key_attrs)); + p12_builder.cert_attributes(Some(cert_attrs)); + p12_builder.iterations(Some(2048)).unwrap(); + + let rng = &mut rand::rng(); + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let mut md = MacDataBuilder::new_with_salt(MacAlgorithm::HmacSha256, salt); + md.iterations(Some(2049)).unwrap(); + p12_builder.mac_data_builder(Some(md)); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "", &mut rand::rng()) + .unwrap(); + check_key_and_cert( + &der_pfx, + "", + key, + cert_bytes, + &Some(key_id.to_vec()), + &Some(key_id.to_vec()), + ); + check_algs( + &MacAlgorithm::HmacSha256, + &EncryptionAlgorithm::Aes256Cbc, + &Pbkdf2Prf::HmacWithSha256, + &der_pfx, + 2048, + 2049, + ); +} + +#[test] +fn different_key_and_cert_ids_test() { + let mut p12_builder = Pkcs12Builder::new(); + let cert_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + let key_id = hex_literal::hex!("AA BB CC DD 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &cert_id).unwrap(); + + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + p12_builder.key_attributes(Some(key_attrs)); + p12_builder.cert_attributes(Some(cert_attrs)); + p12_builder.iterations(Some(2048)).unwrap(); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "", &mut rand::rng()) + .unwrap(); + check_key_and_cert( + &der_pfx, + "", + key, + cert_bytes, + &Some(cert_id.to_vec()), + &Some(key_id.to_vec()), + ); + check_algs( + &MacAlgorithm::HmacSha256, + &EncryptionAlgorithm::Aes256Cbc, + &Pbkdf2Prf::HmacWithSha256, + &der_pfx, + 2048, + 2048, + ); +} + +#[test] +fn cert_id_only_test() { + let mut p12_builder = Pkcs12Builder::new(); + let cert_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &cert_id).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + p12_builder.cert_attributes(Some(cert_attrs)); + p12_builder.iterations(Some(2048)).unwrap(); + + let rng = &mut rand::rng(); + let mut salt = vec![0_u8; 16]; + rng.fill_bytes(salt.as_mut_slice()); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "", &mut rand::rng()) + .unwrap(); + check_key_and_cert( + &der_pfx, + "", + key, + cert_bytes, + &Some(cert_id.to_vec()), + &None, + ); + check_algs( + &MacAlgorithm::HmacSha256, + &EncryptionAlgorithm::Aes256Cbc, + &Pbkdf2Prf::HmacWithSha256, + &der_pfx, + 2048, + 2048, + ); +} + +/// Verify that parsing a P12 with legacy PBE encryption produces a clear error +/// when the `legacy` feature is not enabled. Tests the `get_cert` path directly +/// to bypass the MAC check (the test P12 uses SHA-1 MAC which is also gated). +#[cfg(not(feature = "legacy"))] +#[test] +fn legacy_pbe_cert_rejected_without_feature() { + // Build a valid PBES2 P12, then replace the cert EncryptedData's algorithm OID + // with a legacy PBE OID to simulate a legacy-encrypted cert bag. + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.omit_mac(); // skip MAC so we can tamper freely + p12_builder.iterations(Some(2048)).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "test", &mut rand::rng()) + .unwrap(); + + let pfx = Pfx::from_der(&der_pfx).unwrap(); + let auth_safes = get_auth_safes(&pfx.auth_safe.content).unwrap(); + for auth_safe in &auth_safes { + if ID_ENCRYPTED_DATA == auth_safe.content_type { + let enc_data = EncryptedData::from_der(&auth_safe.content.to_der().unwrap()).unwrap(); + + // Replace the PBES2 OID with pbeWithSHAAnd3-KeyTripleDES-CBC + let legacy_oid = const_oid::ObjectIdentifier::new_unwrap("1.2.840.113549.1.12.1.3"); + let tampered_enc_data = EncryptedData { + version: enc_data.version, + enc_content_info: cms::enveloped_data::EncryptedContentInfo { + content_type: enc_data.enc_content_info.content_type, + content_enc_alg: AlgorithmIdentifier { + oid: legacy_oid, + parameters: enc_data.enc_content_info.content_enc_alg.parameters, + }, + encrypted_content: enc_data.enc_content_info.encrypted_content, + }, + unprotected_attrs: None, + }; + let der_tampered = tampered_enc_data.to_der().unwrap(); + let any_tampered = Any::from_der(&der_tampered).unwrap(); + + let err = match get_cert(&any_tampered, "test") { + Err(e) => e, + Ok(_) => panic!("Expected error for legacy PBE cert without feature"), + }; + let msg = format!("{err}"); + assert!( + msg.contains("legacy") && msg.contains("feature"), + "Expected error mentioning legacy feature, got: {msg}" + ); + return; + } + } + panic!("Did not find ID_ENCRYPTED_DATA in auth_safes"); +} + +/// Verify that parsing a P12 with legacy PBE key encryption produces a clear error +/// when the `legacy` feature is not enabled. Tests the `get_key` path directly. +#[cfg(not(feature = "legacy"))] +#[test] +fn legacy_pbe_key_rejected_without_feature() { + use pkcs12::builder::asn1_utils::get_key; + + // Build a valid PBES2 P12, extract the key auth_safe content, then tamper the + // key encryption OID to a legacy PBE OID. + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.omit_mac(); + p12_builder.iterations(Some(2048)).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "test", &mut rand::rng()) + .unwrap(); + + let pfx = Pfx::from_der(&der_pfx).unwrap(); + let auth_safes = get_auth_safes(&pfx.auth_safe.content).unwrap(); + for auth_safe in &auth_safes { + if ID_DATA == auth_safe.content_type { + let safe_bags = get_safe_bags(&auth_safe.content).unwrap(); + for safe_bag in safe_bags { + if safe_bag.bag_id == PKCS_12_PKCS8_KEY_BAG_OID { + // Parse the encrypted key and replace the algorithm OID + let cs: ContextSpecific = + ContextSpecific::from_der(&safe_bag.bag_value).unwrap(); + let legacy_oid = + const_oid::ObjectIdentifier::new_unwrap("1.2.840.113549.1.12.1.3"); + let tampered_epki = pkcs12::pbe_params::EncryptedPrivateKeyInfo { + encryption_algorithm: AlgorithmIdentifierOwned { + oid: legacy_oid, + parameters: cs.value.encryption_algorithm.parameters, + }, + encrypted_data: cs.value.encrypted_data, + }; + // Re-encode as a SafeBag + let tampered_bag_value = tampered_epki.to_der().unwrap(); + let tampered_safe_bag = pkcs12::safe_bag::SafeBag { + bag_id: PKCS_12_PKCS8_KEY_BAG_OID, + bag_value: tampered_bag_value, + bag_attributes: safe_bag.bag_attributes, + }; + let tampered_bags = vec![tampered_safe_bag]; + let tampered_bags_der = tampered_bags.to_der().unwrap(); + let tampered_os = OctetString::new(tampered_bags_der) + .unwrap() + .to_der() + .unwrap(); + let tampered_any = Any::from_der(&tampered_os).unwrap(); + + let err = match get_key(&tampered_any, "test") { + Err(e) => e, + Ok(_) => panic!("Expected error for legacy PBE key without feature"), + }; + let msg = format!("{err}"); + assert!( + msg.contains("legacy") && msg.contains("feature"), + "Expected error mentioning legacy feature, got: {msg}" + ); + return; + } + } + } + } + panic!("Did not find key SafeBag in auth_safes"); +} + +/// Verify that a P12 with an excessively high MAC iteration count is rejected during parsing. +#[test] +fn excessive_mac_iterations_rejected() { + let mut p12_builder = Pkcs12Builder::new(); + p12_builder.iterations(Some(2048)).unwrap(); + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let der_pfx = p12_builder + .build_with_rng(&cert, key, "test", &mut rand::rng()) + .unwrap(); + + // Parse the valid P12 and re-encode with an excessive MAC iteration count. + let mut pfx = Pfx::from_der(&der_pfx).unwrap(); + let mac_data = pfx.mac_data.as_mut().unwrap(); + mac_data.iterations = 100_000_001; + let tampered = pfx.to_der().unwrap(); + + let err = match parse_pkcs12(&tampered, "test") { + Err(e) => e, + Ok(_) => panic!("Expected error for excessive iterations"), + }; + let msg = format!("{err}"); + assert!( + msg.contains("iterations"), + "Expected error about iterations limit, got: {msg}" + ); +} + +/// Helper to build a PKCS #12 with Oracle TrustedKeyUsage attributes on both key and cert bags. +#[allow(clippy::unwrap_used)] +fn build_p12_with_oracle_tku() -> Vec { + use const_oid::ObjectIdentifier; + use const_oid::db::rfc5280::ANY_EXTENDED_KEY_USAGE; + use x509_cert::attr::Attribute; + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + + // Oracle TrustedKeyUsage attribute: OID value = anyExtendedKeyUsage + let oracle_trusted_key_usage = ObjectIdentifier::new_unwrap("2.16.840.1.113894.746875.1.1"); + let eku_bytes = ANY_EXTENDED_KEY_USAGE.to_der().unwrap(); + let eku_ref = AnyRef::try_from(eku_bytes.as_slice()).unwrap(); + let mut tku_values = SetOfVec::new(); + tku_values.insert(Any::from(eku_ref)).unwrap(); + let tku_attr = Attribute { + oid: oracle_trusted_key_usage, + values: tku_values, + }; + + // Key bag: localKeyID + friendlyName + TrustedKeyUsage + let mut key_attrs = SetOfVec::new(); + add_key_id_attr(&mut key_attrs, &key_id).unwrap(); + add_friendly_name_attr(&mut key_attrs, "my-key").unwrap(); + key_attrs.insert(tku_attr.clone()).unwrap(); + + // Cert bag: localKeyID + friendlyName + TrustedKeyUsage + let mut cert_attrs = SetOfVec::new(); + add_key_id_attr(&mut cert_attrs, &key_id).unwrap(); + add_friendly_name_attr(&mut cert_attrs, "my-cert").unwrap(); + cert_attrs.insert(tku_attr.clone()).unwrap(); + + Pkcs12Builder::new() + .iterations(Some(2048)) + .unwrap() + .key_attributes(Some(key_attrs)) + .cert_attributes(Some(cert_attrs)) + .build_with_rng(&cert, key, "password", &mut rand::rng()) + .unwrap() +} + +/// Build a PKCS #12 with an ORACLE_TrustedKeyUsage attribute on both the key and cert bags, +/// alongside the well-known localKeyID and friendlyName, and verify they all round-trip. +#[allow(clippy::unwrap_used)] +#[test] +fn other_attributes_roundtrip() { + use const_oid::ObjectIdentifier; + + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let key_id = hex_literal::hex!("EF 09 61 31 5F 51 9D 61 F2 69 7D 9E 75 E5 52 15 D0 7B 00 6D"); + let oracle_trusted_key_usage = ObjectIdentifier::new_unwrap("2.16.840.1.113894.746875.1.1"); + + let der_pfx = build_p12_with_oracle_tku(); + + let contents = parse_pkcs12(&der_pfx, "password").unwrap(); + + // Key bag attributes + assert_eq!(*contents.key_der, key); + assert_eq!(contents.key_id, Some(key_id.to_vec())); + assert_eq!(contents.friendly_name.as_deref(), Some("my-key")); + let key_other = contents + .other_key_attributes + .expect("expected other key attributes"); + assert_eq!(key_other.len(), 1); + assert_eq!(key_other[0].oid, oracle_trusted_key_usage); + + // Cert bag attributes + assert_eq!(contents.certificate.der, cert_bytes); + assert_eq!(contents.certificate.local_key_id, Some(key_id.to_vec())); + assert_eq!( + contents.certificate.friendly_name.as_deref(), + Some("my-cert") + ); + let cert_other = contents + .certificate + .other_attributes + .expect("expected other cert attributes"); + assert_eq!(cert_other.len(), 1); + assert_eq!(cert_other[0].oid, oracle_trusted_key_usage); +} diff --git a/pkcs12/tests/builder/sha1_mac.rs b/pkcs12/tests/builder/sha1_mac.rs new file mode 100644 index 000000000..d0895a836 --- /dev/null +++ b/pkcs12/tests/builder/sha1_mac.rs @@ -0,0 +1,196 @@ +//! Tests for the `legacy` feature: HMAC-SHA-1 MAC verification support. + +use std::process::Command; + +use der::Decode; +use tempfile::TempDir; +use x509_cert::Certificate; + +use pkcs12::builder::{MacAlgorithm, MacDataBuilder, Pkcs12Builder, asn1_utils::parse_pkcs12}; + +const PASSWORD: &str = "sha1-test-pass"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Generate a fresh RSA key + self-signed certificate via OpenSSL. +/// Returns `(key_pem_path, cert_pem_path, key_der, cert_der, TempDir)`. +fn generate_credentials() -> (String, String, Vec, Vec, TempDir) { + let dir = TempDir::new().expect("temp dir"); + let key_pem = dir.path().join("key.pem"); + let cert_pem = dir.path().join("cert.pem"); + + let ok = Command::new("openssl") + .args([ + "genpkey", + "-quiet", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + "-out", + key_pem.to_str().unwrap(), + ]) + .status() + .expect("spawn openssl genpkey"); + assert!(ok.success(), "openssl genpkey failed"); + + let ok = Command::new("openssl") + .args([ + "req", + "-quiet", + "-new", + "-x509", + "-key", + key_pem.to_str().unwrap(), + "-out", + cert_pem.to_str().unwrap(), + "-days", + "365", + "-subj", + "/CN=sha1-mac-test/O=Test", + ]) + .status() + .expect("spawn openssl req"); + assert!(ok.success(), "openssl req -x509 failed"); + + let key_der = Command::new("openssl") + .args(["pkey", "-in", key_pem.to_str().unwrap(), "-outform", "DER"]) + .output() + .expect("openssl pkey"); + assert!(key_der.status.success()); + + let cert_der = Command::new("openssl") + .args(["x509", "-in", cert_pem.to_str().unwrap(), "-outform", "DER"]) + .output() + .expect("openssl x509"); + assert!(cert_der.status.success()); + + let key_pem_str = key_pem.to_str().unwrap().to_string(); + let cert_pem_str = cert_pem.to_str().unwrap().to_string(); + ( + key_pem_str, + cert_pem_str, + key_der.stdout, + cert_der.stdout, + dir, + ) +} + +/// Build a PKCS #12 file with OpenSSL using SHA-1 MAC and PBES2 encryption. +fn openssl_export_sha1_mac(cert_pem_path: &str, key_pem_path: &str) -> Vec { + let dir = TempDir::new().expect("temp dir"); + let p12_path = dir.path().join("out.p12"); + + let out = Command::new("openssl") + .args([ + "pkcs12", + "-export", + "-in", + cert_pem_path, + "-inkey", + key_pem_path, + "-out", + p12_path.to_str().unwrap(), + "-passout", + &format!("pass:{PASSWORD}"), + "-keypbe", + "AES-256-CBC", + "-certpbe", + "AES-256-CBC", + "-macalg", + "SHA1", + "-iter", + "2048", + ]) + .output() + .expect("spawn openssl pkcs12 -export"); + + if !out.status.success() { + eprintln!( + "[openssl pkcs12 -export stderr]\n{}", + String::from_utf8_lossy(&out.stderr) + ); + } + assert!( + out.status.success(), + "openssl pkcs12 -export with SHA1 MAC failed" + ); + + std::fs::read(&p12_path).expect("read exported p12") +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Verify that a PKCS #12 file built by OpenSSL with `-macalg SHA1` can be +/// parsed and its MAC verified by our library. +#[test] +fn openssl_sha1_mac_rust_reads() { + let (key_pem, cert_pem, key_der, cert_der, _dir) = generate_credentials(); + + let p12_bytes = openssl_export_sha1_mac(&cert_pem, &key_pem); + + let contents = + parse_pkcs12(&p12_bytes, PASSWORD).expect("get_key_and_cert with SHA1 MAC failed"); + + assert_eq!(*contents.key_der, key_der); + assert_eq!(contents.certificate.der, cert_der); +} + +/// Verify that MAC verification fails with the wrong password on a SHA-1 MAC file. +#[test] +fn openssl_sha1_mac_wrong_password_fails() { + let (key_pem, cert_pem, _, _, _dir) = generate_credentials(); + + let p12_bytes = openssl_export_sha1_mac(&cert_pem, &key_pem); + + let result = parse_pkcs12(&p12_bytes, "wrong-password"); + assert!( + result.is_err(), + "SHA1 MAC verification should fail with wrong password" + ); +} + +/// Verify that `MacDataBuilder` accepts `HmacSha1` for MAC generation (needed for legacy P12 interop). +#[test] +fn mac_data_builder_accepts_sha1() { + let key = include_bytes!("examples/key.der"); + let cert_bytes = include_bytes!("examples/cert.der"); + let cert = Certificate::from_der(cert_bytes).unwrap(); + + let mut md = MacDataBuilder::new(MacAlgorithm::HmacSha1); + md.iterations(Some(2048)).unwrap(); + md.salt(Some(vec![0u8; 16])); + + let mut builder = Pkcs12Builder::new(); + builder.mac_data_builder(Some(md)); + + let result = builder.build_with_rng(&cert, key, "password", &mut rand::rng()); + assert!(result.is_ok(), "building with HmacSha1 should succeed"); + + // Verify the generated P12 can be parsed back with MAC verification + let p12_bytes = result.unwrap(); + let contents = pkcs12::builder::parse_pkcs12(&p12_bytes, "password"); + assert!( + contents.is_ok(), + "should be able to parse back P12 with SHA-1 MAC" + ); +} + +/// Verify OID round-trip for `MacAlgorithm::HmacSha1`. +#[test] +fn hmac_sha1_oid_round_trip() { + let alg = MacAlgorithm::HmacSha1; + let oid = alg.oid(); + let recovered = MacAlgorithm::try_from(oid).unwrap(); + assert_eq!(alg, recovered); +} + +/// Verify `HmacSha1` output size is 20 bytes. +#[test] +fn hmac_sha1_output_size() { + assert_eq!(MacAlgorithm::HmacSha1.output_size(), 20); +}