证书错误只有四种
一句话总结
证书是由某人签名的一份文档,内容是“这个公钥属于这个名称”。验证就是分别检查签名链、有效期、名称(SAN)、密钥匹配这四项,而故障信息会准确指出其中哪一项被破坏了。
为什么需要它
“证书错误”这类报告,原因是四项之一,而画面上的提示往往把它们笼统地混成一句话。可能是过期了,可能是名称不同,可能是缺少中间 CA 导致链断了,也可能是部署时把证书和另一把私钥配成了一对。每一种的修复方法完全不同,所以一上来就想着“重新签发就行了”,就要做两遍。了解其结构后,只需一次 openssl x509 -text,就能读出是哪一栏有问题。
工作原理
格式是 RFC 5280 的 X.509 v3。正文(TBSCertificate)中包含 serialNumber(在签发者内部唯一的编号)、issuer(签名的 CA 的名称)、validity(notBefore、notAfter)、subject、subjectPublicKeyInfo(公钥),以及扩展(extensions),并附上用签发者私钥对整个内容做出的签名。续期时,即使使用同一把密钥,serial 和 validity 也会改变,所以 serial 起到了证书版次编号的作用。这就是确认正在使用哪张证书时,要查看 serial 或 SHA-256 指纹的原因。
扩展之中有两个在运维中起决定性作用。subjectAltName(SAN) 是这张证书所代表的名称列表。RFC 6125 规定,名称检查要依据 SAN 的 dNSName,而不是 subject 的 CN,现代客户端实际上也只看 SAN——把名称写进 CN 是没用的。basicConstraints 的 cA 为 TRUE 的证书,才能为其他证书签名,而 pathLenConstraint 限制在它之下最多可以设置几级 CA。
链的顺序是叶子(服务器证书)→ 中间 CA → 根。客户端只在信任存储中持有根,而服务器必须把叶子和中间 CA 一起发送。如果漏掉中间 CA,客户端就找不到叶子的签发者。OpenSSL 的 verify 通过 -CAfile 接收信任锚,通过 -untrusted 接收“虽然不信任,但用来连接链”的中间证书。
$ openssl verify -CAfile ca.crt api.crt
error 20 at 0 depth lookup: unable to get local issuer certificate ← 중간 CA 가 없다
$ openssl verify -CAfile ca.crt -untrusted inter.crt api.crt
api.crt: OK
创建时有一个陷阱。即使用 -addext "subjectAltName=DNS:..." 把 SAN 放进了 CSR,用 openssl x509 -req 签名时,默认行为也是丢弃 CSR 中的扩展。在 3.0 中必须加上 -copy_extensions copy,SAN 才会被复制到证书中。如果漏掉了这一点,签发看起来毫无问题,浏览器却会报名称不匹配——因为有 CN,人的肉眼看上去一切正常。
密钥匹配,只要比较证书中的公钥和从私钥提取出的公钥即可。两者都用 -pubkey/-pubout 得到相同的 PEM 格式,所以比较哈希就足够了。过期用 -enddate 查看,剩余时间的判定由 -checkend <초>(占位符为秒数)完成——如果在这个秒数之内过期,就以非 0 的退出码结束,所以很适合放进脚本。
$ openssl x509 -in server.crt -noout -checkend 604800 ; echo $? # 7일 안에 만료되는가
Certificate will expire
1
在现场相遇的样子
“昨天还能用,今天不行了”就是过期。证书是按时钟“死亡”的,所以与部署无关,会突然爆发。此时要记住,判定依据的是客户端的时钟,而不是服务器的时钟——确实发生过只有一台时钟不对的机器无法使用的事故。“curl 可以,只有 Java 应用不行”是链的问题。curl 可能在系统存储中缓存了中间 CA,或者会沿着 AIA 去补全链,而 Java 或 Go 客户端只用服务器发来的内容来构建链。给服务器配上 fullchain 就解决了。“更换反向代理之后”要怀疑密钥不匹配。这是从不同路径取来证书和密钥,导致二者不配对,nginx 会在启动时拒绝,但有些服务器要到握手时才会失败。
下一项实验要做什么
创建根 CA,创建带两个 SAN 的 CSR,并签发 3 天有效的服务器证书。亲眼看看漏掉 -copy_extensions 时,为什么评分器会判定为没有 SAN。读取 serial、指纹和到期日,用哈希判定密钥是否匹配,并用 -checkend 判定 7 天窗口内是否需要续期。最后建立中间 CA 并签发叶子证书,构造出只用根会失败、而用 -untrusted 就能通过的链,组装出 fullchain.pem,然后用 -verify_hostname 把名称检查的结果整理成表格留下。