原文:annas-archive.org/md5/655c944001312f47533514408a1a919a
译者:飞龙
协议:CC BY-NC-SA 4.0
第六章:非对称加密和混合加密
本章内容包括
对秘密信息进行加密的非对称加密方法
对数据进行加密到公钥的混合加密方法
非对称和混合加密的标准
在第四章中,您了解到了认证加密,这是一种用于加密数据的加密原语,但受到对称性的限制(连接的两侧必须共享相同的密钥)。在本章中,我将通过介绍非对称加密来解除此限制,这是一种加密到其他人的密钥而无需知道密钥的原语。毫不奇怪,非对称加密利用密钥对,加密将使用公钥而不是对称密钥。
在本章的中间部分,您将看到非对称加密受其可以加密的数据量以及加密速率的限制。为了消除这一障碍,我将向您展示如何将非对称加密与认证加密混合在一起,形成我们所称的混合加密 。让我们开始吧!
注意 对于本章,您需要已经阅读过第四章关于认证加密和第五章关于密钥交换。
6.1 什么是非对称加密?
了解如何加密消息的第一步是理解非对称加密 (也称为公钥加密 )。在本节中,您将了解此加密原语及其属性。让我们看一个以下真实场景:加密电子邮件 。
您可能知道,您发送的所有电子邮件都是"明文"发送的,任何坐在您和您收件人的电子邮件提供商之间的人都可以阅读。这不太好。你该怎么解决这个问题?您可以使用像 AES-GCM 这样的加密原语,这是您在第四章学到的。为此,您需要为想要给您发消息的每个人设置一个不同的共享对称密钥。
练习
使用相同的共享密钥与所有人将非常糟糕;您能理解为什么吗?
但是您不能指望提前知道谁会给您发送消息,随着越来越多的人想要给您加密消息,生成和交换新的对称密钥会变得繁琐。这就是非对称加密的帮助所在,它允许拥有您公钥的任何人向您加密消息。此外,您是唯一能够使用您拥有的相关私钥解密这些消息的人。请参见图 6.1,了解非对称加密的示意图。
图 6.1 使用非对称加密,任何人都可以使用爱丽丝的公钥向她发送加密消息。只有拥有相关私钥的爱丽丝才能解密这些消息。
要设置非对称加密,首先需要通过某种算法生成一对密钥。与任何加密算法的设置函数一样,密钥生成算法接受一个安全参数。这个安全参数通常被翻译为"你想要多大的密钥?"更大意味着更安全。图 6.2 说明了这一步骤。
图 6.2 要使用非对称加密,首先需要生成一对密钥。根据您提供的安全参数,您可以生成不同安全强度的密钥。
密钥生成算法 生成由两个不同部分组成的密钥对:公钥部分(如名称所示)可以在不太担心的情况下发布和共享,而私钥必须保持秘密。与其他加密原语的密钥生成算法类似,需要一个安全参数来决定算法的位安全性。然后任何人都可以使用公钥部分加密消息,您可以使用私钥部分解密,就像图 6.3 所示。与经过认证的解密类似,如果提供不一致的密文,解密可能会失败。
图 6.3 非对称加密允许使用接收者的公钥加密消息(明文 )。接收者随后可以使用与先前使用的公钥相关的私钥使用不同的算法解密加密的消息(密文 )。
请注意,到目前为止我们还没有讨论认证问题。考虑电线的两侧:
现在,我们将假设我们以一种非常安全的方式获得了 Alice 的公钥。在涵盖数字签名的第七章中,您将了解现实世界协议如何解决这个实践中的引导问题。您还将在第七章中学习如何以加密方式向 Alice 传达您的真实身份。剧透警告:您将签署您的消息。
让我们继续下一节,您将了解非对称加密在实践中的应用(以及为什么在实践中很少直接使用)。
6.2 实践中的非对称加密和混合加密
您可能认为非对称加密可能足以开始加密您的电子邮件。实际上,由于它可以加密的消息长度受限,非对称加密相当受限。与对称加密相比,非对称加密和解密的速度也较慢。这是由于非对称构造实施数学运算,而对称原语通常只是操作位。
在本节中,你将了解这些限制,实际上非对称加密用于什么,最后,密码学是如何克服这些障碍的。本节分为两个部分,分别介绍了非对称加密的两个主要用例:
6.2.1 密钥交换和密钥封装
原来非对称加密可以用于执行密钥交换------与我们在第五章中看到的一样!为了做到这一点,你可以开始生成一个对称密钥,并用 Alice 的公钥对其进行加密------我们也称之为 封装密钥 ------就像图 6.4 所示。
图 6.4 要将非对称加密用作密钥交换原语,你需要(1)生成一个对称密钥,然后(2)用 Alice 的公钥对其进行加密。
你随后可以将密文发送给 Alice,她将能够解密它并学习对称密钥。接下来,你们将有一个共享的秘密!图 6.5 展示了完整的流程。
图 6.5 要将非对称加密用作密钥交换原语,你可以(1)生成一个对称密钥,然后(2)用 Alice 的公钥对其进行加密。之后(3)将其发送给 Alice,她可以(4)用她关联的私钥对其进行解密。在协议结束时,你们都拥有共享的秘密,而其他人无法仅从观察到的加密对称密钥中推导出它。
使用非对称加密执行密钥交换通常使用一种称为 RSA 的算法(按照其发明者 Rivest、Shamir 和 Adleman 的名字命名),并在许多互联网协议中使用。今天,RSA 通常不是 进行密钥交换的首选方式,并且在协议中的使用越来越少,而更偏爱椭圆曲线 Diffie-Hellman(ECDH)。这主要是出于历史原因(发现了许多与 RSA 实现和标准相关的漏洞)和 ECDH 提供的更小参数大小的吸引力。
6.2.2 混合加密
实际上,非对称加密只能加密长度不超过一定限制的消息。例如,可以通过 RSA 加密的明文消息的大小受到生成密钥对时使用的安全参数的限制(更具体地说,是模数的大小)。现今,使用的安全参数(4,096 位模数),限制约为 500 个 ASCII 字符 ------ 相当小。因此,大多数应用程序使用混合加密,其限制与使用的认证加密算法的加密限制相关联。
混合加密在实践中与非对称加密具有相同的接口(见图 6.6)。人们可以使用公钥加密消息,拥有相关私钥的人可以解密加密的消息。真正的区别在于您可以加密的消息的大小限制。
图 6.6 混合加密与非对称加密具有相同的接口,只是可以加密的消息大小要大得多。
在幕后,混合加密只是一个非对称 加密原语与一个对称 加密原语的结合(因此得名)。具体来说,它是与接收者进行的非交互式密钥交换,然后使用经过身份验证的加密算法加密消息。
警告 您也可以使用简单的对称加密原语,而不是经过身份验证的加密原语,但对称加密无法防止有人篡改您的加密消息。这就是为什么在实践中我们从不单独使用对称加密的原因(如第四章所示)。
让我们了解一下混合加密的工作原理!如果您想将消息加密给爱丽丝,您首先生成一个对称密钥并使用它加密您的消息,然后使用一个经过身份验证的加密算法,正如图 6.7 所示。
图 6.7 使用混合加密和非对称加密将消息加密给爱丽丝,您(1)为经过身份验证的加密算法生成对称密钥,然后您(2)使用对称密钥将消息加密给爱丽丝。
一旦您加密了您的消息,爱丽丝仍然无法在不知道对称密钥的情况下解密它。我们如何向爱丽丝提供对称密钥?使用爱丽丝的公钥对对称密钥进行非对称加密,就像图 6.8 中所示的那样。
图 6.8 在图 6.7 的基础上,(3)你使用爱丽丝的公钥和非对称加密算法加密对称密钥本身。
最后,你可以将两个结果都发送给爱丽丝。这些包括
这对于爱丽丝解密消息已经足够了。我在图 6.9 中详细说明了整个流程。
图 6.9 在图 6.8 的基础上,(4)在你将加密的对称密钥和加密的消息都发送给爱丽丝后,(5)爱丽丝使用她的私钥解密对称密钥。(6)然后她使用对称密钥解密加密的消息。(请注意,如果在步骤 4 时通信被中间人攻击者篡改,步骤 5 和 6 都可能失败并返回错误。)
这就是我们如何利用两者之间的最佳特性:将非对称加密和对称加密混合以向公钥加密大量数据。我们通常将算法的第一个非对称部分称为密钥封装机制 (KEM),将第二个对称部分称为数据封装机制 (DEM)。
在我们转向下一节并学习存在的不同算法和标准以及非对称加密和混合加密的方法之前,让我们看看(实践中)如何使用加密库执行混合加密。为此,我选择了 Tink 加密库。Tink 是由 Google 的一组密码学家开发的,以支持公司内外的大型团队。由于项目的规模,进行了有意识的设计选择,并暴露了健全的功能,以防止开发人员误用密码原语。此外,Tink 可在几种编程语言中使用(Java、C++、Obj-C 和 Golang)。
列表 6.1 Java 中的混合加密
go
复制代码
import com.google.crypto.tink.HybridDecrypt;
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.hybrid.HybridKeyTemplates
➥ .ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM;
import com.google.crypto.tink.KeysetHandle;
KeysetHandle privkey = KeysetHandle.generateNew( // ❶
ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM); // ❶
KeysetHandle publicKeysetHandle = // ❷
privkey.getPublicKeysetHandle(); // ❷
HybridEncrypt hybridEncrypt = // ❸
publicKeysetHandle.getPrimitive( // ❸
HybridEncrypt.class); // ❸
byte[] ciphertext = hybridEncrypt.encrypt( // ❸
plaintext, associatedData); // ❸
HybridDecrypt hybridDecrypt = // ❹
privkey.getPrimitive(HybridDecrypt.class); // ❹
byte[] plaintext = hybridDecrypt.decrypt( // ❹
ciphertext, associatedData); // ❹
❶ 为特定混合加密方案生成密钥
❷ 获取我们可以发布或共享的公钥部分
❸ 任何知道此公钥的人都可以用它加密明文,并可以验证一些关联数据。
❹ 使用相同的关联数据解密加密消息。如果解密失败,它会抛出异常。
为了帮助你理解ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM
字符串:ECIES(椭圆曲线集成加密方案)是要使用的混合加密标准。你将在本章后面学到这一点。字符串的其余部分列出了用于实例化 ECIES 的算法:
P256 是你在第五章学到的 NIST 标准化椭圆曲线。
HKDF 是一个密钥派生函数,你将在第八章学习它。
HMAC 是你在第三章学到的消息认证码。
SHA-256 是你在第二章学到的哈希函数。
AES-128-GCM 是你在第四章学到的使用 128 位密钥的 AES-GCM 验证加密算法。
看到一切是如何开始拼凑在一起的了吗?在下一节中,你将学习 RSA 和 ECIES,这两种广泛采用的非对称加密和混合加密标准。
6.3 使用 RSA 进行非对称加密:好的和不那么好的
是时候让我们来看一下在实践中定义了非对称加密和混合加密的标准了。在历史上,这两个原语都未能幸免于密码分析家的手,许多漏洞和弱点都被发现在这些标准和实现中。这就是为什么我将从介绍 RSA 非对称加密算法及其不正确使用方式开始这一节。本章的其余部分将介绍你可以遵循的实际标准来使用非对称和混合加密:
6.3.1 教科书 RSA
在本节中,你将了解 RSA 公钥加密算法及其在多年来的标准化。这对理解基于 RSA 的其他安全方案很有用。
不幸的是,自从 1977 年首次发布以来,RSA 一直受到了相当大的诟病。流行的理论之一是 RSA 太容易理解和实现,因此许多人自行实施,这导致了许多易受攻击的实现。这是一个有趣的想法,但它没有抓住整个故事的要点。尽管 RSA 的概念(通常称为教科书 RSA )如果被天真地实现是不安全的,但甚至标准也被发现是不安全的!但是不要那么快,要理解这些问题,您首先需要了解 RSA 的工作原理。
还记得模素数p 的乘法群吗?(我们在第五章中谈论过。)它是严格正整数的集合:
1, 2, 3, 4, ···, p -- 1
让我们假设其中一个数字是我们的消息。对于足够大的p ,比如 4,096 位,我们的消息最多可以包含约 500 个字符。
注意对于计算机来说,一条消息只是一系列字节,也可以解释为一个数字。
我们已经看到通过对一个数字进行幂运算(比如我们的消息),我们可以生成其他形成一个子群 的数字。我在图 6.10 中进行了说明。
图 6.10 对于模素数(这里为 5)的整数被划分为不同的子群。通过选择一个元素作为生成器(假设是数字 2)并对其进行指数运算,我们可以生成一个子群。对于 RSA,生成器就是消息。
当我们定义如何使用 RSA 加密时,这对我们很有用。为此,我们发布一个公共指数e (用于加密 )和一个素数p 。(实际上p 不能是素数,但我们暂时忽略这一点。)要加密一个消息m ,需要计算
密文 = m ^e mod p
例如,要使用e = 2 和p = 5 加密消息m = 2,我们计算
密文 = 2² mod 5 = 4
这就是使用 RSA 加密的理念背后的想法!
注意通常情况下,会选择一个小的数作为公共指数e ,以便加密速度更快。从历史上看,标准和实现似乎已经确定了素数 65,537 作为公共指数。
太棒了!现在你有了一种让人们向你加密消息的方法。但是如何解密呢?记住,如果你继续对一个生成器进行幂运算,你实际上会回到原始数字(见图 6.11)。
图 6.11 假设我们的消息是数字 2。通过对其进行幂运算,我们可以获得我们群中的其他数字。如果我们对其进行足够多次幂运算,我们将回到我们的原始消息 2。我们称该群是循环的 。这个属性可以用来在将消息提升到某个幂之后恢复消息。
这应该让你有一个实现解密的思路:找出你需要对密文进行多少次幂运算才能恢复原始生成器(即消息)。假设你知道这样一个数字,我们将其称为私有指数 d (d 表示解密 )。如果你收到
密文 = 消息^e mod p
你应该能够将其提升到幂次d 以恢复消息:
密文^d = (消息e) d = 消息^(e ×d ) = 模p 的消息
找到这个私有指数d 的实际数学有点棘手。简单来说,你计算群的阶(元素数量)对公共指数取模的逆元:
d = e ^(--1) mod order
我们有一个有效的算法来计算模反函数(如扩展欧几里得算法),所以这不是问题。不过我们有另一个问题!对于一个素数p ,阶很简单,就是p -- 1,因此,任何人都可以很容易地计算出私有指数 。这是因为除了d 之外,这个方程中的每个元素都是公开的。
欧拉的定理
我们如何得到前述方程以计算私有指数d ?欧拉定理说明,对于与p 互质的m (意味着它们没有公共因数):
m ^(order) = 1 mod p
对于order ,即整数对p 取模创建的乘法群中的元素数。这又意味着,对于任何整数multiple
m (1+) (multiple ×order ) = m × (m (order)) (multiple) mod p = m mod p
这告诉我们我们要解决的方程
m ^(e × d) = m mod p
可以简化为
e × d = 1 + multiple × order
这可以重写为
e × d = 1 mod order
这意味着d 是模order 下的e 的逆元。
我们可以防止他人从公共指数计算私有指数的一种方法是隐藏我们群的阶。这是 RSA 背后的精妙思想:如果我们的模数不再是一个素数而是一个素数p × q 的乘积(其中p 和q 是只有你知道的大素数),那么我们的乘法群的阶就不容易计算,只要我们不知道p 和q !
RSA 群的阶
你可以用欧拉的欧拉函数ϕ(N )计算模数N 的乘法群的阶,它返回与N 互质的数字的计数。例如,5 和 6 是互质的,因为唯一能够同时整除它们的正整数是 1。另一方面,10 和 15 不是,因为 1 和 5 分别能整除它们。对于 RSA 模数N = p × q 的乘法群的阶是
ϕ(N ) = (p -- 1) × (q -- 1)
这太难计算了,除非你知道N 的因数。
我们都搞定了!总结一下,这就是 RSA 的工作原理:
用于密钥生成
生成两个大素数p 和q 。
选择一个随机的公共指数e 或一个固定的像e = 65537 这样的。
你的公钥是公共指数e 和公共模数N = p × q 。
求得你的私有指数d = e ^(--1) mod (p -- 1) (q -- 1)。
你的私钥是私有指数d 。
用于加密,计算消息^e mod N 。
用于密文的解密,计算密文^d mod N 。
图 6.12 回顾了 RSA 如何在实践中工作。
图 6.12 RSA 加密通过使用公共指数 e 对消息进行模公共模数 N = p × q 进行指数运算。RSA 解密通过使用私有指数 d 对加密数字进行模公共模数 N 进行指数运算。
我们说 RSA 依赖于因子分解问题 。没有 p 和 q 的知识,没有人可以计算出顺序;因此,除了你之外,没有人可以从公共指数计算出私有指数。这类似于迪菲-赫尔曼基于离散对数问题的方式(见图 6.13)。
图 6.13 迪菲-赫尔曼(DH)、椭圆曲线迪菲-赫尔曼(ECDH)和 RSA 是依赖于数学中三个我们认为难以解决的问题的非对称算法。难以解决 意味着我们不知道如何用大数实例化时解决它们的高效算法。
因此,教科书上的 RSA 在一个复合数 N = p × q 上运行,其中 p 和 q 是两个需要保持秘密的大素数。现在你了解了 RSA 的工作原理,让我们看看它在实践中有多不安全以及标准如何使其安全。
6.3.2 为什么不使用 RSA PKCS#1 v1.5
你了解了"教科书上的 RSA",由于许多原因,默认情况下是不安全的。在学习 RSA 的安全版本之前,让我们看看你需要避免的内容。
有许多原因导致你不能直接使用教科书上的 RSA。一个例子是,如果你加密小消息(例如 m = 2),那么一些恶意行为者可以加密 0 到 100 之间的所有小数字,然后迅速观察他们的加密数字是否与你的密文匹配。如果匹配,他们将知道你加密了什么。
标准通过使你的消息变得过大,以至于无法以这种方式暴力破解来解决这个问题。具体来说,它们通过添加一个 非确定性 填充来最大化消息(加密前)的大小。例如,RSA PKCS#1 v1.5 标准定义了一个填充,向消息添加一些随机字节。我在图 6.14 中进行了说明。
图 6.14 RSA PKCS#1 v1.5 标准指定了在加密之前应用于消息的填充。填充必须是可逆的(以便解密可以去除它),并且必须向消息添加足够的随机字节,以避免暴力破解攻击。
PKCS#1 标准实际上是基于 RSA 的第一个标准,是 RSA 公司在 90 年代初撰写的一系列公钥密码标准(PKCS)文件的一部分。尽管 PKCS#1 标准修复了一些已知问题,但在 1998 年,Bleichenbacher 发现了对 PKCS#1 v1.5 的实际攻击,允许攻击者解密使用标准指定的填充加密的消息。由于需要百万条消息,因此这个攻击被恶名昭彰地称为百万消息攻击 。后来找到了一些缓解方法,但有趣的是,多年来,攻击一再被重新发现,因为研究人员发现这些缓解方法过于难以安全地实现(如果可能的话)。
自适应选择密文攻击
Bleichenbacher 的百万消息攻击是理论密码学中一种称为自适应选择密文攻击 (CCA2)的攻击类型。CCA2 意味着为了执行此攻击,攻击者可以提交任意的 RSA 加密消息(选择密文 ),观察它如何影响解密,并根据先前的观察继续攻击(自适应 部分)。CCA2 经常用于模拟密码学安全证明中的攻击者。
要理解攻击为何可能,您需要了解 RSA 密文是可塑 的:您可以篡改 RSA 密文而不使其解密无效。如果我观察到密文 c = m ^e mod N ,那么我可以提交以下密文:
3^e × m ^e = (3m )^e mod N
解密结果将为
((3m )e) d = (3m )^(e ×d ) = 3m mod N
我在这里以数字 3 作为示例,但我可以用任意数字乘以原始消息。在实践中,消息必须格式良好(由于填充),因此,篡改密文应该会破坏解密。然而,有时,即使在恶意修改之后,解密后仍然接受填充。
Bleichenbacher 在他对 RSA PKCS#1 v1.5 的百万消息攻击中利用了这个属性。他的攻击是通过截获加密消息,修改它,并发送给负责解密的人。通过观察那个人是否能解密它(填充仍然有效),我们可以获得关于消息范围的一些信息。因为前两个字节是 0x0002,所以我们知道解密结果小于某个值。通过迭代执行此操作,我们可以将该范围缩小到原始消息本身。
尽管 Bleichenbacher 攻击是众所周知的,但今天仍然有许多系统使用 RSA PKCS#1 v1.5 进行加密。作为安全顾问工作的一部分,我发现许多应用程序容易受到此攻击的影响------所以要小心!
6.3.3 使用 RSA-OAEP 进行非对称加密
1998 年,同一 PKCS#1 标准的 2.0 版本发布了一个名为Optimal Asymmetric Encryption Padding (OAEP)的 RSA 新填充方案。与其前身 PKCS#1 v1.5 不同,OAEP 不容易受到 Bleichenbacher 的攻击,因此是目前用于 RSA 加密的强标准。让我们看看 OAEP 是如何工作并防止先前讨论的攻击。
首先,让我们提到,像大多数加密算法一样,OAEP 带有一个密钥生成算法。这需要一个安全参数,如图 6.15 所示。
图 6.15 RSA-OAEP,像许多公钥算法一样,首先需要生成一个密钥对,以便后来在加密原语提供的其他算法中使用。
此算法需要一个安全参数,即位数。与 Diffie-Hellman 一样,操作发生在模一个大数的数字集合中。当我们谈论 RSA 的一个实例的安全性时,我们通常指的是那个大模数的大小。如果你记得的话,这与 Diffie-Hellman 类似。
目前,大多数指南(参见keylength.com
)估计模数在 2,048 到 4,096 位之间,以提供 128 位安全性。由于这些估计相当不同,大多数应用程序似乎保守地选择了 4,096 位参数。
注意我们看到 RSA 的大模数不是一个素数,而是两个大素数p 和q 的乘积N = p × q 。对于 4,096 位模数,密钥生成算法通常将事情一分为二,并生成大约 2,048 位大小的p 和q 。
加密时,算法首先对消息进行填充,并与每次加密生成的随机数混合。然后使用 RSA 对结果进行加密。解密密文时,过程与图 6.16 所示相反。
图 6.16 RSA-OAEP 通过在加密之前将消息与随机数混合来工作。混合可以在解密后恢复。在算法的中心,使用掩码生成函数(MGF)来随机化和扩大或缩小输入。
RSA-OAEP 使用这种混合方式,以确保如果 RSA 加密的几位泄漏,就无法获取有关明文的任何信息。实际上,要撤销 OAEP 填充,您需要获取(接近)OAEP 填充明文的所有字节!此外,Bleichenbacher 的攻击不应再起作用,因为该方案使得通过修改密文无法获得格式良好的明文。
注意明文感知性 是一种属性,使得攻击者很难创建一个成功解密的密文(当然没有加密的帮助)。由于 OAEP 提供的明文感知性,Bleichenbacher 的攻击对该方案不起作用。
在 OAEP 内部,MGF 代表掩码生成函数 。在实践中,MGF 是一个可扩展输出函数(XOF);你在第二章已经了解了 XOF。由于 MGF 是在 XOF 之前发明的,因此它们是使用散列函数反复散列输入与计数器的输入来构建的(见图 6.17)。这就是 OAEP 的工作原理!
图 6.17 掩码生成函数(MGF)只是一个接受任意长度输入并产生随机外观任意长度输出的函数。它通过对输入和计数器进行散列,将摘要连接在一起,并截断结果以获得所需长度来工作。
Manger 的填充预言攻击
OAEP 标准发布仅三年后,James Manger 发现了一个与 Bleichenbacher 的百万消息攻击类似但更加实用的 OAEP 定时攻击,如果实现不正确的话。幸运的是,与 PKCS#1 v1.5 相比,安全地实现 OAEP 要简单得多,并且对该方案实现中的漏洞要少得多。
此外,OAEP 的设计并不完美;多年来已经提出并标准化了更好的构造。一个例子是 RSA-KEM,它具有更强的安全性证明,并且要安全地实现要简单得多。您可以观察到设计在图 6.18 中更加优雅。
图 6.18 RSA-KEM 是一种通过简单地使用 RSA 加密随机数来工作的加密方案。不需要填充。我们可以通过密钥派生函数(KDF)传递随机数以获得对称密钥。然后,我们使用对称密钥通过身份验证加密算法加密消息。
注意这里使用的密钥派生函数(KDF)。这是另一个可以用 MGF 或 XOF 替换的加密原语。我将在第八章关于随机性和机密性中更多地谈到 KDF 是什么。
如今,大多数使用 RSA 的协议和应用程序仍然实现不安全的 PKCS#1 v1.5 或 OAEP。另一方面,越来越多的协议正在摒弃 RSA 加密,转而采用椭圆曲线 Diffie-Hellman(ECDH)进行密钥交换和混合加密。这是可以理解的,因为 ECDH 提供更短的公钥,并且通常从更好的标准和更安全的实现中受益。
6.4 使用 ECIES 进行混合加密
虽然存在许多混合加密方案,但最广泛采用的标准是椭圆曲线整合加密方案 (ECIES)。该方案已被指定用于与 ECDH 一起使用,并包含在许多标准中,如 ANSI X9.63,ISO/IEC 18033-2,IEEE 1363a 和 SECG SEC 1。不幸的是,每个标准似乎都实现了不同的变体,并且不同的加密库以不同的方式实现混合加密,部分原因是如此。
出于这个原因,在野外我很少看到两个相似的混合加密实现。重要的是要理解,虽然这很烦人,但如果协议的所有参与者使用相同的实现或记录了他们实现的混合加密方案的详细信息,那么就不会有问题。
ECIES 的工作方式与第 6.2 节中解释的混合加密方案类似。不同之处在于,我们使用 ECDH 密钥交换实现了 KEM 部分,而不是使用非对称加密原语。让我们逐步解释这一点。
首先,如果你想将消息加密给 Alice,你使用基于(EC)DH 的密钥交换与 Alice 的公钥以及你为此生成的密钥对(这称为临时密钥对 )。然后你可以使用获得的共享秘密与像 AES-GCM 这样的认证对称加密算法加密一个更长的消息给她。图 6.19 说明了这一点。
图 6.19 使用混合加密将消息加密给 Alice,使用(EC)DH,你(1)生成一个临时(椭圆曲线)DH 密钥对。然后(2)使用你的临时私钥和 Alice 的公钥进行密钥交换。(3)使用生成的共享秘密作为对称密钥,使用认证加密算法加密你的消息。
之后,你可以将临时公钥和密文发送给 Alice。Alice 可以使用你的临时公钥与自己的密钥对进行密钥交换。然后她可以使用结果来解密密文并恢复原始消息。结果要么是原始消息,要么是错误,如果公钥或加密消息在传输中被篡改。图 6.20 说明了完整的流程。
图 6.20 在图 6.19 的基础上构建,(4)在你将你的临时公钥和你的加密消息发送给 Alice 后,(5)Alice 可以使用她的私钥和你的临时公钥进行密钥交换。(6)最后,她使用生成的共享秘密作为对称密钥,使用相同的认证加密算法解密加密消息。
这基本上就是 ECIES 的工作原理。还有一种使用 Diffie-Hellman 的 ECIES 变体,称为 IES,工作方式基本相同,但似乎没有多少人使用它。
消除密钥交换输出中的偏差
注意,我简化了图 6.20。大多数认证加密原语期望一个均匀随机的对称密钥。因为密钥交换的输出通常不 是均匀随机的,所以我们需要先通过 KDF 或 XOF(在第二章中见过)传递共享秘密。你将在第八章中了解更多相关内容。
这里的 不是均匀随机*意味着从统计上看,密钥交换结果的某些比特可能更多地是 0,或者相反。例如,前几位可能总是被设置为 0。
练习
你看出为什么不能立即使用密钥交换输出了吗?
这就是你可以使用的不同标准。在下一章中,你将学习关于签名的内容,这将是第一部分中最后,也许是最重要的公钥密码算法。
摘要
我们很少使用非对称加密直接加密消息。这是因为非对称加密可以加密的数据相对较小。
混合加密可以通过将非对称加密(或密钥交换)与对称认证加密算法结合来加密更大的消息。
RSA PKCS#1 版本 1.5 标准用于非对称加密在大多数情况下已经被破解。建议使用在 RSA PKCS#1 版本 2.2 中标准化的 RSA-OAEP 算法。
ECIES 是最广泛使用的混合加密方案。由于其参数大小和对坚实标准的依赖,它比基于 RSA 的方案更受青睐。
不同的加密库可能以不同的方式实现混合加密。如果可互操作的应用程序使用相同的实现,这在实践中并不是问题。
第七章:签名和零知识证明
本章包括
零知识证明和数字签名
密码签名的现有标准
签名的微妙行为和避免它们的陷阱
你即将学到一种最普遍和最强大的密码原语------数字签名。简单来说,数字签名类似于你习惯的现实生活中的签名,就像你在支票和合同上写的那种。当然,数字签名是密码学的,所以它们提供比纸笔等价物更多的保证。
在协议的世界里,数字签名解锁了许多不同的可能性,你将会在本书的第二部分中反复遇到它们。在这一章中,我将介绍这个新原语是什么,它如何在现实世界中使用,以及现代数字签名标准是什么。最后,我将谈论安全考虑和使用数字签名的危险。
注:在密码学中,签名经常被称为数字签名 或签名方案 。在本书中,我会交替使用这些术语。
对于本章,你需要阅读
第二章关于哈希函数
第五章关于密钥交换
第六章关于非对称加密
7.1 什么是签名?
我在第一章解释过,密码签名基本上就像现实生活中的签名一样。因此,它们通常是最直观的密码原语之一:
只有你可以使用你的签名来签署任意消息。
任何人都可以验证你在消息上的签名。
因为我们处于非对称密码学的领域,你可能已经猜到了这种不对称性会如何发挥作用。一个签名方案 通常由三种不同的算法组成:
一个签名者用来创建新的私钥和公钥的密钥对生成算法(然后可以将公钥与任何人分享)。
一个签名算法,它接受一个私钥和一个消息,然后产生一个签名。
一个验证算法,它接受一个公钥、一个消息和一个签名,并返回一个成功或错误的消息。
有时私钥也被称为签名密钥 ,公钥被称为验证密钥 。有道理吧?我在图 7.1 中总结了这三个算法。
图 7.1 数字签名的接口。像其他公钥密码算法一样,你首先需要通过一个接受安全参数和一些随机性的密钥生成算法生成密钥对。然后你可以使用一个带有私钥的签名算法对消息进行签名,并使用带有公钥的验证算法验证消息上的签名。如果你没有访问其关联私钥,你就无法伪造一个验证公钥的签名。
签名有什么用?它们用于验证消息的来源以及消息的完整性:
注意:虽然这两个属性与认证相关联,但通常被区分为两个单独的属性:原始认证 和 消息认证 (或完整性)。
从某种意义上说,签名类似于第三章中您了解到的消息认证码(MACs)。但与 MAC 不同的是,它们允许我们对消息进行非对称认证:参与者可以验证消息未被篡改,而无需私钥或签名密钥的知识。接下来,我将向您展示这些算法如何在实践中使用。
练习
正如您在第三章中看到的那样,MAC 生成的认证标签必须以恒定时间验证,以避免时间攻击。您认为我们需要对验证签名做同样的事情吗?
7.1.1 如何在实践中签名和验证签名
让我们看一个实际的例子。为此,我使用了 pyca/cryptography(cryptography.io
),一个广受尊敬的 Python 库。以下清单简单地生成一个密钥对,使用私钥部分签名消息,然后使用公钥部分验证签名。
代码清单 7.1 在 Python 中签名和验证签名
go
复制代码
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey // ❶
)
private_key = Ed25519PrivateKey.generate() // ❷
public_key = private_key.public_key() // ❷
message = b"example.com has the public key 0xab70..." // ❸
signature = private_key.sign(message) // ❸
try: // ❹
public_key.verify(signature, message) // ❹
print("valid signature") // ❹
except InvalidSignature: // ❹
print("invalid signature") // ❹
❶ 使用 Ed25519 签名算法,这是一种流行的签名方案
❷ 首先生成私钥,然后生成公钥
❸ 使用私钥对消息进行签名并获得签名
❹ 使用公钥验证消息上的签名
正如我之前所说,数字签名在现实世界中解锁了许多用例。让我们在下一节中看一个例子。
7.1.2 签名的主要用例:认证密钥交换
第 5 和 6 章介绍了两个参与者之间执行密钥交换的不同方法。在同一章节中,您了解到这些密钥交换对于协商一个共享密钥是有用的,然后可以使用该密钥来使用经过身份验证的加密算法来保护通信。然而,密钥交换并未完全解决在两个参与者之间建立安全连接的问题,因为主动的中间人(MITM)攻击者可以轻易地冒充密钥交换的双方。这就是签名的用武之地。
假设 Alice 和 Bob 正试图在它们之间建立安全通信渠道,并且 Bob 知道 Alice 的验证密钥。知道这一点,Alice 可以使用她的签名密钥来认证她的密钥交换的一面:她生成一个密钥交换密钥对,用她的签名密钥对公钥部分进行签名,然后发送密钥交换的公钥以及签名。Bob 可以使用他已经知道的关联验证密钥验证签名是否有效,然后使用密钥交换的公钥执行密钥交换。
我们称这样的密钥交换为身份验证密钥交换 。如果签名无效,鲍勃可以知道有人正在积极地中间人攻击密钥交换。我在图 7.2 中说明了身份验证密钥交换。
图 7.2 第一张图片(顶部)代表了一个未经身份验证的密钥交换,这对于一个可以轻松伪装成交换双方的主动中间人攻击者来说是不安全的,因为他可以用自己的公钥与双方交换公钥。第二张图片(底部)代表了一个密钥交换的开始,通过爱丽丝对她的公钥签名来进行身份验证。由于被主动中间人攻击者篡改了消息,鲍勃(知道爱丽丝的验证密钥)无法验证签名,于是他中止了密钥交换。
请注意,在此示例中,密钥交换只在一侧进行了身份验证:尽管爱丽丝无法被冒充,但鲍勃可以。如果双方都经过了身份验证(鲍勃会签署他的密钥交换的一部分),我们称这种密钥交换为相互身份验证密钥交换 。签署密钥交换可能看起来并不是很有用。我们似乎是把事先不知道爱丽丝的密钥交换公钥的问题转移到了事先不知道她的验证密钥的问题上。下一节将介绍身份验证密钥交换的一个实际应用,这将更容易理解。
7.1.3 实际应用:公钥基础设施
如果您假设信任是传递 的,签名就会变得更加强大。我的意思是,如果您信任我,我信任爱丽丝,那么您就可以信任爱丽丝。她很酷。
信任的传递允许您以极端的方式扩展系统中的信任。想象一下,您对某个权威及其验证密钥有信心。此外,想象一下,这个权威已经签署了指示查尔斯公钥是什么、戴维公钥是什么等消息。然后,您可以选择相信这个映射!这样的映射称为公钥基础设施 。例如,如果您尝试与查尔斯进行密钥交换,并且他声称他的公钥是一个看起来像 3848... 的大数,您可以通过检查您"心爱的"权威是否已签署了类似"查尔斯的公钥是 3848..."的消息来验证。
这个概念的一个现实应用是网络公钥基础设施 (web PKI)。Web PKI 是您的网络浏览器用来验证其与您每天访问的众多网站执行的密钥交换的机制。Web PKI 的简化解释(如图 7.3 所示)如下:当您下载浏览器时,它会带有一些验证密钥嵌入到程序中。这个验证密钥与一个权威机构相关联,其责任是签署成千上万个网站的公钥,以便您可以信任这些而不必了解它们。您看不到的是这些网站必须向权威机构证明他们真正拥有自己的域名,然后才能获得对其公钥的签名。(实际上,您的浏览器信任许多权威机构来执行这项工作,而不仅仅是一个。)
图 7.3 在网络 PKI 中,浏览器信任一个权威机构来证明某些域名与某些公钥相关联。当安全访问网站时,您的浏览器可以通过验证来自权威机构的签名来验证网站的公钥确实属于他们自己(而不是来自某个中间人)。
在本节中,您从高层次的角度了解了签名。让我们深入了解签名的实际工作原理。但是为此,我们首先需要绕个弯,看看称为零知识证明(ZKP)的东西。
7.2 零知识证明(ZKP):签名的起源
理解密码学中签名工作原理的最佳方法是了解它们的来源。因此,让我们花点时间简要介绍 ZKP,然后我会回到签名。
想象一下,佩姬想向维克多证明某事。例如,她想证明自己知道某个群元素的离散对数。换句话说,她想证明自己知道x ,给定Y = g ^x,其中g 是某个群的生成元。
当然,最简单的解决方案是佩姬简单地发送值x (称为见证 )。这个解决方案将是一个简单的知识证明 ,这样就可以了,除非佩姬不希望维克多知道它。
注意 在理论上,我们说用于生成证明的协议如果完备 ,那么佩姬可以使用它向维克多证明她知道见证。如果她无法使用它证明自己所知,那么这个方案就是无用的,对吧?
在密码学中,我们主要关注不向验证者泄露见证的知识证明。这样的证明被称为零知识证明 (ZKP)。
7.2.1 Schnorr 身份验证协议:一个交互式零知识证明
在接下来的页面中,我将逐步从破损的协议构建一个 ZKP,以向您展示爱丽丝如何证明她知道x 而不泄露x 。
在密码学中解决这种问题的典型方法是用一些随机性"隐藏"这个值(例如,通过加密)。但我们不仅仅是隐藏:我们还想证明它是存在的。为此,我们需要一种代数方法来隐藏它。一个简单的解决方案是简单地将一个随机生成的值 k 添加到证人中。
s = k + x
佩姬随后可以将隐藏的证人 s 与随机值 k 一起发送给维克多。此时,维克多没有理由相信佩姬确实将证人隐藏在 s 中。实际上,如果她不知道证人 x ,那么 s 可能只是一些随机值。维克多知道的是,证人 x 正隐藏在 g 的指数中,因为他知道 Y = g ^x。
为了确定佩姬是否真的知道这个证人,维克多可以检查她给他的东西是否与他所知的相匹配,这也必须在 g 的指数中进行(因为这是证人所在的地方)。换句话说,维克多检查这两个数字是否相等:
思路是只有知道证人 x 的人才能构造出满足这个方程的"蒙眼"证人 s 。因此,这是一种知识证明。我在图 7.4 中重述了这个零知识证明系统。
图 7.4 为了向维克多证明她知道证人 x ,佩姬隐藏它(通过将其添加到随机值 k )并发送隐藏的证人 s 。
不要那么快。这个方案有一个问题------显然不安全!实际上,由于隐藏证人 x 的方程只有一个未知数(x 本身),维克多可以简单地反转方程以检索证人:
x = s -- k
为了解决这个问题,佩姬可以隐藏随机值 k 本身!这次,她必须将随机值隐藏在指数中(而不是将其加到另一个随机值中),以确保维克多的等式仍然成立:
R = g ^k
这样,维克多就不会得知值 k (这是第五章介绍的离散对数问题),因此无法恢复证人 x 。然而,他仍然拥有足够的信息来验证佩姬是否知道 x !维克多只需检查 g ^s (= g ^(k +x ) = g ^k × g ^x) 是否等于 Y × R (= g ^x × g ^k)。我在图 7.5 中审查了这个第二次尝试的零知识证明协议。
图 7.5 为了使知识证明零知识 ,证明者可以用随机值 k 隐藏证人 x ,然后隐藏随机值本身。
我们方案还有一个问题------佩姬可以欺骗。她可以让维克多相信她知道 x ,而实际上并不知道 x !她所要做的就是反转她计算证明的步骤。她首先生成一个随机值 s ,然后基于 s 计算值 R :
R = g ^s × Y ^(--1)
维克托然后计算Y × R = Y × g ^s × Y (--1),这确实与*g* s 匹配。(佩吉使用逆来计算值的技巧在密码学中的许多攻击中都有所应用。)
注意 在理论上,我们说方案"可靠",如果佩吉无法作弊(如果她不知道x ,那么她无法愚弄维克托)。
为了使 ZKP 协议可靠,维克托必须确保佩吉从R 计算出s 而不是反向计算。为此,维克托使协议交互式 :
佩吉必须对她的随机值k 进行承诺,以便以后无法更改。
在收到佩吉的承诺后,维克托在协议中引入了一些自己的随机性。他生成一个随机值c (称为挑战 )并将其发送给佩吉。
佩吉随后可以根据随机值k 和挑战c 计算她的隐藏承诺。
注意 在第二章中,您学习了承诺方案,我们使用哈希函数对我们可以稍后揭示的值进行承诺。但基于哈希函数的承诺方案不允许我们对隐藏值进行有趣的算术运算。相反,我们可以简单地将我们的生成器提升到该值,g ^k,这是我们已经在做的事情。
因为佩吉无法在没有维克托的挑战c 的情况下执行最后一步,而维克托又不会在看到随机值k 的承诺之前发送挑战给她,所以佩吉被迫根据k 计算s 。获得的协议,我在图 7.6 中说明,通常被称为Schnorr 身份验证协议 。
图 7.6 Schnorr 身份验证协议是一个完备的 (佩吉可以证明她知道某个见证人)、可靠的 (如果佩吉不知道见证人,她无法证明任何事情)和零知识的 (维克托对见证人一无所知)交互式 ZKP。
所谓的交互式 ZKP 系统 遵循三个步骤(承诺、挑战和证明)的模式,在文献中通常被称为Sigma 协议 ,有时写作Σ协议(因为希腊字母的形状具有说明性)。但这与数字签名有什么关系呢?
注意 Schnorr 身份验证协议在诚实验证者零知识 (HVZK)模型 中运作:如果验证者(维克托)表现不诚实并且不随机选择挑战,他们可以了解见证人的一些信息。一些更强大的 ZKP 方案在验证者恶意时仍然是零知识的。
7.2.2 签名作为非交互式零知识证明
以前的交互式 ZKP 的问题在于,嗯,它是交互式 的,而现实世界的协议通常不喜欢交互性。交互式协议会增加一些不可忽略的开销,因为它们需要多个消息(可能通过网络)并且会增加无限延迟,除非两个参与者同时在线。由于这个原因,交互式 ZKP 在应用密码学领域中大多缺席。
所有这些讨论都不是毫无意义的!在 1986 年,Amos Fiat 和 Adi Shamir 发表了一种技术,允许将一个交互式的零知识证明(ZKP)轻松转换为一个非交互式的 ZKP。他们引入的技巧(称为费曼-沙米尔启发式 或费曼-沙米尔变换 )是让证明者自己计算挑战,以一种他们无法控制的方式。
这是一个诀窍------将挑战计算为到目前为止协议中发送和接收的所有消息的哈希(我们称之为转录 )。如果我们假设哈希函数产生的输出与真正的随机数不可区分(换句话说,看起来是随机的),那么它可以成功模拟验证者的角色。
Schnorr 更进一步。他注意到任何东西都可以包含在那个哈希中!例如,如果我们在其中包含一条消息会怎样?我们得到的不仅是一个证明我们知道某个见证者x 的证据,而且还是与证据密切相关的密码学链接的消息承诺。换句话说,如果证据是正确的,那么只有知道见证者的人(它变成签名密钥)才能承诺那条消息。
这就是一个签名!数字签名只是非交互式 ZKP。将 Fiat-Shamir 转换应用到 Schnorr 识别协议,我们得到了 Schnorr 签名方案 ,我在图 7.7 中进行了说明。
图 7.7 左边的协议是之前讨论过的 Schnorr 识别协议,这是一个交互式协议。右边的协议是 Schnorr 签名,是左边协议的非交互式版本(其中验证者消息被替换为对转录进行哈希的调用)。
总结一下,Schnorr 签名基本上是两个值,R 和s ,其中R 是对某个秘密随机值的承诺(通常称为nonce ,因为它每个签名需要是唯一的),而s 是通过承诺R 、私钥(见证者x )和一条消息的帮助计算得出的值。接下来,让我们看一下签名算法的现代标准。
7.3 你应该使用(或不使用)的签名算法
像密码学中的其他领域一样,数字签名有许多标准,有时很难理解应该使用哪一个。这就是我在这里的原因!幸运的是,签名算法的类型与密钥交换的类型类似:有基于大数算术模的算法,如 Diffie-Hellman(DH)和 RSA,也有基于椭圆曲线的算法,如椭圆曲线 Diffie-Hellman(ECDH)。
请确保你对第五章和第六章的算法了解足够深入,因为我们现在要基于这些内容进行讨论。有趣的是,引入 DH 密钥交换的论文也提出了数字签名的概念(没有给出解决方案):
为了开发一种能够用一些纯电子形式的通信替代当前书面合同的系统,我们必须发现一个具有与书面签名相同属性的数字现象。 任何人都必须能够轻松识别签名的真实性,但除了合法签署者之外,任何其他人都不可能产生签名。 我们将称这样的技术为单向认证。 由于任何数字信号都可以精确复制,真正的数字签名必须在不被知道的情况下识别 。
------Diffie 和 Hellman(《密码学的新方向》,1976 年)
一年后(1977 年),第一个签名算法(称为 RSA)与 RSA 非对称加密算法一起被引入(您在第六章中学到了)。 RSA 用于签名是我们将学习的第一个算法。
1991 年,NIST 提出了数字签名算法(DSA) ,试图避开 Schnorr 签名的专利。 出于这个原因,DSA 是 Schnorr 签名的一种奇怪的变体,发布时没有安全性证明(尽管目前尚未发现任何攻击)。 该算法被许多人采用,但很快被一个称为ECDSA (代表椭圆曲线数字签名算法)的椭圆曲线版本取代,就像椭圆曲线 Diffie-Hellman(ECDH)取代 Diffie-Hellman(DH)一样,由于其更小的密钥(请参见第五章)。 ECDSA 是我将在本节中讨论的第二种签名算法。
在 2008 年,Schnorr 签名的专利过期后,Daniel J. Bernstein,也就是 ChaCha20-Poly1305(在第四章中介绍)和 X25519(在第五章中介绍)的发明者,推出了一种新的签名方案,称为EdDSA (代表 Edwards 曲线数字签名算法),基于 Schnorr 签名。 自推出以来,EdDSA 迅速获得了采用,并且现在被认为是实际应用中数字签名的最新技术。 EdDSA 是我将在本节中讨论的第三种也是最后一种签名算法。
7.3.1 RSA PKCS#1 v1.5:一个糟糕的标准
RSA 签名目前被广泛应用,尽管它们不应该被使用(正如您将在本节中看到的,它们存在许多问题)。 这是因为该算法是第一个被标准化的签名方案,并且实际应用领域迟迟未能转向更新更好的算法。 因此,在您的学习过程中很可能会遇到 RSA 签名,我无法避免解释它们的工作原理和采用的标准。 但让我说,如果您理解了第六章中 RSA 加密的工作原理,那么本节应该很简单,因为使用 RSA 进行签名与使用 RSA 进行加密相反:
注意 实际上,在签名之前,消息通常会被散列,因为这样会占用更少的空间(RSA 只能签署比其模数小的消息)。结果也被解释为一个大数,以便可以在数学运算中使用。
如果你的私钥是私钥指数d ,公钥是公钥指数e 和公共模数N ,你可以
我在图 7.8 中以图示方式说明了这一点。
图 7.8 要使用 RSA 签名,我们只需对 RSA 加密算法进行逆操作:我们使用私钥指数对消息进行指数运算,然后进行验证,我们使用公钥指数对签名进行指数运算,返回到消息。
这样做的原因是只有了解私钥指数d 的人才能对消息产生签名。与 RSA 加密一样,安全性与因子分解问题的难度紧密相连。
那么用 RSA 进行签名的标准是什么?幸运的是,它们遵循与 RSA 加密相同的模式:
我在第六章关于非对称加密中讨论了 RSA PKCS#1 v1.5。在该文档中标准化的签名方案与加密方案几乎相同。要签名,首先使用所选的哈希函数对消息进行哈希,然后根据 PKCS#1 v1.5 的签名填充进行填充(这与相同标准中的加密填充类似)。接下来,使用私钥指数对填充和散列消息进行加密。我在图 7.9 中说明了这一点。
图 7.9 RSA PKCS#1 v1.5 用于签名。要签名,先使用 PKCS#1 v1.5 填充方案对消息进行哈希和填充。最后一步使用私钥指数d 对填充的哈希消息进行指数运算取模N 。要验证,只需使用公钥指数e 对签名进行指数运算取模N ,并验证它是否与填充的哈希消息匹配。
多个 RSAs
顺便说一句,不要被 RSA 周围的不同术语搞混了。有 RSA(非对称加密原语 )和 RSA(签名原语 )。此外,还有 RSA(公司 ),由 RSA 的发明者创立。提到用 RSA 加密时,大多数人指的是 RSA PKCS#1 v1.5 和 RSA-OAEP 方案。提到用 RSA 签名时,大多数人指的是 RSA PKCS#1 v1.5 和 RSA-PSS 方案。
我知道这可能会让人感到困惑,特别是对于 PKCS#1 v1.5 标准。 尽管在 PKCS#1 v1.5 中有官方名称来区分加密和签名算法(RSAES-PKCS1-v1_5 用于加密,RSASSA-PKCS1-v1_5 用于签名),但我很少看到这些名称被使用。
在第六章中,我提到了对 RSA PKCS#1 v1.5 进行加密的破坏性攻击;不幸的是,对于 RSA PKCS#1 v1.5 签名也是如此。 1998 年,Bleichenbacher 发现了对 RSA PKCS#1 v1.5 加密的毁灭性攻击后,他决定看看签名标准。 Bleichenbacher 在 2006 年提出了对 RSA PKCS#1 v1.5 的签名伪造 攻击,这是对签名的最灾难性的攻击类型之一------攻击者可以在不知道私钥的情况下伪造签名! 与直接破解加密算法的第一次攻击不同,第二次攻击是一种实现攻击。 这意味着如果签名方案按照规范正确实现,攻击就不会奏效。
实现缺陷听起来不像算法缺陷那么糟糕,也就是说,如果很容易避免并且不影响许多实现。 不幸的是,2019 年已经表明,尴尬的是,许多开源实现的 RSA PKCS#1 v1.5 签名实际上陷入了这个陷阱,并且错误地实现了标准(参见 Chau 等人的"使用符号执行分析语义正确性的案例研究:PKCS#1 v1.5 签名验证")。 各种实现缺陷最终导致了不同变体的 Bleichenbacher 的伪造攻击。
不幸的是,RSA PKCS#1 v1.5 签名仍然被广泛使用。 如果您真的必须 出于向后兼容性原因使用此算法,请注意这些问题。 话虽如此,这并不意味着 RSA 签名是不安全的。 故事并没有在这里结束。
7.3.2 RSA-PSS:更好的标准
RSA-PSS 在更新的 PKCS#1 v2.1 中标准化,并包括了安全性证明(与之前的 PKCS#1 v1.5 中标准化的签名方案不同)。 新规范的工作方式如下:
PSS 编码稍微复杂,类似于 OAEP(Optimal Asymmetric Encryption Padding)。 我在图 7.10 中进行了说明。
图 7.10 RSA-PSS 签名方案使用掩码生成函数(MGF)对消息进行编码,就像你在第六章中学到的 RSA-OAEP 算法一样,然后以通常的 RSA 方式进行签名。
验证由 RSA-PSS 产生的签名只是在将签名提升到公共模数的公共指数模下,反转编码的问题。
PSS 的可证明安全性
PSS(概率签名方案 )是可证明安全的,意味着没有人应该能够在不知道私钥的情况下伪造签名。 PSS 并非证明了如果 RSA 安全则 RSA-PSS 安全,而是证明了逆否命题:如果有人能够破解 RSA-PSS,那么该人也能够破解 RSA。这是密码学中证明事物的一种常见方式。当然,这仅在 RSA 安全时才有效,这是我们在证明中假设的。
如果你还记得,我在第六章也谈到了 RSA 加密的第三种算法(称为 RSA-KEM)------这是一种没有任何人使用但被证明安全的更简单的算法。有趣的是,RSA 签名也反映了 RSA 加密历史的这一部分,并且有一个几乎没有人使用的更简单的算法;它被称为完全域哈希 (FDH)。 FDH 通过简单地对消息进行哈希,然后使用 RSA 签名(通过将摘要解释为数字)来工作。
尽管 RSA-PSS 和 FDH 都具有安全性证明并且更容易正确实现,但今天大多数协议仍然使用 RSA PKCS#1 v1.5 进行签名。这只是加密算法淘汰通常发生的缓慢的又一个例子。由于旧的实现仍然必须与新的实现一起工作,因此删除或替换算法变得困难。考虑一下不更新应用程序的用户、不提供软件新版本的供应商、无法更新的硬件设备等等。接下来,让我们看看一个更现代的算法。
7.3.3 椭圆曲线数字签名算法(ECDSA)
在本节中,让我们看看 ECDSA,这是 DSA 的椭圆曲线变体,它本身只是为了规避 Schnorr 签名的专利而发明的。该签名方案在许多标准中指定,包括 ISO 14888-3、ANSI X9.62、NIST 的 FIPS 186-2、IEEE P1363 等等。并非所有标准都兼容,希望进行互操作的应用程序必须确保它们使用相同的标准。
不幸的是,与 DSA 一样,ECDSA 没有安全性证明,而 Schnorr 签名却有。尽管如此,ECDSA 已被广泛采用,并且是最常用的签名方案之一。在本节中,我将解释 ECDSA 的工作原理以及如何使用它。与所有这些方案一样,公钥几乎总是根据相同的公式生成:
更具体地说,在 ECDSA 中,公钥是使用[x ]G 计算的,其中x 与基点G 的标量乘积。
加法还是乘法符号?
请注意,我使用加法符号 (在标量周围放置括号的椭圆曲线语法),但如果我想使用乘法符号 ,我可以写public_key = G ^x。这些差异在实践中并不重要。大多数时候,不关心群的基础性质的加密协议使用乘法符号编写,而专门在椭圆曲线群中定义的协议倾向于使用加法符号编写。
要计算 ECDSA 签名,你需要与 Schnorr 签名所需的相同输入:签署消息的哈希值(H (m )),你的私钥x ,以及每个签名唯一的随机数k 。ECDSA 签名是两个整数,r 和s ,计算如下:
要验证 ECDSA 签名,验证者需要使用相同的哈希消息H (m ),签名者的公钥,以及签名数值r 和s 。验证者然后
计算[H (m ) s ^(--1)]G + [rs ^(--1)]public_key
验证所得点的 x 坐标是否与签名值r 相同
你肯定能够认识到与 Schnorr 签名有一些相似之处。随机数k 有时被称为nonce ,因为它是一个只能使用一次的数字,有时也被称为ephemeral key ,因为它必须保持秘密。
警告我再次强调:k 绝对不能重复或可预测!没有这一点,恢复私钥就变得微不足道。
一般来说,加密库在幕后执行此 nonce(k 值)的生成,但有时不会让调用者提供它。这当然是一场灾难。例如,在 2010 年,索尼的 Playstation 3 被发现使用重复 nonce 的 ECDSA(泄漏了他们的私钥)。
警告更加微妙的是,如果 nonce k 不是均匀和随机选择的(特别是,如果你可以预测前几位),仍然存在可以在瞬间恢复私钥的强大攻击(所谓的格攻击 )。在理论上,我们称这种密钥检索攻击为全面破解 (因为它们破坏了一切!)。这种全面破解在实践中非常罕见,这使得 ECDSA 算法可能以惊人的方式失败。
存在避免 nonce 问题的尝试。例如,RFC 6979 指定了一个基于消息和私钥生成 nonce 的确定性 ECDSA 方案。这意味着两次签署相同消息涉及两次相同的 nonce,因此产生两次相同的签名(这显然不是问题)。
倾向于与 ECDSA 一起使用的椭圆曲线基本上与椭圆曲线 Diffie-Hellman(ECDH)算法(参见第五章)中流行的曲线相同,但有一个显着的例外:Secp256k1 。Secp256k1 曲线在 SEC 2 中定义:"推荐的椭圆曲线域参数" (secg.org/sec2-v2.pdf
),由高效密码学标准组(SECG)编写。在比特币决定使用它而不是更流行的 NIST 曲线之后,它受到了很多关注,原因是我在第五章中提到的对 NIST 曲线的不信任。
Secp256k1 是一种称为 Koblitz 曲线 的椭圆曲线类型。Koblitz 曲线只是具有一些参数约束的椭圆曲线,这些约束允许在曲线上优化一些操作。椭圆曲线具有以下方程式:
y ² = x ³ + ax + b
其中 a = 0 和 b = 7 是常数,x 和 y 定义在模素数 p 上:
p = 2¹⁹² -- 2³² -- 2¹² -- 2⁸ -- 2⁷ -- 2⁶ -- 2³ -- 1
这定义了一个素数阶的群,与 NIST 曲线相似。今天,我们有有效的公式来计算椭圆曲线上点的数量。这是 Secp256k1 曲线中点的数量(包括无穷远点)的素数:
115792089237316195423570985008687907852837564279074904382605163141518161494337
我们使用固定点 G 作为生成器(或基点)的坐标
x = 55066263022277343669578718895168534326250603453777594175500187360389116729240
和
y = 32670510020758816978083085130507043184471273380659243275938904335757337482424
尽管如此,今天 ECDSA 大多数与 NIST 曲线 P-256(有时称为 Secp256r1 ;注意区别)一起使用。接下来让我们看另一种广泛流行的签名方案。
7.3.4 Edwards 曲线数字签名算法(EdDSA)
让我介绍一下本章的最后一个签名算法,Edwards 曲线数字签名算法 (EdDSA),由 Daniel J. Bernstein 于 2011 年发布,以回应对 NIST 和其他政府机构创建的曲线的不信任。EdDSA 这个名字似乎表明它基于 DSA 算法,就像 ECDSA 一样,但这是误导的。EdDSA 实际上基于 Schnorr 签名,这是由于 Schnorr 签名专利在 2008 年早些时候到期而可能的。
EdDSA 的一个特殊之处在于该方案不需要每次签名操作都产生新的随机数。EdDSA 确定性地 生成签名。这使得该算法相当具有吸引力,并且已被许多协议和标准采用。
EdDSA 正在着手包括在 NIST 的即将更新的 FIPS 186-5 标准中(截至 2021 年初仍是草案)。当前的官方标准是 RFC 8032,它定义了两个不同安全级别的曲线,可用于 EdDSA。所定义的两个曲线都是 扭曲的 Edwards 曲线 (一种启用有趣的实现优化的椭圆曲线类型):
Edwards25519 基于 Daniel J. Bernstein 的 Curve25519(在第五章中介绍) 。由于椭圆曲线的类型所启用的优化,其曲线操作可以比 Curve25519 更快地实现。由于它是在 Curve25519 之后发明的,基于 Curve25519 的密钥交换 X25519 并未从这些速度改进中受益。与 Curve25519 一样,Edwards25519 提供了 128 位安全性。
Edwards448 基于 Mike Hamburg 的 Ed448-Goldilocks 曲线 。它提供了 224 位安全性。
在实践中,EdDSA 主要使用 Edwards25519 曲线实例化,该组合被称为 Ed25519 (而带有 Edwards448 的 EdDSA 则缩写为 Ed448)。与现有方案不同,EdDSA 的密钥生成略有不同。EdDSA 不直接生成签名密钥,而是生成一个秘密密钥,然后用于派生实际的签名密钥和我们称之为随机数密钥的另一个密钥。那个随机数密钥很重要!它是用于确定性地生成所需每个签名的随机数的密钥。
注意 根据您使用的加密库,您可能正在存储秘密密钥或两个派生密钥:签名密钥和随机数密钥。不是这很重要,但如果您不知道这一点,那么如果遇到将 Ed25519 秘密密钥存储为 32 字节或 64 字节,具体取决于所使用的实现,则可能会感到困惑。
要签名,EdDSA 首先通过将随机数密钥与要签名的消息进行哈希运算来确定性地生成随机数。之后,类似于 Schnorr 签名的过程如下进行:
计算随机数为 HASH (nonce key || message )
计算承诺 R 为 [nonce ]G ,其中 G 是群的基点
计算挑战为 HASH (commitment || public key || message )
计算证明 S 为 nonce + challenge × signing key
签名是(R ,S )。我在图 7.11 中说明了 EdDSA 的重要部分。
Figure 7.11 EdDSA 密钥生成产生一个秘密密钥,然后用于派生另外两个密钥。第一个派生密钥是实际的签名密钥,因此可用于派生公钥;另一个派生密钥是随机数密钥,在签名操作期间用于确定性地派生随机数。然后,EdDSA 签名类似于 Schnorr 签名,唯一的异常是(1)随机数是根据随机数密钥和消息确定性生成的,并且(2)签名者的公钥包含在挑战的一部分中。
注意随机数(或临时密钥)如何确定性地而不是概率性地从随机数密钥和给定的消息中派生出来。这意味着签署两个不同的消息应该涉及两个不同的随机数,巧妙地防止签署者重复使用随机数,从而泄漏密钥(就像 ECDSA 可能发生的情况一样)。两次签署相同的消息会产生两次相同的随机数,然后也会产生两次相同的签名。这显然不是问题。可以通过计算以下两个方程式来验证签名:
*S* \]*G*
*R* + \[*HASH* (*R* \|\| *public key* \|\| *message* )\] *public key*
如果这两个值匹配,则签名有效。这与 Schnorr 签名的工作方式完全相同,只是现在我们处于一个椭圆曲线组中,我在这里使用了加法表示法。
EdDSA 的最广泛使用的实例是 Ed25519,它使用 Edwards25519 曲线和 SHA-512 作为哈希函数进行定义。 Edwards25519 曲线的定义包含满足以下方程的所有点:
--*x* ² + *y* ² = 1 + *d* × *x* ² × *y* ² mod *p*
其中值 *d* 是大数
37095705934669439343138083508754565189542113879843219016388785533085940283555
变量 *x* 和 *y* 取模 *p* ,即大数 2²⁵⁵ -- 19(用于 Curve25519 的相同素数)。基点是坐标为 *G*
*x* = 15112221349535400772501151409588531511454012693041857206046113283949847762202
和
*y* = 46316835694926478169428394003475163141307993866256225615783033603165251855960
RFC 8032 实际上定义了三种使用 Edwards25519 曲线的 EdDSA 变体。所有三种变体都遵循相同的密钥生成算法,但具有不同的签名和验证算法:
* *Ed25519(或 pureEd25519)* ------ 这就是我之前解释过的算法。
* *Ed25519ctx* ------ 此算法引入了一个强制的定制字符串,并且在实践中很少被实现,甚至很少被使用。唯一的区别是在每次调用哈希函数时都添加了一些用户选择的前缀。
* *Ed25519ph(或 HashEd25519)* ------ 这允许应用程序在签名之前对消息进行预哈希(因此名称中有 *ph*)。它还基于 Ed25519ctx,允许调用者包含一个可选的自定义字符串。
在密码学中增加一个 *定制字符串* 是相当常见的,就像你在第二章中看到的某些哈希函数,或者在第八章中看到的密钥派生函数一样。当协议中的参与者在不同的上下文中使用相同的密钥对消息进行签名时,这是一个有用的补充。例如,你可以想象一个应用程序,允许你使用私钥签名交易,也可以向你交谈的人签署私人消息。如果你错误地签署并发送了一个看起来像交易的消息给你的邪恶朋友 Eve,她可能会尝试将其重新发布为有效的交易,如果无法区分你签署的两种类型的有效载荷的话。
Ed25519ph 仅为了满足需要签署大型消息的调用者而引入。正如您在第二章中看到的,哈希函数通常提供"初始化-更新-完成"接口,允许您连续哈希数据流,而无需将整个输入保留在内存中。
现在您已经完成了对实际应用中使用的签名方案的介绍。接下来,让我们看看在使用这些签名算法时可能如何自掘坟墓。但首先,让我们回顾一下:
* RSA PKCS#1 v1.5 仍然被广泛使用,但正确实现很困难,许多实现已被发现存在问题。
* RSA-PSS 具有安全性证明,更易于实现,但由于基于椭圆曲线的新方案而受到较少采用。
* ECDSA 是 RSA PKCS#1 v1.5 的主要竞争对手,大多数情况下与 NIST 的曲线 P-256 一起使用,除了在加密货币世界中,Secp256k1 似乎占主导地位。
* Ed25519 基于 Schnorr 签名,已经得到广泛采用,并且与 ECDSA 相比更容易实现;它不需要每次签名操作都产生新的随机数。如果可以的话,这是您应该使用的算法。
### 7.4 签名方案的微妙行为
签名方案可能具有一些微妙的特性。虽然它们在大多数协议中可能并不重要,但在处理更复杂和非常规的协议时,不了解这些"陷阱"可能会给您带来麻烦。本章的最后部分重点介绍了数字签名的已知问题。
#### 7.4.1 签名替换攻击
*数字签名并不能唯一地识别密钥或消息*。
------Andrew Ayer(《让我们加密中的重复签名密钥选择攻击》,2015)
*替换攻击* ,也称为*重复签名密钥选择*(DSKS),对 RSA PKCS#1 v1.5 和 RSA-PSS 都是可能的。存在两种 DSKS 变体:
* *密钥替换攻击*------使用不同的密钥对或公钥来验证给定消息上的给定签名。
* *消息密钥替换攻击* ------使用不同的密钥对或公钥来验证给定消息上的*新*签名。
再说一遍:第一次攻击同时修复了消息和签名;第二次攻击只修复了签名。我在图 7.12 中总结了这一点。

