HTTPS 到底解决了什么:证书、密钥交换和 TLS 握手

HTTPS 很容易被一句「HTTP + SSL/TLS」糊弄过去。这个说法没错,但它只是在命名层面把两块东西摆到一起,没有讲清楚真正的问题。

HTTP 本身不是不能传数据。请求行、Header、Body、响应状态码、响应体都很清楚,TCP 也会保证字节流可靠、有序地到达。问题在于普通 HTTP 默认相信中间网络:路由器、代理、公共 Wi-Fi、运营商链路、被入侵的网关都站在传输路径上,只要内容是明文,它们就可能读取、篡改,甚至冒充目标服务器。

HTTPS 做的事情,是在 HTTP 真正发送之前,先用 TLS 建出一条安全通道。HTTP 还是 HTTP,只是 HTTP 报文被放进了 TLS 记录里。

HTTP 明文和 HTTPS 加密通道的区别

RFC 8446 里对 TLS 的目标讲得很直接:在可靠、有序的传输之上提供安全通道。这个安全通道至少要覆盖三件事:身份认证、机密性、完整性。MDN 的 TLS 说明也按这三类能力解释 HTTPS 为什么能抵抗中间人攻击。

TLS 要解决的三类安全问题

身份认证解决「正在连接的服务器是不是目标域名背后的服务器」。机密性解决「中间网络能不能读到 HTTP 内容」。完整性解决「内容在路上被改了,接收方能不能发现」。

这三个点要放在一起看。只加密但不认证,客户端可能把秘密交给冒充者;只认证但不加密,中间人仍然能读请求;只加密但不校验完整性,密文被改动后可能引出更隐蔽的问题。HTTPS 的价值正是把这几件事捆在同一条连接里处理。

先把几个词分开

HTTPS 里会同时出现编码、散列、对称加密、非对称加密、签名、证书、MAC、AEAD。它们名字都和「安全」沾边,但职责完全不一样。

编码只是表示形式转换,比如 URL 编码、Base64、字符集编码。编码后的内容可能不好直接读,但不提供保密性,知道规则就能还原。

散列是把输入压成固定长度摘要,比如 SHA-256。它适合做指纹、完整性校验和派生材料的一部分,但不是加密。散列通常不能从摘要反推出原文;同一份输入又应该得到同一份摘要。

对称加密才是 HTTPS 后续传 HTTP 数据的主力。客户端和服务器拿到同一类会话密钥后,用 AES-GCM、ChaCha20-Poly1305 这类算法保护大量应用数据。对称算法速度快,适合持续传输。

非对称加密使用公钥和私钥。公钥可以公开,私钥必须保密。在现代 HTTPS 里,它更常见的职责是配合证书做身份认证,或者在历史 TLS 1.2 RSA 密钥交换里传递 pre-master secret。现在主流连接更依赖 ECDHE 这类临时密钥交换,再用证书私钥给握手材料签名;「用服务器公钥加密所有 HTTP 内容」已经不是适合描述现代连接的模型。

签名也不是加密。签名的重点是证明「这个摘要确实由某个私钥持有者确认过」。证书链、CertificateVerify 都在用这个思路。

MAC 和 AEAD 用来保护消息完整性。TLS 1.2 里常能看到独立的 MAC 讲法;TLS 1.3 保留下来的记录层算法都是 AEAD,也就是加密和认证一起完成。看到这些词时,先问它在解决身份、保密、完整性里的哪一件事,整条链路就不会乱。

证书证明公钥归属

服务器把一个公钥发给浏览器,这件事本身不难。难的是浏览器要知道:这个公钥是不是属于正在访问的域名。

如果中间人能把服务器公钥替换成自己的公钥,后面的加密就没有意义。客户端会以为自己在和目标服务器建立安全通道,实际是在和中间人建立通道,中间人再另起一条连接去访问真正的服务器。两段连接都可能是「加密」的,但用户的数据已经被中间人看见了。

