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
| PEM | PKCS#12 (PFX) | |
|---|---|---|
| Specification | RFC 7468 (text encoding of DER) | RFC 7292 (PKCS #12 v1.1), RFC 9579 (PBMAC1) |
| Format | Text, one or more BEGIN … END blocks | Binary DER container of "safe bags" |
| Holds | Whatever you put in the file: key, certificates, CRLs | Private keys, certificates, CRLs, secrets, with friendly names and key IDs |
| Protection | Optional per key (encrypted PKCS#8) | Password-based encryption of bags plus an integrity MAC |
| OpenSSL 3.6 default protection | PBES2 with AES-256-CBC for encrypted keys | PBES2, PBKDF2-HMAC-SHA256, AES-256-CBC, 2,048 iterations; HMAC-SHA256 MAC |
OpenSSL -legacy | Not applicable | RC2-40 for certificates, 3DES for keys, HMAC-SHA1 MAC |
| Typical extensions | .pem, .crt, .key | .p12, .pfx |
| Consumed by | OpenSSL, web servers, proxies, Go, Python, Node.js | Windows 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.
#!/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"
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
-legacyexports use when RC2 is built in) load only with-legacy, which loads the legacy provider. Without it, OpenSSL 3.6.5 printserror:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupportedforRC2-40-CBC, and Node.js 26 rejects the file withUnsupported 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
- 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.
- Match key and certificate before packing: the key and certificate matcher compares their public keys.
- Pick algorithms for the oldest consumer, then upgrade: re-export with modern defaults once old platforms are gone.
- 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.
- Do not leave extracted keys around: after converting a
.pfxto PEM for a server, restrict file permissions and delete temporary copies.