PKCS12、PEM 还是 DER:VPN 客户端真正需要什么
打包方式决定客户端能不能导入身份,而不是决定它是否可信。比较 PKCS12、PEM 与 DER,然后验证。
证书讨论里流传着两种说法。第一种是 .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 只是重写外壳而非内容,转回去会得到原来的字节。
| Container | Can hold | Does not decide |
|---|---|---|
| PKCS12 (.p12, .pfx) | Key, leaf certificate, chain, alias, password | Which root the client trusts |
| PEM (.pem, .crt, .cer) | Any number of base64 blocks | Whether the private key is present |
| DER (.der, .cer) | One binary structure | The 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 sha256openssl pkey -in key.pem -pubout | openssl sha256
摘要相同说明这两件本来就是一对。摘要不同说明你拿错了密钥,再怎么转换格式也改不了这一点。
格式永远改变不了的东西
信任判断发生在容器之上。客户端会沿链走到自己信任库里的根,检查有效期窗口,检查客户端认证所需的扩展密钥用法,服务端一侧则检查它拨号用的名字确实出现在证书里。这些步骤没有一步会去读文件扩展名。
这就是为什么一直改文件名直到错误消失,是如此可靠的一种浪费时间的方式。导入成功了,身份依旧是错的,失败会在握手时重新出现,而那时的提示更没用。这也解释了为什么两年前导出的包能干净导入却仍被拒:到期日期是跟着证书走的,不在文件名里。
一个避免返工的次序
先查客户端文档,看它接受哪些编码、客户端身份要哪种容器,然后再看你拿到的是什么。如果客户端要单一可导入的身份而你手头是散装的 PEM 文件,就用你自己掌控的密码把包构建一次,并保留原始文件。如果它要 PEM,就把密钥和链抽出来,之后按团队既有的凭据处理流程清掉中间文件。
导入之后要核对身份而不是对话框:主体、签发者、到期时间,以及客户端将要出示的名字。列表里的友好名称只是个标签,不是经过验证的身份。如果隧道随后失败,你调试的就是认证或路由而不是打包,这场对话要短得多。