证书就是为了解决这个绑定关系。站点证书里会包含域名、公钥、有效期、用途、签发者等信息,也包含 CA 对这些信息的数字签名。RFC 5280 定义的 X.509 证书字段里,tbsCertificate 承载 subject、issuer、公钥、有效期和扩展信息;签名值证明 CA 认可这份证书里「主体和公钥」的绑定关系。

浏览器验证证书时,不是简单地「用 CA 公钥解密一下」。更准确地说,是从本地信任的根证书出发,验证中间证书,再验证站点证书,最后检查域名、有效期、证书用途、吊销状态和本地策略。

浏览器如何验证 HTTPS 证书链

MDN 的证书颁发机构说明里也强调,浏览器预置了一批根证书,可以用它们检查站点证书是否能链回受信任的根。这个「链回去」很关键:服务器通常会发送站点证书和中间证书,根证书一般已经在浏览器或操作系统的信任库里。

所以自签名证书不一定「技术上不能用」,而是默认不在浏览器信任链里。访问 BadSSL 的自签名测试站点 时,Chrome 会直接停在证书错误页,因为这条链无法回到浏览器信任的根。

自签名证书触发浏览器证书错误

开发环境里可以把自建根 CA 安装到本机信任库,再签发本地域名证书。公司代理也可能通过安装企业根证书来解密 HTTPS 流量。这里的本质没有变:浏览器信任了那个根,后续由它签出来的站点证书就可能通过校验。真正要小心的是,不要随便把来源不明的根证书加入系统信任库。

现代握手的主线是 ECDHE

很多 HTTPS 入门资料会把握手讲成这样:服务器发送证书,客户端拿到服务器公钥,生成一个随机密钥,用服务器公钥加密后发回去,之后双方用这个密钥加密 HTTP。

这描述的是 TLS 1.2 里的 RSA 密钥交换主线,而且还经常把 pre-master secret、master secret、traffic keys 混成一个「主密钥」。它能帮助理解历史流程,但不适合当成现代 HTTPS 的默认模型。

RSA 的算法原理:用乘法造一把带陷门的锁

Matrix67 的《跨越千年的 RSA 算法》好读,是因为它没有一上来就把公式堆满,而是先把问题讲成「公开锁和私钥开锁」,再一点点引入同余、欧拉函数和模逆。HTTPS 里也可以沿着这条线看:先理解 RSA 这个盒子为什么能锁住一个小秘密,再看 TLS 为什么后来不再把会话秘密交给它运输。

RSA 的公钥可以写成 (n, e),私钥可以写成 (n, d)n 来自两个大素数 pq 的乘积。e 是公开指数,d 是和 e 配对的私有指数。它们要满足 e * d ≡ 1 (mod φ(n)),其中 φ(n) = (p - 1) * (q - 1)。知道 pq 很容易算出 φ(n),再求出 d;只知道公开的 n,就要先把 n 分解回 pq,这正是 RSA 想利用的难题。

用很小的数字跑一遍会更清楚。选 p = 5q = 11,得到 n = 55φ(n) = 40。再选 e = 3,找出 d = 27,因为 3 * 27 = 81,除以 401。于是公钥是 (55, 3),私钥是 (55, 27)

RSA 玩具数字例子如何加密和解密

如果明文数字是 12,用公钥加密就是 12^3 mod 55 = 23。网络上看到的是 23。服务器用私钥解密,计算 23^27 mod 55 = 12,原来的数字回来了。

为什么能回来?因为 e * d = 1 + k * φ(n)。对和 n 互素的消息,欧拉定理保证 m^φ(n) mod n = 1,所以 m^(e*d) 会绕回 m。这个绕回能力就是 RSA 的陷门:知道 d 的人可以反向打开;只拿到 (n, e) 的人,理论上也知道门长什么样,却缺少从 n 分解出 pq 的那一步。

