格式转换与 PKCS#12

OpenSSL PKCS#12(PFX)命令与 3.0 的 -legacy 变化

作者: · 最后核实 实测 macOS · OpenSSL 3.6.1

PKCS#12(.pfx、.p12)把私钥、证书和证书链装进一个带密码的文件,openssl pkcs12 负责生成和读取。实际使用中最大的坑来自 OpenSSL 3.0:新导出的文件改用 AES-256 和 SHA-256 MAC,老版本 Windows、macOS、Java 可能不认;而老工具导出的文件用 RC2,3.x 不加 -legacy 就读不了。输出示例来自 OpenSSL 3.6.1。

单纯的格式转换(PFX 转 PEM、PFX 转 JKS)见证书格式转换,也可以用浏览器里的 SSL 格式转换工具。

生成包含私钥、证书和证书链的 PFX

-inkey 是私钥,-in 是服务器证书,-certfile 是中间证书,-name 是导入时显示的友好名称。

openssl pkcs12 -export -inkey server.key -in server.crt -certfile intermediate.crt \
  -name "example.com" -out example.pfx
Enter Export Password:
Verifying - Enter Export Password:

友好名称在 Java 密钥库里就是别名,在 Windows 证书管理器里显示为“友好名称”。私钥和证书不配对时会报 No cert in -in file 'server.crt' matches private key。也可以直接把 fullchain.pem 交给 -in,OpenSSL 会挑出和私钥匹配的那张当服务器证书。

让 OpenSSL 自动补全证书链

-chain 会从服务器证书一路构建到受信任的根证书,并把路径上的证书都放进去。

openssl pkcs12 -export -inkey server.key -in server.crt -chain \
  -untrusted intermediate.crt -CAfile root.crt -name example.com -out example.pfx
openssl pkcs12 -in example.pfx -nokeys -passin pass:changeit | grep subject=
subject=CN=example.com
subject=CN=Example Test Intermediate CA
subject=CN=Example Test Root CA

找不到可用的根证书时会报 Error getting chain: unable to get local issuer certificate。公网证书不写 -CAfile 时会用系统信任库。注意 -chain 会把根证书也放进去,服务器其实不需要发送根证书。

查看 PFX 用的是什么算法

-info -noout 只打印 MAC 和加密算法,不输出任何密钥。

openssl pkcs12 -in example.pfx -info -noout
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 16
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Certificate bag
Certificate bag
PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256

这是 OpenSSL 3.x 的默认格式。旧格式会显示 MAC: sha1 以及 pbeWithSHA1And40BitRC2-CBC 或 pbeWithSHA1And3-KeyTripleDES-CBC。导入失败时先跑这条,就知道该改哪一边。

弄清 OpenSSL 3.0 的默认算法变化

OpenSSL 3.0 把 PKCS#12 导出从老的 PKCS#12 PBE 算法换成了 PBES2 + AES-256。

OpenSSL 1.1.1 及更早 OpenSSL 3.0 及以后(默认) 3.x 加 -legacy
证书加密 RC2-40-CBC AES-256-CBC,PBKDF2,HMAC-SHA256 RC2-40-CBC(未编译 RC2 时为 3DES)
私钥加密 3DES AES-256-CBC,PBKDF2,HMAC-SHA256 3DES
MAC SHA-1 SHA-256 SHA-1
迭代次数 2048 2048 2048

3.0 把 RC2 等老算法移进了默认不加载的 legacy provider。所以同一条用了多年的命令,升级系统(比如 Ubuntu 22.04 起自带 OpenSSL 3)后导出的文件突然在老服务器上导入失败;反过来,3.x 不加 -legacy 也打不开老工具导出的文件。

给老 Windows、macOS、Java 生成兼容的 PFX

加 -legacy 就会使用 3.0 之前的算法。

openssl pkcs12 -export -legacy -inkey server.key -in server.crt -certfile intermediate.crt \
  -name example.com -out example-legacy.pfx
openssl pkcs12 -in example-legacy.pfx -info -noout -legacy
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 16
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Certificate bag
Certificate bag
PKCS7 Data
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048

Windows Server 2016 及更早版本的 IIS、旧版 macOS 钥匙串、较老的 Java 8、部分防火墙和负载均衡设备,在密码明明正确却提示“密码错误”时,就用这个办法。OpenSSL 不是装在系统默认路径时,-legacy 可能还需要 -provider-path 或环境变量 OPENSSL_MODULES 才能找到 legacy.so。

显式指定加密算法

-certpbe、-keypbe、-macalg 可以直接选算法,不需要加载 legacy provider。

openssl pkcs12 -export -inkey server.key -in server.crt -certfile intermediate.crt \
  -certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg sha1 \
  -name example.com -out example-compat.pfx
openssl pkcs12 -in example-compat.pfx -info -noout
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 16
PKCS7 Encrypted data: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048
Certificate bag
Certificate bag
PKCS7 Data
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048

3DES 仍在默认 provider 里,所以这个文件 OpenSSL 3.x 不加 -legacy 就能读,老系统基本也都能导入。不知道对方用什么系统时,这是最稳妥的折中。加 -iter 100000 可以提高迭代次数,增加暴力猜密码的成本。

在 OpenSSL 3.x 上读取旧格式 PFX

报 unsupported 并提到 RC2-40-CBC,说明文件用了旧算法,加 -legacy。

openssl pkcs12 -in old.pfx -info -noout
openssl pkcs12 -in old.pfx -info -noout -legacy
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 16
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Error outputting keys and certificates
...:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:...:Algorithm (RC2-40-CBC : 0), Properties ()

MAC: sha1, Iteration 2048
...
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048

