HTTPS 与 TLS 究竟如何工作:握手与证书
你添加了 SSL 证书,将域名指向服务器,然后小锁图标出现了。大多数开发者到此为止。然后有一天,某个 API 客户端报错 UNABLE_TO_VERIFY_LEAF_SIGNATURE,一个移动应用在旧版 Android 上拒绝你的证书,或者负载均衡器终止了 TLS 而你的应用看到的一切都是明文 HTTP。突然之间,你需要知道在 TCP 连接和第一个 HTTP 请求之间到底发生了什么。
本文深入探讨 HTTPS 和 TLS 的真实工作原理:握手做了什么、证书如何验证、TLS 1.3 有哪些变化,以及如何诊断你实际会遇到的错误。
HTTPS 究竟增加了什么
HTTPS 就是通过 TLS 连接发送的 HTTP。TLS 提供三项保证:
- 机密性 —— 流量使用握手期间协商的对称密钥加密。
- 完整性 —— 每条记录都带有认证标签,因此篡改会被检测到。
- 认证 —— 服务器证明它控制着与你请求的主机名匹配的证书的私钥。
注意列表上没有什么:客户端默认不被认证。TLS 认证服务器;你的应用程序认证用户。
TLS 1.2 握手,逐步解析
TLS 1.2 在旧基础设施和许多企业代理中仍然常见,因此值得了解。完整的握手如下所示:
- ClientHello —— 客户端发送支持的 TLS 版本、一个随机值、密码套件列表,以及(如果适用)SNI 主机名。
- ServerHello —— 服务器选择一个版本和密码套件,返回自己的随机值,并发送其证书链。
- 密钥交换 —— 使用 ECDHE 时,服务器发送一个临时公钥;客户端发送自己的。双方推导出相同的共享密钥。服务器用其证书的私钥对握手记录进行签名。
- Finished —— 双方发送握手记录的 MAC,确认没有篡改。
- 应用数据 —— HTTP 请求和响应加密传输。
关键细节是临时密钥交换。使用 ECDHE 时,每个连接都会生成新的密钥对。即使攻击者记录了多年的流量并后来窃取了服务器的私钥,他们也无法解密过去的会话。这种特性称为前向保密,这也是静态 RSA 密钥交换被弃用的主要原因。
TLS 1.3 如何改变局面
TLS 1.3 将握手简化为单个往返,并完全移除了旧算法。
| 方面 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 往返次数(完整握手) | 2 | 1 |
| 密钥交换 | RSA 或 ECDHE | 仅 ECDHE(前向保密) |
| 密码套件 | 独立的加密、MAC、密钥交换 | 仅 AEAD 套件,例如 TLS_AES_128_GCM_SHA256 |
| 静态 RSA、CBC、RC4 | 允许 | 已移除 |
| 会话恢复 | 会话 ID / 票据 | PSK 及 0-RTT 选项 |
在 TLS 1.3 中,客户端猜测密钥交换组并在第一条消息中发送其份额,因此服务器可以立即完成密钥协商。证书仍然发送,但现在被加密,这意味着被动观察者不再能看到正在服务的主机名——只有 ClientHello 中的 SNI 会泄露它,除非使用了加密客户端问候(ECH)。
0-RTT 恢复允许返回的客户端在第一个飞行中发送应用数据。它很快,但 0-RTT 数据可被重放,因此绝不要将其用于非幂等请求(如支付)。
证书与链验证
证书将公钥绑定到主机名。验证是一个链式遍历过程:
- 服务器发送其叶子证书加上中间证书。
- 客户端在其存储中查找受信任的根证书,然后验证链上每个签名。
- 它根据主题备用名称(SAN)字段检查主机名——现代客户端忽略通用名称。
- 它检查有效期和吊销状态(CRL 或 OCSP,通常为装订)。
unable to verify 错误最常见的原因是缺少中间证书。浏览器有时通过缓存中间证书来掩盖此问题,但命令行工具和移动应用会直接失败。始终提供完整的链,叶子证书在前,然后是中间证书——绝不要包含根证书。
使用 OpenSSL 进行实用调试
当出现问题时,先检查服务器实际发送了什么,再责怪客户端。
# 显示服务器提供的证书链
openssl s_client -connect example.com:443 -servername example.com -showcerts
# 仅协商 TLS 1.3 并列出选定的密码套件
openssl s_client -connect example.com:443 -tls1_3
# 一行命令检查证书过期时间
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltName如果 s_client 报告 verify error:num=21:unable to verify the first certificate,则链不完整。如果报告主机名不匹配,请检查 SAN 列表——通配符证书仅覆盖一个标签,因此 *.example.com 匹配 api.example.com,但不匹配 a.b.example.com。
常见错误配置
- 混合内容 —— 一个 HTTPS 页面加载
http://脚本或图片。浏览器阻止主动混合内容,并可能自动升级被动内容。 - TLS 在负载均衡器处终止 —— 后端看到的是明文 HTTP。转发
X-Forwarded-Proto并仅信任来自已知代理的该头,否则你的应用可能生成错误的重定向 URL。 - 启用了弱协议版本 —— 应禁用 TLS 1.0 和 1.1。目标仅使用 TLS 1.2 和 1.3。
- 没有 HSTS —— 没有
Strict-Transport-Security头,第一个请求仍可能被降级。从较短的max-age开始,确认后再提高。 - 生产环境中的自签名证书 —— 内部测试可以,但它们会训练用户和客户端忽略警告。
一个合理的服务器配置
对于 Nginx,现代基线配置如下:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;在 TLS 1.3 中让服务器选择密码套件——客户端偏好也可以,因为套件都很强。除非你轮换票据密钥,否则关闭会话票据,因为泄露的票据密钥可能危及恢复。
常见问题
HTTPS 会加密主机名吗?
默认不会。ClientHello 中的 SNI 扩展以明文发送,以便服务器知道要出示哪个证书。加密客户端问候(ECH)可以隐藏它,但采用仍然有限。路径和查询字符串始终加密。
为什么我的证书在浏览器中有效,但在 curl 或移动应用中失败?
几乎总是缺少中间证书。浏览器通常自行获取或缓存中间证书,而更严格的客户端要求服务器发送完整的链。使用 openssl s_client -showcerts 验证。
TLS 1.3 总是比 TLS 1.2 快吗?
对于新连接,是的——一个往返而不是两个。对于恢复的连接,差距缩小,而 0-RTT 可以使 TLS 1.3 更快,代价是非幂等请求的重放风险。
如果你在 TLS 变更后审计 Web 服务器,日志分析通常揭示真正的影响:损坏的重定向、意外的 4xx 峰值,或客户端仍在访问 HTTP。你可以将访问日志放入 Nginx 日志分析器 来查看状态码分布,并找出从未完成升级到 HTTPS 的客户端。