RFC 8017里的 RSA 加密原语也落在这个模幂操作上:公钥加密、私钥解密本质上都是对整数做指数和取模,只是输入的钥匙不同。真实世界不能裸用这个玩具算法。实际 RSA 还要处理填充、随机性、消息长度、错误处理和旁路攻击;TLS 1.2 历史上的 RSA 密钥交换也正是因为填充 oracle 这类风险,才有很多防御细节。

TLS 1.2 的 RSA 密钥交换更像是 key transport,也就是「把一个秘密运到服务器」。客户端先生成 48 字节的 pre-master secret,再用服务器证书里的 RSA 公钥加密,放进 ClientKeyExchange 发给服务器。服务器用自己的 RSA 私钥解密,拿到同一份 pre-master secret。随后两边再结合 client_randomserver_random 派生 master secret 和后续记录层密钥。RFC 5246 的 RSA-Encrypted Premaster Secret描述的就是这条路径。

这条路径的直觉很好懂:公钥锁盒子,私钥开盒子。客户端把秘密放进盒子里,网络上的人只能看到上锁后的盒子。问题在于,服务器证书私钥是长期身份密钥,一旦未来泄露,过去抓到的那些「上锁盒子」就有机会被重新打开。只要攻击者保存了当年的握手包,就可能从 RSA 加密的 pre-master secret 推回当年的会话密钥。

ECDHE 的算法原理:两边各自算出同一个秘密

ECDHE 属于 key agreement,也就是「双方各自算出同一个秘密」。它继承了 Diffie-Hellman 的主意:秘密不直接经过网络,网络上只交换公开参数。

先用普通 Diffie-Hellman 的小数字看一次。公开约定 p = 23g = 5。客户端偷偷选 a = 6,发出 A = 5^6 mod 23 = 8。服务器偷偷选 b = 15,发出 B = 5^15 mod 23 = 19。客户端拿到 B 后算 B^a mod 23 = 19^6 mod 23 = 2,服务器拿到 A 后算 A^b mod 23 = 8^15 mod 23 = 2。两边得到同一个 2

旁观者能看到 235819,却缺少 ab。想从 5^6 mod 23 = 8 反推出 6,在小数字里可以试出来;换成真实参数后,这就是离散对数问题,靠暴力枚举已经不可行。

ECDHE 把普通 DH 里的 g^a mod p 换成椭圆曲线上的点乘 aG。这里的「乘」指曲线点群上的反复加法:2G = G + G3G = 2G + G。给定私钥 a 和公开基点 G,算出公开点 A = aG 很快;只看 GA,反推出 a 很难,这叫椭圆曲线离散对数问题。RFC 6090把椭圆曲线群定义在有限域上,并把 ECDH 作为椭圆曲线版 Diffie-Hellman 来描述。

普通 DH 和椭圆曲线 ECDH 的计算对应关系

图里的椭圆曲线例子仍然是玩具数字:曲线是 y^2 = x^3 + 2x + 2 (mod 17),基点 G = (5, 1)。客户端私钥选 a = 6,公开点就是 A = 6G = (16, 13);服务器私钥选 b = 11,公开点就是 B = 11G = (13, 10)。客户端计算 aB = 6 * (13, 10) = (7, 6),服务器计算 bA = 11 * (16, 13) = (7, 6)。两边走的是不同路径,最后落到同一个曲线点。

真实 TLS 不会使用这种小曲线。常见组是 X25519、secp256r1 这类经过标准化和长期分析的参数。玩具曲线只用来说明一件事:shared secret 没有被发出去。网络上只有双方的 key share,也就是 AB。临时私钥 ab 留在各自机器里,用完就丢。

再单独看一遍 ECDHE 的计算路径,会更容易理解它和证书之间的边界。

ECDHE 如何协商共享密钥

E 是 ephemeral,意思是临时。这个字母很重要:每次握手都重新生成一套临时私钥和公开参数,用完就丢。即使服务器证书私钥以后泄露,攻击者也只拿到了长期身份私钥,拿不到当年每条连接里已经销毁的 ECDHE 临时私钥。过去抓包里只有双方的 key share,没有足够信息重算 shared secret。

