【计算基础|网络07】HTTPS(下):ECDHE 握手与优化
上篇结尾留了个硬伤:RSA 握手把预主密钥用服务器长期公钥 加密后传输,一旦这把私钥泄露,攻击者翻出历史录屏,所有旧通信全能解密。这篇就来解决这个问题------引入 ECDHE 临时密钥交换,让每次会话都用一次性临时密钥,私钥泄露了也翻不了旧账。
讲完 ECDHE,再看 TLS 1.3 这一版协议做了多大手术:把 RSA 密钥交换、静态 DH、MD5/SHA-1/3DES/RC4 全部扔掉,握手从 2-RTT 砍到 1-RTT,还支持 0-RTT 恢复。最后是工程上真正影响线上性能的几件事:会话恢复、OCSP Stapling、False Start、ECC 证书、ALPN、AES-NI,以及你天天会碰到的证书排错。
环境:macOS + Python 3.14 + OpenSSL 3.5.6。命令均本机实测。
一、ECDHE:让密钥"用完即焚"
1.1 Diffie-Hellman 到底在干嘛
先说 Diffie-Hellman(DH)的核心思想,不用公式,用一个通俗类比:
- 两个人公开约定一个大质数
p和一个生成元g(这两个数谁都能看,包括中间人)。 - 客户端自己想一个私钥
a(保密),算出公钥A = g^a mod p,把A发出去。 - 服务器自己想一个私钥
b(保密),算出公钥B = g^b mod p,把B发出去。 - 客户端拿到
B,算B^a mod p;服务器拿到A,算A^b mod p。 - 数学上这两个结果相等,这就是双方的共享密钥。
🔴 妙处在哪?中间人窃听到的只有 A、B、g、p,但从 A 反推 a、从 B 反推 b 是离散对数难题,计算上不可行。双方从未在网络上传输共享密钥,却各自算出了同一个密钥。
1.2 ECDHE:椭圆曲线版的临时 DH
ECDH (Elliptic Curve DH)就是把上面的整数模运算换成椭圆曲线点乘法,同样的安全强度下密钥短得多:
| 密钥交换 | 同等安全强度的密钥长度 |
|---|---|
| RSA / 传统 DH | 3072 位 |
| ECC / ECDH | 256 位 |
E (Ephemeral,临时)才是关键:每次握手都临时生成一对密钥,会话一结束就扔掉。所以叫 ECDHE------Elliptic Curve Diffie-Hellman Ephemeral。
对比一下静态 DH:
| 类型 | 私钥对 | 前向保密 |
|---|---|---|
| 静态 DH(DHE 静态) | 长期固定的密钥对 | ❌ 没有 |
| ECDHE 临时 | 每次握手新生成,用完即焚 | ✅ 有 |
下面用 Python cryptography 实测一次完整的 ECDHE 交换,你会看到双方从未传输共享密钥本身,却各自算出了同一个 256 位密钥:
python
# ecdh_demo.py
from cryptography.hazmat.primitives.asymmetric import ec
# 模拟 TLS 握手里的 ECDHE:双方各自生成临时密钥对
# 客户端临时密钥对(本次握手用完即焚)
c_priv = ec.generate_private_key(ec.SECP256R1())
c_pub = c_priv.public_key()
# 服务器临时密钥对
s_priv = ec.generate_private_key(ec.SECP256R1())
s_pub = s_priv.public_key()
# 双方交换公钥(网络上明文传输的就是这两个公钥)
# 客户端用服务器公钥 + 自己私钥算共享密钥
c_shared = c_priv.exchange(ec.ECDH(), s_pub)
# 服务器用客户端公钥 + 自己私钥算共享密钥
s_shared = s_priv.exchange(ec.ECDH(), c_pub)
print(f"客户端算得的共享密钥(hex前32): {c_shared.hex()[:32]}...")
print(f"服务器算得的共享密钥(hex前32): {s_shared.hex()[:32]}...")
print(f"双方一致: {c_shared == s_shared}")
print(f"共享密钥长度: {len(c_shared)*8} bit")
实测输出:
text
=== ECDHE 临时密钥交换实测 ===
客户端算得的共享密钥(hex前32): a0bfb4201ffd55cee18517f72f809f3f...
服务器算得的共享密钥(hex前32): a0bfb4201ffd55cee18517f72f809f3f...
双方一致: True
共享密钥长度: 256 bit
注意:传输的只有两个公钥 c_pub 和 s_pub,共享密钥 c_shared/s_shared 是双方各自本地算出来的,从未上过网。中间人即使把这两个公钥全录下来,也推不出共享密钥------这就是椭圆曲线离散对数难题挡住的。
1.3 前向保密(PFS)到底防什么
前向保密(Forward Secrecy,也称 Perfect Forward Secrecy / PFS)的定义:即使服务器长期私钥明天被偷,攻击者今天录下来的所有历史密文仍然解不开。
原因很简单:历史会话用的是当时临时生成的 ECDHE 密钥对,那对临时私钥早就被销毁了,长期私钥泄露跟它没有任何关系。
🔴 这就是为什么 Snowden 事件后,Google、Cloudflare 等公司全面禁用 RSA 密钥交换、强制 ECDHE------被动监听者即使把全球流量全录下来,没有当天的临时私钥,就是一堆死数据。
1.4 ECDHE 握手时序(TLS 1.2)