图 7.12 类似 RSA 的签名算法易受密钥替换攻击的影响,这对大多数密码学用户来说是意外且意想不到的行为。*密钥替换* 攻击允许某人获取消息的签名,并制作一个新的密钥对,以验证原始签名。一种变体称为*消息密钥替换*允许攻击者创建一个新的密钥对和一个新的消息,这些消息在原始签名下是有效的。
存在适应性选择消息攻击下的存在性不可伪造性 (EUF-CMA)
替换攻击是理论密码学和应用密码学之间差距的一种综合症。密码学中的签名通常使用 EUF-CMA 模型进行分析,该模型代表自适应选择消息攻击下的存在性不可伪造性。在这个模型中,您生成一对密钥,然后我请求您对一些任意消息进行签名。当我观察您产生的签名时,如果我能在某个时间点生成一个我以前没有请求过的消息的有效签名,那么我就赢了。不幸的是,这个 EUF-CMA 模型似乎并不包括每个边缘情况,而且危险的细微差别,如替换攻击,也没有被考虑在内。
#### 7.4.2 签名可塑性
*2014 年 2 月,曾经是最大比特币交易所的 MtGox 关闭并申请破产,声称攻击者利用可塑性攻击来清空其账户*。
---Christian Decker 和 Roger Wattenhofer("比特币交易可塑性和 MtGox",2014)
大多数签名方案都是可塑的:如果您给我一个有效的签名,我可以修改签名,使其成为一个不同但仍然有效的签名。我不知道签名密钥是什么,但我设法创建了一个新的有效签名。
非可塑性并不一定意味着签名是唯一的:如果我是签名者,通常可以为相同的消息创建不同的签名,这通常是可以接受的。一些构造,如可验证随机函数(你将在第八章中看到),依赖于签名的唯一性,因此它们必须处理这个问题或使用具有唯一签名的签名方案(如 Boneh--Lynn--Shacham,或 BLS,签名)。
强 EUF-CMA
一个称为 SUF-CMA(用于强 EUF-CMA)的新安全模型试图在签名方案的安全定义中包含非可塑性(或抵抗可塑性)。一些最近的标准,如 RFC 8032,规定了 Ed25519,包括对抗可塑性攻击的缓解措施。由于这些缓解措施并不总是存在或常见,您不应该依赖于您的协议中的签名是非可塑的。
如何处理所有这些信息?请放心,签名方案绝对没有问题,如果您使用的签名不太超出常规,那么您可能不必担心。但是,如果您正在设计加密协议,或者您正在实现比日常密码学更复杂的协议,您可能希望将这些微妙的属性记在心中。
### 摘要
* 数字签名类似于笔和纸签名,但是由密码学支持,使得除了控制签名(私钥)的人之外,任何人都无法伪造。
* 数字签名可以用于验证来源(例如,密钥交换的一方)以及提供传递信任(如果我信任 Alice,她信任 Bob,我就可以信任 Bob)。
* 零知识证明(ZKPs)允许证明者证明对特定信息(称为见证)的知识,而不泄露任何信息。签名可以被视为非交互式 ZKPs,因为在签名操作期间不需要验证者在线。
* 您可以使用许多标准进行签名:
* RSA PKCS#1 v1.5 如今被广泛使用,但不建议,因为很难正确实现。
* RSA-PSS 是一种更好的签名方案,因为它更容易实现并且有安全性证明。不幸的是,由于支持更短密钥的椭圆曲线变体现在更受网络协议青睐,因此它如今并不流行。
* 目前最流行的签名方案基于椭圆曲线:ECDSA 和 EdDSA。ECDSA 经常与 NIST 的曲线 P-256 一起使用,而 EdDSA 经常与 Edwards25519 曲线一起使用(这种组合被称为 Ed25519)。
* 一些微妙的属性可能会很危险,如果签名被以非常规方式使用:
* 始终避免对谁签署了消息产生歧义,因为一些签名方案容易受到密钥替换攻击的影响。外部参与者可以创建一个新的密钥对,该密钥对将验证已经存在的消息上的签名,或者创建一个新的密钥对和一个新消息,该消息将验证给定的签名。
* 不要依赖签名的唯一性。首先,在大多数签名方案中,签名者可以为同一消息创建任意数量的签名。其次,大多数签名方案都是*可塑性*的,这意味着外部参与者可以获取一个签名并为同一消息创建另一个有效的签名。
## 第八章:随机性和秘密
本章涵盖了
* 随机性是什么以及为什么它很重要
* 获取强随机性并生成秘密
* 随机性的陷阱
这是本书第一部分的最后一章,在我们转到第二部分并了解实际世界中使用的协议之前,我有最后一件事要告诉你。这是我迄今为止严重忽视的一点 ------ 随机性。
你一定注意到了,在你学过的每个密码算法中(哈希函数除外),你都必须在某个时候使用随机性:秘密密钥、随机数、初始化向量、素数、挑战等等。当我讲解这些不同的概念时,随机性总是来自某个神奇的黑盒子。这并不罕见。在密码学白皮书中,随机性通常被用一个带有美元符号的箭头表示。但是在某些时候,我们需要问自己一个问题,"这个随机性到底来自哪里?"
在这一章中,我将为你解释当密码学提到随机性时它意味着什么。我还将为你提供有关现实世界密码应用中获取随机性的实用方法的指引。
注意 对于这一章,你需要已经阅读了第二章关于哈希函数和第三章关于消息认证码。
### 8.1 什么是随机性?
每个人在某种程度上都理解随机性的概念。无论是玩骰子还是买彩票,我们都曾接触过它。我第一次遇到随机性是在很小的时候,当我意识到计算器上的一个 RAND 按钮每次按下都会产生不同的数字时。这让我感到非常困扰。我对电子学了解甚少,但我觉得我可以理解一些它的限制。当我将 4 和 5 相加时,肯定会有一些电路进行计算并给我结果。但是一个随机按钮?随机数从哪里来的?我无法理解。
我花了一些时间才问出正确的问题,并且了解到计算器其实是作弊的!它们会硬编码大量随机数列表,并逐一遍历这些列表。这些列表会展现出良好的随机性,这意味着如果你看着得到的随机数,1 的数量和 9 的数量相等,1 的数量和 2 的数量相等,依此类推。这些列表会模拟*均匀分布*:数字均匀分布在等比例中。
当需要用于安全和密码学目的时,随机数必须是*不可预测*的。当然,在那个时候,没有人会将那些计算器的"随机性"用于与安全有关的任何事情。相反,密码应用从观察难以预测的物理现象中提取随机性。
举例来说,即使投掷骰子是一个确定性过程,预测其结果也很困难;如果你知道了所有的初始条件(你如何投掷骰子、骰子本身、空气摩擦、桌面的摩擦力等),你应该能够预测结果。话虽如此,所有这些因素对最终结果的影响如此之大,以至于对初始条件的知识有轻微的不准确性就会影响我们的预测。结果对初始条件的极度敏感性被称为*混沌理论*,这就是为什么像天气这样的事情很难在一定数量的天数后准确预测的原因。
下面的图片是我在访问 Cloudflare 在旧金山总部期间拍摄的一张照片。LavaRand 是一堵熔岩灯墙,这些灯产生难以预测的蜡形状。一台摄像机放置在墙前,提取并将图像转换为随机字节。

