每当密钥和证书要在 Unix 服务器与自带密钥库的平台之间转移时,就会碰到 PKCS#12 与 PEM 的问题。PEM 文件是彼此独立、可以直接阅读的文本块,基于 OpenSSL 的软件可以直接加载;PKCS#12 文件(.p12 或 .pfx)是单个加密容器,操作系统和 Java 一步就能导入。密钥和证书相同,封装方式不同,而导入能否成功取决于封装所用的算法。
概览
| PEM | PKCS#12(PFX) | |
|---|---|---|
| 规范 | RFC 7468(DER 的文本编码) | RFC 7292(PKCS #12 v1.1)、RFC 9579(PBMAC1) |
| 格式 | 文本,一个或多个 BEGIN … END 块 | 由“安全包”(safe bag)组成的二进制 DER 容器 |
| 内容 | 你放进文件的任何东西:密钥、证书、CRL | 私钥、证书、CRL、机密,并带有友好名称和密钥 ID |
| 保护 | 可选,按密钥单独设置(加密的 PKCS#8) | 基于密码对各个包加密,外加完整性 MAC |
| OpenSSL 3.6 的默认保护 | 加密密钥使用 PBES2 加 AES-256-CBC | PBES2、PBKDF2-HMAC-SHA256、AES-256-CBC、2,048 次迭代;HMAC-SHA256 MAC |
OpenSSL -legacy | 不适用 | 证书用 RC2-40,密钥用 3DES,MAC 为 HMAC-SHA1 |
| 常见扩展名 | .pem、.crt、.key | .p12、.pfx |
| 使用方 | OpenSSL、Web 服务器、代理、Go、Python、Node.js | Windows 证书存储、macOS 钥匙串、Java KeyStore、浏览器、signtool |
.pfx 里有什么
PKCS#12 文件是一个 PFX 结构:由内容块组成的认证安全容器(authenticated safe),每个内容块中存放若干个包(bag)。私钥位于 pkcs8ShroudedKeyBag 条目中(加密的 PKCS#8),证书位于 certBag 条目中(通常也放在加密块里),包上还带有 friendlyName 和 localKeyId 等属性,用来把密钥和它的证书对应起来。由密码派生的 MAC 保护整个文件的完整性;RFC 9579(2024年)新增了 PBMAC1,它用 PBKDF2 派生 MAC 密钥,取代 PKCS#12 的旧版 KDF。
PEM 没有容器:服务器配置的是一个密钥文件和一个证书(链)文件,或者一个包含多个块的文件。保护措施取决于密钥文件本身,服务器上的密钥文件通常不加保护,因为进程必须在无人输入密码的情况下读取密钥。
转换
下面的脚本用 OpenSSL 3.6.5 运行,把临时生成的密钥和证书打包成 PKCS#12 文件,再解包还原。
#!/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-----
要包含中间证书,用 -certfile chain.pem 传入;只提取证书用 -nokeys,只提取密钥用 -nocerts。-nodes 选项(在较新版本中名为 -noenc)会把密钥以未加密形式写出,因此要妥善处理输出文件。
兼容性:现代算法与旧算法
OpenSSL 3.0 把导出时的默认算法从 RC2-40 和 3DES 加 HMAC-SHA1 MAC 改成了 AES-256 加 HMAC-SHA256 MAC。许多使用方后来才跟上:
- macOS:从 macOS 15 起,Apple 的 Security 框架才能导入现代 PKCS#12 文件;macOS 14 及更早版本即使密码正确,也会报错“MAC verification failed during PKCS12 import (wrong password?)”。
- Java:JDK 9 把 PKCS#12 设为默认的密钥库类型;JDK 16(以及 8 和 11 在2021年的更新版本)把 Java 自身的默认算法改为 PBES2 加 AES-256 和 HMAC-SHA256 MAC,当前版本都能读取 OpenSSL 3 生成的文件。
- Windows:当前版本可以导入 AES-256 PFX 文件;非常旧的 Windows Server 版本可能需要旧格式。
- OpenSSL 3 读取旧文件:证书用 RC2-40 加密的文件(这是 OpenSSL 1.1 的默认算法,也是编译时包含 RC2 时
-legacy导出所用的算法)只有加上-legacy才能加载,该选项会加载 legacy 提供程序。不加时,OpenSSL 3.6.5 会针对RC2-40-CBC输出error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported,Node.js 26 则以Unsupported PKCS12 PFX data拒绝该文件。只使用 3DES 和 HMAC-SHA1 MAC 的文件无需 legacy 提供程序即可加载。
如果现代格式的文件在已知较旧的目标上导入失败,就用 openssl pkcs12 -export -legacy … 导出,或者显式指定算法(-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg SHA1),并把这个文件在传输过程中视为保护较弱。
这里的错误信息具有误导性:“MAC verification failed”或“wrong password”通常意味着读取方不支持所用的 MAC 或加密算法,而不是密码错了。在猜密码之前,先用 openssl pkcs12 -info -noout(旧文件加上 -legacy)检查文件,看看用了哪些算法。
作为密钥库的 PKCS#12
Java 把 PKCS#12 文件当作密钥库:keytool -list -keystore bundle.p12 -storetype PKCS12 会列出其中的条目,friendlyName 则成为别名。一个密钥库可以容纳多个密钥条目和受信任的证书,PEM 文件若不借助命名约定就无法表达这些。信任库是个特例:只含证书的 PKCS#12 信任库需要 Java 专用的属性来把证书标记为受信任,因此用 keytool -importcert 构建信任库才可靠。其他大多数平台会忽略这些 Java 属性,通过匹配 localKeyId 来导入密钥和证书。
如何选择
- Linux 服务器上的 TLS、反向代理、Kubernetes Ingress:PEM 格式的密钥和完整证书链。
- 向 Windows IIS、Azure App Service 和 Key Vault 导入证书:PKCS#12。
- Java 和 Spring Boot 的 TLS:PKCS#12 密钥库(或者 PEM,较新的 Spring Boot 版本可以直接接受)。
- 用于浏览器和移动设备的客户端证书:使用强密码保护的 PKCS#12,文件与密码分开传递。
- 用 signtool 或 jarsigner 做代码签名:如果密钥确实存放在文件中,就用 PKCS#12;自2023年起,公开受信任的代码签名密钥必须存放在硬件中。
- 查看与调试:PEM,或者使用 PFX 查看器,它在本地列出包和算法,不会暴露密钥。
迁移说明
- 包含完整的证书链:为服务器或客户端构建 PKCS#12 时,按叶子证书在前的顺序放入;缺少中间证书是导入后最常见的故障。
- 打包前核对密钥与证书是否匹配:密钥与证书匹配检测会比较两者的公钥。
- 按最旧的使用方选择算法,之后再升级:旧平台淘汰后,用现代默认设置重新导出。
- 为需要传递的 PKCS#12 文件使用强随机密码,因为文件可能遭到离线破解;按密码存储的标准衡量,OpenSSL 默认的 2,048 次 PBKDF2 迭代偏低。
- 不要随处留下提取出的密钥:为服务器把
.pfx转换为 PEM 后,要限制文件权限,并删除临时副本。