和上篇 RSA 握手对比,多了一个 ServerKeyExchange 消息------服务器把自己的临时 ECDH 公钥连同参数发过来,并用证书里的长期私钥签名(防中间人替换临时公钥)。客户端收到后也发自己的临时公钥,双方各算各的,共享密钥就出来了。
1.5 ECDHE vs RSA 握手对比表
| 维度 | RSA 密钥交换 | ECDHE 密钥交换 |
|---|---|---|
| 预主密钥怎么传 | 用服务器长期公钥加密 | 双方各出临时公钥,各自算共享密钥 |
| 服务器私钥泄露后 | 历史密文全可解 | 历史密文仍不可解 ✅ |
| 握手消息 | 7 个(无 ServerKeyExchange) | 8 个(多 ServerKeyExchange) |
| 额外计算 | RSA 解密一次 | 一次 ECDH 椭圆曲线点乘 |
| 前向保密 | ❌ | ✅ |
| TLS 1.3 | 已移除 ❌ | 唯一支持 ✅ |
二、TLS 1.3:一次彻底的大手术
TLS 1.3(RFC 8446,2018 年发布)不是小修小补,是把 TLS 1.2 十几年积累的历史包袱一次性砍掉。它是现在所有现代浏览器、手机 App、大站的默认版本。
2.1 砍掉了什么
| 移除项 | 原因 |
|---|---|
| RSA 密钥交换 | 无前向保密 |
| 静态 DH 密钥交换 | 无前向保密 |
| 3DES | 加密强度不足 |
| RC4 | 有已知偏见 |
| MD5、SHA-1 作为握手哈希 | 已被碰撞攻击 |
| CBC 模式的大量 cipher suite | 实现复杂、历史漏洞多 |
| 压缩 | CRIME/BREACH 攻击 |
| ALERT 明文细节 | 防止信息泄露 |
🔴 一句话:TLS 1.3 只保留了带前向保密的 AEAD 密码套件。也就是说,只要你跑 TLS 1.3,前向保密就是强制的,没得选。
2.2 握手从 2-RTT 压到 1-RTT
TLS 1.2 握手为什么是 2-RTT?因为 ClientHello 之后,服务器要先回 ServerHello + Certificate + ServerKeyExchange + ServerHelloDone,客户端再回 ClientKeyExchange + Finished,一来一回两个往返。
TLS 1.3 的做法:客户端在 ClientHello 里就直接把自己的 ECDHE 临时公钥带上 (key_share 扩展)。服务器回 ServerHello 时把自己的临时公钥也带上,双方在收完 ServerHello 那一刻就各自算出了共享密钥------第一个往返结束,密钥已经有了。

