Skip to content
ecdsa.com

Create a PKCS#12 bundle · Python

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

To create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Python, call pkcs12.serialize_key_and_certificates from the cryptography package with the key, the certificate, the chain and an encryption built by PrivateFormat.PKCS12.encryption_builder(). pkcs12.load_pkcs12 reads it back with the friendly name. This recipe bundles a P-256 key with its certificate and issuing CA and checks the result with OpenSSL.

Tested with cryptography 50.0.2 (Python 3.14.8) and OpenSSL 3.5.7 (reads the bundle back (inspect.sh)).

create.py — key + certificate + chain into bundle.p12 with AES-256
import os
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs12
password = os.environ["P12_PASSWORD"].encode()
key = serialization.load_pem_private_key(open("key.pem", "rb").read(), password=None)
cert = x509.load_pem_x509_certificate(open("leaf.pem", "rb").read())
chain = x509.load_pem_x509_certificates(open("intermediate.pem", "rb").read())
# Explicit modern protection: AES-256-CBC with PBKDF2-HMAC-SHA256, HMAC-SHA256 integrity
encryption = (
serialization.PrivateFormat.PKCS12.encryption_builder()
.kdf_rounds(600_000)
.key_cert_algorithm(pkcs12.PBES.PBESv2SHA256AndAES256CBC)
.hmac_hash(hashes.SHA256())
.build(password)
)
p12 = pkcs12.serialize_key_and_certificates(b"api.example.com", key, cert, chain, encryption)
with open("bundle.p12", "wb") as f:
f.write(p12)
read.py — load the bundle: friendly name, certificates, key
import os
from cryptography.hazmat.primitives.serialization import pkcs12
bundle = pkcs12.load_pkcs12(open("bundle.p12", "rb").read(), os.environ["P12_PASSWORD"].encode())
print("friendly name:", bundle.cert.friendly_name.decode())
print("certificate:", bundle.cert.certificate.subject.rfc4514_string())
for extra in bundle.additional_certs:
print("chain:", extra.certificate.subject.rfc4514_string())
print("key matches certificate:", bundle.key.public_key() == bundle.cert.certificate.public_key())

How it works

  1. serialize_key_and_certificates(name, key, cert, cas, encryption) writes the bundle; name becomes the friendly name, cas is the chain without the leaf.
  2. The encryption builder sets the protection explicitly: PBES.PBESv2SHA256AndAES256CBC (AES-256-CBC with PBKDF2-HMAC-SHA256) for key and certificates, hmac_hash(SHA256()) for the integrity MAC, kdf_rounds for the PBKDF2 iterations.
  3. pkcs12.load_pkcs12(data, password) returns an object with key, cert (with its friendly_name) and additional_certs.
  4. The run reads the bundle with OpenSSL too: AES-256-CBC with PBKDF2 at 600 000 iterations, both certificates, and the key that belongs to leaf.pem.

Gotchas

  • kdf_rounds sets the PBKDF2 iterations of the encryption only: OpenSSL reports PBKDF2 … Iteration 600000 for the contents but MAC: sha256, Iteration 2048 for the integrity check.
  • A wrong password raises ValueError: Invalid password or PKCS12 data — the same message as a corrupted file, so do not tell users which one it was.
  • load_key_and_certificates returns a plain tuple (key, certificate, list of the rest) and drops the friendly names; use load_pkcs12 when you need them.
  • For readers that only know the old algorithms, PBES.PBESv1SHA1And3KeyTripleDESCBC with hmac_hash(SHA1()) writes 3DES (pbeWithSHA1And3-KeyTripleDES-CBC in the run's legacy.p12); 3DES with SHA-1 is weak, so use it only when such a reader forces you.

Related recipes