ECDHE 为什么替换 RSA 密钥交换

shared secret 也不会直接拿去加密 HTTP。TLS 会把 ClientHello、ServerHello、随机数、握手 transcript 等材料纳入派生过程,得到不同方向、不同阶段使用的流量密钥。TLS 1.2 里会看到 PRF、pre-master secret、master secret、key block 这些名字;TLS 1.3 改成了基于 HKDF 的 key schedule。RFC 8446 的 TLS 1.3 差异说明明确提到,TLS 1.3 移除了静态 RSA 和静态 DH 密码套件,公钥密钥交换都提供前向安全。

前向安全的意思是:即使服务器证书私钥未来泄露,攻击者也不能仅凭过去抓到的握手包解出历史会话内容。因为每次连接里的 ECDHE 临时私钥已经消失,过去的 shared secret 无法重算出来。历史 RSA 密钥交换做不到这一点:如果当时客户端把 pre-master secret 用服务器长期 RSA 公钥加密发送,未来服务器私钥泄露后,曾经抓包的人就有机会解出旧会话。

RSA 密钥传输和 ECDHE 密钥协商的本质区别

ECDHE 还顺手把职责切得更清楚。证书私钥只负责证明服务器身份,ECDHE 负责协商新鲜的会话秘密,HKDF / PRF 再负责把 shared secret 和握手 transcript 派生成真正的流量密钥。长期身份密钥不再直接承担「解开会话秘密」的职责,历史会话和当前身份之间的耦合就小很多。

理解它为什么替换 RSA 密钥交换,关键要看长期身份私钥是否还负责解开会话秘密。RSA 仍然可以用于证书签名,TLS 1.3 里也引入了 RSASSA-PSS 这类签名方案;真正被淘汰的是 RSA key transport 这种把会话秘密交给长期私钥解开的握手方式。ECDHE 提供前向安全,密钥协商更适合每条连接都生成新鲜秘密,也更符合 TLS 1.3 把认证、密钥交换和记录层保护拆开的设计方向。

证书私钥在 ECDHE 流程里仍然重要。服务器需要用证书对应的私钥证明自己确实持有这个身份。TLS 1.3 里的 CertificateVerify 会对握手 transcript 签名;浏览器用证书里的公钥验证签名,确认「这条临时 ECDHE 协商」和「这个域名证书」被绑定在一起。RFC 8446 的协议概览也把这两件事分开:ClientHelloServerHellokey_share 确定共享密钥,CertificateCertificateVerifyFinished 负责认证和握手完整性。

TLS 1.3 握手怎么走

TLS 1.3 把握手流程压得比 TLS 1.2 更短,也把更多握手内容加密起来。可以按三段看。

第一段是 Key Exchange。客户端发 ClientHello,里面带支持的 TLS 版本、密码套件、扩展、随机数和 key_share。服务器回 ServerHello,选定版本、密码套件,并返回自己的 key_share。这两条消息确定握手后续需要的共享密钥材料。

第二段是 Server Parameters 和 Authentication。服务器发送加密后的 EncryptedExtensions,再发送证书、CertificateVerifyFinishedCertificateVerify 证明服务器拥有证书私钥;Finished 是对整个握手 transcript 的确认,能发现中间人是否篡改过前面的协商内容。

第三段由客户端验证服务器消息,再发送自己的 Finished。这之后,客户端和服务器开始用派生出的应用流量密钥收发 HTTP 数据。

TLS 1.3 完整握手主线

RFC 8446 的协议概览给出的 TLS 1.3 完整握手图,也正是这条主线:ClientHelloServerHello 负责确定密钥材料,CertificateCertificateVerifyFinished 负责身份和握手完整性,握手结束后应用层数据用认证加密保护。