Node.js 17 以上、Python 等链接 OpenSSL 3 的程序读老 PFX 时也会报同样的 0308010C ... unsupported。一劳永逸的办法是读入时加 -legacy、导出时用默认 AES 重新生成一次,做法见下文“修改 PFX 密码”。

处理 “Mac verify error: invalid password?”

这表示 MAC 校验没通过,绝大多数情况是密码错了,偶尔是 MAC 算法不被支持。

openssl pkcs12 -in example.pfx -noout -passin pass:wrong
Mac verify error: invalid password?

程序库里同样的情况会报 mac verify failure。逐项排查:密码是否为空(-passin pass:)、密码文件末尾是否多了换行、从云厂商密码文件复制时是否带了空格、密码里有没有中文等非 ASCII 字符。确认密码无误而文件来自很新的系统时,可能是旧程序不认识新的 MAC 算法(例如 OpenSSL 3.4 起可用 -pbmac1_pbkdf2 生成的 PBMAC1)。

提取私钥、证书和 CA 证书

-nocerts、-clcerts -nokeys、-cacerts -nokeys 分别取出三部分。

openssl pkcs12 -in example.pfx -nocerts -noenc -out example.com.key
openssl pkcs12 -in example.pfx -clcerts -nokeys -out example.com.crt
openssl pkcs12 -in example.pfx -cacerts -nokeys -out chain.crt
head -3 example.com.key
Bag Attributes
    localKeyID: 12 21 0D 64 22 E9 E3 EB CC 6B 65 BE B6 A8 CA 17 5F 63 39 3A 
    friendlyName: example.com

-clcerts 只输出和私钥匹配的那张证书,避免在乱序文件里拿错。不加 -noenc 会让你给私钥再设一个 PEM 密码。旧格式文件每条命令都加 -legacy。从 Windows 导出的 PFX 转给 Nginx 或宝塔面板用,就是这三步。

生成只含证书的 PKCS#12 信任库

-nokeys 可以把 CA 证书打包成 Java 等客户端用的信任库。

openssl pkcs12 -export -nokeys -in root.crt -jdktrust anyExtendedKeyUsage \
  -out truststore.p12
openssl pkcs12 -in truststore.p12 -nokeys | head -2
Bag Attributes
    Trusted key usage (Oracle): Any Extended Key Usage (2.5.29.37.0)

Java 只把带有 Oracle“受信任密钥用途”属性的证书当作信任条目,-jdktrust(OpenSSL 3.2 起)负责加这个属性。更老的 OpenSSL 请改用 keytool -importcert 生成信任库。

生成无密码 PFX

-passout pass: 只是用空密码加密;-keypbe NONE -certpbe NONE -nomac 才是完全不加密。

openssl pkcs12 -export -inkey server.key -in server.crt -passout pass: -out nopass.pfx
openssl pkcs12 -export -inkey server.key -in server.crt \
  -keypbe NONE -certpbe NONE -nomac -passout pass: -out plain.pfx
openssl pkcs12 -in plain.pfx -info -noout -passin pass:
Warning: MAC is absent!
PKCS7 Data
Certificate bag
PKCS7 Data
Key bag

部分软件和容器平台要求无密码 PFX。完全不加密的版本等于换了个外壳的明文私钥,要像 .key 文件一样保管。启用 FIPS provider 时也会用 -nomac,因为 FIPS 模式算不了 PKCS#12 的 MAC。

修改 PFX 密码

先导出成临时 PEM,再用新密码重新导出,最后删掉临时文件。

umask 077
openssl pkcs12 -in example.pfx -noenc -passin file:old.txt -out tmp-all.pem
openssl pkcs12 -export -in tmp-all.pem -name example.com -passout file:new.txt -out example-new.pfx
rm -f tmp-all.pem
(成功时没有输出)

不能用管道把第一条直接接到第二条:pkcs12 -export 要把输入读两遍,而标准输入只能读一次,会报 Could not find certificates from -in file from <stdin>。第一条加 -legacy,就能顺便把老的 RC2 文件升级成 AES。

在脚本里免交互运行

用 file:、env: 或 fd: 传密码,不用手动输入。

P12PASS='s3cret' openssl pkcs12 -in example.pfx -noout -passin env:P12PASS && echo ok
openssl pkcs12 -export -inkey server.key -in server.crt -passout file:pfx-pass.txt -out example.pfx
ok

pass:密码 也能用,但会出现在 ps 输出和 shell 历史里。-passin 是读取 PFX(或 -export 时读取输入私钥)的密码,-passout 是新写出文件的密码。其他 OpenSSL 报错的含义见常见问题排查。

常见问题

用 -legacy 导出的 PFX 安全吗?

RC2-40、3DES、SHA-1 MAC 抵抗暴力猜密码的能力较弱,文件一旦泄露,密码不够强就可能被破解。所以旧格式 PFX 要配长随机密码、不要放在共享盘或群文件里,导入完就删除。目标系统支持时优先用 AES 默认格式。

.pfx 和 .p12 有区别吗?

没有,都是 PKCS#12。Windows 习惯叫 .pfx,Java 和 macOS 习惯叫 .p12,直接改扩展名就行。

云厂商下载的 IIS 证书包里的 txt 文件是什么?

是 .pfx 的导入密码。阿里云、腾讯云的 IIS 和 Tomcat 证书包都会附一个密码文件,导入 IIS 或配置 Tomcat 的 keystorePass 时填它。用 OpenSSL 读取时可以直接 -passin file:密码文件.txt。

资料来源

  1. OpenSSL 3.0 手册:openssl-pkcs12
  2. OpenSSL 3.5 手册:openssl-pkcs12
  3. OpenSSL 3.0 手册:OSSL_PROVIDER-legacy
  4. OpenSSL 3.0 迁移指南
  5. RFC 7292:PKCS #12 v1.1