TLS 学习
HTTPS 的常见误解是“用证书加密数据”。证书的主要工作不是加密大流量,而是让客户端相信“这个公钥属于我正在访问的域名”。真正的应用数据通常由握手协商出的对称密钥通过 AEAD 加密。
TLS 需要同时解决三件事
- 认证:对端是谁?至少要认证 server,mTLS 时还认证 client。
- 密钥协商:双方如何在窃听者面前得到相同的会话密钥?
- 完整性与保密性:之后的数据是否被篡改或窃听?
TLS 1.3 规范把目标描述为在可靠有序传输之上提供安全信道;server 认证是常态,client 认证是可选。
一张证书链里有什么
一张叶子证书至少会写下:签发者(Issuer)、持有者信息、有效期、公钥、域名或 IP 的 SAN、允许用途,以及 CA 对这些内容做出的签名。它表达的是一条可验证的声明:这个公钥在这段时间内可代表这些名字。
线上服务通常发送 leaf certificate + intermediate certificate(s),这组内容常被称作 fullchain;根证书一般已经在客户端的信任库里,通常不随服务端发送。客户端从叶子开始,用中间 CA 的公钥逐级验签,直到某个本地信任的根。根证书不是拿来“解密服务器证书”的,而是信任链的锚点。

把根 CA 直接拿来给每个网站签名会让根私钥暴露得过于频繁。因此公共 CA 和较成熟的内部 PKI 通常让离线、保护严格的根证书签发中间 CA,再让中间 CA 签发业务证书。这样既能分层管理,也能在中间 CA 发生问题时缩小轮换与吊销的范围。
证书验证:不是“拿根证书解密”
服务器发送的证书链通常是:叶子证书(域名与公钥)→ 中间 CA → 根 CA。客户端的操作系统/浏览器已经信任一组根 CA。验证过程大致是:
证书链签名能否逐级验证?
↓
叶子证书的 SAN 是否匹配访问的 hostname?
↓
是否在有效期内、用途是否允许 serverAuth?
↓
撤销状态与本地策略是否接受?
最关键的是 SAN(Subject Alternative Name),而不是老旧的 Common Name。访问 api.example.com 时,即使链完全可信,SAN 没有该名称也必须失败。直接访问 IP 则需要 IP 类型的 SAN;这也是自签证书“明明签出来了却不被信任”的高频原因。
从私钥到 fullchain:证书是怎样签出来的
做内部服务证书时,最常见的一条线是:服务先生成私钥,再用私钥生成 CSR(Certificate Signing Request);CSR 带着公钥、主体信息和期望的 SAN,由 CA 按自己的策略审核并签发叶子证书。部署到服务端时,再把叶子证书和中间证书拼成 fullchain。
server.key ──生成──> server.csr ──由 CA 签名──> server.crt
│
server.crt + intermediate.crt(s) ──────────────┘──> fullchain.pem
CSR 只是请求,不是最终的信任结论;真正决定证书能代表哪些名称、能做什么用途的是 CA 签发时写入的扩展。服务端证书通常需要 serverAuth,客户端证书则需要 clientAuth;Key Usage 还会约束它可用于签名、密钥协商等哪些密码学操作。Kubernetes、etcd 这类内部 PKI 中,拿错 serverAuth / clientAuth 往往会得到看似莫名其妙的握手失败。
自签叶子证书的签发者就是自己,适合临时调试,但每个客户端都要单独信任它,轮换也麻烦。更稳定的内部做法是维护一个私有根 CA:只把根证书一次性装入受控客户端的信任库,再由它签发各个服务证书。对公网浏览器而言,仍需要其信任库认可的公共 CA;浏览器不会因为证书格式正确就自动信任私有根。
TLS 1.2 到 1.3:更少往返、更少历史包袱
TLS 1.2 的 cipher suite 组合了密钥交换、认证、加密和哈希的多个选择,历史兼容让握手复杂。TLS 1.3 移除了许多旧算法与静态 RSA 密钥交换,使用 (EC)DHE 进行临时密钥协商,默认以 AEAD 保护记录层数据。这样既缩短了握手,也默认获得 forward secrecy:即使服务器长期私钥未来泄漏,过去被截获的会话通常不能被解密。

这张图适合帮助建立两个直觉:CA 的签名让浏览器能核验“公钥确实属于这个服务”,而私钥始终留在服务器一侧。不过图中“浏览器用公钥加密数据、服务器用私钥解密”的部分描述的是早期 RSA 密钥传输,不应把它当作现代 TLS 1.3 的握手过程;TLS 1.3 中,公钥不再用来直接加密会话密钥。