TLS 1.2 不是不能用 ECDHE。TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 这种套件就是 TLS 1.2 里很典型的 ECDHE 路线。真正要区分的是:TLS 1.2 同时保留了更多历史组合,密码套件名称也把密钥交换、证书认证、记录层算法和哈希都塞在一起;TLS 1.3 则把密钥交换和认证从密码套件名称里拆出去了。

TLS 1.2 和 TLS 1.3 密码套件命名差异

看到 TLS_AES_128_GCM_SHA256 这种 TLS 1.3 名字时,不要再按 TLS 1.2 的方式问「这里怎么没有 ECDHE 或 RSA」。它只描述记录层保护算法和 HKDF 使用的哈希。ECDHE 的组、证书签名算法、ALPN、SNI 等信息都通过扩展和握手消息协商。

HTTPS 到底加密了哪些内容

握手完成后,HTTP 请求和响应会放在 TLS Application Data 里。按照 MDN 的 TLS 握手说明,后续消息包括 HTTP Header 和 Body 都会用协商出的密钥保护。

这意味着路径、查询参数、Cookie、Authorization、请求体、响应体都不该被普通中间网络直接看见。抓包工具如果没有拿到会话密钥,通常只能看到 TCP 连接、TLS 握手和加密后的 Application Data。

不过 HTTPS 没有隐藏所有元数据。对方 IP、端口、连接时间、流量大小、记录长度仍然在链路层面可见。DNS 查询如果没有走 DoH、DoT 或其他加密通道,访问的域名也可能先在 DNS 层暴露。传统 SNI 还会把要访问的主机名放进 ClientHello,方便同一个 IP 上的服务器选择正确证书。RFC 6066 定义的 server_name 扩展就是这个用途。

TLS 1.3 已经加密了 ServerHello 之后的大部分握手内容,但 ClientHello 里仍有敏感元数据。2026 年发布的 RFC 9849 定义了 Encrypted Client Hello,目标是把 ClientHello 里的 SNI、ALPN 等字段也保护起来。它能减少域名元数据暴露,但是否生效取决于浏览器、DNS 记录、CDN 或服务器部署情况,不能把它当成所有 HTTPS 连接已经默认具备的能力。

所以 HTTPS 更准确的边界是:保护 HTTP 内容和 TLS 记录层数据,不等于隐藏所有连接元数据;保护访问某个域名时的传输过程,不等于证明这个网站本身可信。

TLS 记录怎么保护应用数据

握手结束后,TLS 不会把一整个 HTTP 响应攒完再加密。它会把应用层数据切成一条条记录,每条记录独立保护。这样大响应可以边传边解,错误也更容易定位在记录层。

TLS 1.2 时代常见的解释是:记录里有类型、版本、长度、加密后的数据和 MAC。不同方向还会派生不同的写入密钥和 MAC 密钥,比如客户端写入密钥、服务器写入密钥、客户端写入 MAC 密钥、服务器写入 MAC 密钥。序列号不一定显式出现在记录里,但会参与 MAC 计算,避免同一条连接里记录被重排或重放后仍然通过校验。

TLS 1.3 的记录层主线更简洁:保留的套件都使用 AEAD。AEAD 把加密和认证合在一起,记录序列号会参与 nonce 构造,记录头等关联数据会纳入认证。接收方解不开或认证失败,就不能把这条记录交给 HTTP 层。

这里也能解释很多 TLS 资料里提到的「为什么不重数 nonce 很重要」。同样的明文和同样的密钥如果反复产生同样的密文,会暴露模式;AEAD 算法需要每条记录使用合适的 nonce。TLS 协议负责把序列号、派生出的 IV 和算法规则组合起来,避免应用自己直接管理这些细节。

False Start 和 0-RTT 不要混着讲

TLS 1.2 时代经常讨论 False Start。它的思路是客户端发送自己的 Finished 后,不等服务器 Finished 回来,就提前发送加密的应用数据,用一个 RTT 换延迟。这个优化需要浏览器和服务器满足特定条件,也和前向安全、ALPN/NPN 等能力相关。RFC 7918 后来把 TLS False Start 标准化成独立文档。

