import os from cryptography import x509from cryptography.hazmat.primitives import hashes, serializationfrom 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 integrityencryption = ( 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) 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
serialize_key_and_certificates(name, key, cert, cas, encryption)writes the bundle;namebecomes the friendly name,casis the chain without the leaf.- 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_roundsfor the PBKDF2 iterations. pkcs12.load_pkcs12(data, password)returns an object withkey,cert(with itsfriendly_name) andadditional_certs.- 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_roundssets the PBKDF2 iterations of the encryption only: OpenSSL reportsPBKDF2 … Iteration 600000for the contents butMAC: sha256, Iteration 2048for 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_certificatesreturns a plain tuple (key, certificate, list of the rest) and drops the friendly names; useload_pkcs12when you need them.- For readers that only know the old algorithms,
PBES.PBESv1SHA1And3KeyTripleDESCBCwithhmac_hash(SHA1())writes 3DES (pbeWithSHA1And3-KeyTripleDES-CBCin the run'slegacy.p12); 3DES with SHA-1 is weak, so use it only when such a reader forces you.
Related recipes
- How to create a PKCS#12 (.p12/.pfx) file from a key and certificate chain with OpenSSL
- How to create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Go
- How to create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Java
- How to convert a certificate and a private key between PEM and DER in Python
- How to verify a certificate chain against your own trust store in Python