HTTPS 与 TLS 究竟如何工作:握手与证书

Security2026-10-27TryQuickToolBox

你添加了 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 在旧基础设施和许多企业代理中仍然常见,因此值得了解。完整的握手如下所示:

  1. ClientHello —— 客户端发送支持的 TLS 版本、一个随机值、密码套件列表,以及(如果适用)SNI 主机名。
  2. ServerHello —— 服务器选择一个版本和密码套件,返回自己的随机值,并发送其证书链。
  3. 密钥交换 —— 使用 ECDHE 时,服务器发送一个临时公钥;客户端发送自己的。双方推导出相同的共享密钥。服务器用其证书的私钥对握手记录进行签名。
  4. Finished —— 双方发送握手记录的 MAC,确认没有篡改。
  5. 应用数据 —— HTTP 请求和响应加密传输。

关键细节是临时密钥交换。使用 ECDHE 时,每个连接都会生成新的密钥对。即使攻击者记录了多年的流量并后来窃取了服务器的私钥,他们也无法解密过去的会话。这种特性称为前向保密,这也是静态 RSA 密钥交换被弃用的主要原因。

TLS 1.3 如何改变局面

TLS 1.3 将握手简化为单个往返,并完全移除了旧算法。

方面TLS 1.2TLS 1.3
往返次数(完整握手)21
密钥交换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 数据可被重放,因此绝不要将其用于非幂等请求(如支付)。

证书与链验证

证书将公钥绑定到主机名。验证是一个链式遍历过程:

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。

常见错误配置

一个合理的服务器配置

对于 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 的客户端。