公钥基础设施(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 递归信任。我遇到过一种情况:服务端只发了叶子证书,没发中间证书,客户端因为找不到完整链直接握手失败。这就是经典的“缺少中间证书”问题。
链验证四步走:
- 从叶子证书开始,逐级向上,构建从叶子到根的所有证书路径。
- 每一级用上一级 CA 的公钥验证下一级证书的签名。
- 检查每张证书的有效期、吊销状态、密钥用途等约束。
- 如果路径终点是受信任的根证书(且所有检查通过),则信任成立。
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 | 高(缓存近实时) | 好(不暴露客户端) | 好(无额外请求) |
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 握手失败,先看看证书链是不是完整,再查查吊销状态——八成能解决问题。