连上正确的 IP,为什么还不够?
客户端要向 https://shop.example.com 提交订单。DNS 给出地址,TCP 建立连接,只能说明它连到了一个网络端点,不能证明这个端点有资格代表 shop.example.com。
如果途中有人冒充服务器,单纯把 HTTP 内容“加密”给对方,仍然可能把订单和凭据交给冒充者。HTTPS 需要一起解决对端身份认证、密钥建立,以及传输内容的保密与完整性。
本章讨论常见的 HTTP/1.1、HTTP/2 经 TLS over TCP 通信。HTTP/3 的 QUIC 会集成 TLS 1.3 握手机制,不能直接套用 TCP 连接步骤,留到第 20 章。
截至 2026-10-09,TLS 1.3 主规范已由 RFC 9846 替代 RFC 8446。下面按 RFC 9846 解释普通证书认证握手,不把旧编号称为最新规范。RFC 9846:TLS 1.3
加密、摘要、签名,分别做什么?
先把经常混在一起的工具拆开:
| 工具 | 核心作用 | 单独使用时缺什么 |
|---|---|---|
| 对称加密 | 用共享密钥保护数据内容,适合大量传输 | 还要安全建立密钥并认证对方 |
| 摘要(哈希) | 把输入映射成摘要,供完整性检查等机制使用 | 无密钥哈希本身不能证明发送者身份 |
| 数字签名 | 用私钥签名,公钥验证相应内容 | 还要确认公钥属于预期身份 |
| 密钥协商 | 让双方建立共享秘密,例如临时 ECDHE | 未认证的协商仍可能被中间人替换 |
如果攻击者可以同时替换文件与普通哈希值,接收者比较二者仍可能“通过”。完整性保护需要可信校验信息,不能只在消息后附一个 SHA-256 就宣称防伪造。
签名也不应理解为“拿私钥加密整个网页”。不同算法有不同数学过程;TLS 中签名用于认证特定握手内容,后续大块数据主要由对称机制保护。TLS 1.3 使用 AEAD(带关联数据的认证加密)保护记录,把保密与完整性校验结合起来。RFC 9846:记录保护
ECDHE 是使用椭圆曲线的临时 Diffie-Hellman 密钥交换。双方交换公开份额,结合各自保留的临时秘密建立共享秘密,再派生不同用途、不同方向的密钥。共享秘密不是以明文直接发给对方,证书公钥也不等于本次会话的对称密钥。
证书把哪两件事连接起来?
服务器证书包含公钥与身份信息,由签发者签名。客户端可以沿叶子证书、中间 CA 等建立验证路径,最终连接到自己信任的锚点。
客户端信任某个根,不是因为“它能给自己签名,所以必然可信”,而是因为该根已通过客户端的信任配置被接受。服务器常发送叶子及必要中间证书,信任锚点通常来自客户端;不是对方随便附上一个根就让客户端自动信任。
接着还要回答:有效证书里的身份,是不是本次 URL 要访问的名字? 现代服务身份验证使用证书 subjectAltName 中相应标识;不能把 Common Name 当作通用替代方案,也不能只检查证书“没过期”。RFC 9525:TLS 服务身份验证
| 客户端检查 | 在本例中判断什么 | 只通过这一项还不够的原因 |
|---|---|---|
| 信任路径与签名 | 是否连接到受信任锚点,路径是否合法 | 可能是另一域名的有效证书 |
| 证书有效期 | 当前时间是否在允许范围内 | 仍可能不受信或名字不匹配 |
| 身份匹配 | SAN 是否匹配 shop.example.com | 自签的正确名字也不自动可信 |
| 用途与约束 | 是否允许用于相应服务器认证 | 名称正确不代表用途无条件适用 |
| 握手中的私钥证明 | 对端是否持有相应私钥并参与本次握手 | 光复制别人的公开证书不能冒充 |
证书撤销、算法政策和浏览器其他检查也需要考虑,实际行为依赖实现与部署;这张表不是完整浏览器证书验证器的实现清单。
CA 对证书的签名,与服务器对握手内容的签名是不同层次:前者支持公钥与身份的绑定,后者证明对端掌握私钥并把认证绑定到当前握手。公开证书可以被任何人下载;保密的是私钥,而不是证书文件本身。
TLS 1.3:从公开份额到受保护的 HTTP
下面限定普通证书认证、临时 ECDHE、服务器认证、无客户端证书、无重试、无恢复、无 0-RTT 的路径。TCP 已建立,图按逻辑消息分组,不表示一条箭头必然等于一个 TCP 包。
查看 Mermaid 源码
sequenceDiagram
participant A as 客户端 A
participant S as shop 服务器 S
A->>S: ClientHello:支持版本/算法、客户端 key_share
S-->>A: ServerHello:选择参数、服务器 key_share
Note over A,S: 建立共享秘密,派生握手密钥
S-->>A: 加密的 EncryptedExtensions、Certificate、CertificateVerify、Finished
Note over A: 验证证书/名字、握手签名与 Finished
A->>S: 加密的客户端 Finished
A->>S: 应用密钥保护的 HTTP 请求
S-->>A: 应用密钥保护的 HTTP 响应ClientHello 声明能力,key_share 带公开份额。ServerHello 选择参数并提供服务器份额;双方有了派生握手密钥所需的材料。
之后的服务器握手消息受握手密钥保护。Certificate 提供证书链,CertificateVerify 对规定的握手上下文签名,Finished 使用基于握手秘密的认证码确认握手记录。Finished 不是另一张证书,也不等同于数字签名。 客户端正确验证后发送自己的 Finished,应用数据再使用相应方向的应用流量密钥。RFC 9846:协议总览与认证消息
图只画客户端请求后的业务响应;协议在某些位置允许服务器提前发送应用数据,不能从教学图推出所有实现必须等待同样的业务时序。握手密钥与应用密钥有不同用途,也不是双方所有通信共用一把不变的密钥。
TLS 1.3 不再使用旧式 RSA 密钥传输。证书仍可使用 RSA 签名,这不意味着“恢复了 RSA 加密会话秘密”的流程;认证算法、密钥交换与对称套件不要混成一个名字。
临时密钥交换在适当前提下提供前向安全性:未来泄露长期认证私钥,不应直接解出之前录下的会话。前提包括临时秘密被正确清除、算法与实现可靠等;不能扩张成“任何设备秘密泄露都解不开历史数据”。
TLS 1.2 与 1.3,面试怎样比较?
TLS 1.2 曾支持多种密钥交换。这里用完整的 ECDHE 服务器证书握手对比,不把 TLS 1.2 全部讲成 RSA 密钥传输。
| 对照项 | 本节 TLS 1.2 完整 ECDHE 路径 | 本节 TLS 1.3 完整路径 |
|---|---|---|
| 公开份额 | 服务器通过 ServerKeyExchange 提供,客户端随后提交份额 | ClientHello/ServerHello 的 key_share 提供 |
| 服务器证书 | 通常在未加密握手阶段传输 | ServerHello 后在加密握手消息中传输 |
| 完整握手等待 | 客户端通常经历 2 RTT 后可开始应用通信 | 客户端通常经历 1 RTT 后可发送应用请求 |
| RSA 密钥传输 | 历史上存在,但不是本例 ECDHE 路径 | 移除;RSA 签名仍可用于认证 |
| 数据保护与协商 | 有较多历史算法组合 | 收敛到 AEAD 等机制,协商结构简化 |
这些 RTT 只估计已有 TCP 连接上的普通完整 TLS 握手,忽略计算、分片、丢包等额外耗时;不包含 DNS、TCP 建连或 HTTP 响应。HelloRetryRequest 会增加等待,恢复和 0-RTT 又有其他前提,下一章单独讨论。RFC 5246:历史 TLS 1.2 握手
协议可描述的历史选项不等于当前部署建议。实际系统应依照安全政策禁用过时版本和弱算法,而不是为了复现旧题目重新启用它们。RFC 9325:TLS 安全使用建议
真实实验:同一证书,三个验证结果
本地实验临时生成一个 CA 和它签发的服务器证书,SAN 为 shop.example.com,然后只连接 127.0.0.1 随机端口。没有外部 DNS 请求,也不把 CA 导入系统或浏览器信任库。
先运行公开入口,再读 core 的实现映射:
python3 examples/network-interview/user_code/tls_identity.py需要 Python 3.10+ 的 ssl 模块及支持所用命令的 OpenSSL CLI。实测为 Python 3.13.3、OpenSSL 3.6.3 / macOS。每次 socket 等待最多 3 秒,线程结束最多等待 4 秒,单次证书命令限时 15 秒;端点关闭,临时目录随上下文清理。
| 客户端配置 | 预期结果 | 实测结论 |
|---|---|---|
| 显式信任临时 CA,验证 shop.example.com | 完成握手,PING 收到 PONG | 成功协商 TLSv1.3,SAN 匹配,收到 PONG |
| 不添加临时 CA,仍验证 shop.example.com | 信任链验证失败 | SSLCertVerificationError,无法找到受信签发者 |
| 信任临时 CA,但验证 other.example.com | 主机名验证失败 | SSLCertVerificationError,名字不匹配 |
这里的 server_hostname 同时用于传递名字及客户端主机名验证,底层网络地址仍是回环地址。连接地址、要验证的服务名字、信任锚点,是三个不同输入。 不通过关闭验证来让失败分支“成功”。Python 官方:默认验证上下文与主机名检查
证书命令使用显式 CA 约束和服务器 SAN,用法依据 OpenSSL 官方 req 与 x509 文档。固定 RSA 2048 只是本地证书夹具的选择,不代表本次 TLS 使用 RSA 密钥传输。
实验没有抓包,不证明图中的消息分组、1 RTT 时长、公开网站信任、撤销机制或前向安全性。公开入口归档在源码仓库的 examples/network-interview/user_code/,正式实验实现位于 core/tls.py。
动手切换两个独立的验证条件
证书与连接地址保持不变。分别取消CA信任、改变目标名字,再执行教学验证,解释为什么名字匹配与信任路径缺一不可。真实验证结果以上面的Python实验为准。
底层连接地址始终为127.0.0.1,服务器证书由临时CA签发,SAN固定为shop.example.com。只改变客户端验证输入。
SAN:shop.example.com
签发者:临时CA
其他有效期/用途/算法检查正常
对端具备私钥,握手证明有效
连接地址:127.0.0.1
目标名字:shop.example.com
信任配置:包含临时CA
网络地址、服务名字与信任锚点是三个不同输入;得到证书不等于已经认证对端。
修改条件后需要重新验证;不通过关闭证书检查来修复失败。
此处只展示验证条件,不创建TLS连接或实现证书验证器。真实实验使用Python ssl/OpenSSL、随机回环端口、临时证书且不修改系统信任。省略撤销与其他政策差异,不证明抓包时序、握手RTT或网站业务信誉;证书公钥不是会话对称密钥。
HTTPS 保护到哪里?
HTTPS 保护客户端与 TLS 终止端之间的传输。若 CDN 或代理终止 TLS,该端可以看到 HTTP 内容;代理到后端的另一段链路需要自己的保护,不能只看到浏览器地址是 HTTPS 就认为每一跳都端到端保密。
网络观察者通常仍能看到连接地址和流量特征;DNS 与握手中的某些元信息也需分别判断。普通路径中并非从第一个字节起所有消息都加密。TLS 不负责防止终端日志泄露、XSS、订单越权或恶意站点欺骗用户;有效域名证书也不是对网站业务信誉的认证。
30—60 秒参考回答
HTTPS 通过 TLS 建立受保护的传输。客户端需要验证证书信任路径、有效期、用途和目标名字,还要验证握手中的私钥证明。证书绑定公钥与身份,不能直接当作会话对称密钥。普通 TLS 1.3 证书握手交换公开份额,派生握手密钥,再验证加密传输的证书、签名和 Finished,最后用应用密钥保护 HTTP。TLS 1.3 移除 RSA 密钥传输并减少普通完整握手等待;比较 RTT 时要排除 DNS/TCP 等成本。HTTPS 的保护边界是 TLS 终止端,不能替代应用授权与终端安全。
四个追问及解析
1. CA 可信,为什么还要检查域名?
可信 CA 可以签发很多服务的证书。攻击者持有另一个域名的合法证书,也不该代表 shop.example.com。信任路径与目标身份匹配必须同时通过,实验第三条分支正说明这一点。
2. 公钥公开,别人复制证书能冒充服务器吗?
复制证书不等于持有私钥。证书认证握手要求对端对本次握手做相应私钥证明;客户端需要验证它。只有看证书文件、不验证握手签名与相关条件,才会遗漏关键步骤。
3. TLS 1.3 的 RSA 证书为什么仍能使用?
RSA 可以用作认证签名。被移除的是旧式 RSA 密钥传输;本例共享秘密由临时 ECDHE 建立,二者分属不同职责。不要看到证书算法名就认定会话密钥用同一算法传递。
4. 忽略证书错误,内容仍加密,为什么有风险?
你可能与冒充者建立了一条加密连接。加密保证内容交给当前密钥持有者,但关闭身份验证后,未必是预期服务。应定位信任链、名字、有效期和配置问题,而不是把关闭验证作为正常修复。
两个易错辨析
“HTTPS 先用服务器公钥加密所有数据,再用私钥解密。” 大量应用数据主要由对称机制保护;认证、密钥建立与数据保护是不同步骤。旧 RSA 密钥传输也不是给整页网页做这种处理。
“TLS 1.3 握手 1 RTT,所以网页一定 1 RTT 打开。” 这只描述限定路径的 TLS 等待。DNS、TCP、HTTP、渲染,以及重试、丢包等还可能贡献时间;后续连接复用与握手恢复才是下一章要比较的成本。
下一章讨论 HTTPS 连接复用、会话恢复、0-RTT 重放风险,以及 SNI 和 ALPN 怎样参与连接协商。