应用程序通常依赖操作系统提供可用的随机性,而操作系统又根据运行的设备类型使用不同的技巧收集随机性。常见的随机性来源(也称为*熵源*)可以是硬件中断的时间(例如,您的鼠标移动)、软件中断、硬盘寻道时间等。
熵
在信息理论中,*熵*一词用于判断一个字符串包含多少随机性。该术语是由克劳德·香农创造的,他设计了一个熵公式,该公式将随着字符串表现出越来越多的不可预测性而输出越来越大的数字(从完全可预测的 0 开始)。对于我们来说,公式或数字本身并不那么有趣,但在密码学中,你经常会听到"这个字符串的熵低"(意思是可预测的)或"这个字符串的熵高"(意思是不太可预测的)。
观察中断和其他事件以产生随机性并不理想;当设备启动时,这些事件往往是高度可预测的,它们也可能受到外部因素的恶意影响。如今,越来越多的设备可以访问额外的传感器和硬件辅助设备,提供更好的熵源。这些硬件随机数发生器通常称为*真随机数发生器*(TRNG),因为它们利用外部不可预测的物理现象(如热噪声)来提取随机性。
通过所有这些不同类型的输入获得的噪声通常不是"干净"的,有时甚至没有足够的熵(如果有的话)。例如,从某些熵源获得的第一个比特往往是 0,或者连续的比特可能(比机会更大)相等。因此,在用于密码应用之前,*随机性提取器*必须清理和收集几种噪声源。例如,可以通过将不同源应用哈希函数并将摘要进行异或来完成此操作。
随机性就只有这些吗?不幸的是不是。从噪声中提取随机性是一个可能会很慢的过程。对于一些可能需要快速生成大量随机数的应用程序,这可能成为瓶颈。下一节将描述操作系统和现实世界应用程序如何提高随机数的生成。
### 8.2 慢随机性?使用伪随机数生成器(PRNG)
随机性随处可见。此时,您应该至少相信这对于密码学是真实的,但令人惊讶的是,密码学并不是唯一一个大量使用随机数的地方。例如,像 ls 这样的简单 Unix 程序也需要随机性!由于程序中的错误如果被利用可能会产生灾难性后果,二进制文件试图通过多种技巧来防御低级攻击;其中之一是*ASLR*(地址空间布局随机化),它在每次运行时随机化进程的内存布局,因此需要随机数。另一个例子是网络协议 TCP,每次创建连接时都使用随机数来产生不可预测的数字序列,并阻止试图劫持连接的攻击。虽然所有这些都超出了本书的范围,但了解现实世界中出于安全原因使用了多少随机性是很好的。
在上一节中,我暗示了,不幸的是,获得不可预测的随机性有点慢。这有时是因为熵源产生噪声的速度较慢。因此,操作系统通常通过使用*伪随机数生成器*(PRNGs)来优化它们的随机数生成过程。
注意为了与那些不设计为安全的随机数生成器进行对比(在不同类型的应用程序中很有用,比如视频游戏),PRNG 有时被称为*CSPRNGs* ,代表*密码学安全* PRNGs。NIST 想要以不同的方式做事情(像往常一样),通常将他们的 PRNG 称为*确定性随机位生成器*(DRBGs)。
PRNG 需要一个初始秘密,通常称为*种子*,我们可以通过混合不同的熵源来获得,然后可以快速产生大量随机数。我在图 8.1 中说明了一个 PRNG。

