Skip to content
ecdsa.com

PKCS#12 (.pfx/.p12) vs PEM: bundling keys and certificates

PEM keeps keys and certificates as separate text blocks. PKCS#12 packs them into one password-protected binary file that Windows, macOS, Java and browsers import directly.

PKCS#12 vs PEM comes up whenever a key and certificate move between Unix servers and platforms with their own key stores. PEM files are separate, readable blocks that OpenSSL-based software loads directly; a PKCS#12 file (.p12 or .pfx) is a single encrypted container that operating systems and Java import in one step. Same keys and certificates, different packaging, and the packaging's algorithms decide whether an import works.

At a glance

PEMPKCS#12 (PFX)
SpecificationRFC 7468 (text encoding of DER)RFC 7292 (PKCS #12 v1.1), RFC 9579 (PBMAC1)
FormatText, one or more BEGIN … END blocksBinary DER container of "safe bags"
HoldsWhatever you put in the file: key, certificates, CRLsPrivate keys, certificates, CRLs, secrets, with friendly names and key IDs
ProtectionOptional per key (encrypted PKCS#8)Password-based encryption of bags plus an integrity MAC
OpenSSL 3.6 default protectionPBES2 with AES-256-CBC for encrypted keysPBES2, PBKDF2-HMAC-SHA256, AES-256-CBC, 2,048 iterations; HMAC-SHA256 MAC
OpenSSL -legacyNot applicableRC2-40 for certificates, 3DES for keys, HMAC-SHA1 MAC
Typical extensions.pem, .crt, .key.p12, .pfx
Consumed byOpenSSL, web servers, proxies, Go, Python, Node.jsWindows certificate store, macOS Keychain, Java KeyStore, browsers, signtool

What is inside a .pfx

A PKCS#12 file is a PFX structure: an authenticated safe made of content blocks, each holding bags. Private keys sit in pkcs8ShroudedKeyBag entries (encrypted PKCS#8), certificates in certBag entries (often in an encrypted block too), and bags carry attributes such as friendlyName and localKeyId that tie a key to its certificate. A MAC derived from the password protects the whole file's integrity; RFC 9579 (2024) adds PBMAC1, which derives the MAC key with PBKDF2 instead of PKCS#12's legacy KDF.

PEM has no container: a server is configured with a key file and a certificate (chain) file, or one file with several blocks. Protection is whatever the key file has, usually none on servers because the process must read the key without a person typing a password.

Converting

The script below, run with OpenSSL 3.6.5, packs a throwaway key and certificate into a PKCS#12 file and unpacks it again.

OpenSSL 3.6: PEM to PKCS#12 and back
#!/bin/sh
dir=$(mktemp -d)
cd "$dir" || exit 1
openssl req -new -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout key.pem \
  -subj "/CN=example.com" -days 30 -out cert.pem 2>/dev/null
openssl pkcs12 -export -inkey key.pem -in cert.pem -name demo -passout pass:demo -out bundle.p12
openssl pkcs12 -in bundle.p12 -passin pass:demo -info -noout 2>&1 | grep -E "MAC:|Shrouded|PKCS7 Encrypted"
openssl pkcs12 -in bundle.p12 -passin pass:demo -nodes -out extracted.pem 2>/dev/null
grep -E "^-----BEGIN" extracted.pem
cd / && rm -rf "$dir"
Output
MAC: sha256, Iteration 2048
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
-----BEGIN CERTIFICATE-----
-----BEGIN PRIVATE KEY-----

To include intermediates, pass them with -certfile chain.pem; to extract only certificates use -nokeys, only the key -nocerts. The -nodes flag (named -noenc in newer releases) writes the key unencrypted, so handle the output accordingly.

Compatibility: modern versus legacy algorithms

OpenSSL 3.0 changed the export defaults from RC2-40 and 3DES with an HMAC-SHA1 MAC to AES-256 with an HMAC-SHA256 MAC. Many consumers caught up later:

  • macOS: Apple's Security framework imports modern PKCS#12 files from macOS 15; macOS 14 and earlier fail with "MAC verification failed during PKCS12 import (wrong password?)", even with the right password.
  • Java: JDK 9 made PKCS#12 the default keystore type; JDK 16 (and 2021 updates of 8 and 11) switched Java's own default to PBES2 with AES-256 and an HMAC-SHA256 MAC, and current versions read OpenSSL 3 files.
  • Windows: current releases import AES-256 PFX files; very old Windows Server versions may need the legacy form.
  • OpenSSL 3 reading old files: files whose certificates are encrypted with RC2-40 (the OpenSSL 1.1 default, and what -legacy exports use when RC2 is built in) load only with -legacy, which loads the legacy provider. Without it, OpenSSL 3.6.5 prints error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported for RC2-40-CBC, and Node.js 26 rejects the file with Unsupported PKCS12 PFX data. Files that use only 3DES and an HMAC-SHA1 MAC load without the legacy provider.

When a modern file fails on a known-old target, export with openssl pkcs12 -export -legacy …, or choose explicit algorithms (-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg SHA1), and treat that file as weakly protected in transit.

Error messages are misleading here: "MAC verification failed" or "wrong password" usually means the reader does not support the MAC or encryption algorithm, not that the password is wrong. Inspect the file with openssl pkcs12 -info -noout (add -legacy for old files) to see which algorithms were used before guessing passwords.

PKCS#12 as a keystore

Java treats a PKCS#12 file as a keystore: keytool -list -keystore bundle.p12 -storetype PKCS12 shows its entries, and the friendlyName becomes the alias. A keystore can hold several key entries and trusted certificates, which a PEM file cannot express without naming conventions. Truststores are a special case: a PKCS#12 truststore containing only certificates needs Java-specific attributes to mark them as trusted, so keytool -importcert is the reliable way to build one. Most other platforms ignore those Java attributes and import keys and certificates by matching localKeyId.

When to choose which

  • TLS on Linux servers, reverse proxies, Kubernetes ingress: PEM key and full-chain certificate.
  • Windows IIS, Azure App Service and Key Vault certificate imports: PKCS#12.
  • Java and Spring Boot TLS: a PKCS#12 keystore (or PEM, which recent Spring Boot versions accept directly).
  • Client certificates for browsers and mobile devices: PKCS#12 with a strong password, delivered separately from the password.
  • Code signing with signtool or jarsigner: PKCS#12 if the key is in a file at all; publicly trusted code-signing keys must live in hardware since 2023.
  • Inspection and debugging: PEM, or the PFX inspector, which lists bags and algorithms locally without revealing the key.

Migration notes

  1. Include the full chain when building a PKCS#12 for servers or clients, in leaf-first order; missing intermediates are the most common post-import failure.
  2. Match key and certificate before packing: the key and certificate matcher compares their public keys.
  3. Pick algorithms for the oldest consumer, then upgrade: re-export with modern defaults once old platforms are gone.
  4. Use strong, random passwords for PKCS#12 files that travel, since the file can be attacked offline; OpenSSL's default 2,048 PBKDF2 iterations are low by password-storage standards.
  5. Do not leave extracted keys around: after converting a .pfx to PEM for a server, restrict file permissions and delete temporary copies.

References

All comparisons