Skip to content
ecdsa.com

Create a PKCS#12 bundle · Rust

How to create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Rust

To create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Rust, use the pure-Rust p12-keystore crate: build a KeyStore with a PrivateKeyChain entry and write it with store.writer(password).write(); KeyStore::from_pkcs12 reads it back. This recipe bundles a P-256 key with its certificate and issuing CA and checks the file with OpenSSL.

Tested with p12-keystore 0.4.1 (Rust 1.99.1, pem-rfc7468 1.0.0) and OpenSSL 3.5.7 (reads the bundle back (inspect.sh)).

Cargo.toml — p12-keystore, and pem-rfc7468 for the PEM inputs
[dependencies]
p12-keystore = "=0.4.1"
pem-rfc7468 = { version = "=1.0.0", features = ["alloc"] }
src/main.rs — write the bundle, read it back, refuse a wrong password
use p12_keystore::{Certificate, KeyStore, KeyStoreEntry, Pkcs12ImportPolicy, PrivateKey, PrivateKeyChain};
/// The DER inside a PEM file with the expected label.
fn pem_der(path: &str, label: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let text = std::fs::read(path)?;
let (found, der) = pem_rfc7468::decode_vec(&text)?; // found borrows from text
if found != label {
return Err(format!("{path}: {found}, expected {label}").into());
}
Ok(der)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let password = std::env::var("P12_PASSWORD")?;
let key = PrivateKey::from_der(&pem_der("key.pem", "PRIVATE KEY")?)?; // PKCS#8 only
let leaf = Certificate::from_der(&pem_der("leaf.pem", "CERTIFICATE")?)?;
let intermediate = Certificate::from_der(&pem_der("intermediate.pem", "CERTIFICATE")?)?;
// One key chain (leaf first); the alias becomes the friendly name
let chain = PrivateKeyChain::new(b"api.example.com".to_vec(), key, [leaf, intermediate]);
let mut store = KeyStore::new();
store.add_entry("api.example.com", KeyStoreEntry::PrivateKeyChain(chain));
// Default protection: PBES2 with AES-256-CBC, HMAC-SHA-256, 10 000 iterations
std::fs::write("bundle.p12", store.writer(&password).write()?)?;
// Read it back
let data = std::fs::read("bundle.p12")?;
let store = KeyStore::from_pkcs12(&data, &password, Pkcs12ImportPolicy::Strict)?;
let (alias, chain) = store.private_key_chain().ok_or("no private key in the bundle")?;
println!("alias {alias}: key {} bytes of PKCS#8, chain of {}", chain.key().as_der().len(), chain.certs().len());
for cert in chain.certs() {
println!(" {}", cert.subject());
}
if let Err(error) = KeyStore::from_pkcs12(&data, "wrong-password", Pkcs12ImportPolicy::Strict) {
println!("wrong password: {error}");
}
Ok(())
}

How it works

  1. PrivateKey::from_der takes the PKCS#8 DER and Certificate::from_der each certificate; pem_der strips the PEM armour and checks the label.
  2. PrivateKeyChain::new(local_key_id, key, [leaf, intermediate]) keeps the chain leaf first; add_entry stores it under an alias, which becomes the key's friendly name.
  3. store.writer(password).write() uses PBES2 with AES-256-CBC, HMAC-SHA-256 and 10 000 iterations by default — modelled on the JDK's defaults, as OpenSSL's -info shows in the run.
  4. KeyStore::from_pkcs12(&data, password, Pkcs12ImportPolicy::Strict) reads it; private_key_chain() returns the first key with its certificates.

Gotchas

  • Keys must be PKCS#8 (BEGIN PRIVATE KEY); convert SEC1 (BEGIN EC PRIVATE KEY) or PKCS#1 keys with openssl pkcs8 -topk8 -nocrypt first.
  • A wrong password fails with MAC tag mismatch — the MAC is checked before anything is decrypted.
  • Certificate::subject() prints the attributes in reverse order (C=US, O=Example Corp, CN=api.example.com); compare certificates by their DER, not by these strings.

Related recipes