use crate::tls::wallet::{WalletContents, WalletError};
use der::asn1::ObjectIdentifier;
use der::{Decode, Reader, SliceReader, Tag};
const OID_DATA: &str = "1.2.840.113549.1.7.1";
const OID_ENCRYPTED_DATA: &str = "1.2.840.113549.1.7.6";
const OID_PBES2: &str = "1.2.840.113549.1.5.13";
const OID_PKCS12_PBE_SHA1_3DES: &str = "1.2.840.113549.1.12.1.3";
const OID_PBKDF2: &str = "1.2.840.113549.1.5.12";
const OID_HMAC_SHA256: &str = "1.2.840.113549.2.9";
const OID_HMAC_SHA1: &str = "1.2.840.113549.2.7";
const OID_AES128_CBC: &str = "2.16.840.1.101.3.4.1.2";
const OID_AES192_CBC: &str = "2.16.840.1.101.3.4.1.22";
const OID_AES256_CBC: &str = "2.16.840.1.101.3.4.1.42";
const OID_KEY_BAG: &str = "1.2.840.113549.1.12.10.1.1";
const OID_PKCS8_SHROUDED_KEY_BAG: &str = "1.2.840.113549.1.12.10.1.2";
const OID_CERT_BAG: &str = "1.2.840.113549.1.12.10.1.3";
fn p12(msg: impl Into<String>) -> WalletError {
WalletError::Pkcs12(msg.into())
}
pub(super) fn decrypt_encrypted_private_key_info(
der_bytes: &[u8],
password: &[u8],
) -> Result<Vec<u8>, WalletError> {
let map_err = |e: WalletError| match e {
WalletError::Pkcs12(msg) => WalletError::KeyDecrypt(msg),
other => other,
};
let mut root = into_seq(der_bytes).map_err(map_err)?;
let (tag, epki_body) = read_tlv(&mut root).map_err(map_err)?;
if tag != Tag::Sequence {
return Err(WalletError::KeyDecrypt(
"EncryptedPrivateKeyInfo: expected SEQUENCE".to_string(),
));
}
let mut epki = into_seq(epki_body).map_err(map_err)?;
let (alg_tag, alg_body) = read_tlv(&mut epki).map_err(map_err)?;
if alg_tag != Tag::Sequence {
return Err(WalletError::KeyDecrypt(
"EncryptedPrivateKeyInfo: expected AlgorithmIdentifier".to_string(),
));
}
let (key, iv) = derive_pbes2_params(alg_body, password).map_err(map_err)?;
let (ct_tag, ct) = read_tlv(&mut epki).map_err(map_err)?;
if ct_tag != Tag::OctetString {
return Err(WalletError::KeyDecrypt(
"EncryptedPrivateKeyInfo: expected encrypted OCTET STRING".to_string(),
));
}
aes_cbc_decrypt(&key, &iv, ct).map_err(|e| match e {
WalletError::Pkcs12(msg) => WalletError::KeyDecrypt(format!(
"{msg} (wrong wallet_password, or corrupted key material)"
)),
other => other,
})
}
fn read_oid(reader: &mut SliceReader<'_>) -> Result<ObjectIdentifier, WalletError> {
ObjectIdentifier::decode(reader).map_err(|e| p12(format!("OID decode: {e}")))
}
fn read_tlv<'a>(reader: &mut SliceReader<'a>) -> Result<(Tag, &'a [u8]), WalletError> {
let header = der::Header::decode(reader).map_err(|e| p12(format!("TLV header: {e}")))?;
let len = usize::try_from(header.length).map_err(|_| p12("length overflow"))?;
let bytes = reader
.read_slice(header.length)
.map_err(|e| p12(format!("TLV body ({len} bytes): {e}")))?;
Ok((header.tag, bytes))
}
fn into_seq<'a>(bytes: &'a [u8]) -> Result<SliceReader<'a>, WalletError> {
SliceReader::new(bytes).map_err(|e| p12(format!("subreader: {e}")))
}
pub(super) fn parse_pfx(data: &[u8], password: &[u8]) -> Result<WalletContents, WalletError> {
let mut root = into_seq(data)?;
let (tag, pfx_body) = read_tlv(&mut root)?;
if tag != Tag::Sequence {
return Err(p12("PFX: expected outer SEQUENCE"));
}
let mut pfx = into_seq(pfx_body)?;
let _ = read_tlv(&mut pfx)?;
let (tag, ci_body) = read_tlv(&mut pfx)?;
if tag != Tag::Sequence {
return Err(p12("PFX authSafe: expected ContentInfo SEQUENCE"));
}
let auth_safe_data = read_content_info_data(ci_body, OID_DATA)?;
let mut as_reader = into_seq(auth_safe_data)?;
let (tag, authsafe_seq) = read_tlv(&mut as_reader)?;
if tag != Tag::Sequence {
return Err(p12("AuthenticatedSafe: expected SEQUENCE OF ContentInfo"));
}
let mut decrypted_safe_contents: Vec<u8> = Vec::new();
let mut plain_safe_contents: Vec<Vec<u8>> = Vec::new();
let mut inner = into_seq(authsafe_seq)?;
while !inner.is_finished() {
let (tag, ci) = read_tlv(&mut inner)?;
if tag != Tag::Sequence {
return Err(p12("AuthenticatedSafe element: expected ContentInfo"));
}
let mut ci_reader = into_seq(ci)?;
let content_type = read_oid(&mut ci_reader)?;
let ct = content_type.to_string();
let (ctx_tag, content_bytes) = read_tlv(&mut ci_reader)?;
if ctx_tag.is_context_specific() {
let mut ctx = into_seq(content_bytes)?;
if ct == OID_ENCRYPTED_DATA {
let dec = decrypt_encrypted_data(&mut ctx, password)?;
decrypted_safe_contents = dec;
} else if ct == OID_DATA {
let (t, os) = read_tlv(&mut ctx)?;
if t == Tag::OctetString {
plain_safe_contents.push(os.to_vec());
}
}
}
}
let mut contents = WalletContents::default();
if !decrypted_safe_contents.is_empty() {
read_safe_contents(&decrypted_safe_contents, password, &mut contents)?;
}
for sc in &plain_safe_contents {
read_safe_contents(sc, password, &mut contents)?;
}
if contents.ca_certificates.is_empty() && contents.client_cert_chain.is_empty() {
return Err(p12("PFX produced no certificates"));
}
if contents.client_private_key.is_some() && contents.client_cert_chain.is_empty() {
contents.client_cert_chain = contents.ca_certificates.clone();
}
Ok(contents)
}
fn read_content_info_data<'a>(
ci_body: &'a [u8],
expected_oid: &str,
) -> Result<&'a [u8], WalletError> {
let mut reader = into_seq(ci_body)?;
let oid = read_oid(&mut reader)?;
if oid.to_string() != expected_oid {
return Err(p12(format!(
"ContentInfo: expected {expected_oid}, got {oid}"
)));
}
let (ctx_tag, content) = read_tlv(&mut reader)?;
if !ctx_tag.is_context_specific() {
return Err(p12("ContentInfo: expected [0] EXPLICIT content"));
}
let mut ctx = into_seq(content)?;
let (t, os) = read_tlv(&mut ctx)?;
if t != Tag::OctetString {
return Err(p12("ContentInfo content: expected OCTET STRING"));
}
Ok(os)
}
fn decrypt_encrypted_data(
ctx: &mut SliceReader<'_>,
password: &[u8],
) -> Result<Vec<u8>, WalletError> {
let (tag, ed_body) = read_tlv(ctx)?;
if tag != Tag::Sequence {
return Err(p12("EncryptedData: expected SEQUENCE"));
}
let mut ed = into_seq(ed_body)?;
let _ = read_tlv(&mut ed)?; let (tag, eci_body) = read_tlv(&mut ed)?;
if tag != Tag::Sequence {
return Err(p12("EncryptedContentInfo: expected SEQUENCE"));
}
let mut eci = into_seq(eci_body)?;
let content_type = read_oid(&mut eci)?;
if content_type.to_string() != OID_DATA {
return Err(p12("EncryptedContentInfo: content type must be data"));
}
let (tag, alg_body) = read_tlv(&mut eci)?;
if tag != Tag::Sequence {
return Err(p12("EncryptionAlgorithm: expected SEQUENCE"));
}
let material = derive_content_encryption(alg_body, password)?;
let (ctag, enc_content) = read_tlv(&mut eci)?;
if !ctag.is_context_specific() {
return Err(p12("encryptedContent: expected [0] IMPLICIT"));
}
decrypt_ciphertext(material, enc_content)
}
#[derive(Clone, Copy)]
enum ContentCipher {
AesCbc,
TripleDesCbc,
}
struct DerivedEncryption {
key: Vec<u8>,
iv: Vec<u8>,
cipher: ContentCipher,
}
fn decrypt_ciphertext(material: DerivedEncryption, ct: &[u8]) -> Result<Vec<u8>, WalletError> {
match material.cipher {
ContentCipher::AesCbc => aes_cbc_decrypt(&material.key, &material.iv, ct),
ContentCipher::TripleDesCbc => des_ede3_cbc_decrypt(&material.key, &material.iv, ct),
}
}
fn derive_content_encryption(
alg_body: &[u8],
password: &[u8],
) -> Result<DerivedEncryption, WalletError> {
let mut alg = into_seq(alg_body)?;
let algo = read_oid(&mut alg)?;
let oid = algo.to_string();
if oid == OID_PBES2 {
let (key, iv) = derive_pbes2_params(alg_body, password)?;
return Ok(DerivedEncryption {
key,
iv,
cipher: ContentCipher::AesCbc,
});
}
if oid == OID_PKCS12_PBE_SHA1_3DES {
let (tag, params) = read_tlv(&mut alg)?;
if tag != Tag::Sequence {
return Err(p12("PKCS#12 PBE params: expected SEQUENCE"));
}
let (key, iv) = derive_pkcs12_pbe_sha1_3des(password, params)?;
return Ok(DerivedEncryption {
key,
iv,
cipher: ContentCipher::TripleDesCbc,
});
}
Err(p12(format!(
"unsupported PFX encryption algorithm {algo}; supported: PBES2 (PBKDF2 + AES-CBC) \
and PKCS#12 PBE-SHA1-3DES ({OID_PKCS12_PBE_SHA1_3DES}) — re-export with a modern \
cipher if this is RC2 or another legacy scheme"
)))
}
fn derive_pbes2_params(
alg_body: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), WalletError> {
let mut alg = into_seq(alg_body)?;
let algo = read_oid(&mut alg)?;
if algo.to_string() != OID_PBES2 {
return Err(p12(format!(
"unsupported PFX encryption algorithm {algo}; only PBES2 \
(PBKDF2 + AES-CBC) is supported — re-export the wallet with a \
modern (AES) cipher, e.g. `orapki wallet create` 19c+ or \
`openssl pkcs12 -export`"
)));
}
let (tag, params) = read_tlv(&mut alg)?;
if tag != Tag::Sequence {
return Err(p12("PBES2 params: expected SEQUENCE"));
}
let mut p = into_seq(params)?;
let (tag, kdf_body) = read_tlv(&mut p)?;
if tag != Tag::Sequence {
return Err(p12("PBES2 KDF: expected SEQUENCE"));
}
let (tag, enc_body) = read_tlv(&mut p)?;
if tag != Tag::Sequence {
return Err(p12("PBES2 encScheme: expected SEQUENCE"));
}
let mut kdf = into_seq(kdf_body)?;
let kdf_oid = read_oid(&mut kdf)?;
if kdf_oid.to_string() != OID_PBKDF2 {
return Err(p12(format!("unsupported KDF {kdf_oid}; only PBKDF2")));
}
let (tag, pbkdf2_params) = read_tlv(&mut kdf)?;
if tag != Tag::Sequence {
return Err(p12("PBKDF2 params: expected SEQUENCE"));
}
let mut pk = into_seq(pbkdf2_params)?;
let (tag, salt) = read_tlv(&mut pk)?;
if tag != Tag::OctetString {
return Err(p12("PBKDF2 salt: expected OCTET STRING"));
}
let (tag, iter_bytes) = read_tlv(&mut pk)?;
if tag != Tag::Integer {
return Err(p12("PBKDF2 iterations: expected INTEGER"));
}
let iterations = be_uint(iter_bytes)?;
let mut key_len: Option<usize> = None;
let mut prf = PrfHash::Sha1; while !pk.is_finished() {
let (tag, body) = read_tlv(&mut pk)?;
if tag == Tag::Integer {
key_len = Some(usize::try_from(be_uint(body)?).map_err(|_| p12("keylen overflow"))?);
} else if tag == Tag::Sequence {
let mut prf_reader = into_seq(body)?;
let prf_oid = read_oid(&mut prf_reader)?;
prf = match prf_oid.to_string().as_str() {
OID_HMAC_SHA256 => PrfHash::Sha256,
OID_HMAC_SHA1 => PrfHash::Sha1,
other => return Err(p12(format!("unsupported PBKDF2 PRF {other}"))),
};
}
}
let mut enc = into_seq(enc_body)?;
let enc_oid = read_oid(&mut enc)?;
let derived_key_len = match enc_oid.to_string().as_str() {
OID_AES128_CBC => 16,
OID_AES192_CBC => 24,
OID_AES256_CBC => 32,
other => {
return Err(p12(format!(
"unsupported PBES2 cipher {other}; only AES-CBC"
)))
}
};
let (tag, iv) = read_tlv(&mut enc)?;
if tag != Tag::OctetString {
return Err(p12("AES-CBC IV: expected OCTET STRING"));
}
if iv.len() != 16 {
return Err(p12("AES-CBC IV must be 16 bytes"));
}
let key_len = match key_len {
Some(specified) if specified != derived_key_len => {
return Err(p12(format!(
"PBES2 keyLength {specified} does not match AES key size {derived_key_len}"
)));
}
Some(specified) => specified,
None => derived_key_len,
};
let key = pbkdf2_derive(password, salt, iterations, key_len, prf)?;
Ok((key, iv.to_vec()))
}
#[derive(Clone, Copy)]
enum PrfHash {
Sha1,
Sha256,
}
const MAX_PBKDF2_KEY_LEN: usize = 1024;
const MAX_PBKDF2_ITERATIONS: u64 = 10_000_000;
fn pbkdf2_derive(
password: &[u8],
salt: &[u8],
iterations: u64,
key_len: usize,
prf: PrfHash,
) -> Result<Vec<u8>, WalletError> {
use hmac::Hmac;
if key_len > MAX_PBKDF2_KEY_LEN {
return Err(p12(format!(
"PBKDF2 keyLength {key_len} exceeds maximum {MAX_PBKDF2_KEY_LEN}"
)));
}
if iterations == 0 || iterations > MAX_PBKDF2_ITERATIONS {
return Err(p12(format!(
"PBKDF2 iteration count {iterations} must be in 1..={MAX_PBKDF2_ITERATIONS}"
)));
}
let iters =
u32::try_from(iterations).map_err(|_| p12("PBKDF2 iteration count exceeds u32 range"))?;
let mut out = vec![0u8; key_len];
match prf {
PrfHash::Sha1 => {
pbkdf2::pbkdf2::<Hmac<sha1::Sha1>>(password, salt, iters, &mut out).unwrap_or_default();
}
PrfHash::Sha256 => {
pbkdf2::pbkdf2::<Hmac<sha2::Sha256>>(password, salt, iters, &mut out)
.unwrap_or_default();
}
}
Ok(out)
}
fn aes_cbc_decrypt(key: &[u8], iv: &[u8], ct: &[u8]) -> Result<Vec<u8>, WalletError> {
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
let mut buf = ct.to_vec();
macro_rules! run {
($aes:ty) => {{
type Dec = cbc::Decryptor<$aes>;
let dec = Dec::new_from_slices(key, iv).map_err(|e| p12(format!("AES init: {e}")))?;
let pt = dec
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| p12(format!("AES decrypt/unpad: {e}")))?;
Ok(pt.to_vec())
}};
}
match key.len() {
16 => run!(aes::Aes128),
24 => run!(aes::Aes192),
32 => run!(aes::Aes256),
n => Err(p12(format!("bad AES key length {n}"))),
}
}
fn des_ede3_cbc_decrypt(key: &[u8], iv: &[u8], ct: &[u8]) -> Result<Vec<u8>, WalletError> {
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
if key.len() != 24 {
return Err(p12(format!("bad 3DES key length {}", key.len())));
}
if iv.len() != 8 {
return Err(p12(format!("3DES IV must be 8 bytes, got {}", iv.len())));
}
type Dec = cbc::Decryptor<des::TdesEde3>;
let mut buf = ct.to_vec();
let dec = Dec::new_from_slices(key, iv).map_err(|e| p12(format!("3DES init: {e}")))?;
let pt = dec
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| p12(format!("3DES decrypt/unpad: {e}")))?;
Ok(pt.to_vec())
}
fn derive_pkcs12_pbe_sha1_3des(
password: &[u8],
params: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), WalletError> {
let mut p = into_seq(params)?;
let (tag, salt) = read_tlv(&mut p)?;
if tag != Tag::OctetString {
return Err(p12("PKCS#12 PBE salt: expected OCTET STRING"));
}
let (tag, iter_bytes) = read_tlv(&mut p)?;
if tag != Tag::Integer {
return Err(p12("PKCS#12 PBE iterations: expected INTEGER"));
}
let iterations = be_uint(iter_bytes)?;
if iterations == 0 {
return Err(p12("PKCS#12 PBE iterations must be >= 1"));
}
let bmp = password_to_bmp(password)?;
let key = pkcs12_kdf(PKCS12_ID_KEY, &bmp, salt, iterations, 24)?;
let iv = pkcs12_kdf(PKCS12_ID_IV, &bmp, salt, iterations, 8)?;
Ok((key, iv))
}
const PKCS12_ID_KEY: u8 = 1;
const PKCS12_ID_IV: u8 = 2;
const MAX_PKCS12_ITERATIONS: u64 = 10_000_000;
fn password_to_bmp(password: &[u8]) -> Result<Vec<u8>, WalletError> {
let s = std::str::from_utf8(password)
.map_err(|_| p12("wallet password is not valid UTF-8 for a PKCS#12 BMPString"))?;
let mut out = Vec::with_capacity(password.len() * 2 + 2);
for unit in s.encode_utf16() {
out.extend_from_slice(&unit.to_be_bytes());
}
out.extend_from_slice(&[0u8, 0u8]);
Ok(out)
}
fn pkcs12_kdf(
id: u8,
password: &[u8],
salt: &[u8],
iterations: u64,
n: usize,
) -> Result<Vec<u8>, WalletError> {
use sha1::{Digest, Sha1};
const U: usize = 20;
const V: usize = 64;
if iterations > MAX_PKCS12_ITERATIONS {
return Err(p12(format!(
"PKCS#12 iteration count {iterations} exceeds maximum {MAX_PKCS12_ITERATIONS}"
)));
}
let d = [id; V];
let mut i_buf = pkcs12_expand(salt, V);
i_buf.extend_from_slice(&pkcs12_expand(password, V));
let blocks = n.div_ceil(U);
let mut out = Vec::with_capacity(blocks * U);
for _ in 0..blocks {
let mut a = {
let mut h = Sha1::new();
h.update(d);
h.update(&i_buf);
h.finalize()
};
for _ in 1..iterations {
let mut h = Sha1::new();
h.update(a);
a = h.finalize();
}
let b = pkcs12_expand(&a, V);
let k = i_buf.len() / V;
for j in 0..k {
pkcs12_add_block(&mut i_buf[j * V..(j + 1) * V], &b);
}
out.extend_from_slice(&a);
}
out.truncate(n);
Ok(out)
}
fn pkcs12_expand(data: &[u8], v: usize) -> Vec<u8> {
if data.is_empty() {
return Vec::new();
}
let out_len = data.len().div_ceil(v) * v;
let mut out = Vec::with_capacity(out_len);
for i in 0..out_len {
out.push(data[i % data.len()]);
}
out
}
fn pkcs12_add_block(block: &mut [u8], b: &[u8]) {
let mut carry: u16 = 1;
for idx in (0..block.len()).rev() {
let sum = u16::from(block[idx]) + u16::from(b[idx]) + carry;
block[idx] = (sum & 0xff) as u8;
carry = sum >> 8;
}
}
fn read_safe_contents(
data: &[u8],
password: &[u8],
out: &mut WalletContents,
) -> Result<(), WalletError> {
let mut reader = into_seq(data)?;
let (tag, seq) = read_tlv(&mut reader)?;
if tag != Tag::Sequence {
return Err(p12("SafeContents: expected SEQUENCE OF SafeBag"));
}
let mut bags = into_seq(seq)?;
while !bags.is_finished() {
let (tag, bag) = read_tlv(&mut bags)?;
if tag != Tag::Sequence {
continue;
}
let mut bag_reader = into_seq(bag)?;
let bag_id = read_oid(&mut bag_reader)?;
let (ctx_tag, value) = read_tlv(&mut bag_reader)?;
if !ctx_tag.is_context_specific() {
continue;
}
match bag_id.to_string().as_str() {
OID_KEY_BAG => {
out.client_private_key = Some(value.to_vec());
}
OID_PKCS8_SHROUDED_KEY_BAG => {
let mut bv = into_seq(value)?;
let (epki_tag, epki_body) = read_tlv(&mut bv)?;
if epki_tag != Tag::Sequence {
continue;
}
let mut epki = into_seq(epki_body)?;
let (alg_tag, alg_body) = read_tlv(&mut epki)?;
if alg_tag != Tag::Sequence {
return Err(p12("shrouded key: expected AlgorithmIdentifier"));
}
let material = derive_content_encryption(alg_body, password)?;
let (ct_tag, ct) = read_tlv(&mut epki)?;
if ct_tag != Tag::OctetString {
return Err(p12("shrouded key: expected encrypted OCTET STRING"));
}
let pkcs8 = decrypt_ciphertext(material, ct)?;
out.client_private_key = Some(pkcs8);
}
OID_CERT_BAG => {
let mut bv = into_seq(value)?;
let (cb_tag, cb_body) = read_tlv(&mut bv)?;
if cb_tag != Tag::Sequence {
continue;
}
let mut cb = into_seq(cb_body)?;
let _cert_id = read_oid(&mut cb)?;
let (ct_tag, cv) = read_tlv(&mut cb)?;
if ct_tag.is_context_specific() {
let mut cvr = into_seq(cv)?;
let (t, der) = read_tlv(&mut cvr)?;
if t == Tag::OctetString {
out.ca_certificates.push(der.to_vec());
}
}
}
_ => {}
}
}
Ok(())
}
fn be_uint(bytes: &[u8]) -> Result<u64, WalletError> {
if bytes.len() > 8 {
return Err(p12("integer too large"));
}
let mut v: u64 = 0;
for &b in bytes {
v = (v << 8) | u64::from(b);
}
Ok(v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pbkdf2_rejects_oversized_key_len_instead_of_allocating() {
let huge = pbkdf2_derive(b"pw", b"saltsalt", 1000, usize::MAX, PrfHash::Sha256);
assert!(huge.is_err(), "oversized key_len must error, not allocate");
let over = pbkdf2_derive(
b"pw",
b"saltsalt",
1000,
MAX_PBKDF2_KEY_LEN + 1,
PrfHash::Sha256,
);
assert!(over.is_err(), "key_len just past the cap must error");
let ok = pbkdf2_derive(b"pw", b"saltsalt", 1000, 32, PrfHash::Sha256);
assert!(ok.is_ok() && ok.unwrap().len() == 32);
}
#[test]
fn pbkdf2_rejects_hostile_iteration_counts_without_working_them() {
for iterations in [0, MAX_PBKDF2_ITERATIONS + 1, u64::MAX] {
let err = pbkdf2_derive(b"pw", b"saltsalt", iterations, 32, PrfHash::Sha256)
.expect_err("hostile PBKDF2 work factor must be rejected");
assert!(matches!(err, WalletError::Pkcs12(_)));
}
}
}