当我们在浏览器里点开一把小锁:SSL/TLS是怎么保护我们的
事情从地址栏那把小锁说起
我以前一直没认真想过一个问题:每次在浏览器里输入网址,凭什么就敢把账号密码发出去?明文HTTP的时代早就过去了,现在几乎所有网站地址栏前面都顶着一把小绿锁,点开能看到一句"连接是安全的"。这把锁背后站着的,就是 SSL/TLS。
说起来 SSL 这个名字其实有点误导。它最早是 Netscape 在 1994 年搞出来的,叫 Secure Sockets Layer,经历了 2.0、3.0 两版。1999 年以后 IETF 接手,改名 TLS(Transport Layer Security),从此 TLS 1.0、1.1、1.2、1.3 一路走下来。但因为 SSL 3.0 的设计底子打得好,习惯上大家还是把整族协议笼统地叫"SSL/TLS",连 OpenSSL 这个最出名的实现库都保留着这个名字。
所以当我们说"SSL"的时候,很多时候其实在说 TLS。这一点一开始把我绕晕过。
它到底在 TCP 和应用之间塞了什么
SSL/TLS 不改动 TCP,也不改动 HTTP,它只是像一个夹心层一样,蹲在两者中间。上层应用把明文交给它,它加密之后再交给 TCP 发出去;接收端反过来,TCP 把密文给它,它解密还原成明文递给应用。对应用来说,这一切几乎是透明的。
这个设计很聪明。任何跑在 TCP 上的协议——HTTP、SMTP、IMAP——都能不改动自身逻辑就获得加密保护。HTTPS 就是 HTTP over TLS 的缩写,仅此而已。
协议内部分两层。下面是记录协议(Record Protocol),负责把数据切片、计算消息认证码、加密、打包;上面是握手协议(Handshake Protocol),负责在通信双方之间协商好"用什么算法、用什么密钥"。我后来发现,理解 SSL/TLS 的关键,几乎全在那个握手里。
两个人在公开网络里怎么"对暗号"
握手是最有意思的部分。问题本身听起来就有点矛盾:两个素未谋面的人,在一个满是窃听者的网络里,要商量出一把只有他们俩知道的密钥,还不能被中间人骗。怎么做到?
SSL/TLS 的答案是:用非对称密码学开路,用对称密码学干活。
以 TLS 1.2 的完整握手为例,大致是这样几步。客户端先发 ClientHello,把自己支持的版本、密码套件、一个随机数都列出来;服务器回 ServerHello,从中挑一套,再附上自己的证书和服务器随机数。然后双方做一次密钥交换——可能是 RSA,也可能是 ECDHE——各自算出同一个预主密钥,再结合两个随机数派生出会话密钥。最后双方各发一条 Finished 消息,里面带着整个握手过程的校验值,确认没人篡改过握手内容。握手一结束,后面就全用对称加密跑数据了。
这个设计的精妙之处在于"混合":非对称算法慢但能解决密钥分发,对称算法快但不能在公开信道上商量密钥,于是各取所长。Schneier 在《应用密码学》里就指出过这一点。我读到这段时觉得,密码协议设计很多时候不是发明新东西,而是把已有的几块拼成一个自洽的整体。
还有一个小细节让我印象深刻:握手里的两个随机数,一个客户端给的、一个服务器给的,看似多余,其实是为了防重放。每次握手随机数都不一样,攻击者就算录下整个会话也没法原样重播。
这协议踩过的那些坑
光看原理会觉得 SSL/TLS 设计得很漂亮,但翻它的安全历史,简直是一部事故编年史。我挑几个印象深的写。
POODLE(2014,CVE-2014-3566)打的是 SSL 3.0 的 CBC 填充。攻击者先把连接降级到 SSL 3.0,再利用 CBC 填充的冗余性,通过观察服务器对错误填充的反应一点点把明文——通常是 Cookie——抠出来。这个漏洞直接把 SSL 3.0 送进坟墓,IETF 在 2015 年发了 RFC 7568 正式废弃它。
BEAST(2011,CVE-2011-3389)打的是 TLS 1.0 的 CBC 初始化向量可预测。攻击者在浏览器里塞一段恶意 JavaScript,配合中间人,能逐字节猜出会话 Cookie。TLS 1.1 早就用显式 IV 修了,但因为兼容性,TLS 1.0 在 BEAST 公开后还在用。
Heartbleed(2014,CVE-2014-0160)最让我警醒。它根本不是协议设计的问题,是 OpenSSL 实现心跳扩展时少做了一次边界检查,结果攻击者发个伪造长度的心跳请求,就能从服务器内存里读走最多 64KB 的数据——私钥、会话密钥、用户凭证,什么都可能漏。
Heartbleed 给我的触动是:协议再安全,实现一烂全完。这也是为什么后来 BoringSSL、LibreSSL 这些 OpenSSL 的分支会冒出来——光靠修补不够,得从工程上重新清理。
TLS 1.3:一次回头的重塑
TLS 1.3(RFC 8446,2018)不是小修小补,是推翻重来。我读这份 RFC 的时候能感觉到设计者的一种决绝。
它干了几件大事。第一,把所有已知不安全的算法和机制全砍了:RSA 密钥交换、CBC 模式、RC4、3DES、MD5、SHA-1,一个不留。剩下 5 个密码套件,全部用 AEAD 认证加密,全部强制 ECDHE 前向安全。前向安全的意思是,就算服务器长期私钥哪天泄露,以前抓过的密文也解不开——因为每次会话的密钥都是临时算的、用完就扔。
第二,握手从 2-RTT 压到 1-RTT,还搞了个 0-RTT 模式,重连时第一个包就能带数据。这对手机端延迟敏感的场景很实在。
第三,ServerHello 之后的所有握手消息都加密了。老版本里大部分握手是明文,被动观察者能看清你协商了什么。TLS 1.3 把这些都盖上了。
读到这里我有个感受:TLS 1.3 的设计哲学其实是"做减法"。把所有历史包袱一刀切掉,只留最安全的极简组合。这和软件工程里"less is more"的思路是一回事。
自己动手:用 Python 窥探真实的握手
光看文献不过瘾,我想自己看看真实的 TLS 握手长什么样。Python 标准库的 ssl 模块刚好够用,不用装第三方包。
核心代码其实不长。思路是:建一个安全的 TLS 上下文,强制证书校验,然后连到目标站点,握手完了把协商出来的版本、密码套件、证书信息都打出来。
Python
import ssl
import socket
def create_secure_context():
ctx = ssl.create_default_context()
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
return ctx
def connect_and_inspect(host, port=443):
ctx = create_secure_context()
with socket.create_connection((host, port), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
print("协议版本:", ssock.version())
cipher = ssock.cipher()
print("密码套件:", cipher[0])
cert = ssock.getpeercert()
subject = dict(x[0] for x in cert.get("subject", []))
print("证书主体:", subject.get("commonName"))
print("有效期:", cert.get("notBefore"), "至", cert.get("notAfter"))
我顺手把它扩成一个扫描脚本,跑了 8 个知名网站。结果挺有意思,挑几个列出来:
| 站点 | 协商版本 | 密码套件 | 证书颁发者 |
| www.python.org | TLSv1.3 | TLS_AES_128_GCM_SHA256 | GlobalSign Atlas R3 DV TLS CA 2025 Q4 |
| www.cloudflare.com | TLSv1.3 | TLS_AES_256_GCM_SHA384 | WE1 |
| www.mozilla.org | TLSv1.3 | TLS_AES_128_GCM_SHA256 | YR1 |
| www.microsoft.com | TLSv1.3 | TLS_AES_256_GCM_SHA384 | Microsoft TLS G2 RSA OCSP 04 |
| www.apple.com | TLSv1.3 | TLS_AES_128_GCM_SHA256 | Apple Public EV Server RSA CA 4 - G1 |
| www.baidu.com | TLSv1.2 | ECDHE-RSA-AES128-GCM-SHA256 | GlobalSign RSA OV SSL CA 2018 |
几个观察。第一,6 个成功连上的站点里有 5 个跑的是 TLS 1.3,说明这协议真的在落地,不是我调研报告里空写的。第二,密码套件清一色 ECDHE + AES-GCM,全支持前向安全,没有 CBC 模式的影子,和 TLS 1.3 的设计取舍完全吻合。第三,百度还在用 TLS 1.2,这其实不奇怪——大站要照顾海量老客户端,协议升级永远是渐进的,不能一刀切。这个细节让我对"理论上的最优"和"工程上的现实"之间的差距有了点实感。
还有个小插曲。我本来想扫 github.com,结果本地证书链验证没过,报了 CERTIFICATE_VERIFY_FAILED。这多半是我机器上 CA 证书库的问题,但反过来也说明:证书校验这道关卡是真实在工作的,不是摆设。
写在最后
这次调研让我对 SSL/TLS 的认识从"知道有这么个东西"变成"大概能讲清楚它怎么工作、踩过什么坑、为什么现在是这个样子"。几个感受记一下。
一是密码协议的安全是个动态过程。SSL/TLS 这二十多年,几乎每个版本都是被漏洞推着往前走的——POODLE 推动了 SSL 3.0 退役,BEAST 推动了显式 IV,Heartbleed 暴露了实现层的风险,最后这些教训汇总成 TLS 1.3 的大重构。没有哪个协议是"设计完就安全"的,安全是持续攻防的结果。
二是设计层和实现层是两件事。Heartbleed 不是协议的错,是 OpenSSL 的错。一个协议设计得再严密,落到代码里一个边界检查漏了,照样可能全军覆没。这也解释了为什么密码学库里"用经过审查的成熟实现"几乎是铁律,自己造轮子的代价太高。
三是对称和非对称的配合很优雅。非对称负责在公开信道上安全地商量密钥,对称负责高效地加密数据,两者各取所长。这种"混合"思路在密码协议里随处可见,SSL/TLS 只是把它用得最显眼的一个。
往远了看,量子计算的发展正在威胁 RSA 和 ECC 这些公钥算法。NIST 已经在推后量子密码学标准化,IETF 也在研究怎么把后量子算法塞进 TLS。SSL/TLS 的故事还没完,下一个十年大概率又是一次大改。我作为一个大二学生,能在这个时间点把这个协议的来龙去脉摸一遍,挺值的。
最后要说明,文中的算法描述、RFC 编号、漏洞编号都核对过原文;扫描数据是我自己跑脚本拿到的真实结果,没有手工编造。理解有偏差的地方,责任在我。
参考资料
Freier A, Karlton P, Kocher P. The Secure Sockets Layer (SSL) Protocol Version 3.0. RFC 6101, IETF, 2011.
Dierks T, Allen C. The TLS Protocol Version 1.0. RFC 2246, IETF, 1999.
Dierks T, Rescorla E. The Transport Layer Security (TLS) Protocol Version 1.1. RFC 4346, IETF, 2006.
Dierks T, Rescorla E. The Transport Layer Security (TLS) Protocol Version 1.2. RFC 5246, IETF, 2008.
Rescorla E. The Transport Layer Security (TLS) Protocol Version 1.3. RFC 8446, IETF, 2018.
Barnes R, Thomson M, Pironti A, et al. Deprecating Secure Sockets Layer Version 3.0. RFC 7568, IETF, 2015.
Wagner D, Schneier B. Analysis of the SSL 3.0 Protocol. The Second USENIX Workshop on Electronic Commerce Proceedings, 1996: 29-40.
Rescorla E. SSL and TLS: Designing and Building Secure Systems. Addison-Wesley Professional, 2000.
Stallings W. Cryptography and Network Security: Principles and Practice. 7th Edition. Pearson, 2017.
Schneier B. Applied Cryptography: Protocols, Algorithms, and Source Code in C. 2nd Edition. John Wiley & Sons, 1996.
NIST. Guidelines for the Selection, Configuration, and Use of TLS Implementations. SP 800-52 Rev. 2, 2019.
韦卫, 王德杰, 张英, 等. 基于SSL的安全WWW系统的研究与实现. 计算机研究与发展, 1999.