TLS 1.3 之后,常规完整握手本身已经变成典型 1-RTT。对于曾经连接过的站点,还可以通过 PSK resumption 做更短的恢复握手,甚至发送 0-RTT early data。

0-RTT 的代价是重放风险。RFC 8446 的 0-RTT 章节明确把它的安全属性和普通 1-RTT 数据区分开:early data 没有跨连接的不可重放保证。适合放幂等请求,不适合支付、下单、状态变更这类重复执行会出问题的操作。

把 False Start、TLS 1.2 四次握手、TLS 1.3 0-RTT 放在一条时间线上讲,很容易把优化手段和协议主线混到一起。更稳的记法是:TLS 1.2 有历史优化,TLS 1.3 直接重塑了握手;0-RTT 是恢复连接时的特殊能力,不是所有 HTTPS 请求都能无脑提前发。

排查 HTTPS 时看哪些字段

浏览器里最直观的是安全面板和证书详情。证书错误页通常已经把失败类型说得很清楚:未知 CA、域名不匹配、证书过期、证书被吊销、证书链不完整,处理方向都不一样。

命令行里可以先用 curl 看协议、证书和响应头。-I 只取响应头,-v 打印连接细节,--tlsv1.3 可以临时要求 TLS 1.3,适合判断服务是否支持某个版本。

curl -Iv --tlsv1.3 https://shengsheng.fun/

如果要看证书字段,用 openssl s_client 拉证书链,再交给 openssl x509 展开关键信息。

openssl s_client \
  -connect shengsheng.fun:443 \
  -servername shengsheng.fun \
  </dev/null 2>/dev/null |
  openssl x509 \
    -noout \
    -subject \
    -issuer \
    -dates \
    -ext subjectAltName \
    -fingerprint \
    -sha256

用 openssl 检查证书字段

这里最常看的字段有几个。

subject 是证书主体。现在浏览器判断域名主要看 SAN,也就是 subjectAltName,不要只盯着老的 CN 字段。

issuer 是签发者。它应该能通过服务器给出的中间证书链,最终回到本机信任库里的某个根证书。

notBeforenotAfter 是有效期。客户端本机时间不准,也会造成看起来莫名其妙的证书过期或尚未生效。

subjectAltName 要覆盖正在访问的域名。访问 www.example.com 时,只有 example.com 不一定够,除非证书里有对应通配符或明确列出 www.example.com

指纹是证书内容的哈希,适合人工比对「是不是同一张证书」,不是浏览器建立信任链的主要依据。生产环境不要指望用户手工看指纹来完成 HTTPS 信任。

如果要看协商出的协议和密码套件,可以让 openssl s_client 多打印连接信息。

openssl s_client \
  -connect shengsheng.fun:443 \
  -servername shengsheng.fun \
  -tls1_3 \
  </dev/null

输出里的 ProtocolCipher、证书链、验证结果都值得看。Verify return code: 0 (ok) 说明 OpenSSL 按本机信任库验证通过;如果这里失败,再结合浏览器错误页判断是链路问题、域名问题,还是本机信任库问题。

把主线记成四句话

HTTPS 可以直接理解成 HTTP over TLS。HTTP 负责表达业务请求,TLS 负责在不可信网络上建立安全通道。

证书不负责加密所有流量,证书负责把域名和公钥绑定起来,并让浏览器能沿着 CA 信任链验证这个绑定。

现代握手通常靠 ECDHE 协商临时共享秘密,再通过 HKDF 或 PRF 派生不同阶段、不同方向的流量密钥。证书私钥用于证明服务器身份,不是拿来逐字节加密 HTTP。

握手完成后,HTTP Header 和 Body 都在 TLS 记录里被保护;IP、端口、长度、DNS、传统 SNI 这类元数据则要按各自层级单独判断,不能把「用了 HTTPS」理解成「网络上什么都看不见」。