图中最醒目的差异是完整握手的往返数:TLS 1.2 通常要 2-RTT 后才能安全发送应用数据,TLS 1.3 则通常在 1-RTT 后完成。假设客户端到服务端单程 50ms,图中 TLS 1.2 约在 200ms 后才发出 HTTP 请求,TLS 1.3 则约在 100ms 后即可进入这一步。图里的 HTTP 请求位于 TLS 1.3 的第二个客户端发送批次:客户端先验证 server 的 Finished,再发送自己的 Finished,随后就可接着发送加密的应用数据。
| 切面 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 完整握手延迟 | 常见为 2-RTT | 常见为 1-RTT |
| 密钥协商 | 可选 RSA、DHE、ECDHE;是否有前向保密取决于套件 | 仅使用临时 (EC)DHE,默认前向保密 |
| 握手的加密边界 | 证书等大部分握手消息在 ChangeCipherSpec 前可见 | ServerHello 后的证书、扩展和 Finished 都被握手密钥保护 |
| cipher suite 的含义 | 一组名称常同时捆绑密钥交换、认证、对称加密与哈希 | 只选择 AEAD 对称加密与哈希;签名算法、密钥组分别协商 |
| 会话恢复与附加功能 | 可用 Session ID / Session Ticket 恢复,但没有标准化的 early data | 以 ticket 恢复会话;可选 0-RTT early data,代价是重放风险 |
| 历史功能 | 存在静态 RSA、压缩、重新协商等兼容包袱 | 移除静态 RSA、压缩与重新协商,规则更少 |
TLS 1.2 并不等于“不安全”:使用 ECDHE 和 AEAD 的 TLS 1.2 配置仍是合理的兼容方案。TLS 1.3 的价值在于把更好的默认选择写进协议,减少服务器因为历史套件配置不当而失去前向保密或打开旧攻击面的机会。
一个典型 TLS 1.3 全握手可简化为:
ClientHello: 支持版本、cipher、key share、SNI/ALPN
ServerHello: 选定参数与 key share
Server: EncryptedExtensions + Certificate + CertificateVerify + Finished
Client: Finished
→ 双方开始用 application traffic keys 发送 HTTP
不要把“认证”和“协商密钥”混为一件事
TLS 1.3 仍然需要 server 发 Certificate,client 也仍然需要用本地信任库中的根 CA 逐级验证链、检查 SAN 是否匹配域名、检查有效期与用途。否则中间人也能自己发送一组临时公钥,和 client 协商出一条“加密但通向攻击者”的连接。
CertificateVerify 证明服务器掌握叶子证书对应的长期私钥;它不是“CA 替服务器签每个会话”。CA 的签名把域名和公钥绑定,服务器再用私钥签名本次握手的消息,证明自己确实是该绑定的持有者。
与此同时,临时 key share 的职责是生成本次会话的对称密钥。可以把两者的分工记成:
证书 + CA 验证 + CertificateVerify → 对面是不是目标服务器?
临时 (EC)DHE key share → 本次连接怎样得到共同的秘密?
从共同秘密派生的对称密钥 → 怎样高效加密之后的 HTTP 数据?
临时 (EC)DHE:key share 怎样变成同一把对称密钥
这里的 (EC)DHE 更准确地说是密钥协商,不是把 HTTP 数据或“会话密钥”直接加密发过去。双方都生成一次性的临时私钥和相应公钥:client 的私钥是 a、公钥是 A;server 的私钥是 b、公钥是 B。ClientHello 带 A,ServerHello 带 B,这两个公钥就是 key share。
用传统 Diffie–Hellman 的简化写法,公开参数为 g:
client: A = g^a server: B = g^b
client 收到 B 后:B^a = (g^b)^a = g^(ab)
server 收到 A 后:A^b = (g^a)^b = g^(ab)
两边都得到同一个 g^(ab),也就是 shared secret;旁观者即使看到了 g、A、B,在合理的密码学假设下也难以算出它。真实 TLS 1.3 通常采用椭圆曲线版本,所以名称中有 EC;数学表达不同,关键性质相同:公开交换的是临时公钥,临时私钥 a、b 从不离开各自机器。
TLS 会把这个共同秘密连同握手上下文送入密钥派生函数,得到握手加密密钥,以及 client→server、server→client 两个方向各自的应用数据对称密钥。这样仍然能用对称加密高效传输 HTTP,只是双方不必在网络上交付那把对称密钥本身。私钥 a、b 在连接结束后应丢弃。
这就是 (EC)DHE 中的 E(Ephemeral,临时)。即使服务器未来泄漏了证书的长期私钥,攻击者通常也无法从历史抓包倒推出已经销毁的 a、b,继而不能解开过去的会话;这叫前向保密。早期 TLS 中“client 生成会话密钥,再用服务器 RSA 公钥加密发送”的静态 RSA 密钥传输没有这个性质,TLS 1.3 已将它移除。
1-RTT 说的是等待时间,不是只有两条消息
RTT(Round-Trip Time)是一来一回的网络时间:若单程是 50ms,client 发到 server 50ms、server 回到 client 再 50ms,合起来就是 1 RTT,即 100ms。
TLS 1.3 的完整握手仍有三个消息批次,只是最后一个 client 批次不必再等待 server 回包,便能和首个 HTTP 请求一起发出:
Client → Server: ClientHello + key_share A
Server → Client: ServerHello + key_share B
EncryptedExtensions + Certificate + CertificateVerify + Finished
Client → Server: Finished + 加密的 HTTP request
client 收到第二批消息后,先验证证书和 server 的 Finished,然后发送自己的 Finished;server 验证它后,确认双方看到的是同一份、未被篡改的握手记录。因为 key_share A 已经放在首包里,server 无须先索要 client 的临时公钥再等一次响应。这就是常说的“1-RTT 握手”。
早期 TLS 中常见的叙述是“客户端生成会话密钥,再用服务器 RSA 公钥加密发送”。那对应的正是图中的静态 RSA 密钥传输;TLS 1.3 已将它移除。现在双方通过临时 (EC)DHE key share 共同导出会话密钥,证书私钥主要用于身份认证,而不是解开客户端送来的会话密钥。
今天的 HTTPS:优先 TLS 1.3,但不会只有 TLS 1.3
不是所有 HTTPS 连接都已经是 TLS 1.3。TLS 在 ClientHello 中协商版本,最终选择的是双方共同支持的最高版本:现代浏览器连上已启用 TLS 1.3 的站点,通常会用 TLS 1.3;若站点、CDN、负载均衡、企业代理或旧客户端的任一端不支持,仍会回退到 TLS 1.2。MDN 将 TLS 1.3 称为当前且最广泛使用的版本,但也明确 TLS 1.2 仍被部分网站使用。MDN 的 TLS 文档
新建的公网服务通常应同时启用 TLS 1.3,并将最低版本设为 TLS 1.2:前者优先服务现代客户端,后者保留仍在使用的 TLS 1.2 客户端;TLS 1.0/1.1 则不应继续开放。只有在客户端范围完全可控、已确认全数支持时,才适合考虑把最低版本提高到 TLS 1.3。Cloudflare 的当前建议也正是启用 TLS 1.3,同时将最低版本设为 TLS 1.2。Cloudflare TLS 配置说明
0-RTT:恢复旧会话时,第一包就带请求
首次连接仍需要上面的完整 1-RTT 握手。连接结束时,server 可以给 client 一个用于恢复会话的 ticket。之后 client 再连接同一服务时,能在首个 ClientHello 里带上 ticket,并附带 early data,例如一个 HTTP GET 请求:
Client → Server: ClientHello + 恢复 ticket + early data(HTTP request)
Server → Client: ServerHello + Finished
Client → Server: Finished
client 不必先等 server 回包再发送请求,因此称为 0-RTT。它不是跳过证书与握手验证:client 收到回包后仍要继续完成本次握手;它只是基于上一次已建立的会话状态,提前发送了数据。
代价是 early data 可能被重放。攻击者不一定能读懂或篡改它,却可能在一定条件下让 server 再收到一次相同的 early data。因此只应放可安全重复执行的幂等读取,例如查询或静态资源;不要用于转账、创建订单、修改密码、写入或删除数据等“只能执行一次”的操作。性能优化必须先服从请求语义。
mTLS 把认证变成双向
普通 HTTPS 中,client 验证 server;mTLS 还要求 client 出示证书,并用自己的私钥完成 CertificateVerify,server 再验证 client 的证书链、有效期、clientAuth 用途和身份。服务网格和集群内部组件常用它建立 workload identity。mTLS 并不自动完成授权:证书告诉你“调用者是谁”,是否允许访问某个资源仍是 RBAC/策略问题。
排障时,用 OpenSSL 先看事实
证书问题不必先猜配置。先把服务真正发出的链、SAN 和用途看清楚,通常就能区分是链不全、名字不匹配、信任根缺失,还是私钥配错了。
# 查看服务端实际返回的证书链;SNI 不能漏
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null
# 检查一张本地证书的 SAN、Issuer、有效期、Key Usage / Extended Key Usage
openssl x509 -in server.crt -noout -text
# 按域名验证;私有 CA 可额外指定 -CAfile internal-root-ca.crt
openssl s_client -connect api.example.com:443 -servername api.example.com \
-verify_return_error -verify_hostname api.example.com </dev/null
# 连 IP 时应使用 -verify_ip,而不是把 IP 作为 hostname
openssl s_client -connect 192.0.2.10:443 -verify_return_error \
-verify_ip 192.0.2.10 </dev/null
确认私钥、CSR 和证书是不是同一对公钥,也可以分别导出公钥后比较摘要:
openssl pkey -in server.key -pubout -outform pem | sha256sum
openssl req -in server.csr -pubkey -noout -outform pem | sha256sum
openssl x509 -in server.crt -pubkey -noout -outform pem | sha256sum
三条摘要应相同;若不相同,问题不是 TLS 参数,而是 key、CSR 或证书在某一步配错了。
References
Notion 相关零散笔记
- Https
- Auth
- Kubernetes certificates
- OpenSSL