2.3 TLS 1.2 vs TLS 1.3 对比表
| 维度 | TLS 1.2(RFC 5246) | TLS 1.3(RFC 8446) |
|---|---|---|
| 发布年份 | 2008 | 2018 |
| 密钥交换 | RSA / DHE / ECDHE 可选 | 仅 ECDHE(强制前向保密) |
| 握手 RTT | 2-RTT | 1-RTT(支持 0-RTT 恢复) |
| 加密的握手消息 | 只有 Finished 之后的应用数据 | ServerHello 之后全部加密 |
| 密码套件数量 | 30+ 个,参差不齐 | 仅 5 个 AEAD 套件 |
| 废弃算法 | 仍兼容(可配) | 强制移除 |
| 密钥分离 | 较模糊 | 加密密钥、流量密钥、导出密钥彻底分开 |
🔴 提一句 TLS 1.3 的密钥分离 改进。TLS 1.2 里主密钥派生出来的那一堆密钥(加密、MAC、写 IV)边界比较模糊,一旦某个密钥被侧信道泄露,可能牵连其他。TLS 1.3 把密钥推导拆成一条严格的链路:握手密钥 → 应用数据密钥 → 恢复密钥,每层用不同的哈希标签派生,互不影响。0-RTT 用的恢复密钥和普通应用密钥也是分开的------这就是为什么重放攻击只影响 0-RTT 数据,不影响正常会话。
2.4 用 openssl 看一次真实的 TLS 1.3 握手
bash
echo | openssl s_client -connect github.com:443 -servername github.com -tls1_3
实测关键输出:
text
Peer signature type: ecdsa_secp256r1_sha256
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
Protocol: TLSv1.3
Verify return code: 0 (ok)
再用 Python ssl 模块确认一下协商结果:
python
# tls13_demo.py
import ssl, socket
ctx = ssl.create_default_context()
with socket.create_connection(("github.com", 443), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname="github.com") as ssock:
print(f"协议版本 : {ssock.version()}") # TLSv1.3
print(f"密码套件 : {ssock.cipher()[0]}") # TLS_AES_128_GCM_SHA256
实测输出:
text
协议版本 : TLSv1.3
密码套件 : TLS_AES_128_GCM_SHA256
证书 CN : github.com
有效期 : Sep 1 00:00:00 2026 GMT ~ Nov 29 23:59:59 2026 GMT
⚠️ 你现在去看任何主流大站(github.com、cloudflare.com、google.com),基本都只支持 TLS 1.3 了。强制指定 TLS 1.2 反而会失败------这不是 bug,是趋势。
2.5 0-RTT:快是快了,但有重放风险
TLS 1.3 还支持 0-RTT 会话恢复:客户端把上次握手的会话票据带上,在第一个 ClientHello 里直接把应用数据也捎上,服务器一次往返都不用等就收到了请求。
🔴 但 0-RTT 有个致命安全风险:重放攻击。0-RTT 加密的应用数据是用一个双方都知道的"恢复密钥"加密的,攻击者如果把这个请求录下来,重放到服务器上,服务器没法区分"这是真客户端重发"还是"攻击者重放"。
所以工程上的规矩是:0-RTT 只能用于幂等请求(GET),绝不能用于 POST 下单、支付这类有副作用的操作。下一篇讲 RPC 时还会再碰到幂等这个词。
三、HTTPS 性能优化:每省一个 RTT 都是钱
握手安全了,但每次都全量握手要 1-RTT 甚至 2-RTT,移动网络下一个 RTT 可能 100-300ms。线上优化基本就下面这几招。
3.1 会话恢复:Session ID vs Session Ticket
| 方案 | 状态存哪 | 优点 | 缺点 |
|---|---|---|---|
| Session ID | 服务器端存会话上下文 | 简单 | 服务器要存状态,多机集群难共享 |
| Session Ticket | 客户端存一个加密的票据 | 服务器无状态 | 票据加密密钥泄露则所有票据失效 |
Session Ticket 流程:第一次全量握手后,服务器把会话密钥等信息用一个只有服务器知道的 ticket 密钥加密成一张"票据"发给客户端;下次连接客户端把票据带上,服务器解开票据直接恢复密钥,不用重新 ECDHE。
⚠️ 会话恢复牺牲前向保密:因为复用了上次的密钥,所以 0-RTT/会话恢复严格来说不具备完整 PFS。安全敏感场景要权衡。
3.2 OCSP Stapling:别让浏览器去单独查吊销状态
上篇说浏览器要查证书吊销状态,标准做法是浏览器实时去 OCSP 服务器查------这又多了一次往返,而且很多用户直接把 OCSP 检查关了(太慢)。
OCSP Stapling 反过来:服务器自己定期去 CA 拿 OCSP 响应,在握手时把这个响应"捎带"给浏览器。浏览器不用额外发请求,直接验。
实测看服务器有没有 stapling,用 -status 参数:
bash
echo | openssl s_client -connect github.com:443 -servername github.com -status 2>/dev/null | grep -A2 "OCSP response"
3.3 TLS False Start:客户端抢跑
TLS 1.2 时代,客户端要等服务器的 Finished 收到、验完,才敢发应用数据。False Start 让客户端在发出自己的 Finished 之后就直接发应用数据,不用等服务器------抢了半个 RTT。
这个优化现在浏览器和 TLS 1.3 基本内置了,你不用配。
3.4 证书优化:用 ECC 证书、精简链、Must-Staple
| 优化点 | 为什么 |
|---|---|
| ECC 证书(P-256 或 X25519) | 256 位 ECC 证书比 2048 位 RSA 证书小一半,握手包更小、验签更快 |
| 证书链精简 | 服务器只发必要的中间 CA,根 CA 不发(浏览器自己有) |
| OCSP Must-Staple | 证书里加个扩展,要求服务器必须 stapling,否则浏览器拒绝------防吊销绕过 |
实测里 example.com 的证书链就是 ECC 的(签发者是 Cloudflare TLS Issuing ECC CA 3),4 张证书,正好是 服务器证书 + 2 张中间 + 1 张根。
3.5 ALPN:在 TLS 握手里协商应用层协议
HTTP/2 要求 TLS,但浏览器怎么知道该用 HTTP/1.1 还是 HTTP/2?靠 ALPN 扩展(Application-Layer Protocol Negotiation):客户端在 ClientHello 里说"我会 h2 和 http/1.1",服务器在 ServerHello 里选一个。
bash
echo | openssl s_client -connect github.com:443 -servername github.com -alpn h2,http/1.1 2>/dev/null | grep ALPN
协商成功会打印 ALPN protocol: h2。
3.6 硬件加速:AES-NI
现代 x86 CPU 有 AES-NI 指令集,一条硬件指令就能完成 AES 一轮加密。没有 AES-NI 时纯软件 AES 慢 5-10 倍。这也是为什么 AES-GCM 是 TLS 1.3 首选密码套件------硬件支持最好。
四、常见 HTTPS 排错:线上 90% 的证书问题就这几种
| 错误现象 | 原因 | 怎么查 |
|---|---|---|
| NET::ERR_CERT_DATE_INVALID | 证书过期或未生效 | openssl x509 -in cert.pem -noout -dates 看有效期 |
| NET::ERR_CERT_COMMON_NAME_INVALID | 域名不匹配(SAN 不含你访问的域名) | openssl x509 -in cert.pem -noout -ext subjectAltName |
| NET::ERR_CERT_AUTHORITY_INVALID | 证书链不完整(服务器漏发中间 CA)或自签名 | openssl s_client -showcerts 数证书张数,必须能链到根 CA |
| no protocols available / unsupported protocol | 服务器只支持老 TLS 1.0/1.1,客户端拒绝 | openssl s_client -tls1_2 / -tls1_3 分别试 |
| 无法获取证书 / handshake fail | SNI 没开:一台服务器多个域名,客户端没在 ClientHello 里带 SNI,服务器不知道该发哪张证书 | 命令行必须加 -servername 域名;程序里要设 SNI |
⚠️ SNI 是最容易被忽略的 。一台 Nginx 上跑 10 个 HTTPS 站点,靠什么区分该发哪张证书?就靠 ClientHello 里的 SNI 扩展(服务器名指示)。你用 openssl s_client 不加 -servername,很多服务器会返回默认站证书或干脆不发证书------上篇里我们第一次连百度时就遇到了这个现象("no peer certificate available")。
五、总结:你真正需要记住的 9 件事
- ECDHE = 临时 ECC 版 DH:每次握手生成临时密钥对,用完即焚,这就是前向保密的实现。
- 前向保密的意义:服务器长期私钥泄露,历史密文仍然不可解------这是 RSA 密钥交换做不到的。
- TLS 1.2 ECDHE 握手比 RSA 多一个 ServerKeyExchange 消息,带服务器临时公钥和长期私钥签名。
- TLS 1.3 砍掉了一切:RSA 密钥交换、静态 DH、3DES、RC4、MD5、SHA-1、CBC 套件全部移除,只留 ECDHE + AEAD。
- TLS 1.3 握手从 2-RTT 压到 1-RTT,因为客户端在 ClientHello 里就把临时公钥带上了;ServerHello 之后所有消息全部加密。
- 0-RTT 有重放风险,只能用于幂等 GET,不能用于 POST 支付。
- 性能优化三板斧:会话恢复 (Session ID/Ticket)、OCSP Stapling (捎带吊销响应)、False Start(客户端抢跑)。
- 证书选型用 ECC (小、快),Nginx 上记得开 SNI,否则多域名服务器发错证书。
- 线上证书报错就五类:过期、域名不匹配、链不完整、协议版本不支持、SNI 没开------对着 openssl 输出一一排查即可。
验证清单
- 能用大白话讲清 DH 密钥交换为什么不用传密钥本身就能算出共享密钥
- 能解释 ECDHE 里那个 "E"(Ephemeral)是什么意思,以及它怎么带来前向保密
- 能在纸上画出 TLS 1.3 握手时序,并指出客户端在哪个消息里带上了临时公钥
- 能说出 TLS 1.3 移除了哪四类老算法,以及为什么
- 能用
openssl s_client -tls1_3连一个真实网站,读出协议版本和 cipher suite - 能用 Python
ssl模块建立 TLS 连接并打印version()和cipher() - 能解释 0-RTT 为什么不能用于支付请求
- 能说出 Session ID 和 Session Ticket 的区别,以及为什么会话恢复会牺牲 PFS
- 遇到"证书链不完整"错误时,知道用
-showcerts数有几张证书
参考资源
- RFC 8446:TLS 1.3 协议 datatracker.ietf.org/doc/html/rf...
- RFC 5246:TLS 1.2 协议(对照用)datatracker.ietf.org/doc/html/rf...
- MDN:传输层安全(TLS)developer.mozilla.org/zh-CN/docs/...
- MDN:证书透明度与 OCSP developer.mozilla.org/zh-CN/docs/...
- Cloudflare 博客:TLS 1.3 介绍(英文)blog.cloudflare.com/tls-1-3-exp...
- OWASP TLS 配置速查表 cheatsheetseries.owasp.org/cheatsheets...
- OpenSSL s_client 手册 www.openssl.org/docs/man3.0...
下一篇《网络08:RPC 与 WebSocket》讲:HTTP 是"请求-响应"一问一答,那微服务之间的 RPC 怎么从 HTTP 演进到 gRPC、为什么需要长连接、WebSocket 怎么在一个 TCP 连接上做全双工通信,以及 SSE 和 WebSocket 的取舍。