证书讨论里流传着两种说法。第一种是 .p12 是强选项、.pem 是松选项;第二种是文本文件既然能读就一定安全可发。两种说法都经不起一分钟的检查。

PKCS12、PEM 和 DER 描述的是证书对象如何打包和编码。它们并不决定对端是谁、签发者是否可信,也不决定文件是否需要密码。

客户端接受什么是兼容性问题。文件里装的是什么则是身份问题。把这两件事分开,大多数导入错误就不再神秘。

三种容器,一个对象模型

先从你真正拥有的东西开始。证书是关于公钥的一份签名声明。私钥是独立的材料,用来证明你掌握那把密钥。链是把你的证书连接到客户端所信任的根的签发者集合。无论怎么打包,这三样都存在。

PKCS12 文件通常叫 .p12 或 .pfx,是 RFC 7292 定义的容器,可以把私钥、一张或多张证书以及链装进一个受密码保护的 blob,并用别名给这个身份命名。PEM 文件是 RFC 7468 定义的文本框架:BEGIN 与 END 行之间是 base64 正文,一个文件可以依次容纳多个块。DER 是同一批 ASN.1 结构的二进制编码,而 PEM 正文其实就是外面裹了 base64 的 DER。

最后这句最值得记住。把 PEM 转成 DER 只是重写外壳而非内容,转回去会得到原来的字节。

ContainerCan holdDoes not decide
PKCS12 (.p12, .pfx)Key, leaf certificate, chain, alias, passwordWhich root the client trusts
PEM (.pem, .crt, .cer)Any number of base64 blocksWhether the private key is present
DER (.der, .cer)One binary structureThe object type, whatever the extension says

编码不是加密

Base64 只是一种传输字母表。它什么也藏不住,任何人都能在 shell 里解出一个 PEM 块。DER 文件是二进制的,同样公开。这两个性质都不是保护,也都不意味着存在密码。

真正的保护在密钥材料里。PKCS12 包可以用由密码派生出的密钥加密其内容。PEM 私钥可以用旧式头块加密,或放进 PKCS#8 EncryptedPrivateKeyInfo 里。证书本身不含任何秘密,所以一次只导出证书的操作,无论你套上哪种格式,都会让客户端无法完成认证。

扩展名是提示,不是契约。.cer 和 .crt 结尾的文件在两种编码里都出现,设备把 DER 证书导出成 .pem 名字的情况也多得足以让按后缀猜的做法浪费一个下午。不如直接读头几个字节:PEM 文件以 BEGIN 行开头,而 DER 以 0x30 开头,因为外层结构是 SEQUENCE。

导入究竟为什么失败

客户端的错误信息把好几种不同的问题压成一句证书无效,所以要按原因拆开。最常见的是编码不匹配:只解析 PEM 的客户端在 DER 文件上返回解析错误,解决办法是转换而不是换新证书。第二种是缺私钥,证书本身没问题,但身份无法证明。第三种是链,缺少中间证书的包在导入对话框里看着正常,一到握手就失败。

第四种由加密参数造成,看起来和密码错误一模一样。老设备导出的 PKCS12 用 RC2-40 和 3DES,OpenSSL 3 除非加载 legacy provider 否则一律拒绝。从设备上用 AES-256-CBC 和 PBKDF2 重新导出才是长久之计。为迁就一个文件而让客户端机器永久开着旧密码算法,是笔坏买卖。

第五种是歧义。一个容器里装两把密钥和两张证书,或者一个 PEM 文件里有三个 CERTIFICATE 块,都会逼客户端自己挑,而它挑的往往就是第一项。隧道起来时身份不对,通常就是这个原因。

转换之前先检查

读结构,但别把秘密打出来。openssl pkcs12 -in bundle.p12 -info -noout 会报告加密算法和别名,同时压住密钥与证书的输出,openssl-pkcs12 手册 记录了这些开关。去掉 -noout 再加上 -nodes 是为了生成你要长期保存的文件,而不是快速看一眼,因为它会把私钥明文写出来。

对证书来说,openssl x509 -in cert.pem -noout -subject -issuer -dates 能回答导入对话框不会回答的问题,加上 -ext subjectAltName 则能看出这张证书实际对哪些名字有效。如果文件是 DER,就加上 -inform DER;这一个开关能解决大多数无法加载的错误。

把密钥和证书配对是机械操作,值得在任何导入之前先做一遍:

openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in key.pem -pubout | openssl sha256

摘要相同说明这两件本来就是一对。摘要不同说明你拿错了密钥,再怎么转换格式也改不了这一点。

格式永远改变不了的东西

信任判断发生在容器之上。客户端会沿链走到自己信任库里的根,检查有效期窗口,检查客户端认证所需的扩展密钥用法,服务端一侧则检查它拨号用的名字确实出现在证书里。这些步骤没有一步会去读文件扩展名。

这就是为什么一直改文件名直到错误消失,是如此可靠的一种浪费时间的方式。导入成功了,身份依旧是错的,失败会在握手时重新出现,而那时的提示更没用。这也解释了为什么两年前导出的包能干净导入却仍被拒:到期日期是跟着证书走的,不在文件名里。

一个避免返工的次序

先查客户端文档,看它接受哪些编码、客户端身份要哪种容器,然后再看你拿到的是什么。如果客户端要单一可导入的身份而你手头是散装的 PEM 文件,就用你自己掌控的密码把包构建一次,并保留原始文件。如果它要 PEM,就把密钥和链抽出来,之后按团队既有的凭据处理流程清掉中间文件。

导入之后要核对身份而不是对话框:主体、签发者、到期时间,以及客户端将要出示的名字。列表里的友好名称只是个标签,不是经过验证的身份。如果隧道随后失败,你调试的就是认证或路由而不是打包,这场对话要短得多。

相关文章