公钥基础设施(PKI)与数字证书

说到 TLS 密钥交换,绕不开一个基础设施——公钥基础设施(PKI)。它的核心任务就两个:证明“你是谁”,以及保护“你的公钥不被篡改”。说白了,没有 PKI,我们没法信任网线那头飘来的任何一串公钥。

我刚开始接触 HTTPS 时,总觉得证书就是个“加密文件”。后来在生产环境排查过几次中间人攻击,才意识到:证书的信任体系比算法本身还脆弱。嗯,这节课咱们就把它拆开揉碎看。

3.1 数字证书长什么样?

数字证书(X.509 v3 格式)就是一张绑定公钥和身份的数字身份证。它由 CA(证书颁发机构)签发,包含以下核心字段:

字段 说明 我在项目中踩过的坑
版本号 目前主流是 v3(0x02) 有些旧库只支持 v1,v3 扩展会解析失败
序列号 CA 分配的唯一编号,用于吊销 记得用足够随机性,否则容易被预测
签名算法 如 sha256WithRSAEncryption 别用 SHA-1,浏览器已经当你裸奔了
颁发者 上一级 CA 的 DN(Distinguished Name) DN 拼写错误会导致链验证失败
有效期 notBefore ~ notAfter 我曾经忘了更新证书,半夜被报警叫醒
主体 证书持有者的 DN,如 www.example.com 通配符证书只匹配一个层级,别想偷懒
公钥信息 算法(RSA/ECDSA) + 公钥值 ECC 密钥比 RSA 短很多,性能优势明显
扩展 基本约束、密钥用途、SAN、CDP、AIA 等 没有 SAN 的证书在 Chrome 里直接报错
CA 签名 CA 用自己的私钥对上述字段签出的值 签名不合法 = 证书无效,没有商量余地
小技巧: 使用 openssl x509 -in cert.pem -text -noout 可以查看证书所有字段。我习惯先检查 Subject Alternative Name(SAN),这地方最容易搞错。

3.2 证书链验证流程

你想想看,浏览器拿到服务器证书后,不会直接信任它。它会沿着“证书链”往上找,直到找到一个“自签名”的根证书。这个过程叫链式信任验证。

为什么需要链?因为我们不可能把每个网站的证书都预装在操作系统里。操作系统只预装了大约 100~200 个根证书,剩下的靠中间 CA 递归信任。我遇到过一种情况:服务端只发了叶子证书,没发中间证书,客户端因为找不到完整链直接握手失败。这就是经典的“缺少中间证书”问题。

链验证四步走:

  1. 从叶子证书开始,逐级向上,构建从叶子到根的所有证书路径。
  2. 每一级用上一级 CA 的公钥验证下一级证书的签名。
  3. 检查每张证书的有效期、吊销状态、密钥用途等约束。
  4. 如果路径终点是受信任的根证书(且所有检查通过),则信任成立。
TLS证书链验证流程 Root CA(信任锚) Intermediate CA(由根签发) Server Certificate(由中间CA签发) 验证要点 1. 签名逐一校验 2. 有效期不过期 3. 扩展符合用途 4. 吊销状态正常 5. 根证书受信 全部通过 → 信任
避坑指南: 我曾经在生产环境配置了一个“自签名中间证书”——明明根是受信任的,但中间证书不是由该根签发的。结果所有客户端都报 unable to get local issuer certificate。排查了三个小时,才发现中间证书的颁发者 DN 写错了一个字符。记住:颁发者 DN 必须严格等于上一级证书的主体 DN,包括大小写和空格。

3.3 证书吊销机制

证书即使没到期,也可能被吊销。比如私钥泄露、域名变更、CA 出事故。TLS 客户端需要检查证书是否还在有效期内——吊销状态。主要两种方式:

  • CRL(证书吊销列表):CA 定期发布一个带签名的列表,里面是所有未过期但已吊销的证书序列号。缺点:列表越积越大,下载延迟高。
  • OCSP(在线证书状态协议):客户端实时向 OCSP 响应器查询单张证书的状态。更快更精确。但 OCSP 服务器挂了怎么办?浏览器有“软失败”策略——查不到就当没吊销。这里存在安全风险,我记得 2019 年就有人利用 OCSP 不可用搞过攻击。

我个人更推荐 OCSP Stapling:服务器自己定期向 CA 获取 OCSP 响应,然后在 TLS 握手时“钉”在 Certificate 消息里发给客户端。这样客户端不用额外发起请求,既快又避免了隐私泄露。你想想看,如果每次访问一个网站都要去 CA 查状态,CA 就能知道所有人的访问习惯——OCSP Stapling 把这一步挪到服务器侧,就解决了。

机制 实时性 隐私 性能
CRL 低(按周期更新) 中(列表公开) 差(列表大)
OCSP 查询 高(实时) 差(暴露用户访问) 中(新增网络请求)
OCSP Stapling 高(缓存近实时) 好(不暴露客户端) 好(无额外请求)
配置建议: Nginx 中启用 OCSP Stapling 只需几行配置。我习惯在 ssl_stapling on; 之后加上 ssl_stapling_verify on; 并指定 ssl_trusted_certificate,确保响应的合法性。

3.4 CA 信任模型与交叉认证

现在的 PKI 不是一棵树,而是多棵树组成“森林”。每个操作系统、浏览器都维护自己的根证书列表。这导致同一个根证书在 macOS 上受信任,在 Android 上可能不受。我之前给一个跨国客户部署 TLS,发现他们的根证书是 Globalsign 的,但在某些老旧 Android 版本里没有预装。最后只能让客户装根证书,麻烦得很。

解决方案之一是交叉认证:由两个 CA 互相签发证书,让一个 CA 下的证书也能被另一个 CA 的客户端信任。嗯,这玩意配置起来极其复杂,我至今只在一个金融项目里真正用过。平时更推荐直接买主流 CA(如 DigiCert、Let's Encrypt)的证书,它们几乎在所有终端里都受信。

3.5 自签发证书 vs 公开 CA 证书

开发环境或内网服务经常用自签发证书。但要注意:自签发证书不会自动被任何客户端信任。你需要在客户端手动安装根证书,或者在代码里忽略证书验证——后者非常危险,我见过有人把 verify=False 写进了生产代码,结果被安全审计直接打成高危漏洞。

给你一个实用建议:内网也用正规 CA 签发的证书,现在 Let's Encrypt 免费且支持通配符,用 acme.sh 可以全自动续期。我自己的家庭服务器都用这种方式,省心省力。真的不要为了省事搞自签发,后面维护成本远高于那点麻烦。

核心要点总结:

  • 数字证书 = 公钥 + 身份 + CA 签名,核心是防篡改。
  • 证书链验证必须完整:叶子 → 中间 → 受信根。
  • 吊销检查推荐 OCSP Stapling,兼顾安全与性能。
  • 别在生产环境忽略证书验证,别漏掉中间证书。

好了,PKI 这一套东西说简单也简单,说复杂,光一个证书策略配置就能折腾一整天。你只要记住:信任不是凭空来的,是靠签名链一层层攒出来的。 下次如果遇到 TLS 握手失败,先看看证书链是不是完整,再查查吊销状态——八成能解决问题。