直观理解 TLS
直观理解 TLS
2025年4月1日
如何在公共场合进行私密对话?思考一下这个问题。
互联网存在一个由来已久的问题:你和所访问的网站之间的任何人都可以看到(甚至 编辑 )所有的来回通信。你如何防止 Starbucks 的路由器看到你的登录密码? 这正是诸如 Transport Layer Security (TLS, 前身是 SSL) 之类的技术试图解决的问题。
但在深入之前,先思考一下:你会怎么做?
加密
最简单的答案是使用只有你和网站知道的代码(密码)进行对话,其中一方对消息进行扰乱(加密),另一方对其进行还原(解密)。 这是一个好的开始,但存在问题:首先,密码必须相对复杂,否则人们会破解它(“字母顺延一位” 这种方式过一段时间就会变得非常明显)。 其次,每个网站不能使用相同的密码…… 那样它就不再是秘密了。
好的,新方案:为每个网站 随机 且 唯一 地生成密码。 我们有一个 随机数生成器 (RNG) 的构造,它可以使用一个起始点(种子)来创建。 每个网站都有自己的种子。 加密从 RNG 中获取字节,并将其与我们的消息组合:M + RNG(seed) = Enc
。 解密执行相反的操作:Enc - RNG(seed) = M
。 该种子可以称为 “密钥”,因为它有效地锁定和解锁消息。 很好!
等等…… 你如何知道网站的密钥? 如果它通过互联网发送,其他人也会看到它,并创建相同的 RNG(key)
来解密我们的消息。 也许可以提前分发? 但是,对于我们第一次访问的网站呢? 真是令人头疼。
密钥交换
幸运的是,有一个巧妙的算法可以解决这个问题:Diffie-Hellman 密钥交换。 它依赖于一个操作 (op),该操作既是 “单向的” ( A op B = C
很容易,但反向 A = B ?? C
非常困难) 又是 “可交换的” ( A op B = C
或者 B op A = C
,顺序无关紧要)。 下面是它的工作原理:
网站和我生成我们自己的随机“私有”密钥,该密钥永远不会共享(分别是 K1
和 K2
)。 我们每个人都使用一个已知的常量来计算我们自己的“公共”密钥 (K1 op C = P1
),并通过互联网相互共享。 最后,我们将我们自己的“私有”密钥与 对方的 “公共”密钥 (K1 op P2 = S
) 结合起来,得到一个只有我们双方都知道的“共享密钥”。
这看起来像魔法,但请看发生了什么:一方计算 K1 op (K2 op C)
,而另一方计算 K2 op (K1 op C)
。 由于 op 是可交换的,我们最终得到相同的结果。 由于 op 是单向的,因此通过网络发送的公钥 K op C
无法实际反转以找到 K
。
将“共享密钥”插入 RNG,万事大吉! 我们现在有一种与任何网站建立秘密通道的方法! ... 或者你可能会这么认为。
所有这些都只是防止其他人阅读我们的消息。 但是如果他们也可以编辑消息怎么办?
身份验证
这里有两种妥协方法:
- 在发送公钥时,有人可能会将我们看到的“对方的公钥”替换为他们自己的公钥,导致我们“秘密地”与中间人而不是彼此交谈。 我如何相信
website.com
的另一端确实是他们? (密钥真实性) - 在解密消息时,有人可能只是将加密文本替换为一些解密为不需要的消息/响应的文本。 想象一下,我的银行向我发送一条加密消息,要求转移资金。 有人使我的回复解密为“是”而不是“否”是非常糟糕的。 即使使用有效的共享密钥,我如何相信对方所说的是他们所想的? (数据真实性)
我将先解释第 2 个问题的解决方案,然后再解释第 1 个问题,因为后者涉及更多。
数据真实性
首先,我们需要引入一个哈希函数 H(x) -> y
。 这是一种接受输入 x
并产生输出 y
的操作,该输出能够抵抗 “原像” (仅从 y
找到 x
非常困难)和 “第二原像” (找到也哈希到 y
的某个 z
非常困难)。
然后,我们将加密消息与秘密 RNG 密钥 H(Enc ++ key)
一起哈希,并将 Enc
和结果一起发送。 哈希结果充当解密方可以用来双重检查消息是否正确的一种方式,然后再接受它。
如果我们只对加密消息进行哈希,中间人也可以只对他们自己的恶意加密消息进行哈希。 但是,此哈希包括对中间人保密的 RNG 密钥。 这意味着他们无法实际计算满足 H(MaliciousEnc ++ key)
的 MaliciousEnc
的哈希值,以通过解密。
这种方案被称为 Authenticated Encryption (选择性地哈希一些 Additional Data)。 或者简称为 AEAD。
密钥真实性
好的,但是我们如何保护共享密钥不被泄露?
老实说? 除了事先达成协议之外,你无能为力。 没有聪明的解决方案,这有点令人失望,但请考虑一下; 如果你不知道对方是谁,你就无法知道你在与谁交谈……
因此,最直接的方法是 Pre-Shared-Keys (PSK),双方可以直接将其放入 AEAD 中(跳过密钥交换),或者使用它来随机化 Diffie-Hellman 共享密钥结果。 这里的问题是,同样,它需要每个网站都有一个 PSK,这是不切实际的。
证书
幸运的是,我们可以使用两个新的原语来解决这个问题:Sign(K, M) -> S
接受一个私钥和一条消息,并生成一个签名。 然后,可以将其馈送到 Verify(P, M, S) -> bool
中,后者接受公钥和相同的消息,并返回签名是否确实来自用于创建公钥的任何私钥。 这如何工作可能 很棘手,但只需知道它建立在与 Diffie-Hellman 中的 op
相似的概念之上。
现在,我们引入一个受信任的第三方(Root),该第三方跟踪谁是谁并拥有自己的公钥/私钥对。 双方都预先共享了 Root 的公钥。 我作为客户端,只信任由 Root Sign
的东西。 因此,服务器获取由 Root 签名的其公钥 + 网站名称,并在进行密钥交换时传递该签名。
中间人更改签名将无法通过我的 Verify
。 为了正确地进行干预,他们必须让 Root 签署 他们自己的 恶意公钥 + 相同的网站名称。 但是 Root 已经将该网站注册给其他人,并且显然不会让他们这样做。 只要我信任 Root 的签名,我就可以知道我正在与之交谈的任何网站确实是他们。
这些“公钥 + 网站名称”签名可以称为 Certificates,它们构成了一个称为 Public Key Infrastructure (PKI) 的集中式信任系统。 你还可以通过说 “我信任 Root2 签署的任何内容,因为它们是由 Root 签署的” 来创建“信任链”。
证书还可以包括诸如“公钥的有效期”之类的东西,以最大程度地减少有人设法暴力破解私钥的可能性。 这可能就是为什么 Web 管理员会说 “我的证书已过期;需要续订” 之类的话。
这就是在互联网上进行私密对话所需的高级知识。 这是一个非常恶劣的环境,TLS 在解决这个问题,同时也处理了我没有涵盖的一系列边缘情况,例如 前向保密 和各种 数据/时序攻击。
如果你对它的工作原理感到好奇,我真的建议从浏览诸如 Monocypher 和 TweetNaCl 之类的工具开始。 它们为诸如密钥交换、AEAD 和签名之类的底层位提供实现。 之后,查看 Julia Evans 的 TLS 1.3 的玩具版本 或 xargs 的 The Illustrated TLS 1.3 Connection,了解协议如何将它们拼接在一起。 似乎没有很多关于 TLS 的学习资源(主要是在 TLS 1.2 上比较优缺点的规范和营销资料)。 如果有其他资源,请告诉我。
protty
- protty