图 8.1 伪随机数生成器(PRNG)基于种子生成随机数序列。使用相同的种子使 PRNG 产生相同的随机数序列。应该不可能使用随机输出的知识来恢复状态(函数`next`是一种方式)。由此得出,仅从观察产生的随机数就不可能预测未来的随机数或恢复先前生成的随机数。
加密安全的 PRNG 通常具有以下属性:
* *确定性* --- 使用相同的种子两次会产生相同的随机数序列。这与我之前谈到的不可预测的随机性提取不同:如果你知道 PRNG 使用的种子,那么 PRNG 应该是完全可预测的。这就是为什么这种构造被称为*伪*随机的原因,这也是使 PRNG 能够非常快速的原因。
* *与随机不可区分*--- 在实践中,你不应该能够区分 PRNG 输出的随机数与一个小精灵公正地从相同集合中选择随机数的情况(假设该精灵知道一种魔法方式来选择一个数,以使每个可能的数都可以等概率地被选择)。因此,仅观察生成的随机数不应该允许任何人恢复 PRNG 的内部状态。
最后一点非常重要!PRNG 模拟从*均匀随机*选择一个数字,这意味着集合中的每个数字都有相等的被选中的机会。例如,如果你的 PRNG 生成 8 字节的随机数,那么集合就是所有可能的 8 字节字符串,每个 8 字节值都应该有相等的概率成为可以从你的 PRNG 获得的下一个值。这包括已经由 PRNG 在过去某个时候生成的值。
此外,许多 PRNG 还表现出其他安全性质。如果攻击者学习到状态(例如在某个时间点进入您的计算机),则 PRNG 不会允许其检索先前生成的随机数,那么 PRNG 具有*正向保密性*。我在图 8.2 中进行了说明。

图 8.2 如果 PRNG 的状态泄露不会导致恢复先前生成的随机数,则 PRNG 具有正向保密性。
获取 PRNG 的*状态* 意味着你可以确定它将生成的所有未来伪随机数。为了防止这种情况发生,一些 PRNG 具有定期"修复"自身的机制(以防出现泄密)。这种修复可以通过在 PRNG 已经被种子化后重新注入(或重新播种)新的熵来实现。这种属性被称为*逆向保密性*。我在图 8.3 中进行了说明。

图 8.3 如果 PRNG 的状态被泄露,而这并不会导致能够预测 PRNG 生成的未来随机数,则 PRNG 具有逆向保密性。这仅在产生新的熵并在泄密后注入更新函数时才成立。
注意 *前向* 和 *后向保密性* 这两个术语经常让人感到困惑。如果你读到这一部分时认为前向保密性应该是后向保密性,反之亦然,那么你并不疯狂。因此,后向保密性有时被称为*未来保密性* ,甚至是*事后妥协安全*(PCS)。
如果适当地种子化,PRNGs 可以非常快速,并被认为是生成大量用于加密目的的随机值的安全方法。使用可预测的数字或数字过小显然不是安全的种子 PRNG 的方式。这实际上意味着我们有安全的加密方式,可以快速地将适当大小的秘密扩展到数十亿个其他秘密密钥。很酷,对吧?这就是为什么大多数(如果不是全部)加密应用程序不直接使用从噪声中提取的随机数,而是在初始步骤中使用它们来种子 PRNG,然后在需要时切换到从 PRNG 生成随机数。
双重 EC 后门
如今,伪随机数生成器(PRNGs)主要是基于启发式构建的。这是因为基于困难数学问题(如离散对数)的构建方式速度太慢,不够实用。一个臭名昭著的例子是由 NSA 发明的*双重 EC*,依赖于椭圆曲线。双重 EC PRNG 被推广到各种标准,包括 2006 年左右的一些 NIST 出版物,不久之后,几位研究人员独立发现了算法中的潜在后门。这在 2013 年斯诺登的披露中得到了确认,一年后,该算法被撤回了多个标准。
要保证安全,PRNG 必须用一个*不可预测* 的秘密种子。更准确地说,我们说 PRNG 以\* n *字节的密钥均匀随机采样。这意味着我们应该从所有可能的* n \* -字节字符串集中随机选择密钥,每个字节字符串被选中的机会相同。
在本书中,我谈到了许多产生与随机输出不可区分的密码算法(从将被均匀选择的值)。直觉上,你应该在想我们能否使用这些算法来生成随机数呢?你是对的!哈希函数、XOFs、块密码、流密码和 MACs 可以用来生成随机数。哈希函数和 MACs 在理论上并没有被定义为提供与随机不可区分的输出,但在实践中,它们经常是如此。另一方面,像密钥交换和签名这样的非对称算法(几乎总是)不可区分于随机。因此,它们的输出在被用作随机数之前经常被哈希。
实际上,因为大多数计算机上都支持 AES,因此通常会看到使用 AES-CTR 来生成随机数。对称密钥成为种子,而密文成为随机数(例如,用于加密无限的 0 字符串)。在实践中,为了提供前向和后向保密性,对这些构造添加了一些复杂性。幸运的是,您现在已经了解足够多的内容,可以进入下一节,该节提供了实际获取随机性的概述。
### 8.3 在实践中获取随机性
您已经了解了操作系统向其程序提供加密安全随机数所需的三个要素:
* *噪声源* --- 这些是操作系统从不可预测的物理现象(如设备温度或鼠标移动)中获取原始随机性的方法。
* *清理和混合* --- 虽然原始随机性可能质量较差(一些位可能是偏倚的),但操作系统会清理并混合多个来源,以产生良好的随机数。
* *PRNGs* --- 因为前两个步骤很慢,所以可以使用单个、均匀分布的随机值来种子一个可以快速生成随机数的 PRNG。
在本节中,我将解释系统如何将这三个概念捆绑在一起,以向开发人员提供简化的接口。操作系统提供的这些函数通常允许您通过发出系统调用生成随机数。在这些系统调用背后,确实有一个系统将噪声源、混合算法和 PRNG 捆绑在一起(在图 8.4 中总结)。

图 8.4 在系统上生成随机数通常意味着从不同的噪声源混合熵并用于种子长期 PRNG。
根据操作系统和可用硬件的不同,这三个概念可能会以不同的方式实现。在 2021 年,Linux 使用基于 ChaCha20 流密码的 PRNG,而 macOS 使用基于 SHA-1 散列函数的 PRNG。此外,向开发人员公开的随机数生成器接口将根据操作系统而异。在 Windows 上,可以使用 `BCryptGenRandom` 系统调用生成随机数,而在其他平台上,则公开了一个特殊文件(通常称为 /dev/urandom),可以读取以提供随机性。例如,在 Linux 或 macOS 上,可以使用 `dd` 命令行工具从终端读取 16 字节:
```go
$ dd if=/dev/urandom bs=16 count=1 2> /dev/null | xxd -p
40b1654b12320e2e0105f0b1d61e77b1
```
`/dev/urandom` 的一个问题是,如果在设备启动后太早使用,可能提供的熵不足(其数字不够随机)。像 Linux 和 FreeBSD 这样的操作系统提供了一种称为 `getrandom` 的解决方案,它是一种系统调用,几乎提供与从 `/dev/urandom` 读取相同功能的功能。在很少的情况下,如果初始化其 PRNG 的熵不足,`getrandom` 将阻止程序的继续运行,并等待适当的种子化。因此,如果系统可用,建议您使用 `getrandom`。以下清单显示了如何在 C 中安全使用 `getrandom`:
`8.1` 在 C 中获取随机数示例
```go
#include
uint8_t secret[16]; // ❶
int len = getrandom(secret, sizeof(secret), 0); // ❷
if (len != sizeof(secret)) {
abort(); // ❸
}
```
❶ 使用随机字节填充缓冲区(请注意,`getrandom` 每次调用最多限制为 `256` 字节)。
❷ 默认标志(`0`)是不阻塞的,除非适当种子化。
❸ 函数可能失败或返回少于所需的随机字节数。如果是这种情况,则系统已损坏,中止可能是最好的选择。
有了这个例子,还要指出许多编程语言都有标准库和密码库,提供更好的抽象。例如,很容易忘记 `getrandom` 每次调用最多只返回 `256` 个字节。因此,您应该始终尝试通过所使用的编程语言的标准库生成随机数。
警告 注意许多编程语言公开了产生可预测随机数的函数和库。这些不适用于密码学用途!确保使用生成*密码强度强* 的随机数的随机库。通常库的名称有助于选择(例如,在 Golang 中,您可能可以猜出应该使用 `math/rand` 和 `crypto/rand` 包之间的哪一个),但是阅读手册是无可替代的!
清单 `8.2` 显示了如何在 PHP `7` 中生成一些随机字节。任何密码算法都可以使用这些随机字节。例如,作为使用认证加密算法加密的秘密密钥。每种编程语言都有不同的做法,因此请务必查阅您的编程语言文档,以找到获取密码用途的随机数的标准方法。
`8.2` 在 PHP 中获取随机数示例
```go
```
❶ 产生 `0` 到 `10` 之间的随机整数。虽然快速,但 `rand` 不会产生密码学安全的随机数,因此不适用于密码算法和协议。
❷ `random_bytes` 创建并填充一个包含 `16` 个随机字节的缓冲区。结果适用于密码算法和协议。
现在您已经了解了如何在程序中获得密码学安全的随机性,让我们思考一下在生成随机性时需要牢记的安全考虑事项。
### `8.4` 随机性生成与安全考虑
在这一点上记住是很好的,任何基于密码学的有用协议都需要良好的随机性,一个破损的 PRNG 可能导致整个密码协议或算法不安全。你应该清楚地知道,MAC 只有与其一起使用的密钥一样安全,或者即使有微小的可预测性通常也会破坏 ECDSA 等签名方案,等等。
到目前为止,本章让生成随机性听起来应该是应用密码学的一个简单部分,但实际上并非如此。由于多种问题:使用非密码学 PRNG、错误地种子化 PRNG(例如使用可预测的当前时间)等,随机性实际上是真实世界密码学中许多许多错误的根源。
一个例子包括使用*用户空间 PRNG* 而不是*内核 PRNG*的程序,后者在系统调用后面。用户空间 PRNG 通常会增加不必要的摩擦,如果被误用,最坏的情况下可能会破坏整个系统。这在 2006 年某些操作系统中补丁到的 OpenSSL 库提供的 PRNG 中就是一个明显的例子,无意中影响了使用受影响 PRNG 生成的所有 SSL 和 SSH 密钥。
*删除这段代码的副作用是瘫痪了 OpenSSL PRNG 的种子过程。而不是混合随机数据用于初始种子,唯一使用的随机值是当前进程 ID。在 Linux 平台上,默认的最大进程 ID 是 32,768,导致所有 PRNG 操作只使用了很少的种子值*。
---H. D. Moore("Debian OpenSSL 可预测 PRNG 玩具",2008)
出于这个原因和其他原因,我将在本章后面提到明智的做法是避免使用用户空间 PRNG,并在可用时坚持使用操作系统提供的随机性。在大多数情况下,坚持使用编程语言的标准库或一个良好的加密库提供的内容应该足够了。
*我们不能在开发人员在编写日常代码时需要记住的'最佳实践'之后不断添加更多内容*。
---Martin Boßlet("OpenSSL PRNG 不是(真的)分叉安全的",2013)
不幸的是,任何建议都无法真正为你准备好获取良好随机性的许多陷阱。因为随机性是每个加密算法的核心,做出微小错误可能导致灾难性后果。如果你遇到以下边缘情况,记住以下内容是很好的:
* *分叉进程*------当使用用户空间伪随机数生成器(一些对性能要求极高的应用可能别无选择)时,重要的是要记住,一个分叉的程序会产生一个新的子进程,其 PRNG 状态与其父进程相同。因此,从那时起,两个 PRNG 将产生相同的随机数序列。因此,如果你真的想使用用户空间 PRNG,你必须小心让分叉使用不同的种子来生成他们的 PRNG。
* *虚拟机(VMs)*---当使用操作系统 PRNG 时,克隆 PRNG 状态也可能成为一个问题。想想虚拟机。如果整个 VM 的状态被保存,然后从这一点开始多次启动,每个实例可能会产生完全相同的随机数序列。有时这可以通过虚拟化程序和操作系统来解决,但在运行请求在虚拟机中生成随机数的应用程序之前,最好了解一下您正在使用的虚拟化程序的操作。
* *早期启动熵*---虽然操作系统在用户操作设备时应该没有问题收集熵,因为用户与设备的交互产生的噪声,但嵌入式设备和无头系统在启动时需要克服更多的挑战以产生良好的熵。历史表明,一些设备倾向于以类似的方式启动并从系统中积累相同的初始噪声,导致使用相同种子用于其内部 PRNG 并生成相同系列的随机数。
*存在一个漏洞窗口---启动时的熵空洞---在这个窗口期内,Linux 的 urandom 可能是完全可预测的,至少对于单核系统来说。\[...\] 当我们禁用了可能在无头或嵌入式设备上不可用的熵源时,Linux RNG 在每次启动时产生了相同可预测的流*。
---Heninger 等人("挖掘您的 P 和 Q:检测网络设备中普遍存在的弱密钥",2012)
在这些罕见的情况下,当您确实需要在启动过程中尽早获取随机数时,可以通过提供从另一台机器的良好种子的`getrandom`或/dev/urandom 生成的初始熵来帮助系统。不同的操作系统可能提供此功能,如果您发现自己处于这种情况,请查阅它们的手册(像往常一样)。
如果可用,TRNG 为这个问题提供了一个简单的解决方案。例如,现代英特尔 CPU 嵌入了一个特殊的硬件芯片,从热噪声中提取随机性。这种随机性可以通过一个名为`RDRAND`的指令获得。
`RDRAND`争议
有趣的是,英特尔的`RDRAND`由于存在后门的恐惧而引起了很大争议。大多数集成了`RDRAND`作为熵源的操作系统会将其与其他熵源混合在一起,以*协同*的方式。这里的协同意味着一个熵源不能强制影响随机数生成的结果。
练习
想象一下,如果将不同的熵源简单地通过异或操作在一起,您能看出这可能无法成为协同的吗?
最后,让我提一下避免随机性缺陷的一个解决方案是使用更少依赖于随机性的算法。例如,你在第七章看到了,ECDSA 要求你每次签名时都要生成一个随机的 nonce,而 EdDSA 则不需要。另一个例子是在第四章中看到的 AES-GCM-SIV,如果你偶尔重复使用相同的 nonce,它不会发生灾难性的故障,而 AES-GCM 则会泄露认证密钥,然后失去密文的完整性。
### 8.5 公共随机性
到目前为止,我主要谈论了*私密随机性* ,即你可能需要用于私钥的类型。有时,不需要隐私,需要*公共随机性*。在本节中,我简要概述了一些获得此类公共随机性的方法。我区分了两种情况:
* *一对多* ------ 你想为其他人产生随机性。
* *多对多* ------ 一组参与者希望共同产生随机性。
首先,让我们想象一下,你想以一种许多参与者可以验证的方式生成一系列的随机性。换句话说,这个流应该是不可预测的,但是从你的角度来看不可能被更改。现在想象一下,你有一个签名方案,它基于一个密钥对和一个消息提供唯一的签名。有了这样的签名方案,存在一种叫做*可验证随机函数*(VRF)的构造来以可验证的方式获得随机数(图 8.5 说明了这个概念)。以下是它的工作原理:
1. 你生成一个密钥对并公布验证密钥。你还公布了一个公共种子。
2. 为了生成随机数,你对公共种子进行签名并哈希签名。摘要就是你的随机数,签名也被公布为证明。
3. 要验证随机数,任何人都可以对签名进行哈希以检查是否与随机数匹配,并使用公共种子和验证密钥验证签名是否正确。

图 8.5 可验证随机函数(VRF)通过公钥密码学生成可验证的随机性。要生成一个随机数,只需使用一个产生唯一签名的签名方案(如 BLS)对种子进行签名,然后对签名进行哈希以生成公共随机数。要验证生成的随机性,确保签名的哈希确实是随机数,并验证种子上的签名。
这个构造可以通过使用公共种子类似于计数器来产生许多随机数。因为签名是唯一的且公共种子是固定的,签署者无法生成不同的随机数。
练习
像 BLS(在图 8.5 和第七章中提到)这样的签名方案会生成唯一的签名,但对于 ECDSA 和 EdDSA 并非如此。你知道为什么吗?
要解决这个问题,互联网草案(一个旨在成为 RFC 的文档)[`tools.ietf.org/html/draft-irtf-cfrg-vrf-08`](https://tools.ietf.org/html/draft-irtf-cfrg-vrf-08) 指定了如何使用 ECDSA 实现 VRF。在某些场景中(例如,抽奖游戏),几个参与者可能希望随机决定一个赢家。我们称他们为*去中心化随机信标* ,因为他们的角色是即使一些参与者决定不参与协议,也要产生相同的可验证随机性。一个常见的解决方案是使用先前讨论过的 VRF,不是使用单一密钥,而是使用*阈值分布密钥*,即将密钥分割在许多参与者之间,只有在一定数量的参与者签署消息后才为给定消息生成唯一有效签名。这可能听起来有点混乱,因为这是我第一次谈到分布式密钥。请注意,您将在本章后面更多地了解这些内容。
一个流行的去中心化随机信标称为*drand* ,由几个组织和大学共同运行。它可以在[`leagueofentropy.com`](https://tools.ietf.org/html/draft-irtf-cfrg-vrf-08)找到。
*生成良好随机性的主要挑战在于参与随机性生成过程的任何一方都不应能够预测或偏向最终输出。drand 网络不受其任何成员控制。没有单点故障,也没有任何 drand 服务器运营商可以偏向网络生成的随机性*。
---[`drand.love`](https://drand.love)("drand 的工作原理",2021)
现在我已经广泛讨论了随机性以及程序如何获取它,让我们将讨论转向密码学中秘密的作用以及如何管理这些秘密。
### 8.6 使用 HKDF 进行密钥派生
PRNG 并不是唯一可以用来从一个秘密派生更多秘密(换句话说,拉伸密钥)的构造。从一个秘密派生多个秘密实际上是密码学中如此频繁的模式,以至于这个概念有自己的名字:*密钥派生*。所以让我们看看这是什么意思。
*密钥派生函数*(KDF)在许多方面类似于 PRNG,除了以下列表中指出的一些微妙之处。这些差异在图 8.6 中总结。
* *KDF 并不一定需要一个均匀随机的秘密(只要有足够的熵)。* 这使得 KDF 可以从密钥交换输出中派生秘密,产生高熵但有偏差结果的密钥(参见第五章)。结果的秘密反过来是均匀随机的,因此您可以在需要均匀随机密钥的构造中使用这些密钥。
* *KDF 通常用于需要参与者多次重新派生相同密钥的协议中。* 在这个意义上,KDF 被期望是确定性的,而 PRNG 有时通过频繁地使用更多熵重新种子化自身来提供向后保密性。
* *KDF 通常不被设计用来产生大量随机数*。相反,通常用于派生有限数量的密钥。

图 8.6 密钥派生函数(KDF)和伪随机数发生器(PRNG)是两个类似的构造。主要区别在于 KDF 不期望输入是完全均匀随机的秘密(只要具有足够的熵)并且通常不用于生成太多的输出。
最流行的 KDF 是基于 HMAC 的密钥派生函数(HKDF)。您在第三章中学到了 HMAC(基于哈希函数的 MAC)。HKDF 是建立在 HMAC 之上的轻量级 KDF,并在 RFC 5869 中定义。因此,人们可以使用不同的哈希函数来使用 HKDF,尽管它最常用于 SHA-2。HKDF 被指定为两个不同的函数:
* *HKDF-Extract*---从一个秘密输入中移除偏差,产生一个均匀随机的秘密。
* *HKDF-Expand* ---产生任意长度和均匀随机的输出。与伪随机数发生器一样,*它期望一个均匀随机的秘密作为输入*,因此通常在 HKDF-Extract 之后运行。

图 8.7 HKDF-Expand 是由 HKDF 指定的第二个函数。它接受一个可选的`info`字节串和一个需要均匀随机的输入秘密。使用相同的输入秘密与不同的`info`字节串会产生不同的输出。输出的长度由`length`参数控制。
首先让我们看一下 HKDF-Extract,我在图 8.7 中进行了说明。从技术上讲,哈希函数足以使输入字节串的随机性均匀化(请记住,哈希函数的输出应该是不可区分于随机的),但是 HKDF 更进一步,接受一个额外的输入:*盐*。对于密码哈希,盐区分了同一协议中对 HKDF-Extract 的不同用法。虽然这个盐是可选的,如果不使用,则设置为全零字节串,但建议您使用它。此外,HKDF 不期望盐是一个秘密;它可以被所有人,包括对手,知道。HKDF-Extract 不使用哈希函数,而是使用一个 MAC(具体来说是 HMAC),巧合的是,它有一个接受两个参数的接口。
现在让我们看看 HKDF-Expand,我在图 8.8 中进行了说明。如果您的输入秘密已经是均匀随机的,您可以跳过 HKDF-Extract 并使用 HKDF-Expand。

图 8.8 HKDF-Extract 是由 HKDF 指定的第一个函数。它接受一个可选的盐,该盐用作 HMAC 中的密钥,以及可能不是均匀随机的输入秘密。使用相同的输入秘密与不同的盐会产生不同的输出。
与 HKDF-Extract 类似,HKDF-Expand 还接受一个名为`info`的附加和可选的自定义参数。虽然盐旨在在 HKDF(或 HKDF-Extract)的相同协议中的调用之间提供一些域分隔,但`info`旨在用于区分您的 HKDF(或 HKDF-Expand)版本与其他协议。您还可以指定您需要多少输出,但请记住,HKDF 不是 PRNG,并且不设计为导出大量的密钥。HKDF 受您使用的哈希函数的大小限制;更准确地说,如果您使用 SHA-512(产生 512 位输出)与 HKDF,则对于给定的密钥和一个`info`字节字符串,您限于 512 × 255 位 = 16,320 字节的输出。
多次使用相同的参数调用 HKDF 或 HKDF-Expand,除了输出长度之外,会产生相同的输出截断为不同长度的请求(请参阅图 8.9)。此属性称为*相关输出*,在罕见情况下,可能会令协议设计人员感到惊讶。记住这一点是很好的。

图 8.9 HKDF 和 HKDF-Expand 提供相关输出,这意味着使用不同输出长度调用该函数会将相同结果截断为所请求的长度。
大多数密码库将 HKDF-Extract 和 HKDF-Expand 组合成单个调用,如图 8.10 所示。通常,在使用 HKDF 之前,请务必阅读手册(在本例中为 RFC 5869)。

图 8.10 HKDF 通常以单个函数调用的形式实现,该函数同时结合了 HKDF-Extract(从输入密钥中提取均匀随机性)和 HKDF-Expand(生成任意长度的输出)。
HKDF 并不是从一个秘密中导出多个秘密的唯一方法。更为朴素的方法是使用*哈希函数* 。由于哈希函数不期望均匀随机的输入并产生均匀随机的输出,因此它们适合这项任务。然而,哈希函数并不完美,因为它们的接口不考虑*域分隔*(没有自定义字符串参数),并且它们的输出长度是固定的。最佳做法是在可以使用 KDF 时避免使用哈希函数。尽管如此,一些被广泛接受的算法确实使用哈希函数来实现这一目的。例如,您在第七章学到的 Ed25519 签名方案就是使用 SHA-512 对 256 位密钥进行哈希以产生两个 256 位密钥。
这些函数真的会产生随机输出吗?
理论上,哈希函数的属性并不代表输出是均匀随机的;这些属性仅仅规定了哈希函数应该具备抗碰撞、抗原像和抗第二原像的特性。然而,在现实世界中,我们到处使用哈希函数来实现随机预言机(正如你在第二章学到的那样),因此,我们假设它们的输出是均匀随机的。这与 MAC(在理论上不应产生均匀随机输出,不像第三章中介绍的 PRF 那样)也是一样的,但在实践中,大多数情况下确实如此。这就是为什么 HMAC 被用于 HKDF 的原因。在本书的其余部分,我会假设流行的哈希函数(如 SHA-2 和 SHA-3)和流行的 MAC(如 HMAC 和 KMAC)产生随机输出。
我们在第二章看到的扩展输出函数(XOFs)也可以用作 KDF!记住,XOF
* 不期望均匀随机输入
* 可以产生一个实际上无限大的均匀随机输出
此外,KMAC(第三章介绍的 MAC)没有我之前提到的相关输出问题。实际上,KMAC 的长度参数随机化了算法的输出,有效地起到了额外的定制字符串的作用。
最后,存在低熵输入的边缘情况。例如,考虑密码,相对于 128 位密钥,密码可能相对容易猜测。用于哈希密码的基于密码的密钥派生函数(在第二章中介绍)也可以用于派生密钥。
### 8.7 管理密钥和秘密
好了,一切顺利,我们知道如何生成加密随机数,也知道如何在不同类型的情况下派生秘密。但我们还没有摆脱困境。
现在我们正在使用所有这些加密算法,我们最终需要维护大量的秘密密钥。我们如何存储这些密钥?我们如何防止这些极度敏感的秘密被泄露?如果一个密钥被泄露了,我们该怎么办?这个问题通常被称为*密钥管理*。
*加密是将一系列问题转化为密钥管理问题的工具*。
---Lea Kissner(2019,[`mng.bz/eMrJ`](http://mng.bz/eMrJ))
虽然许多系统选择将密钥留在使用它们的应用程序附近,但这并不意味着应用程序在出现问题时没有任何补救措施。为了应对可能发生的违规行为或泄漏密钥的漏洞,大多数严肃的应用程序采用了两种深度防御技术:
* *密钥轮换*--- 通过为密钥(通常是公钥)关联到期日期,并定期用新密钥替换你的密钥,你可以从可能的妥协中"恢复"。到期日期和轮换频率越短,你就可以更快地替换可能已知给攻击者的密钥。
* *密钥吊销* --- 密钥轮换并不总是足够的,当你听说密钥已被泄露时,你可能希望立即取消一个密钥。因此,一些系统允许你在使用密钥之前询问该密钥是否已被吊销。(你将在下一章关于安全传输中了解更多信息。)
自动化通常是使用这些技术成功的不可或缺的部分,因为一个运转良好的机器在危机时更容易正常工作。此外,你还可以将特定角色与密钥关联起来,以限制妥协的后果。例如,你可以在某个虚构的应用程序中区分两个公钥,公钥 1 仅用于签署交易,而公钥 2 仅用于进行密钥交换。这样,与公钥 2 关联的私钥的妥协不会影响交易签署。
如果不想让密钥留在设备存储介质上,硬件解决方案可以防止密钥被提取。你将在第十三章关于硬件密码学中了解更多信息。
最后,应用程序有许多方式可以委托密钥管理。这在提供*密钥存储* 或*密钥链*的移动操作系统中经常发生,这些系统将为你保留密钥,甚至执行加密操作!
存在一些云应用程序可以访问云密钥管理服务。这些服务允许应用程序委托创建秘密密钥和加密操作,并避免考虑攻击这些方式的许多方法。尽管如此,与硬件解决方案一样,如果应用程序受到妥协,它仍然可以向委托服务发出任何类型的请求。
注意:并没有银弹,你仍应考虑如何检测和应对妥协。
密钥管理是一个棘手的问题,超出了本书的范围,所以我不会过多讨论这个话题。在下一节中,我将介绍试图避免密钥管理问题的加密技术。
### 8.8 使用阈值密码学去分散信任
密钥管理是一个广阔的研究领域,投资其中可能会令人烦恼,因为用户并不总是有资源来实施最佳实践,也没有空间中可用的工具。幸运的是,密码学为那些想减轻密钥管理负担的人提供了一些东西。我将首先讨论的是*秘密共享* (或*秘密分割*)。秘密分割允许你将一个秘密分成多个部分,可以在一组参与者之间共享。在这里,秘密可以是任何你想要的东西:对称密钥、签名私钥等等。
通常,一个称为*经销商* 的人生成秘密,然后将其拆分并将不同的部分分享给所有参与者,然后删除秘密。最著名的秘密分享方案由 Adi Shamir(RSA 的共同发明人之一)发明,称为*Shamir 的秘密分享*(SSS)。我在图 8.11 中说明了这个过程。

图 8.11 给定一个密钥和一些份额\* n *,Shamir 的秘密分享方案创建与原始密钥大小相同的* n \*部分密钥。
当时机成熟并且需要秘密来执行一些加密操作(加密、签名等)时,所有股东都需要将他们的私密份额归还给负责重建原始秘密的经销商。这种方案防止了攻击者针对单个用户,因为每个份额本身都是无用的,而是迫使攻击者在利用密钥之前先妥协所有参与者!我在图 8.12 中说明了这一点。

图 8.12 Shamir 的秘密分享方案用于分割\* n *部分密钥以重构原始密钥需要所有* n \*部分密钥。
该方案算法背后的数学实际上并不难理解!所以让我在这里花几段文字给你一个简化的想法。
想象一条二维空间中的随机直线,假设其方程为---\* y \* = \* ax \* + \* b \*---是秘密。通过让两个参与者持有线上的两个随机点,他们可以合作恢复线方程。该方案推广到任何次数的多项式,因此可以用于将秘密分割成任意数量的份额。这在图 8.13 中有所说明。

图 8.13 Shamir 的秘密分享方案背后的想法是将定义曲线的多项式视为秘密,将曲线上的随机点视为部分密钥。要恢复定义曲线的次数为\* n *的多项式,需要知道曲线上的* n \* + 1 个点。例如,\* f *(* x *)= 3 \* x \* + 5 是 1 次,因此您需要任何两个点(* x *,* f *(* x *))来恢复多项式,而* f *(* x \*)= 5 \* x \* ² + 2 \* x \* + 3 是 2 次,因此您需要任何三个点来恢复多项式。
秘密分割是一种常用的技术,因其简单性而被广泛采用。然而,为了有用,密钥份额必须收集到一个地方,以便在每次用于加密操作时重新创建密钥。这会创建一个窗口期,其中秘密变得容易受到盗窃或意外泄漏的机会,有效地使我们回到了一个*单点故障*模型。为了避免这种单点故障问题,在不同场景中存在几种有用的加密技术。
例如,想象一个只有被 Alice 签署的财务交易才能被接受的协议。这给 Alice 带来了很大的负担,她可能害怕成为攻击者的目标。为了减少对 Alice 攻击的影响,我们可以改变协议,接受(在同一交易中)来自 *n* 个不同公钥的 *n* 个签名,其中包括 Alice 的签名。攻击者必须破坏所有 *n* 个签名才能伪造有效交易!这种系统被称为 *多重签名* (通常缩写为 *multi-sig*),在加密货币领域被广泛采用。
然而,天真的多重签名方案可能会增加一些烦人的开销。实际上,在我们的示例中,交易的大小随所需签名数量的增加而线性增长。为了解决这个问题,一些签名方案(如 BLS 签名方案)可以将多个签名压缩成一个。这被称为 *签名聚合* 。一些多重签名方案甚至通过允许将 *n* 个公钥聚合成一个单一公钥来进一步压缩。这种技术被称为 *分布式密钥生成* (DKG),是一种称为 *安全多方计算* 的密码学领域的一部分,我将在第十五章中介绍。
DKG 让 *n* 个参与者在计算公钥时不需要在过程中明文存储相关私钥(与 SSS 不同,没有经销商)。如果参与者想要签署一条消息,他们可以协作使用每个参与者的私密份额来创建签名,这些签名可以使用他们之前创建的公钥进行验证。再次强调,私钥在物理上从未存在,避免了 SSS 存在的单点故障问题。因为你在第七章看到了 Schnorr 签名,图 8.14 展示了简化的 Schnorr DKG 方案背后的直觉。

图 8.14 Schnorr 签名方案可以分散为分布式密钥生成方案。
最后,请注意
* 我提到的每种方案都可以在只有 *n* 个参与者中的阈值 *m* 参与协议时运行。这对于大多数现实世界系统必须容忍一些恶意或不活跃的参与者非常重要。
* 这些类型的方案可以与其他非对称加密算法一起使用。例如,使用阈值加密,一组参与者可以协作地对一条消息进行非对称解密。
我在图 8.15 中回顾了所有这些示例。

图 8.15 对将我们对一个参与者的信任分割为多个参与者的现有技术进行了回顾。
阈值方案是密钥管理领域的一个重要新范式,跟踪它们的发展是一个好主意。NIST 目前有一个阈值密码学组,组织研讨会,并有意在长远的未来标准化原语和协议。
### 摘要
* 如果一个数字是与该集合中的所有其他数字相比以相等的概率选择的,则从集合中均匀且随机地获取一个数字。
* 熵是衡量字节串具有多少随机性的度量标准。高熵指的是均匀随机的字节串,而低熵指的是容易猜测或预测的字节串。
* 伪随机数生成器(PRNGs)是一种算法,它以均匀随机的种子生成(实际上)几乎无限数量的随机性,如果种子足够大,则可以用于加密目的(例如作为加密密钥)。
* 要获取随机数,应该依赖于编程语言的标准库或其知名的加密库。如果这些不可用,操作系统通常提供接口来获取随机数:
* Windows 提供了`BCryptGenRandom`系统调用。
* Linux 和 FreeBSD 提供了`getrandom`系统调用。
* 其他类 Unix 操作系统通常有一个名为`/dev/urandom`的特殊文件,显示出随机性。
* 密钥派生函数(KDF)在希望从偏向但熵值高的秘密派生密钥的场景中非常有用。
* HKDF(基于 HMAC 的密钥派生函数)是最广泛使用的 KDF,基于 HMAC。
* 密钥管理是保持秘密的领域,主要包括找到存储秘密的位置、积极地过期和轮换秘密、确定秘密被泄露时该做什么等。
* 为了减轻密钥管理的负担,可以将一个参与者的信任分散到多个参与者中。
## 第二部分:协议:密码学的配方
你现在要进入本书的第二部分,这一部分将充分利用你在第一部分学到的知识。可以这样理解:如果你在第一部分学到的密码学原语是密码学的基本成分,那么你现在要学习的就是一些配方。而要做的菜有很多!虽然凯撒大帝可能只对加密他的通信感兴趣,但如今的密码学无处不在,要跟踪这一切是相当困难的。
在第 9、10 和 11 章中,我会告诉你最有可能遇到密码学的地方以及密码学是如何用于解决现实问题的;也就是说,密码学是如何加密通信以及如何对协议参与者进行认证的。在很大程度上,这就是密码学的内容。参与者可能众多也可能少数,由比特或者血肉构成。你很快就会意识到,现实世界的密码学涉及到各种权衡,并且根据不同的背景,解决方案也会有所不同。
第 12 和 13 章带你进入两个迅速发展的密码学领域:加密货币和硬件密码学。前者的话题被大多数密码学书籍所忽视。(我相信这本书,《现实世界的密码学》,是第一本包含加密货币章节的密码学书籍。)后者,硬件密码学,也经常被忽视;密码学家常常假设他们的原语和协议在受信任的环境中运行,而这种情况越来越少见。硬件密码学是关于推动密码学运行的边界,并在攻击者越来越接近你的时候提供安全保障。
在第 14 和第十五章中,我涉及到了最前沿的内容:还未出现但即将出现的内容以及现阶段已经存在的内容。你将了解到后量子密码学,这是一个取决于我们作为人类是否发明出可扩展的量子计算机而可能有用的密码学领域。这些基于量子物理领域新范式的量子计算机可能会彻底改变研究,并且,也许甚至会打破我们的加密...... 你还将了解到我所称之为"下一代密码学"的内容,这些密码学原语很少被使用,但随着这些原语被研究、变得更加高效并被应用设计者采用,你很可能会更频繁地看到它们。最后,在第十六章中,我就现实世界的密码学做了一些最终的备注,并就伦理问题发表了一些看法。
## 第九章:安全传输
这一章涵盖了
* 安全传输协议
* 传输层安全协议(TLS)
* 噪声协议框架
今天加密通信最大的使用量可能是为了加密通信。毕竟,加密学就是为了这个目的而发明的。为了做到这一点,应用程序通常不直接使用像认证加密这样的加密原语,而是使用更复杂的协议来抽象加密原语的使用。我将这些协议称为*安全传输协议*,因为没有更好的术语。
在本章中,你将了解到最广泛使用的安全传输协议:传输层安全协议(TLS)。我也会简要介绍其他安全传输协议以及它们与 TLS 的区别。
### 9.1 SSL 和 TLS 安全传输协议
为了理解为什么*传输协议* (用于加密机器间通信的协议)是必要的,让我们通过一个激励场景来走一遍。当你在浏览器中输入,比如说,`http://example.com`,你的浏览器会使用多个协议来连接到一个网络服务器并获取你请求的页面。其中一个是*超文本传输协议*(HTTP),你的浏览器用它来告诉另一边的网络服务器它感兴趣的是哪个页面。HTTP 使用的是一种人类可读的格式。这意味着你可以查看正在通过网络发送和接收的 HTTP 消息,并且不需要任何其他工具就可以阅读它们。但这对于你的浏览器来与网络服务器通信还不够。
HTTP 消息被封装到其他类型的消息中,称为*TCP 帧* ,这些帧在传输控制协议(TCP)中定义。TCP 是一个二进制协议,因此,它不是人类可读的:你需要一个工具来理解 TCP 帧的字段。TCP 消息进一步被封装到 Internet 协议(IP)中,并且 IP 消息进一步被封装到其他东西中。这被称为*Internet 协议套件*,因为它是许多书籍的主题,我不会进一步深入讨论这个。
回到我们的场景,因为存在保密性问题,我们需要谈论一下。任何坐在你的浏览器和[example.com](http://example.com)的网络服务器之间的线上的人都有一个有趣的位置:他们可以被动地观察和读取你的请求以及服务器的响应。更糟糕的是,中间人攻击者也可以主动篡改和重新排序消息。这并不好。
想象一下,每次在互联网上购物时您的信用卡信息泄露,每次登录网站时密码被盗,每次向朋友发送图片和私人消息时被窃取等等。这足以让足够多的人感到恐慌,以至于在 1990 年代,TLS 的前身------*安全套接字层* (SSL)*协议* 诞生了。虽然 SSL 可以用于不同类型的情况,但它最初是由网页浏览器构建和用于的。因此,它开始与 HTTP 一起使用,将其扩展为*超文本传输安全协议*(HTTPS)。现在,HTTPS 允许浏览器将其与访问的不同网站之间的通信安全地连接起来。
#### 9.1.1 从 SSL 到 TLS
尽管 SSL 并不是唯一尝试保护网络的协议,但它吸引了大部分关注,并且随着时间的推移,已成为事实上的标准。但这并不是整个故事。在第一个 SSL 版本和我们今天使用的之间,发生了很多事情。所有版本的 SSL(最后一个是 SSL v3.0)由于设计不良和加密算法不佳的组合而被破解。(许多攻击已在 RFC 7457 中总结。)
在 SSL 3.0 之后,该协议正式转移到了互联网工程任务组(IETF),这是负责发布*请求评论*(RFCs)标准的组织。SSL 的名称被更改为 TLS,TLS 1.0 于 1999 年作为 RFC 2246 发布。TLS 的最新版本是 TLS 1.3,规定在 RFC 8446 中,并于 2018 年发布。与其前身不同,TLS 1.3 源自行业和学术界之间的紧密合作。然而,如今,互联网仍然在许多不同版本的 SSL 和 TLS 之间分裂,因为服务器更新速度缓慢。
注意 关于 SSL 和 TLS 这两个名称存在很多混淆。该协议现在被称为*TLS* ,但许多文章甚至库仍然选择使用术语*SSL*。
TLS 已经不仅仅是保护网络的协议;它现在在许多不同的场景和各种类型的应用程序和设备中被用作保护通信的协议。因此,在本章中学到的关于 TLS 的知识不仅对网络有用,而且对任何需要保护两个应用程序之间通信的场景都有用。
#### 9.1.2 在实践中使用 TLS
人们如何使用 TLS?首先让我们定义一些术语。在 TLS 中,想要保护通信的两个参与者被称为*客户端* 和*服务器*。它的工作方式与其他网络协议(如 TCP 或 IP)相同:客户端是发起连接的一方,服务器是等待连接被发起的一方。一个 TLS 客户端通常是由
* *一些配置*---客户端配置了它想要支持的 SSL 和 TLS 版本,愿意使用的加密算法来保护连接,可以对服务器进行身份验证的方式等。
* *它想要连接的服务器的一些信息* --- 至少包括 IP 地址和端口,但对于 Web,通常会使用完全合格的域名(如 example.com)。
有了这两个参数,客户端就可以与服务器建立连接以建立一个安全的 *会话*,这是客户端和服务器都可以用来相互分享加密消息的通道。在某些情况下,安全会话可能无法成功创建并在中途失败。例如,如果攻击者试图篡改连接,或者服务器的配置与客户端不兼容(稍后详细介绍),客户端将无法建立安全会话。
TLS 服务器通常要简单得多,因为它只需要一个配置,这与客户端的配置类似。然后服务器等待客户端连接以建立一个安全会话。在实践中,在客户端使用 TLS 可以像下面的清单所示那样简单(即,如果你使用像 Golang 这样的编程语言)。
清单 9.1 Golang 中的 TLS 客户端
```go
import "crypto/tls"
func main() {
destination := "google.com:443" // ❶
TLSconfig := &tls.Config{} // ❷
conn, err := tls.Dial("tcp", destination, TLSconfig)
if err != nil {
panic("failed to connect: " + err.Error())
}
conn.Close()
}
```
❶ 完全合格的域名和服务器的端口(443 是 HTTPS 的默认端口)。
❷ 空配置作为默认配置。
客户端如何知道它建立的连接确实是与 [google.com](http://google.com) 而不是某个冒名顶替者?默认情况下,Golang 的 TLS 实现使用您操作系统的配置来确定如何对 TLS 服务器进行身份验证。(本章后面,您将了解 TLS 中身份验证的确切工作原理。)在服务器端使用 TLS 也非常简单。下面的清单展示了这是多么简单。
清单 9.2 Golang 中的 TLS 服务器
```go
import (
"crypto/tls"
"net/http"
)
func hello(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte("Hello, world\n"))
}
func main() {
config := &tls.Config{ // ❶
MinVersion: tls.VersionTLS13, // ❶
} // ❶
http.HandleFunc("/", hello) // ❷
server := &http.Server{ // ❸
Addr: ":8080", // ❸
TLSConfig: config, // ❸
}
cert := "cert.pem"
key := "key.pem"
err := server.ListenAndServeTLS(cert, key) // ❹
if err != nil {
panic(err)
}
}
```
❶ TLS 1.3 服务器的稳定最小配置
❷ 提供一个显示"Hello, world"的简单页面。
❸ 在端口 8080 上启动一个 HTTPS 服务器。
❹ 包含证书和私钥的一些 .pem 文件(稍后详细介绍)
Golang 及其标准库在这方面为我们做了很多工作。不幸的是,并非所有语言的标准库都提供易于使用的 TLS 实现,如果提供的话,也并非所有 TLS 库都提供默认安全的实现!因此,根据库的不同,配置 TLS 服务器并不总是直截了当的。在下一节中,你将了解 TLS 的内部工作原理及其不同的微妙之处。
注意 TLS 是在 TCP 之上运行的协议。为了保护 UDP 连接,我们可以使用 DTLS(*D* 代表 *数据报*,即 UDP 消息的术语),它与 TLS 非常相似。因此,本章中我忽略了 DTLS。
### 9.2 TLS 协议是如何工作的?
正如我之前所说,如今 TLS 是保护应用程序之间通信的事实标准。在本节中,您将了解 TLS 在表面下如何工作以及它在实践中的使用方式。您会发现这一节对于学习如何正确使用 TLS 以及理解大多数(如果不是全部)安全传输协议如何工作非常有用。您还将了解为什么重新设计或重新实现这些协议是困难的(并且强烈不建议)。
在高层次上,TLS 分为两个阶段,如下列表所示。图 9.1 说明了这个概念。
* *握手阶段*---两个参与者之间协商并创建了安全通信。
* *后握手阶段*---两个参与者之间的通信被加密。

图 9.1 在高层次上,安全传输协议首先在握手阶段创建安全连接。之后,安全连接两侧的应用程序可以安全通信。
此时,由于您在第六章学习了混合加密,您应该对这两个步骤的工作原理有以下(正确的)直觉:
* *握手本质上只是一个密钥交换过程。* 握手最终导致两个参与者就一组对称密钥达成一致。
* *后握手阶段纯粹是关于在参与者之间加密消息。* 这个阶段使用经过认证的加密算法和在握手结束时产生的密钥集。
大多数传输安全协议都是这样工作的,这些协议的有趣部分总是在握手阶段。接下来,让我们看看握手阶段。
#### 9.2.1 TLS 握手
正如您所见,TLS(以及大多数传输安全协议)分为两部分:*握手* 和*后握手*阶段。在本节中,您将首先了解握手。握手本身有四个方面,我想告诉您:
* *协商*---TLS 高度可配置。客户端和服务器都可以配置为协商一系列 SSL 和 TLS 版本以及一组可接受的加密算法。握手的协商阶段旨在在客户端和服务器的配置之间找到共同点,以便安全连接两个对等方。
* *密钥交换*---握手的整个目的是在两个参与者之间执行密钥交换。要使用哪种密钥交换算法?这是客户端/服务器协商过程的一部分决定的事情之一。
* *认证*---正如您在第五章中学到的关于密钥交换的知识,中间人攻击者可以轻易冒充密钥交换的任何一方。因此,密钥交换必须经过认证。例如,您的浏览器必须有一种方式来确保它正在与 google.com 通信,而不是您的互联网服务提供商(ISP)。
* *会话恢复*---由于浏览器经常连接到同一网站,密钥交换可能成本高昂,可能会减慢用户体验。因此,TLS 集成了快速跟踪安全会话而无需重新进行密钥交换的机制。
这是一个全面的列表!像闪电一样快,让我们从第一项开始。
TLS 中的协商:选择哪个版本和哪些算法?
TLS 中的大部分复杂性来自协议的不同部分的协商。臭名昭著的是,这种协商也是 TLS 历史上许多问题的根源。像 FREAK、LOGJAM、DROWN 等攻击利用旧版本中存在的弱点来破坏协议的更近期版本(有时甚至在服务器不支持旧版本的情况下!)。虽然并非所有协议都具有版本控制或允许协商不同算法,但 SSL/TLS 是为网络设计的。因此,SSL/TLS 需要一种方式来与可能更新缓慢的旧客户端和服务器保持向后兼容性。
这就是今天网络上发生的事情:你的浏览器可能是最新的,支持 TLS 版本 1.3,但当访问一些旧网页时,很可能其背后的服务器只支持 TLS 版本 1.2 或 1.1(或更糟糕)。反之亦然,许多网站必须支持旧浏览器,这意味着支持旧版本的 TLS(因为一些用户仍停留在过去)。
旧版 SSL 和 TLS 安全吗?
大多数 SSL 和 TLS 版本都存在安全问题,除了 TLS 版本 1.2 和 1.3。为什么不只支持最新版本(1.3)并结束呢?原因在于一些公司支持无法轻松更新的旧客户端。由于这些要求,通常会发现库实施对已知攻击的缓解措施,以安全地支持旧版本。不幸的是,这些缓解措施通常太复杂,难以正确实施。
例如,像 Lucky13 和 Bleichenbacher98 这样的著名攻击一再被安全研究人员在各种 TLS 实现中重新发现,这些实现先前曾试图修复这些问题。虽然可以减轻对旧版 TLS 的一些攻击,但我建议不要这样做,而且我不是唯一一个这样告诉你的人。2021 年 3 月,IETF 发布了 RFC 8996:"淘汰 TLS 1.0 和 TLS 1.1",从而正式宣布了淘汰。
协商始于客户端向服务器发送第一个请求(称为*ClientHello*)。ClientHello 包含一系列支持的 SSL 和 TLS 版本,客户端愿意使用的一套加密算法,以及可能与握手的其余部分或应用程序相关的其他信息。加密算法套件包括
* *一个或多个密钥交换算法*------TLS 1.3 定义了用于协商的以下算法:ECDH 与 P-256、P-384、P-521、X25519、X448,以及 FFDH 与 RFC 7919 中定义的群。这些内容在第五章中有介绍。TLS 的先前版本也提供了 RSA 密钥交换(在第六章中介绍),但它们已在最新版本中删除。
* *握手的不同部分需要两个或更多数字签名算法*------TLS 1.3 规定了 RSA PKCS#1 版本 1.5 和更新的 RSA-PSS,以及更近期的椭圆曲线算法如 ECDSA 和 EdDSA。这些内容在第七章有介绍。请注意,数字签名是用散列函数指定的,这使得你可以协商使用,例如,RSA-PSS 与 SHA-256 或 SHA-512。
* *用于 HMAC 和 HKDF 的一个或多个散列函数*------TLS 1.3 指定了 SHA-256 和 SHA-384,这是 SHA-2 散列函数的两个实例。(你在第二章学习过 SHA-2。)这种散列函数的选择与数字签名算法使用的散列函数无关。作为提醒,HMAC 是你在第三章学习的消息认证码,而 HKDF 是我们在第八章介绍的密钥派生函数。
* *一个或多个经过身份验证的加密算法*------这些可以包括 128 位或 256 位密钥的 AES-GCM,ChaCha20-Poly1305 和 AES-CCM。这些内容在第四章有介绍。
然后服务器以*ServerHello*消息回复,其中包含从客户端的选择中精选出的每种类型的加密算法。下图描述了这个响应。

如果服务器无法找到支持的算法,它将中止连接。但在某些情况下,服务器不必中止连接,而是可以要求客户端提供更多信息。为此,服务器将以一条称为*HelloRetryRequest*的消息回复,要求提供缺失的信息。然后客户端可以重新发送其 ClientHello,这次带上额外请求的信息。
TLS 和前向安全密钥交换
密钥交换是 TLS 握手中最重要的部分!没有它,显然就没有对称密钥的协商。但是要进行密钥交换,客户端和服务器必须首先交换各自的公钥。
在 TLS 1.2 和之前的版本中,客户端和服务器只有在双方同意使用哪种密钥交换算法后才开始密钥交换。这发生在协商阶段。TLS 1.3 通过尝试同时进行协商和密钥交换来优化这个流程:客户端推测选择一个密钥交换算法,并在第一条消息(ClientHello)中发送一个公钥。如果客户端未能预测服务器选择的密钥交换算法,则客户端回退到协商的结果,并发送包含正确公钥的新 ClientHello。以下步骤描述了这种情况可能是什么样子。我在图 9.2 中说明了这种差异。
1. 客户端发送一个 TLS 1.3 ClientHello 消息,宣布它可以执行 X25519 或 X448 密钥交换。它还发送了一个 X25519 公钥。
2. 服务器不支持 X25519,但支持 X448。它向客户端发送一个 HelloRetryRequest,宣布它只支持 X448。
3. 客户端发送相同的 ClientHello,但是使用 X448 公钥。
4. 握手继续进行。

图 9.2 在 TLS 1.2 中,客户端在发送公钥之前等待服务器选择要使用的密钥交换算法。在 TLS 1.3 中,客户端推测服务器将选择哪种密钥交换算法,并在第一条消息中预先发送一个(或多个)公钥,可能避免额外的往返。
TLS 1.3 中充满了这样的优化,对于网络来说非常重要。事实上,全球许多人拥有不稳定或缓慢的连接,保持非应用通信的最低限度是非常重要的。此外,在 TLS 1.3 中(与之前的 TLS 版本不同),所有密钥交换都是*临时* 的。这意味着对于每个新会话,客户端和服务器都会生成新的密钥对,然后在密钥交换完成后将其丢弃。这为密钥交换提供了*前向保密性*:客户端或服务器的长期密钥泄露不会允许攻击者解密此会话,只要临时私钥被安全删除。
想象一下,如果一个 TLS 服务器在与客户端执行每次密钥交换时都使用单个私钥会发生什么。通过执行临时密钥交换并在握手结束后立即摆脱私钥,服务器可以防止此类攻击者。我在图 9.3 中进行了说明。

图 9.3 在 TLS 1.3 中,每个会话都以临时密钥交换开始。如果服务器在某个时间点被攻破,之前的会话不会受到影响。
练习
如果服务器的私钥在某个时间点被泄露,那么中间人攻击者将能够解密所有先前记录的对话。你明白这是如何发生的吗?
一旦临时公钥交换完成,就会执行密钥交换,并且可以推导出密钥。TLS 1.3 在不同时间点推导出不同的密钥,以使用独立密钥加密不同的阶段。
前两条消息,即 ClientHello 和 ServerHello,在此时不能加密,因为此时没有交换公钥。但是在此之后,一旦密钥交换发生,TLS 1.3 就会加密握手的其余部分。(这与之前的 TLS 版本不同,之前的版本没有加密任何握手消息。)
为了推导出不同的密钥,TLS 1.3 使用与协商的哈希函数的 HKDF。在密钥交换的输出上使用 HKDF-Extract 来消除任何偏差,而使用不同的 `info` 参数与 HKDF-Expand 来推导出加密密钥。例如,`tls13` `c` `hs` `traffic`(表示"客户端握手流量")用于推导出客户端在握手期间加密到服务器的对称密钥,而 `tls13` `s` `ap` `traffic`(表示"服务器应用流量")用于推导出服务器在握手之后加密到客户端的对称密钥。请记住,*未经身份验证* 的密钥交换是不安全的!接下来,您将看到 TLS 如何解决此问题。
TLS 身份验证和 web 公钥基础设施
经过一些协商和密钥交换之后,握手必须继续。接下来发生的是 TLS 的另一个最重要的部分 ------ *身份验证*。在密钥交换的第五章中,您看到拦截密钥交换并冒充密钥交换的一方或双方是微不足道的。在本节中,我将解释您的浏览器如何通过密码验证确保它正在与正确的网站通信,而不是与冒充者通信。但首先,让我们退一步。实际上,TLS 1.3 握手分为三个不同的阶段(如图 9.4 所示):
1. *密钥交换* ------ 此阶段包含提供一些协商并执行密钥交换的 *ClientHello* 和 *ServerHello* 消息。此阶段之后的所有消息,包括握手消息,在此阶段之后都将被加密。
2. *服务器参数* ------ 此阶段的消息包含来自服务器的附加协商数据。这是不必包含在服务器的第一条消息中的协商数据,但是可以受益于加密。
3. *身份验证* ------ 此阶段包括来自服务器和客户端的身份验证信息。

图 9.4 TLS 1.3 握手分为三个阶段:密钥交换阶段、服务器参数阶段以及(最后)身份验证阶段。
在网络上,TLS 中的身份验证通常是单向的。只有浏览器验证例如 google.com 是否确实是 google.com,但是 google.com 不验证您是谁(或至少不是作为 TLS 的一部分)。
双向认证的 TLS
客户端认证通常通过应用层进行,最常见的方式是通过一个表单要求您输入凭据。也就是说,如果服务器在服务器参数阶段请求,客户端认证也可以在 TLS 中发生。当连接的双方都经过认证时,我们称之为*相互认证的 TLS*(有时缩写为 mTLS)。
客户端认证与服务器认证的方式相同。这可以在服务器认证之后的任何时候发生(例如,在握手期间或在握手后阶段)。
现在让我们回答一个问题,"当连接到 google.com 时,您的浏览器如何验证您确实正在与 google.com 握手?"答案是通过使用*web 公钥基础设施(web PKI)*。
在第七章关于数字签名中,您了解了公钥基础设施的概念,但让我简要地重新介绍一下这个概念,因为它在理解 Web 运作方式方面非常重要。Web PKI 有两个方面。首先,浏览器必须信任一组我们称之为*证书颁发机构*(CAs)的根公钥。通常,浏览器要么使用一组硬编码的受信任公钥,要么依赖操作系统提供它们。
web PKI
对于 Web,存在数百家由世界各地不同公司和组织独立运行的这些 CA。这是一个相当复杂的系统,这些 CA 有时也可以签署中间 CA 的公钥,而中间 CA 反过来也有权签署网站的公钥。因此,像*证书颁发机构浏览器论坛*(CA/Browser Forum)这样的组织制定规则,并决定何时新组织可以加入受信任公钥集合,或者何时 CA 不再可信并必须从该集合中移除。
其次,想要使用 HTTPS 的网站必须有一种方式从这些 CA 那里获取认证(对其签名公钥的签名)。为了做到这一点,网站所有者(或者我们过去常说的网站管理员)必须向 CA 证明他们拥有特定的域名。
注意:为自己的网站获取证书过去需要支付费用。现在情况已经不同了,因为像 Let's Encrypt 这样的 CA 提供免费证书。
要证明你拥有 example.com,例如,CA 可能会要求你在 example.com/some_path/file.txt 上托管一个包含为你的请求生成的一些随机数字的文件。以下漫画展示了这个交换过程。

在此之后,CA 可以对网站的公钥提供签名。由于 CA 的签名通常有效期数年,我们称其为长期签名公钥(与临时公钥相对)。更具体地说,CA 实际上并不签署公钥,而是签署*证书*(稍后详细介绍)。证书包含长期公钥,以及一些额外重要的元数据,如网页的域名。
为了向您的浏览器证明其正在与 google.com 通信,服务器在 TLS 握手的一部分发送一个*证书链*。该链包括
* 其自身的叶子证书,包含(其他内容)域名([google .com](http://google.com),例如),谷歌的长期签名公钥,以及 CA 的签名
* 从签署谷歌证书的中间 CA 证书链到签署最后一个中间 CA 的根 CA 的一系列中间 CA 证书
这有点冗长,所以我在图 9.5 中进行了说明。

图 9.5 Web 浏览器只需信任相对较小的一组根 CA 即可信任整个网络。这些 CA 存储在所谓的*信任存储*中。为了让浏览器信任网站,该网站必须将其叶子证书签名为这些 CA 之一。有时根 CA 只签署中间 CA,然后中间 CA 签署其他中间 CA 或叶子证书。这就是所谓的 Web PKI。
服务器通过 TLS 消息和客户端发送证书链,就好像要求客户端进行身份验证一样。随后,服务器可以使用其经过认证的长期密钥对来签署所有已接收和先前发送的握手消息,这称为*CertificateVerify*消息。图 9.6 回顾了这个流程,其中只有服务器对自己进行身份验证。
CertificateVerify 消息中的签名向客户端证明了服务器目前所见的内容。如果没有此签名,中间人攻击者可以拦截服务器的握手消息,并替换 ServerHello 消息中包含的服务器的临时公钥,从而使攻击者能够成功冒充服务器。请花点时间理解在 CertificateVerify 签名存在的情况下,攻击者为何不能替换服务器的临时公钥。

图 9.6 握手的身份验证部分始于服务器向客户端发送证书链。证书链以叶子证书开始(包含网站的公钥和附加元数据,如域名),并以浏览器信任的根证书结束。每个证书都包含上面证书的签名。
故事时间
几年前,我被聘请来审查一个大公司制作的自定义 TLS 协议。结果他们的协议让服务器提供了一个不包含临时密钥的签名。当我告诉他们这个问题时,整个房间沉默了整整一分钟。这当然是一个重大错误:一个能够拦截自定义握手并用自己的密钥替换临时密钥的攻击者将成功冒充服务器。
这里的教训是重复造轮子很重要。安全传输协议很难正确实现,如果历史已经表明了什么,那就是它们可能以许多意想不到的方式失败。相反,你应该依赖于成熟的协议如 TLS,并确保你使用的是一个受到大量公众关注的流行实现。
最后,为了正式结束握手,连接的双方都必须在身份验证阶段发送一个 *Finished* 消息。Finished 消息包含一个由 HMAC 生成的认证标签,用于与会话协商的哈希函数。这允许客户端和服务器告诉对方,"这些是我在这个握手过程中发送和接收的所有消息的顺序。"如果握手被中间人攻击者拦截和篡改,这个完整性检查允许参与者检测并中止连接。这尤其有用,因为一些握手模式*没有*签名(稍后详细介绍)。
在继续谈握手的不同方面之前,让我们先来看看 X.509 证书。它们是许多密码协议的重要细节。
通过 X.509 证书进行身份验证
虽然在 TLS 1.3 中证书是可选的(您始终可以使用普通密钥),但许多应用程序和协议,不仅仅是网络,都大量使用它们来认证额外的元数据。具体来说,使用了 X.509 证书标准第 3 版。
X.509 是一个相当古老的标准,旨在足够灵活,可以用于多种场景:从电子邮件到网页。X.509 标准使用了一种称为 *Abstract Syntax Notation One* (ASN.1) 的描述语言来指定证书中包含的信息。在 ASN.1 中描述的数据结构如下所示:
```go
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
```
你可以把它看作是一个包含三个字段的结构:
* `tbsCertificate` --- 待签名的证书。这包含了想要认证的所有信息。对于网络,这可以包含域名(例如 google.com)、公钥、过期日期等。
* `signatureAlgorithm` --- 用于签署证书的算法。
* `signatureValue` --- 来自 CA 的签名。
练习
值 `signatureAlgorithm` 和 `signatureValue` 不包含在实际的证书 `tbsCertificate` 中。你知道为什么吗?
您可以通过使用 HTTPS 连接到任何网站,然后使用浏览器功能观察服务器发送的证书链来轻松检查 X.509 证书中的内容。请参见图 9.7 以获取示例。

图 9.7 使用 Chrome 的证书查看器,我们可以观察到谷歌服务器发送的证书链。根 CA 是 Global Sign,这是您的浏览器信任的。在链中,一个名为 GTS CA 101 的中间 CA 由于其证书包含来自 Global Sign 的签名而受到信任。反过来,谷歌的叶子证书,适用于\*.google.com(google.com,mail.google.com 等),包含来自 GTS CA 101 的签名。
您可能会遇到以.pem 文件形式存在的 X.509 证书,其中包含一些被 base64 编码的内容,周围包含一些人类可读的提示,说明 base64 编码的数据包含的内容(这里是一个证书)。以下代码片段表示.pem 格式证书的内容:
```go
-----BEGIN CERTIFICATE-----
MIIJQzCCCCugAwIBAgIQC1QW6WUXJ9ICAAAAAEbPdjANBgkqhkiG9w0BAQsFADBC
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMRMw
EQYDVQQDEwpHVFMgQ0EgMU8xMB4XDTE5MTAwMzE3MDk0NVoXDTE5MTIyNjE3MDk0
NVowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT
[...]
vaoUqelfNJJvQjJbMQbSQEp9y8EIi4BnWGZjU6Q+q/3VZ7ybR3cOzhnaLGmqiwFv
4PNBdnVVfVbQ9CxRiplKVzZSnUvypgBLryYnl6kquh1AJS5gnJhzogrz98IiXCQZ
c7mkvTKgCNIR9fedIus+LPHCSD7zUQTgRoOmcB+kwY7jrFqKn6thTjwPnfB5aVNK
dl0nq4fcF8PN+ppgNFbwC2JxX08L1wEFk2LvDOQgKqHR1TRJ0U3A2gkuMtf6Q6au
3KBzGW6l/vt3coyyDkQKDmT61tjwy5k=
-----END CERTIFICATE-----
```
如果您解码被`BEGIN` `CERTIFICATE`和`END` `CERTIFICATE`包围的 base64 内容,您将得到一个*Distinguished Encoding Rules* (DER)编码的证书。DER 是一种*确定性*(只有一种编码方式)的二进制编码,用于将 X.509 证书转换为字节。所有这些编码对新手来说通常相当令人困惑!我在图 9.8 中总结了所有这些。

图 9.8 在左上角,使用 ASN.1 表示法编写了一个 X.509 证书。然后将其转换为可以通过 DER 编码进行签名的字节。由于这不是可以轻松复制或被人类识别的文本,因此进行了 base64 编码。最后一步是使用 PEM 格式将 base64 数据包装在一些方便的上下文信息中。
DER 只编码信息为"这是一个整数"或"这是一个字节数组"。在编码后,ASN.1 中描述的字段名称(如`tbsCertificate`)将丢失。因此,如果没有原始 ASN.1 描述每个字段真正含义的知识,解码 DER 就毫无意义。像 OpenSSL 这样的便捷命令行工具允许您解码和将 DER 编码的证书内容翻译成人类术语。例如,如果您下载 google.com 的证书,您可以使用以下代码片段在终端中显示其内容。
```go
$ openssl x509 -in google.pem -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0b:54:16:e9:65:17:27:d2:02:00:00:00:00:46:cf:76
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = Google Trust Services, CN = GTS CA 1O1
Validity
Not Before: Oct 3 17:09:45 2019 GMT
Not After : Dec 26 17:09:45 2019 GMT
Subject: C = US, ST = California, L = Mountain View, O = Google LLC,
CN = *.google.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:74:25:79:7d:6f:77:e4:7e:af:fb:1a:eb:4d:41:
b5:27:10:4a:9e:b8:a2:8c:83:ee:d2:0f:12:7f:d1:
77:a7:0f:79:fe:4b:cb:b7:ed:c6:94:4a:b2:6d:40:
5c:31:68:18:b6:df:ba:35:e7:f3:7e:af:39:2d:5b:
43:2d:48:0a:54
ASN1 OID: prime256v1
NIST CURVE: P-256
[...]
```
尽管如此,X.509 证书颇具争议。在 2012 年,一组研究人员将验证 X.509 证书戏称为"世界上最危险的代码"。这是因为 DER 编码是一个难以正确解析的协议,而 X.509 证书的复杂性可能导致许多错误具有潜在的破坏性。因此,我不建议任何现代应用程序使用 X.509 证书,除非必须使用。
预共享密钥和 TLS 中的会话恢复,或者如何避免密钥交换
密钥交换可能是昂贵的,有时是不必要的。例如,您可能有两台只连接到彼此的机器,并且您可能不想为了保护它们之间的通信而处理公钥基础结构。TLS 1.3 提供了一种使用*预共享密钥*(PSKs)避免这种开销的方法。PSK 只是客户端和服务器都知道的一个密钥,可以用来为会话导出对称密钥。
在 TLS 1.3 中,PSK 握手的工作原理是使客户端在其 ClientHello 消息中宣布它支持一系列 PSK 标识符。如果服务器识别其中一个 PSK ID,它可以在其响应中(ServerHello 消息)表示如此,然后双方可以避免进行密钥交换(如果他们想要的话)。通过这样做,认证阶段被跳过,使得握手结束时的 Finished 消息成为防止中间人攻击的重要手段。
客户端随机和服务器随机
一个热心的读者可能已经注意到,临时公钥为会话带来了随机性,如果没有它们,握手结束时的对称会话密钥可能始终相同。为不同的会话使用不同的对称密钥非常重要,因为您不希望这些会话被关联起来。更糟糕的是,由于会话之间的加密消息可能不同,这可能导致使用 nonce 重用及其灾难性后果(见第四章)。
为了减轻这一点,客户端 Hello 和服务器 Hello 消息都有一个`random`字段,为每个新会话随机生成(通常称为*客户端随机* 和*服务器随机*)。由于这些随机值用于在 TLS 中导出对称密钥,因此它有效地为每个新连接的会话对称密钥进行了随机化。
PSK 的另一个用例是*会话恢复*。会话恢复是指重用从先前会话或连接创建的密钥的过程。如果您已经连接到 google.com 并已验证其证书链,执行了密钥交换,同意了共享密钥等等,为什么在几分钟或几小时后重新访问时还要再做一次这个过程呢?TLS 1.3 提供了一种在成功执行握手后生成 PSK 的方法,该方法可用于后续连接,以避免必须重新执行完整的握手。
如果服务器想提供此功能,它可以在后握手阶段的任何时候发送一个新的会话票证消息。服务器可以通过几种方式创建所谓的*会话票证*。例如,服务器可以发送一个与数据库中相关信息关联的标识符。这不是唯一的方式,但由于这种机制相当复杂,而且大多数情况下并不必要,所以我在本章中不会深入讨论更多。接下来,让我们看看 TLS 中最简单的部分------通信最终如何加密。
#### 9.2.2 TLS 1.3 如何加密应用数据
一旦握手完成并派生了对称密钥,客户端和服务器都可以相互发送加密的应用程序数据。TLS 还确保这样的消息不能被重播或重新排序!为了做到这一点,认证加密算法使用的 nonce 从一个固定值开始,并在每个新消息中递增。如果消息被重播或重新排序,nonce 将与预期值不同,解密将失败。当发生这种情况时,连接将被终止。
隐藏明文长度
正如您在第四章中学到的,加密并不总是隐藏被加密内容的长度。TLS 1.3 附带了*记录填充*,您可以配置为在加密之前使用随机数量的零字节填充应用程序数据,从而有效地隐藏消息的真实长度。尽管如此,可能存在去除添加噪声的统计攻击,并且不容易缓解它们。如果您确实需要这种安全属性,您应该参考 TLS 1.3 规范。
从 TLS 1.3 开始,如果服务器决定允许,客户端可以在 ClientHello 消息之后的第一系列消息中发送加密数据。这意味着浏览器不一定需要等到握手结束才开始向服务器发送应用数据。这种机制称为*早期数据* 或*0-RTT*(零往返时间)。它只能与 PSK 的组合一起使用,因为它允许在 ClientHello 消息期间派生对称密钥。
注意 这个特性在 TLS 1.3 标准制定过程中引起了很大争议,因为被动攻击者可以重放观察到的 ClientHello,然后是加密的 0-RTT 数据。这就是为什么只能使用 0-RTT 来传输可以安全重播的应用程序数据。
对于网络,浏览器将每个 GET 查询视为*幂等*,这意味着 GET 查询不应更改服务器端的状态,只能用于检索数据(与 POST 查询不同)。当然,并不总是如此,应用程序可以随心所欲。因此,如果您面临是否使用 0-RTT 的决定,最简单的方法就是不要使用它。
### 9.3 加密网络的当前状态
如今,标准推动废弃所有不是 TLS 版本 1.2 和 TLS 1.3 的 SSL 和 TLS 版本。然而,由于旧客户端和服务器,许多库和应用程序仍然支持协议的旧版本(有时直到 SSL 版本 3!)。这并不是一件简单的事情,由于需要防御的漏洞数量,许多难以实现的缓解措施必须得到维护。
警告 使用 TLS 1.3(和 TLS 1.2)被认为是安全和最佳实践。使用任何更低版本意味着您将需要咨询专家,并且必须想办法避免已知的漏洞。
默认情况下,浏览器仍然使用 HTTP 连接到 Web 服务器,网站仍然必须手动向 CA 申请证书。这意味着使用当前协议,Web 永远不会完全加密,尽管一些估计显示截至 2019 年全球 Web 流量的 90% 已加密。
默认情况下,您的浏览器始终使用不安全的连接也是一个问题。现今的 Web 服务器通常会将通过 HTTP 访问其页面的用户重定向到 HTTPS。Web 服务器还可以(而且通常会)告诉浏览器使用 HTTPS 进行后续连接。这是通过一个名为*HTTP 严格传输安全* (HSTS)的 HTTPS 响应头完成的。然而,对网站的第一次连接仍然不受保护(除非用户考虑在地址栏中键入 `https`),并且可以被拦截以移除到 HTTPS 的重定向。
此外,其他像*NTP* (获取当前时间)和*DNS*(获取域名背后的 IP)等 Web 协议目前主要是未加密的,并容易受到中间人攻击。虽然有研究努力改善现状,但这些都是需要注意的攻击向量。
TLS 用户面临的另一个威胁是行为不端的 CA。如果今天,一个 CA 决定为您的域名签发证书和它控制的公钥,会怎么样?如果它可以获取 MITM 位置,它可以开始冒充您的网站向您的用户发送消息。如果您控制连接的客户端部分,明显的解决方案是要么不使用 Web PKI(并依赖自己的 PKI),要么将特定证书或公钥*固定*。
证书或公钥固定是一种技术,其中服务器的证书(通常是其哈希),或者公钥,直接硬编码在客户端代码中。如果服务器未提供预期的证书,或者证书不包含预期的长期公钥,客户端会在握手的认证阶段中中止连接。这种做法通常在移动应用程序中使用,因为它们确切地知道服务器的公钥或证书应该是什么样子的(不像浏览器必须连接到无数的服务器)。然而,硬编码证书和公钥并非总是可行的,还有其他两种机制共存来处理不良证书:
* *证书吊销*---顾名思义,这允许 CA 撤销证书并警告浏览器。
* *证书监控*---这是一个相对较新的系统,强制 CA 公开记录每个签发的证书。
证书吊销的故事在历史上一直曲折。首先提出的解决方案是*证书吊销列表*(CRLs),它允许 CA 维护一份吊销的证书列表,即不再被视为有效的证书。CRLs 的问题在于它们可能会变得相当庞大,并且需要不断检查。
CRLs 已被淘汰,取而代之的是*在线证书状态协议* (OCSP),这是一种简单的网络接口,您可以查询以查看证书是否被吊销。OCSP 也有自己的问题:它要求 CA 拥有一个高度可用的服务来回答 OCSP 请求,它会向 CA 泄漏网络流量信息,并且浏览器经常决定忽略超时的 OCSP 请求(以不干扰用户体验)。目前的解决方案是通过*OCSP 装订*来增强 OCSP:网站负责查询 CA 签署其证书状态的响应,并在 TLS 握手期间将响应附加(装订)到其证书上。我在图 9.9 中回顾了这三种解决方案。

图 9.9 网络上的证书吊销有三种流行的解决方案:证书吊销列表(CRLs)、在线证书状态协议(OCSP)和 OCSP 装订。
证书吊销可能看起来不是一个主要的支持功能(特别是对比全球网络的较小系统),直到证书被 compromise。就像汽车安全带一样,证书吊销是一个大部分时间无用但在罕见情况下可能拯救生命的安全功能。这就是我们在安全领域所说的"深度防御"。
注意 对于网络来说,证书吊销在很大程度上被证明是一个明智的决定。在 2014 年,心脏出血漏洞证明是 SSL 和 TLS 历史上最具破坏性的漏洞之一。最广泛使用的 SSL/TLS 实现(OpenSSL)被发现存在*缓冲区过读*漏洞(超出数组限制的读取),允许任何人向任何 OpenSSL 服务器发送一个特制消息并接收其内存转储,通常会显示其长期私钥。
然而,如果 CA 真的行为不端,它可以决定不吊销恶意证书或不报告它们。问题在于我们在盲目地信任一定数量的行为者(CA)做正确的事情。为了在规模上解决这个问题,*证书透明度*在 2012 年由谷歌提出。证书透明度的背后思想是强制 CA 将每个颁发的证书添加到一个巨大的证书日志中供所有人查看。为了做到这一点,像 Chrome 这样的浏览器现在会拒绝那些不包含在公共日志中的证书。这种透明度允许您检查是否为您拥有的域错误颁发了证书(过去应该没有其他证书除了您以前请求的)。
请注意,证书透明度依赖于人们监控自己域的日志以在事后捕捉到不良证书。CA 也必须迅速做出反应,一旦检测到错误颁发的证书就吊销它们。在极端情况下,浏览器有时会从信任存储中移除行为不端的 CA。因此,证书透明度并不像证书或公钥固定那样强大,可以减轻 CA 的不端行为。
### 9.4 其他安全传输协议
您现在已经了解了 TLS,这是加密通信的最流行协议。但是,您还没有完成。TLS 并不是安全传输协议类中唯一的协议。还有许多其他协议存在,您很可能已经在使用它们。然而,大多数都是类似 TLS 的协议,定制以支持特定用例。例如:
* *Secure Shell (SSH)*---用于安全连接到不同机器上的远程终端的最广泛使用的协议和应用程序。
* *Wi-Fi Protected Access (WPA)*---连接设备到私人网络访问点或互联网的最流行协议。
* *IPSec*---用于连接不同私人网络的最流行的虚拟网络协议(VPN)之一。它主要由公司用于连接不同办公网络。正如其名称所示,它在 IP 层起作用,通常在路由器、防火墙和其他网络设备中找到。另一个流行的 VPN 是 OpenVPN,它直接使用 TLS。
所有这些协议通常重新实现握手/后握手范式并在其上添加一些自己的特色。重新发明轮子并非没有问题,例如,几种 Wi-Fi 协议已经被破解。为了完成本章,我想向您介绍*噪声协议框架*。噪声是 TLS 的一个更现代的替代品。
### 9.5 噪声协议框架:TLS 的现代替代品
由于历史原因、向后兼容性约束和整体复杂性,TLS 现在已经相当成熟,并在大多数情况下被认为是一个可靠的解决方案。然而,TLS 给使用它的应用程序增加了很多开销,这是由于历史原因、向后兼容性约束和整体复杂性。实际上,在许多情况下,您可能不需要 TLS 提供的所有功能,尤其是在您控制所有端点的情况下。下一个最佳解决方案被称为*噪声协议框架*。
噪声协议框架通过避免握手中的所有协商来消除 TLS 的运行时复杂性。运行噪声的客户端和服务器遵循一个不分支的线性协议。与可以根据不同握手消息中包含的信息采取许多不同路径的 TLS 相比,噪声将所有复杂性推到设计阶段。
想要使用噪声协议框架的开发人员必须决定他们的应用程序要使用框架的什么特定实例。 (这就是为什么它被称为协议*框架*而不是协议。) 因此,他们必须首先决定将使用什么加密算法,哪一端的连接被认证,是否使用任何预共享密钥等。之后,协议被实现并变成一系列严格的消息,如果需要在维护与无法更新的设备的向后兼容性的同时稍后更新协议可能会成为问题。
#### 9.5.1 噪声的许多握手
Noise 协议框架提供了不同的*握手模式* 供您选择。握手模式通常带有指示正在进行的操作的名称。例如,*IK* 握手模式表示客户端的公钥作为握手的一部分被发送(第一个 *I* 表示*即时* ),并且服务器的公钥已被客户端预先知道(*K* 表示*已知*)。一旦选择了握手模式,使用它的应用程序将永远不会尝试执行任何其他可能的握手模式。与 TLS 相反,这使得 Noise 在实践中成为一个简单而线性的协议。
在本节的其余部分中,我将使用一个名为 *NN* 的握手模式来解释 Noise 的工作原理。这个模式足够简单来解释,但是不安全,因为有两个 *N* 表示双方都没有进行认证。在 Noise 的术语中,该模式被写成这样:
```go
NN:
-> e
<- e, ee
```
每一行代表一个消息模式,箭头指示消息的方向。每个消息模式都是一系列标记的连续(这里只有两个:`e` 和 `ee`),指示连接的两侧需要做什么:
* `->` `e`---表示客户端必须生成临时密钥对并将公钥发送给服务器。服务器解释此消息方式不同:它必须接收临时公钥并存储它。
* `<-` `e,` `ee`---表示服务器必须生成临时密钥对,并将公钥发送给客户端,然后必须与客户端的临时(第一个 `e`)和自己的临时(第二个 `e`)进行 Diffie-Hellman(DH)密钥交换。另一方面,客户端必须从服务器接收临时公钥,并使用它进行 DH 密钥交换。
注意 Noise 使用一组定义的标记来指定不同类型的握手方式。例如,`s` 标记表示*静态密钥* (另一个词是*长期密钥* ),而不是临时密钥,而 `es` 标记表示两个参与者必须使用客户端的临时密钥和服务器的静态密钥进行 DH 密钥交换。
这还不止:在每个消息模式(`->` `e` 和 `<-` `e,` `ee`)的结尾,发送方还可以传输有效载荷。如果先前进行了 DH 密钥交换(这在第一个消息模式 `->` `e` 中并非如此),则有效载荷将被加密和验证。在握手结束时,双方派生一组对称密钥,并开始像 TLS 一样加密通信。
#### 9.5.2 使用 Noise 进行握手
Noise 的一个特点是它持续验证其握手记录。为了实现这一点,双方维护两个变量:哈希(`h`)和链接密钥(`ck`)。发送或接收的每个握手消息都与上一个 `h` 值一起进行哈希处理。我在图 9.10 中说明了这一点。

在噪声协议框架中,连接的每一侧都跟踪一条摘要`h`,其中包括在握手期间发送和接收的所有消息。当发送消息并使用带有相关数据的认证加密(AEAD)算法进行加密时,当前的`h`值将用作相关数据,以便对到目前为止的握手进行认证。
在每个消息模式结束时,一个(可能为空的)有效负载将使用带有相关数据的认证加密(AEAD)算法(在第四章中介绍)进行加密。发生这种情况时,`h`值将通过 AEAD 的相关数据字段进行认证。这使得噪声能够持续验证连接的双方是否以相同的消息序列和相同的顺序进行查看。
此外,每当进行 DH 密钥交换时(在握手期间可能会发生多次),其输出将连同前一个链密钥(`ck`)一起输入到 HKDF 中,该密钥将导出一个新的链密钥和一组新的对称密钥,以用于对随后的消息进行认证和加密。我在图 9.11 中说明了这一点。

在噪声协议框架中,连接的每一侧都跟踪一个*链密钥* ,`ck`。每次执行 DH 密钥交换时,此值都用于导出新的链密钥和新的加密密钥,以在协议中使用。
这使得噪声在运行时成为一个简单的协议;没有分支,连接的双方只需做他们需要做的事情。实现噪声的库也非常简单,最终只有几百行代码,而 TLS 库有数十万行代码。虽然噪声使用起来更复杂,需要了解噪声如何工作的开发人员将其集成到应用程序中,但它是 TLS 的强大替代品。
### 摘要
* 传输层安全性(TLS)是一种安全传输协议,用于加密机器之间的通信。它以前被称为安全套接字层(SSL),有时仍然被称为 SSL。
* TLS 在 TCP 之上运行,并且每天都用于保护浏览器、网络服务器、移动应用程序等之间的连接。
* 为了在用户数据报协议(UDP)之上保护会话,TLS 有一种称为数据报传输层安全性(DTLS)的变体,它与 UDP 一起使用。
* TLS 和大多数其他传输安全协议都有一个握手阶段(在此阶段创建安全协商)和一个后握手阶段(在此阶段使用从第一阶段导出的密钥进行加密通信)。
* 为了避免向 Web 公钥基础设施委托过多的信任,使用 TLS 的应用程序可以使用证书和公钥固定来仅允许与特定证书或公钥进行安全通信。
* 作为深度防御措施,系统可以实现证书吊销(以删除受损的证书)和监视(以检测到受损的证书或 CA)。
* 为了避免 TLS 的复杂性和大小以及连接双方是否受控制,可以使用 Noise 协议框架。
* 要使用 Noise,必须在设计协议时决定要使用哪种握手的变体。因此,它比 TLS 更简单、更安全,但灵活性较差。
## 第十章:端到端加密
本章涵盖
* 端到端加密及其重要性
* 解决电子邮件加密的不同尝试
* 端到端加密如何改变消息传递的格局
第九章解释了通过诸如 TLS 和 Noise 等协议的传输安全。同时,我花了相当多的时间解释了信任在网络上的根基:由您的浏览器和操作系统信任的数百个证书颁发机构(CA)。虽然并不完美,但这个系统迄今为止在 Web 上运作良好,Web 是一个复杂的参与者网络,他们彼此一无所知。
找到信任他人(及其公钥)并使其规模化的方法是现实世界密码学的核心问题。一位著名的密码学家曾经说过,"对称加密问题已经解决了",以描述一门已经过时的研究领域。而且,在很大程度上,这种说法是正确的。我们很少遇到加密通信的问题,我们对当前使用的加密算法有很强的信心。在加密方面,大多数工程挑战不再是关于算法本身,而是关于谁是 Alice 和 Bob 以及如何证明它的问题。
密码学没有提供对信任的一个解决方案,而是提供了许多不同的解决方案,这些解决方案更或者更少地依赖于上下文。在本章中,我将调查人们和应用程序用于创建用户之间信任的一些不同技术。
### 10.1 为什么端到端加密?
本章以"为什么"而不是"什么"开始。这是因为端到端加密是一个概念而不是一个密码协议;它是一个在敌对路径上保护两个(或更多)参与者之间通信的概念。我在这本书中以一个简单的例子开始:女王 Alice 想要向爵士 Bob 发送一条消息,而中间没有任何人能够看到。如今,许多应用程序如电子邮件和消息传递存在以连接用户的方式,并且大多数情况下很少对消息进行端到端加密。
你可能会问,TLS 不够吗?在理论上,它可能足够。在第九章中,你了解到 TLS 被用于许多地方来保护通信。但端到端加密是涉及实际人类的概念。相比之下,TLS 大多被设计为"中间人",在这些系统中,TLS 仅用于保护中央服务器与其用户之间的通信,允许服务器看到一切。实际上,这些 MITM 服务器位于用户之间,对应用程序的功能是必要的,并且是协议的*受信任的第三方*。也就是说,为了使协议被视为安全(剧透警告:这不是一个很好的协议),我们必须信任系统的这些部分。

图 10.1 在大多数系统中,一个中央服务器(顶部图表)在用户之间传递消息。通常在用户和中央服务器之间建立安全连接,因此中央服务器可以看到所有用户消息。提供端到端加密的协议(底部图表)将通信从一个用户加密到其预期接收者,防止中间任何服务器观察明文消息。
在实践中,存在更糟糕的拓扑结构。用户和服务器之间的通信可能经过许多网络跳点,其中一些跳点可能是查看流量的机器(通常称为*中间盒* )。即使流量被加密,有些中间盒被设置为终止 TLS 连接(我们称之为*终止 TLS*),然后要么从那一点开始明文转发流量,要么与下一个跳点建立另一个 TLS 连接。有时终止 TLS 是出于"好"的原因:为了更好地过滤流量,地理上或数据中心内部平衡连接等。这增加了攻击面,因为流量现在在更多地方以明文形式可见。有时,终止 TLS 是出于"坏"的原因:为了拦截、记录和监视流量。
2015 年,联想被发现销售预装有自定义 CA(在第九章中介绍)和软件的笔记本电脑。该软件使用联想的 CA 进行 HTTPS 连接的中间人攻击,并向网页注入广告。更令人担忧的是,像中国和俄罗斯这样的大国被发现在互联网上重定向流量,使其经过他们的网络以拦截和观察连接。2013 年,爱德华·斯诺登泄露了来自 NSA 的大量文件,显示了许多政府(不仅仅是美国)在通过拦截连接世界的互联网电缆来监视人们通信方面的滥用行为。
拥有和查看用户数据对公司也是一种责任。正如我在本书中多次提到的那样,数据泄震和黑客攻击发生得太频繁,可能对公司的信誉造成毁灭性打击。从法律角度来看,像《通用数据保护条例》(GDPR)这样的法律可能会让组织付出巨额代价。政府要求,比如臭名昭著的国家安全信函(NSLs),有时会阻止公司和相关人员甚至提及他们收到了信函(所谓的禁言令),这也可以被视为对组织的额外成本和压力,除非你没有太多可以分享的内容。
总的来说,如果你正在使用一个流行的在线应用程序,很可能一个或多个政府已经可以访问或有能力访问你在那里写下或上传的所有内容。根据应用程序的*威胁模型*(应用程序想要防范的威胁)或应用程序最容易受到攻击的用户的威胁模型,端到端加密在确保最终用户的机密性和隐私方面发挥着重要作用。
本章介绍了为建立人与人之间的信任而创建的不同技术和协议。特别是,你将了解当今电子邮件加密的工作原理以及安全消息传递如何改变端到端加密通信的格局。
### 10.2 无处可寻的信任根源
最简单的端到端加密场景之一是:Alice 想要通过互联网向 Bob 发送加密文件。通过本书前几章学到的所有加密算法,你可能可以想到一种方法来实现这一点。例如
1. Bob 向 Alice 发送他的公钥。
2. Alice 用 Bob 的公钥加密文件并发送给 Bob。
或许 Alice 和 Bob 可以在现实生活中见面,或者使用他们已经共享的另一个安全渠道来在第一条消息中交换公钥。如果这是可能的,我们称他们有一种*out-of-band*的方式来建立信任。然而,并非总是如此。你可以想象我在这本书中包含了我的公钥,并要求你使用它向我发送加密消息到某个电子邮件地址。谁说我的编辑没有用她的公钥替换我的公钥呢?
对于 Alice 也是一样:她如何确定她收到的公钥是否真的是 Bob 的公钥?中间某人可能篡改了第一条消息。正如你将在本章中看到的,密码学对于这个信任问题没有真正的答案。相反,它提供了不同的解决方案来帮助不同的情况。没有真正解决方案的原因是我们试图将现实(真实的人类)与理论的加密协议联系起来。
*保护公钥免受篡改的整个过程是实际公钥应用中最困难的问题。这是公钥密码学的"阿喀琉斯之踵",许多软件复杂性都与解决这一问题有关*。
---Zimmermann 等人("PGP 用户指南第一卷:基本主题",1992)
回到我们简单的设置,Alice 想要向 Bob 发送文件,并假设他们不受信任的连接是他们唯一拥有的,他们面临着一种几乎不可能解决的信任问题。Alice 没有好的方法确切地知道什么才是 Bob 的真正公钥。这是一种鸡生蛋蛋生鸡的情况。然而,让我指出,如果没有恶意的*主动*中间人攻击者在第一条消息中替换了 Bob 的公钥,那么协议是安全的。即使消息被被动记录,攻击者也来不及事后解密第二条消息。
当然,依赖于你被主动中间人攻击的机会*不太高*并不是进行密码学的最佳方式。不幸的是,我们通常无法避免这种情况。例如,Google Chrome 预装了一组证书颁发机构(CA),它选择信任这些机构,但你最初是如何获取 Chrome 的呢?也许你使用了操作系统的默认浏览器,它依赖于自己的一组 CA。但这些又是从哪里来的呢?从你购买的笔记本电脑。但这台笔记本电脑又是从哪里来的呢?很快你就会发现,这是"无穷的乌龟"。在某个时刻,你将不得不相信某件事是正确的。
威胁模型通常选择在特定的层次停止解决问题,并认为任何更深层次的问题都不在范围之内。这就是为什么本章的其余部分将假设你有一种安全的方式来获取一些*信任根源*。所有基于密码学的系统都依赖于一个信任根源,一个协议可以在其上构建安全性的东西。信任根源可以是一个我们用来启动协议的秘密或公共值,或者是一个我们可以用来获取它们的带外信道。
### 10.3 加密电子邮件的失败
电子邮件被创建为(今天仍然是)一个*未加密* 的协议。我们只能责怪一个安全性是次要考虑的时代。电子邮件加密开始变得不再只是一个想法,是在 1991 年发布了一个名为*Pretty Good Privacy*(PGP)的工具之后。当时,PGP 的创造者 Phil Zimmermann 决定在同一年早些时候几乎成为法律的一项法案发布 PGP。该法案允许美国政府从任何电子通信公司和制造商获取所有语音和文本通信。在他 1994 年的文章"为什么你需要 PGP?"中,Philip Zimmermann 结束时说:"PGP 让人们能够掌握自己的隐私。这是一个日益增长的社会需求。这就是为什么我写了它。"
该协议最终在 1998 年的 RFC 2440 中标准化为*OpenPGP* ,并随着开源实现*GNU Privacy Guard*(GPG)的发布而受到关注。今天,GPG 仍然是主要的实现,人们可以互换使用术语 GPG 和 PGP 来几乎表示相同的意思。
#### 10.3.1 PGP 还是 GPG?它是如何工作的?
PGP,或者 OpenPGP,通过简单地使用混合加密(在第六章中介绍)来工作。详细信息在 RFC 4880 中,这是 OpenPGP 的最新版本,可以简化为以下步骤:
1. 发件人创建一封电子邮件。在加密之前,电子邮件的内容会被压缩。
2. OpenPGP 实现生成一个随机对称密钥,并使用该对称密钥对电子邮件进行对称加密。
3. 对称密钥被非对称加密到每个接收者的公钥上(使用你在第六章学到的技术)。
4. 所有预期收件人的加密版本的对称密钥都与加密消息连接在一起。电子邮件正文被替换为此数据块并发送给所有收件人。
5. 要解密电子邮件,收件人使用他们的私钥解密对称密钥,然后使用解密后的对称密钥解密电子邮件的内容。
请注意,OpenPGP 还定义了如何签署电子邮件以验证发件人的方法。为此,明文电子邮件的主体被散列,然后使用发件人的私钥进行签名。在第 2 步加密之前,签名然后被添加到消息中。最后,为了使接收者能够找出要用于验证签名的公钥,发件人的公钥在第 4 步加密电子邮件中发送。我在图 10.2 中说明了 PGP 流程。

图 10.2 PGP 的目标是加密和签署消息。当与电子邮件客户端集成时,它不关心隐藏主题或其他元数据。
练习
你知道为什么电子邮件内容在加密之前被压缩而不是之后吗?
乍一看,这种设计本质上没有问题。它似乎防止中间人攻击者查看您的电子邮件内容,尽管主题和其他电子邮件标题未加密。
注意:重要的是要注意,加密并不总是能够隐藏所有元数据。在注重隐私的应用程序中,元数据是一个大问题,而且在最糟糕的情况下,可以对您进行去匿名化!例如,在端到端加密协议中,您可能无法解密用户之间的消息,但您可能可以知道他们的 IP 地址、他们发送和接收的消息的长度、他们通常与谁交谈(他们的社交图谱)等信息。有很多工程工作被投入到隐藏这种类型的元数据中。
然而,在细节方面,PGP 实际上相当糟糕。OpenPGP 标准及其主要实现 GPG 使用了老算法,并且向后兼容性阻碍了它们改善情况。最关键的问题是加密没有经过身份验证,这意味着拦截未签名的电子邮件的任何人可能能够在一定程度上篡改加密内容,具体取决于所使用的确切加密算法。仅因为这个原因,我不建议任何人今天使用 PGP。
PGP 的一个令人惊讶的缺陷源于签名和加密操作的不合理组合。在 2001 年,唐·戴维斯指出,由于这种加密算法的天真组合,一个人可以重新加密他们收到的已签名电子邮件,并将其发送给另一个收件人。这实际上允许 Bob 将 Alice 发送给他的电子邮件发送给你,就好像你是预期的收件人一样!
如果你想知道,用密文而不是明文签名仍然有缺陷,因为然后可以简单地删除随密文一起发送的签名,然后添加自己的签名。实际上,Bob 可以假装他发送给你一封实际上来自 Alice 的电子邮件。我在图 10.3 中总结了这两个签名问题。

图 10.3 在顶部图中,Alice 使用 Bob 的公钥对消息进行加密,并对消息进行签名。Bob 可以重新加密此消息给 Charles,Charles 可能认为最初就是为他准备的。这是 PGP 的流程。在底部图中,这次 Alice 向 Charles 加密了一条消息。她还对加密消息进行了签名,而不是明文内容。截获加密消息的 Bob 可以用自己的签名替换签名,愚弄 Charles 以为他写了消息的内容。
练习
你能想到一种明确的签名消息的方式吗?
雪上加霜的是,默认情况下该算法不提供*前向保密性*。作为提醒,没有前向保密性,你的私钥被泄露意味着可以解密以该密钥加密的所有先前发送给你的电子邮件。你仍然可以通过更改你的 PGP 密钥来强制前向保密性,但这个过程并不简单(你可以,例如,用你的旧密钥签署你的新密钥),大多数用户并不在意。总之,记住
* PGP 使用旧的加密算法。
* PGP 没有经过身份验证的加密,因此,如果没有签名,它是不安全的。
* 由于设计不良,收到签名消息并不一定意味着我们是预期的接收者。
* 默认情况下没有前向保密性。
#### 10.3.2 在用户之间使用信任网络进行信任扩展
那么我为什么在这里谈论 PGP 呢?好吧,PGP 有件有趣的事情我还没谈到:你如何获取并信任其他人的公钥?答案是在 PGP 中,你自己建立信任!
好的,这意味着什么?想象一下,你安装了 GPG,并决定想给你的朋友发送一些加密消息。首先,你必须找到一种安全的方式获取你朋友的 PGP 公钥。面对面见面是一种确保这样做的方法。你们见面了,你抄写下他们的公钥在一张纸上,然后你回到家里将那些密钥输入你的笔记本电脑。现在,你可以用 OpenPGP 发送给你的朋友签名和加密的消息了。但这很繁琐。你必须为每个你想发送电子邮件的人都这样做吗?当然不是。让我们看看以下情景:
* 你已经在现实生活中获得了 Bob 的公钥,因此你信任它。
* 你没有 Mark 的公钥,但 Bob 有,并且他信任它。
在这里花一点时间思考一下如何信任马克的公钥。鲍勃可以简单地签署马克的密钥,向你展示他信任公钥与马克的电子邮件之间的关联。如果你信任鲍勃,现在你就可以信任马克的公钥并将其添加到你的资源库中。这就是 PGP 概念中 *分散式* 信任的主要思想。正如图 10.4 所示,这被称为 *信任网络*(WOT)。

图 10.4 信任网络(WOT)是指用户可以通过依赖签名来转移信任给其他用户的概念。在这个图中,我们可以看到爱丽丝信任鲍勃,鲍勃信任查理。爱丽丝可以使用鲍勃对查理身份和公钥的签名来信任查理。
有时你会在会议上看到"密钥派对",人们在现实生活中相遇并签署各自的公钥。但其中大部分都是角色扮演,在实践中,很少有人依赖 WOT 来扩大他们的 PGP 圈子。
#### 10.3.3 密钥发现是一个真实的问题
PGP 确实尝试了另一种解决发现公钥问题的方法------ *密钥注册表*。这个概念非常简单:在某个公共列表上发布你的 PGP 公钥和其他人为你的身份作证的关联签名,以便人们可以找到它。在实践中,这并不奏效,因为任何人都可以发布一个与你的电子邮件相匹配的密钥和关联签名。事实上,一些攻击者故意在密钥服务器上伪造密钥,尽管可能更多是为了制造混乱而不是窃听电子邮件。在某些情况下,我们可以放宽我们的威胁模型,允许一个可信任的权威对身份和公钥进行证明。例如,想象一家公司管理他们员工的电子邮件。
1995 年,RSA 公司提出了作为 MIME 格式的扩展(MIME 本身是电子邮件标准的扩展)和 PGP 的一种替代方案的 *安全/多用途互联网邮件扩展*(S/MIME)。S/MIME,标准化在 RFC 5751 中,通过使用公钥基础设施来构建信任,与 WOT 有了有趣的区别。这几乎是 S/MIME 与 PGP 唯一的概念性区别。随着公司建立起了适用于员工的流程,开始使用诸如 S/MIME 之类的协议来启动对内部电子邮件生态系统的信任也就有了意义。
需要注意的是,PGP 和 S/MIME 通常用于 *简单邮件传输协议*(SMTP),这是今天用于发送和接收电子邮件的协议。PGP 和 S/MIME 也是后来才被发明出来的,因此它们与 SMTP 和电子邮件客户端的集成远非完美。例如,只有电子邮件的正文是加密的,而不是主题或任何其他电子邮件头部信息。与 PGP 类似,S/MIME 也是一个相当古老的协议,使用过时的加密和做法。与 PGP 类似,它也不提供身份验证加密。
最近的研究(Efail:"利用渗透通道破解 S/MIME 和 OpenPGP 电子邮件加密")关于在电子邮件客户端中集成这两种协议显示,大多数客户端都容易受到*渗透攻击*的影响,攻击者可以通过发送篡改版本的加密邮件给接收者来检索内容。
最终,这些缺点甚至可能无关紧要,因为世界上发送和接收的大多数电子邮件都在全球网络上未加密传输。对于非技术人员以及需要理解 PGP 的许多微妙之处和流程才能加密他们的电子邮件的高级用户来说,PGP 使用起来相当困难。例如,经常会看到用户在不使用加密的情况下回复加密邮件,以明文引用整个线程。此外,流行电子邮件客户端对 PGP 的支持(或根本没有支持)也没有帮助。
*在 1990 年代,我对未来感到兴奋,梦想着一个每个人都会安装 GPG 的世界。现在,我仍然对未来感到兴奋,但我梦想着一个我可以卸载它的世界*。
---Moxie Marlinspike("GPG 和我",2015)
由于这些原因,PGP 已经逐渐失去了支持(例如,Golang 在 2019 年从其标准库中移除了对 PGP 的支持),而越来越多的真实世界的加密应用程序正致力于取代 PGP 并解决其可用性问题。如今,很难争辩电子邮件加密会像 HTTPS 那样取得相同的成功和普及。
*如果消息可以以明文发送,它们就会以明文发送。电子邮件默认是端到端未加密的。电子邮件的基础是明文。所有主流电子邮件软件都期望明文。在某种意义上,互联网电子邮件系统简单地设计成不加密*。
---Thomas Ptacek("停止使用加密电子邮件",2020)
#### 10.3.4 如果不是 PGP,那又是什么呢?
我花了几页的篇幅讨论了像 PGP 这样简单的设计在实践中可能以许多不同和令人惊讶的方式失败。是的,我建议不要使用 PGP。虽然电子邮件加密仍然是一个未解决的问题,但正在开发替代方案来取代不同的 PGP 使用情况。
*saltpack* 是一种类似于 PGP 的协议和消息格式。它试图修复我所谈到的一些 PGP 的缺陷。在 2021 年,saltpack 的主要实现是 keybase([`keybase.io`](https://keybase.io))和 keys.pub([`keys.pub`](https://keys.pub))。图 10.5 展示了 keys.pub 工具。

图 10.5 keys.pub 是一个实现 saltpack 协议的本地桌面应用程序。您可以使用它导入其他人的公钥,并向他们加密和签名消息。
这些实现都已经摆脱了信任路径(WOT),允许用户在不同的社交网络上广播他们的公钥,以将他们的身份融入到他们的公钥中(如图 10.6 所示)。PGP 显然无法预见到这种密钥发现机制,因为它早于社交网络的蓬勃发展。

图 10.6 一个 keybase 用户在 Twitter 社交网络上广播他们的公钥。这使得其他用户可以获得额外的证据,证明他的身份与特定的公钥相关联。
另一方面,如今大多数安全通信远非一次性消息,使用这些工具的意义越来越不明显。在下一节中,我将讨论*安全通信*,这是一个旨在取代 PGP 通信方面的领域。
### 10.4 安全通信:使用 Signal 进行端到端加密的现代视角
2004 年,*Off-The-Record* (OTR\*)\* 在一篇名为"离线记录通信,或者,为什么不使用 PGP"的白皮书中介绍。与 PGP 或 S/MIME 不同,OTR 不用于加密电子邮件,而是聊天消息;具体来说,它扩展了一种称为*可扩展消息和出席协议*(XMPP)的聊天协议。
OTR 的一个独特特性是*可否认性* ------即您的消息接收者和被动观察者无法在法庭上使用您发送给他们的消息。因为您发送的消息是经过身份验证和对称加密的,使用的密钥是您的接收者与您共享的,他们很容易伪造这些消息。相比之下,使用 PGP,消息被签名,因此,与否认相反------消息是*不可否认的*。据我所知,这些属性实际上没有在法庭上进行过测试。
在 2010 年,Signal 手机应用程序(当时称为 TextSecure)发布,使用了一个新创建的协议,称为*Signal 协议* 。当时,大多数安全通信协议如 PGP、S/MIME 和 OTR 都是基于*联邦协议*的,即网络无需中央实体即可运行。Signal 手机应用程序在很大程度上背离了传统,通过运行一个中央服务并提供一个官方 Signal 客户端应用程序。
虽然 Signal 阻止了与其他服务器的互操作性,但 Signal 协议是开放标准,并已被许多其他消息应用程序采用,包括 Google Allo(现已停用)、WhatsApp、Facebook Messenger、Skype 等等。Signal 协议真正是一个成功的故事,透明地被数十亿人使用,包括记者、政府监视目标,甚至是我 92 岁的奶奶(我发誓我没有让她安装)。
研究 Signal 如何工作是很有趣的,因为它试图修复我之前提到的 PGP 的许多缺陷。在本节中,我将逐个讨论 Signal 的以下有趣特性:
* 我们能比 WOT 做得更好吗?有没有办法升级现有的社交图与端到端加密?Signal 的答案是使用*首次使用信任*(TOFU)方法。TOFU 允许用户在第一次通信时盲目信任其他用户,依靠这种首次不安全的交换来建立持久的安全通信渠道。然后用户可以自由地通过在任何时候在辅助渠道上匹配会话密钥来检查第一次交换是否被 MITM 攻击。
* 我们如何升级 PGP 以在每次与某人开始对话时都获得前向保密性?Signal 协议的第一部分与大多数安全传输协议类似:它是一个密钥交换,但是一个特殊的称为*扩展三重 Diffie-Hellman*(X3DH)的密钥交换。稍后详细介绍。
* 我们如何升级 PGP 以使每条消息都获得前向保密性?这很重要,因为用户之间的对话可能会跨越多年,某个时间点的泄密不应该暴露多年的通信。Signal 用一种称为*对称棘轮*的东西来解决这个问题。
* 如果两个用户的会话密钥在某个时间点被泄露,会怎么样?这意味着游戏结束吗?我们是否也可以从中恢复?Signal 引入了一个称为*后置泄密安全* (PCS)的新安全属性,并用所谓的*Diffie-Hellman* (DH)*棘轮*来解决这个问题。
让我们开始吧!首先,我们将看看 Signal 的 TOFU 是如何工作的。
#### 10.4.1 比 WOT 更用户友好:信任但验证
电子邮件加密最大的失败之一是其依赖于 PGP 和 WOT 模型将社交图转化为*安全* 的社交图。PGP 的原始设计打算让人们面对面进行*密钥签名仪式* (也称为*密钥签名派对*)来确认彼此的密钥,但这在许多方面都很繁琐和不方便。今天很少见到人们互相签署 PGP 密钥。
大多数人使用 PGP、OTR、Signal 等应用程序的方式是盲目信任第一次见到的密钥,并拒绝任何未来的更改(如图 10.7 所示)。这样,只有第一次连接才可能受到攻击(而且仅受到主动 MITM 攻击者的攻击)。

图 10.7 的首次使用信任(TOFU)允许 Alice 信任她的第一个连接,但如果后续连接没有展示相同的公钥,则不信任。当第一个连接被潜在的中间人攻击的可能性很低时,TOFU 是一种建立信任的简单机制。公钥与身份(这里是 Bob)之间的关联也可以在之后的不同渠道中验证。
尽管 TOFU 不是最佳的安全模型,但它通常是我们拥有的最佳模型,并且已被证明非常有用。例如,安全外壳(SSH)协议通常在初始连接时信任服务器的公钥(参见图 10.8),并拒绝任何未来的更改。

图 10.8 SSH 客户端使用第一次使用时信任。当您第一次连接到 SSH 服务器时(左图),您可以选择盲目地信任 SSH 服务器和显示的公钥之间的关联。如果 SSH 服务器的公钥稍后更改(右图),您的 SSH 客户端将阻止您连接到它。
虽然 TOFU 系统信任它们看到的第一个密钥,但它们仍允许用户稍后验证该密钥是否确实正确,并捕捉任何冒充尝试。在现实世界的应用中,用户通常比较*指纹*,这些指纹通常是公钥的十六进制表示或公钥的哈希值。当然,此验证是在带外完成的。(如果 SSH 连接被破坏,那么验证也会被破坏。)
注意 当然,如果用户不验证指纹,则可能在不知情的情况下成为中间人攻击的受害者。但这是现实世界应用在实现大规模端到端加密时必须处理的一种权衡。事实上,WOT 的失败表明,面向安全的应用必须考虑可用性才能被广泛采用。
在 Signal 移动应用中,Alice 和 Bob 之间的指纹是通过以下方式计算的:
1. 以 Alice 的用户名(在 Signal 中是电话号码)作为前缀,对她的身份密钥进行哈希,并将该摘要的截断解释为一系列数字
2. 对 Bob 做同样的操作
3. 将两系列数字的串联显示给用户
应用程序像 Signal 使用*QR 码*让用户更轻松地验证指纹,因为这些码可能很长。图 10.9 展示了这种用法。

图 10.9 使用 Signal,您可以通过使用不同的通道(就像在现实生活中一样)来验证与朋友的连接的真实性和机密性,以确保您和朋友的两个指纹(Signal 称它们为*安全号码*)匹配。通过使用 QR 码,可以更容易地完成此操作,该码以可扫描的格式编码此信息。Signal 还对会话密钥进行哈希处理,而不是两个用户的公钥,使它们可以验证一个大字符串而不是两个。
接下来,让我们看看 Signal 协议在幕后是如何工作的------具体来说,Signal 如何确保前向安全性。
#### 10.4.2 X3DH:Signal 协议的握手
在 Signal 之前,大多数安全消息应用程序都是*同步* 的。这意味着,例如,如果 Bob 不在线,Alice 就无法开始(或继续)与 Bob 进行端到端加密的对话。另一方面,Signal 协议是*异步*的(像电子邮件一样),这意味着 Alice 可以开始(并继续)与离线的人进行对话。
记住 *前向保密性* (在第九章中介绍)意味着密钥的泄露不会泄露先前的会话,并且前向保密性通常意味着密钥交换是交互式的,因为双方都必须生成临时的 Diffie-Hellman(DH)密钥对。在本节中,您将了解 Signal 如何使用 *非交互式* 密钥交换(其中一方有可能处于离线状态)仍然保持前向安全性。好的,让我们开始吧。
要与 Bob 开始对话,Alice 与他启动密钥交换。Signal 的密钥交换 X3DH 将三(或更多)个 DH 密钥交换组合成一个。但在了解其工作原理之前,您需要了解 Signal 使用的三种不同类型的 DH 密钥:
* *身份密钥* --- 这些是代表用户的长期密钥。您可以想象,如果 Signal 只使用身份密钥,那么该方案与 PGP 非常相似,并且不会有前向保密性。
* *一次性 prekeys* --- 为了在密钥交换中添加前向保密性,即使新对话的接收方不在线,Signal 让用户上传多个 *单次使用* 公钥。它们只是预先上传的短暂密钥,在使用后将被删除。
* *签名的 prekeys* --- 我们可以到此为止,但是有一个边缘情况被忽略了。因为用户上传的一次性 prekeys 在某个时候会用完,用户还必须上传一个被签名的 *中期* 公钥:一个签名的 prekey。这样,如果服务器上您用户名下没有更多的一次性 prekeys,某人仍然可以使用您的签名 prekey 来添加前向保密性,直到您上次更改签名 prekey 的时间。这也意味着您必须定期轮换您的签名 prekey(例如,每周)。
这足以预览 Signal 中对话创建流程的流程。图 10.10 提供了概述。

图 10.10 信号流程始于用户注册一系列公钥。如果 Alice 想和 Bob 聊天,她首先要获取 Bob 的公钥(称为 *prekey bundle*),然后她使用这些密钥进行 X3DH 密钥交换,并使用密钥交换的输出创建初始消息。收到消息后,Bob 可以在他这边执行相同的操作来初始化并继续对话。
让我们更深入地了解每个步骤。首先,用户通过发送以下内容进行注册:
* 一个身份密钥
* 一个签名的 prekey 及其签名
* 一定数量的一次性 prekeys
在此时,用户有责任定期轮换签名 prekey 并上传新的一次性 prekeys。我在图 10.11 中总结了这个流程。
请注意,Signal 使用身份密钥对签名进行签名,并在 X3DH 密钥交换期间执行密钥交换。虽然我已经警告不要将同一密钥用于不同的目的,但 Signal 已经故意分析过,在他们的情况下不应该有问题。这并不意味着这会在*您* 的情况下以及*您*的密钥交换算法中起作用。我建议一般情况下不要为不同的目的使用同一密钥。

图 10.11 基于图 10.10,第一步是用户通过生成一些 DH 密钥对并将公共部分发送给中央服务器来注册。
在图 10.11 中引入的步骤之后,**Alice** (回到我们的示例中)然后通过检索开始与 **Bob** 对话:
* **Bob** 的身份密钥。
* **Bob** 的当前签名前置密钥及其相关签名。
* 如果仍然存在一些情况,那么是 **Bob** 的一次性预密钥之一(然后服务器会删除发送给 **Alice** 的一次性预密钥)。
**Alice** 可以验证签名是否正确。然后,她与以下人进行 X3DH 握手:
* 来自 **Bob** 的所有公钥
* 她为此生成的一对临时密钥,以添加前向保密性
* 她自己的身份密钥
X3DH 的输出然后用于后-X3DH 协议,该协议用于将她的消息加密发送给 **Bob**(关于此后详细介绍)。X3DH 由三(可选四)个 DH 密钥交换组成,分组在一起。DH 密钥交换是在以下之间进行的:
1. **Alice** 的身份密钥和 **Bob** 的签名前置密钥
2. **Alice** 的临时密钥和 **Bob** 的身份密钥
3. **Alice** 的临时密钥和 **Bob** 的签名前置密钥
4. 如果 **Bob** 仍然有一个可用的一次性预密钥,他的一次性预密钥和 **Alice** 的临时密钥
X3DH 的输出是所有这些 DH 密钥交换的连接,传递给密钥派生函数(KDF),我们在第八章中介绍了它。不同的密钥交换提供不同的属性。前两个是用于相互认证,而最后两个是用于前向保密性。所有这些都在 X3DH 规范中更深入地分析了([`signal.org/docs/specifications/x3dh/`](https://signal.org/docs/specifications/x3dh/)),我建议您阅读,因为它写得很好。图 10.12 概述了这个流程。

图 10.12 基于图 10.10,要向 **Bob** 发送消息,**Alice** 获取一个预密钥包,其中包含 **Bob** 的长期密钥、**Bob** 的签名前置密钥,以及可选地,**Bob** 的一次性预密钥之一。在与不同密钥进行不同的密钥交换后,所有输出都被串联并传递到 KDF 中,以生成在后续的后-X3DH 协议中用于加密消息发送给 **Bob** 的输出。
现在,Alice 可以将她的身份公钥、她生成的用于开始对话的临时公钥以及其他相关信息(比如她使用了 Bob 的一次性预密钥中的哪个)发送给 Bob。Bob 收到消息后,可以使用其中包含的公钥执行与 X3DH 相同的密钥交换。(因此,我跳过了此流程的最后一步的说明。)如果 Alice 使用了 Bob 的一次性预密钥之一,Bob 将其丢弃。X3DH 完成后会发生什么?让我们来看看接下来发生了什么。
#### 10.4.3 双扭转:Signal 的后握手协议
在双方用户不删除对话或不丢失任何密钥的情况下,后 X3DH 阶段将持续存在。因此,Signal 在消息级别引入*前向保密性* 。在这个部分,您将学习到这个后握手协议(称为*双扭转*)是如何工作的。
但首先,想象一下一个简单的后 X3DH 协议。Alice 和 Bob 可以将 X3DH 的输出作为会话密钥,并将其用于加密他们之间的消息,如图 10.13 所示。

图 10.13 像个简单的后 X3DH 协议,Alice 和 Bob 可以将 X3DH 的输出作为会话密钥,用于加密他们之间的消息。
通常,我们希望将用于不同目的的密钥分开。我们可以将 X3DH 的输出用作 KDF 的*种子*(或根密钥,根据双扭转规范),以便派生出另外两个密钥。Alice 可以使用一个密钥来加密发给 Bob 的消息,而 Bob 可以使用另一个密钥来加密发给 Alice 的消息。我在图 10.14 中说明了这一点。

在图 10.13 的基础上构建一个更好的后 X3DH 协议会利用 KDF 与密钥交换的输出来区分用于加密 Bob 和 Alice 消息的密钥。这里 Alice 的发送密钥与 Bob 的接收密钥相同,而 Bob 的发送密钥与 Alice 的接收密钥相同。
这种方法可能已经足够了,但 Signal 指出,短信会话可能持续数年。这与通常预期是短暂的第九章的 TLS 会话不同。因此,如果在任何时间点会话密钥被窃取,所有先前记录的消息都可以被解密!
为了解决这个问题,Signal 引入了所谓的*对称扭转* (如图 10.15 所示)。*发送密钥* 现在被重命名为*发送链密钥*,并且不直接用于加密消息。在发送消息时,Alice 将不断将发送链密钥传入一个单向函数,该函数产生下一个发送链密钥以及实际用于加密她的消息的发送密钥。另一方面,Bob 将不得不使用接收链密钥执行相同的操作。因此,通过牺牲一个发送密钥或发送链密钥,攻击者无法恢复以前的密钥。(接收消息时也是如此。)

图 10.15 在图 10.14 的基础上构建,在 post-X3DH 协议中可以通过 *ratcheting* (传递到 KDF)每次需要发送消息时引入前向保密性,并在每次接收消息时对另一个链密钥进行 *ratcheting*。因此,发送或接收链密钥的 compromise 不允许攻击者恢复先前的密钥。
很好。我们现在在我们的协议中和消息级别都嵌入了前向保密性。每个发送和接收的消息都保护了所有先前发送和接收的消息。请注意,这在某种程度上是值得商榷的,因为一个攻击者如果 compromise 了一个密钥,可能是通过 compromise 一个用户的手机,而这很可能会在密钥旁边以明文的方式包含所有先前的消息。尽管如此,如果对话中的两个用户都决定删除先前的消息(例如,通过使用 Signal 的"消失消息"功能),则实现了前向保密性属性。
Signal 协议还有一件我想谈论的有趣事情:PCS(*post-compromise security* ,也称为 *backward secrecy*,正如你在第八章学到的)。PCS 是一个想法,即如果你的密钥在某个时候被 compromise,你仍然可以设法恢复,因为协议会自动修复。当然,如果攻击者在 compromise 后仍然可以访问你的设备,那么这就没有用了。
PCS 只能通过重新引入非持久性 compromise 无法访问的新熵来工作。新熵必须对两个对等体相同。Signal 找到这种熵的方法是通过进行短暂密钥交换。为此,Signal 协议在所谓的 *DH ratchet* 中不断执行密钥交换。协议发送的每个消息都带有当前的 *ratchet* 公钥,如图 10.16 所示。

图 10.16 Diffie-Hellman(DH)*ratchet* 通过在每个发送的消息中广告一个 *ratchet* 公钥来工作。这个 *ratchet* 公钥可以与上一个相同,也可以在参与者决定刷新自己的时候广告一个新的 *ratchet* 公钥。
当 Bob 察觉到来自 Alice 的新 *ratchet* 密钥时,他必须与 Alice 的新 *ratchet* 密钥和 Bob 自己的 *ratchet* 密钥进行新的 DH 密钥交换。然后可以将输出与对称 *ratchet* 一起用于解密接收到的消息。我在图 10.17 中说明了这一点。

图 10.17 当 Bob 从 Alice 那里接收到一个新的 *ratchet* 公钥时,他必须与它和他自己的 *ratchet* 密钥进行密钥交换,以派生解密密钥。这是用对称 *ratchet* 来完成的。然后可以解密 Alice 的消息。
当 Bob 接收到新的 *ratchet* 密钥时,他必须为自己生成一个新的随机 *ratchet* 密钥。通过他的新 *ratchet* 密钥,他可以与 Alice 的新 *ratchet* 密钥进行另一次密钥交换,然后用它来加密发给她的消息。这应该看起来像图 10.18。

图 10.18 在图 10.17 的基础上构建,在接收到新的棘轮密钥后,Bob 还必须为自己生成新的棘轮密钥。这个新的棘轮密钥用于派生加密密钥,并在他的下一系列消息中向 Alice 广告(直到他收到来自 Alice 的新的棘轮密钥)。
在双棘轮规范中,密钥交换的这种来回被提及为"乒乓":
*这导致了一种"乒乓"行为,因为各方轮流替换棘轮密钥对。一个窃听者可能会短暂地 compromise 其中一方,但他可能了解当前棘轮私钥的值,但该私钥最终将被替换为未被 compromise 的私钥。在这一点上,棘轮密钥对之间的 Diffie-Hellman 计算将定义攻击者不知道的 DH 输出*。
---双棘轮算法
最后,DH 棘轮和对称棘轮的组合被称为*双棘轮*。作为一个图表来视觉化有点密集,但图 10.19 尝试这样做。

图 10.19 双棘轮(从 Alice 视角)将 DH 棘轮(左侧)与对称棘轮(右侧)结合起来。这为后 X3DH 协议提供了 PCS 和前向保密性。在第一条消息中,Alice 还不知道 Bob 的棘轮密钥,因此她使用了他的预签名密钥。
我知道最后这个图表相当密集,所以我鼓励你查看 Signal 的规范,这些规范已经发布在 [`signal.org/docs`](https://signal.org/docs) 上。它们提供了协议的另一个写得很好的解释。
### 10.5 端到端加密的状态
如今,用户之间的大多数安全通信都是通过安全的消息应用程序进行而不是加密电子邮件。在其类别中,Signal 协议一直是明确的赢家,被许多专有应用程序采用,也被开源和联合协议如 XMPP(通过 OMEMO 扩展)和 Matrix(IRC 的现代替代品)采用。另一方面,PGP 和 S/MIME 正在被放弃,因为已经发表的攻击导致了信任的丧失。
如果你想编写自己的端到端加密消息应用程序怎么办?不幸的是,这个领域使用的大部分东西都是临时的,你必须自己填写许多细节才能获得一个功能齐全且安全的系统。Signal 已经开源了大部分代码,但缺乏文档,并且可能难以正确使用。另一方面,你可能会更容易地使用像 Matrix 这样的分散式开源解决方案,这可能更容易与之集成。这就是法国政府所做的。
在我们结束本章之前,我还想谈谈一些未解决的问题和正在进行的研究问题。例如
* 群组消息
* 对多个设备的支持
* 比 TOFU 更好的安全保证
让我们从第一项开始:*群组消息传递* 。目前,虽然不同应用程序以不同的方式实现,但群组消息传递仍在积极研究中。例如,Signal 应用程序让客户端理解群聊。服务器只看到一对一的用户交谈------从来不少,也从来不多。这意味着客户端必须将群聊消息加密发送给所有群聊参与者并单独发送。这称为*客户端端点扩散*,并不是非常适合扩展。当服务器看到例如 Alice 向 Bob 和 Charles 发送多条长度相同的消息时,它也不太难弄清楚谁是群组成员(见图 10.20)。

图 10.20 有两种方法可以实现群聊的端到端加密。客户端端点扩散方法意味着客户端必须使用其已经存在的加密通道向每个接收者单独发送消息。这是一个很好的方法,可以隐藏群组成员。服务器端端点扩散方法允许服务器将消息转发给每个群聊参与者。从客户端的角度来看,这是一种减少发送消息数量的好方法。
另一方面,WhatsApp 使用 Signal 协议的变体,其中服务器知道群聊成员。这一变化允许参与者向服务器发送单个加密消息,服务器负责将其转发给群成员。这称为*服务器端端点扩散*。
群聊的另一个问题是*扩展性* ,可以扩展到大量成员。为此,行业中的许多参与者最近围绕*消息层安全*(MLS)标准聚集在一起,以应对大规模安全的群组消息传递。但是似乎还有很多工作要做,人们可以想象一下,在拥有一百多名参与者的群聊中是否真的有保密性?
注意 这仍然是一个积极研究的领域,不同的方法具有安全性和可用性方面的不同权衡。例如,在 2021 年,似乎没有任何群聊协议提供*转录一致性*,这是一种确保群聊所有参与者以相同顺序看到相同消息的加密属性。
支持多个设备要么根本不存在,要么以各种方式实现,最常见的是假装你的不同设备是群聊的不同参与者。TOFU 模型可以使处理多个设备变得非常复杂,因为每个设备具有不同的身份密钥可能会成为一个真正的密钥管理问题。想象一下,为每个设备以及每个朋友的设备验证指纹。例如,Matrix 让用户签署自己的设备。然后其他用户可以通过验证其关联的签名来信任所有你的设备作为一个实体。
最后,我提到 TOFU 模型也不是最好的,因为它是基于第一次看到公钥时信任它,而大多数用户后来并不验证指纹是否匹配。这个问题能做些什么?如果服务器决定只向 Alice 冒充 Bob 怎么办?这是*密钥透明度*试图解决的问题。密钥透明度是 Google 提出的一个协议,类似于我在第九章讨论过的证书透明度协议。还有一些研究利用区块链技术,我将在第十二章关于加密货币的部分谈到。
### 摘要
* 端到端加密是为了保护真实人类之间的通信。实施端到端加密的协议对于服务器之间发生的漏洞更具弹性,并且可以极大地简化公司的法律要求。
* 端到端加密系统需要一种方法来在用户之间建立信任。这种信任可以来自我们已经知道的公钥,或者是我们信任的带外信道。
* PGP 和 S/MIME 是今天用于加密电子邮件的主要协议,然而它们都不被认为是安全的使用方式,因为它们使用了旧的密码算法和实践。它们还与已经被证明在实践中容易受到不同攻击的电子邮件客户端集成很差。
* PGP 使用信任网(WOT)模型,用户相互签署公钥以便让其他人信任他们。
* S/MIME 使用公钥基础设施来建立参与者之间的信任。它最常用于公司和大学。
* PGP 的一种替代方案是 saltpack,它修复了一些问题,同时依赖社交网络来发现其他人的公钥。
* 电子邮件在加密方面始终存在问题,因为该协议并未考虑加密。另一方面,现代消息传递协议和应用程序被认为是加密电子邮件的更好选择,因为它们在设计时考虑了端到端加密。
* Signal 协议被大多数消息传递应用程序用于保护用户之间的端到端通信。Signal Messenger、WhatsApp、Facebook Messenger 和 Skype 都宣称他们使用 Signal 协议来保护消息。
* 其他协议,如 Matrix,试图标准化端到端加密消息传递的联邦协议。联邦协议是任何人都可以与之交互操作的开放协议(与限制在单个应用程序中的集中协议相对)。