斯坦福密码学-I-笔记-全-
斯坦福密码学 I 笔记(全)
001:课程概述 🎯


在本节课中,我们将学习密码学课程的整体框架、核心目标以及基本概念。课程将分为两部分:对称加密系统和公钥密码学。我们将探讨如何正确使用密码学原语,并理解其安全性的基本原理。
课程目标与学习方法 🎯
我的目标是教会你密码学原语的工作原理,更重要的是,教会你如何正确使用这些原语,并能够推理你所构建系统的安全性。我们将看到密码学原语的各种抽象形式,并进行一些安全性证明。到课程结束时,你将能够推理密码学构造的安全性,并能够破解那些不安全的构造。
关于如何学习这门课程,我强烈建议你在听课时记笔记。我鼓励你用自己的话总结和记录所呈现的材料。此外,视频中的讲解速度可能比正常课堂快,因此我建议你定期暂停视频,思考正在讲解的内容,直到你完全理解后再继续。
视频中会不时暂停并弹出问题。这些问题旨在帮助你理解材料,我强烈建议你自己回答这些问题,而不是跳过它们。通常,这些问题与刚刚讲过的内容相关,因此回答起来应该不会太困难。

密码学的广泛应用 🌐
如今,密码学被广泛应用于所有计算机领域,是保护数据的常用工具。例如:
- 网络流量:使用HTTPS协议保护。
- 无线流量:例如,Wi-Fi流量使用WPA2协议(属于802.11i标准的一部分)保护。
- 手机流量:GSM中使用加密机制保护。
- 蓝牙流量:使用密码学保护。
我们将在课程中详细了解这些系统的工作原理,特别是SSL和802.11i。
密码学也用于保护存储在磁盘上的文件。通过加密文件,即使磁盘被盗,文件内容也不会泄露。它还用于内容保护,例如,你购买的DVD和蓝光光盘上的电影是加密的。具体来说,DVD使用一种称为CSS(内容扰乱系统)的系统,而蓝光使用AACS系统。我们将讨论CSS和AACS的工作原理。事实证明,CSS是一个相对容易破解的系统,我们将进行一些密码分析,并展示如何破解CSS中使用的加密。

密码学还用于用户认证以及我们将在下一节讨论的许多其他应用。
安全通信:Alice与Bob的故事 🔐

现在,让我们回到安全通信,讨论一台笔记本电脑试图与网络服务器通信的情况。这也是介绍我们的朋友Alice和Bob的好时机,他们将陪伴我们整个学期。本质上,Alice试图与Bob安全通信。在这里,Alice在笔记本电脑上,Bob在服务器上。用于实现此目的的协议称为HTTPS,实际的协议称为SSL,有时也称为TLS。这些协议的目标是确保数据在网络中传输时,攻击者既不能窃听数据,也不能在数据通过网络时修改数据。即实现无窃听和无篡改。
正如我所说,用于保护网络流量的TLS协议实际上由两部分组成。第一部分称为握手协议,Alice和Bob在此过程中相互通信。在握手结束时,双方之间会生成一个共享的密钥K。因此,Alice和Bob都知道这个密钥K,但观察对话的攻击者不知道密钥K是什么。
建立这个共享密钥、进行握手的方式是使用公钥密码学技术,这将是课程第二部分的内容。

一旦Alice和Bob拥有了共享密钥,他们就可以使用这个密钥,通过正确加密彼此之间的数据来进行安全通信。这实际上是课程的第一部分内容,即一旦双方拥有共享密钥,他们如何使用该密钥来加密和保护彼此之间传输的数据。
磁盘文件加密:与时间对话的Alice 💾
如前所述,密码学的另一个应用是保护磁盘上的文件。这里有一个被加密的文件,这样即使磁盘被盗,攻击者也无法读取文件内容。如果攻击者试图修改磁盘上的文件数据,当Alice尝试解密该文件时,这种篡改将被检测到,她将忽略文件内容。因此,存储在磁盘上的文件同时具有机密性和完整性。

我想提出一个小的哲学观点:实际上,在磁盘上存储加密文件与保护Alice和Bob之间的通信非常相似。具体来说,当你在磁盘上存储文件时,本质上是今天的Alice想要在明天读取该文件。因此,与双方(Alice和Bob)通信不同,在磁盘存储加密的情况下,是今天的Alice与明天的Alice通信。但本质上,安全通信和安全文件存储这两种场景在哲学上是相通的。
对称加密系统:共享的秘密 🔑
保护流量的基础构建块是所谓的对称加密系统,我们将在课程的前半部分详细讨论它。
在对称加密系统中,双方Alice和Bob共享一个攻击者不知道的密钥K,只有他们知道密钥K。他们将使用一个密码,该密码由两个算法E和D组成。E称为加密算法,D称为解密算法。
加密算法以消息m和密钥K作为输入,并产生相应的密文c。解密算法则相反,它以密文c和密钥K作为输入,并产生相应的消息m。
我想强调一个非常重要的观点,我现在只说一次,但以后不会再重复,这是一个极其重要的观点:算法E和D(实际的加密算法)是公开已知的。对手确切地知道它们是如何工作的。唯一保密的是密钥K。除此之外,其他一切都是完全公开的。认识到这一点非常重要:你应该只使用公开的算法,因为这些算法已经经过了由数百人组成的庞大社区多年的同行评审,只有在社区证明它们无法被破解后,这些算法才会开始被使用。事实上,如果有人对你说:“嘿,我有一个你可能想用的专有密码”,通常的答案应该是坚持使用标准算法,而不是使用专有密码。实际上,有许多专有密码的例子,一旦它们被逆向工程,就很容易被简单的分析破解。

一次性密钥与多次性密钥 🔄
即使在我们将要讨论的对称加密的简单情况下,实际上也有两种情况我们将依次讨论。
第一种情况是每个密钥只用于加密一条消息,我们称之为一次性密钥。例如,当你加密电子邮件时,通常每封电子邮件都使用不同的对称密钥加密。由于密钥只用于加密一条消息,实际上存在相当高效和简单的方法来使用这些一次性密钥加密消息,我们将在下一个模块中讨论这些方法。
然而,在许多情况下,密钥需要用于加密多条消息,我们称之为多次性密钥。例如,当你在文件系统中加密文件时,同一个密钥用于加密许多不同的文件。事实证明,如果密钥现在要用于加密多条消息,我们需要更多的机制来确保加密系统的安全。实际上,在讨论了一次性密钥之后,我们将转而讨论专门为多次性密钥设计的加密模式,并且我们将看到,在这些情况下,需要采取更多步骤来确保安全。

密码学的局限性与重要原则 ⚠️
最后,我想指出关于密码学需要记住的几个重要事项。
首先,密码学当然是保护计算机系统中信息的绝佳工具。然而,同样重要的是,密码学有其局限性。首先,密码学并非所有安全问题的解决方案。例如,如果你有软件漏洞,那么密码学通常无法帮助你。同样,如果你担心社会工程攻击(攻击者试图欺骗用户采取会伤害用户的行动),那么密码学通常也帮不上忙。因此,尽管它是一个极好的工具,但它并非所有安全问题的解决方案,这一点非常重要。
另一个非常重要的点是,如果密码学实现不正确,它基本上就变得毫无用处。例如,有许多系统运行良好(我们将看到这些系统的例子),实际上允许Alice和Bob通信,并且Bob能够接收和解密Alice发送的消息。然而,由于密码学实现不正确,这些系统完全不安全。实际上,我想提一个旧的加密标准,称为WEP(有线等效保密),用于加密Wi-Fi流量。WEP中包含许多错误。通常,当我想向你展示在密码学中不应该怎么做时,我会以WEP中的做法为例。所以对我来说,有一个可以指出的协议例子来说明不应该怎么做是非常幸运的。
最后,我想让你记住的一个非常重要的点是:密码学不是你应该尝试自己发明和设计的东西。正如我所说,密码学中有标准,有标准的密码学原语,我们将在本课程中详细讨论这些。你应该主要使用这些标准的密码学原语,而不是自己发明这些东西。这些标准已经经过了数百人多年的评审,这是临时设计无法比拟的。正如我所说,多年来有许多临时设计的例子,一旦被分析,就立即被破解。

总结 📝
本节课我们一起学习了密码学课程的整体概述。我们明确了课程的目标是理解密码学原语的工作原理并学会安全地使用它们。我们看到了密码学在安全通信(如HTTPS)、文件加密和内容保护等领域的广泛应用。我们引入了对称加密系统的基本模型,其中双方共享一个密钥K,并使用公开的加密E和解密D算法。我们区分了一次性密钥和多次性密钥的不同应用场景。最后,我们强调了密码学的局限性、正确实现的重要性,以及遵循经过验证的密码学标准而非自行设计的核心原则。在接下来的课程中,我们将深入探讨这些概念的具体实现和安全性分析。
002:密码学概述

在本节课中,我们将要学习密码学的基本概念及其广泛的应用领域。密码学不仅仅是关于加密通信,它还涵盖了数字签名、匿名通信、安全多方计算等众多神奇而强大的技术。

密码学的核心:安全通信
密码学的核心是安全通信,它主要包含两个部分:安全密钥建立和使用共享密钥的安全通信。
上一节我们介绍了密码学的整体图景,本节中我们来看看其核心组成部分。
- 安全密钥建立:这指的是通信双方(例如Alice和Bob)通过交换信息,最终协商出一个只有他们两人知道的共享密钥
K。在此过程中,双方能确认彼此的身份,而窃听者则无法获知密钥内容。 - 安全通信:在获得共享密钥后,双方需要使用加密方案来安全地交换信息。一个完善的加密方案不仅能提供机密性(防止攻击者读取信息),还能提供完整性(防止攻击者篡改信息而不被发现)。

超越通信:密码学的广泛应用
然而,密码学的能力远不止于此。以下是密码学的一些关键应用领域。
数字签名
数字签名是物理世界签名在数字世界的对应物。其核心挑战在于:数字签名不能在所有文件上都相同,否则攻击者可以轻易复制粘贴。

为了解决这个问题,数字签名被设计为所签署内容的一个函数。这意味着,签名值依赖于被签名的具体数据。如果攻击者试图将一个签名复制到另一个不同的文档上,验证将会失败。我们将在课程后续学习如何构建并证明数字签名方案的安全性。
匿名通信与数字现金
密码学可以保护用户的隐私。
- 匿名通信:例如,用户Alice希望匿名地与聊天服务器Bob通信。通过像Tor(洋葱路由)这样的系统,Alice的消息可以通过一系列代理加密转发,使得Bob和代理都无法确定通信者的真实身份,同时保持双向通信能力。
- 匿名数字现金:这模拟了物理现金的匿名性。用户Alice拥有一个数字美元硬币,她可以匿名地在线消费它。但数字世界的难题是:Alice可以轻易复制这个硬币并进行双重消费。
这个悖论在于:匿名性与安全性似乎冲突。如果现金完全匿名,就无法追查双重消费的欺诈者。
解决方案的精妙之处在于:如果Alice只花费硬币一次,她的身份保持匿名;但如果她尝试双重消费,她的身份将会完全暴露。我们将在课程中看到如何实现这一点。
安全多方计算
密码学能让我们在不泄露隐私的情况下进行协同计算。以下是两个经典例子:
- 选举系统:选民希望统计出哪个政党获得了多数票,但又不希望自己的个人投票被泄露。通过引入一个选举中心,选民发送加密选票,选举中心可以计算出获胜者,但除了选举结果外,无法获知任何个人的投票选择。
- 私人拍卖(维克里拍卖):在这种拍卖中,出价最高者获胜,但只需支付第二高的出价金额。目标是在不公开具体出价的情况下,计算出获胜者和其应付价格(即第二高价)。

以上都是安全多方计算的具体实例。抽象地看,多个参与者各自拥有秘密输入(如选票、出价),他们希望共同计算某个函数的结果(如多数票、第二高价),但除了函数输出外,不泄露任何关于个人输入的信息。
一种简单但不安全的方法是引入一个可信第三方来收集所有输入、计算结果并公布。而密码学一个惊人的核心定理指出:任何可以通过可信第三方完成的计算,都可以在没有可信第三方的情况下完成。参与者通过执行特定的协议相互通信,最终能共同得到计算结果,同时保证各自输入的私密性。
“魔法”般的应用
有些密码学应用近乎魔法,它们展示了该领域的巨大潜力。
- 隐私外包计算:想象一下,Alice可以将她的搜索查询加密后发送给Google。Google能够在不解密的情况下,直接在加密数据上执行搜索算法,并将加密的搜索结果返回给Alice。Alice解密后得到答案,而Google全程不知道她搜索了什么。这种全同态加密技术目前虽不高效,但其理论可能性已足够惊人。
- 零知识证明:Alice知道一个巨大合数
N的因子分解(N = p * q)。她可以向Bob证明自己知道这个分解,但在证明过程中,Bob完全学不到任何关于p和q的信息。这不仅适用于因数分解,几乎对于任何你能解答的难题,你都可以在不透露答案的前提下,向他人证明你拥有答案。
现代密码学的科学方法
现代密码学是一门严谨的科学。在介绍每一个密码学原语(如数字签名)时,我们都会遵循以下三个严格步骤:
- 精确定义:首先,我们会精确界定威胁模型。即,明确攻击者的能力(能做什么)和目标(想达成什么)。例如,对于数字签名,我们需要精确定义什么是“不可伪造性”。
- 提出构造:然后,我们提出一个具体的密码学方案或构造。
- 给出证明:最后,我们提供安全性证明。证明的核心思路是:如果存在攻击者能破坏我们的构造,那么我们就可以利用这个攻击者来解决一个公认的困难问题(如大整数分解)。由于该问题被认为是困难的,因此我们的构造在定义的威胁模型下就是安全的。

总结

本节课中我们一起学习了密码学的广阔天地。我们从安全通信这一核心出发,探索了数字签名、匿名系统、安全多方计算等高级应用,并领略了如同隐私外包计算和零知识证明这样“魔法”般的可能性。最后,我们了解了现代密码学严谨的“定义-构造-证明”科学范式。密码学不仅关乎秘密,更关乎在数字世界中构建信任、隐私和安全的新规则。
003:密码学历史 📜

在本节课中,我们将要学习密码学的历史,了解几种经典但已被破解的密码。通过分析这些历史上的密码,我们可以理解为什么它们不安全,并为学习现代密码学打下基础。
密码学基础概念
在开始介绍历史密码之前,我们首先需要理解密码学的基本模型。这个模型涉及三个角色:爱丽丝(Alice) 和 鲍勃(Bob) 希望安全通信,而一个 攻击者(Attacker) 试图窃听他们的对话。
为了安全通信,爱丽丝和鲍勃会共享一个秘密 密钥(Key),我们用 K 表示。攻击者对这个密钥一无所知。

他们使用一个 密码(Cipher) 进行通信,它由一对算法组成:加密算法(E) 和 解密算法(D)。
这些算法的工作原理如下:
- 加密算法
E接收消息M和密钥K作为输入,输出一个 密文(Ciphertext)C。我们可以用公式表示为:
C := E(K, M) - 密文
C通过某种方式(例如互联网)传输给鲍勃。 - 解密算法
D接收密文C和相同的密钥K作为输入,输出原始消息M。公式表示为:
M := D(K, C)
我们称这类密码为 对称密码(Symmetric Cipher),因为加密方和解密方使用相同的密钥 K。在本课程后面,我们还会看到加密和解密使用不同密钥的密码。
历史上的密码示例
上一节我们介绍了密码学的基本模型,本节中我们来看看几种历史上著名的、但已被完全破解的密码。
替换密码
最简单的例子是 替换密码(Substitution Cipher)。它的密钥是一个替换表,规定了如何将字母表中的每个字母映射到另一个字母。
例如,一个密钥可能规定:
- 字母
A映射到C - 字母
B映射到W - 字母
C映射到N - ...
- 字母
Z映射到A
使用这个密钥加密消息时,我们逐个字母进行替换。例如,消息 B C Z A 会被加密为 W N A C。解密过程则使用相同的替换表进行反向查找。
凯撒密码(Caesar Cipher) 是替换密码的一个特例,但它不是一个真正的密码,因为它没有可变的密钥。它只是将字母固定地移位3位(例如,A 变成 D,B 变成 E)。由于密钥固定且公开,攻击者一旦知道加密方式,就能轻松解密。
现在,让我们回到使用随机替换表的替换密码。它的密钥空间有多大?对于26个字母,可能的替换表(即排列)数量是 26!(26的阶乘),大约等于 2^88。这意味着描述一个密钥大约需要88比特,这个密钥空间大小本身是足够的。
然而,替换密码非常不安全。以下是破解它的方法:
破解替换密码的核心是利用 字母频率分析。在英文文本中,字母的出现频率并不均匀。例如:
- 字母
E是最常见的,出现频率约为12.7%。 - 字母
T次之,约为9.1%。 - 字母
A再次之,约为8.1%。
破解步骤如下:
- 统计密文中每个字母的出现频率。
- 频率最高的字母极有可能是
E加密后的结果,由此可以恢复密钥表中的一项。 - 频率次高的字母极有可能是
T加密后的结果,恢复第二项。 - 以此类推,可以恢复出几个高频字母的映射关系。
当高频字母分析遇到瓶颈时(因为中低频字母频率接近),我们可以转而分析 双字母组合(Digrams) 的频率。例如,在英文中,“TH”、“HE”、“IN”、“AN” 等组合非常常见。
通过统计密文中双字母组合的频率,并与已知的英文双字母组合频率进行匹配,可以进一步恢复密钥。如果需要,还可以分析 三字母组合(Trigrams)。
最终,攻击者仅凭密文(唯密文攻击)就能恢复出整个解密密钥和原始明文。因此,使用替换密码加密几乎没有意义。
维吉尼亚密码
现在我们从古罗马时代快进到文艺复兴时期,看看由16世纪的学者维吉尼亚设计的一种密码——维吉尼亚密码(Vigenère Cipher)。
在维吉尼亚密码中,密钥是一个单词(例如 CRYPTO)。加密时,将明文写在密钥下方,并重复密钥以覆盖整个明文长度。
加密过程是:将明文字母和对应位置的密钥字母进行模26加法(即 A=0, B=1, ..., Z=25)。例如,Y (24) + A (0) = Z (25);T (19) + A (0) = U (20)。如果相加结果超过25,则绕回开头。解密则是进行模26减法。
破解维吉尼亚密码也相对容易,假设我们知道密钥长度(例如6):
- 将密文按密钥长度(6)分组。
- 观察每组中的第1个字母。所有这些字母都是用密钥的第1个字母(例如
C)加密的,相当于一个移位密码。 - 统计这些“第1个字母”的频率,最常见的那个极有可能是最常用的英文字母
E加密后的结果。由此可以反推出密钥的第1个字母(H - E = C)。 - 对每组中的第2个、第3个字母...重复此过程,即可恢复整个密钥。
如果不知道密钥长度,可以尝试不同的长度(1, 2, 3...)进行上述分析,直到解密的文本变得通顺有意义为止。这也是一种 唯密文攻击。
维吉尼亚密码的核心理念——模加法——是好的,但它的具体实现方式(短密钥重复使用)存在严重缺陷,我们将在后续课程中看到如何修正它。

转子机
快进到19世纪,电气时代来临,人们设计了使用电动机的密码机,即 转子机(Rotor Machine)。
早期的例子是 赫本机(Heburn),它使用一个转子。密钥编码在一个圆盘(转子)上,实质上是一个替换表。每按一次打字机按键,转子就转动一格,从而改变当前的替换表。因此,即使连续输入相同的字母,输出的密文字母也会不同。
然而,赫本机很快也被通过字母频率、双字母组合频率等统计方法破解。
为了对抗统计攻击,转子机变得越来越复杂,最终发展到著名的 恩尼格玛密码机(Enigma)。恩尼格玛机使用3个、4个或5个转子(不同版本不同)。密钥是这些转子的初始位置设置。对于3转子版本,密钥空间是 26^3;对于4转子版本,是 26^4 ≈ 2^18。

以今天的标准看,2^18 的密钥空间很小,一台计算机可以瞬间暴力破解所有可能密钥。然而,在二战时期,英国布莱切利园的密码学家们成功地对恩尼格玛机实施了唯密文攻击,破译了德国的通信,这在战争中发挥了重要作用。
向现代密码学过渡
战后,机械时代结束,数字时代开启。随着计算机的普及,美国政府意识到它从工业界采购大量数字设备,因此希望工业界使用良好的密码。
于是,政府发起了 数据加密标准(Data Encryption Standard, DES) 的提案征集。1974年,IBM团队提出的一个密码被采纳为DES。DES使用56比特密钥,一次加密64比特(8个字节)的数据块。
DES的56比特密钥空间在今天看来太小,可以通过暴力搜索破解,因此已被认为不安全,不应在新项目中使用(尽管一些遗留系统可能还在用)。如今,我们有新的密码标准,如 高级加密标准(Advanced Encryption Standard, AES),它使用128比特或更长的密钥,我们将在课程后面详细讨论。
总结
本节课中我们一起学习了密码学的简要历史,重点分析了三种经典密码:
- 替换密码:通过字母频率分析可轻易破解。
- 维吉尼亚密码:通过分组和频率分析可破解,但其模加法的思想被沿用。
- 转子机(如恩尼格玛):虽然机械结构复杂,但密钥空间有限或存在其他弱点,最终被破解。

这些历史密码的失败教训告诉我们,一个安全的密码系统不能仅依赖算法的保密性,必须能够抵抗基于统计特性等的各种分析攻击,并且需要足够大的密钥空间来对抗暴力破解。这些原则为现代密码学的设计奠定了基础。
004:离散概率速成课程 🎲
在本节课中,我们将要学习现代密码学的基础数学语言——离散概率。我们将从基本概念开始,逐步理解概率分布、事件、随机变量和随机算法,这些都是构建安全密码系统所必需的工具。

概率分布
上一节我们介绍了密码学需要严谨的数学基础。本节中,我们来看看概率论的核心概念——概率分布。
概率总是定义在一个全集 U 上。在我们的语境中,U 总是一个有限集合。最常见的情况是,全集是所有 n 位二进制字符串的集合,记作 {0,1}^n。
例如,集合 {0,1}^2 是所有2位字符串的集合,包含 00、01、10、11 四个元素。更一般地,集合 {0,1}^n 包含 2^n 个元素。
全集 U 上的一个概率分布是一个函数 P。这个函数为全集中的每一个元素 x 分配一个介于0和1之间的数字,称为该元素的权重或概率。
这个函数 P 只有一个要求:所有权重的总和必须为1。
公式表示为:
∑ P(x) = 1, 其中 x ∈ U
让我们看一个简单的例子。回到我们的2位字符串全集 {00, 01, 10, 11}。我们可以考虑以下概率分布:
- 为元素
00分配概率1/2 - 为元素
01分配概率1/8 - 为元素
10分配概率1/4 - 为元素
11分配概率1/8
这些数字的总和为 1/2 + 1/8 + 1/4 + 1/8 = 1,因此 P 是一个有效的概率分布。这些数字的含义是:如果我从这个分布中抽样,我以 1/2 的概率得到字符串 00,以 1/8 的概率得到 01,以此类推。
两种经典分布
现在我们已经理解了什么是概率分布,让我们看看两种经典的分布示例。
第一种是均匀分布。均匀分布为全集中的每个元素分配完全相同的权重。
我们用 |U| 表示全集 U 的大小(即元素的数量)。由于我们希望所有权重之和为1,并且所有权重相等,这意味着对于全集中的每个元素 x,我们分配的概率是 1/|U|。
具体来说,在我们2位字符串的例子中,均匀分布会为每个字符串分配 1/4 的权重。显然,所有权重之和为1。这意味着如果我从这个分布中随机抽样,我会均匀地抽样到所有四个2位字符串,每个字符串被抽到的可能性相同。
另一种非常常见的分布是点分布。点分布 P_x0 基本上将所有权重放在一个特定的点 x0 上。
公式表示为:
P_x0(x0) = 1
P_x0(x) = 0, 对于所有 x ≠ x0

回到我们的例子,一个将所有质量放在字符串 10 上的点分布,会为 10 分配概率 1,为所有其他字符串分配概率 0。如果我从这个分布中抽样,我几乎总是保证抽到字符串 10,永远不会抽到其他字符串。
事件与概率
接下来,我们定义事件的概念。
考虑全集 U 的一个子集 A。我们定义子集 A 的概率,就是集合 A 中所有元素的权重之和。
换句话说,我对集合 A 中的所有元素 x 的权重 P(x) 进行求和。
因为整个全集所有权重之和为1,这意味着:
- 整个全集
U的概率是1。 - 全集任意子集的概率是
0到1之间的一个数。
我们称全集 U 的子集 A 为一个事件,集合 A 的概率称为该事件的概率。
让我们看一个简单的例子。
假设我们观察的全集 U 由所有8位字符串(即所有字节值)组成。这个全集的大小 |U| 是 256。
现在定义以下事件 A:该事件包含所有8位字符串中,最低两位(最低有效位)为 11 的字符串。
例如,字符串 01011010 不在集合 A 中,但字符串 01011011 在集合 A 中。
现在,考虑全集 U 上的均匀分布。问题是:事件 A 的概率是多少?即,当我们随机选择一个字节时,该字节的最低两位恰好是 11 的概率是多少?
答案是 1/4。原因是,在 256 个8位字符串中,恰好有 64 个(即四分之一)字符串的最低两位是 11。由于我们考虑的是均匀分布,每个字符串的概率是 1/256。因此,这 64 个元素的总概率是 64 * (1/256) = 1/4。
并集界限

事件概率的一个非常简单的界限称为并集界限。
假设我们有两个事件 A1 和 A2,它们都是某个全集 U 的子集。我们想知道事件 A1 发生或事件 A2 发生的概率是多少?换句话说,就是这两个事件并集 A1 ∪ A2 的概率。
并集界限告诉我们,A1 或 A2 发生的概率小于或等于两个概率之和。
公式表示为:
Pr[ A1 ∪ A2 ] ≤ Pr[A1] + Pr[A2]
这其实很容易理解。当我们计算两个概率之和 Pr[A1] + Pr[A2] 时,我们实际上是在对 A1 中所有元素的概率和 A2 中所有元素的概率进行求和。如果两个事件有交集,那么交集部分的元素概率在右边被重复计算了两次。因此,两个概率之和实际上会大于或等于并集的真实概率。
如果两个事件是不相交的(即它们的交集为空集),那么 A1 或 A2 发生的概率恰好等于两个概率之和。
公式表示为:
如果 A1 ∩ A2 = ∅,则 Pr[ A1 ∪ A2 ] = Pr[A1] + Pr[A2]

我们将在课程中不时使用这些事实。
让我们看一个简单的例子。
假设事件 A1 是所有 n 位字符串中以 11 结尾的字符串集合。事件 A2 是所有 n 位字符串中以 11 开头的字符串集合(n 可以是8或更大的数)。
现在的问题是:事件 A1 发生或事件 A2 发生的概率是多少?即,如果我从全集 U 中均匀抽样,最低有效位是 11 或最高有效位是 11 的概率是多少?
根据我们之前的计算,每个事件的概率是 1/4。因此,根据并集界限,A1 或 A2 发生的概率 ≤ 1/4 + 1/4 = 1/2。我们由此证明了,看到最高有效位是 11 或最低有效位是 11 的概率小于等于二分之一。这是如何使用并集界限来限定两个事件之一发生概率的一个简单例子。
随机变量
我们需要定义的下一个概念是随机变量。
随机变量是相当直观的对象,但其正式定义可能有点令人困惑。我将通过一个例子来说明,希望这能足够清晰。
形式上,一个随机变量 X 是一个从全集 U 到某个集合 V 的函数。我们说集合 V 是随机变量取值的地方。
让我们看一个具体的例子。
假设我们有一个随机变量 X,它映射到集合 {0,1}。因此,这个随机变量的值要么是 0,要么是 1(基本上就是1比特)。
这个随机变量将我们的全集(所有 n 位二进制字符串的集合 {0,1}^n)映射到 {0,1}。它是如何做到的呢?给定全集中的一个特定样本(一个特定的 n 位字符串 y),随机变量 X 会简单地输出 y 的最低有效位。就是这样,这就是整个随机变量。
现在让我问你:假设我们观察集合 {0,1}^n 上的均匀分布。这个随机变量输出 0 的概率是多少?输出 1 的概率又是多少?
答案是各一半。让我们推理一下为什么是这样。
当随机变量输出 0 时,意味着全集中的样本其最低有效位被设置为 0。当随机变量输出 1 时,意味着样本的最低有效位被设置为 1。

如果我们均匀随机地选择字符串,那么选到一个最低有效位为 0 的字符串的概率恰好是 1/2。这就是为什么随机变量以恰好 1/2 的概率输出 0。同样,选到一个最低有效位为 1 的随机 n 位字符串的概率也是 1/2。因此我们说,随机变量输出 1 的概率也恰好是 1/2。
更一般地说,如果一个随机变量在某个集合 V 中取值,那么这个随机变量实际上在集合 V 上诱导了一个分布。
本质上,它的意思是,变量输出某个值 v 的概率,等于我们在全集中随机抽样一个元素,然后应用函数 X,得到的输出恰好等于 v 的可能性。
一个特别重要的随机变量叫做均匀随机变量。它的定义正如你所期望的那样。
假设 U 是某个有限集合(例如所有 n 位二进制字符串的集合)。我们用一个带小写 R 的奇怪箭头 R ← U 来表示一个随机变量 R,它从集合 U 中均匀抽样。这表示随机变量 R 就是一个在集合 U 上的均匀随机变量。
用符号表示,这意味着对于全集中的所有元素 a,R 等于 a 的概率就是 1/|U|。
为了确保这个概念清晰,让我问你一个简单的谜题。
假设我们有一个在2位字符串集合 {00, 01, 10, 11} 上的均匀随机变量 R。

现在定义一个新的随机变量 X,它基本上是 R 的第一位和第二位之和。即 X = R1 + R2,将这两个比特位视为整数进行相加。
例如,如果 R 恰好是 00,那么 X 将是 0 + 0 = 0。
问题是:X 等于 2 的概率是多少?
不难看出,答案恰好是 1/4。因为基本上 X 等于 2 的唯一方式是 R 恰好是 11。而 R 等于 11 的概率是 1/4,因为 R 在所有2位字符串集合上是均匀的。

随机算法
本段我要定义的最后一个概念是随机算法。
我相信你们都熟悉确定性算法。这些算法接受特定的输入数据 M,并总是产生相同的输出 y。如果我们用相同的输入运行算法100次,总是会得到相同的输出。你可以把确定性算法看作一个函数,给定特定的输入 M,总是产生完全相同的输出 A(M)。
随机算法则有些不同。它和以前一样,将输入数据 M 作为输入,但它还有一个隐式的参数 R。这个 R 在每次运行算法时都会被重新采样。具体来说,R 是从所有 n 位字符串的集合中均匀随机采样的(对于某个任意的 n)。
现在的情况是,每次我们在特定输入 M 上运行算法时,都会得到不同的输出,因为每次都会生成不同的 R。第一次运行算法得到一个输出,第二次运行时生成一个新的 R 得到不同的输出,第三次运行时又生成一个新的 R 得到第三个输出,依此类推。
因此,真正理解随机算法的方式是:它实际上定义了一个随机变量。给定一个特定的输入消息 M,它定义了一个随机变量,这个随机变量定义了该算法在给定输入 M 下所有可能输出的集合上的一个分布。
需要记住的一点是:随机算法的输出每次运行都会改变。实际上,该算法定义了一个在所有可能输出集合上的分布。
让我们看一个具体的例子。
假设我们有一个随机算法 A,它以消息 M 作为输入。当然,它还有一个隐式输入,即用于随机化其操作的随机字符串 R。
现在,算法要做的是简单地使用随机字符串作为输入来加密消息 M。这基本上定义了一个随机变量,该随机变量取的值是消息 M 的加密结果。实际上,这个随机变量是在均匀密钥下所有可能的消息 M 加密结果的集合上的一个分布。
需要记住的主要点是:即使随机算法的输入可能总是相同,但每次运行随机算法时,你都会得到不同的输出。
总结


本节课中我们一起学习了离散概率的基础知识,这是现代密码学的核心数学语言。我们定义了概率分布,并探讨了均匀分布和点分布两种特例。我们学习了事件及其概率的计算,以及用于估算复合事件概率的并集界限。接着,我们引入了随机变量的概念,它从概率空间映射到值域并诱导出一个分布。最后,我们了解了随机算法,其输出会随着内部随机种子的变化而改变,从而定义了输出上的一个概率分布。掌握这些概念对于理解后续密码学方案的安全性证明至关重要。
005:离散概率速成课程(续)🔢

在本节课中,我们将继续学习离散概率中的几个重要工具。我们将探讨独立性、异或运算的关键性质以及著名的生日悖论。这些概念是理解后续加密系统的基础。
回顾:离散概率基础
上一节我们介绍了离散概率的基本定义。本节中我们来看看几个更深入的概念。

离散概率总是定义在一个有限集合 U 上。对我们而言,U 通常是所有 n 位二进制字符串的集合,记作 {0,1}^n。
概率分布 P 是一个函数,它为 U 中的每个元素分配一个在区间 [0,1] 内的权重,且所有权重之和为 1。
事件 是 U 的一个子集,其概率是该子集中所有元素权重之和。整个全集 U 的概率为 1。
随机变量 是一个从全集 U 到另一个集合 V 的函数。它本质上在 V 上定义了一个概率分布。
独立性

接下来,我们需要理解独立性的概念。直观地说,如果知道事件 A 发生与否,不会影响你对事件 B 是否发生的判断,那么这两个事件就是独立的。
以下是独立性的正式定义:
两个事件 A 和 B 是独立的,当且仅当:
P(A ∩ B) = P(A) * P(B)
这个公式表明,两个事件同时发生的概率等于它们各自概率的乘积。
同样,对于随机变量,我们说两个随机变量 X 和 Y 是独立的,如果对于它们取值范围内的所有 a 和 b,都有:
P(X=a ∧ Y=b) = P(X=a) * P(Y=b)
这意味着,知道 X 的值不会透露任何关于 Y 值的信息。
一个简单的例子
假设我们从集合 {00, 01, 10, 11} 中均匀随机地选择一个元素 R。定义两个随机变量:
- X = R 的最低有效位(最右边的位)。
- Y = R 的最高有效位(最左边的位)。
我们可以验证 X 和 Y 是独立的。例如:
P(X=0) = 1/2P(Y=0) = 1/2P(X=0 ∧ Y=0) = P(R=00) = 1/4
由于 1/4 = (1/2) * (1/2),独立性成立。这相当于抛两次公平的硬币,第一次的结果不影响第二次。
异或运算的关键性质
在密码学中,异或运算极其重要。首先,我们快速回顾一下异或的定义。
对于单个比特,异或是模 2 加法:
0 ⊕ 0 = 0
0 ⊕ 1 = 1
1 ⊕ 0 = 1
1 ⊕ 1 = 0
对于比特串,我们按位进行异或操作。
异或运算有一个关键性质,使其在密码学中非常有用:
定理:设 Y 是定义在 {0,1}^n 上的任意随机变量(其分布可以是任意的)。设 X 是定义在 {0,1}^n 上的均匀随机变量,且 X 与 Y 独立。定义 Z = X ⊕ Y。那么,Z 也是一个在 {0,1}^n 上的均匀随机变量。
换句话说,用独立的均匀随机值对任意数据进行异或,结果总是均匀随机的。
证明(以 n=1 为例)
设:
P(Y=0) = p0,P(Y=1) = p1,且p0 + p1 = 1。- 由于 X 均匀分布,
P(X=0) = P(X=1) = 1/2。 - 由于 X 与 Y 独立,联合概率为各自概率的乘积。
计算 P(Z=0)。当 Z=0 时,有两种可能:(X,Y) = (0,0) 或 (1,1)。
P(Z=0) = P(X=0 ∧ Y=0) + P(X=1 ∧ Y=1)
= (p0 * 1/2) + (p1 * 1/2)
= (p0 + p1) / 2
= 1 / 2
因此,P(Z=1) = 1 - 1/2 = 1/2。所以 Z 是均匀的。这个性质可以推广到任意长度的比特串。

生日悖论
最后,我们介绍离散概率中一个著名且实用的现象——生日悖论。
定理:从一个大小为 N 的集合 U 中,独立且随机地选取元素。当我们选取大约 √N 个元素时,有很大概率会出现两个相同的元素(即发生碰撞)。
更形式化地说,如果随机选取大约 1.2 * √N 个独立样本,那么至少有两个样本相同的概率将超过 1/2。
为什么叫“悖论”?
传统上,它用生日来举例:一个房间需要有多少人,才能使其中两人生日相同的概率超过50%?
- 一年有 N = 365 天。
√365 ≈ 19.1,1.2 * 19.1 ≈ 23。- 因此,只需要大约 23-24 个人,这个概率就会超过一半。这个数字比人们通常直觉认为的要小得多,因此被称为“悖论”。
在密码学中的应用示例
假设我们的集合 U 是所有 128 位字符串的集合,其大小是巨大的 2^128。根据生日悖论,如果我们独立随机地采样大约 2^64 个消息(因为 √(2^128) = 2^64),那么极有可能找到两个相同的消息。这个结论对分析哈希函数等密码学原语的安全性至关重要。
下图展示了碰撞概率随采样数量增长的趋势:在达到 √N 附近时,概率迅速上升至接近 1。

我们将在后续课程中更详细地讨论生日悖论及其影响。

总结
本节课中我们一起学习了离散概率的三个核心进阶概念:
- 独立性:事件或随机变量之间互不影响的性质,由概率的乘法规则
P(A∩B)=P(A)P(B)定义。 - 异或的完美掩码性质:
Z = X ⊕ Y,其中 X 是均匀且独立的,则无论 Y 的分布如何,Z 总是均匀的。这是许多密码方案的基础。 - 生日悖论:从大小为 N 的集合中采样约
√N次,碰撞(重复)的概率就很高。这对评估密码系统的安全边界非常重要。

掌握了这些概率论工具后,在下一节中,我们将开始构建我们的第一个加密系统示例。
006:信息论安全与一次性密码本 🔐

在本节课中,我们将学习密码学中一个核心的安全概念——信息论安全,并深入探讨一个经典的、理论上绝对安全的密码方案:一次性密码本。我们将从精确的密码定义开始,逐步理解完美保密性的含义,并分析一次性密码本为何能达到这种安全级别。
密码的定义与构成
上一节我们回顾了几个历史上的密码,它们都已被攻破。现在,我们将转换思路,讨论设计得更好的密码。但在开始之前,我们需要更精确地定义什么是密码。
一个密码实际上由两个算法构成:加密算法和解密算法。更准确地说,一个密码定义在一个三元组之上:
- 密钥空间
K:所有可能密钥的集合。 - 明文空间
M:所有可能消息的集合。 - 密文空间
C:所有可能密文的集合。

这个三元组在某种意义上定义了密码运行的环境。密码本身则是一对高效的算法:
- 加密算法
E:接收密钥和明文,输出密文。 - 解密算法
D:接收密钥和密文,输出明文。
这两个算法必须满足一致性要求,即对于明文空间中的任意消息 m 和密钥空间中的任意密钥 k,以下等式必须成立:
D(k, E(k, m)) = m
这个等式被称为一致性方程,是任何密码能够正常解密的基础。
关于“高效”一词,理论研究者通常理解为算法在输入规模上是多项式时间运行的,而实践者则理解为能在特定时间约束(例如一分钟内加密1GB数据)内完成。加密算法 E 通常是一个随机化算法,它在加密过程中会为自己生成随机比特。相反,解密算法 D 总是确定性的,给定相同的密钥和密文,其输出总是相同的。
一次性密码本:一个安全的例子
现在我们已经更好地理解了密码是什么,接下来我们来看第一个安全密码的例子:一次性密码本。它由维纳姆在20世纪初设计。
首先,我们用刚刚学到的术语来描述它:
- 明文空间
M:所有长度为n的二进制字符串的集合。 - 密文空间
C:所有长度为n的二进制字符串的集合。 - 密钥空间
K:所有长度为n的二进制字符串的集合。一个密钥就是一个随机的比特串,其长度与待加密的消息相同。

定义了密码的环境后,我们可以描述其工作原理,它实际上非常简单。
加密过程是,密文 c 就是密钥 k 与明文 m 的异或结果:
c = E(k, m) = k ⊕ m
这里,⊕ 表示按位模2加法(异或运算)。
解密过程是类似的,要解密密文 c,只需再次用密钥 k 与之异或:
D(k, c) = k ⊕ c
我们可以验证其一致性:D(k, E(k, m)) = k ⊕ (k ⊕ m) = (k ⊕ k) ⊕ m = 0 ⊕ m = m。这证明了一次性密码本确实是一个合法的密码。

从性能角度看,一次性密码本非常出色,因为它只涉及快速的异或运算。然而,它在实践中很难使用,因为密钥长度必须与消息长度相等。如果通信双方需要安全地传输一个长消息,他们首先需要安全地传输一个同样长的密钥。如果已经有安全传输长密钥的渠道,那么直接用这个渠道传输消息本身可能更简单。因此,密钥过长是其主要问题。

完美保密性:香农的定义
既然一次性密码本是一个密码,那么它安全吗?要回答这个问题,我们首先需要定义什么是安全的密码。为了研究密码的安全性,我们需要引入一些信息论的概念。事实上,第一个严格研究密码安全性的人是信息论之父——克劳德·香农。他在1949年发表了一篇著名论文,分析了一次性密码本的安全性。
香农安全定义的核心思想是:如果攻击者只能看到密文,那么他应该对明文一无所知。换句话说,密文不应泄露关于明文的任何信息。这需要形式化地定义“信息”的含义,而这正是香农所做的。
以下是香农的完美保密性定义:
假设我们有一个定义在 (K, M, C) 三元组上的密码 (E, D)。我们说这个密码具有完美保密性,如果对于明文空间 M 中任意两个长度相同的消息 m0 和 m1,以及对于密文空间 C 中的任意密文 c,以下条件成立:
Pr[ E(k, m0) = c ] = Pr[ E(k, m1) = c ]
其中,概率来源于密钥 k 是从密钥空间 K 中均匀随机选取的。

这个定义意味着什么?它表明,如果攻击者截获了一个特定的密文 c,那么该密文由消息 m0 加密而来的概率,与它由消息 m1 加密而来的概率完全相同。因此,仅凭密文 c,攻击者无法判断它到底来自 m0 还是 m1。由于这个性质对所有消息对都成立,攻击者实际上无法从密文中获得关于明文的任何信息。
用一句话概括:对于具有完美保密性的密码,不存在有效的唯密文攻击。这意味着,无论攻击者多么强大、计算能力多强,仅通过分析密文,他学不到任何关于明文的内容。
一次性密码本的完美保密性证明

理解了完美保密性的含义后,一个自然的问题是:我们能构建具有完美保密性的密码吗?答案是肯定的,而且不必舍近求远——一次性密码本本身就具有完美保密性。这是香农的第一个重要结果,其证明非常简单。
根据定义,对于任意消息 m 和密文 c,概率 Pr[ E(k, m) = c ] 等于将 m 映射到 c 的密钥数量,除以密钥的总数。即:
Pr[ E(k, m) = c ] = |{ k in K : E(k, m) = c }| / |K|
如果对于所有消息 m 和密文 c,分子 |{ k in K : E(k, m) = c }| 是一个常数,那么对于任意两个消息 m0 和 m1,以及任意密文 c,上述概率将总是相等,从而满足完美保密性的定义。
现在,我们来看一次性密码本的这个数量是多少。问题是:给定一个消息 m 和一个密文 c,有多少个一次性密码本密钥 k 能满足 E(k, m) = c,即 k ⊕ m = c?
通过简单的代数变换 k ⊕ m = c ⇒ k = m ⊕ c,我们可以发现,满足条件的密钥 k 有且仅有一个,即 k = m ⊕ c。这对于所有 (m, c) 对都成立。

因此,对于一次性密码本,|{ k in K : E(k, m) = c }| = 1 是一个常数。根据之前的推理,这直接证明了一次性密码本具有完美保密性。
这个简单的证明得出了一个强有力的结论:对于一次性密码本,不存在唯密文攻击。这与替换密码、维吉尼亚密码或转轮机等历史密码形成了鲜明对比,后者都可以被唯密文攻击攻破。
完美保密性的局限与结论
然而,故事并未结束。完美保密性并不意味着一次性密码本就是可以高枕无忧的“安全密码”。我们强调,它只保证了抵抗唯密文攻击的安全性。在实践中,还存在其他类型的攻击(如已知明文攻击、选择明文攻击等),而一次性密码本对这些攻击可能是脆弱的,我们将在后续课程中看到。
此外,一次性密码本的主要问题——密钥过长——在实践中是致命的。香农在证明了一次性密码本具有完美保密性后,还证明了另一个定理:任何具有完美保密性的密码,其密钥空间的大小必须至少等于其明文空间的大小。这意味着,密钥的长度至少要和消息的长度一样长。
由于一次性密码本的密钥长度恰好等于消息长度,它在这个意义上是“最优”的完美保密密码。这也意味着,完美保密性是一个要求非常高的概念,它导致了像一次性密码本这样密钥管理极其不便的密码方案。
因此,尽管完美保密性是一个有趣且重要的理论概念,并且一次性密码本的思想非常精妙,但它本身并不直接导向实用的密码系统。在下一讲中,我们将探讨如何借鉴一次性密码本的思路,构建出既安全又实用的现代密码系统。

本节课总结:
我们一起学习了密码的正式定义,引入了克劳德·香农提出的完美保密性这一核心安全概念。我们深入分析了一次性密码本的工作原理,并证明了它满足完美保密性,从而能够抵抗最强的唯密文攻击。同时,我们也认识到完美保密性要求密钥至少与消息等长,这使得一次性密码本在实践中难以使用,但它所蕴含的“异或”和“一次一密”思想为现代密码学奠定了基础。
007:流密码与伪随机数生成器 🔐
在本节课中,我们将要学习如何将一次性密码本(One-Time Pad)这一理论完美的密码方案,转化为一个更实用的加密方案,即流密码(Stream Cipher)。其核心思想是使用一个短密钥,通过伪随机数生成器(PRG)来生成一个看似随机的长密钥流,从而模拟一次性密码本的效果。


上一节我们介绍了一次性密码本及其完美保密性,但同时也指出了其密钥必须与消息等长的致命缺陷。本节中我们来看看如何通过伪随机数生成器来克服这一缺陷,构建一个实用的流密码。
密码学基础回顾

首先,让我们快速回顾一下密码学的基本概念。一个密码方案由三个集合和一个算法对定义:
- 密钥空间(K):所有可能密钥的集合。
- 明文空间(M):所有可能消息的集合。
- 密文空间(C):所有可能密文的集合。
密码方案包含一对高效的算法:
- 加密算法(E):
C = E(K, M) - 解密算法(D):
M = D(K, C)
它们必须满足的基本属性是:解密是加密的逆运算。即对于任意密钥 K 和明文 M,都有 D(K, E(K, M)) = M。
我们之前看到,像替换密码和维吉尼亚密码这样的弱密码都容易被攻破,绝对不应该在实际中使用。而一次性密码本则是我们遇到的第一个“好”密码。
一次性密码本与完美保密性


一次性密码本定义如下:
- 明文、密文和密钥空间都是所有
n比特字符串的集合:{0,1}^n。 - 加密:
C = M ⊕ K(⊕表示按位异或 XOR 操作)。 - 解密:
M = C ⊕ K。

一次性密码本具有完美保密性:攻击者仅凭单个密文,无法获得关于明文的任何信息。然而,香农证明了一个“坏消息”引理:任何具有完美保密性的密码,其密钥长度必须至少与消息长度相等。这使得一次性密码本在实际中难以应用,因为如果双方能安全交换与消息等长的密钥,他们或许可以直接交换消息本身。
流密码的核心思想
为了构建一个实用的方案,我们引入流密码。其核心思路是:不再使用一个完全随机的长密钥,而是使用一个短的种子(Seed)作为密钥,并通过一个伪随机数生成器(PRG) 将其扩展成一个长的、看似随机的比特序列(即密钥流)。
一个伪随机数生成器 G 是一个确定性函数:
G: {0,1}^s → {0,1}^n
其中 s 是种子长度,n 是输出长度,且 n >> s(n 远大于 s)。例如,它可以输入一个128比特的种子,输出一个千兆字节长的序列。该函数必须是高效可计算的。
以下是流密码的构建步骤:
- 密钥:一个短的随机种子
K(例如128比特)。 - 生成密钥流:使用 PRG 扩展密钥:
G(K)。 - 加密:像一次性密码本一样,将密钥流与明文进行异或:
C = M ⊕ G(K)。 - 解密:接收方使用相同的密钥
K生成相同的密钥流,并与密文异或即可恢复明文:M = C ⊕ G(K)。
流密码的安全性
一个自然的问题是:流密码安全吗?它显然不具备完美保密性,因为其密钥长度远小于消息长度。因此,我们需要一个新的、更实用的安全定义来评估流密码的安全性(这将在下一讲详细讨论)。流密码的安全性完全依赖于所使用的伪随机数生成器 G 的质量。
伪随机数生成器的核心属性:不可预测性
对于用于密码学的 PRG,一个最基本的安全属性是不可预测性。我们首先看看如果 PRG 是可预测的会有什么后果。

什么是可预测的 PRG?
如果一个 PRG 是可预测的,意味着存在一个高效算法 A 和某个位置 i,使得在给定输出序列的前 i 个比特 G(K)[1...i] 后,算法 A 能够以显著高于 1/2 的概率成功预测出第 i+1 个比特 G(K)[i+1]。
为什么可预测性是致命的?
假设攻击者知道(或能猜到)密文所对应明文的前缀(例如,在电子邮件中,开头通常是“From: ”)。攻击者可以进行以下操作:
- 截获密文
C。 - 将已知的明文前缀
M[1...i]与密文前缀C[1...i]异或,得到密钥流的前缀G(K)[1...i]。 - 利用 PRG 的可预测性,根据密钥流前缀
G(K)[1...i]计算出后续的整个密钥流G(K)[i+1...n]。 - 使用计算出的完整密钥流解密整个密文,从而获得全部明文。
因此,一个用于流密码的 PRG 必须是不可预测的。形式化地说,对于所有位置 i 和所有高效算法 A,给定 G(K)[1...i],算法 A 正确预测出 G(K)[i+1] 的概率与 1/2 的差值必须是一个可忽略的量。
切勿使用的弱伪随机数生成器
以下是两个绝对不应该用于密码学的 PRG 例子,它们都很容易被预测:
1. 线性同余生成器(Linear Congruential Generator)
其状态更新公式为:R[i] = (A * R[i-1] + B) mod P
其中 A, B, P 是参数,R[0] 是种子。每次迭代输出 R[i] 的若干低位。尽管它可能具有良好的统计特性,但因其线性结构,给定少量输出就足以预测整个序列。

2. Glibc 的 random() 函数
这是许多编程语言标准库中实现的随机数生成器,其本质与线性同余生成器类似。它不是密码学安全的随机数生成器。历史上,Kerberos 版本4曾因使用 random() 函数而遭受攻击。
重要教训:永远不要使用标准库中的非加密随机数函数(如 C 语言的 random(),Python 的 random 模块)来生成加密密钥或密钥流。
可忽略与不可忽略函数
在密码学的安全性定义中,我们经常用到“可忽略(Negligible)”和“不可忽略(Non-negligible)”这两个概念来量化攻击成功的概率。
- 直观理解(实践角度):
- 不可忽略:概率大于某个阈值(例如
1/2^30,约十亿分之一)。对于处理大量数据(如千兆字节)的加密系统,这个概率的事件很可能发生。 - 可忽略:概率小于某个极小的阈值(例如
1/2^80)。在密钥的生命周期内,这种事件几乎不可能发生。
- 不可忽略:概率大于某个阈值(例如

- 理论定义(更严谨):
我们将概率视为安全参数λ的函数f(λ)。- 可忽略函数:对于任意多项式
poly(λ)(如λ^d),当λ足够大时,f(λ) < 1/poly(λ)。也就是说,f(λ)的衰减速度比任何多项式的倒数都要快。典型的例子是指数衰减函数:f(λ) = 1/2^λ。 - 不可忽略函数:存在某个多项式
poly(λ),使得对于无穷多个λ,有f(λ) ≥ 1/poly(λ)。例如,f(λ) = 1/λ^1000虽然衰减很慢,但仍是不可忽略的。
- 可忽略函数:对于任意多项式
理论定义避免了实践中固定阈值可能带来的问题,为安全性证明提供了更坚实的基础。在本课程中,我们通常将“可忽略”理解为“小于任何多项式倒数”(特别是像指数衰减那样快),将“不可忽略”理解为“大于某个多项式倒数”。
总结
本节课中我们一起学习了如何从一次性密码本出发,构建一个更实用的流密码。我们了解到,流密码通过一个短密钥和伪随机数生成器来模拟长随机密钥的效果。其安全性的核心在于 PRG 必须是不可预测的。我们看到了两个弱 PRG 的例子,并强调了切勿在密码学中使用它们。最后,我们介绍了用于严格分析安全性的“可忽略”与“不可忽略”函数的概念。


下一讲,我们将引入一个新的安全定义——选择明文攻击下的不可区分性(IND-CPA),来形式化地论证基于一个“好”的 PRG(即下一讲将定义的伪随机生成器)构建的流密码是安全的。
008:对流密码和一次性密码本的攻击 🔐
在本节中,我们将探讨对一次性密码本的攻击,以及在使用流密码时需要注意的一些事项。

在开始之前,我们先快速回顾一下之前的内容。

回顾:一次性密码本与流密码
回想一下,一次性密码本通过将消息与一个秘密密钥进行异或运算来加密消息,其中秘密密钥的长度与消息相同。解密过程类似,将密文与相同的秘密密钥进行异或运算。当密钥是均匀随机时,我们证明了这种方案具有香农所说的完美保密性。
当然,一个问题是密钥长度与消息长度相同,这使得一次性密码本在实际使用中非常困难。

为了使一次性密码本变得实用,我们讨论了使用伪随机数生成器的方法,它将一个短种子扩展成一个长得多的消息。流密码的工作原理本质上与一次性密码本相同,但我们不是使用真正的随机密钥流,而是使用由输入到生成器的短密钥扩展而成的、与消息等长的伪随机密钥流。
公式: 密文 C = 消息 M ⊕ 伪随机密钥流 G(K)
我们提到,安全性不再依赖于完美保密性,因为流密码无法实现完美保密。相反,安全性依赖于伪随机数生成器的特性。我们说过,伪随机数生成器本质上需要是不可预测的。但实际上,这个定义有点难以处理,我们将在后续章节中看到一个更好的伪随机数生成器安全性定义。
攻击一:两次密码本攻击
在本节中,我们将讨论对一次性密码本的攻击。第一个要讨论的攻击叫做两次密码本攻击。
请记住,一次性密码本之所以叫“一次性”,是因为密钥流只能用于加密一条消息。我们将展示,如果相同的密钥流被用于加密多条消息,那么安全性将荡然无存,窃听者基本上可以完全解密加密的消息。
让我们看一个例子。假设有两条消息 M1 和 M2 使用相同的密钥流 K 进行加密。
公式:
C1 = M1 ⊕ K
C2 = M2 ⊕ K
假设窃听者截获了 C1 和 C2。他自然会计算 C1 和 C2 的异或值。当他计算这个异或值时,会得到什么?

公式: C1 ⊕ C2 = (M1 ⊕ K) ⊕ (M2 ⊕ K) = M1 ⊕ M2
可以看到,密钥流 K 被抵消了,剩下的就是两条明文消息的异或值。事实证明,英语(以及ASCII编码)具有足够的冗余度,以至于给定两条ASCII编码消息的异或值,你实际上可以完全恢复出这两条原始消息。
因此,这里的教训是:如果你曾经使用相同的密钥流加密多条消息,那么截获密文的攻击者基本上可以不费吹灰之力恢复原始明文。所以,流密码的密钥或一次性密码本的密钥绝对、永远、永远不要使用超过一次。
实践中的例子
在实践中,重复使用流密码密钥或一次性密码本密钥是一个非常常见的错误。让我们看一些例子,以便你在构建自己的系统时避免这些错误。
以下是几个历史案例:
- 历史案例:维农纳计划:在20世纪40年代初,俄罗斯人使用一次性密码本加密各种消息。不幸的是,他们使用的密钥流是由人掷骰子生成的。由于生成这些密钥流很费力,他们觉得只用一次很浪费,于是最终用同一个密钥流加密了多条消息。美国情报机构能够截获这些“两次密码本”密文,并在几年内解密了大约3000条明文。这个项目被称为“维农纳计划”,这是一个因两次密码本不安全而导致的密码分析的精彩故事。
- 网络协议案例:点对点隧道协议:在Windows的一个名为点对点隧道协议的协议中,客户端和服务器共享一个密钥K。问题在于,从客户端到服务器的所有消息被视为一个长流,并使用密钥K加密;同样,从服务器到客户端的消息也被视为一个长流,但不幸的是,也使用了相同的伪随机种子(即相同的流密码密钥)。这实际上导致了两次密码本攻击。这里的教训是:永远不要使用同一个密钥来加密双向流量。实际上,你需要一个密钥用于客户端到服务器的交互,另一个不同的密钥用于服务器到客户端的交互。
- Wi-Fi通信案例:WEP协议:原始的802.11b加密层称为WEP,它是一个设计非常糟糕的协议。在WEP中,客户端和接入点共享一个长期密钥K。为了加密一个帧(包含明文M),客户端首先计算一个校验和,然后使用流密码加密,其密钥是IV(一个24位字符串)与K的拼接。IV随每个数据包递增,并以明文形式与密文一起发送。这里的问题是IV只有24位长,这意味着在传输了1600万个帧后,IV必然会循环。一旦IV重复,相同的密钥(IV拼接K)就会被用来加密两个不同的帧,攻击者就可以恢复两个帧的明文。更糟糕的是,许多802.11卡在重启后IV会重置为零,导致每次重启后都会多次使用“零拼接K”这个密钥。此外,WEP使用的伪随机数生成器RC4,在面对这种紧密相关的密钥(所有密钥都有104位相同的后缀)时并不安全。攻击表明,监听大约4万个帧就足以恢复秘密密钥K。因此,WEP几乎不提供任何安全性。
那么,WEP的设计者应该怎么做呢?一种方法是将整个交互视为一个长流,使用伪随机数生成器生成一个长密钥流,然后分段用于加密每个帧。或者,如果他们坚持为每个帧使用不同的密钥,更好的方法是使用一个PRG:用长期密钥K通过PRG生成一个长的随机比特流,然后将其分段作为每个帧的加密密钥。这样,每个帧的密钥看起来都是随机的,彼此之间没有关联。
磁盘加密中的两次密码本
另一个例子出现在磁盘加密中。假设一个文件被分成块,每个块使用流密码加密后存储在磁盘上。如果用户后来编辑了文件(例如,将“致Bob”改为“致Eve”)并重新保存,那么只有被修改的块会发生变化。攻击者对比编辑前后的磁盘快照,即使不知道内容,也能精确定位到发生更改的位置。这泄露了攻击者本不应知道的信息。
这本质上是另一种两次密码本攻击,因为相同的密钥流被用来加密两个非常相似但不同的消息。攻击者可以了解到更改是什么,甚至可能推断出更改的具体内容。

因此,这里的教训是:通常不建议使用流密码进行磁盘加密。我们需要为磁盘加密做一些不同的事情,这将在后面的章节中讨论。
小结两次密码本攻击
总而言之,我希望我已经让你相信,绝对不要多次使用流密码密钥。即使在自然场景下可能发生,你也必须小心确保不会重复使用同一个密钥。

- 对于网络流量,通常每个会话应有自己的密钥。在会话内,从客户端到服务器的消息被视为一个完整的流,使用一个密钥加密;从服务器到客户端的消息被视为另一个流,使用另一个不同的密钥加密。
- 对于磁盘加密,通常不应使用流密码,因为对文件的修改会泄露文件内容的信息。
攻击二:可延展性与完整性缺失
接下来要提到的攻击是:一次性密码本和流密码根本不提供完整性。它们只在密钥只使用一次时试图提供保密性,但完全不提供完整性。更糟糕的是,实际上很容易修改密文,并对相应的明文产生已知的影响。这个特性被称为可延展性。
让我解释一下这是什么意思。假设我们有一条消息M,使用流密码加密得到密文 C = M ⊕ K。攻击者截获了密文C。虽然他无法得知明文是什么,但他可以成为一个主动攻击者并修改密文。假设他将密文与某个扰动值P进行异或。
公式: C' = C ⊕ P = (M ⊕ K) ⊕ P
那么,当我们解密这个修改后的密文C‘时,会得到什么?

公式: 解密(C') = C' ⊕ K = (M ⊕ K ⊕ P) ⊕ K = M ⊕ P
可以看到,通过与扰动值P进行异或,攻击者能够对解密后的明文产生非常具体的影响。总结一下:你可以修改密文,这些修改是无法被检测到的,更糟糕的是,它们对结果明文有非常具体的影响——你对密文异或了什么,就会对明文产生完全相同的效果。
一个危险示例
假设用户发送了一封以“From Bob”开头的电子邮件。攻击者截获了对应的密文。假设攻击者知道(或猜测)消息来自Bob,但他想修改密文,使明文看起来像是来自Eve。他需要做的就是在密文的相应位置异或一个特定的三字符序列。
具体来说,“Bob”的ASCII编码是 42 6F 62(十六进制),“Eve”的编码是 45 76 65。计算 Bob ⊕ Eve 得到 07 19 07。因此,攻击者只需将这三个字节 07 19 07 异或到密文“From Bob”中“Bob”对应的位置,解密后的明文就会变成“From Eve”。

这是一个例子,说明对密文产生可预测的影响实际上会导致相当多的问题。这种特性被称为可延展性。我们说一次性密码本是可延展的,因为很容易对密文进行计算,并对相应的明文做出规定的更改。
为了防止所有这些,我们将在后续课程中展示如何为加密机制添加完整性。但现在,我希望你记住,一次性密码本本身没有完整性,并且对于实际修改密文的攻击者来说完全不安全。
本节总结
在本节课中,我们一起学习了:
- 两次密码本攻击:重复使用流密码密钥是灾难性的,会导致明文完全泄露。我们通过历史案例(维农纳计划)、网络协议(PPTP、WEP)和磁盘加密的例子,看到了这种攻击在实践中的多种表现形式。核心教训是:永远不要重复使用流密码密钥,并为双向通信使用不同的密钥。
- 可延展性与完整性缺失:流密码和一次性密码本不提供任何完整性保护。攻击者可以修改密文,并精确控制其对解密明文的影响(例如,将“From Bob”改为“From Eve”)。这种可延展性是一个严重的安全缺陷。

记住,虽然流密码在提供保密性方面有其作用,但必须谨慎使用,避免密钥重用,并且通常需要与其他机制结合以提供完整性和认证。
009:现实世界中的流密码 🔐

在本节课中,我们将学习几种实际应用中使用的流密码。我们将从两个不应在新系统中使用但仍被广泛使用的旧例子开始,分析它们的弱点。接着,我们将介绍一个更现代的、来自 eSTREAM 项目的安全流密码示例,并了解其工作原理和性能优势。
旧式流密码示例 🕰️
上一节我们讨论了流密码的基本概念,本节中我们来看看两个历史上重要但存在安全缺陷的流密码:RC4 和 CSS。
RC4 流密码
RC4 设计于 1987 年,它接受一个可变长度的种子(例如 128 位)作为密钥。其工作原理是先将密钥扩展为 2048 字节的内部状态,然后通过一个简单的循环迭代,每次输出一个字节。
尽管 RC4 曾相当流行,例如在 HTTPS 协议和 WEP 中使用,但现已发现多个弱点,不推荐在新项目中使用。

以下是 RC4 的两个已知弱点:
- 初始字节偏差:RC4 输出的前几个字节(尤其是第二个字节)存在统计偏差。例如,第二个字节为 0 的概率是
2/256,而非理想的1/256。因此,建议在使用时丢弃前 256 个字节的输出。 - 长输出序列偏差:在生成了数 GB 的数据后,输出中连续两个零字节
(0,0)出现的频率略高于随机序列的预期值1/(256^2),这为攻击者提供了区分其输出与真随机序列的可能。
此外,RC4 还存在相关密钥攻击等弱点。
CSS(内容扰乱系统)流密码
CSS 是一种用于加密 DVD 电影的流密码,它基于硬件设计者偏爱的线性反馈移位寄存器(LFSR)。LFSR 是一个包含多个比特单元的寄存器,通过特定“抽头”位置的比特进行异或运算,来生成新的比特并移位。
CSS 使用两个 LFSR(一个 17 位,一个 25 位),其 40 位密钥被分割用于初始化这两个寄存器。两个 LFSR 每运行 8 个周期产生 8 比特输出,通过一个模 256 加法器合并,产生最终的密钥流字节。

然而,CSS 存在严重的安全漏洞,可以在大约 2^17 次操作内被破解。攻击原理如下:
- 由于 DVD 文件格式(MPEG)已知,攻击者可能知道密文前 20 字节对应的明文。
- 将密文与已知明文异或,即可得到 CSS 生成的前 20 字节密钥流。
- 遍历所有
2^17种可能的 17 位 LFSR 初始状态。对于每一种猜测:- 运行该 LFSR 生成 20 字节输出。
- 用已知的 20 字节密钥流减去(模 256)猜测的 LFSR 输出,得到候选的 25 位 LFSR 输出。
- 检查这个候选输出序列是否可能来自一个 25 位的 LFSR。如果不是,则当前猜测错误;如果是,则找到了正确的两个 LFSR 初始状态,从而可以解密整个电影。
目前已有许多开源工具利用此方法解密 CSS 加密的 DVD。
现代流密码示例 🚀
现在我们已经看到了两个脆弱的例子,让我们转向更好的示例。现代伪随机数生成器的一个重要来源是 eSTREAM 项目(于 2008 年结束),它评选出了多个安全的流密码。
这些现代流密码的参数与我们之前熟悉的略有不同。它们不仅有一个种子(密钥 K),还有一个称为 Nonce(一次性数值) 的输入 R。PRG 的输出长度远大于种子和 Nonce 的长度。

引入 Nonce 的关键在于:只要密钥 K 不变,Nonce R 必须永不重复。这样,每一对 (K, R) 都是唯一的。这使得我们可以在不同会话中安全地重用同一个密钥,而无需每次都更换新密钥,只要确保每次加密使用的 Nonce 不同即可。
Salsa20 流密码
我想展示的 eSTREAM 具体示例是 Salsa20。它设计用于软件和硬件实现,支持 128 位或 256 位密钥。这里以 128 位密钥版本为例,它还需要一个 64 位的 Nonce。
Salsa20 通过一个核心函数 H 来生成任意长度的密钥流。函数 H 接收三个输入:密钥 K、Nonce R 和一个从 0 开始递增的计数器 i。通过不断递增 i 并计算 H(K, R, i),就可以得到连续的密钥流块。
函数 H 的具体构造如下:
- 状态扩展:将 16 字节的密钥
K、8 字节的 NonceR和 8 字节的计数器i,与一些预定义的 4 字节常量T0, T1, T2, T3按特定顺序拼接,形成一个 64 字节的块。状态块 = T0 || K || T1 || R || i || T2 || K || T3 - 核心置换:对这个 64 字节的状态块应用一个名为
h的可逆函数(即给定输出能算出输入)。这个函数专为高效设计,在 x86 架构上能利用 SSE2 指令集高速运行。此置换过程重复进行 10 轮。 - 最终输出:将第 10 轮置换后的输出与最初的 64 字节状态块进行逐字(word)加法(而非异或),得到最终的 64 字节输出块,这就是函数
H的结果。

通过这种方式,Salsa20 能快速产生看似随机的密钥流。目前没有已知的重大攻击能有效威胁其安全,其安全性接近 2^128 的强度。
在性能方面,以一台 2.2 GHz 的 x86 机器为例,RC4 的加密速度较慢,而像 Salsa20 这样的 eSTREAM 决赛算法可以达到每秒数百兆字节的加密速度,足以满足高清视频流等高性能需求。
总结 📝
本节课中我们一起学习了现实世界中的流密码。
- 我们首先分析了两个旧式流密码 RC4 和 CSS,了解了它们的工作原理以及导致其不安全的偏差、相关密钥攻击和基于 LFSR 的快速破解方法。
- 接着,我们引入了现代流密码设计,重点介绍了 eSTREAM 项目 和其中的 Salsa20 算法。现代流密码通过引入 Nonce 实现了密钥的安全重用,其设计兼顾了软件和硬件效率。
- Salsa20 通过扩展状态、多轮可逆核心置换和最终加法的步骤,构建了一个快速且安全的伪随机数生成器。

因此,如果你需要在项目中使用流密码,应当选择像 Salsa20 这样的现代、经过严格评估的算法,并确保正确使用 Nonce 来保证安全性。
010:伪随机数生成器的安全定义


在本节课中,我们将学习伪随机数生成器的核心安全定义。我们将理解如何形式化地定义“一个生成器的输出看起来像随机数”,并探讨该定义的重要应用和含义。
统计测试的概念
为了理解如何定义“与随机数不可区分”,我们需要引入统计测试的概念。
统计测试是一个算法,它接收一个n比特的字符串作为输入,并输出0或1。我们将0解释为“输入看起来不随机”,将1解释为“输入看起来随机”。
以下是几个统计测试的例子:
- 测试1: 如果输入字符串中0的数量与1的数量之差小于
10 * sqrt(n),则输出1(认为随机),否则输出0。 - 测试2: 如果输入字符串中连续“00”模式的数量与
n/4的差小于10 * sqrt(n),则输出1,否则输出0。 - 测试3: 如果输入字符串中最长的连续0序列的长度小于
10 * log(n),则输出1,否则输出0。
统计测试可以执行任何它认为合适的检查,它们不一定是完美的。过去,人们通过一组固定的统计测试来评估生成器的好坏,但这对于密码学安全来说并不是一个好的定义。
优势:衡量统计测试的能力
为了评估一个统计测试的好坏,我们定义“优势”的概念。
对于一个输出n比特字符串的生成器G和一个统计测试A,其优势 Adv_PRG[A, G] 定义如下:
Adv_PRG[A, G] = | Pr[ A(G(k)) = 1 ] - Pr[ A(r) = 1 ] |
其中:
k是从密钥空间中均匀随机选取的。r是从{0,1}^n中均匀随机选取的。
这个优势值在区间 [0, 1] 内。
- 优势接近1: 意味着测试A在区分伪随机输出和真随机输出方面表现得非常不同,即它成功“攻破”了生成器G。
- 优势接近0: 意味着测试A在伪随机输入和真随机输入上的行为几乎相同,即它无法区分生成器G的输出。

让我们看两个例子:
- 无用的测试: 测试A总是输出0。那么
Pr[A(...)=1]总是0,因此优势为0。它无法区分任何东西。 - 有缺陷的生成器: 假设生成器G有三分之二的密钥会使输出字符串的最高有效位(第一个比特)为1。我们设计测试A:如果输入字符串的第一个比特是1,则输出1(认为随机);否则输出0。
- 对于伪随机输入
G(k),测试输出1的概率是2/3。 - 对于真随机输入
r,测试输出1的概率是1/2。 - 因此,优势
|2/3 - 1/2| = 1/6。这是一个不可忽略的优势,表明测试A成功区分了G的输出,因此G是不安全的。
- 对于伪随机输入
安全PRG的定义
现在我们可以定义什么是安全的伪随机数生成器。

我们说一个生成器G是安全的,如果所有高效的统计测试都无法以显著的优势区分其输出与真随机数。
形式化地说:对于所有高效的统计测试A,其优势 Adv_PRG[A, G] 都是可忽略的(即非常接近0)。
这是一个优雅而强大的定义:安全意味着生成器能通过所有可能的高效统计测试的检验,而不仅仅是一组预设的测试。
重要说明: 将测试限制为“高效的”是必要的。如果要求所有(包括计算能力无限的)测试都无法区分,那么这个定义将无法被满足(这是一个有趣的思考题)。此外,目前我们无法在标准计算复杂性假设(如P≠NP)之外,“证明”某个具体的生成器是安全的。但我们有许多经过深入分析、被认为是安全的启发式候选方案。
安全性与不可预测性的关系

上一节我们介绍了生成器的不可预测性。现在我们来探讨安全PRG定义的一个重要含义:一个安全的PRG必然是下一比特不可预测的。
我们将通过证明其逆否命题来展示这一点:如果一个生成器是可预测的,那么它必然是不安全的(即可区分的)。
证明思路如下:
- 假设存在一个高效的预测器算法A,给定生成器输出的前
i个比特,它能以1/2 + ε的概率(ε不可忽略)成功预测第i+1个比特。 - 我们可以利用这个预测器A来构造一个统计测试B:
- 输入:一个n比特字符串
x。 - 操作:运行预测器A,输入
x的前i个比特,得到它对第i+1个比特的预测值。 - 输出:如果A的预测值等于
x的实际第i+1个比特,则B输出1;否则输出0。
- 输入:一个n比特字符串
- 分析测试B的优势:
- 当输入是真随机字符串
r时,第i+1比特与前i比特独立,因此A只能随机猜测,B输出1的概率是1/2。 - 当输入是伪随机字符串
G(k)时,根据假设,A能以1/2 + ε的概率预测成功,因此B输出1的概率至少是1/2 + ε。
- 当输入是真随机字符串
- 因此,测试B的优势至少是
ε,这是一个不可忽略的值。所以B是一个能成功区分生成器G的统计测试,故G不安全。
由此,我们证明了:安全性 ⇒ 不可预测性。
姚期智定理:不可预测性意味着安全性
一个非常卓越的定理(姚期智,1982)指出,上述关系的逆命题也成立:如果一个生成器对所有位置都是下一比特不可预测的,那么它就是一个安全的PRG。
定理更精确的表述:如果对于所有位置 i,给定生成器输出的前 i 个比特,任何高效算法都无法以显著优势预测第 i+1 个比特,那么这个生成器就是安全的。
这个定理的证明虽然不在此详述,但其思想非常精妙。它意味着“下一比特预测器”在区分伪随机与真随机方面具有某种“普适性”:如果你无法构建任何下一比特预测器,那么你也无法构建任何类型的区分器。

定理的一个简单应用:
假设有一个生成器G,根据其输出的最后 n/2 个比特,可以轻松计算出其前 n/2 个比特。这是否意味着G是可预测的?
根据姚定理,我们可以推理:既然存在一个算法能从后半部分推出前半部分,那么很容易构造一个统计测试来区分G的输出(例如,检查这个关系是否成立)。因此G不是安全的。根据姚定理,G不安全意味着G一定是可预测的(即存在某个位置 i,可以从前 i 比特预测第 i+1 比特)。尽管我们可能无法直接指出这个预测器是什么,但定理保证了它的存在。
推广:计算不可区分性
最后,我们将“与均匀分布不可区分”的概念推广到更一般的“两个分布之间不可区分”。
设 P1 和 P2 是两个概率分布。如果对于所有高效的统计测试A,以下优势可忽略:
| Pr[ A(sample from P1) = 1 ] - Pr[ A(sample from P2) = 1 ] |
那么我们就称分布 P1 和 P2 是计算不可区分的,记作 P1 ≈_p P2。
使用这个简洁的记号,安全PRG的定义可以优雅地表示为:
{ G(k) | k ← K } ≈_p Uniform({0,1}^n)
即,由G生成的伪随机分布与均匀分布在计算上是不可区分的。
这个记号非常有用,在下一节定义加密方案的安全性时,我们将会用到它。
总结
本节课我们一起学习了伪随机数生成器的核心安全定义。
- 我们引入了统计测试和优势的概念,用于量化一个测试区分两个分布的能力。
- 我们给出了安全PRG的形式化定义:所有高效的统计测试都无法以显著优势区分其输出与真随机数。
- 我们探讨了安全性与不可预测性的等价关系(由姚期智定理证明):一个PRG是安全的,当且仅当它是下一比特不可预测的。
- 最后,我们推广了概念,引入了计算不可区分性的记号,为后续学习更复杂的密码学原语的安全性定义打下了基础。

理解这个定义是理解现代密码学如何形式化“安全”概念的基石。在接下来的课程中,我们将看到这个定义如何应用于构建安全的加密方案。
011:语义安全


在本节课中,我们将要学习流密码安全性的核心定义——语义安全。我们将探讨为什么简单的安全定义(如无法恢复密钥或明文)是不够的,并最终引入一个严谨且实用的安全定义。
定义流密码的安全性
上一节我们介绍了伪随机生成器(PRG)。本节中我们来看看如何定义流密码的安全性。每当定义安全性时,我们总是基于攻击者的能力和目标来考虑。对于流密码,它们仅与一次性密钥一起使用,因此攻击者最多只能看到一个用该密钥加密的密文。目前,我们只允许攻击者获得一个密文。

我们想定义密码安全意味着什么。首先想到的定义可能是:攻击者无法恢复密钥。但这一定义很糟糕。考虑以下“绝妙”的密码:加密算法 E(K, M) = M。给定密文(即明文本身),攻击者确实无法恢复密钥,但这个密码显然不安全。因此,仅以无法恢复密钥来定义安全是不行的。
接下来可以尝试定义:攻击者难以恢复整个明文。但这也不奏效。考虑以下加密方案:它处理两个消息 M0 和 M1 的拼接 M0 || M1。加密算法输出 M0(明文)拼接上 M1 的一次性密码本加密结果。攻击者获得一个密文后,由于 M1 是用一次性密码本加密的,他无法恢复 M1,因此无法恢复整个明文。但这个方案显然不安全,因为它泄露了一半的明文 M0。所以,即使无法恢复全部明文,能恢复大部分明文也是不安全的。
从完美保密到计算安全性
我们已知香农的解决方案:完美保密。香农的理念是,攻击者截获密文后,不应获得关于明文的任何信息,哪怕是一个比特。完美保密的定义是:对于任意两个等长的消息 M0 和 M1,用随机密钥加密 M0 产生的密文分布,与加密 M1 产生的密文分布完全相同。这意味着攻击者无法区分密文来自哪个消息。

然而,这个定义太强,要求密钥长度至少与消息一样长,这对于短密钥的流密码是无法实现的。因此,我们需要弱化这个定义。

借鉴上一节关于伪随机生成器的思路,我们可以不要求两个分布完全相同,而只要求它们在计算上不可区分。即,一个高效的攻击者无法区分来自这两个分布的样本。但这一定义仍然有点太强,无法被满足。我们需要增加一个约束:这个定义不需要对所有 M0 和 M1 都成立,只需要对攻击者能够实际给出的那对 M0 和 M1 成立即可。
语义安全的定义
这引导我们得出语义安全的定义(针对一次性密钥场景)。我们通过定义两个实验来阐述。
我们定义实验 Exp(b),其中 b 可以是 0 或 1。实验过程如下:
- 挑战者随机选择一个密钥
K。 - 攻击者
A输出两个等长的消息M0和M1。 - 挑战者计算密文
C ← E(K, Mb)并将其发送给攻击者A。 - 攻击者
A输出一个猜测比特b'。
定义事件 W_b 为在实验 Exp(b) 中,攻击者输出 1(即 b' = 1)的事件。攻击者 A 对抗加密方案 E 的语义安全优势定义为:
Adv_SS[A, E] = | Pr[W_0] - Pr[W_1] |
这个优势值介于 0 和 1 之间。如果优势值接近 0,意味着攻击者无法区分实验 0 和实验 1(即无法区分 M0 和 M1 的加密)。如果优势值接近 1,则意味着攻击者能很好地区分。

最终定义:一个对称加密方案是语义安全的,如果对于所有“高效的”攻击者 A,其优势 Adv_SS[A, E] 都是可忽略的。

语义安全意味着什么
这个定义非常优雅。让我们通过一些例子来理解其含义。
例子1:泄露最低有效位的方案
假设存在一个不安全的加密方案,攻击者 A 总能从密文中推导出明文的最低有效位(LSB)。我们将展示这个方案不是语义安全的。
我们构造一个语义安全攻击者 B 来利用 A:
B输出两个消息M0和M1,使得LSB(M0) = 0,LSB(M1) = 1。- 收到挑战密文
C后,B将其转发给A。 A分析C并输出其推断的明文 LSB 值b‘。B直接输出b‘作为自己的猜测。
分析优势:
- 在实验0(加密
M0)中,A总是输出 0,因此B输出 1 的概率Pr[W_0] = 0。 - 在实验1(加密
M1)中,A总是输出 1,因此B输出 1 的概率Pr[W_1] = 1。 - 优势
Adv = |0 - 1| = 1。
这表明攻击者 B 完全打破了系统的语义安全。这个论证不仅适用于 LSB,也适用于明文的任何谓词(如最高有效位、所有比特的异或值等)。因此,如果密码是语义安全的,那么对于高效攻击者而言,明文的任何单比特信息都不会被泄露。这实质上是“完美保密”概念在计算能力受限攻击者下的版本。

例子2:一次性密码本
一次性密码本(OTP)是语义安全的吗?是的,事实上它满足更强的完美保密。原因在于,对于任何 M0 和 M1,密文 C = K ⊕ Mb(K 是均匀随机密钥)的分布都是均匀随机分布,与 Mb 无关。因此,在两个实验中,攻击者收到的输入分布完全相同,其行为不会有差异,优势 Adv = 0。一次性密码本甚至对计算能力无限制的攻击者也是语义安全的。
总结
本节课中我们一起学习了语义安全,这是定义流密码安全性的核心概念。我们了解到:
- 简单的安全定义(如无法恢复密钥或完整明文)存在缺陷。
- 香农的完美保密定义太强,不适用于短密钥密码。
- 语义安全通过一个交互实验来定义,要求没有高效攻击者能够区分任意一对自选明文的加密结果。
- 语义安全意味着密文不会泄露明文任何比特的信息给高效攻击者。
- 一次性密码本满足语义安全(甚至是完美保密)。

下一节,我们将证明:如果使用一个安全的伪随机生成器(PRG),那么构造出的流密码也是语义安全的。
012:流密码的语义安全性证明 🛡️

在本节课中,我们将学习如何证明一个基于安全伪随机数生成器(PRG)的流密码是语义安全的。我们将通过构建一个严谨的证明来展示这一点,这是密码学中一个重要的概念验证。
概述
上一节我们介绍了安全PRG和语义安全的概念。本节中,我们将看到如何利用安全PRG来证明流密码的语义安全性。这是一个直接的证明过程,我们将逐步解析。
定理陈述
给定一个安全的伪随机数生成器 G,由该生成器派生的流密码是语义安全的。需要强调的是,我们无法针对香农的“完美保密”概念证明类似的定理,因为流密码的密钥长度小于消息长度,而完美保密要求密钥长度至少等于消息长度。因此,这个证明是语义安全概念实用性的一个重要例证。
证明思路
我们将通过证明其逆否命题来完成证明。具体思路如下:

假设存在一个针对流密码的语义安全攻击者 A。我们将基于 A 构建一个针对PRG G 的攻击者 B,并满足以下不等式关系:
Advantage(A) ≤ 2 * Advantage(B)
由于 G 是安全的,任何高效攻击者 B 的优势都是可忽略的。因此,A 的优势也必须是可忽略的,从而证明流密码是语义安全的。
证明过程
第一步:定义攻击者A的游戏
首先,我们有一个针对流密码 E 的语义安全攻击者 A。在标准语义安全游戏中:
- 挑战者随机选择一个密钥 K。
- 攻击者 A 输出两个等长的消息 M₀ 和 M₁。
- 挑战者随机选择 b ∈ {0, 1},并发送密文 C = M_b ⊕ G(K) 给 A。
- A 输出一个猜测 b'。
第二步:引入“游戏”概念
为了证明,我们将玩两个“游戏”:
- Game 0 (真实游戏):使用伪随机流 G(K) 进行加密。
- Game 1 (理想游戏):使用真正的随机流 R 进行加密(即一次性密码本)。
我们定义四个事件:
- W_b:在 Game 0 中,当密文由 M_b 加密时,攻击者 A 输出 1 的事件。
- R_b:在 Game 1 中,当密文由 M_b 加密时,攻击者 A 输出 1 的事件。
第三步:分析关系

以下是事件之间的关键关系:
-
一次性密码本的安全性:在 Game 1(一次性密码本)中,攻击者无法获得任何信息。因此:
Pr[R₀] = Pr[R₁]
这意味着攻击者在 Game 1 中的优势为 0。 -
PRG的安全性:由于 G 是安全的伪随机生成器,其输出与真随机数不可区分。因此,攻击者 A 无法区分 Game 0 和 Game 1。这意味着 W_b 的概率必须非常接近 R_b 的概率。具体来说,存在一个针对 G 的攻击者 B,使得:
|Pr[W_b] - Pr[R_b]| = Advantage(B)
其中 Advantage(B) 是可忽略的。
第四步:构建攻击者B
现在,我们来具体构建攻击者 B,它是一个针对PRG G 的统计测试。
以下是攻击者 B 的算法描述:
def adversary_B(input_string y):
# 1. 运行语义安全攻击者A,让它输出两个消息 M0 和 M1
M0, M1 = A.get_messages()
# 2. 使用输入字符串 y 作为“密钥流”,加密 M0
ciphertext = M0 XOR y
# 3. 将密文交给攻击者A,并获取它的输出比特 b_prime
b_prime = A.guess(ciphertext)
# 4. 输出A的猜测结果
return b_prime

B 的优势定义如下:
Advantage(B) = | Pr[B outputs 1 | y is truly random R] - Pr[B outputs 1 | y = G(K)] |
分析这个优势:
- 当 y = R(真随机)时,B 给 A 的正是 Game 1 中的场景(用 R 加密 M₀)。因此,
Pr[B outputs 1] = Pr[R₀]。 - 当 y = G(K)(伪随机)时,B 给 A 的正是 Game 0 中的场景(用 G(K) 加密 M₀)。因此,
Pr[B outputs 1] = Pr[W₀]。
因此,Advantage(B) = |Pr[R₀] - Pr[W₀]|。同理,我们可以构建另一个 B' 来证明 |Pr[R₁] - Pr[W₁]| 也是可忽略的。
第五步:完成证明
综合以上分析:
Pr[R₀] = Pr[R₁](一次性密码本安全性)|Pr[W₀] - Pr[R₀]|是可忽略的 (PRG安全性,通过 B)|Pr[W₁] - Pr[R₁]|是可忽略的 (PRG安全性,通过 B')
根据三角不等式,攻击者 A 在原始流密码游戏(Game 0)中的优势为:
|Pr[W₀] - Pr[W₁]| ≤ |Pr[W₀] - Pr[R₀]| + |Pr[R₁] - Pr[W₁]| ≤ 2 * Advantage(B)
由于 Advantage(B) 是可忽略的,因此 A 的优势也是可忽略的。证毕。
总结


本节课中,我们一起学习了如何严谨地证明:一个基于安全伪随机数生成器(PRG)的流密码是语义安全的。证明的核心思想是通过构建一个“混合论证”,将流密码的安全性问题归约到其底层PRG的安全性问题。我们首先假设存在一个能破解流密码的攻击者,然后利用它来构造一个能区分PRG输出与真随机数的攻击者。由于安全PRG能抵抗这种区分攻击,因此最初的假设不成立,从而证明了流密码的安全性。这个证明是密码学归约论证的一个经典范例。
013:什么是分组密码 🔐

在本节课中,我们将要学习密码学中的一个核心概念——分组密码。我们将了解它的定义、工作原理、安全目标,以及它与之前学过的流密码的区别。
概述
上一节我们介绍了流密码,本节中我们将转向一个功能更强大的密码学原语——分组密码。分组密码是现代密码学的基石,被广泛应用于数据加密、消息认证码等多种安全协议中。

分组密码的定义
分组密码由两个算法构成:加密算法 E 和 解密算法 D。这两个算法都接收一个密钥 K 作为输入。
分组密码的核心特点是:它接收一个 n 比特的明文作为输入,并输出一个长度完全相同的 n 比特的密文。其功能是将 n 比特的输入映射到 n 比特的输出。

用公式表示,对于一个分组密码,其加密和解密过程如下:
- 加密:
C = E(K, P),其中P是 n 比特明文,C是 n 比特密文。 - 解密:
P = D(K, C),其中D是E的逆运算。
经典分组密码示例
以下是两个经典的分组密码实例及其参数:
-
三重DES (3DES):
- 分组大小:64 比特。
- 密钥长度:168 比特。
- 它将 64 比特的输入块映射为 64 比特的输出块。
-
高级加密标准 (AES):
- 分组大小:128 比特。
- 密钥长度:可以是 128、192 或 256 比特。
- 它将 128 比特的输入块映射为 128 比特的输出块。
- 通常,密钥越长,密码越安全,但加密速度也越慢。
分组密码的工作原理:迭代结构

分组密码通常采用迭代结构构建。其工作流程如下:
- 密钥扩展:首先,输入的密钥 K 被扩展成一系列称为 轮密钥 的密钥:
K1, K2, ..., Kn。 - 轮函数迭代:然后,明文通过一个称为 轮函数 R 的组件进行多轮迭代加密。
- 轮函数接收两个输入:当前轮的轮密钥
Ki和当前轮的消息状态(初始状态为明文)。 - 每一轮,轮函数使用对应的轮密钥对当前状态进行变换,产生新的状态。
- 这个过程重复进行,直到完成所有轮次。最终的状态即为密文。
- 轮函数接收两个输入:当前轮的轮密钥
用伪代码描述这个过程:
state = plaintext_block
for i from 1 to number_of_rounds:
state = R(round_key[i], state)
ciphertext_block = state
不同的分组密码有不同的轮数和不同的轮函数设计:
- 对于 三重DES,轮数为 48。
- 对于 AES-128,轮数为 10。

性能对比
与流密码相比,分组密码的计算开销通常更大。以下是性能对比(数值为近似值,单位:周期/字节):
- 流密码:RC4 (~7), Salsa20/12 (~4), Sosemanuk (~4)
- 分组密码:3DES (~72), AES (~10)
可以看到,分组密码的速度普遍慢于流密码。然而,分组密码能实现许多流密码难以高效完成的功能,我们将在后续课程中看到。

抽象概念:伪随机函数与伪随机置换
为了更清晰地讨论如何正确使用分组密码,我们引入两个重要的抽象概念。
伪随机函数
一个 伪随机函数 定义在密钥空间 K、输入空间 X 和输出空间 Y 上。它是一个函数 F: K × X → Y。
- 核心要求:给定密钥
k ∈ K和输入x ∈ X,存在一个高效算法可以计算出F(k, x)。 - 不要求 函数可逆。
伪随机置换
一个 伪随机置换 更准确地描述了分组密码。它定义在密钥空间 K 和集合 X 上。它是一个函数 E: K × X → X,并且满足:
- 高效计算:存在高效算法计算
E(k, x)。 - 一一映射:对于固定的密钥
k,函数E(k, ·)是从 X 到 X 的一一映射(双射)。 - 高效可逆:存在高效的反函数(解密算法)
D,使得D(k, E(k, x)) = x。
关系:一个伪随机置换可以看作是一个特殊的伪随机函数,其中输入空间和输出空间相同(X = Y),并且该函数在已知密钥时可高效求逆。
安全性定义
分组密码(伪随机置换)的安全目标是什么?其核心思想是:一个安全的伪随机置换应该与一个真正的随机置换在计算上不可区分。

安全性游戏
设想一个挑战者与一个攻击者进行如下游戏:
- 挑战者秘密地抛一枚硬币。
- 如果硬币正面朝上,挑战者选择一个 真正的随机置换
π(从所有可能的X → X置换中均匀随机选取)。 - 如果硬币反面朝上,挑战者选择一个 随机的密钥
k,并使用伪随机置换E(k, ·)。
- 如果硬币正面朝上,挑战者选择一个 真正的随机置换
- 攻击者可以自适应地向挑战者提交查询:输入值
x ∈ X。 - 对于每次查询,挑战者返回对应的输出值
π(x)或E(k, x)。 - 最后,攻击者需要猜测挑战者使用的是真正的随机置换还是伪随机置换。
定义:如果对于任何高效的计算攻击者,其猜对硬币结果的概率与纯粹随机猜测(即 1/2)的差距可以忽略不计,那么我们就称这个伪随机置换是 安全的。
伪随机函数的安全性定义与此类似,只是将“随机置换”替换为“随机函数”。

一个反例
假设我们有一个安全的伪随机函数 F,我们构造一个新函数 G:
定义 G(k, x):
如果 x == 0:
返回 0
否则:
返回 F(k, x)
G 是安全的伪随机函数吗?不是。攻击者只需查询 x = 0 即可区分:
- 对于真正的随机函数,输出为 0 的概率极低(
1/|Y|)。 - 对于
G,输出总是 0。
因此,即使只有一个点的输出行为异常,也足以破坏整个构造的安全性。
分组密码的应用示例:构建伪随机数生成器
分组密码(伪随机函数)功能强大。以下是一个简单的应用:用伪随机函数构建一个 伪随机数生成器。
构造(计数器模式):
- 种子:伪随机函数的密钥
k。 - 输出:
F(k, 0) || F(k, 1) || F(k, 2) || ... || F(k, t-1)。 - 这将密钥
k扩展成了n * t比特的输出。

安全性直觉:
- 根据伪随机函数的定义,
F(k, ·)与一个真正的随机函数f(·)不可区分。 - 如果我们用真正的随机函数
f来构建生成器:f(0) || f(1) || ... || f(t-1),那么输出显然是真正随机的(因为每个f(i)都是独立随机的)。 - 因此,用伪随机函数
F构建的生成器,其输出也与随机串不可区分。
优势:此生成器是 可并行化 的。因为计算 F(k, i) 彼此独立,如果有多个处理器,可以同时计算不同 i 的输出,从而大幅提升速度。这与我们之前看到的 RC4 等流密码(本质上是顺序的)形成对比。
总结
本节课中我们一起学习了分组密码的核心知识:
- 定义:分组密码是对固定长度分组进行加密/解密的对称密码,由算法
(E, D)和密钥K定义。 - 实例:我们了解了 3DES 和 AES 这两个经典分组密码的参数。
- 结构:分组密码通常采用迭代结构,通过多轮应用轮函数和轮密钥来达到混淆和扩散的效果。
- 抽象:我们引入了 伪随机函数 和 伪随机置换 这两个关键抽象,后者更精确地刻画了分组密码。
- 安全目标:安全的分组密码(伪随机置换)应与真正的随机置换在计算上不可区分。
- 应用:我们看到了如何利用伪随机函数构建一个可并行化的伪随机数生成器,这体现了其强大功能。
在接下来的章节中,我们将深入探讨 3DES 和 AES 的具体构造细节。
014:数据加密标准 🔐


在本节课中,我们将学习一个经典的块密码实例——数据加密标准。我们将了解DES的历史背景、其核心构造原理(Feistel网络),并详细解析其内部结构,特别是S盒的作用。

历史背景 📜
上一节我们介绍了块密码的基本概念,本节中我们来看看一个具体的例子——数据加密标准。
DES的历史始于20世纪70年代初。IBM意识到其客户需要加密技术,因此成立了一个密码学小组,由Horst Feistel领导。Feistel在70年代初设计了一个名为Lucifer的密码。有趣的是,Lucifer的一个后期变体拥有128位的密钥长度和128位的块长度。
1973年,美国政府意识到其采购了大量商用计算机,因此希望供应商能提供一个良好的加密算法,用于销售给政府的产品。同年,国家标准局(当时名称)发布了一份提案请求,旨在制定一个将成为联邦标准的块密码。IBM提交了Lucifer的一个变体,该变体在标准化过程中经过了一些修改。

最终在1976年,国家标准局采纳了DES作为联邦标准。值得注意的是,DES的密钥长度从Lucifer的128位大幅缩减至56位,块长度也缩减至64位。缩减密钥长度的决定,尤其是缩减至56位,成为了DES的阿喀琉斯之踵,在其生命周期内招致了许多批评。事实上,早在1997年,DES就通过穷举搜索被攻破,这意味着一台机器能够遍历所有2^56个可能的密钥来恢复一个特定的挑战密钥。
1997年的这次实验宣告了DES的终结,意味着它本身不再安全。因此,美国国家标准与技术研究院(NIST)发布了下一代块密码标准的提案请求,并于2000年标准化了一个名为Rijndael的密码,即高级加密标准(AES)。我们将在后续章节讨论AES。
DES作为一个密码算法,取得了惊人的成功。它被广泛应用于银行业,例如一个名为电子清算中心的经典网络,银行用它来相互清算支票,DES用于这些交易的完整性保护。它也用于商业领域,直到最近,它还是Web的主要加密机制(当然,现在已被AES和其他密码取代)。总体而言,DES在部署方面非常成功。DES也拥有丰富的攻击历史,我们将在下一节讨论。
Feistel网络结构 🏗️
现在我们来谈谈DES的构造。DES背后的核心思想是由Horst Feistel提出的Feistel网络。这是一个非常巧妙的想法,用于从任意函数F1到Fd构建块密码。

想象我们有一组从n位映射到n位的任意函数F1到Fd。这些函数可以是任意的,不必是可逆的。我们的目标是利用这d个函数构建一个可逆的函数。我们将构建一个称为大写F的新函数,它将2n位映射到2n位。
以下是其构造描述:
输入是2n位,分为左右两部分,记为L0和R0。
对于第i轮(i从1到d):
- Li = R(i-1)
- Ri = L(i-1) XOR F_i( R(i-1) )
最终输出是Rd和Ld。
用公式表示即:
Li = R(i-1)
Ri = L(i-1) XOR F_i( R(i-1) ), 其中 i = 1 ... d
一个惊人的结论是:无论你给我什么函数F1到Fd,由此构建的Feistel网络函数都是可逆的,并且是高效可逆的。我们可以通过构造其逆来证明这一点。
观察单轮Feistel网络,其输入是R(i-1), L(i-1),输出是Ri, Li。现在,假设我们已知输出Ri, Li,想要计算输入R(i-1), L(i-1)。我们可以如下计算:
- R(i-1) = Li
- L(i-1) = Ri XOR F_i( R(i-1) ) = Ri XOR F_i( Li )

如果我们画出逆运算的图,会发现它与正向的Feistel轮函数非常相似,基本上是镜像关系。当我们将这些逆轮组合起来时,就得到了整个Feistel网络的逆。有趣的是,逆电路看起来与加密电路几乎相同,唯一的区别是函数以相反的顺序应用:解密时从Fd开始到F1结束,而加密时从F1开始到Fd结束。
对于硬件设计者来说,这非常有吸引力,因为如果你想节省硬件,加密硬件与解密硬件是相同的。你只需要实现一种算法,通过以相反顺序应用函数就能获得两种算法。

Feistel机制是一种从任意函数F1到Fd构建可逆函数的通用方法,事实上,它被用于许多不同的块密码中。不过有趣的是,AES并没有使用Feistel网络。AES使用了一种完全不同的结构,我们将在后面看到。
Feistel网络的安全性理论 🛡️
既然我们知道了什么是Feistel网络,让我提一个关于Feistel网络理论的重要定理,它说明了为什么这是一个好主意。这个定理由Luby和Rackoff在1985年提出。
定理内容如下:假设我有一个安全的伪随机函数。它作用于n位,使用密钥K。那么,如果你在三轮Feistel网络中使用这个函数,最终得到的是一个安全的伪随机置换。换句话说,你最终得到一个可逆函数,其与真正的随机可逆函数不可区分。

我希望你记得,安全块密码的定义就是它需要是一个安全的伪随机置换。所以这个定理说的是:如果你从一个安全的伪随机函数开始,你最终会得到一个安全的块密码。
让我更详细地解释一下这里实际发生了什么。本质上,PRF被用于Feistel网络的每一轮。换句话说,这里计算的是使用一个秘密密钥K0的PRF,这里计算的是使用另一个秘密密钥K1的PRF(当然,应用于R1),这里我们还有另一个秘密密钥K2应用于R2。请注意,这个F构造使用了K^3中的密钥,即使用了三个独立的密钥,所以密钥实际上是独立的这一点非常重要。然后我们最终得到一个安全的伪随机置换。
这就是Feistel网络背后的理论。现在我们理解了这一点,就可以具体看看DES了。

DES的具体构造 ⚙️
DES基本上是一个16轮的Feistel网络。所以有函数F1到F16,它们将32位映射到32位。因此,DES本身作用于64位块(2 * 32)。
DES的16个轮函数实际上都派生自同一个函数F,只是使用了不同的密钥。实际上,这些是不同的轮密钥,所以Ki是一个轮密钥,它基本上是从56位的DES主密钥K派生出来的。
现在,让我描述一下这个函数F。基本上,它接受一个32位的输入(称为X,实际上是R0, R1, R2等)和一个48位的轮密钥Ki。
以下是函数F的工作步骤:
- 扩展盒:首先,32位输入通过一个扩展盒,被扩展成48位。扩展盒所做的只是复制一些比特并移动其他比特的位置。例如,输入X的第1位被复制到输出的第2位和第48位。
- 异或运算:接下来,这48位与轮密钥Ki进行异或运算。
- S盒替换:然后,这48位被分成8组,每组6位。每组6位输入进入一个称为S盒的组件。S盒将6位映射到4位。因此,8个S盒的输出是8组4位,总共32位。
- 置换:最后,这32位经过另一个置换,只是将比特重新排列。例如,第1位可能去到第9位,第2位去到第15位,等等。这就是函数F的最终32位输出。
通过使用不同的轮密钥,我们本质上得到了不同的轮函数,从而形成了DES的16个轮函数。
现在,唯一需要具体说明的就是这些S盒。S盒实际上只是从6位到4位的函数,它们通过查找表实现。描述一个从6位到4位的函数基本上就是写出该函数在所有2^6=64种可能输入下的输出。例如,第五个S盒就是一个包含64个值(每个值4位)的表格。要查找输入011011对应的输出,你可以查看前两位01和中间四位1011,在表格交叉处找到输出1001。


S盒的设计与重要性 🔑
S盒是如何被设计者选择的呢?为了给你一些直觉,让我们从一个非常糟糕的S盒选择开始。

想象一下S盒是线性的。我的意思是,想象这6位输入只是以不同的方式相互异或,以产生4位输出。另一种写法是,我们可以将S盒写成一个矩阵向量乘积。如果S盒以这种方式实现,那么它们所做的只是将矩阵A应用于输入向量X,这就是为什么我们说在这种情况下S盒是完全线性的。
我断言,如果S盒是线性的,那么DES将完全不安全。原因是,如果S盒是线性的,那么DES所做的就只是计算各种东西的异或以及置换和打乱比特。所以它只是异或和比特置换,这意味着整个DES只是一个线性函数。
换句话说,会存在一个矩阵B。基本上,我会将64位消息加上16个轮密钥写成一个长向量。然后就是这个矩阵B,当你计算这些矩阵向量乘积时,你基本上就得到了密文的不同比特。所以如果DES是线性的,那么它作为一个安全的伪随机置换将是糟糕的。
让我举一个非常简单的例子。如果你对DES的三个输出取异或,你最终会得到DES在点M1 XOR M2 XOR M3处的值。这个关系对于一个随机函数来说是不会成立的,因此你得到了一个非常简单的测试来告诉你DES不是一个随机函数。事实上,给定足够的输入输出对,你甚至可以恢复整个秘密密钥。你只需要832个输入输出对就能恢复整个密钥。
因此,如果S盒是线性的,这将完全不安全。事实上,即使S盒接近线性,也就是说对于64个输入中的60个,S盒是线性的,这也足以攻破DES。特别是,如果你随机选择S盒,它们往往会有些接近线性函数,结果你将能够轻易恢复密钥。
因此,DES的设计者实际上指定了一些他们用于选择S盒的规则。毫不奇怪,第一条规则是这些函数远离线性函数。换句话说,不存在一个函数能与S盒的大部分输出一致。然后还有其他规则,例如,它们正好是四对一映射,所以每个输出正好有四个原像,等等。我们现在理解了为什么他们以这种方式选择S盒,事实上,这都是为了抵御对DES的某些攻击。

总结 📝

本节课中我们一起学习了数据加密标准。我们回顾了DES的历史,了解了其核心构造——Feistel网络的工作原理,它能够从任意函数构建可逆的加密函数。我们详细剖析了DES的具体实现,包括其16轮结构、轮函数F的步骤(扩展、异或、S盒替换、置换),并重点探讨了S盒作为DES中唯一非线性组件的重要性及其设计原则。DES虽然因其56位密钥长度已被攻破而淘汰,但其Feistel网络结构和设计思想对密码学发展产生了深远影响。在接下来的章节中,我们将探讨DES的安全性。
015:穷举搜索攻击 🔍

在本节课中,我们将要学习针对DES(数据加密标准)的穷举搜索攻击。我们将从理解攻击的基本原理开始,分析DES密钥的唯一性,然后探讨如何通过穷举搜索找到密钥。接着,我们会了解历史上著名的DES挑战赛及其结果,并讨论如何通过三重DES等方法来增强DES的安全性。最后,我们会分析双重DES的弱点以及一种名为DESX的替代方案。

密钥的唯一性分析
上一节我们介绍了DES的工作原理,本节中我们来看看针对DES的几种攻击,我们将从一种名为“穷举搜索”的攻击开始。
我们的目标是:给定几组输入输出对 (M_i, C_i),找到将明文 M_i 映射为密文 C_i 的密钥 k。换句话说,我们的目标是找到满足 C_i = DES(k, M_i) 的密钥。
第一个问题是:我们如何知道这个密钥是唯一的?让我们通过一个简单的分析来证明,实际上,仅需一对明文-密文就足以完全确定一个DES密钥。
我们假设DES是一个“理想密码”。这意味着我们将DES视为由随机可逆函数组成的集合。具体来说,对于每一个56位的密钥,DES实现了一个从64位到64位的随机可逆函数。虽然DES并非真正的随机函数集合,但我们可以将其理想化。
在这种情况下,可以证明:仅给定一对明文 M 和密文 C,存在唯一一个密钥 k 将 M 映射到 C 的概率约为99.5%。
以下是证明概要:
- 我们关心的是,是否存在另一个密钥
k' ≠ k,同样满足C = DES(k', M)。 - 根据概率的并集界限,这个概率可以上界为对所有
2^56个可能密钥k'求和:Pr[DES(k', M) = C]。 - 对于一个固定的
k',DES(k', M)是一个64位的随机输出。它恰好等于特定值C的概率最多是1 / 2^64。 - 因此,总概率上界为
(2^56) * (1 / 2^64) = 1 / 2^8 = 1/256。 - 所以,密钥不唯一的概率是
1/256,唯一的概率就是1 - 1/256 ≈ 99.5%。
实际上,如果给定两对明文-密文对 (M1, C1) 和 (M2, C2),存在唯一密钥的概率将极其接近100%。因此,我们可以明确地提出穷举搜索问题:给定两到三对明文-密文对,请找到那个唯一的密钥。

DES挑战赛与穷举搜索实践
那么如何找到这个密钥呢?最直接的方法就是尝试所有可能的密钥,直到找到正确的那一个,这就是穷举搜索。
历史上著名的DES挑战赛生动地展示了这一点。RSA公司发布了一系列密文,其中三组密文对应已知的明文(例如“The unknown message is:”)。挑战者需要利用这三对已知的明文-密文,通过穷举搜索找到密钥,并用它来解密其余密文。
以下是DES挑战赛的时间线,它清晰地表明了56位密钥的DES已不再安全:
- 1997年:通过互联网分布式计算(如DESCHALL项目),在大约3个月内搜索了约一半的密钥空间(
2^55次尝试),找到了密钥。 - 1998年:电子前沿基金会(EFF)建造了名为Deep Crack的专用硬件,耗资约25万美元,在3天内破解了DES。
- 1999年:结合Deep Crack和分布式计算,在22小时内破解了DES。
- 后续发展:随着硬件技术进步,使用现成的FPGA(现场可编程门阵列)仅花费1万美元,就能在大约7天内完成穷举搜索。
结论非常明确:56位的密码已彻底失效。 DES无法抵御穷举搜索攻击。

增强DES:三重DES (3DES)
由于DES被广泛部署,人们希望增强其安全性。最直接的想法是增加密钥长度以抵抗穷举搜索。三重DES 应运而生。
三重DES是一个通用构造。给定一个分组密码 E: K × M -> M,三重DES构造 3E: K^3 × M -> M 定义如下:
def 3E_encrypt((k1, k2, k3), m):
# 加密-解密-加密 (E-D-E) 模式
c_temp = E(k3, m) # 第一次加密
c_temp = D(k2, c_temp) # 解密
c = E(k1, c_temp) # 第二次加密
return c
这里使用 E-D-E 模式(加密-解密-再加密)而非 E-E-E,主要是为了兼容性:当三个密钥都相同时(k1 = k2 = k3),中间的 D 和第一个 E 相互抵消,整个操作退化为单次DES加密。这使得支持3DES的硬件也能兼容单DES。
三重DES的有效密钥长度为 3 * 56 = 168 位,使得穷举搜索需要 2^168 次操作,这在计算上是不可行的。虽然存在一种“中间相遇攻击”将其安全性降至约 2^118,但这仍然是足够安全的。然而,三重DES的主要缺点是速度慢,因为它需要执行三次加密/解密操作。

为什么不使用双重DES?
既然三重DES速度慢,一个自然的想法是使用双重DES,它只使用两个密钥和两次加密:
def 2E_encrypt((k1, k2), m):
c_temp = E(k2, m)
c = E(k1, c_temp)
return c
它的密钥长度为 2 * 56 = 112 位,并且只比单DES慢一倍。然而,双重DES存在严重的安全漏洞,使其无法有效抵抗穷举搜索。
攻击者可以利用一种称为中间相遇攻击的方法。假设我们有一个明文-密文对 (M, C),满足 C = E(k1, E(k2, M))。我们可以将这个等式重写为:
D(k1, C) = E(k2, M)
这里,D 是解密函数。

攻击步骤如下:
- 构建前向表:对于所有
2^56个可能的密钥k2,计算X = E(k2, M)。将结果(k2, X)存储在一个表中,并按照X的值排序。此步骤耗时约O(2^56)。 - 反向搜索:对于所有
2^56个可能的密钥k1,计算Y = D(k1, C)。 - 查找碰撞:在排序好的表中快速查找(二分查找)是否存在
X等于Y。如果找到,那么对应的(k1, k2)就是正确的密钥对。
这种攻击的总时间复杂度约为 O(2^56 * log(2^56)) ≈ 2^63,远小于对112位密钥进行穷举搜索的 2^112。虽然它需要 O(2^56) 的存储空间,但时间上的巨大优势使得双重DES并不比单DES安全多少。
因此,永远不要使用双重DES。 如果必须使用DES系算法,应选择三重DES。

另一种增强方案:DESX
除了三重DES,还有一种旨在抵抗穷举搜索且性能损失更小的构造,称为DESX。它并未被NIST标准化,因为它不能防御DES的其他一些深层攻击,但如果只关心穷举搜索,它是一个有趣的选择。
对于分组密码 E: K × {0,1}^n -> {0,1}^n,DESX构造使用三个密钥 (k1, k2, k3):
def DESX_encrypt((k1, k2, k3), m):
# k1, k3 是 n 位密钥(与分组等长),k2 是 E 的原生密钥
xored_input = m XOR k3
encrypted = E(k2, xored_input)
output = encrypted XOR k1
return output
DESX的总密钥长度为 |k1| + |k2| + |k3|。对于DES而言,n=64,|k2|=56,所以总长为 64 + 56 + 64 = 184 位。
已知对DESX的最佳通用攻击时间复杂度约为 2^(n + |k2|),即 2^(64+56)=2^120,这仍然非常安全。它的优点是只增加两次快速的XOR操作和一次加密操作,性能损失远小于3DES。

重要警告:DESX必须在输入和输出两端都进行XOR(白化)。如果只在其中一端进行XOR,则构造无法增强安全性,仍然容易受到针对原始密码 E 的穷举搜索攻击。

总结

本节课中我们一起学习了针对DES的穷举搜索攻击。我们首先分析了给定明文-密文对时密钥的唯一性。然后,通过DES挑战赛的历史,我们认识到56位密钥的DES在现有计算能力下已完全失效。为了增强安全性,我们介绍了三重DES,它通过增加密钥长度有效抵抗了穷举搜索,但付出了速度变慢的代价。我们还分析了双重DES的不安全性,其“中间相遇攻击”使其形同虚设。最后,我们简要了解了DESX构造,它在抵抗穷举搜索和性能之间提供了一个折衷方案。下一节,我们将探讨针对DES的更复杂的密码分析攻击。
016:对分组密码的更多攻击 🔐

在本节课中,我们将学习针对分组密码的几种高级攻击方法。这些攻击不仅针对算法设计本身,还针对其实现方式,甚至包括未来可能出现的量子计算威胁。了解这些攻击有助于我们理解为何必须使用经过严格审查的标准算法,而非自行设计或实现。
实现层面的攻击 ⚙️
上一节我们介绍了分组密码的基本原理,本节中我们来看看针对其实现方式的攻击。这些攻击不直接破解算法,而是利用其物理实现中的信息泄露。
计时攻击 ⏱️
以下是计时攻击的基本原理:
- 攻击者精确测量加密或解密操作所花费的时间。
- 如果操作时间与密钥的某些比特存在依赖关系,攻击者通过多次测量就能推断出密钥信息。
- 这种攻击不仅限于智能卡,在多核处理器上,攻击者甚至可以通过观察缓存未命中情况来提取另一个核心上运行的加密算法的密钥。
功耗分析攻击 🔋

以下是功耗分析攻击的基本原理:
- 攻击者测量设备(如智能卡)在执行加密操作时的功耗曲线。
- 功耗的细微变化可能与密钥比特或中间运算结果相关。
- 即使设备采取了一些防护措施,差分功耗分析等高级攻击仍可通过分析大量运行数据来提取密钥。
故障攻击 ⚡
以下是故障攻击的基本原理:
- 攻击者通过物理手段(如超频、加热)使加密设备产生故障,导致其输出错误结果。
- 研究表明,如果在加密过程的最后一轮引入错误,产生的错误密文可能足以泄露整个密钥。
- 防御方法通常包括多次计算并验证结果,但这本身也增加了实现的复杂性。
这些例子表明,实现密码学原语非常微妙。因此,我们不应自行实现这些算法,而应使用像 OpenSSL 这样的标准库。

密码分析攻击:线性密码分析 📊
现在,我们转向更复杂的、针对算法设计本身的密码分析攻击。这里我们以针对 DES 的线性密码分析为例。
假设我们拥有大量明密文对 (M, C),其中 C = Enc(K, M)。攻击的目标是找到比暴力搜索更快的方法来恢复密钥 K。
线性密码分析的核心思想是:在算法中寻找一个微小的线性偏差。具体来说,攻击者试图找到一个涉及明文、密文和密钥比特的线性关系,该关系成立的概率不是精确的 1/2,而是 1/2 + ε,其中 ε 是一个很小的偏差(称为偏差)。
对于 DES,由于其第五个 S 盒设计存在缺陷,过于接近一个线性函数,导致在整个算法中产生了这样一个关系。这个关系的偏差 ε 非常小,约为 2^{-21}。
如何利用线性关系
假设我们发现了如下关系(其中 ⊕ 表示异或运算):
[明文比特子集] ⊕ [密文比特子集] = [密钥比特子集]
并且该等式成立的概率是 1/2 + ε。
以下是利用此关系恢复部分密钥比特的步骤:
- 收集大约
1/ε^2个随机的明密文对。 - 对于每一对
(M, C),计算等式的左边部分(即[明文比特子集] ⊕ [密文比特子集])。 - 对所有计算结果取众数。由于存在
ε的偏差,这个众数将以高概率(例如 97.7%)等于等式右边的[密钥比特子集]。
对于 DES,ε = 2^{-21},因此需要约 2^{42} 个明密文对。通过这种方法,可以恢复出约 14 个密钥比特。剩下的 42 个比特则通过暴力搜索完成。总攻击时间约为 2^{43},这比完整的 2^{56} 暴力搜索要快得多。
这个攻击的教训是深刻的:算法中任何微小的线性缺陷都可能导致严重的攻击。这再次强调了使用经过充分分析的标准算法的重要性。
量子攻击:格罗弗算法 ⚛️
最后,我们探讨一种理论上对所有分组密码都构成威胁的通用攻击——量子攻击,特别是格罗弗搜索算法。
格罗弗算法简介
考虑一个在定义域 X 上的函数 F,它几乎对所有输入都输出 0,仅对唯一一个输入 x* 输出 1。我们的目标是找到这个 x*。
- 在经典计算机上,我们只能将
F当作黑盒,逐个尝试所有输入,所需时间与|X|成正比。 - 格罗弗算法表明,在量子计算机上,可以在大约
√|X|的时间内找到x*。这是一个平方级的加速。
对分组密码的影响

这如何用于攻击分组密码呢?假设我们有一个明密文对 (M, C)。我们可以定义一个关于密钥 k 的函数:
F(k) = 1, 如果 Enc(k, M) == C;否则 F(k) = 0。
显然,只有正确的密钥 K 能使 F(K)=1。因此,寻找密钥的问题就转化成了上述搜索问题。
- 对于 DES(56 位密钥),经典暴力搜索需要
2^56步,而量子攻击仅需约2^28步。 - 对于 AES-128(128 位密钥),量子攻击将时间降至约
2^64步,这已被认为是不安全的。 - 因此,如果未来能够建造出强大的量子计算机,我们必须使用更长的密钥,例如 AES-256(256 位密钥),其量子攻击时间
2^128在可预见的未来仍然是安全的。
总结 📝

本节课中我们一起学习了针对分组密码的多种攻击:
- 实现攻击:如计时攻击、功耗分析和故障攻击,它们利用物理实现的副作用泄露密钥。
- 密码分析攻击:以线性密码分析为例,展示了算法设计中微小的统计偏差如何被放大并用于恢复密钥。
- 量子攻击:介绍了格罗弗算法,它理论上能为所有分组密码的密钥搜索提供平方加速,强调了使用长密钥(如 256 位)以应对未来威胁的重要性。
所有这些攻击都指向同一个核心结论:分组密码的设计和实现极其复杂且充满陷阱。因此,在实践中,我们绝不应该自行设计或实现加密算法,而应始终依赖并正确使用经过严格审查和广泛测试的标准(如 AES)。
017:AES分组密码 🔐

在本节课中,我们将学习高级加密标准(AES)。我们将了解AES诞生的背景、其基本设计原理、具体的工作方式、不同的实现策略以及其安全性分析。
概述

随着时间推移,DES和三重DES因其设计不适合现代硬件且速度过慢的问题日益凸显。因此,美国国家标准与技术研究院(NIST)启动了一项新进程,旨在标准化一种名为高级加密标准(AES)的新分组密码。
AES的诞生与基本参数
上一节我们提到了DES的局限性,本节中我们来看看它的继任者AES是如何被选定的。
NIST在1997年发起征集新分组密码提案的倡议。一年后,它收到了15份提交方案。最终在2000年,NIST采用了名为Rijndael的密码作为高级加密标准。这是一种在比利时设计的密码。
AES的分组大小为128位,并具有三种可能的密钥长度:128位、192位和256位。通常假设,密钥长度越大,该分组密码作为伪随机置换的安全性就越高。但同时,由于操作中涉及的轮数更多,加密速度也会变慢。例如,AES-128是这些密码中最快的,而AES-256则是最慢的。
AES的设计结构:代换-置换网络
AES被构建为一种“代换-置换网络”。这与Feistel网络不同,在Feistel网络中,每轮有一半的比特保持不变;而在代换-置换网络中,所有比特在每一轮都会被改变。
以下是代换-置换网络第一轮的工作原理:
- 与轮密钥异或:首先,将当前状态与第一轮密钥进行异或操作。
- 代换层:然后,状态块根据代换表(S盒)被替换为其他块。
- 置换层:接着,比特被重新排列和打乱。
这个过程会重复进行:与下一个轮密钥异或,经过代换阶段,再置换比特,如此循环,直到最后一轮。在最后一轮,与最终的轮密钥异或后,输出密文。
关于这个设计的一个重要点是,由于其构建方式,网络中的每一步都必须是可逆的,以确保整个加密过程可逆。因此,解密本质上就是按相反顺序应用网络中的每一步:从置换步骤开始(必须确保该步可逆),然后是代换层(也必须可逆)。这与DES非常不同,DES的代换表(将6比特映射到4比特)本身是不可逆的。当然,与轮密钥的异或操作也是可逆的。

所以,代换-置换网络的逆运算就是简单地按相反顺序应用所有步骤。
AES的具体实现细节
现在我们已经理解了通用结构,让我们具体看看AES的细节。AES操作在128位(16字节)的分组上。在AES中,我们将这16字节写成一个4x4的矩阵,矩阵中的每个单元格包含一个字节。
加密过程如下:
- 首先与第一轮密钥进行异或。
- 然后对状态应用一个包含代换、置换和其他操作的特定函数(轮函数)。同样,这里应用的函数必须是可逆的,以便能够解密。
- 接着与下一个轮密钥异或,并再次应用轮函数。
- 这个过程重复进行10轮(对于AES-128)。值得注意的是,在最后一轮中,混合列步骤实际上被省略了。
- 最后,与最后一轮轮密钥异或后输出密文。
在整个过程中,我们始终保持这个4x4的数组,因此输出也是4x4(16字节,128位)。

轮密钥本身是通过密钥扩展从一个16字节的AES密钥派生出来的。密钥扩展将16字节的AES密钥映射成11个轮密钥,每个也是16字节(同样表示为4x4数组),用于与当前状态进行异或。
以下是AES工作的示意图,剩下的就是具体说明这三个函数:字节代换、行移位和混合列。
AES的轮函数详解
这三个函数相对容易解释,我将给出它们的高层描述,感兴趣的读者可以在线查阅细节。


- 字节代换:这本质上是一个包含256字节的S盒。它对当前状态矩阵中的每一个字节应用这个S盒。具体来说,对于4x4状态矩阵中的每个元素
a[i][j],我们使用该字节的值作为索引查找S盒,并用查找到的值替换原字节。a[i][j]<--S_box[a[i][j]]
- 行移位:这基本上只是一个置换操作。它对每一行进行循环移位。具体是:第一行不移位,第二行循环左移1个位置,第三行循环左移2个位置,第四行循环左移3个位置。
- 混合列:这实际上是对每一列应用一个线性变换。有一个特定的矩阵会乘以每一列,从而得到新的列。这个线性变换独立地应用于每一列。
需要指出的是,行移位和混合列在代码中很容易实现。字节代换本身也是可计算的,因此你可以编写少于256字节的代码来计算S盒,从而通过存储计算S盒的代码来压缩AES的描述,而不是在实现中硬编码整个S盒表。
AES的实现策略与权衡

事实上,关于AES有一个普遍现象:如果你完全不允许预计算(包括动态计算S盒),那么你可以得到一个相当小的AES实现,可以适应存储空间非常有限的环境。当然,这将是速度最慢的实现,因为所有操作都需要动态计算。
相反,如果你有足够的空间支持大型代码,你可以预计算刚才提到的三个步骤。有多种预计算选项:可以构建一个只有4KB大小的表,也可以构建更大的表(例如24KB)。这样,你的实现中会有这些大表,但实际性能会非常好,因为你所做的只是查表和异或操作,没有其他复杂的算术运算。
因此,这三个步骤(字节代换、行移位、混合列)可以转化为少量查表和异或操作。这里存在代码大小和性能之间的权衡:在高端机器上,你可以预计算并存储这些大表以获得最佳性能;而在低端设备(如8位智能卡或手表)上,你会有一个相对较小的AES实现,但速度当然不会那么快。

一个特殊案例:JavaScript实现
这里有一个有点不寻常的例子:假设你想在JavaScript中实现AES,以便将库发送到浏览器,让浏览器自己执行AES加密。在这种情况下,你希望既缩小代码大小(以减少网络传输流量),又希望浏览器性能尽可能快。
我们之前做过的一个方案是:实际发送到浏览器的代码不包含任何预计算表,因此代码相当小。但是,一旦代码在浏览器中加载,浏览器会立即预计算所有表格。这样,代码从紧凑状态“膨胀”为包含所有这些预计算表,但这些表存储在内存充足的笔记本电脑上。一旦有了预计算表,你就可以使用它们进行加密,从而获得最佳性能。因此,如果你需要通过网络发送AES实现,你可以获得两全其美的效果:网络上的代码很小,但当它到达客户端时,可以自我“膨胀”并获得最佳加密性能。
硬件加速:AES-NI指令集
AES如今是如此流行的分组密码,以至于当在产品中构建加密功能时,基本上都应该使用AES。因此,英特尔实际上在处理器本身加入了AES支持。
自Westmere架构以来,英特尔处理器中就有了特殊的指令来帮助加速AES。这些指令主要分为两对:AESENC 和 AESENCLAST,以及 AESKEYGENASSIST。

AESENC指令本质上实现了AES的一轮操作:应用三个函数(字节代换、行移位、混合列)并与轮密钥异或。AESENCLAST指令实现了AES的最后一轮操作(最后一轮没有混合列步骤,只有字节代换和行移位)。- 调用这些指令时使用128位寄存器(如XMM寄存器),一个寄存器包含AES状态,另一个包含当前轮密钥。调用
AESENC后,结果会放回状态寄存器。
因此,如果你想实现整个AES加密,你只需要调用9次AESENC和1次AESENCLAST。这10条指令基本上就是AES的完整实现。由于这些操作在处理器内部完成,而不是使用外部指令,据称它们可以获得比在同一硬件上不使用这些特殊指令的实现快14倍的速度提升。这是一个相当显著的加速。事实上,现在很多产品都在利用这些特殊指令。这并非英特尔独有,AMD在其推土机架构中也实现了非常类似的指令。
AES的安全性分析
最后,我们来谈谈AES的安全性。这里我只想提及两种攻击。显然,AES已经被广泛研究,但对完整AES的攻击主要有以下两种:
- 密钥恢复攻击:最好的攻击方法大约只比穷举搜索快4倍。这意味着,对于128位密钥,你应该将其视为相当于126位密钥的安全性,因为穷举搜索的速度大约是应有的4倍。当然,2^126次操作仍然远超我们现有的计算能力,这实际上并未损害AES的安全性。
- 相关密钥攻击(针对AES-256):AES的密钥扩展设计存在一个弱点,允许进行所谓的“相关密钥攻击”。简单来说,如果你提供大约2100个来自四个相关密钥的输入-输出对(这些密钥彼此非常接近,例如只有少数比特不同),那么存在一种复杂度约为2100的攻击。虽然2100在今天仍然不切实际,但它远优于2256的穷举搜索,这算是该密码的一个局限。然而,这通常不是一个重大问题,因为该攻击要求使用相关密钥。在实践中,你应该随机选择密钥,这样系统中就不会有相关密钥,因此该攻击不适用。但如果你确实使用了相关密钥,那么就会存在问题。
总结

本节课中我们一起学习了高级加密标准(AES)。我们了解了AES取代DES的背景,其作为代换-置换网络的基本设计原理,以及具体的加密步骤:字节代换、行移位和混合列。我们还探讨了AES在不同场景下的实现策略与权衡,以及现代处理器通过AES-NI指令集提供的硬件加速。最后,我们简要分析了AES的安全性,认识到虽然存在理论上的相关密钥攻击,但在正确使用随机密钥的实践中,AES仍然是一个非常安全且高效的分组密码标准。
018:基于PRG构建分组密码

在本节中,我们将探讨一个核心问题:能否使用伪随机生成器这类更简单的原语来构建分组密码?答案是肯定的。
从PRG到PRF
上一节我们介绍了伪随机函数和伪随机置换的概念。本节中,我们来看看如何从一个伪随机生成器构建一个伪随机函数。
首先,我们从一个能将输入长度翻倍的PRG开始。设其种子空间为 K,输出是两个 K 中的元素。其安全性意味着输出与 K² 中的随机元素是不可区分的。
基于此,我们可以轻松定义一个“1比特PRF”,即输入域仅为1比特的PRF。定义如下:
- 如果输入比特 X = 0,则输出PRG的左半部分。
- 如果输入比特 X = 1,则输出PRG的右半部分。
用公式表示,即:
F(k, x) = G(k)[x]
其中 G(k)[0] 表示左输出,G(k)[1] 表示右输出。

可以证明,如果 G 是一个安全的PRG,那么这个1比特PRF就是一个安全的PRF。这个证明几乎是同义反复,你可以自行思考验证。
扩展PRF的输入域
真正的挑战在于,我们能否构建一个输入域更大的PRF,例如像AES那样支持128比特输入?
让我们逐步推进。首先,我们尝试构建一个输出四倍于输入的PRG,即从 K 映射到 K⁴。方法是对原始PRG的输出再次应用PRG。
具体构造 G₁(k) 如下:
- 首先计算
(t₀, t₁) = G(k)。 - 然后计算
(t₀₀, t₀₁) = G(t₀)和(t₁₀, t₁₁) = G(t₁)。 - 最终输出为四元组
(t₀₀, t₀₁, t₁₀, t₁₁)。

现在,我们可以基于 G₁ 构建一个2比特PRF。给定2比特输入(00, 01, 10, 11),只需输出 G₁(k) 对应的那个块即可。
为什么 G₁ 是安全的?
我们可以通过一个“图示证明”来理解。核心思想是利用PRG的安全性,逐步将伪随机输出替换为真正的随机值。由于每一步替换都不会被高效敌手察觉,最终可以证明 G₁(k) 的输出与 K⁴ 中的随机四元组是不可区分的。
构建GGM PRF
既然我们可以将输出扩展一次,就可以重复这个过程。通过反复应用PRG,我们可以构建输出 K⁸、K¹⁶ 等元素的生成器,并相应地得到3比特、4比特的PRF。
这种迭代模式引出了著名的 GGM PRF(由Goldreich、Goldwasser和Micali发明)。它能从一个“翻倍PRG” G 构建出输入域为 {0,1}ⁿ 的PRF,其中 n 可以很大(如128)。

以下是GGM PRF的求值过程,给定密钥 k 和输入 x = (x₀, x₁, ..., xₙ₋₁):
k₀ = k
for i from 0 to n-1:
(t₀, t₁) = G(kᵢ)
kᵢ₊₁ = t_{xᵢ} # 根据输入比特 xᵢ 选择左输出(t₀)或右输出(t₁)
输出 kₙ
这个过程就像沿着一个二叉树向下走,每一步根据输入比特选择左分支或右分支,最终到达叶子节点作为输出。
因此,只要底层PRG G 是安全的,GGM PRF就是一个在超大域上的安全PRF。这是一个理论上非常优雅的成果。
为何GGM PRF不常用?
主要原因是效率。例如,若使用Salsa20作为PRG G,为了计算一个128比特输入的PRF值,需要调用Salsa20核心函数128次。这比AES等启发式构造的PRF要慢得多。不过,我们将在后续课程中看到这种构造在其他方面的应用(如消息完整性)。
从PRF到分组密码(PRP)
最后一步,我们回到最初的目标:能否从一个安全的PRG构建一个分组密码(即可逆的PRP)?
答案是肯定的。我们现在已经掌握了所有必要的组件:
- 我们可以从一个安全的PRG构建一个安全的PRF(使用GGM构造)。
- 我们知道,通过 Luby-Rackoff构造(一个三轮Feistel网络),可以将一个安全的PRF转换成一个安全的PRP。


将这两者结合,我们就得到了一个从伪随机生成器到伪随机置换的、可证明安全的构造方案。

虽然这个构造在理论上非常完美,但由于其效率远低于AES等实践中的启发式算法,因此并未被广泛用于构建实际的分组密码。
总结

本节课中我们一起学习了如何从基础的伪随机生成器逐步构建出伪随机函数,并最终通过Feistel网络得到伪随机置换(即分组密码)。我们介绍了1比特PRF、扩展输出的PRG、GGM PRF构造及其安全性证明思路,并理解了理论构造与实用算法在效率上的权衡。这为我们理解密码学组件的层次化构建奠定了坚实基础。
019:回顾PRP与PRF 🔐


在本节课中,我们将回顾伪随机置换(PRP)与伪随机函数(PRF)这两个核心概念。它们是理解分组密码如何用于安全加密的基础。我们将学习它们的定义、安全模型以及两者之间的关系。
上一节我们介绍了分组密码的概念。现在,在探讨如何使用它们进行安全加密之前,我们需要简要回顾一个重要的抽象概念:伪随机函数(PRF)和伪随机置换(PRP)。
分组密码将n比特的输入映射到n比特的输出,例如三重DES和AES。PRP和PRF是对分组密码概念的重要抽象。

一个伪随机函数(PRF)是一个接受两个输入的函数:一个密钥K和一个来自集合X的元素,并输出一个属于集合Y的元素。其基本要求是存在一个高效的算法来计算这个函数。我们稍后将讨论其安全性。
一个伪随机置换(PRP)与PRF类似,同样存在高效的计算算法E。但PRP有一个额外要求:它必须是一个对所有密钥都成立的一一映射(双射),并且存在一个高效的逆算法D。因此,PRP本质上是一个可逆的PRF。

现在我们来讨论如何定义安全的PRF。PRF的目标是看起来像一个从集合X到Y的随机函数。
为了更精确地描述这一点,我们定义以下符号:
Funs[X, Y]:表示所有从集合X映射到集合Y的函数的集合。这是一个极其庞大的集合。S_F:表示由某个特定PRF(通过固定不同密钥K)所定义的所有具体函数的集合。这个集合的大小等于密钥空间的大小。
我们希望达到的效果是:从这个庞大集合Funs[X, Y]中随机选择一个函数,与从这个较小集合S_F中随机选择一个函数,是计算上不可区分的。
“不可区分”意味着,一个能够与某个函数进行交互的对手,无法区分他是在与S_F中的一个函数交互,还是在与Funs[X, Y]中的一个真正随机函数交互。
我们通过两个实验来形式化定义安全性:
- 实验0:挑战者随机选择一个PRF的密钥
K,从而确定一个函数f(属于S_F)。 - 实验1:挑战者从
Funs[X, Y]中真正随机地选择一个函数f。
在这两个实验中,对手都可以自适应地提交最多Q个查询x1, x2, ..., xQ,并得到对应的函数值f(x1), f(x2), ..., f(xQ)。最后,对手输出一个比特b'来猜测他处于哪个实验。
我们说一个PRF是安全的,如果对于任何高效(多项式时间)的对手,他在实验0中输出1的概率与在实验1中输出1的概率之差是可忽略的。这完美地捕捉了“对手无法区分伪随机函数与真正随机函数”这一概念。
安全伪随机置换(PRP,即安全的分组密码)的定义与PRF非常相似,关键区别在于比较的基准集合不同。
- 实验0:挑战者随机选择一个PRP的密钥
K,从而确定一个置换f。 - 实验1:挑战者从
Perms[X](所有从集合X到自身的双射函数的集合)中真正随机地选择一个置换f。
对手的交互和判断方式与PRF实验相同。我们说一个PRP是安全的,如果对手无法区分他是在与一个PRP实例交互,还是与一个真正随机的置换交互。

让我们通过一个简单例子来理解这些概念。假设集合X只包含两个点:{0, 1}。
以下是关于PRP的思考:
- 此时,
Perms[X](所有双射函数)只包含两个函数:恒等函数f(x)=x和取反函数f(x)=1-x。 - 考虑一个PRP,其密钥空间
K={0,1},定义加密函数为E(k, x) = x XOR k。 - 问题是:这个PRP是一个安全的PRP吗?答案是是。因为该PRP实现的函数集合
{x XOR 0, x XOR 1}正好等于Perms[X]。因此,随机选择密钥k产生的分布与从Perms[X]中随机选择函数的分布完全相同,对手无法区分。
我们已有一些安全PRP的例子,如三重DES和AES。关于AES,一个合理的具体安全假设是:所有运行时间不超过2^80步的算法,其攻击AES的优势最多为2^-40。

现在考虑同一个PRP:E(k, x) = x XOR k,其中X={0,1}。但这次的问题是:它是一个安全的PRF吗?
我们需要比较的不再是Perms[X],而是Funs[X, X](所有从X到X的函数)。这个集合包含四个函数:两个双射函数(恒等和取反),以及两个常数函数(f(x)=0和f(x)=1)。
这个PRP是安全的PRF吗?答案是否。存在一个简单的区分攻击:
以下是攻击步骤:
- 对手查询
f(0)和f(1)。 - 如果发现
f(0) == f(1)(即发生碰撞),那么他肯定不是在和一个PRP交互(因为PRP是双射,不可能有碰撞),所以他可以断定自己是在和Funs[X, X]中的一个随机函数交互。 - 计算优势:当与PRP交互时,输出“是随机函数”的概率为0;当与真正随机函数交互时,四个函数中有两个满足
f(0)=f(1),所以输出“是随机函数”的概率为1/2。因此对手的优势为|0 - 1/2| = 1/2,这不是可忽略的,所以该PRP不是安全的PRF。
这个反例成立仅仅是因为定义域X太小。实际上,有一个重要的引理,称为PRF转换引理。它指出:一个安全的PRP,当其定义域X足够大时,它也是一个安全的PRF。
该引理表述如下:
对于一个定义在集合X上的PRP E,任何最多进行Q次查询的对手A,其区分E与随机函数的优势,和其区分E与随机置换的优势,两者之差的绝对值不超过 Q² / (2 * |X|)。

当X很大时(例如AES的块大小|X|=2^128),即使对手进行大量查询(例如Q=2^32约10亿次),Q²/(2*|X|)的值也是可忽略的。这意味着,对手攻击PRF的优势几乎等于他攻击PRP的优势。
因此,如果E是一个安全的PRP(如我们相信AES那样),那么它自动也是一个安全的PRF。所以对于AES,我们既可以将其视为一个安全的PRP,也可以将其视为一个安全的PRF来使用。

作为最后的总结,从今往后,我们可以暂时忘记AES和三重DES的内部工作原理,只需简单地假设它们都是安全的PRP。我们将基于这个抽象来学习如何构建加密方案。每当我提到PRP或PRF时,你可以在脑海中将其对应为AES或三重DES。

本节课中,我们一起学习了:
- PRF和PRP的抽象定义及其区别。
- 如何通过交互实验形式化定义它们的安全性。
- 通过小域的例子理解了概念和攻击。
- 掌握了重要的PRF转换引理,它说明了在定义域足够大的情况下,安全的PRP必然是安全的PRF。
- 确立了将AES等分组密码作为安全PRP/PRF使用的理论基础。
020:一次性密钥 🔑

在本节课中,我们将学习如何使用分组密码进行加密,特别是探讨在一次性密钥场景下的正确使用方法。我们将从一种常见但不安全的模式开始,然后介绍一种安全的加密模式。
概述

上一节我们介绍了分组密码的基本概念。本节中,我们来看看如何利用分组密码,在密钥仅使用一次的情况下,安全地加密长消息。我们将首先分析一个经典但存在严重缺陷的模式,然后学习一种基于伪随机函数构建的安全加密方案。
电子密码本模式:一个经典错误
当我们想用分组密码加密时,首先想到的方法可能是将消息分割成块,然后独立加密每个块。这种模式被称为电子密码本模式。
以下是其工作原理:
- 将消息分割成与分组密码块大小相等的块。对于AES,每个块为16字节。
- 使用相同的密钥独立加密每个块。
然而,这种模式存在严重的安全问题。如果两个明文块相同,那么加密后得到的两个密文块也必然相同。攻击者即使不知道明文内容,也能通过观察密文块是否相等,推断出明文块之间的关系,从而泄露了本不应泄露的信息。
为了更直观地说明,考虑加密一张图片。如果图片中某些区域(如深色头发)的像素值重复出现,在ECB模式下,这些区域的密文也会重复,导致原始图像的轮廓在加密后的数据中依然可见。这清楚地展示了ECB模式如何泄露明文信息。

因此,ECB模式绝不能用于加密长度超过一个块的消息。


实现语义安全的目标
在深入安全模式之前,我们先简要回顾一下我们的安全目标:语义安全。在一次性密钥的场景下,攻击者选择两个消息 M0 和 M1,然后获得其中一个的加密结果。我们的目标是确保攻击者无法区分他获得的是 M0 还是 M1 的加密结果。由于密钥只用于加密一条消息,攻击者只会看到一个用该密钥加密的密文。
现在,我们可以形式化地证明ECB模式不满足语义安全。假设我们加密两个块。攻击者可以构造如下两个消息:
M0: 两个块不同(例如,“hello” 和 “world”)。M1: 两个块相同(例如,“hello” 和 “hello”)。
挑战者随机选择加密 M0 或 M1,返回两个密文块。攻击者只需检查这两个密文块是否相等:
- 如果相等,则说明明文块相同,他收到的必然是
M1的加密。 - 如果不相等,则说明明文块不同,他收到的必然是
M0的加密。
该攻击者的优势为1,完美地区分了两种加密,因此ECB模式不是语义安全的。
安全的方案:确定性计数器模式

那么,我们应该怎么做呢?一个简单而安全的方法是使用确定性计数器模式。这种模式本质上是用分组密码(作为一个安全的伪随机函数PRF)来构建一个流密码。
以下是其工作原理:
- 将消息
M分割成L个块:M[1], M[2], ..., M[L]。 - 使用密钥
K和伪随机函数F(如AES),在计数器值0, 1, 2, ..., L-1上进行计算,生成一个伪随机密钥流。 - 将每个消息块与对应的密钥流块进行异或操作,得到密文块。
用公式表示加密过程为:
C[i] = M[i] ⊕ F(K, i),其中 i = 0, 1, ..., L-1
这实际上就是用像AES这样的PRF构建的一个流密码。
安全性证明简述


我们已经学过流密码使用伪随机数生成器的安全性证明,这里的证明思路类似。对于任何试图攻击确定性计数器模式的敌手A,我们可以构造一个攻击PRF的敌手B。其优势关系可以表示为:
Advantage[A] ≤ Advantage[B] + negligible term
由于PRF是安全的,Advantage[B] 可忽略,因此 Advantage[A] 也可忽略。这意味着敌手无法以显著优势攻破该模式。
证明的核心思想是一个三步混合论证:
- 在真实世界中,敌手面对的是使用PRF
F(K, ·)的加密。 - 我们假设将PRF替换为一个真正的随机函数
RF(·)。由于PRF是安全的,敌手无法区分步骤1和步骤2。 - 在步骤2中,密钥流
RF(i)是真正随机的。此时,加密方案等同于一次性密码本,因此加密M0和加密M1的分布在统计上是不可区分的(实际上是相同的分布)。
通过这一系列不可区分的步骤,我们证明了确定性计数器模式在一次性密钥场景下是语义安全的。
总结
本节课中我们一起学习了分组密码在一次性密钥下的操作模式。
- 我们首先分析了电子密码本模式,指出其通过暴露明文块间的相等性而存在严重安全漏洞,绝对不可用于加密多块消息。
- 接着,我们介绍了确定性计数器模式。该模式将分组密码作为伪随机函数,通过将其应用于递增的计数器值来生成密钥流,再与明文进行异或加密。我们概述了其安全性证明,表明它能满足语义安全的要求。


下一节,我们将探讨如何扩展这些概念,使用同一个密钥安全地加密多条消息。
021:CPA安全 🔐

在本节中,我们将学习如何使用分组密码,在同一个密钥下加密多条消息。这在实践中很常见,例如,在文件系统中使用同一个密钥加密多个文件,或在网络协议中使用同一个密钥加密多个数据包。我们将探讨如何实现这一点,并首先定义当同一个密钥用于加密多条消息时,密码系统的安全性意味着什么。
选择明文攻击(CPA)模型
当我们多次使用同一个密钥时,攻击者会看到许多用该密钥加密的密文。因此,在定义安全性时,我们将允许攻击者发起所谓的“选择明文攻击”。这意味着攻击者可以获取他选择的任意消息的加密结果。
例如,如果攻击者与Alice交互,他可以请求Alice加密他选择的任意消息,而Alice会加密这些消息并将生成的密文交给攻击者。你可能会问,Alice为什么会这样做?这在现实中确实可能发生。实际上,这种模型是对现实情况的一种相当保守的建模。例如,攻击者可能向Alice发送一封电子邮件。当Alice收到邮件后,她将其写入自己的加密磁盘,从而使用她的密钥加密了攻击者的邮件。如果后来攻击者窃取了磁盘,他就获得了用Alice密钥加密的他发送给Alice的邮件密文。这就是一个选择明文攻击的例子。

攻击者的能力就是如此,而其目标基本上是破坏语义安全性。
CPA安全性的精确定义
接下来,我们将更精确地定义这种安全性。与往常一样,我们将通过两个实验(实验0和实验1)来定义选择明文攻击下的语义安全性,这两个实验被建模为挑战者和攻击者之间的一个游戏。
游戏开始时,挑战者会选择一个随机密钥 K。然后攻击者可以开始查询挑战者。攻击者首先提交一个语义安全查询,即他提交两条等长的消息 M0 和 M1。然后,攻击者会收到其中一条消息的加密结果:在实验0中,他收到 M0 的加密;在实验1中,他收到 M1 的加密。到目前为止,这看起来像一个标准的语义安全游戏。
然而,在选择明文攻击中,攻击者可以重复此查询。他可以再次提交另一对等长的选择明文,并再次收到其中一条的加密结果。攻击者可以继续发出此类查询,我们假设他最多可以发出 Q 次这样的查询。每次他提交一对等长的消息,他要么得到左边消息的加密,要么得到右边消息的加密。在实验0中,他总是得到左边消息的加密;在实验1中,他总是得到右边消息的加密。
攻击者的目标是弄清楚他处于实验0还是实验1。换句话说,他是持续收到左边消息的加密,还是右边消息的加密。从某种意义上说,这是一个标准的语义安全游戏,只是攻击者可以自适应地、一个接一个地发出多次查询。
选择明文攻击体现在:如果攻击者想要某个特定消息 M 的加密,他可以在某次查询 j 中,将 M0 和 M1 都设置为完全相同的消息 M。由于两条消息相同,他知道他将收到他感兴趣的消息 M 的加密。这正是我们所说的选择明文攻击,攻击者可以提交消息 M 并收到该消息的加密。
因此,他的一些查询可能是这种选择明文性质的(左右消息相同),而另一些查询可能是标准的语义安全查询(两条消息不同),这实际上为他提供了关于他处于哪个实验的信息。
现在,我们应该熟悉这个定义了:我们说一个系统在选择明文攻击下是语义安全的,如果对于所有高效的攻击者,他们无法区分实验0和实验1。换句话说,攻击者输出 b'(我们记作实验 b 的输出)的概率,在实验0和实验1中是相同的。攻击者无法区分总是收到左边消息的加密与总是收到右边消息的加密。
确定性加密的不足

我断言,到目前为止我们看到的所有密码,例如确定性计数器模式或一次性密码本,在选择明文攻击下都是不安全的。更一般地说,假设我们有一个加密方案,对于特定消息 M 总是输出相同的密文。换句话说,如果我让加密方案加密消息 M 一次,然后再让它加密消息 M 一次,如果两次都输出相同的密文,那么这个系统不可能在选择明文攻击下是安全的。
确定性计数器模式和一次性密码本都具有这种特性:给定相同的消息,它们总是输出相同的密文。
让我们看看为什么这不能是选择明文安全的。攻击实际上相当简单。攻击者将输出相同的消息两次。这表明他确实想要 M0 的加密。这里攻击者得到了 C0,即 M0 的加密。这是他选择明文查询的结果,他实际上收到了他选择的消息 M0 的加密。现在他要破坏语义安全性。他做的是输出两条等长的消息 M0 和 M1,然后他会得到 M_b 的加密。但是请注意,我们说加密系统在加密消息 M0 时总是输出相同的密文。因此,如果 b = 0,我们知道挑战密文 C 就等于 C0。然而,如果 b = 1,那么挑战密文是 M1 的加密,它不等于 C0。
攻击者所做的就是检查 C 是否等于 C0。如果相等,他输出 0;否则输出 1。在这种情况下,攻击者能够完美地猜出比特 b,因此他确切地知道他被给予的是 M0 的加密还是 M1 的加密。结果,他在这个游戏中获胜的优势是 1,这意味着该系统不可能是CPA安全的(1 不是一个可忽略的数字)。
这表明确定性加密方案不可能是CPA安全的。但在实践中这意味着什么呢?这意味着每条消息总是被加密成相同的密文。如果你在磁盘上加密文件,并且恰好加密了两个相同的文件,它们会产生相同的密文。那么攻击者通过查看加密磁盘就会知道这两个文件实际上包含相同的内容。攻击者可能不知道内容是什么,但他会知道两个加密文件是相同内容的加密,而他不应该能够知道这一点。
类似地,如果你在网络上发送两个相同的加密数据包,攻击者将不知道这些数据包的内容,但会知道这两个数据包包含相同的信息。例如,考虑一个加密的语音对话,每次线路安静时,系统都会发送零的加密。但由于零的加密总是映射到相同的密文,观察网络的攻击者将能够准确识别对话中安静的时刻,因为他每次都会看到完全相同的密文。
这些是确定性加密不可能安全的例子。形式上,我们说确定性加密在选择明文攻击下不可能是语义安全的。
解决方案:随机化加密与Nonce加密
那么我们该怎么办呢?这里的教训是:如果密钥将被用于加密多条消息,那么最好做到,给定相同的明文加密两次时,加密算法必须产生不同的密文。有两种方法可以实现这一点。
第一种方法称为随机化加密。在这里,加密算法本身会在加密过程中选择一些随机字符串,并使用该随机字符串来加密消息 M。这意味着,例如,一个特定的消息 M0 不会只映射到一个密文,而是会映射到一整“球”密文。每次加密时,我们基本上输出这个球中的一个点。每次加密时,加密算法选择一个随机字符串,该随机字符串导致球中的一个点。当然,解密算法在获取球中的任何点时,总是会将结果映射到 M0。类似地,消息 M1 的密文也会映射到一个球。由于加密算法使用随机性,如果我们用高概率加密相同的消息两次,我们会得到不同的密文。
不幸的是,这意味着密文必须比明文长,因为用于生成密文的随机性现在以某种方式编码在密文中。因此,密文占用更多空间,粗略地说,密文大小将比明文大,大约等于加密过程中使用的随机比特数。如果明文非常大(例如千兆字节长),随机比特数可能在128位左右,也许这个额外的空间并不重要。但如果明文非常短(可能它们本身只有128位),给每个密文增加额外的128位将使总密文大小翻倍,这可能相当昂贵。

因此,随机化加密是一个很好的解决方案,但在某些情况下,它实际上会带来相当大的成本。
随机化加密示例
让我们看一个简单的例子。假设我们有一个伪随机函数 F,它接受某个空间 R(称为nonce空间)中的输入,并输出在消息空间 M 中。
现在定义以下随机化加密方案:当我们想要加密消息 M 时,加密算法首先在nonce空间 R 中生成一个随机的 r。然后它输出一个由两部分组成的密文:第一部分是值 r,第二部分是伪随机函数在点 r 处的评估值与消息 M 的异或结果:F(r) XOR M。
我的问题是:这个加密系统在选择明文攻击下是语义安全的吗?

正确答案是是的,但前提是nonce空间 R 足够大,使得 r 以极高的概率永不重复。让我们快速论证为什么这是真的。首先,因为 F 是一个安全的伪随机函数,我们不妨用一个真正的随机函数来替换它。换句话说,这与我们使用真正的随机函数 f 在点 r 处求值然后与 M 异或来加密消息 M 的情况是无法区分的。
但由于这个 r 从不重复,每个密文使用不同的 r,这意味着 F(r) 的值每次都是随机、均匀、独立的字符串。因此,每次我们加密消息时,我们基本上都是使用一个新的均匀随机的一次性密码本来加密它。由于将均匀字符串与任何字符串进行异或只会产生一个新的均匀字符串,因此生成的密文分布为两个随机均匀的字符串(我们称它们为 r 和 r')。因此,在实验0和实验1中,攻击者看到的都是真正的均匀随机字符串 (r, r')。由于在两个实验中攻击者看到的是相同的分布,他无法区分这两个分布。因此,既然在使用真正的随机函数时安全性完全成立,那么在使用伪随机函数时,安全性也将成立。
这是一个很好的例子,展示了我们如何利用伪随机函数表现得像随机函数这一事实,来论证这个特定加密方案的安全性。
另一种方案:Nonce加密

构建选择明文安全加密方案的另一种方法是所谓的Nonce加密。
在Nonce加密系统中,加密算法实际上接受三个输入,而不是两个:通常接受密钥和消息,但它还接受一个称为Nonce的额外输入。类似地,解密算法也接受Nonce输入,然后产生解密后的明文。
这个Nonce值 N 是什么?Nonce是一个公开值,不需要对攻击者隐藏。唯一的要求是,密钥-Nonce 对 (K, N) 只用于加密一条消息。换句话说,这个 (K, N) 对必须随消息而变化。有两种改变它的方法:一种是为每条消息选择一个新的随机密钥;另一种是始终使用相同的密钥,但必须为每条消息选择一个新的Nonce。
我想再次强调,这个Nonce不需要是秘密的,也不需要是随机的。唯一的要求是Nonce是唯一的。实际上,在整个课程中我们将使用这个术语:对我们来说,Nonce意味着一个不重复的唯一值,它不必是随机的。
Nonce选择示例

让我们看一些选择Nonce的例子。最简单的选择是让Nonce成为一个计数器。例如,在网络协议中,你可以想象Nonce是一个数据包计数器,每次发送方发送数据包或接收方接收数据包时递增。这意味着加密方必须在消息之间保持状态,即必须保留这个计数器并在每条消息传输后递增。
有趣的是,如果解密方拥有相同的状态,那么就没有必要在密文中包含Nonce,因为Nonce是隐含的。
让我们看一个例子:HTTPS协议运行在可靠的传输机制上,这意味着发送方发送的数据包假设按顺序被接收方接收。因此,如果发送方发送数据包5,然后发送数据包6,接收方将按该顺序接收数据包5和数据包6。这意味着如果发送方维护一个数据包计数器,接收方也可以维护一个数据包计数器,并且两个计数器基本上同步递增。在这种情况下,没有理由在数据包中包含Nonce,因为Nonce在双方之间是隐含的。
然而,在其他协议中,例如在IPSec(设计用于加密IP层的协议)中,IP层不保证按顺序交付。因此,发送方可能发送数据包5,然后发送数据包6,但这些数据包可能以相反的顺序被接收方接收。在这种情况下,仍然可以使用数据包计数器作为Nonce,但现在Nonce必须包含在数据包中,以便接收方知道使用哪个Nonce来解密接收到的数据包。
因此,基于Nonce的加密是实现CPA安全的一种非常高效的方式。特别是,如果Nonce是隐含的,它甚至不会增加密文长度。
当然,生成唯一Nonce的另一种方法是简单地随机选择Nonce,假设Nonce空间足够大,使得在密钥的生命周期内,Nonce以高概率永不重复。在这种情况下,Nonce加密就简化为随机化加密。然而,这样做的好处是发送方不需要在消息之间保持任何状态。例如,如果加密发生在多个设备上(例如,我可能同时拥有笔记本电脑和智能手机,它们可能使用相同的密钥),那么如果要求有状态的加密,我的笔记本电脑和智能手机就必须协调以确保它们从不重用相同的Nonce。而如果它们都简单地随机选择Nonce,它们就不需要协调,因为以极高的概率,它们根本不会选择相同的Nonce(再次假设Nonce空间足够大)。因此,在某些情况下,无状态加密非常重要,特别是当同一个密钥被多台机器使用时。
Nonce加密的安全性定义
我想更精确地定义Nonce加密的安全性,特别是要强调,当Nonce由攻击者选择时,系统必须保持安全。允许攻击者选择Nonce很重要,因为攻击者可以选择他想要攻击的密文。想象一下Nonce恰好是一个计数器。当计数器达到值15时,也许在那个时候攻击者很容易破坏语义安全性。因此,攻击者会等到第15个数据包被发送,然后才要求破坏语义安全性。因此,当我们谈论基于Nonce的加密时,我们通常允许攻击者选择Nonce,并且即使在这种情况下,系统也应保持安全。
让我们在这种情况下定义CPA游戏,它实际上与之前的游戏非常相似。基本上,攻击者提交消息对 (M_i0, M_i1)(显然它们必须等长),并且他提供Nonce N_i。作为回应,攻击者被给予使用他选择的Nonce N_i 加密的 M_i0 或 M_i1。当然,与往常一样,攻击者的目标是分辨他被给予的是左边明文的加密还是右边明文的加密。
和以前一样,攻击者可以迭代这些查询,他可以发出任意多次查询,我们通常用 Q 表示攻击者发出的查询次数。现在,唯一的关键限制是:尽管攻击者可以选择Nonce,但他被限制选择不同的Nonce。我们强制他选择不同Nonce的原因是,这是实践中的要求。即使攻击者欺骗Alice为他加密多条消息,Alice也永远不会再次使用相同的Nonce。因此,攻击者永远不会看到使用相同Nonce加密的消息,所以即使在游戏中,我们也要求所有Nonce都是不同的。
然后像往常一样,我们说一个Nonce加密系统在选择明文攻击下是语义安全的,如果攻击者无法区分实验0(他被给予左边消息的加密)和实验1(他被给予右边消息的加密)。
Nonce加密示例
让我们看一个Nonce加密系统的例子。和之前一样,我们有一个安全的伪随机函数 F,它接受Nonce空间 R 中的输入,并输出消息空间 M 中的字符串。
当选择新密钥时,我们将重置计数器 r 为 0。现在,当加密特定消息 M 时,我们将递增计数器 r,然后使用应用于该值 r 的伪随机函数来加密消息 M。和以前一样,密文将包含两个部分:计数器的当前值,以及消息 M 的一次性密码本加密结果:(r, F(r) XOR M)。
我的问题是:这是一个安全的Nonce加密系统吗?

和之前一样,答案是是的,但前提是Nonce空间足够大,以至于当我们递增计数器 r 时,它永远不会循环回 0,从而Nonce将始终是唯一的。我们以与之前相同的方式论证安全性:因为PRF是安全的,我们知道这个加密系统与使用真正的随机函数是无法区分的。换句话说,如果我们对计数器应用一个真正的随机函数,并将结果与明文 M 异或。现在,由于Nonce r 从不重复,每次我们计算 F(r) 时,都会得到一个真正随机、均匀且独立的字符串,因此我们实际上是在使用一次性密码本加密每条消息。结果,攻击者在两个实验中所看到的都基本上只是一对随机字符串。
因此,在实验0和实验1中,攻击者看到的分布完全相同,即所有这些选择明文查询的响应都只是均匀分布的字符串对。这在实验0和实验1中基本相同,因此攻击者无法区分这两个实验。既然他无法在使用真正随机函数的语义安全游戏中获胜,那么他在使用安全PRF的语义安全游戏中也无法获胜,因此该方案是安全的。
总结
现在,我们理解了当密钥用于加密多条消息时,对称系统安全意味着什么。要求是它必须在选择明文攻击下是安全的。我们说过,基本上要在选择明文攻击下安全的唯一方法是使用随机化加密,或使用Nonce永不重复的Nonce加密。

在接下来的两节中,我们将构建两个经典的加密系统,它们在密钥被多次使用时是安全的。
022:多密钥CBC 🔐

在本节课中,我们将要学习一种称为密码分组链接的操作模式,它能够利用分组密码构建出具备选择明文安全性的加密方案。我们将详细探讨其工作原理、安全性定理、实际应用中的注意事项以及如何处理非标准长度的消息。
概述
上一节我们介绍了选择明文安全的概念。本节中,我们来看看如何构建满足该安全性的加密方案。第一个这样的方案就是密码分组链接模式。
CBC是一种使用分组密码来获得选择明文安全性的方法。具体来说,我们将探讨一种使用随机初始化向量的CBC模式。
CBC加密的工作原理
假设我们有一个分组密码 E。我们定义CBC加密方案 E_CBC 如下:

加密算法在加密消息 M 时,首先会选择一个随机的IV。IV的长度恰好是一个分组密码块的大小。对于AES,IV是16字节。

然后,算法执行以下步骤:我们选择的IV会与第一个明文块进行异或运算。结果使用分组密码进行加密,输出第一个密文块。接着是链接部分:我们使用第一个密文块来掩盖第二个明文块。将两者异或后,加密结果成为第二个密文块,依此类推。
这就是密码分组链接。可以看到,每个密文块都被链接并异或到下一个明文块中。最终的密文将包含最初选择的IV以及所有的密文块。

IV代表初始化向量。每当加密方案在开始时需要随机选择某些内容时,我们通常会称其为IV。
需要注意的是,密文比明文略长,因为我们必须将IV包含在密文中,这基本上捕获了加密过程中使用的随机性。
以下是加密过程的公式化描述:
C[0] = IV
For i = 1 to L:
C[i] = E(K, P[i] XOR C[i-1])
最终密文为 (IV, C[1], ..., C[L])。
CBC解密的工作原理
第一个问题是,我们如何解密CBC加密的结果?

回顾加密过程:加密第一个消息块时,我们将其与IV异或,加密结果成为第一个密文块。那么,给定第一个密文块,如何恢复原始的第一个明文块?
解密实际上与加密非常相似。解密电路几乎相同,只是异或运算在底部而不是顶部。本质上,我们在解密过程中去掉了IV,只输出原始消息。IV被解密算法丢弃。
解密过程的公式如下:
P[i] = D(K, C[i]) XOR C[i-1]
其中 C[0] = IV
CBC模式的安全性定理
以下定理表明,使用随机IV的CBC模式加密在选择明文攻击下实际上是语义安全的。
更精确地说,如果我们从一个伪随机置换开始,即一个定义在空间 X 上的安全分组密码 E,那么我们将得到一个加密算法 E_CBC,它接受长度为 L 的消息,并输出长度为 L+1 的密文。
假设有一个进行 Q 次选择明文查询的敌手。那么我们可以陈述以下安全事实:对于每个攻击 E_CBC 的敌手 A,都存在一个攻击原始PRP(分组密码)的敌手 B,满足以下关系:
Adv_CPA[A, E_CBC] <= Adv_PRP[B, E] + (Q^2 * L^2) / |X|

这意味着,由于 E 是一个安全的PRP,Adv_PRP[B, E] 是可忽略的。我们的目标是说明敌手 A 的优势也是可忽略的。然而,这里我们被一个额外的误差项所阻碍。要论证CBC是安全的,我们必须确保这个误差项也是可忽略的。
实际安全界限分析
这表明,为了使 E_CBC 安全,Q^2 * L^2 必须远小于 |X| 的值。
让我们回顾一下 Q 和 L 是什么。L 是我们加密的消息的长度。Q 是敌手在CPA攻击下能看到的密文数量。在实际中,Q 基本上是我们使用密钥 K 加密消息的次数。
让我们看看这在现实世界中意味着什么。假设我们希望敌手的优势小于 1/2^32。这意味着误差项最好小于 1/2^32。
以AES为例。AES使用128位块,所以 |X| = 2^128。将其代入表达式,你会发现乘积 Q * L 最好小于 2^48。这意味着在使用特定密钥加密 2^48 个AES块后,我们必须更换密钥。本质上,CBC在密钥用于加密 2^48 个不同的AES块后就不再安全。
有趣的是,如果将同样的分析应用于DES,DES的块大小只有64位,你会发现密钥必须更频繁地更换,大约每 2^16 个DES块就需要生成一个新密钥。这是AES具有更大块大小的原因之一,这样像CBC这样的模式会更安全,并且可以在更长时间内使用密钥而无需更换。
关于IV可预测性的警告

需要警告你一个在使用带随机IV的CBC时非常常见的错误:一旦攻击者能够预测你将用于加密特定消息的IV,密码方案 E_CBC 就不再是CPA安全的。
因此,在使用带随机IV的CBC时,确保IV不可预测至关重要。让我们来看一个攻击示例。
假设攻击者给定某个消息的加密后,能够预测将用于下一条消息的IV。让我们展示这个系统实际上不是CPA安全的。
敌手首先请求加密一个单块消息,该块为全0。敌手得到的是消息的加密,即 0 XOR IV 的加密,当然,敌手也获得了IV。
接着,敌手根据假设可以预测将用于下一次加密的IV。然后,敌手发出他的语义安全挑战:消息 M0 是预测的IV与之前加密中使用的 IV1 的异或,而消息 M1 是其他任意消息。
现在,当敌手收到语义安全挑战的结果时,他会收到 M0 或 M1 的加密。如果收到的是 M0 的加密,那么实际被加密的明文是 (IV XOR IV1) XOR IV,这正好等于 IV1。敌手会收到 IV1 的块加密。请注意,他已经从之前的选择明文查询中获得了这个值。
而当他收到消息 M1 的加密时,他收到的只是 M1 的正常CBC加密。
因此,他现在有一个简单的方法来破解该方案:他会检查挑战密文的第二个块是否等于他在CPA查询中收到的值。如果相等,他就说收到了 M0 的加密,否则就说收到了 M1 的加密。这个敌手的优势是1,因此他完全破坏了CBC加密的CPA安全性。
这里的教训是:如果IV是可预测的,那么就没有CPA安全性。不幸的是,这实际上是一个非常常见的错误实践。例如,在SSL协议和TLS 1.1中,记录 i 的IV实际上是记录 i-1 的最后一个密文块。这意味着,给定记录 i-1 的加密,敌手确切地知道将用于记录 i 的IV是什么。就在去年夏天,这被转化为对SSL的一次相当具有破坏性的攻击。
基于Nonce的CBC加密
现在,我将向你展示CBC加密的基于Nonce的版本。在这种模式下,IV被一个非随机但唯一的Nonce所取代,例如数字1, 2, 3, 4, 5都可以用作Nonce。
这种模式的吸引力在于,如果接收者确实知道Nonce应该是什么,那么就没有理由将Nonce包含在密文中。在这种情况下,密文的长度与明文完全相同,这与带随机IV的CBC不同,后者必须扩展密文以包含IV。
因此,使用非随机但唯一的Nonce是完全可行的。然而,必须绝对清楚的是,如果这样做,在使用Nonce进入CBC链之前,还有一步必须完成。
具体来说,在这种模式下,我们将使用两个独立的密钥 K 和 K1。密钥 K 和之前一样,用于加密各个消息块。然而,密钥 K1 将用于加密非随机但唯一的Nonce,使得输出成为一个随机的IV,然后该IV被用于CBC链中。
这个用密钥 K1 加密Nonce的额外步骤绝对至关重要。没有它,CBC模式加密将不安全。特别是,如果你直接使用Nonce输入CBC加密,换句话说,你将Nonce用作IV,那么我们已经知道非随机的Nonce不会是CPA安全的。但事实上,即使你设 K 等于 K1,即你只是用密钥 K 加密Nonce,这也不会是CPA安全的,并可能导致严重的攻击。
因此,我希望你明白,如果CBC模式加密中的Nonce不是随机的,就必须进行这个额外的加密步骤。这是一个极其常见的实践错误。许多真实世界的产品和密码库实际上忘记了在使用Nonce进入CBC链之前对其进行加密,这导致了实际且严重的攻击。
此外,了解这一点非常重要,因为实际上许多密码API的设置几乎是在故意误导用户错误地使用CBC。例如,在OpenSSL中,用户被要求提供IV参数,而该函数直接在CBC加密机制中使用这个IV,没有在使用前对其进行加密。因此,如果你曾经使用非随机IV调用此函数,生成的加密系统将不是CPA安全的。
处理非标准长度消息
关于CBC的最后一个技术细节是,当消息长度不是分组密码块长度的倍数时该怎么办?也就是说,如果最后一个消息块短于AES的块长度(例如,小于16字节),我们该怎么办?

答案是,我们向最后一个块添加填充,使其长度达到16字节(AES块大小),这个填充当然会在解密过程中被移除。
以下是TLS中使用的典型填充方式:如果你用 n 个字节填充,那么你基本上将数字 n 重复写 n 次。例如,如果你用5个字节填充,你就用字符串 0x05 0x05 0x05 0x05 0x05 填充。
这种填充的关键在于,当解密者收到消息时,他会查看最后一个块的最后一个字节。假设该值是5,那么他只需移除消息的最后五个字节。
现在的问题是,如果消息恰好是16字节的倍数,我们该怎么办?实际上,如果不需要填充,那就会有问题,因为解密者会查看最后一个块的最后一个字节(现在是实际消息的一部分),并从中移除那么多字节的明文,这实际上会造成问题。
解决方案是,即使实际上不需要填充,我们仍然必须添加一个虚拟块。这个虚拟块基本上包含16个字节,每个字节都是数字16。解密者在解密时,查看最后一个块的最后一个字节,看到值是16,因此移除整个块,剩下的就是实际的明文。
这有点不幸,因为如果你用CBC加密短消息,并且消息恰好是32字节(16字节的倍数),那么你必须再添加一个块,使整个密文变成48字节,只是为了适应CBC填充。我应该提到,CBC有一个变体称为“密文窃取CBC”,可以避免这个问题,但这里不进行描述。
总结


本节课中,我们一起学习了密码分组链接模式。我们了解了CBC如何使用随机IV或加密后的Nonce来构建CPA安全的加密方案。我们探讨了其加密和解密过程,分析了其安全性定理及实际应用中的界限。我们强调了IV不可预测性的极端重要性,并指出了API使用中的常见陷阱。最后,我们讨论了如何处理非标准长度的消息。理解这些细节对于正确和安全地实现CBC至关重要。下一节,我们将看看如何使用计数器模式来加密多个消息。
023:多密钥CTR

概述
在本节课中,我们将学习一种比CBC模式更优的、能够实现选择明文安全性的加密方法:随机化计数器模式。我们将详细探讨其工作原理、安全定理,并与CBC模式进行全面比较。
随机化计数器模式的工作原理
上一节我们介绍了CBC模式,本节中我们来看看随机化计数器模式。与CBC不同,随机化计数器模式使用一个安全的伪随机函数,而非必须使用分组密码。这是因为在计数器模式中,我们永远不需要对函数F进行求逆。

我们令F为一个安全的伪随机函数,它作用于n比特的分组。例如,若使用AES-128,则n=128。计数器模式的加密算法工作方式如下:首先选择一个随机的初始化向量。在AES中,这是一个128比特的随机IV。然后,我们从这个随机IV开始计数。第一个加密的是IV本身,然后是IV+1,直到IV+L。我们生成这个随机密钥流,将其与明文进行异或操作,从而得到密文。需要注意的是,IV会与密文一起被包含在输出中,因此密文实际上比原始明文略长。加密算法为每条消息选择一个新的IV,因此即使对同一消息加密两次,也会得到不同的密文结果。
计数器模式的一个显著特点是完全可并行化。与CBC的顺序处理不同,CBC在加密第5个分组前必须先加密完前4个分组。这意味着即使硬件拥有多个AES引擎,在使用CBC时也无法并行利用它们,因为CBC本质上是顺序的。而计数器模式则完全不同,如果你有三个AES引擎,加密速度基本上可以提高三倍。

基于Nonce的计数器模式
计数器模式还有一种对应的变体,称为基于Nonce的计数器模式,其中IV并非真正随机,而只是一个计数器之类的Nonce。实现方式是将AES使用的128比特分组分成两部分:左边的64比特用作Nonce,右边的64比特用作内部计数器。Nonce部分可以是一个从0计数到264的计数器。一旦指定了Nonce,低位的64比特就在计数器加密内部进行计数。Nonce放在左边,计数器放在右边。只要这个Nonce是不可预测的,就没有问题。唯一的限制是,使用一个特定的Nonce最多只能加密264个分组。危险在于,要避免计数器重置为零,否则会导致两个分组使用相同的一次性密钥流进行加密。
随机化计数器模式的安全定理
现在,让我们快速陈述随机化计数器模式的安全定理。给定一个安全的伪随机函数F,我们最终得到一个加密方案,我们称之为E_CTR。该方案在选择明文攻击下是语义安全的。它能加密长度为L个分组的消息,并产生长度为L+1个分组的密文,因为IV必须包含在密文中。错误界限如下所示,基本上与CBC加密的情况相同。我们通常认为这一项是可忽略的,因为PRF F是安全的,并希望由此推断出这一项也是可忽略的,从而证明E_CTR是安全的。然而,我们这里有一个误差项,必须确保这个误差项是可忽略的。为此,我们需要确保Q²L小于分组大小。其中,Q是在特定密钥下加密的消息数量,L是这些消息的最大长度。有趣的是,在CBC的情况下,要求是Q²L²小于X,这比计数器模式的要求更严格。换句话说,计数器模式实际上可以比CBC加密更多的分组。

让我们看一个快速示例。这是计数器模式的误差项。假设我们希望对手的优势最多为1/232,这基本上要求Q²L/X小于1/232。对于AES,代入X=2128,我们得到Q²L应小于248。如果你加密的消息每个都是232个分组,那么在加密了232条消息后,就必须更换你的密钥,否则随机化计数器模式将不再具备选择明文安全性。这意味着使用单个密钥总共可以加密264个AES分组。而CBC对应的值是248个分组。因此,由于计数器模式具有更好的安全参数化,我们实际上可以使用同一个密钥,在计数器模式下比在CBC模式下加密更多的分组。
计数器模式与CBC模式的比较
接下来,我们对计数器模式和CBC模式进行快速比较,并论证在每一个方面,计数器模式都优于CBC模式。这也是为什么大多数现代加密方案实际上开始转向计数器模式并放弃CBC,尽管CBC仍然被广泛使用。
以下是详细的比较:

首先,CBC实际上必须使用分组密码,因为如果你查看解密电路,它实际上是反向运行分组密码的,使用了分组密码的解密能力。而计数器模式只需要一个伪随机函数,我们永远不需要使用分组密码的解密能力,只使用其正向的加密能力。因此,计数器模式实际上更通用,你可以使用像Salsa20这样的原语,它是一个PRF但不是PRP。计数器模式可以使用Salsa20,但CBC不能。从这个意义上说,计数器模式比CBC更通用。
其次,正如我们所说,计数器模式是可并行的,而CBC是一个非常顺序的过程。
第三,计数器模式更安全,其安全界限和误差项优于CBC。因此,在计数器模式下,你可以使用一个密钥加密比CBC更多的分组。
第四,在CBC中,我们讨论过虚拟填充分组的问题。如果消息长度是分组长度的整数倍,在CBC中我们必须添加一个虚拟分组。而在计数器模式中,这是不必要的。不过需要指出,CBC有一个称为“密文窃取”的变体,可以避免虚拟分组问题。但对于标准化的CBC,我们确实需要一个虚拟分组,而计数器模式则不需要。

最后,假设你正在加密一系列1字节的消息,并使用基于Nonce的加密(Nonce隐含在上下文中,不包含在密文里)。在这种情况下,每个1字节的消息都必须扩展成一个16字节的分组,然后加密,结果是一个16字节的分组。如果你有100条1字节的消息流,每条消息单独处理都会变成一个16字节的分组,最终你会得到一串16字节的密文。与明文长度相比,密文长度扩大了16倍。在计数器模式下,这当然不是问题。你只需用计数器模式生成的密钥流的第一个字节对每个1字节的消息进行异或加密。因此,每个密文将只有1字节,与对应的明文相同,完全没有扩展。由此可见,在每一个方面,计数器模式都优于CBC,这也是为什么它实际上是当今推荐使用的模式。
总结
本节课中我们一起学习了选择明文安全性。我们快速总结并提醒,我们将继续使用这些将分组密码抽象为PRP和PRF的方法,这实际上是思考分组密码的正确方式。我们总是将它们视为伪随机排列或伪随机函数。
此外,我们回顾了目前看到的两种安全概念,它们都只提供针对窃听的保护,而不提供针对密文篡改的保护。一种用于密钥仅加密单条消息的情况,另一种用于密钥加密多条消息的情况。正如我们所说,由于两者都不是为了防御篡改而设计的,因此都不提供数据完整性。我们将看到这是一个真正的问题。事实上,在下一节中,我们将指出这些模式实际上永远不应该单独使用。你应该只在结合了完整性机制的情况下使用这些模式,而完整性是我们下一个主题。
到目前为止,我们已经看到,如果密钥只使用一次,你可以使用流密码或确定性计数器模式。如果你要多次使用密钥,你可以使用随机化的CBC或随机化的计数器模式。一旦我们涵盖了完整性这一主题,我们将讨论如何同时提供完整性和机密性。
024:消息认证码 🔐
在本节课中,我们将停止讨论加密,转而探讨消息完整性。之后,我们会回到加密,并展示如何同时提供加密和完整性保护。


概述
正如之前所说,我们的目标是在不提供任何机密性的前提下,提供完整性保护。现实中确实存在许多这样的场景。例如,您磁盘上的操作系统文件(如果您使用Windows),这些文件并非机密,它们是公开且众所周知的。但确保它们不被病毒或恶意软件修改却至关重要。这就是一个需要完整性但无需机密性的例子。
另一个例子是网页上的横幅广告。广告提供商完全不关心是否有人复制并展示这些广告给其他人,因此不存在机密性问题。但他们确实关心广告是否被修改,例如,他们希望防止人们将广告篡改为其他类型的广告。这是另一个完整性至关重要而机密性无关紧要的例子。
消息认证码(MAC)简介
那么,我们如何提供消息完整性呢?基本机制被称为消息认证码。其工作方式如下:
我们有通信双方爱丽丝和鲍勃,他们共享一个密钥K,攻击者不知道这个密钥,但爱丽丝和鲍勃都知道。爱丽丝想要发送一个公开消息M给鲍勃,并确保攻击者无法在传输途中修改此消息。
爱丽丝使用一个MAC签名算法(记为S)来实现这一点。该算法以密钥和消息作为输入,并生成一个非常短的标签。标签可能只有90比特或100比特左右。即使消息长达千兆字节,标签也非常短。然后,她将标签附加到消息上,并将两者的组合发送给鲍勃。
鲍勃收到消息和标签后,运行MAC验证算法。该算法以密钥、消息和标签作为输入,并输出“是”或“否”,以指示消息是否有效或是否被篡改。
更精确地说,一个MAC系统由两个算法组成:一个签名算法和一个验证算法。它们定义在密钥空间、消息空间和标签空间上。签名算法输出一个标签,验证算法在给定密钥、消息和标签后输出“是”或“否”。

需要提醒的是,通常存在一致性要求:对于密钥空间中的每个密钥和消息空间中的每个消息,如果用特定密钥对消息签名,然后用同一密钥验证该标签,应该得到“是”的响应。这是标准的一致性要求,类似于我们在加密中看到的要求。
完整性为何需要共享密钥
需要指出的是,完整性确实需要爱丽丝和鲍勃之间共享一个密钥。事实上,人们常犯的一个错误是试图在没有共享密钥的情况下提供完整性。
这里有一个例子:考虑循环冗余校验。这是一种经典的校验算法,旨在检测消息中的随机错误。假设爱丽丝不使用密钥生成标签,而是使用CRC算法(该算法不需要密钥)生成一个标签,然后将此标签附加到消息上发送给鲍勃。
鲍勃将验证CRC是否正确。换句话说,鲍勃将验证标签是否等于CRC(M)。如果是,验证算法输出“是”;否则输出“否”。

这样做的问题是攻击者很容易破解。攻击者可以轻松地在途中修改消息并欺骗鲍勃,使其认为新消息是有效的。攻击者的做法是:拦截消息和标签,然后生成自己的消息M‘,并计算其CRC值,最后将M‘和CRC(M‘)的组合发送给鲍勃。鲍勃运行验证算法,验证会成功,因为右侧确实是左侧的有效CRC。结果,鲍勃会认为此消息来自爱丽丝,但实际上它已被攻击者完全修改,与爱丽丝发送的原始消息无关。
问题的关键在于CRC没有密钥。爱丽丝和攻击者之间没有区别,因此鲍勃不知道消息的来源。一旦我们引入密钥,爱丽丝就能做一些攻击者无法做到的事情,从而可能计算出攻击者无法修改的标签。
请记住,CRC旨在检测随机错误,而非恶意错误。而我们的目标是确保即使是恶意攻击者也无法在途中修改消息。
定义MAC系统的安全性
接下来,我们想定义MAC系统安全的含义。与往常一样,我们根据攻击者的能力和目标来定义安全性。
在MAC的情况下,攻击者的能力被称为选择消息攻击。换句话说,攻击者可以向爱丽丝提供任意他选择的消息M1到Mq,爱丽丝将为这些消息计算标签并交给他。
您可能会问,爱丽丝为什么会这样做?为什么爱丽丝会为攻击者给她的消息计算标签?就像在选择明文攻击中一样,在现实世界中,攻击者给爱丽丝一个消息,爱丽丝计算该消息的标签,然后攻击者获得结果标签,这种情况非常常见。例如,攻击者可能向爱丽丝发送一封电子邮件,爱丽丝可能希望将电子邮件保存到磁盘,以防止有人篡改磁盘,因此她会计算消息的标签,并将消息和标签保存到磁盘。后来,攻击者可能窃取爱丽丝的磁盘,现在他恢复了爱丽丝对他发送的消息的标签。这就是现实世界中攻击者实际获得爱丽丝对他所给消息的标签的选择消息攻击的例子。
那么,攻击者的目标是什么?他的目标是进行所谓的存在性伪造。换句话说,他试图产生一个新的有效消息-标签对,这个对不同于在选择消息攻击期间给他的任何一个对。如果他能做到这一点,那么我们就说系统是不安全的;如果他不能,那么我们就说系统是安全的。
需要强调的是,存在性伪造意味着攻击者无法为任何消息(即使是完全无意义的乱码)产生新的消息-标签对。您可能会想,如果攻击者计算了一个无意义消息的标签,这对攻击者有什么价值?但我们希望构建在任何使用场景下都安全的MAC。事实上,确实存在这样的情况,例如,您可能希望为一个随机密钥计算完整性标签。在这种情况下,即使攻击者能够为一个完全随机的消息计算标签,他也可能欺骗用户使用错误的密钥。因此,我们希望确保,如果MAC是安全的,攻击者就不能为任何消息(无论它是乱码还是有意义的)产生有效标签。
安全定义隐含的另一个属性是:如果攻击者获得某个消息-标签对,他应该无法为同一消息产生新的标签。换句话说,即使可能存在另一个标签T‘用于同一消息M,给定M和T的攻击者也不应能找到这个新的T‘。您可能会想,攻击者已经拥有消息M的一个标签,为什么还要关心能否为消息M产生另一个标签?他已经有一个标签了。但正如我们将看到的,确实存在一些应用,其中攻击者不能为先前签名的消息产生新标签这一点非常重要,特别是在我们结合加密和完整性时。
因此,我们将要求:给定消息的一个标签,不可能找到同一消息的另一个标签。
安全性的形式化定义

现在我们已经理解了要达成的目标,让我们像往常一样用一个更精确的游戏来定义它。
这里我们有两个算法S和V,以及一个对手A。游戏进行如下:挑战者像往常一样为MAC选择一个随机密钥。然后对手进行他的选择消息攻击:他向挑战者提交M1并收到该消息的标签;然后他提交M2并收到该消息的标签;依此类推,直到他向对手提交q条消息并收到所有这些消息的q个标签。
这就是选择消息攻击部分。然后对手继续尝试进行存在性伪造,即他输出一个新的消息-标签对。我们说如果他满足以下两个条件,他就赢得了游戏:首先,他输出的消息-标签对是有效的,即验证算法输出“是”;其次,这是一个新鲜的消息-标签对,即它不是我们之前给他的任何一个消息-标签对。

与往常一样,我们定义对手的优势为挑战者在此游戏中输出1的概率。我们说一个MAC系统是安全的,如果对于所有高效的对手,其优势都是可忽略的。换句话说,没有高效的对手能够以不可忽略的概率赢得这个游戏。
关于MAC安全性的两个问题
在我们构建安全的MAC之前,我想问两个问题。
第一个问题是:假设我们有一个MAC。碰巧攻击者能找到两个消息M0和M1,对于大约一半的密钥,它们具有相同的标签。换句话说,如果你随机选择一个密钥,有1/2的概率,消息M0的标签与消息M1的标签相同。我的问题是:这能是一个安全的MAC吗?需要强调的是,攻击者不知道M0和M1的标签是什么,他只知道这两个消息有1/2的概率具有相同的标签。
答案是否定的,这不是一个安全的MAC。原因在于选择消息攻击。本质上,攻击者可以请求消息M0的标签,然后他会从挑战者那里收到(M0, T)。实际上,T将是消息M0的有效标签。然后,他输出(M1, T)作为他的存在性伪造。请注意,(M1, T)不同于(M0, T)。这是一个有效的存在性伪造,因此攻击者以1/2的优势赢得游戏。我们得出结论,这个MAC不安全。
第二个问题是:假设我们有一个MAC,它总是输出一个5比特的标签。换句话说,这个MAC的标签空间是{0,1}^5。对于每个密钥和每个消息,签名算法只输出一个5比特的标签。问题是:这个MAC能是安全的吗?
答案当然是否定的,因为攻击者可以简单地猜测标签。他会这样做:他不会进行任何选择消息攻击,他只会如下输出一个存在性伪造:他会在{0,1}5中随机选择一个标签T,然后输出他的存在性伪造——消息0和标签T。现在,以1/(25)的概率,这个标签将是消息0的有效标签,因此对手的优势是1/32,这是不可忽略的。
这基本上说明标签不能太短,它们必须有一定的长度。实际上,典型的标签长度可能是64比特、96比特或128比特。例如,TLS使用96比特长的标签。当标签是96比特时,尝试猜测消息的标签,猜对的概率是1/(296),因此对手的优势仅为1/(296),这是可忽略的。

MAC的应用:保护系统文件
现在我们已经理解了MAC是什么,我想展示一个简单的应用。具体来说,让我们看看MAC如何用于保护磁盘上的系统文件。
想象一下,当您安装操作系统时,比如在您的机器上安装Windows,Windows会做的一件事是要求用户输入密码,然后从该密码派生出一个密钥K。然后,对于磁盘上的每个文件(假设文件是F1, F2, ..., Fn),操作系统会计算该文件的标签,然后将标签与文件一起存储。在这里,它将标签附加到每个文件上,然后擦除密钥K,不再在磁盘、内存或任何地方存储密钥K。

现在,假设后来机器感染了病毒,病毒试图修改一些系统文件。问题是用户能否检测到哪些文件被修改了。以下是一种方法:用户将机器重新启动到一个干净的操作系统中,比如从USB磁盘启动。一旦机器从这个干净的操作系统启动,用户将向这个正在运行的干净操作系统提供他的密码。然后,这个新的、正在运行的干净操作系统将继续检查每个系统文件的MAC。
MAC安全的事实意味着,可怜的病毒实际上无法创建一个具有有效标签的新文件(我们称之为F‘)。它无法创建(F‘, T‘),因为如果它能,那将是这个MAC上的存在性伪造。由于MAC是存在性不可伪造的,病毒无法创建任何F‘,无论F‘是什么。因此,由于MAC的安全性,用户将检测到所有被病毒修改的文件。
这里有一个需要注意的地方:病毒可以做的一件事是交换两个文件。例如,它可以交换文件F1和文件F2的位置。当用户尝试运行文件F1时,实际上会运行文件F2,这当然会造成各种损害。防御这种情况的方法本质上是在MAC计算区域中包含文件名。实际上,我们计算的是文件名和文件内容的完整性校验。因此,如果病毒试图交换两个文件,系统会说:“位于F1位置的文件没有正确的名称”,从而检测到病毒进行了交换,即使MAC验证通过。
重要的是要记住,MAC可以帮助您防御文件篡改或一般的数据篡改,但它们无助于防御已验证数据的交换,这必须通过其他方式来完成。
总结

本节课我们一起学习了消息认证码的基本概念。我们了解到,MAC是一种在不提供机密性的情况下确保消息完整性的机制,它依赖于共享密钥。我们定义了MAC的安全性,要求其能够抵抗选择消息攻击下的存在性伪造。我们还探讨了MAC的一个实际应用——保护系统文件免受恶意篡改。在下一节中,我们将开始构建第一个安全的MAC实例。
025:基于PRF的MAC构建 🛡️

在本节课中,我们将学习如何基于伪随机函数构建一个安全的消息认证码。我们将从定义回顾开始,逐步构建第一个安全的MAC,并探讨如何将适用于短消息的MAC扩展为适用于长消息的MAC。

回顾MAC的定义
上一节我们介绍了消息认证码的基本概念。MAC由一对算法构成:
- 签名算法:接收一个消息
M和一个密钥K,生成一个对应的标签T。
T = Sign(K, M) - 验证算法:接收密钥
K、消息M和标签T,输出0或1,表示标签是否有效。
Verify(K, M, T) -> {0, 1}
一个MAC是安全的,当它在选择消息攻击下具有存在不可伪造性。这意味着攻击者可以提交任意消息并获得对应的标签,但即便如此,他也无法伪造出一个新的、未被查询过的有效消息-标签对。
从PRF构建安全MAC

既然我们了解了MAC的定义,本节中我们来看看如何构建第一个安全的MAC。一个直接的方法是使用伪随机函数。
假设我们有一个伪随机函数 F,它将输入空间 X 映射到输出空间 Y。我们可以基于它定义如下MAC:
- 签名:对消息
M的标签就是函数在M点的取值。
Tag = F(K, M) - 验证:重新计算
F(K, M),并检查结果是否与收到的标签T相等。
Verify: F(K, M) == T ?
在通信中,发送方Alice计算标签并附加到消息后发送。接收方Bob收到后,重新计算并比对标签以验证完整性。
一个不安全的例子
让我们看一个基于PRF构建MAC的不安全例子。假设我们使用的PRF输出长度很短,例如只有10比特。虽然它本身可能是一个安全的PRF,但用它构建的MAC是不安全的。


考虑一个简单的攻击者:他任意选择一个消息 M,然后随机猜测其10比特的标签。由于标签空间只有 2^10 = 1024 种可能,他猜中的概率是 1/1024,这是一个不可忽略的优势。因此,这个MAC不安全。
这个例子揭示了一个关键点:只有当PRF的输出空间足够大时,由此构建的MAC才是安全的。
安全定理
以下是基于PRF构建MAC的安全定理:
如果 F: K × X -> Y 是一个安全的伪随机函数,那么由它派生的MAC也是安全的。具体来说,任何攻击者在选择消息攻击下成功伪造标签的优势,至多为:
Adv_MAC ≤ Adv_PRF + 1/|Y|
其中 1/|Y| 是攻击者纯粹靠猜测成功的概率。
因此,为了确保MAC安全,我们需要PRF的输出空间 Y 足够大,使得 1/|Y| 可忽略。例如,使用输出80比特的PRF,攻击者的优势最多为 1/2^80。
证明思路:
- 首先考虑一个真正的随机函数
R。攻击者查询了q个不同消息的标签后,对于一个全新的消息M,函数值R(M)与之前所有查询结果独立。因此,攻击者最佳策略是随机猜测,成功概率为1/|Y|。 - 由于
F是伪随机函数,攻击者无法区分F和真正的随机函数R。因此,即使使用F,攻击者的优势也不会显著高于使用R时的优势1/|Y|。
我们的第一个MAC实例:基于AES
现在我们知道任何安全的PRF都能产生安全的MAC。我们立即得到了第一个MAC实例:AES。
我们相信AES是一个安全的伪随机置换,因此它也是一个安全的PRF(在固定密钥下)。AES的输入块大小是16字节(128比特)。因此,基于AES构建的MAC可以直接用于认证恰好16字节长的消息。
Tag = AES(K, M) // 其中 len(M) = 16 bytes
从小MAC构建大MAC:扩展问题
然而,AES只能处理固定16字节的输入。在实际应用中,我们需要认证的数据可能是任意长度的,比如几GB的文件。这就引出了“从小MAC构建大MAC”的问题,或者形象地称为“巨无霸问题”。

我们需要一种构造方法,能够以一个处理短消息的安全PRF(如AES)为基础,构建出一个能处理任意长消息的安全MAC。
以下是两种广泛使用的构造方法,它们都从短输入PRF出发,生成支持极长消息的PRF(进而得到MAC):
- CBC-MAC:常用于银行金融系统(如自动清算所ACH)。
- HMAC:广泛应用于网络协议(如SSL/TLS、SSH)中保证数据完整性。
这两种构造都可以用AES作为底层的密码原语来实例化。
关于输出截断的说明

最后,我们讨论一个关于基于PRF的MAC的实用技巧:输出截断。
假设我们有一个输出 n 比特的安全PRF(例如AES输出128比特)。一个简单的引理表明,如果我们只输出前 t 比特(t < n),结果仍然是一个安全的PRF。直观上,如果完整的 n 比特输出看起来是随机的,那么它的一个子集(t 比特)看起来也同样是随机的,攻击者甚至获得了更少的信息。
由于安全的PRF能产生安全的MAC,这意味着我们可以安全地截断MAC的标签长度。但必须注意,根据安全定理,截断后的标签长度 t 必须足够大,使得 1/2^t 可忽略(例如 t=80 或 t=64)。如果截断到只有几位,MAC将不再安全。
因此,即使我们使用AES构建出输出128比特标签的MAC,在实践中也可以安全地将其截断为更短的长度(如80比特),以减少通信开销,同时保持安全性。
总结
本节课中我们一起学习了基于伪随机函数构建消息认证码的核心方法:
- 任何输出空间足够大的安全PRF,都可以直接用作一个安全的MAC。
- 这为我们提供了第一个具体的MAC实例——基于AES的MAC,但它只能处理固定长度的短消息。
- 为了认证长消息,我们需要扩展构造,如CBC-MAC和HMAC,它们能以短输入PRF为基础构建出长输入MAC。
- 基于PRF的MAC其输出标签可以被安全地截断,以在安全性和效率之间取得平衡。


在下一节中,我们将深入探讨第一种扩展构造:CBC-MAC 的工作原理。
026:CBC-MAC与NMAC 🔐

在本节课中,我们将学习如何从安全的伪随机函数(PRF)构造两种经典的消息认证码(MAC):加密CBC-MAC(ECBC)和嵌套MAC(NMAC)。我们将详细探讨它们的构造原理、安全性,以及为什么某些步骤对于安全至关重要。

概述 📋
上一节我们介绍了如何使用安全的伪随机函数(PRF)来构造一个安全的MAC。具体来说,对于一个消息M,其签名可以简单地定义为函数在点M处的值。唯一的注意事项是PRF F的输出必须足够大(例如80位或128位),这样才能生成一个安全的MAC。
然而,像AES这样的PRF只能处理固定长度的短消息(例如16字节)。本节的核心问题是:给定一个用于短消息的PRF(如AES),我们能否构造一个可以处理可能长达数吉字节的长消息的PRF?
为了便于讨论,我们用 X 表示集合 {0,1}^n,其中n是底层PRF的分组大小。由于我们通常将AES视为底层PRF,因此可以认为n为128位。
加密CBC-MAC(ECBC)的构造 🧱
我们的第一个构造称为加密CBC-MAC,简称ECBC。ECBC使用一个PRF F: X -> X。我们的目标是构建一个PRF F_ECBC,它接受一对密钥和任意长度的消息(最多L个分组),并输出一个位于X中的标签。
X^(≤L) 表示输入消息可以包含1到L个之间的任意数量的分组。因此,ECBC可以处理长度为1个分组、2个分组、10个分组甚至100个分组的消息。


以下是ECBC的工作原理:
- 首先,将消息M分割成多个分组,每个分组的长度与底层函数F的分组长度相同。
- 然后,我们运行CBC链,但不输出中间值。具体过程是:用密钥K加密第一个分组,将结果与第二个分组进行异或运算,再用K加密该结果,以此类推,直到处理完所有分组。
- 最后,我们得到一个称为CBC链输出的值。
- 关键步骤:我们使用一个独立的密钥K1(与K不同且独立选择)对这个CBC链输出再进行一次加密。最终的结果就是我们的标签T。
F_ECBC 接受一对密钥 (K, K1) 作为输入,可以处理可变长度的消息,并产生一个在集合X中的输出。

你可能会问,最后这个加密步骤是做什么用的?如果不进行这最后一步加密,得到的函数称为 原始CBC函数。我们稍后会看到,原始CBC实际上并不是一个安全的MAC。因此,这最后一步对于确保MAC的安全性至关重要。
嵌套MAC(NMAC)的构造 🔄

另一种将小型PRF转换为大型PRF的经典构造称为NMAC。NMAC从一个PRF F: X -> K 开始,注意其输出位于密钥空间K中(而CBC-MAC的输出在集合X中)。
我们的目标是构建一个PRF F_NMAC,它同样接受一对密钥作为输入,可以处理最多L个分组长的可变长度消息,并输出一个密钥空间K中的元素。

以下是NMAC的工作原理:
- 同样,将消息M分割成多个分组。
- 我们取密钥K,将其作为密钥输入到函数F中,第一个消息分组作为数据输入。输出结果成为下一个分组的密钥。
- 这个新的密钥用于下一次PRF评估,数据是下一个消息分组,以此类推,直到处理完所有消息分组。
- 最终输出是一个位于密钥空间K中的元素T。
- 如果在此停止,我们得到的函数称为 级联函数。然而,仅凭级联函数并不能构成安全的MAC。
- 为了获得安全的MAC,我们需要将这个位于K中的元素T映射到集合X中。通常,NMAC与那些分组长度X远大于密钥长度K的PRF一起使用。
- 我们简单地将一个固定的填充
pad附加到标签T后面,使其成为一个X中的元素。 - 关键步骤:然后,我们使用一个独立的密钥K1对这个块进行最后一次加密。最终输出的标签是K中的一个元素,这就是NMAC的输出。
同样,没有最后加密步骤的函数称为级联函数,而包含最后加密步骤(这对安全性是必要的)的函数才是一个可以处理可变长度消息的PRF。
为什么需要最后的加密步骤? ⚠️

在分析这些MAC构造的安全性之前,我们需要更好地理解最后加密步骤的作用。
NMAC中的级联函数不安全
我们首先来看NMAC。如果省略最后一步加密,即只使用级联函数,那么这个MAC是完全不安全的。



假设我们定义了一个MAC,它直接输出应用于消息M的级联函数值(没有最后加密步骤)。那么,给定一个选择消息查询的输出(即级联函数在M上的值),攻击者可以推导出该函数在消息 M || W(M与任意消息W连接)上的值。
这是因为攻击者可以将级联函数的输出值T作为下一个F函数的输入,并将W作为数据输入,计算出 M || W 的标签T‘。这样,攻击者通过请求一个消息的标签,就可以伪造出另一个更长消息的标签,从而实现了存在性伪造。
这种攻击被称为 扩展攻击。级联函数容易受到这种攻击,而最后一步的加密可以防止此类扩展攻击。

ECBC中的原始CBC函数不安全
接下来,我们看看为什么ECBC也需要额外的加密步骤。同样,我们定义一个使用原始CBC(即没有最后加密步骤的CBC-MAC)的MAC,并展示它也是不安全的。
假设攻击者请求一个单分组消息M的标签。对于原始CBC,这仅仅是 F_K(M),我们称结果为T。

现在,攻击者可以构造一个两分组消息 M' = (M, T ⊕ M)。我们可以验证,之前得到的T同样是这个消息 M' 的有效标签。
验证过程如下:
- 应用原始CBC到
M':首先加密第一个分组M,得到F_K(M) = T。 - 将结果T与第二个分组
T ⊕ M进行异或:T ⊕ (T ⊕ M) = M。 - 然后对M再次应用F:
F_K(M) = T。
因此,T确实是M'的有效MAC。攻击者从未查询过两分组的M',却成功伪造了其标签,从而攻破了MAC。
这个例子表明,如果不包含最后的加密步骤,MAC将因为此类攻击而不安全。值得注意的是,许多实际产品和甚至某些标准都错误地省略了这一步,导致MAC不安全。

ECBC与NMAC的安全定理 📜
现在,我们来看ECBC和NMAC的安全定理。对于任意我们想要应用MAC的消息长度,定理的表述是类似的:对于每个PRF攻击者A,都存在一个高效的攻击者B。
需要关注的是误差项。对于ECBC,分析实际上利用了F是一个伪随机置换(PRP)的事实,即使ECBC的计算过程中从未需要求逆F。对于NMAC,底层的PRF不需要是可逆的。
这些误差项基本上表明,只要一个密钥没有被用来对超过 √|X|(对于ECBC)或 √|K|(对于NMAC)条消息进行MAC计算,这些MAC就是安全的。

安全界限示例
让我们以CBC-MAC的安全定理为例。假设我们的目标是确保对于所有攻击者,其区分PRF与真随机函数的优势小于 1/2^32。
根据安全定理,我们需要确保 Q^2 / |X| < 1/2^32(这里为简化忽略了常数因子)。对于AES,|X| = 2^128。解不等式可得 Q < 2^48。这意味着在使用AES时,最多只能对 2^48 条消息使用同一个密钥进行MAC计算,之后必须更换密钥,否则将无法达到安全目标。

相比之下,如果使用分组长度为64位的三重DES,则 |X| = 2^64,计算可得 Q < 2^16,即每处理约65000条消息后就必须更换密钥。这说明了AES使用更大分组长度的优势之一:在这些模式下能保持安全,且无需像使用三重DES那样频繁更换密钥。
现实攻击与生日悖论 🎂

上述安全定理中的界限并非空谈,确实存在与之对应的现实攻击。在签署了大约 √|X|(对于ECBC)或 √|K|(对于NMAC)条消息后,MAC确实会变得不安全。

这两种构造都有一个 扩展属性:如果两条不同的消息X和Y产生了碰撞(即相同的MAC标签),那么对于任意附加块W,扩展后的消息 X || W 和 Y || W 也会产生碰撞。

基于此,可以构造如下攻击:
- 攻击者发出大约
√|Y|条选择消息查询(对于AES,Y可以是输出空间,大小2^128,所以约2^64条查询)。 - 根据 生日悖论,在这
2^64个随机生成的标签中,有很大概率找到两个来自不同消息M和M'的相同标签T。 - 一旦找到碰撞,攻击者可以任意选择一个块W,并请求消息
M || W的标签。 - 由于M和M‘的标签相同,根据扩展属性,
M || W和M' || W的标签也必然相同。因此,攻击者获得的M || W的标签,同样也是他从未查询过的M' || W的有效标签,从而完成了存在性伪造。
这个攻击表明,在大约 √|Y| 次查询后,攻击者就能以相当高的概率伪造MAC。这印证了安全定理中的界限是真实存在的。
总结与对比 📊
本节课我们一起学习了两种从短PRF构造长消息MAC的方法:ECBC和NMAC。

- ECBC 是一个非常常用的MAC,基于AES等分组密码构建。例如,802.11i标准就使用基于AES的ECBC来保证完整性。
- NMAC 通常不直接与AES这类分组密码使用,因为它的构造要求每个分组都更换密钥,而AES的密钥扩展计算开销较大,不适合频繁更换密钥。然而,NMAC是另一个非常流行的MAC——HMAC 的基础,我们将在下一节中看到这一点。

关键要点是,这两种构造都需要一个 最后的、使用独立密钥的加密步骤 来防止扩展攻击,从而确保安全性。同时,它们的安全使用都受到 √|X| 或 √|K| 的数量级限制,这意味着在使用像AES这样的算法时,需要注意单个密钥的MAC计算数量上限。

在下一节中,我们将继续探讨更多的MAC构造。
027:MAC填充方案
在本节课中,我们将要学习当消息长度不是分组密码块大小的整数倍时,如何安全地计算MAC。我们将探讨几种填充方案,分析其安全性,并介绍一种现代标准。
概述
上一节我们介绍了CBC-MAC和N-MAC,但始终假设消息长度是块大小的整数倍。本节中,我们来看看当消息长度不满足此条件时该如何处理。
消息填充的必要性


回忆一下,加密的CBC-MAC(简称ECBC-MAC)使用伪随机置换F来计算CBC函数。但之前我们假设消息本身可以被分解为整数个分组密码块。问题是,当消息长度不是块大小的整数倍时,我们该怎么办?
这里有一条消息,其最后一个块实际上比完整块要短。问题是如何在这种情况下计算ECBC-MAC。
简单的零填充方案及其缺陷
解决方案当然是填充消息。首先想到的方法是简单地用零填充消息。换句话说,我们取最后一个块,并向其添加零,直到最后一个块的长度达到一个完整块的大小。


我的问题是,由此产生的MAC是否安全?答案是否定的,MAC不安全。让我解释一下原因。
问题在于,现在可以构造出消息M和消息M||0,使得它们恰好具有相同的填充结果。因此,一旦我们将M和M||0输入ECBC,就会得到相同的标签。这意味着M和M||0具有相同的标签,因此攻击者可以发起存在性伪造攻击。他会请求消息M的标签,然后将其作为消息M||0的标签进行伪造输出。
很容易理解为什么会这样。具体来说,我们有一条消息M,填充后变为M||000(假设我们添加了三个0)。这里我们有消息M||0,它以0结尾,填充后我们基本上只需要再添加两个0。结果它们变成了相同的填充,因此将具有完全相同的标签,这允许对手发起存在性伪造攻击。
所以这不是一个好主意。事实上,附加全零是一个糟糕的主意。如果你考虑一个具体的应用场景,例如用于清算支票的自动清算所系统,我可能有一张100美元的支票及其标签。现在攻击者基本上可以在我的支票上附加一个零,使其变成1000美元的支票,而这实际上不会改变标签。因此,这种在不改变标签的情况下扩展消息的能力可能会带来灾难性的后果。
填充函数的核心要求
我希望这个例子能让你相信,填充函数本身必须是一个单射函数。换句话说,两条不同的消息应始终映射到两条不同的填充后消息。填充函数不应发生碰撞。另一种说法是填充函数必须是可逆的,这保证了填充函数是单射的。
ISO标准填充方案
国际标准化组织(ISO)提出了一种标准方法。他们建议在消息末尾附加字符串“100...00”,使消息长度成为块长度的整数倍。
为了说明这种填充是可逆的,我们只需描述其逆算法。该算法从右向左扫描消息,直到遇到第一个“1”,然后移除该“1”及其右侧的所有比特。
你会看到,一旦我们以这种方式移除填充,就得到了原始消息。以下是一个例子。
这里有一条消息,其最后一个块比块长度短,然后我们附加“100”字符串。很容易看出填充是什么:只需从右边开始寻找第一个“1”,我们可以移除这个填充并恢复原始消息。
处理边界情况:消息长度恰好为块大小的整数倍
现在有一个非常重要的边界情况:如果原始消息长度已经是块大小的整数倍,我们该怎么办?
在这种情况下,我们必须添加一个包含填充“100...0”的额外虚拟块。我无法告诉你有多少产品和标准实际上犯了这个错误,他们没有添加虚拟块,结果导致MAC不安全,因为存在简单的存在性伪造攻击。让我告诉你为什么。
假设消息长度是块长度的整数倍,我们没有添加虚拟块,而是直接对这里的消息进行MAC计算。现在的结果是,如果你看这条消息(它是块大小的整数倍)和另一条消息(它不是块大小的整数倍但被填充到块大小),并且想象后一条消息M‘1恰好以“100”结尾。
此时你会意识到,这里的原始消息(让我这样画)在填充后会变得与第二条根本没有被填充的消息相同。因此,如果我请求这边消息的标签,我也会得到恰好以“100”结尾的第二条消息的标签。
所以,如果我们没有添加虚拟块,填充函数将再次变得不可逆,因为两条不同的消息碰巧映射到相同的填充结果。结果,MAC变得不安全。
总结一下,ISO标准是一种非常好的填充方式,但你必须记住,如果消息一开始就是块长度的整数倍,也要添加一个虚拟块。
避免虚拟块:CMAC方案
你们中的一些人可能想知道是否存在一种永远不需要添加虚拟块的填充方案。答案是,如果你看确定性填充函数,那么很容易论证总会有需要填充的情况。原因很简单,长度是块长度整数倍的消息数量远小于不需要是块长度整数倍的消息总数。因此,我们无法从这个更大的所有消息集合到那个较小的、长度是块长度整数倍的消息集合建立一个单射函数。总会有需要扩展原始消息的情况,在这种情况下,就对应于添加这个虚拟填充块。

然而,有一个非常巧妙的想法叫做CMAC,它表明使用随机化填充函数可以避免添加虚拟块。让我解释一下CMAC是如何工作的。
CMAC实际上使用三个密钥,有时这被称为三密钥构造。第一个密钥K用于标准的CBC-MAC算法。然后密钥K1和K2仅用于最后一个块的填充方案。事实上,在CMAC标准中,密钥K1和K2是通过某种伪随机生成器从密钥K派生出来的。

CMAC的工作方式如下:如果消息长度不是块长度的整数倍,那么我们向其附加ISO填充,但随后我们还用对手不知道的密钥K1对这个最后一个块进行异或操作。
然而,如果消息长度是块长度的整数倍,那么我们当然不附加任何东西,但我们用一个不同的密钥K2进行异或,同样,对手实际上并不知道这个密钥。
事实证明,仅仅通过这样做,现在就不可能对我们能在级联函数和原始CBC上进行的扩展攻击了,因为可怜的对手实际上不知道进入函数的最后一个块是什么,他不知道K1,因此他不知道这个特定点的值。结果,他无法进行扩展攻击。事实上,这是一个可证明的陈述,即这里的这个构造(仅仅通过异或K1或异或K2)确实是一个伪随机函数,尽管在计算原始CBC函数后没有进行最终的加密步骤。
这是一个好处:没有最终的加密步骤。第二个好处是,我们通过使用两个不同的密钥来区分消息长度是块长度的整数倍与消息长度不是整数倍但有填充附加的情况,从而解决了填充是否发生的歧义。这两个不同的密钥解决了这两种情况之间的歧义,因此这种填充实际上是足够安全的。正如我所说,实际上有一个很好的安全定理与CMAC相关,它表明CM构造确实是一个具有与CBC-MAC相同安全属性的伪随机函数。
CMAC的应用与标准
我想提一下,CMAC是由NIST标准化的联邦标准。如果你现在想在任何地方使用CBC-MAC,你实际上会使用CMAC作为标准方式,特别是在CMAC中,底层的分组密码是AES,这为我们提供了一个从AES派生的安全CBC-MAC。
总结
本节课中我们一起学习了MAC的填充方案。我们首先看到简单的零填充是不安全的,因为它允许存在性伪造攻击。接着,我们学习了ISO标准填充方案,它要求填充函数是可逆的单射函数,并特别注意了消息长度恰好为块大小整数倍时需要添加虚拟块的边界情况。最后,我们介绍了更先进的CMAC方案,它通过使用额外的密钥进行随机化异或操作,巧妙地避免了添加虚拟块的需要,并提供了可证明的安全性。CMAC已成为现代应用中的标准方法。
028:PMAC与卡特-韦格曼MAC
在本节课中,我们将要学习两种重要的消息认证码(MAC)构造方法:PMAC和卡特-韦格曼MAC。我们将了解PMAC如何实现并行化处理,以及如何将一次性安全的MAC转换为多次安全的MAC。
PMAC:一种并行MAC

在前两节中,我们讨论了CBC-MAC和NMAC,它们能将适用于短消息的伪随机函数(PRF)转换为适用于长消息的PRF。这两种构造本质上是顺序的,即使有多个处理器,也无法加快处理速度。在本节中,我们将探讨一种并行MAC,它同样能将短PRF转换为长PRF,但能以高度并行的方式实现。具体来说,我们将研究一种名为PMAC的并行MAC构造。
PMAC使用一个底层的PRF来构造一个能处理更长消息的PRF。这个PRF可以处理长度可变、最多包含L个分块的消息。
其构造工作原理如下。我们获取消息并将其分割成多个分块。然后,我们独立地处理每个分块。
以下是PMAC的构造步骤:
- 首先,我们对每个消息分块应用一个函数P,并将结果与第一个消息分块进行异或运算。
- 然后,我们使用密钥K1应用我们的函数F。
- 我们对每个消息分块重复此过程。请注意,所有这些处理都可以并行进行,所有消息分块都是独立处理的。
- 最后,我们将所有这些结果收集到一个最终的异或运算中,然后再进行一次加密以得到最终的标签值。
出于技术原因,实际上在最后一个分块上,我们不需要应用PRF F。但正如我所说,这只是技术细节,我们暂时忽略它。

现在,我想解释一下函数P的作用和目的。
假设函数P不存在,也就是说,我们直接将每个消息分块输入到PRF中,而不进行任何其他处理。那么,我断言由此产生的MAC是完全不安全的。
原因在于,消息分块之间没有强制顺序。具体来说,如果我交换两个消息分块,由于异或运算满足交换律,最终标签的值不会改变。因此,攻击者可以请求特定消息的标签,然后他就能获得交换了两个分块的消息的标签,这构成了一种存在性伪造攻击。
函数P试图做的是在这些分块上强制施加顺序。请注意,该函数首先是一个密钥函数,因此它接收一个密钥作为输入。其次,更重要的是,它接收一个分块编号作为输入。换句话说,该函数的值对于每个分块都是不同的,而这正是防止这种分块交换攻击的原因。

函数P实际上是一个非常容易计算的函数。给定密钥和消息分块,它本质上只是一个有限域上的乘法运算,因此计算起来非常简单。它为PMAC的运行时间增加的开销非常小,但足以确保PMAC实际上是安全的。
如前所述,PMAC的密钥是这对密钥:一个用于PRF,一个用于这个掩码函数P。最后,我要说明,如果消息长度不是分块长度的整数倍,即最后一个分块短于完整的分块长度,那么PMAC实际上会使用类似于CBC-MAC的填充方式,这样就永远不需要额外的虚拟分块。
这就是PMAC的构造。和往常一样,我们可以陈述其安全定理。安全定理现在你应该很熟悉了,它本质上说:如果你给我一个攻击PMAC的敌手,我可以构造一个攻击底层PRF的敌手,再加上一个额外的误差项。由于PRF是安全的,我们知道这一项是可忽略的。因此,如果我们希望这一项是可忽略的,我们需要这个误差项也是可忽略的。这里,Q是使用特定密钥进行MAC计算的消息数量,L是所有那些消息的最大长度。只要这个乘积小于分块大小的平方根,PMAC就是安全的。对于AES,分块大小是2128,其平方根是264,因此只要Q乘以L小于2^64,MAC就是安全的。每当接近这个值时,当然需要更换密钥才能继续对更多消息进行MAC计算。主要要记住的是,PMAC也为我们提供了一个PRF,并且它独立地处理消息分块。
事实证明,PMAC还有一个非常有趣的特性,即PMAC是增量的。让我解释一下这意味着什么。
假设用于构造PMAC的函数F不仅仅是一个PRF,实际上是一个置换,一个PRP,因此我们可以在需要时对其进行求逆。现在,假设我们已经为一个特别长的消息M计算了MAC。现在假设这个长消息中只有一个消息分块发生了变化。这里,M1变成了M‘1,但其余的消息分块都保持不变。对于像CBC-MAC这样的其他MAC,即使只有一个消息分块发生变化,你也必须重新计算整个消息的标签,重新计算标签基本上需要与消息长度成正比的时间。
事实证明,对于PMAC,如果我们只改变一个分块或少数几个分块,实际上我们可以非常、非常快地重新计算新消息的标签值。让我给你出个谜题,看看你是否能自己弄清楚如何做到这一点。记住函数F是一个PRP,因此是可逆的。让我们看看你是否能自己弄清楚如何计算新消息的MAC。
事实证明,这是可以做到的,你可以使用这里的第三行快速重新计算新消息的标签。为了确保我们都看到解决方案,让我们快速回到PMAC的原始图表,我可以向你展示我的意思。想象一下,这个消息分块变成了另一个分块,比如说变成了M‘1。

那么我们可以做的是,我们可以获取更改前原始消息的标签,然后我们可以对函数F求逆,以确定应用函数F之前的值。现在,由于我们现在有一组分块的异或结果,我们可以做的是,通过将来自原始消息分块的值异或到这个异或累加器中,然后再将来自新消息分块的值异或回来,从而抵消来自原始消息分块的异或贡献。然后再次应用函数F,这将为我们提供仅更改了一个分块的新消息的标签。用符号表示,我基本上在这里写了公式。你可以看到,我们解密标签,然后与来自原始消息分块的块进行异或,再与来自新消息分块的块进行异或,然后我们重新加密最终的异或累加器,以得到更改了一个分块的消息的新标签。
这是一个相当简洁的特性,它表明如果你有非常大的消息,你可以非常快速地更新标签。当然,你需要一个密钥来进行更新,但如果只有少数消息分块发生变化,你可以快速更新标签。
一次性MAC
好了,关于PMAC的讨论到此结束。现在我想稍微转换一下话题,谈谈一次性MAC的概念,这基本上是一次性密码本在完整性世界中的类比。

让我解释一下我的意思。想象一下,我们想要构建一个仅用于单条消息完整性的MAC。换句话说,每次我们计算特定消息的完整性时,我们也会更改密钥,使得任何特定密钥仅用于一条消息的完整性。
然后,我们可以将安全游戏定义为:攻击者将看到一条消息。因此,我们只允许他进行一次选择消息攻击。他可以提交一条消息查询,并获得对应于该消息查询的标签。现在,他的目标是伪造一个消息-标签对。你可以看到,他的目标是产生一个能通过验证且与他实际收到的消息-标签对不同的消息-标签对。正如我们将看到的,就像一次性密码本和流密码在某些应用中非常有用一样,一次性MAC在那些我们只想使用一个密钥来加密或为单条消息提供完整性的应用中也非常有用。
和往常一样,我们会说,如果基本上没有敌手能赢得这个游戏,那么一次性MAC就是安全的。
现在有趣的是,一次性MAC,就像一次性密码本一样,可以抵抗无限强大的敌手。不仅如此,因为它们被设计为仅一次性使用安全,所以它们实际上可以比基于PRF的MAC更快。
因此,我只想给你一个一次性MAC的简单例子。这是一个经典的一次性MAC构造,让我向你展示它是如何工作的。
第一步是选择一个比我们的分块大小稍大的素数。在这个例子中,我们将使用128位分块,所以让我们选择第一个大于2128的素数,这恰好是2128 + 51。
现在,密钥将是一对在1到我们的素数Q范围内的随机数。所以我们在1到Q的范围内选择两个随机整数。现在我们收到一条消息,我们将把消息分成多个分块,每个分块是128位,我们将每个数字视为0到2^128 - 1范围内的整数。
MAC定义如下:我们首先获取消息分块,并从中构造一个多项式。如果我们的消息中有L个分块,我们将构造一个L次多项式。请注意,这个多项式的常数项被设置为0。
然后我们非常简单地定义MAC:基本上,我们取对应于消息的多项式,在点k(这是我们秘密密钥的一半)处求值,然后加上值A(这是我们秘密密钥的另一半)。就是这样,这就是整个MAC。基本上就是构造对应于我们消息的多项式,在秘密密钥的一半处求值该多项式,并将秘密密钥的另一半加到结果中,当然最后要对结果模Q取余。
好了,就是这样,这就是整个MAC。它是一个一次性安全的MAC。我们论证这个MAC是一次性安全的方式,本质上是通过论证:如果我告诉你一个特定消息的MAC值,这完全不会告诉你另一个消息的MAC值。因此,即使你已经看到了特定消息的MAC值,你也无法伪造其他消息的MAC。
现在我应该强调,这是一个一次性MAC,但不是两次安全的。换句话说,如果你看到两个不同消息的MAC值,那实际上会完全泄露秘密密钥,你实际上可以预测你选择的第三条或第四条消息的MAC。因此,MAC就变得可伪造了,但对于一次性使用,它是一个完全安全的MAC。我还要告诉你,实际上这是一个计算速度非常快的MAC。
卡特-韦格曼MAC
现在我们已经构造了一次性MAC,事实证明,有一种通用技术可以将一次性MAC转换为多次MAC。我只想简要地向你展示这是如何工作的。

假设我们有一个一次性MAC,我们称其为S和V,分别代表签名和验证算法。我们假设标签本身是比特串。
此外,让我们也考虑一个PRF,一个安全的伪随机函数,它也输出比特串,但也接收比特串作为输入。
现在让我们定义一个MAC的通用构造。这些MAC被称为卡特-韦格曼MAC,其工作原理如下:基本上,我们将一次性MAC应用于消息M,然后我们将使用PRF加密结果。我们如何加密结果呢?我们选择一个随机的R,然后通过将PRF应用于R来计算一种一次性密码本,然后我们将其与实际的一次性MAC结果进行异或运算。
这个构造的巧妙之处在于,快速的一次性MAC应用于可能是千兆字节长的长消息,而较慢的PRF仅应用于这个随机数R,然后R被用来加密MAC的最终结果。你可以论证,如果作为构建块提供给我们的MAC是一次性安全的MAC,并且PRF是安全的,那么实际上我们得到一个多次安全的MAC,它恰好输出两个n位的标签。我们稍后在讨论认证加密时会看到卡特-韦格曼MAC,实际上,进行带完整性加密的NIST标准方法之一,就是使用卡特-韦格曼MAC来提供完整性。

我想提一下,这个卡特-韦格曼MAC是随机化MAC的一个很好的例子,其中这个随机数R在每次计算标签时都是重新选择的。因此,例如,如果你尝试为同一条消息计算两次标签,每次你都会选择一个不同的R,结果你会得到不同的标签。所以这是一个很好的例子,说明这个MAC实际上不是一个伪随机函数,不是一个PRF,因为一条消息实际上可能映射到许多不同的标签,所有这些标签对于那条消息都是有效的。
为了结束我们对卡特-韦格曼MAC的讨论,让我问你以下问题。这里我们有卡特-韦格曼MAC的方程。和往常一样,你看到随机数R是MAC的一部分,MAC的第二部分我将表示为T。这基本上是将一次性MAC应用于消息M,然后使用应用于随机数R的伪随机函数进行加密。我们将这个异或运算的结果表示为T。那么我的问题是:给定特定消息M的卡特-韦格曼MAC对(R, T),你将如何验证这个MAC是有效的?回想一下,这里的算法V是底层一次性MAC的验证算法。
这是正确的答案。要理解原因,只需观察到这里的异或运算将量T解密为其明文值,这基本上是原始的底层一次性MAC。因此,我们可以直接将其输入到一次性MAC的验证算法中。
总结与展望
我想告诉你的最后一种MAC是在互联网协议中非常流行的一种,它叫做HMAC。但在我们讨论HMAC之前,我们必须讨论哈希函数,特别是抗碰撞哈希函数,我们将在下一个模块中讨论这些。
这是我们关于MAC的第一个模块的结束。我想指出,构建我们所看到的所有MAC背后有非常漂亮的理论。我大致向你展示了主要的构造,但构建这些MAC确实涉及相当多的理论。如果你想了解更多,我列出了一些你可能想查阅的关键论文。让我快速告诉你它们是什么:第一篇是所谓的“三密钥构造”,它是CMAC的基础,这是一篇非常优雅的论文,给出了基于CBC-MAC的高效构造。第二篇论文是一篇技术性更强的论文,但基本上展示了如何证明CBC-MAC作为PRF的界限。第三篇论文讨论了PMAC及其构造,同样是一篇非常敏锐的论文。第四篇论文讨论了NMAC和HMAC的安全性(HMAC我们将在下一个模块中介绍)。我列出的最后一篇论文提出了一个有趣的问题。回想一下,在我们所有的构造中,我们总是假设AES是一个适用于16字节消息的伪随机函数,然后我们构建了一个适用于更长消息的伪随机函数,从而构建了MAC。这篇论文说,好吧,如果AES不是一个伪随机函数,但仍然满足一些较弱的安全属性,称为不可预测函数,那该怎么办?然后他们问,如果AES只是一个不可预测函数而不是伪随机函数,我们还能为长消息构建MAC吗?他们成功地仅基于AES是不可预测函数这一较弱假设给出了构造,但他们的构造远不如我们在这些节中讨论的CBC-MAC、NMAC或PMAC高效。因此,如果你对如何从像AES这样的分组密码构建MAC有不同的视角感兴趣,请看看这篇论文。实际上,在这个领域还有一些很好的开放性问题值得研究。

本节课中我们一起学习了PMAC和卡特-韦格曼MAC。我们了解了PMAC如何实现并行处理,以及如何将一次性安全的MAC转换为多次安全的MAC。这结束了我们关于完整性的第一部分内容,在下一部分中,我们将讨论抗碰撞性。
029:碰撞抵抗与消息完整性
在本节课中,我们将学习一个称为“碰撞抵抗”的新概念,它在提供消息完整性方面扮演着重要角色。我们的最终目标是描述一种在互联网协议中广泛使用的流行MAC算法——Hmac。Hmac本身是基于碰撞抵抗的哈希函数构建的。在开始之前,我们先快速回顾一下之前的内容。

上一节我们讨论了消息完整性,并指出一个MAC系统是安全的,当且仅当它在选择消息攻击下是存在性不可伪造的。这意味着,即使攻击者获得了任意选择消息的标签,也无法为某个新消息构造出有效的标签。
我们证明了任何安全的伪随机函数都能立即为我们提供一个安全的MAC。因此,我们转而探讨如何构建能够处理大消息输入的安全PRF。
我们研究了四种构造方法。第一种构造基于CBC模式,我们看了它的两种变体:加密CBC和CMMAC。这些方法通常与AES一起使用,例如在802.11i标准中,CBC-MAC就与AES算法结合用于消息完整性。
我们还研究了另一种称为NMac的模式,它也能将处理短输入的PRF转换为能处理极大消息输入的PRF。这两种都是顺序型MAC。
接着,我们看了一种可并行化的MAC,称为Pmac。它同样能将处理小输入的PRF转换为处理大输入的PRF,但以并行方式进行。因此,在多处理器系统上,Pmac比CBC-MAC更高效。以上三种方法都是通过为大消息构建PRF来构建MAC。

最后,我们研究了Carter-Wegman MAC,它实际上不是一个PRF,而是一个随机化MAC。因此,单个消息可以有许多不同的有效标签。Carter-Wegman MAC首先使用快速的一次性MAC将大消息哈希成一个小标签,然后使用PRF加密该标签。其优势在于,大消息的哈希是使用快速的一次性MAC完成的。

在本节中,我们将从“碰撞抵抗”这个新概念出发来构建MAC。我们要做的第一件事就是构建碰撞抵抗的哈希函数。
碰撞抵抗的定义
首先,我们来定义哈希函数的“碰撞抵抗”意味着什么。设想一个哈希函数H,它将消息空间M映射到标签空间T。消息空间M应远大于标签空间T。消息可能长达千兆字节,而标签可能只有160比特。
对于函数H,一个碰撞是指一对不同的消息M0和M1,当应用函数H时,它们产生了相同的输出。可以想象,有两个输入M0和M1属于巨大的消息空间,但当应用哈希函数后,它们“碰撞”到了标签空间中的同一个输出点。
我们说函数H是碰撞抵抗的,如果很难找到该函数的碰撞。这听起来可能有点违反直觉,因为根据鸽巢原理,输出空间远小于输入空间,必然存在大量消息映射到相同的输出。问题在于,是否存在一个高效的算法能明确地找到任何一个这样的碰撞。
我们称函数H是碰撞抵抗的,如果对于所有显式、高效的算法A,这些算法都无法为函数H输出一个碰撞。我们通常将算法的优势定义为算法A能够输出一个碰撞的概率。这里“显式”意味着算法必须是我们可以实际编写并在计算机上运行的,而不仅仅是理论上存在。
一个经典的碰撞抵抗哈希函数例子是SHA-256。它可以输出256比特,但能接受任意大的输入(例如千兆字节的数据),并将其映射到256比特。目前,没有人知道如何为这个特定函数找到碰撞。
碰撞抵抗的应用:构建大消息MAC
为了展示碰撞抵抗概念的实用性,我们来看一个快速应用:如何使用碰撞抵抗哈希函数简单地构建一个MAC。
假设我们有一个用于短消息的MAC(例如AES,可以处理16字节的消息)。同时,假设我们有一个碰撞抵抗的哈希函数H,它能将包含千兆字节的大消息空间映射到我们的小消息空间(例如16字节的输出)。

我们可以定义一个新的、能处理大消息的MAC,称之为I_big。我们简单地通过将小MAC应用于哈希函数的输出来定义它。验证时,我们重新哈希给定的消息,然后检查小MAC是否能在给定标签下验证通过。
这是一种非常简单的方式,展示了碰撞抵抗如何将一个为小输入构建的原语扩展为能为极大输入构建的原语。实际上,我们不仅会在MAC中看到这一点,稍后讨论数字签名时,我们也会做同样的事情:为小输入构建数字签名方案,然后使用碰撞抵抗来扩展输入空间。
其安全定理在某种意义上很简单:如果底层MAC是安全的,并且H是碰撞抵抗的,那么这个能处理大消息的组合也是一个安全的MAC。
作为一个快速示例,我们可以将其应用于AES。使用我们提到的SHA-256。SHA-256输出256比特(32字节)。我们需要构建一个能处理32字节消息的MAC。我们可以通过将16字节的AES嵌入到一个两块的CBC模式中来实现,这将把AES从一个处理16字节的PRF扩展为处理32字节的PRF。然后,将SHA-256的输出输入到这个基于AES的两块CBC中,我们就得到了一个非常简单的MAC,只要假设AES是一个PRF且SHA-256是碰撞抵抗的,它就是安全的。
需要指出的是,碰撞抵抗对于这个构造的安全性是必要的。如果哈希函数H不是碰撞抵抗的,即存在算法能找到两个不同消息映射到相同输出,那么组合MAC将不安全。攻击者可以使用选择消息攻击获取消息M0的标签,然后输出(M1,标签)作为伪造。因为H(M1)等于H(M0),所以标签t对M1也是有效的。仅通过一次选择消息攻击,攻击者就能攻破这个组合MAC,原因就在于哈希函数不是碰撞抵抗的。因此,如果有人公布了SHA-256的一个碰撞(哪怕只是一对消息),这个构造就会变得不安全。
另一个应用:软件包完整性验证

碰撞抵抗的另一个直接用于消息完整性的应用是软件包验证。
想象我们有需要保护的文件,比如我们想在系统中安装的软件包。用户想下载软件包,并确保他下载的是正确的版本,而不是攻击者篡改过的版本。

他可以参考一个相对较小的、只读的公共空间。这个空间只需要存储软件包的小哈希值,因此所需空间不大。唯一的要求是这个空间是只读的,即攻击者无法修改其中存储的哈希值。
用户下载软件包后,可以轻松计算其哈希值,并与公共空间中的值进行比较。如果两者匹配,他就知道下载的软件包版本是正确的。为什么?因为函数H是碰撞抵抗的,攻击者无法找到一个不同的文件F1‘,使得其哈希值与F1的哈希值相同。因此,攻击者无法在不被检测到的情况下修改F1,因为他无法让F1‘的哈希值映射到公共空间中存储的值。
这个例子与MAC的例子形成对比。在MAC例子中,我们需要一个密钥来验证单个文件的标签,但不需要一个只读的公共空间资源。而使用碰撞抵抗哈希,我们得到了互补的特性:我们不需要密钥来验证(任何人都可以验证),但突然需要这个攻击者无法修改的额外资源(只读公共空间)。稍后我们会看到,通过数字签名,我们可以兼得两者:既具有公开可验证性,又不需要只读空间。但到目前为止,使用MAC或碰撞抵抗,我们只能拥有其一。
实际上,这种方案非常流行。例如,Linux发行版经常使用公共空间来公布其软件包的哈希值,任何人都可以在将软件包安装到计算机之前,确保他们下载的是正确的版本。这在现实世界中得到了相当广泛的应用。
总结

本节课我们一起学习了碰撞抵抗的概念及其在密码学中的重要性。我们定义了碰撞抵抗,并看到它如何使我们能够将处理小消息的密码原语(如MAC)安全地扩展到处理极大消息。我们还探讨了碰撞抵抗在软件包完整性验证中的实际应用,对比了它与基于密钥的MAC方案在公开验证和资源需求上的不同。在下一节中,我们将讨论针对碰撞抵抗的通用攻击,并开始学习如何构建碰撞抵抗的哈希函数。
030:通用生日攻击 🎂


在本节课中,我们将学习一种针对抗碰撞哈希函数的通用攻击方法——生日攻击。我们将了解其工作原理、背后的数学原理(生日悖论),以及它对哈希函数输出长度安全性的影响。
上一节我们讨论了抗碰撞哈希函数的基本概念,本节中我们来看看一种能够攻击任何此类函数的通用方法。
生日攻击算法
生日攻击是一种通用算法,可以作用于任意的抗碰撞哈希函数。假设哈希函数 H 输出 n 比特的值,即输出空间大小约为 2^n。消息空间则远大于 n 比特,例如消息长度为 100n 比特。
该算法能在约 2^(n/2) 的时间内找到碰撞,这大约是输出空间大小的平方根。以下是算法的步骤:
以下是算法的具体步骤:
- 随机选择
2^(n/2)条消息,记为M1, M2, ..., M2^(n/2)。由于消息长度远大于n比特,这些消息极大概率是互不相同的。 - 对每条消息应用哈希函数,得到对应的标签(哈希值)
ti = H(Mi)。每个ti都是n比特长的字符串。 - 在这些标签
ti中寻找碰撞,即找到一对(i, j)使得ti = tj。 - 一旦找到这样的碰撞,由于
Mi极大概率不等于Mj,但H(Mi) = H(Mj),我们就成功为函数H找到了一个碰撞。 - 如果在当前这批
2^(n/2)个标签中没有找到碰撞,则返回步骤1,重新选择一批消息并重复此过程。
接下来,我们需要分析这个算法需要迭代多少次才能成功找到碰撞。分析的关键在于理解“生日悖论”。
生日悖论 📊
为了分析上述攻击,我们需要了解生日悖论。假设我们有 n 个在区间 [1, B] 内的随机变量 R1, R2, ..., Rn。我们假设这些变量是相互独立且同分布的(例如,都是在 [1, B] 上均匀分布)。
生日悖论指出:如果我们设置 n ≈ 1.2 * √B,那么至少有两个样本相等的概率不小于 1/2。均匀分布是生日悖论中的“最坏情况”,对于非均匀分布,所需的样本数会更少。
以下是该定理的证明(针对均匀分布情况):
我们计算至少存在一对 (i, j) 使得 Ri = Rj 的概率。更方便的方法是计算其对立事件——所有样本都互不相同的概率。
- 选择
R1:第一个样本不会与任何样本碰撞。 - 选择
R2:R2不与R1碰撞的概率是(B-1)/B。 - 选择
R3:R3不与R1或R2碰撞的概率是(B-2)/B。 - 依此类推,选择
Rk时不与前k-1个样本碰撞的概率是(B - (k-1))/B。
由于样本相互独立,所有样本都互不相同的概率是这些概率的乘积:
P(无碰撞) = ∏_{i=1}^{n-1} (1 - i/B)

利用不等式 1 - x ≤ e^{-x}(对于 x > 0),我们可以得到:
P(无碰撞) ≤ ∏_{i=1}^{n-1} e^{-i/B} = e^{-∑_{i=1}^{n-1} i/B} = e^{-(n(n-1))/(2B)}
因此,存在碰撞的概率至少为:
P(碰撞) ≥ 1 - e^{-(n^2)/(2B)}
现在,代入 n = 1.2√B:
n^2/(2B) = (1.44B)/(2B) = 0.72
所以 P(碰撞) ≥ 1 - e^{-0.72} ≈ 1 - 0.487 ≈ 0.513 > 1/2。证明完毕。
它被称为“悖论”是因为结果与直觉相悖。例如,应用于生日(B=365),只需要约 1.2 * √365 ≈ 23 个人,就有超过一半的概率找到两个生日相同的人。直觉上,这个数字看起来很小。其背后的直觉是,n 个人可以组成约 n^2/2 对,每对生日相同的概率是 1/B,当 n ≈ √B 时,总的对数约为 B,因此很可能有一对发生碰撞。
下图展示了当 B=1,000,000 时,碰撞概率随样本数 n 的变化。可以看到,在 n 接近 √B 时,碰撞概率从接近0迅速上升到接近1,呈现一种阈值现象。
分析攻击算法 ⚙️
现在我们可以分析之前的攻击算法了。我们选择了 2^(n/2) 条随机消息并计算其哈希值。这些哈希标签 t1, t2, ..., t2^(n/2) 是相互独立的。
根据生日悖论,当我们采样约 1.2 * √(2^n) = 1.2 * 2^(n/2) 个样本(即哈希值)时,找到碰撞的概率约为 1/2。
那么,我们需要迭代这个算法多少次才能找到碰撞呢?由于每次尝试(生成一批哈希值)有 1/2 的概率成功,期望的迭代次数约为2次。因此,该算法的总运行时间大致为 2 * 2^(n/2) = O(2^(n/2)) 次哈希函数计算。
这个结论非常关键:对于一个输出为 n 比特的哈希函数,总存在一个运行时间约为 2^(n/2) 的通用攻击算法来寻找碰撞。

对哈希函数设计的影响 🛡️
这个结论直接影响哈希函数输出长度的安全标准。例如:
- 如果哈希函数输出128比特(
n=128),则通用攻击可在约2^64时间内找到碰撞。这在当今计算能力下被认为是不够安全的。 - 因此,抗碰撞哈希函数通常需要更长的输出。
以下是几个哈希函数标准的例子及其对应的通用攻击复杂度:

| 哈希函数 | 输出长度 (n bits) | 生日攻击复杂度 (≈2^(n/2)) | 备注 |
|---|---|---|---|
| SHA-1 | 160 | 2^80 | 已不推荐在新项目中使用。存在理论攻击(~2^51)。 |
| SHA-256 | 256 | 2^128 | 目前的安全标准。 |
| SHA-512 | 512 | 2^256 | 更高的安全性,但通常更慢。 |
| Whirlpool | 512 | 2^256 | 另一个512位输出的哈希函数示例。 |
需要注意的是,SHA-1虽然尚未被实际攻破(找到碰撞),但已存在理论上更快的攻击方法。因此,在新项目中应避免使用SHA-1,转而使用SHA-256或SHA-512。
总结


本节课中我们一起学习了针对抗碰撞哈希函数的通用生日攻击。我们首先介绍了攻击算法的步骤,即通过随机采样约 2^(n/2) 条消息并比较其哈希值来寻找碰撞。然后,我们深入探讨了其理论基础——生日悖论,并完成了证明。最后,我们分析了该攻击的意义,它决定了哈希函数的安全输出长度必须足够长(例如256比特或以上),以使得 2^(n/2) 的计算量在现有和可预见的计算能力下不可行。这为选择安全的哈希函数提供了重要依据。
031:默克尔-达姆加德范式 🧩
在本节课中,我们将学习一种用于构建抗碰撞哈希函数的通用范式——默克尔-达姆加德范式。我们将了解其工作原理、关键组成部分,并通过一个证明来理解为何它能将短消息的抗碰撞性扩展到长消息上。

目标回顾

首先,让我们明确目标。哈希函数 H 将一个大的消息空间映射到一个小的标签空间。哈希函数的碰撞是指两个不同的消息在该哈希函数下映射到了相同的值。我们的目标是构建抗碰撞的哈希函数,即难以找到任何碰撞的函数,即使我们知道存在许多碰撞。
我们将分两步构建这些抗碰撞哈希函数。第一步(即本节内容)是展示:如果给定一个用于短消息的抗碰撞哈希函数,我们可以扩展它,构建一个用于更长消息的抗碰撞哈希函数。下一步将实际构建用于短消息的抗碰撞哈希函数。
构造方法

构造方法非常通用,事实上,所有抗碰撞哈希函数都遵循这个范式。这是一个非常优雅的范式。
我们有一个函数 H,我们假设它是一个用于小尺寸输入的抗碰撞哈希函数。这个 H 有时被称为压缩函数。
我们取一个长消息 M,将其分割成块。然后,我们使用一个称为 IV(初始向量)的固定值。这个 IV 是永久固定的,被嵌入在代码和标准中,是函数定义的一部分。
接下来,我们将小的压缩函数 H 应用于第一个消息块和这个 IV。产生的结果被称为链变量,它将被输入到下一个压缩函数中,该函数压缩下一个消息块和前一个链变量,并输出下一个链变量。我们以此类推,处理后续的消息块,直到最后一个消息块。
在最后一个消息块,我们有一件特殊的事情要做:必须附加一个填充块 PB。我稍后会解释填充块是什么。在附加填充块后,我们再次压缩最后一个链变量和最后一个消息块(包含填充块),其输出就是哈希函数的实际输出。

用符号总结:压缩函数将标签空间(哈希函数输出)和消息块(x)中的元素映射到下一个链变量。从这个压缩函数,我们能够为大型输入(即最多 L 个 x 块的空间)构建哈希函数。这些是可变长度的,因此不同长度的消息都可以作为输入。输出是标签空间中的一个标签。
唯一需要定义的是填充块。填充块非常重要。它基本上是一个序列 1000... 来表示实际消息块的结束,然后填充块最重要的部分是我们在其中编码消息长度。消息长度字段固定为 64 位。因此,在所有 SHA 哈希函数中,最大消息长度是 2^64 - 1 位。这个上限足以处理我们将要处理的所有消息。
如果最后一个块的长度正好是压缩函数块长度的倍数,没有空间放置填充块怎么办?答案是:如果需要,我们将添加一个额外的虚拟块,并将填充块放在那里,并正确放置 10...0 序列。关键在于,填充块包含消息长度,这一点非常重要。
这就是默克尔-达姆加德构造。最后一个术语是链变量:H0 是初始值,H1, H2, H3... 直到 Ht+1,即哈希函数的最终输出。
所有标准哈希函数都遵循这个范式,从压缩函数构建抗碰撞哈希函数。这个范式如此流行的原因在于以下定理:如果小的压缩函数是抗碰撞的,那么大的默克尔-达姆加德哈希函数也是抗碰撞的。换句话说,要构建用于大输入的抗碰撞函数,我们只需要构建抗碰撞的压缩函数。
定理证明
让我们来证明这个定理。证明很优雅,也不太困难。我们将使用逆否命题来证明:如果你能在大的哈希函数上找到一个碰撞,那么我们将推导出在小的压缩函数上存在一个碰撞。因此,如果小 H 是抗碰撞的,那么大 H 也将是抗碰撞的。
假设你给了我一个大哈希函数的一个碰撞,即两个不同的消息 M 和 M' 哈希到相同的输出。我们将使用 M 和 M' 来构建小压缩函数的一个碰撞。
首先,我们必须记住默克尔-达姆加德构造的工作原理。特别是,让我们为哈希 M 和哈希 M' 时产生的链变量命名。
对于消息 M,链变量为:H0 是启动整个过程的固定 IV,H1 是第一个消息块与 IV 哈希的结果,依此类推,直到 H_{t+1},这是默克尔-达姆加德链的最终输出,也是最终哈希值。
类似地,对于 M',定义 H0' 为第一个链变量,H1' 为压缩 M' 的第一个消息块后的结果,依此类推,直到 H'_{r+1},即消息 M' 的最终哈希值。注意,消息 M 和 M' 的长度不必相同;M 长度为 t,而 M' 长度为 r。
由于发生了碰撞,我们知道这两个值相等:H(M) = H(M')。换句话说,最后的链变量彼此相等。
现在,让我们仔细看看 H_{t+1} 和 H'_{r+1} 是如何计算的。H_{t+1} 是通过将压缩函数应用于前一个链变量和 M 的最后一个消息块(包括填充块)来计算的。类似地,H'_{r+1} 是通过压缩前一个链变量和 M' 的最后一个消息块(包括填充块)来计算的。由于这两个值相等,我们突然得到了压缩函数的一个候选碰撞。
如果压缩函数的参数不同,那么我们就得到了小 H 的一个碰撞。更精确地说,如果 H_t ≠ H'_r 或 M_t ≠ M'_r 或填充块不同,那么我们就有了压缩函数 H 的一个碰撞,证明完成。
我们需要继续的唯一情况是上述条件不成立。这意味着最后第二个链变量相等、消息的最后一块相等且两个填充块相等。填充块相等意味着消息长度被编码在填充块中,因此 M 和 M' 的长度相同,即 t = r。从现在起,我可以假设 t = r。
现在,我们可以对倒数第二个链变量应用完全相同的论证。H_t 是通过哈希前一个链变量 H_{t-1} 和倒数第二个消息块来计算的。H'_t 也是类似计算的。如果 H_{t-1} ≠ H'_{t-1} 或 M_{t-1} ≠ M'_{t-1},那么我们就有压缩函数的一个碰撞,证明完成。
我们需要继续的唯一情况是这些条件也不成立。我们可以继续迭代这个论证,从消息的末尾一直迭代到开头。两种情况必居其一:要么我们找到了压缩函数的一个碰撞;要么 M 和 M' 的所有消息块都相同。由于我们已经证明消息长度相同,这意味着 M 实际上等于 M'。但这与你一开始给我一个碰撞的事实相矛盾。因此,从消息末尾向开头迭代的过程中,我们必定会在某个点找到小 H 的一个碰撞。

这基本上完成了定理的证明。因此,如果小的压缩函数 H 是抗碰撞的,那么大的默克尔-达姆加德函数 H 也必须是抗碰撞的。人们喜欢这个构造的原因再次在于:它表明要构建大的哈希函数,只需为小输入构建压缩函数就足够了。我们将在下一节中完成这一步。
总结

本节课中,我们一起学习了默克尔-达姆加德范式。我们了解了它如何通过一个抗碰撞的压缩函数来构建一个能处理任意长度消息的抗碰撞哈希函数。关键点包括使用初始向量 IV、将消息分块处理、链变量的传递,以及至关重要的包含消息长度的填充块。最后,我们通过一个严谨的证明理解了该范式安全性的核心:对大哈希函数的任何碰撞都会导致对小压缩函数的碰撞,从而将抗碰撞性从“小”模块传递到了“大”系统。这为后续实际构建压缩函数奠定了理论基础。
032:构造压缩函数 🔧

在本节中,我们将学习如何构建安全的压缩函数。具体来说,我们将探讨如何构建具有抗碰撞性的压缩函数,这是构建安全哈希函数的关键组件。

概述
上一节我们介绍了Merkle-Damgård结构,它能将一个小型压缩函数扩展为可处理任意长输入的哈希函数。我们证明了,只要底层压缩函数是抗碰撞的,那么构造出的哈希函数也是抗碰撞的。因此,本节的核心目标就是学习如何构建这种抗碰撞的压缩函数。
我们将看到两种主要方法:一种基于已有的分组密码,另一种则基于数论中的困难问题。
基于分组密码的构造
一个很自然的问题是:能否利用我们已经掌握的分组密码来构建压缩函数?答案是肯定的。
假设我们有一个分组密码 E,它处理 n 比特的数据块。输入和输出都是 n 比特。这里有一个经典的构造,称为 Davies-Meyer 结构。
其工作原理如下:给定消息块 M 和链变量 H,我们使用消息块 M 作为密钥来加密链变量 H,然后将输出与原始的链变量 H 进行异或操作。
用公式表示如下:
H(H, M) = E(M, H) XOR H
这看起来可能有些奇怪,因为消息块 M 完全由攻击者控制,而我们却将其用作分组密码的密钥。然而,我们可以证明,当 E 是一个理想密码时,这个构造的碰撞阻力达到了理论上的最优值。
定理:如果 E 是一个理想分组密码,那么找到 Davies-Meyer 压缩函数碰撞所需的时间约为 2^(n/2)。这与通用的生日攻击复杂度相同,意味着该构造的抗碰撞性已尽可能强。
Davies-Meyer 结构在实践中被广泛使用,例如 SHA 系列哈希函数就采用了它。
一个不安全的变体
并非所有基于分组密码的构造都是安全的。考虑以下变体,它去掉了最后的异或操作:
H(H, M) = E(M, H)
这个压缩函数不是抗碰撞的。以下是证明方法:
假设我们想为这个函数找到一个碰撞。我们可以任意选择两个不同的消息块 M 和 M',以及一个任意的链变量 H。然后,我们通过解密操作来构造碰撞的另一个输入 H':
H' = D(M', E(M, H))
其中 D 是 E 的解密函数。可以验证,(H, M) 和 (H', M') 在这个压缩函数下会产生相同的输出。因此,攻击者可以轻松制造碰撞。
这个例子说明,设计安全的压缩函数需要非常谨慎。
其他安全与不安全的构造
除了 Davies-Meyer,还有其他安全的构造方式,例如 Miyaguchi-Preneel 结构(被 Whirlpool 哈希函数使用)。同样,也存在许多不安全的变体。
以下是一个不安全构造的例子,作为练习供你思考如何找到其碰撞:
H(H, M) = E(M, H) XOR M
SHA-256 的构成
现在,我们可以完整描述 SHA-256 哈希函数了:
- 它采用 Merkle-Damgård 迭代结构。
- 其压缩函数采用 Davies-Meyer 模式。
- Davies-Meyer 模式中使用的底层分组密码是一个名为 SHACAL-2 的密码。
- 密钥大小为 512 比特(对应消息块)。
- 分组大小为 256 比特(对应链变量)。
至此,你已从原理上理解了 SHA-256 的工作机制(当然,SHACAL-2 分组密码的内部细节是另一个话题)。
基于数论的“可证明”压缩函数
另一类压缩函数基于数论中的困难问题构建。我们称之为“可证明安全的”压缩函数,因为如果能找到该函数的碰撞,就意味着你能解决一个公认困难的数论问题。
构造方法如下:
- 选择一个非常大的素数
p(例如 2000 比特)。 - 在
1到p-1之间随机选择两个数u和v。 - 定义压缩函数。它输入两个在
0到p-1之间的数(链变量H和消息块M),输出一个数:H(H, M) = u^H * v^M mod p
定理:如果能为这个压缩函数找到一个碰撞,那么你就能解决“离散对数问题”。由于大家普遍相信离散对数问题是困难的,因此这个压缩函数是(可证明)抗碰撞的。
你可能会问,为什么实践中不常用这种函数(例如用于 SHA-256)?主要原因是它的计算速度远慢于基于分组密码的构造。因此,它通常只用于某些对可证明安全性有极高要求、且对性能不敏感的特殊场景。
总结
本节课我们一起学习了构建抗碰撞压缩函数的两种主要途径:
- 基于分组密码的构造,如 Davies-Meyer 结构,它高效且被广泛采用(如 SHA-256)。
- 基于数论难题的构造,它具有可证明的安全性,但性能较低。

理解如何安全地构建压缩函数,是理解现代密码学哈希函数如何工作的基石。下一节,我们将利用这些知识,探讨如何构建安全的消息认证码(MAC),具体来说是 HMAC。
033:HMAC 🛡️
在本节课中,我们将学习如何利用抗碰撞哈希函数来构建一个安全的MAC(消息认证码)。我们将重点介绍一个广泛使用的标准——HMAC,并解释其工作原理与安全性。

概述
我们已经理解了抗碰撞哈希函数的概念及其构造方法。现在,我们将探讨如何直接利用一个大型哈希函数(如SHA-256)来构建MAC,而无需依赖伪随机函数(PRF)。本节将介绍HMAC的构造原理,并解释为何简单的“密钥拼接”方法不安全。
从哈希函数到MAC的初步尝试
上一节我们介绍了Merkle-Damgård构造。本节中我们来看看能否直接用它来构建MAC。
一个直观的想法是:将MAC密钥与待认证的消息拼接起来,然后对整个拼接结果进行哈希。用公式表示,即 Tag = H(K || M)。

然而,这种方法完全不安全。原因在于Merkle-Damgård构造固有的扩展攻击漏洞。
以下是攻击原理:
- 攻击者获得某个消息
M的认证标签Tag。 - 由于
Tag实际上是哈希链在某个中间状态的值,攻击者可以在此状态后附加任意的数据块W。 - 攻击者只需对附加的块
W再应用一次压缩函数H,就能计算出H(K || M || padding || W)的标签,从而实现对扩展消息M || padding || W的存在性伪造。

因此,H(K || M) 这种构造绝不应该被使用。
HMAC:安全的构造方法
为了避免上述安全问题,业界采用了一个标准化的方法将抗碰撞哈希函数转换为MAC,即HMAC。例如,我们可以基于SHA-256构建HMAC,其输出为256位。实际上,HMAC被认为是一个伪随机函数。
以下是HMAC的符号化构造,它按如下步骤工作:
- 首先,将密钥
K与一个固定的内部填充(iPad)进行异或运算,使其成为Merkle-Damgård构造的一个输入块(例如,对于SHA-256,块大小为512位)。 - 将上一步的结果作为前缀,与消息
M拼接,然后进行哈希运算。我们已知,仅这一步并不安全。 - HMAC的额外步骤是:将上一步哈希得到的输出(256位),再次与密钥
K异或一个固定的外部填充(Opad)组合成另一个块(512位)。 - 最后,对这个组合进行哈希运算,得到消息
M的最终认证标签。
用公式简要描述核心步骤:
HMAC(K, M) = H( (K ⊕ Opad) || H( (K ⊕ iPad) || M ) )

HMAC的工作原理图示
比起符号,图示能更直观地展示HMAC的工作原理。
如下图所示,HMAC包含两次Merkle-Damgård哈希过程:
- 内层哈希:密钥与iPad异或后,作为初始向量(IV)输入,与消息
M一起经过哈希链处理。 - 外层哈希:内层哈希的输出,与密钥和Opad异或后的结果组合,再进行一次哈希,产生最终标签。

HMAC与NMAC的关系及安全性
现在我们来分析HMAC的安全性。我们之前已经见过非常类似的东西。
具体来说,如果我们把压缩函数 H 看作一个伪随机函数(PRF),其中上方的输入作为密钥,那么:
- 在内层,我们对固定值IV应用该PRF(密钥为
K ⊕ iPad),会得到一个随机值,可称为K1。 - 在外层,我们再次对固定值IV应用该PRF(密钥为
K ⊕ Opad),会得到另一个随机值K2。

此时,使用 K1 和 K2 计算HMAC的过程,看起来就非常熟悉了——它基本上就是我们在前面章节讨论过的 NMAC 构造。

需要注意的是,HMAC与NMAC的主要区别在于:HMAC的密钥 K1 和 K2 是由同一个主密钥 K 通过与固定常量(iPad和Opad)异或派生而来的,因此它们是相关的。
为了论证HMAC的安全性,我们需要假设压缩函数 H 在即使密钥相关的情况下,仍然是一个安全的PRF。在此假设下,适用于NMAC的安全性分析同样适用于HMAC。HMAC的安全界限与NMAC相同:只要被认证的消息数量远少于标签输出空间大小的平方根,就是安全的。对于HMAC-SHA256(输出256位),其安全界限约为2^128次消息认证,这在实际应用中足够安全。
关于HMAC的补充说明
最后,关于HMAC还有两点需要说明:
- TLS标准与HMAC-SHA1:TLS标准要求支持HMAC-SHA1-96(即基于SHA-1构建HMAC,并截断输出至96位)。你可能会问,SHA-1不是已被认为是不安全的哈希函数了吗?这是因为HMAC的安全性分析并不要求底层哈希函数抗碰撞,而只要求其压缩函数在作为PRF时是安全的。就我们所知,SHA-1的压缩函数目前仍满足这一性质,因此在其内部使用是可行的。
- 时序攻击:HMAC的实现需要考虑时序攻击的防范,我们将在下一节讨论这个问题。

总结

本节课中我们一起学习了HMAC。我们首先看到了直接将密钥与消息拼接后哈希的不安全性(易受扩展攻击),然后详细介绍了HMAC的标准构造方法,它通过两次哈希和固定的内外填充来避免安全问题。我们还分析了HMAC与NMAC的关联,并理解了其安全性的核心假设。最后,我们了解到即使使用已不抗碰撞的SHA-1,HMAC在特定假设下仍然是安全的,并提示了实现时需注意时序攻击。
034:MAC验证的时序攻击 ⏱️
在本节课中,我们将要学习一种影响许多MAC算法实现的通用攻击——时序攻击。我们将通过分析一个具体的HMAC验证实现,来理解这种攻击的原理、危害以及如何防御。

概述
上一节我们介绍了MAC(消息认证码)的基本概念。本节中,我们来看看一个看似正确但存在严重安全漏洞的MAC验证实现。我们将分析攻击者如何利用代码执行时间的微小差异,逐字节地破解出正确的MAC值,并学习两种有效的防御方法。
一个存在漏洞的HMAC验证实现

以下是一个来自Python keysar库的HMAC验证代码的简化版本。它接收密钥、消息和待验证的标签字节作为输入。
def verify(key, message, tag_bytes):
# 重新计算消息的HMAC
computed_mac = hmac(key, message)
# 将计算出的16字节MAC与提供的标签字节进行比较
return computed_mac == tag_bytes
这段代码看起来完全正确,许多人也是这样实现的。问题在于Python解释器内部进行字符串比较的方式。
时序攻击的原理
字符串比较通常是逐字节进行的。Python内部的循环会遍历所有字节,但一旦发现第一个不匹配的字节,循环就会立即终止并返回“不相等”。
这个“发现不匹配即退出”的特性,为攻击者实施时序攻击打开了大门。
以下是攻击者如何利用这一漏洞:
-
攻击场景:攻击者有一个目标消息
M,想获取其有效的MAC标签。他可以向一个存储了HMAC密钥的服务器发起请求,服务器会验证提交的(消息,标签)对,有效则处理消息,无效则返回“拒绝”。 -
攻击步骤:
- 攻击者提交大量(目标消息,猜测标签)的查询。
- 他通过测量服务器的响应时间,来推断标签的正确字节。
以下是攻击的具体流程:
- 第一步:提交一个完全随机的标签,并记录服务器的响应时间(作为基准时间)。
- 第二步:开始猜测标签的第一个字节。攻击者提交的标签格式为:
[猜测字节] + [任意15个字节]。- 他首先尝试第一个字节为
0,测量响应时间。 - 如果响应时间与第一步的基准时间相同(说明第一个字节就错了,比较立即终止),则尝试第一个字节为
1。 - 以此类推,直到尝试到某个值(例如
3)时,发现服务器的响应时间稍微变长了一点。 - 这意味着服务器在比较时,第一个字节匹配成功了,比较进行到了第二个字节才失败。因此,攻击者得知第一个字节是
3。
- 他首先尝试第一个字节为
- 第三步:固定第一个字节为
3,开始用同样的方法猜测第二个字节。提交标签格式:[3, 猜测字节] + [任意14个字节]。- 通过测量响应时间的变化,可以推断出第二个字节的正确值(例如
53)。
- 通过测量响应时间的变化,可以推断出第二个字节的正确值(例如
- 后续步骤:重复此过程,逐字节地破解出完整的16字节MAC标签。
最终,攻击者能够构造出完全正确的标签,从而欺骗服务器接受他伪造的消息。
防御方法一:恒定时间比较
这个例子表明,一个看似合理的实现方式可能是完全脆弱的。那么,我们该如何防御呢?
第一种防御方法是实现一个恒定时间的比较函数,确保无论字节在何处不匹配,比较所花费的时间都是相同的。
以下是keysar库更新后采用的防御代码:
def verify_secure(key, message, sig_bytes):
computed_mac = hmac(key, message)
# 首先检查长度
if len(sig_bytes) != len(computed_mac):
return False
# 恒定时间比较
result = 0
for x, y in zip(sig_bytes, computed_mac):
result |= x ^ y # 如果x和y不同,x^y为非零值,通过|运算会使result变为非零
return result == 0
这个比较函数会遍历所有字节对,计算它们的异或(XOR)值,并通过或(OR)运算累积结果。只有当所有字节都相同时,最终结果result才为0。循环总是执行固定的次数,不提前退出,从而消除了时序信息泄露。
然而,这种方法存在一个潜在问题:优化编译器。编译器可能会“聪明地”优化这段代码,一旦发现result变为非零,就提前终止循环,这反而重新引入了时序漏洞。
防御方法二:隐藏比较对象
另一种防御思路是,不让攻击者知道他提交的字符串在和什么进行比较。
验证流程修改如下:
- 计算正确的MAC:
correct_hmac = hmac(key, message) - 不直接比较
correct_hmac和sig_bytes。 - 而是对两者再进行一次哈希:
h1 = hash(correct_hmac)h2 = hash(sig_bytes)
- 最后比较
h1和h2。
如果sig_bytes等于correct_hmac,那么h1必然等于h2。但如果sig_bytes只是部分字节正确(例如仅第一个字节匹配),那么经过哈希后,h1和h2将很可能是两个完全不同的值,标准字符串比较会在第一个字节就快速返回不匹配。攻击者无法知道他猜测的标签字节在与哪个哈希值进行比较,因此无法实施之前描述的逐字节时序攻击。这种方法对编译器的优化不那么敏感。
总结与核心教训
本节课中我们一起学习了针对MAC验证的时序攻击。
- 我们看到了一个逐字节比较且提前退出的实现如何泄露关键信息。
- 我们分析了攻击者如何利用响应时间的微小差异,像剥洋葱一样逐字节破解出完整的MAC。
- 我们探讨了两种防御策略:
- 实现恒定时间比较,确保执行时间不依赖数据。
- 对比较对象进行哈希,隐藏真实的比较内容。
从所有这些内容中得到的主要教训是:即使是加密库的实现专家也可能犯错,写出功能正常但完全无法抵御侧信道攻击(如时序攻击)的代码,从而彻底破坏系统的安全性。

因此,核心建议是:
- 不要发明自己的加密算法。
- 甚至不要自己实现加密算法,因为你很可能无法抵御各种侧信道攻击。
- 使用标准的、经过严格审计的加密库,如OpenSSL或已修复漏洞的
keysar库,这能大大降低遭受此类攻击的风险。
035:对CPA安全加密的主动攻击 🔓

在本节课中,我们将学习主动攻击者如何破坏仅具备CPA安全性的加密方案。我们将看到,如果加密数据在传输过程中可以被篡改,那么即使加密本身是CPA安全的,也无法保证机密性。这引出了我们需要将机密性与完整性结合的需求。
上一节我们介绍了消息完整性,本节我们将回到加密的话题,并展示如何构建比之前更强的安全加密方案。但首先,让我们回顾一下目前的情况。
在之前的章节中,我们讨论了机密性,特别是如何加密消息以实现针对选择明文攻击的语义安全。我反复强调,针对选择明文攻击的安全性仅能提供针对窃听的安全性。换句话说,这只能保护我们免受那些监听网络流量但不实际修改或注入数据包的对手的攻击。
在本模块中,我们的目标是设计能够抵御主动攻击者的加密方案。这些攻击者可以通过拦截、篡改或注入数据包来干扰流量。

我们还学习了如何提供消息完整性,即消息本身不保密,但我们希望确保消息在传输过程中不被修改。我们讨论了消息认证码,即MAC算法,它能提供针对选择消息攻击的存在不可伪造性。即使攻击者能够获取任意选择消息的MAC标签,他也无法为任何其他消息构造有效的MAC标签。我们研究了几种MAC构造,特别是CBC-MAC、HMAC、并行MAC构造以及一种名为Carter-Wegman MAC的快速MAC构造。
在本模块中,我们将展示如何结合这些机密性和完整性机制,以获得能够抵御更强大对手的加密方案。这些对手能够在网络中篡改流量、注入自己的数据包、拦截特定数据包等。我们的目标是确保即使面对如此强大的对手,我们也能维持机密性。换句话说,对手无法获知明文内容,甚至无法修改密文并导致接收者认为发送了不同的明文。
在此之前,我想先举几个例子,说明能够篡改流量的对手如何完全破坏CPA安全加密的安全性。这将表明,如果不提供完整性,机密性也可能被破坏。换句话说,如果我们想要实现针对主动攻击者的安全性,完整性和机密性必须齐头并进。
让我们看一个来自网络世界的例子,特别是TCP/IP协议。我将使用一个高度简化的TCP/IP版本,以便我们快速聚焦于攻击本身,而不被细节所困扰。
这里有两台机器正在通信。用户坐在一台机器前,另一台是服务器。服务器当然有一个TCP/IP协议栈来接收数据包,然后根据数据包中的目标字段将其转发到适当的位置。例如,这里有两个进程在监听这些数据包:一个Web服务器(监听端口80)和另一个用户Bob(监听端口25)。
当一个数据包到达时,TCP/IP协议栈会查看目标端口。如果是目标端口80,协议栈会将数据包转发给Web服务器。如果目标端口是25,协议栈会将数据包转发给监听端口25的Bob。
一个相当知名的安全协议IPsec会在发送方和接收方之间加密这些IP数据包。发送方和接收方共享一个密钥K。当发送方发送IP数据包时,这些数据包会使用密钥K进行加密。当数据包到达目标服务器时,TCP/IP协议栈会解密数据包,然后查看目标端口,并将解密后的数据发送到适当的位置。请注意,这里的数据是解密后的。如果目标端口是80,就会发送给Web服务器。如果目标端口是25,TCP/IP协议栈会解密数据包,查看目标端口,并将明文数据发送给监听端口25的进程。

现在我想展示,在这种设置下,如果没有完整性,我们就不可能实现任何形式的机密性。让我们看看原因。
想象攻击者拦截了一个发送给Web服务器的数据包,即一个发送给端口80的加密数据包。请记住,攻击者实际上可以接收任何发送给端口25的数据包解密后的内容,因为TCP协议栈会乐意解密端口25的数据包并将其发送给Bob。
攻击者Bob会拦截这个数据包,阻止它按原样到达服务器,并修改数据包,使目标端口变为25。这是在密文上进行的操作,我们稍后会看到具体做法。
当这个修改后的数据包到达服务器时,目标端口显示为25。服务器将解密数据包,看到目标是25,并将数据转发给Bob。

因此,Bob仅仅通过更改目标端口,就能够读取本意是发送给Web服务器的数据。
如果数据是使用带随机IV的CBC加密的(请记住,这是一个CPA安全的方案),攻击者可以轻易地更改密文,使其目标端口变为25而不是80。唯一需要改变的只是IV字段,其他所有内容都保持不变。
让我们看看具体怎么做。攻击者捕获的是一个CBC加密的数据包,他知道目标端口是80,但不知道数据内容。他的目标是构建一个新的加密数据包,使其目标端口变为25。
正如我们所说,他只需更改IV即可。解密CBC加密数据的方式是:第一个明文块等于第一个密文块解密后与IV进行异或。我们知道,在原始数据包中,这将得到D=80,因为原始数据包的目标端口是80。现在的问题是,攻击者如何更改IV,使目标端口变为D=25?
这很容易看出。如果攻击者取原始IV,将其与(一串零和80异或25的结果)在端口字节的相应位置进行异或,那么当这个新的IV‘与原始密文一起发送时,解密后,新的IV会抵消原始明文中本应得到的80,再通过与25异或,目标端口就变成了25。
这是一个很好的例子,说明通过对IV字段进行简单更改,攻击者就能够转移数据包,使得解密后的数据包流向攻击者而非实际的Web服务器,从而让攻击者能够读取本意发送给服务器的明文数据。
这个例子表明,如果没有完整性,当攻击者可以修改数据包时,CPA安全加密根本无法提供机密性。CPA安全加密仅在攻击者只窃听数据而无法实际修改密文时提供机密性。正如你所见,如果可以修改密文,一个简单的修改就会完全泄露明文。

我想展示另一个篡改攻击,它只需要访问网络流量,而不要求攻击者实际存在于解密机器上。
让我们看一个远程终端应用程序的例子,用户每次击键,一个加密的击键信息就会被发送到服务器。假设加密的击键使用计数器模式加密。这里,TCP/IP数据包中的D对应一个字节的击键信息,并使用计数器模式加密。你可能知道,每个TCP数据包实际上都包含一个校验和,这是一个16位的校验和,仅用于检测传输错误。如果服务器收到一个校验和错误的数据包,它会直接丢弃并忽略它。TCP头部(包括校验和)和击键信息都使用计数器模式加密。
攻击者想知道击键内容是什么。让我展示他能做什么。攻击者将拦截这个数据包,但不会立即修改它。他会先将原始数据包发送给服务器,但同时记录下这个数据包。稍后,他会修改这个数据包,并将修改后的版本发送给服务器。他会用值T异或加密后的校验和字段,用值S异或加密后的数据字段。他会对大量的T和S值重复此操作。
请记住计数器模式的一个特性:如果你用T异或密文,解密后的明文也会被T异或。同样,如果你用S异或加密数据,解密后的数据也会被S异或。
服务器将解密这个修改后的数据包,得到的结果数据包将具有被T异或的校验和和被S异或的数据。
如果修改后的校验和对于这个修改后的数据包是正确的,服务器将发送一个ACK确认包。如果修改后的校验和是错误的,服务器将直接丢弃数据包,不做任何回应。
因此,攻击者可以简单地观察是否有ACK包返回。通过这样做,他了解到这个特定的(T, S)异或对是否对应于一个有效的校验和。
攻击者将对大量的T和S值进行此操作。他了解到的是:如果我通过用特定值S异或来修改数据,这会使校验和改变一个特定值T。他获得了大量这样的(T, S)对信息。
事实证明,对于某些校验和算法,通过观察一系列此类方程,你实际上可以推断出D的值。我应该指出,对于TCP校验和,这可能不成立,但肯定存在一些简单的校验和算法,这绝对是成立的。

因此,通过观察大量此类方程,攻击者可以恢复出D。这是一个很好的选择密文攻击例子。攻击者基本上提交了他选择的密文(这些密文源自他想解密的密文),然后通过观察服务器的响应,他能够了解到一些关于结果明文的信息。通过对许多不同的T和S值重复此过程,他最终能够恢复出完整的明文。
在本节中,我们将看到更多此类攻击的例子,它们被称为主动攻击,攻击者实际上在途中修改流量。我希望这两个简单的例子能让你相信,如果只提供CPA安全性(即仅针对窃听的安全性),你甚至无法保证对主动攻击者的保密性。不仅你的密文缺乏完整性(接收者可能获得与发送者发送的不同的消息),而且你甚至没有机密性。我向你展示了两个例子,在没有完整性的情况下,攻击者可以简单地利用接收者作为解密部分数据的“预言机”来解密数据包。
因此,我将在本模块中反复强调的教训是:如果你的消息需要完整性但不需要机密性,只需使用MAC。但如果你的消息既需要完整性又需要机密性,你必须使用所谓的认证加密模式,这正是本模块的主题。
接下来,我们将定义认证加密的含义,并构建认证加密系统。但我想让你记住的一点是,我们之前讨论的CPA安全模式本身绝不应该被单独用于加密数据。因此,带随机IV的CBC是构建认证加密的一个组件,但绝不应该单独使用。

我们将在下一节中定义认证加密。
036:认证加密的定义

在本节课中,我们将学习一种新的加密概念——认证加密。上一节我们看到了两种可以完全破坏CPA安全加密方案安全性的主动攻击。本节中,我们将定义认证加密,它能够在面对主动攻击者时保持安全。在后续章节中,我们将构建满足这一新概念的加密方案。
什么是认证加密?🔐
认证加密是一种密码方案。与往常一样,加密算法 E 接收一个密钥 K、一条消息 M 和一个随机数,并输出一个密文 C。解密算法 D 通常输出一条消息。然而,在这里,解密算法被允许输出一个特殊的符号 ⊥(底部符号)。当解密算法输出 ⊥ 时,意味着该密文无效,应被忽略。唯一的要求是 ⊥ 不在消息空间 M 中,因此它是一个指示密文应被拒绝的唯一符号。
那么,认证加密系统的安全性意味着什么呢?该系统必须满足两个属性。
认证加密的安全属性
以下是认证加密方案必须满足的两个核心安全属性。
- 语义安全(CPA安全):与之前一样,方案必须在选择明文攻击下是语义安全的。
- 密文完整性:即使攻击者看到了许多密文,他也应该无法生成另一个能正确解密的密文。换句话说,他无法生成一个解密结果不是
⊥的新密文。

更精确地说,让我们看看密文完整性游戏。
密文完整性游戏详解
在这个游戏中,(E, D) 是一个消息空间为 M 的密码方案。挑战者首先选择一个随机密钥 K。然后,攻击者可以提交他选择的消息,并接收这些消息的加密结果。
C1 = E(K, M1),其中M1由攻击者选择。- 攻击者可以重复此操作,提交
M2并获得C2 = E(K, M2),依此类推,直到提交M_q并获得C_q = E(K, M_q)。
因此,攻击者获得了 q 个他选择消息的对应密文。然后,他的目标是产生一个新的、有效的密文。

我们说攻击者赢得游戏,如果他创建的新密文 C' 能够正确解密(即 D(K, C') ≠ ⊥),并且 C' 不是之前挑战者给攻击者的那 q 个密文中的任何一个。
像往常一样,我们将攻击者在密文完整性游戏中的优势定义为游戏结束时挑战者输出1的概率。如果对于所有高效的攻击者,赢得该游戏的优势都是可忽略的,那么我们就说该密码方案具有密文完整性。
理解了密文完整性后,我们就可以定义认证加密了。我们说一个密码方案具有认证加密属性,当且仅当它在选择明文攻击下是语义安全的,并且同时具有密文完整性。
一个反面例子

作为一个不好的例子,需要指出的是,使用随机IV的CBC模式不提供认证加密。因为攻击者很容易赢得密文完整性游戏。攻击者只需提交一个随机的密文串。由于CBC的解密算法从不输出 ⊥,它总是输出某个消息,所以攻击者可以轻松赢得游戏。任何旧的随机密文解密后都会得到非 ⊥ 的结果,因此攻击者直接赢得了密文完整性游戏。这是一个不提供认证加密的CPA安全密码方案的简单例子。
认证加密的两个重要推论
接下来,我们看看认证加密带来的两个重要安全推论。
-
真实性:这意味着攻击者无法欺骗接收者Bob,让他相信Alice发送了一条她实际并未发送的消息。攻击者可以与Alice交互,让她加密任意选择的消息(即选择密文攻击)。然后,攻击者的目标是产生一个并非由Alice实际创建的密文。由于攻击者无法赢得密文完整性游戏,他无法做到这一点。这意味着,当Bob收到一个能通过解密算法正确解密的密文时,他知道该消息必定来自知道秘密密钥
K的人。特别是,如果只有Alice知道K,那么Bob就知道该密文确实来自Alice,而不是攻击者发送的篡改版本。- 需要注意的一点是:认证加密本身不防御重放攻击。攻击者可能拦截了从Alice到Bob的某个密文,然后重新发送它,而Bob看来这两个密文都是有效的。例如,Alice可能发送消息“转账100美元给Charlie”,然后Charlie可以重放该密文,导致Bob再次转账100美元给Charlie。因此,任何加密协议都需要防御重放攻击,而这并非认证加密直接防止的。我们将在后面讨论重放攻击。
-
抵御选择密文攻击:认证加密能够防御一种非常强大的攻击者类型,即能够发起所谓“选择密文攻击”的攻击者。我们将在下一节详细讨论这一点。

总结

本节课中,我们一起学习了认证加密的定义。我们了解到,一个安全的认证加密方案必须同时满足选择明文攻击下的语义安全和密文完整性。它不仅能保证消息的机密性,还能保证消息的真实性和完整性,确保接收者收到的消息确实来自预期的发送方且未被篡改。我们还看到了一个不满足认证加密的例子(CBC模式),并了解了认证加密带来的两个重要安全推论:消息真实性和对选择密文攻击的潜在防御能力。
037:选择密文攻击 🔐
在本节课中,我们将学习一个强大的安全概念——选择密文攻击(CCA),并理解为何认证加密是抵御此类攻击的正确安全概念。我们将通过具体的攻击示例和形式化定义来阐明这一点。


概述
上一节我们定义了认证加密,但并未深入解释为何它是正确的安全概念。本节中,我们将展示认证加密实际上是一个非常自然的安全概念,因为它能够抵御一种非常强大的攻击,即选择密文攻击。
选择密文攻击的实例
以下是选择密文攻击在现实中的两个具体例子。
第一个例子中,攻击者可以向解密服务器提交任意密文。如果解密后的明文以特定字符串(如“destination=25”)开头,服务器就会将明文直接返回给攻击者。这允许攻击者获取某些特定密文的解密结果。

第二个例子涉及网络协议。攻击者可以向服务器提交加密的TCP/IP数据包。如果服务器返回一个ACK确认包,攻击者就知道解密后的明文具有有效的校验和;否则,校验和无效。这同样是选择密文攻击,攻击者通过提交密文并观察服务器的反应,来了解该密文解密后的部分信息。
选择密文安全的形式化定义
为了应对这类威胁,我们定义一个非常通用的安全概念,称为选择密文安全(CCA安全)。在这个模型中,我们赋予攻击者极大的能力。
攻击者可以进行两种查询:
- 选择明文攻击(CPA)查询:攻击者提交两个等长的消息
M0和M1,挑战者返回其中一条消息的加密结果。 - 选择密文攻击(CCA)查询:攻击者提交任意一个密文
C(不能是之前CPA查询中获得的挑战密文),挑战者返回该密文的解密结果。

攻击者的目标是判断在CPA查询中,他收到的是 M0 还是 M1 的加密结果。我们说一个加密方案是CCA安全的,如果攻击者无法区分他处于实验0(加密 M0)还是实验1(加密 M1),即使他可以进行上述两种查询。
CBC模式为何不抗CCA攻击
让我们通过一个简单例子说明,使用随机IV的CBC模式加密并不具备CCA安全性。

假设攻击者提交两个单块消息 M0 和 M1 进行CPA查询,并收到密文 C = (IV, c),其中 c 是核心密文块。
攻击者可以构造一个新密文 C‘ = (IV ⊕ 1, c)。由于 C‘ 不等于挑战密文 C,攻击者可以将其作为CCA查询提交。
解密 C‘ 时,由于IV被异或了1,解密得到的明文将是 M0 ⊕ 1 或 M1 ⊕ 1。攻击者收到这个结果后,可以立即判断出他收到的是 M0 还是 M1 的加密,从而轻松赢得CCA安全游戏。
这个攻击精确地模拟了我们之前看到的第一个主动攻击实例,攻击者通过微调密文并诱使解密方为其解密,从而窃听本不该他获得的消息。

认证加密蕴含CCA安全
一个关键的定理是:认证加密意味着选择密文安全。这正是认证加密成为一个如此自然概念的原因。
定理表述如下:如果一个加密方案提供认证加密(即同时满足CPA安全和密文完整性),那么它也是CCA安全的。
证明思路非常直观:
- 在CCA安全游戏中,攻击者会提交CCA查询以获得某些密文的解密。
- 由于方案具有密文完整性,攻击者无法伪造出能解密为有效明文(而非特殊错误符号
⊥)的新密文。 - 因此,在CCA游戏中,挑战者可以将所有CCA查询的响应都替换为
⊥,而攻击者无法察觉这一变化。 - 一旦CCA查询的响应被固定为
⊥,它们就不再向攻击者提供任何有用信息。 - 此时,整个游戏就退化为了一个标准的CPA安全游戏。
- 因为方案是CPA安全的,所以攻击者无法区分实验0和实验1。
通过这一系列推理,我们证明了认证加密方案必然是CCA安全的。
重要说明与限制
认证加密确保了机密性,即使攻击者能够解密一部分密文,甚至发动全面的选择密文攻击,他仍然无法破坏系统的语义安全。
然而,必须记住其工具局限性:
- 无法防止重放攻击:认证加密本身不防御重放攻击,需要额外的机制(如序列号、时间戳)来应对。
- 错误信息泄露:如果解密引擎在拒绝一个密文时,不仅输出
⊥,还通过错误信息、时间差等方式泄露更多关于“为何被拒绝”的信息(例如,是因为MAC校验失败还是密文格式错误),那么这可能完全破坏认证加密系统的安全性。我们将在后续章节看到此类精巧的攻击。
总结


本节课中,我们一起学习了选择密文攻击(CCA)这一强大的攻击模型。我们看到了它在现实中的实例,并形式化地定义了CCA安全。通过分析,我们发现CBC模式等传统加密方案无法抵御CCA攻击。最后,我们证明了认证加密这一组合概念天然地蕴含了CCA安全性,因为它同时保证了机密性(CPA安全)和完整性(密文不可伪造),使得攻击者即使能进行选择密文查询,也无法获得有效信息来破坏加密。在下一节中,我们将转向如何具体构建认证加密系统。
038:基于密码和MAC的构造

在本节课中,我们将学习如何构造认证加密系统。我们已经掌握了CPA安全加密和安全的消息认证码,一个很自然的问题是:我们能否将两者结合起来以获得认证加密?这正是本节课要探讨的内容。
认证加密的概念于2000年在两篇独立的论文中被提出。在此之前,许多密码学库提供的API是分别支持CPA安全加密和MAC的。

例如,一个函数用于实现CPA安全加密(如使用随机IV的CBC模式),另一个函数用于实现MAC。每个希望实现加密的开发者都需要自己分别调用CPA安全加密方案和MAC方案。特别是,每个开发者都必须发明自己的方式来组合加密和MAC,以提供某种形式的认证加密。但由于当时组合加密和MAC的目标尚未被明确定义(认证加密的概念还未出现),因此并不清楚哪些组合是正确的,哪些是错误的。

正如我所说,每个项目都必须发明自己的组合方式,而事实上,并非所有组合都是正确的。我可以告诉你,软件项目中最常见的错误基本上就是错误地组合了加密和完整性机制。希望在本模块结束时,你能知道如何正确地组合它们,从而避免自己犯这些错误。
接下来,让我们看看不同项目引入的一些CPA安全加密和MAC的组合方式。

这里有三个例子。首先,在所有三个例子中,都有一个独立的加密密钥和一个独立的MAC密钥。这两个密钥彼此独立,都在会话建立时生成。我们将在课程后面看到如何生成这两个密钥。
第一个例子是SSL协议。SSL组合加密和MAC以期实现认证加密的方式如下:你获取明文M,然后使用MAC密钥KI计算消息M的标签,接着将标签附加到消息后面,最后加密消息和标签的组合,得到最终的密文。这是第一种方案。
第二种方案是IPSec的做法。这里,你首先加密消息,然后在得到的密文上计算标签。注意,标签本身是在生成的密文上计算的。
第三种方案是SSH协议的做法。SSH使用CPA安全加密方案加密消息,然后附加一个消息的标签。IPSec和SSH的区别在于,IPSec的标签是在密文上计算的,而SSH的标签是在明文上计算的。
这是三种完全不同的组合加密和MAC的方式。问题是:哪一种方案是安全的?我会让你思考一下,然后我们继续一起分析。
好的,让我们从SSH方法开始。在SSH方法中,你注意到标签是在消息上计算的,然后以明文形式附加到密文后面。这实际上是一个很大的问题,因为MAC本身并非设计用来提供机密性。MAC仅设计用于完整性。事实上,一个MAC在其标签中输出消息M的几个比特是完全正常的,这仍然是一个完美的标签。然而,如果我们这样做,就会完全破坏这里的CPA安全性,因为消息的一些比特会在密文中泄露。因此,尽管SSH的具体实现是好的,协议本身并未因这种特定组合而受损,但通常不建议使用这种方法,因为MAC签名算法的输出可能会泄露消息的比特。
现在让我们看看SSL和IPSec。事实证明,推荐的方法实际上是IPSec方法。因为无论使用什么CPA安全系统和MAC密钥,这种组合总能提供认证加密。让我简要解释一下原因:一旦我们加密了消息,消息内容就被隐藏在密文中。当我们在密文上计算标签时,这个标签实际上“锁定”了密文,确保没有人能产生一个看起来有效的不同密文。因此,这种方法确保了对密文的任何修改都会被解密者检测到,因为MAC将无法验证。
事实证明,SSL方法存在一些病态的例子,当你将CPA安全加密系统与安全MAC结合时,结果容易受到选择密文攻击,因此实际上并不能提供认证加密。可能发生这种情况的原因是加密方案和MAC算法之间存在某种不良交互,从而导致存在选择密文攻击。
因此,如果你正在设计一个新项目,现在的建议是始终使用“先加密后MAC”,因为无论你组合哪种CPA安全加密和安全MAC算法,它都是安全的。现在,为了设定术语,SSL方法有时被称为“先MAC后加密”。


IPSec方法被称为“先加密后MAC”。SSH方法(尽管你不应该使用它)被称为“加密和MAC”。好的,我经常会用“加密和MAC”以及“先MAC后加密”来区分SSL和IPSec。
好的,重复一下我刚才所说的:IPSec方法“先加密后MAC”总是提供认证加密。如果你从一个CPA安全密码和一个安全MAC开始,你总会得到认证加密。正如我所说,“先MAC后加密”实际上存在一些病态情况,其结果容易受到CCA攻击,因此不提供认证加密。然而,故事比这更有趣一些,因为事实证明,如果你实际使用的是随机计数器模式或随机CBC模式,那么对于这些特定的CPA安全加密方案,“先MAC后加密”实际上确实能提供认证加密,因此是安全的。
事实上,这里还有一个更有趣的转折:如果你使用随机计数器模式,那么你的MAC算法只需要是“一次性安全”的就足够了,它不必是一个完全安全的MAC。它只需要在密钥用于加密单个消息时是安全的。当我们讨论消息完整性时,我们看到实际上存在比完全安全的MAC快得多的“一次性安全”MAC。因此,如果你使用随机计数器模式,“先MAC后加密”实际上可能产生更高效的加密机制。
然而,我要再次重申这一点。建议是使用“先加密后MAC”,我们将看到一些未使用“先加密后MAC”的系统所遭受的攻击。所以,为了确保安全,你无需对此思考太多,我再次建议你始终使用“先加密后MAC”。
一旦认证加密的概念变得更加流行,一些标准化的组合加密和MAC的方法出现了,甚至被国家标准与技术研究院标准化。我将提到其中的三个标准,其中两个由NIST标准化。


它们被称为伽罗瓦计数器模式和CBC计数器模式。让我解释一下它们的作用。伽罗瓦计数器模式基本上使用计数器模式加密(或随机计数器模式)与Carter-Wegman MAC(一种非常快的Carter-Wegman MAC)。在GCM中,Carter-Wegman MAC的工作方式基本上是对被MAC处理的消息进行哈希,然后使用PRF加密结果。GCM中的这个哈希函数已经非常快,以至于GCM的大部分运行时间由计数器模式加密主导。英特尔甚至引入了一个特殊指令PCLMULQDQ,专门设计用于使GCM中的哈希函数运行得尽可能快。
现在,CCM计数器模式是另一个新标准,它使用CBC-MAC,然后是计数器模式加密。你注意到这种机制使用了类似SSL的“先MAC后加密”,所以这实际上不是推荐的做法。但由于使用了计数器模式加密,这实际上是一个完全没问题的加密机制。我想指出关于CCM的一点是,一切都基于AES。你注意到它使用AES进行CBC-MAC,并使用AES进行计数器模式加密。因此,CCM可以用相对较少的代码实现,因为你只需要一个AES引擎,别无其他。
😊,正因为如此,CCM实际上被Wi-Fi联盟采用。事实上,如果你使用加密的Wi-Fi 802.11i,那么你基本上每天都在使用CCM来加密你的笔记本电脑和接入点之间的流量。
还有另一种模式叫做EAX,它使用计数器模式加密,然后是CMAC。再次注意,这是“先加密后MAC”,这是另一种可以使用的良好模式。我们稍后将比较所有这些模式。
现在我想提一下,首先,所有这些模式都是基于Nonce的。换句话说,它们不使用任何随机性,但它们确实将Nonce作为输入,并且Nonce对于每个密钥必须是唯一的。换句话说,正如你记得的,密钥-Nonce对永远、永远、永远不应该重复,但Nonce本身不必是随机的。例如,使用计数器作为Nonce是完全没问题的。


另一个要点是,实际上所有这些模式都是所谓的“带关联数据的认证加密”。这是认证加密的一个扩展,在网络协议中经常出现。AEAD背后的想法是,实际上提供给加密模式的消息并不打算被完全加密,只有部分消息打算被加密,但整个消息都打算被认证。一个很好的例子是网络数据包,比如IP数据包,它有一个头部和一个有效载荷。通常,头部不会被加密,例如,头部可能包含数据包的目的地。但头部最好不要被加密,否则沿途的路由器将不知道如何路由数据包。因此,通常头部以明文发送,而有效载荷当然总是被加密的。但你希望的是头部被认证。😊,不被加密,但被认证。
这正是这些AEAD模式所做的。它们将认证头部,然后加密有效载荷,但头部和有效载荷在认证中被绑定在一起,因此实际上无法分离。这并不难做到。在这三种模式(GCM、CCM和EAX)中,MAC应用于整个数据,但加密仅应用于需要加密的部分数据。
我想向你展示这些带关联数据的认证加密方案的API是什么样的。例如,这是OpenSSL中GCM的API。你调用初始化函数来初始化加密模式。你注意到你给它一个密钥和一个Nonce。Nonce再次强调,不必是随机的,但必须是唯一的。初始化后,你将调用这个加密函数,你给它将被认证但不加密的关联数据,以及将被认证和加密的数据,它返回完整的密文,这是对数据的加密,但当然不包括AED,因为AED将以明文发送。

现在我们已经理解了这种“先加密后MAC”的模式,我们可以回到MAC安全的定义,我可以向你解释一些在我们查看该定义时可能有点模糊的东西。
如果你还记得,从我们安全MAC的定义中得出的一个要求是,给定一个消息-MAC对,攻击者不能为同一消息M产生另一个标签。换句话说,即使攻击者已经拥有消息M的一个标签,他也不应该能够为同一消息M产生一个新标签。这真的不清楚为什么这很重要,谁在乎攻击者是否已经拥有消息M的标签?谁在乎他是否能产生另一个标签?嗯,事实证明,如果MAC没有这个属性,换句话说,给定一个消息-MAC对,你可以为同一消息产生另一个MAC,那么这个MAC将导致不安全的“先加密后MAC”模式。
因此,如果我们希望我们的“先加密后MAC”具有密文完整性,那么我们的MAC安全必须包含这种强安全概念,这当然是正确的,因为我们正确地定义了它。所以,让我们看看如果实际上很容易产生这种伪造,会出什么问题。

我将向你展示对由此产生的“先加密后MAC”系统的选择密文攻击。由于该系统存在选择密文攻击,这必然意味着它不提供认证加密。让我们看看。
攻击者将首先发送两条消息M0和M1。他将像往常一样收到其中一条的加密,要么是M0的加密,要么是M1的加密。由于我们使用的是“先加密后MAC”,攻击者收到一个密文,我们称之为C0,以及密文C0上的一个MAC。
现在,我们说过,给定MAC和消息,攻击者可以为同一消息产生另一个MAC。所以他接下来要做的就是为消息C0产生另一个MAC。现在他有了一个新的密文C0,T‘,这是一个完全有效的密文,T’是C0的有效MAC。
因此,攻击者现在可以提交一个关于C‘的选择密文查询,这是一个有效的选择密文查询,因为它不同于C,它是一个新的密文。可怜的挑战者现在被迫解密这个密文C‘,所以他将发回C’的解密结果。这是一个有效的密文,因此C‘的解密结果是消息M。现在攻击者刚刚得知了B的值,因为他可以测试M是等于M0还是等于M1。结果,他可以输出B并获得优势1,从而击败该方案。
所以再次强调,如果我们的MAC安全不包含这里的这个属性,那么就会存在对“先加密后MAC”的选择密文攻击,因此它将不安全。所以我们正确定义MAC安全这一事实意味着“先加密后MAC”确实提供了认证加密,并且我们讨论过的所有MAC实际上都满足这种强不可伪造性。

有趣的是,故事并没有就此结束。正如我们之前所说,在认证加密概念被引入之前,每个人都只是以各种方式组合MAC和加密,以期实现某种认证加密。在认证加密的概念被形式化和严格化之后,人们开始挠头思考:“嘿,等等,也许我们可以比组合MAC和加密方案更高效地实现认证加密。”
事实上,如果你思考一下这种MAC和加密组合的工作方式,假设我们将计数器模式与CBC-MAC结合。那么对于每个明文块,你首先必须为计数器模式使用你的分组密码,然后你还必须为CBC-MAC再次使用你的分组密码。这意味着,如果你将CPA安全加密与MAC结合,对于每个明文块,你必须评估你的分组密码两次,一次用于MAC,一次用于加密方案。
所以很自然的问题是:我们能否直接从PRP构造一个认证加密方案,使得我们每个块只需评估PRP一次?事实证明答案是肯定的,有一个美丽的构造叫做OCB,它几乎做了你想要的一切,并且比单独从加密和MAC构建的构造快得多。

我写下了OCB的示意图,我不想详细解释它,我只想从高层次上解释一下。这里我们在顶部有输入的明文。你首先注意到,OCB是完全可并行化的。所以每个块都可以独立于其他块进行加密。😊。

另一个要注意的是,正如我承诺的,每个明文块只评估一次你的分组密码,然后在最后再评估一次以构建你的认证标签。OCB除了分组密码之外的开销是最小的,你只需要评估一个非常简单的函数P。Nonce进入这个P,你注意到密钥也进入这个P,然后有一个块计数器进入这个P。所以你只需为每个块评估这个函数P两次,并在使用分组密码加密之前和之后对结果进行异或操作,就是这样。这就是你需要做的全部,然后你就得到了一个从分组密码构建的非常快速和高效的认证加密方案。
所以OCB实际上有一个与之相关的很好的安全定理,我将在本模块结束时指向一篇关于OCB的论文,在那里我会列出一些你可以查阅的进一步阅读论文。
你可能会想,如果OCB比我们迄今为止看到的所有东西都好得多,比所有这三个标准(CCM、GCM和EAX)都好,为什么OCB没有被使用?或者为什么OCB不是标准?答案有点令人遗憾,OCB没有被使用的主要原因是由于各种专利,我就说到这里。
为了总结这一节,我想向你展示一些性能数据。在右边,我列出了你不应该使用的模式的性能数据,这是针对随机计数器模式和随机CBC的,你也可以看到CBC-MAC的性能基本上与CBC加密的性能相同。

好的,现在这里是认证加密模式。这些是你应该使用的模式。你不应该单独使用前两个,你永远不应该使用前两个,因为它们只提供CPA安全性,实际上不提供针对主动攻击的安全性。你应该只使用认证加密进行加密。所以我列出了三个标准的性能数据。让我提醒你,GCM基本上使用一个非常快的哈希,然后使用计数器模式进行实际加密。你可以看到GCM相对于计数器模式的开销相对较小。
CCM和EAX都使用基于分组密码的加密和基于分组密码的MAC。因此,它们大约比计数器模式慢两倍。你可以看到OCB实际上是这些模式中最快的,主要是因为它每个消息块只使用一次分组密码。
基于这些性能数据,你可能会认为GCM正是应该始终使用的正确模式。事实证明,如果你在空间受限的硬件上,GCM并不理想,主要是因为它的实现需要比其他两种模式更大的代码量。然而,正如我所说,英特尔专门添加了指令来加速GCM模式,因此在英特尔架构上实现GCM需要很少的代码。但在其他硬件平台上,比如智能卡或其他受限环境中,实现GCM的代码量将比其他两种模式大得多。但如果代码大小不是限制,那么GCM是应该使用的正确模式。
为了总结这一节,我想再说一次,当你加密消息时,你必须使用认证加密模式。

推荐的方法是使用标准之一,即这三种模式之一来提供认证加密。不要自己实现加密方案,换句话说,不要自己实现“先加密后MAC”,只需使用这三种标准之一。现在许多密码学库为这三种模式提供标准API,这些是你应该使用的,而不是其他任何东西。

在下一节中,我们将看看当你试图自己实现认证加密时,还会出什么问题。


本节课总结


在本节课中,我们一起学习了如何构造认证加密系统。我们首先回顾了历史上开发者自行组合CPA安全加密和MAC时遇到的问题,然后分析了三种常见的组合方式(SSL/IPSec/SSH)的安全性,并得出结论:“先加密后MAC”是安全且推荐的方法。接着,我们介绍了三种标准化的认证加密模式(GCM、CCM、EAX)及其特点,包括它们支持“带关联数据的认证加密”。我们还探讨了MAC安全定义中一个关键属性对于“先加密后MAC”安全性的重要性。最后,我们简要介绍了更高效的OCB模式及其未被广泛采用的原因,并通过性能对比强调了在实际应用中应优先使用标准化的认证加密API,而非自行实现。
039:TLS 1.2

在本节课中,我们将要学习认证加密在现实世界中的应用。我们将以TLS协议为例,详细解析其工作原理,并了解一个错误实现的反面教材。
概述
TLS(传输层安全)协议是互联网上保障通信安全的核心。它使用认证加密来确保数据的机密性和完整性。我们将首先了解TLS记录协议如何工作,然后分析早期版本中的安全漏洞,最后通过一个反面案例(WEP协议)来理解错误设计的后果。
TLS记录协议结构

TLS记录协议负责数据的加密传输。每个TLS记录都从一个头部开始,后面跟着加密的数据。记录的最大长度为16千字节,如果数据超过此限制,则会被分割成多个记录。
TLS使用单向密钥,这意味着从浏览器到服务器和从服务器到浏览器使用不同的密钥。这些密钥在TLS密钥交换协议中生成,我们将在课程第二部分讨论。目前,我们假设这些密钥已经建立,并且浏览器和服务器都知道它们。
状态加密与计数器
TLS记录协议使用状态加密,这意味着每个数据包的加密都依赖于浏览器和服务器内部维护的特定状态。这个状态的核心是两个64位计数器。
- 浏览器到服务器计数器:用于从浏览器发送到服务器的流量。
- 服务器到浏览器计数器:用于从服务器发送到浏览器的流量。

当会话首次初始化时,这两个计数器被设置为零。每当发送一个记录时,相应的计数器就会递增。接收方在收到记录后也会递增其本地的计数器副本。这些计数器的目的是防止重放攻击,因为攻击者无法简单地记录并重放一个数据包,因为到那时计数器已经改变,验证会失败。
记录加密流程详解
上一节我们介绍了TLS记录的结构和状态机制,本节中我们来看看一个记录是如何被具体加密和发送的。TLS使用“先MAC后加密”的模式,其中MAC算法是HMAC-SHA1,加密算法是AES-128的CBC模式。
以下是浏览器向服务器发送数据时的加密步骤,该过程使用浏览器到服务器的密钥(该密钥本身又包含一个MAC密钥和一个加密密钥):
-
计算MAC:首先,计算以下数据的MAC值:
- 记录头部(类型、版本、长度)。
- 有效载荷数据。
- 当前计数器的值。
计算完成后,计数器递增。值得注意的是,计数器的值虽然包含在MAC计算中,但从不在记录中发送。因为接收方(服务器)根据其本地状态知道预期的计数器值。
-
构建待加密数据:将上一步计算出的MAC标签附加到有效载荷数据之后。
-
填充:将头部、数据和MAC标签一起填充到AES块长度的整数倍。填充方式是,如果需要填充5个字节,则填充
0x05五次(即0x05 0x05 0x05 0x05 0x05)。 -
CBC加密:使用加密密钥和一个新鲜的、随机的初始化向量(IV)对填充后的数据进行CBC加密。这个IV随后会嵌入到密文中。
-
组装记录:最后,将明文头部(类型、版本、长度)预置到加密后的数据前面,形成完整的TLS记录并发送。
用伪代码表示核心加密过程:
# 伪代码:TLS记录加密(浏览器到服务器方向)
def encrypt_record(header, data, mac_key, enc_key, counter):
# 1. 计算MAC(包含头部、数据和计数器)
tag = HMAC-SHA1(mac_key, header || data || counter)
counter += 1 # 递增状态
# 2. 连接数据和MAC
plaintext = data || tag
# 3. 填充
pad_len = calculate_padding_length(plaintext)
padding = bytes([pad_len] * pad_len)
padded_plaintext = plaintext || padding
# 4. 生成随机IV并CBC加密
iv = generate_random_iv()
ciphertext = AES-128-CBC-encrypt(enc_key, iv, padded_plaintext)
# 5. 组装最终记录(头部是明文的)
final_record = header || iv || ciphertext
return final_record
记录解密与验证流程
当服务器收到一个加密记录时,它会使用对应的密钥和计数器进行解密和验证。
以下是解密步骤:
- 解密:使用加密密钥对密文部分进行CBC解密。
- 检查填充格式:查看解密后数据的最后一个字节(假设为
pad_len),并验证最后pad_len个字节是否都等于pad_len。如果不是,则发送“错误记录MAC”警报并终止连接。 - 移除填充:如果填充格式正确,则根据
pad_len移除最后pad_len个字节。 - 提取并验证MAC:从移除填充后的数据中分离出MAC标签。然后,使用本地的MAC密钥、记录头部、解密出的数据以及服务器本地认为正确的计数器值来计算MAC,并与提取的标签进行比较。
- 处理结果:
- 如果MAC验证失败,发送“错误记录MAC”警报并终止连接。
- 如果MAC验证成功,则移除标签和头部,剩余部分即为明文数据,交给上层应用。
这个流程巧妙地利用计数器防止了重放攻击。如果攻击者重放一个旧记录,服务器本地的计数器值已经不同,导致MAC验证失败。由于双方隐式地知道计数器值,因此无需在记录中传输,节省了带宽。
TLS早期版本的安全漏洞
我们刚刚分析了TLS 1.1/1.2中相对安全的实现。然而,早期版本的TLS(如1.0及更早版本)存在严重错误,导致了多种攻击。
以下是两个主要漏洞:

1. 可预测的IV(链式IV)
在TLS 1.0中,用于CBC加密的IV是可预测的——下一个记录的IV被设置为当前记录的最后一个密文块。这意味着攻击者如果观察到当前记录,就知道下一个记录的IV,从而可以破坏其语义安全性。这种攻击被称为BEAST攻击。TLS 1.1的解决方案是改用“显式IV”,即每个记录都使用自己随机的、不可预测的IV。
2. 填充预言攻击
在TLS 1.0中,服务器在解密失败时会返回不同的错误信息:
- 如果是因为填充无效,返回“解密失败”警报。
- 如果是因为MAC无效,返回“错误记录MAC”警报。
攻击者可以通过观察服务器返回的警报类型,来判断其发送的篡改后的密文是填充错误还是MAC错误。这为一种称为“填充预言攻击”的强力攻击打开了大门。TLS 1.1的修复方法是,无论解密失败的原因是填充错误还是MAC错误,统一返回“错误记录MAC”警报,从而掩盖了失败的具体原因。
核心教训:当解密失败时,绝不应该透露失败的具体原因。只需输出一个统一的“拒绝”信号即可。解释失败原因常常会为攻击者提供可利用的信息。
反面案例:WEP协议的错误设计
现在我们已经看到了TLS中相对正确的实现,让我们来看一个完全错误的协议设计案例:802.11b WEP(有线等效加密)。它几乎在各个方面都出了问题。
以下是WEP的工作流程:
- 发送方(如笔记本电脑)计算消息的CRC(循环冗余校验)校验和,并将其附加到消息后。
- 使用流密码(RC4)加密“消息+CRC”。加密密钥是
IV || K,其中IV每包变化,K是长期密钥。 - 将IV和密文一起发送给接收方(如接入点)。

WEP存在多个致命问题:
- IV重复:IV空间很小,必然重复,导致“两次一密”攻击。
- 相关密钥:密钥结构为
IV || K,只有IV变化,导致大量密切相关的密钥被用于RC4。RC4并非为此设计,会完全失效。 - 脆弱的完整性校验:即使忽略上述问题,其用于提供完整性的CRC机制也完全无效。
CRC的线性特性与攻击
WEP使用CRC校验来试图防止篡改,但CRC具有线性特性,这使其在密码学上完全不安全。
CRC的线性意味着:如果知道CRC(M),那么对于任何扰动P,可以很容易地计算出CRC(M XOR P),它等于CRC(M) XOR F(P),其中F是一个公开的已知函数。
基于此,攻击可以如下进行:
- 假设攻击者截获一个目标端口为80的数据包。
- 他想将其改为目标端口25。
- 他在密文中对应端口80的位置,异或上值
Δ = 25 XOR 80。由于流密码的特性,解密后该位置的明文也会被异或Δ,从而变成25。 - 为了通过CRC校验,他利用CRC的线性特性,在密文中CRC校验和所在的位置,异或上
F(Δ)。这样,解密后得到的CRC值就会自动变为适用于修改后消息(端口为25)的正确CRC值。
结论:CRC校验不能提供任何针对主动攻击的完整性保护,永远不应该被用作密码学上的完整性机制。正确的做法是使用加密学的MAC,如HMAC。
总结
本节课中我们一起学习了认证加密在现实协议中的应用。
- 我们深入剖析了TLS记录协议,了解了其如何使用状态计数器、先MAC后加密的模式来提供机密性、完整性和防重放攻击。
- 我们探讨了TLS早期版本(如1.0)由于使用链式IV和返回不同的解密错误信息而引入的严重漏洞(BEAST攻击、填充预言攻击),并理解了其修复方案。
- 最后,我们通过分析WEP协议这个反面教材,明白了使用流密码时IV管理的重要性、避免相关密钥的必要性,以及CRC线性特性如何导致完整性保护完全失效,强调了必须使用加密学MAC(如HMAC)来保证完整性。

在下一节中,我们将探讨针对错误实现的认证加密系统的真实攻击。
040:CBC填充攻击

在本节课中,我们将学习一种针对使用CBC模式加密的TLS记录协议的精巧攻击——填充预言攻击。我们将了解攻击的原理、实施方式以及防御措施。
概述

上一节我们介绍了认证加密的概念及其重要性。本节中,我们来看看一个具体的攻击实例,它利用了TLS协议在解密CBC加密数据时,因错误信息泄露而导致的漏洞。
认证加密回顾
认证加密意味着系统同时提供选择明文攻击(CPA)安全性和密文完整性。它能确保在存在主动攻击者的情况下保护机密性,并且攻击者无法在不被检测的情况下修改密文。认证加密还能防止强大的选择密文攻击。
然而,认证加密有一个显著的局限性:它无法帮助一个糟糕的实现。如果错误地实现了认证加密,那么你的实现将容易受到主动攻击。
标准构造与实践建议
我们之前也看过了标准的构造方法。以下是三种提供认证加密的标准:
- GCM
- CCM
- EAX
当你在实践中需要使用认证加密时,应该直接使用这三种标准之一,而不是尝试自己实现。本节展示的攻击将有力地证明这一点。

一般来说,提供认证加密的正确方法是 “先加密后MAC”。因为无论你组合使用哪种加密和MAC算法,只要它们本身实现正确,结果都将是一个安全的认证加密方案。
TLS记录协议与CBC解密流程
现在,让我们来看一个非常精巧的攻击,它针对的是TLS记录协议,特别是当它使用CBC加密时。
首先,简要回顾一下TLS解密的工作流程:
- 对接收到的密文进行CBC解密。
- 检查填充(pad)格式是否正确。例如,如果填充长度为5,格式应为
55555。如果格式不正确,则拒绝该密文。 - 如果填充格式正确,则检查MAC标签。
- 如果标签无效,再次拒绝记录。
- 如果标签有效,则认为剩余数据是可信的,并将其交给应用程序。
这里存在两种类型的错误:
- 填充错误
- MAC错误
关键在于,绝不能让攻击者知道发生了哪种错误。让我简要解释原因。
填充预言机的形成

假设攻击者能够区分这两种错误,即他能知道发生的是填充错误还是MAC错误。
结果就形成了一个所谓的填充预言机。攻击者可以拦截一个密文,并尝试解密它。他可以提交这个密文给服务器。服务器将解密它并检查填充格式。
- 如果填充无效,服务器会返回一个错误(填充错误)。
- 如果填充有效(攻击者自己构造的随机密文,其MAC很可能无效),服务器会返回另一个错误(MAC错误)。
因此,攻击者通过提交密文,就能知道解密后密文的最后几个字节是否构成有效的填充格式(例如,以1、22、333等结尾)。这泄露了关于解密后明文的信息。
这是一个典型的选择密文攻击示例:攻击者提交密文,并了解到一些关于结果明文的信息。

时序攻击与填充预言机的维持
你可能会想,TLS的旧版本确实通过返回不同的警报消息泄露了错误类型。但在此攻击被公开后,SSL实现改为总是报告相同的错误类型。仅看警报类型,攻击者无法区分错误。
然而,填充预言机依然存在。让我解释原因。
Canvel、Kohno等人观察到,TLS解密的实现方式是:先解密记录,然后检查填充。如果填充无效,解密立即中止并生成错误。如果填充有效,则继续检查MAC,如果MAC无效,再中止并生成错误。
这导致了时序攻击。攻击者发现:
- 如果填充无效,警报消息会很快发送回来(例如,平均21毫秒)。
- 如果填充有效但MAC无效,由于需要额外时间检查MAC,警报消息的生成会稍慢一些(例如,平均23毫秒)。
因此,即使返回相同的警报,攻击者只需观察生成警报所需的时间:时间短意味着填充无效;时间长意味着填充有效但MAC无效。这样,攻击者仍然拥有一个能告诉他填充是否有效的填充预言机。
利用填充预言机解密
现在,让我们看看如何利用填充预言机。我断言,如果攻击者拥有一个密文C,他可以利用填充预言机完全解密这个密文。
作为一个例子,假设他想获取M1,即密文第二个块的解密结果。
以下是攻击者会做的事情。他有一个拦截到的密文C,其解密过程遵循CBC模式:一个密文块会直接与下一个密文块的解密结果进行异或操作。攻击者的目标是获取M1。
以下是具体步骤:
- 首先,丢弃
C2,使得最后一个块就是他想解密的C1。 - 假设他对
M1的最后一个字节有一个猜测值G(G是0到255之间的一个值)。 - 攻击者将执行以下操作:他将值
G XOR 0x01异或到前一个块C0的最后一个字节中。 - 现在,考虑当这个修改后的两块密文被解密时会发生什么。
C0会被解密成一些我们不关心的垃圾数据。但当C1被解密时,其最后一个字节将与修改后的C0的最后一个字节进行异或。 - 因此,最终明文的最后一个字节将是原始的
M1最后一个字节,再异或上我们插入C0的额外值(G XOR 0x01)。 - 关键点来了:如果对
M1最后一个字节的猜测G是正确的,那么M1_last_byte XOR G就等于0,最终我们得到的明文最后一个字节就是0x01(即数字1的十六进制表示)。 - 数字
1是一个格式正确的填充。因此,填充有效,填充预言机会返回“填充有效”。 - 如果猜测
G不正确,我们得到的值很可能不是1,从而导致无效的填充,填充预言机会返回“填充无效”。
因此,攻击者可以创建他修改后的密文(只修改了第二个块),将其发送给填充预言机,然后根据结果学习最后一个字节是否等于G。

迭代解密整个块
现在,攻击者可以简单地对G从0到255重复此过程。这平均需要128次选择密文查询,他就能找到正确的G,从而得知M1的最后一个字节。
得知最后一个字节后,我们可以使用完全相同的过程来获取M1的倒数第二个字节。这次,我们不再使用单字节填充1,而是使用双字节填充0x02 0x02。因为我们已知最后一个字节,我们可以通过异或操作确保解密后明文的最后一个字节是0x02。然后,我们猜测倒数第二个字节G,直到填充预言机告诉我们填充有效(即形成了0202)。这需要额外的256次查询。
我们可以继续迭代这个过程。由于块长度为16字节,经过16 * 256 = 4096次查询后,我们就能得知整个M1块。这是一个相当严重的攻击,能够解密TLS记录的块。
攻击的现实限制与IMAP案例
了解TLS内部工作原理的人可能会质疑这个攻击的有效性。问题是,当TLS接收到一个具有错误填充或错误MAC的记录时,它会关闭连接并重新协商新密钥。因此,攻击者只能提交一次查询,之后密钥就失效了。
然而,这个攻击非常精巧,只要协议中存在此类错误,总会在某些场景下出现。在这个案例中,场景就是IMAP服务器。
IMAP是一种从邮件服务器读取邮件的流行协议,通常通过在其上运行TLS协议来保护。IMAP客户端每五分钟会连接一次IMAP服务器检查新邮件。连接时,它会先发送包含用户名和密码的登录消息。
这意味着,每五分钟,攻击者就能获得完全相同消息(特别是密码)的加密版本。因此,每五分钟他可以对包含密码的块进行一次猜测。
如果密码是8个字符长,攻击者只需逐个字符地恢复它们,每五分钟进行一次猜测。Canvel等人证明,在几个小时内,就可以基本恢复用户的密码。这是对TLS实现的一个相当严重的攻击。
防御措施与经验教训
针对此攻击的防御措施是:无论填充是否有效,都始终检查MAC。这样,响应所需的时间就相同了,从而消除了时序攻击,使得攻击更难实施。
这里有两个重要的经验教训:
- 加密与MAC的顺序至关重要:如果TLS使用了“先加密后MAC” 而不是“先MAC后加密”,那么整个问题将完全不存在。因为在“先加密后MAC”中,MAC首先被检查,只有MAC有效才会进行解密和填充检查。如果MAC无效,密文会被立即丢弃,我们甚至不会进行填充检查。因此,任何对密文的篡改或操作都会因为MAC失败而被立即丢弃。
- 错误信息泄露的危害:记住我曾说过,“先MAC后CBC”实际上可以提供认证加密,但前提是不泄露解密失败的原因。在这个案例中,填充预言机完全破坏了认证加密属性,我展示的攻击表明,在这种错误信息泄露的情况下,该模式不再提供认证加密。
思考题
假设在TLS中,不使用“先MAC后CBC”,而是使用“先MAC后计数器模式(CTR)加密”,填充预言机攻击是否仍然可能?
答案是否定的。原因很简单,因为计数器模式根本不使用任何填充。因此,这种填充预言机攻击只影响TLS中使用CBC加密的模式。TLS也支持计数器模式加密,而这些模式完全不受此类填充攻击的影响。
总结

本节课中,我们一起学习了针对CBC模式加密的TLS记录协议的填充预言机攻击。我们了解了攻击如何通过区分填充错误和MAC错误(或利用时序差异)形成预言机,并利用该预言机迭代解密整个密文块。我们还看到了该攻击在IMAP over TLS场景下的实际危害。最后,我们总结了防御措施(统一错误处理时间)和两个关键教训:正确使用“先加密后MAC”的重要性,以及避免泄露解密失败具体原因的必要性。在下一节中,我们将看到另一个针对认证加密系统的非常巧妙的攻击。
041:攻击非原子解密
在本节课中,我们将要学习一种针对认证加密系统的巧妙攻击。上一节我们介绍了填充预言攻击,本节中我们来看看另一种攻击,它利用了SSH二进制数据包协议中解密过程的非原子性。

概述
SSH协议使用一种称为“加密后MAC”的模式。具体来说,每个SSH数据包包含一个序列号、数据包长度、CBC填充长度、有效载荷和填充。整个红色区块(如下图所示)使用CBC模式加密,然后对整个明文数据包计算MAC,并将MAC以明文形式随数据包一起发送。

解密过程与漏洞
以下是SSH服务器解密数据包的过程:
- 服务器首先解密加密的数据包长度字段(即第一个密文块的前几个字节)。
- 然后,服务器根据解密出的长度字段值,从网络中读取相应数量的字节。
- 服务器使用CBC解密剩余的密文块,恢复整个SSH数据包。
- 最后,服务器检查明文数据包的MAC,如果无效则报告错误。
问题在于:数据包长度字段在解密后,在MAC验证之前就被直接用于确定要读取的数据包长度。这引入了一个严重的漏洞。
攻击原理
假设攻击者截获了一个特定的密文块 C,这是消息块 M 的直接AES加密结果。攻击者的目标是恢复 M。
攻击步骤如下:
- 攻击者构造一个数据包发送给服务器。该数据包以正常的序列号开始,然后将截获的密文块
C作为第一个密文块注入。 - 服务器收到后,解密第一个AES块的前几个字节,并将其解释为数据包的长度字段值
L。 - 接着,服务器期望从网络读取
L个字节,然后才会检查MAC是否有效。 - 攻击者开始一个字节一个字节地向服务器发送数据。
- 服务器每读取一个字节,就计数一次。
- 当服务器读取的字节数达到
L时,它会认为数据包已完整,于是开始验证MAC。 - 由于攻击者发送的都是垃圾字节,MAC验证必然失败,服务器会返回一个MAC错误信息。
- 关键点:攻击者一直在计数自己发送了多少字节。当他收到MAC错误时,他就知道服务器在触发错误前恰好读取了
L个字节。 - 因此,攻击者可以推断出:密文块
C解密后,其最高32位(即被解释为长度字段的部分)的值就是L。
通过这种方式,攻击者无需破解加密算法,就泄露了密文块 C 解密后前32位的信息。由于可以对任何密文块重复此攻击,攻击者可以逐步获取长消息中每个密文块的前32位。


错误根源与教训
这个攻击揭示了两个主要的设计错误:
- 非原子解密:解密操作不是原子的。解密算法没有将整个数据包作为输入,然后输出整个明文或直接拒绝。相反,它部分解密(获取长度字段),然后等待读取更多数据,最后才完成解密过程。这种非原子操作非常危险。
- 未经验证即使用:长度字段在通过MAC验证之前就被使用。任何解密出的字段在通过认证之前都不应该被使用。
如何修复?
如果让你重新设计SSH,你会做哪些最小改动来抵御这种攻击?以下是几个选项:
- 将长度字段以明文发送(如TLS所做的那样)。这样,攻击者就无法提交选择的密文进行攻击,因为长度字段从未被解密。
- 单独对长度字段计算MAC。这样服务器可以先读取并验证长度字段的MAC,确认其有效性后,再根据该长度读取后续字节,最后验证整个数据包的MAC。
- 使用“先加密后MAC”模式。但请注意,仅更换模式(如改用“加密后MAC”的变体)本身无法解决此问题,因为核心问题(长度字段在验证前被使用)依然存在。
最根本的教训是:不要自己设计或实现认证加密系统,应使用GCM等标准模式。如果必须自己实现,请确保使用“先加密后MAC”模式,并避免重复上述错误。
总结
本节课中我们一起学习了针对SSH协议中非原子解密过程的攻击。该攻击巧妙地利用了协议在验证MAC之前就使用解密出的长度字段这一缺陷,从而泄露了密文块的部分信息。这再次强调了使用经过严格验证的标准加密方案的重要性,以及在设计协议时确保解密操作的原子性和数据验证的及时性。
042:密钥派生

在本节课中,我们将学习如何从一个主密钥派生出多个会话密钥,以及如何从低熵的密码中安全地派生密钥。这是对称加密应用中的关键步骤。
概述
我们已经讨论了对称加密的核心概念。在进入下一个主题之前,我们还需要了解几个重要的细节。首先,我们将探讨如何从一个源密钥派生出多个密钥,这在实践中非常常见。其次,我们将学习如何从低熵的密码中派生密钥。
密钥派生函数
上一节我们介绍了对称加密的多种模式。本节中,我们来看看如何从一个源密钥生成多个会话密钥。
在实际应用中,我们通常需要多个密钥来保护一个会话。例如,在TLS协议中,我们需要为每个通信方向生成独立的密钥,每个方向又可能需要加密密钥、MAC密钥和初始化向量等多个密钥。问题在于,我们通常只通过硬件随机数生成器或密钥交换协议获得一个源密钥。密钥派生函数就是用来解决这个问题的。
KDF的作用是接收一个源密钥,并输出多个看似随机的密钥,用于保护会话。

源密钥均匀分布的情况
首先,假设我们有一个安全的伪随机函数,其密钥空间为K。同时,假设我们的源密钥SK在K中是均匀分布的。在这种情况下,源密钥本身就是PRF的一个均匀随机密钥,我们可以直接用它来生成会话密钥。

以下是构建KDF的方法:
- KDF的输入包括:源密钥、一个上下文字符串和一个长度参数。
- KDF的工作方式是:使用源密钥作为PRF的密钥,依次对
context || 0,context || 1,context || 2... 进行求值,直到生成了足够长度的输出比特流。 - 最后,根据需要从这个输出流中截取比特来生成所有会话密钥。
上下文字符串是一个唯一标识应用程序的字符串。它的目的是将不同应用程序的密钥派生过程分隔开。例如,即使SSH、Web服务器和IPSec三个服务从硬件随机数生成器获得了相同的源密钥,由于它们使用了不同的上下文字符串,最终派生出的会话密钥也将是独立且不同的。

源密钥非均匀分布的情况
然而,源密钥可能并非均匀分布。例如,密钥交换协议通常生成的是高熵密钥,但可能只均匀分布在密钥空间的某个子集中。硬件随机数生成器的输出也可能存在偏差。
如果源密钥不是PRF的均匀密钥,我们就不能再假设PRF的输出是随机的。直接使用上述KDF可能导致生成的会话密钥被攻击者预测。
为了解决这个问题,我们采用“先提取,后扩展”的范式来构建KDF。
第一步是使用一个“提取器”从源密钥中提取出一个伪随机密钥。提取器通常需要一个公开的、随机生成的“盐值”作为输入。盐值的作用是抵御可能破坏提取器的恶意分布。即使攻击者知道盐值,只要源密钥具有足够的熵,提取器就能输出一个在计算上不可区分的均匀随机密钥。
第二步,一旦我们获得了伪随机密钥,就可以使用之前介绍的方法,通过一个安全的伪随机函数将其扩展成我们所需数量的会话密钥。
标准实现:HKDF

标准化的KDF实现称为HKDF,它基于HMAC构建。HMAC在这里同时充当了提取器和扩展器所需的PRF。
在提取步骤中,我们使用公开的盐值作为HMAC的密钥,源密钥作为HMAC的数据。这样可以得到一个伪随机的中间密钥。
在扩展步骤中,我们使用这个中间密钥作为HMAC的密钥,通过迭代计算来生成所需长度的会话密钥。
总结:一旦获得源密钥,切勿直接将其用作会话密钥。正确的做法是将其输入一个KDF(如HKDF)来生成所有需要的会话密钥和随机数。
基于密码的密钥派生
上一节我们讨论了从高熵源密钥派生密钥。本节中,我们来看看如何从低熵的密码中派生密钥。
基于密码的KDF用于从密码生成加密密钥或MAC密钥。问题在于,密码的熵通常很低(估计约20比特),不足以直接生成安全的会话密钥。如果使用普通的KDF(如HKDF),派生出的密钥将容易受到字典攻击。
PBKDF通过两种手段来防御字典攻击:
- 使用一个公开的、固定的盐值。
- 使用一个计算缓慢的哈希函数。
标准方法是PKCS#5,特别是PBKDF1。其工作原理如下:
以下是PBKDF1的基本流程:
- 将密码和盐值连接起来。
- 对这个连接后的字符串进行一次哈希运算。
- 将上一步的输出作为输入,再次进行哈希运算。
- 重复步骤3很多次(例如1000次或100万次)。
- 将最终输出作为派生出的密钥。
迭代哈希很多次在现代CPU上对用户体验影响很小(可能只需零点几秒),但对于尝试所有可能密码的攻击者来说,每次尝试都需要进行同样多次的缓慢哈希计算,这将极大地拖慢字典攻击的速度,从而增加攻击成本。
注意:你不必自己实现PBKDF,所有密码学库都提供了相关函数(如derive_key_from_password)。你只需调用这些函数即可。但必须明白,从密码派生出的密钥熵值依然不高,只是让猜测变得尽可能困难。
总结

本节课中我们一起学习了密钥派生的两个核心场景。首先,我们了解了如何使用密钥派生函数从一个高熵的源密钥安全地派生出多个会话密钥,并介绍了标准的HKDF构造。其次,我们探讨了如何从低熵的密码中派生密钥,通过使用盐值和缓慢的哈希函数来抵御字典攻击。记住,永远不要直接使用源密钥或密码作为会话密钥,正确的做法是总是通过一个合适的KDF来处理它们。
043:确定性加密 🔐
在本节课中,我们将要学习确定性加密的概念。这是一种在实践中经常出现的加密方式,其特点是对于给定的消息和密钥,总是生成完全相同的密文。我们将探讨它的应用场景、安全性问题以及为什么它无法满足选择明文攻击(CPA)安全。
确定性加密的定义

上一节我们介绍了课程概述,本节中我们来看看确定性加密的具体定义。
确定性加密是指一种加密系统,对于给定的消息和密钥,总是映射到完全相同的密文。这意味着,如果我们用同一个密钥对同一消息加密三次,每次都会得到完全相同的密文。这里没有随机性,它是一个一致的加密方案。
用公式表示,对于一个确定性加密方案 E,其加密过程可以描述为:
C = E(K, M)
其中,对于固定的密钥 K 和消息 M,输出 C 总是相同的。
确定性加密的应用场景:加密数据库查询
确定性加密在实践中一个常见的应用场景是加密数据库查询。让我们来看看它是如何工作的。
设想有一个服务器,它需要将信息存储在一个加密数据库中。服务器存储的是记录,每条记录包含一个索引和一些数据。
以下是服务器处理记录的过程:
- 服务器首先加密这条记录。
- 索引部分使用密钥
K1加密,数据部分使用密钥K2加密。 - 加密后的记录被发送到数据库存储。


最终,数据库会存储许多加密记录,其中索引使用密钥 K1 加密,数据使用密钥 K2 加密。
当服务器稍后需要从数据库中检索一条记录时,如果加密是确定性的,那么操作会非常方便:
- 服务器只需向数据库发送它感兴趣的索引的加密值(例如,加密后的“Alice”)。
- 由于加密是确定性的,这个加密后的索引与当初写入记录时生成的加密索引完全相同。
- 数据库可以找到包含这个加密索引的记录,并将其发送回服务器。
这种机制的好处在于,数据库完全不知道数据库中存储了什么数据,甚至不知道服务器正在检索哪些记录,因为它看到的只是对加密索引的请求。
确定性加密的安全性问题
上一节我们看到了确定性加密的便利性,本节中我们来看看它带来的核心安全问题。
这应该会引起许多人的警觉,因为我们之前说过,确定性加密根本不可能达到选择明文攻击(CPA)安全。问题在于,攻击者可以观察不同的密文。如果他看到两次相同的密文,他就知道底层加密的消息也一定是相同的。
换句话说,通过观察密文,他可以了解到关于相应明文的一些信息,因为每次他看到两次相同的密文时,他就知道底层的消息是相等的。
在实践中,这会导致严重的攻击,特别是当消息空间很小时。例如,如果我们通过网络传输单个字节(比如一次传输一个击键),那么攻击者可以简单地建立一个所有可能密文的字典。如果只有单个字节,那么最多只有256种可能的密文。然后,通过学习这些密文的解密结果,他实际上可以通过在这个字典中查找来弄清楚所有未来的密文。
因此,在许多消息空间较小的情况下,确定性加密根本就是不安全的。
具体到加密数据库的例子,攻击者会看到:如果有两条记录在索引位置恰好有相同的密文,那么他现在就知道这两条记录对应于相同的索引。同样,尽管他不知道索引是什么,但他了解到了关于相应明文的一些信息。
形式化证明:确定性加密无法实现CPA安全

我想简要地提醒你,我们形式化地认为确定性加密不能是CPA安全的,这是通过描述一个在CPA游戏(选择明文攻击游戏)中获胜的对手来证明的。
让我快速回顾一下这个游戏是如何进行的:
- 游戏开始时,对手发送两条消息
M0和M0。记住,在这个游戏中,对手总是会得到左消息的加密或右消息的加密。在这种情况下,由于他在左右两边使用了相同的消息,他只会得到消息M0的加密。 - 在下一步,他将发送消息
M0, M1。现在,他将得到M0的加密或M1的加密,他的目标是判断他得到的是哪一个。 - 但由于加密是确定性的,这对他来说非常容易。他只需要测试
C是否等于C0。如果C等于C0,那么他知道他得到了M0的加密;如果C不等于C0,他知道他得到了M1的加密。 - 因此,如果
C等于C0,他输出0;如果C不等于C0,他输出1。他在这个特定游戏中的优势将是1,这是尽可能高的优势,意味着攻击者完全击败了选择明文安全性。
这只是形式化地说明,攻击者基本上了解到了比允许更多的关于明文的信息。

解决方案:限制消息类型
那么问题是我们该怎么办?事实证明,解决方案基本上是限制在单个密钥下可以加密的消息类型。
这个想法是:假设加密者永远、永远、永远不会在单个密钥下加密相同的消息。换句话说,消息-密钥对总是不同的,永远不会重复。因此,对于每一次加密,要么消息改变,要么密钥改变,或者两者都改变,但不能在同一个密钥下对同一消息加密两次。
为什么这种情况会发生呢?事实证明,有一些非常自然的情况会发生这种情况:
- 消息本身是随机的:例如,加密者正在加密密钥,而这些密钥是128位的密钥。那么,它们实际上极不可能重复。在这种情况下,当我们用一个主密钥加密密钥时,所有被加密的消息很可能总是不同的,因为这些密钥极不可能重复。
![]()
- 消息空间本身具有某种结构:例如,如果我们只加密唯一的用户ID。想象一下在数据库例子中,索引对应于一个唯一的用户ID,并且如果数据库中每个用户恰好有一条记录,那就意味着每条加密记录基本上都包含一个加密索引,并且该索引对于数据库中的所有记录都是不同的。
这是消息可能永远不会重复的两个原因,这也是实践中相当常见且合理的情况。
确定性选择明文攻击(Det-CPA)安全
现在,如果消息永远不会重复,也许我们实际上可以定义安全性,并给出满足我们定义的构造。
这引出了确定性选择明文攻击(Det-CPA) 的概念。让我解释一下它的含义。
和往常一样,我们有一个密码,定义为一个加密算法。我们有一个密钥空间、一个消息空间和一个密文空间。我们将像往常一样使用两个实验(实验0和实验1)来定义安全性。
这个游戏看起来与标准的选择明文攻击游戏非常相似,几乎是相同的游戏。攻击者发出查询,这些查询由消息对 M0 和 M1 组成(它们必须长度相同)。对于每个查询,攻击者要么得到 M0 的加密,要么得到 M1 的加密。攻击者可以反复这样做 Q 次。在游戏结束时,他应该说出他得到的是左消息的加密还是右消息的加密。
这是标准的选择明文攻击游戏。现在有一个额外的注意事项:如果比特 B 等于0(攻击者总是看到左消息的加密),那么左消息必须全部不同。换句话说,他永远不会看到同一消息被加密两次,因为这些左消息总是不同的。同样,我们要求所有右消息也互不相同。


这个要求,即所有左消息互不相同,所有右消息也互不相同,基本上抓住了这个用例:加密者永远不会使用一个特定的密钥多次加密相同的消息。因此,攻击者根本无法要求使用同一个密钥多次加密相同的消息。
需要指出的是,如果你回到我们之前对确定性加密的原始攻击,你会发现那实际上不是一个有效的确定性CPA游戏攻击,因为在那里攻击者确实要求对同一消息 M0 加密了两次。因此,在确定性CPA游戏中,这种攻击将是非法的攻击。通过排除这种攻击,我们实际上使得给出满足确定性CPA安全的构造成为可能。
和往常一样,我们说,如果对手无法区分实验0(他得到左消息的加密)和实验1(他得到右消息的加密),那么该方案在确定性CPA攻击下是语义安全的。我们要求对手在实验0中输出1的概率与在实验1中输出1的概率接近。如果对于所有高效的对手来说都是如此,那么我们就说该方案在确定性CPA攻击下是语义安全的。


不安全的构造示例:固定IV的CBC模式
我想做的第一件事是向你展示,使用固定初始化向量(IV)的密码分组链接(CBC)模式实际上并不是确定性CPA安全的。这是实践中一个常见的错误。有许多产品本应使用确定性CPA安全的密码,但它们却只使用带固定IV的CBC,并假设这是一种安全机制,而事实上并非如此。我想向你展示原因。
假设我们有一个在n位块上操作的伪随机置换(PRP),我们将使用这个PRP的CBC模式。如果消息有五个块,那么这个PRP将被调用五次来加密每个块。
以下是攻击如何进行:
- 攻击者首先请求加密消息
0^n || 1^n(即第一个块全0,第二个块全1)。左消息等于右消息,这意味着他只是想要这个0^n || 1^n消息的加密。 - 让我们看看密文是什么样子。为了完整起见,我将把固定IV作为密文的第一个元素写出。如果你考虑CBC的工作原理,密文的第二个元素基本上是
IV XOR消息第一个块的加密。在我们的例子中,消息的第一个块是全0。所以实际上加密的只是固定IV。因此,密文的第二个块将只是这个固定IV的加密。 - 接下来,攻击者将输出两个单块消息。左消息将是全0块,右消息将是全1块。注意这是一个有效的查询,因为左消息互不相同,右消息也互不相同。这些都是有效的确定性CPA查询。
- 现在攻击者会得到什么响应?如果他得到左消息的加密,那么单块消息
0^n的加密就只是固定IV的加密,正如我们之前看到的。然而,如果他得到右消息的加密,那将是1^n XOR固定IV的加密。 - 你可以看到攻击将如何进行。注意,如果这两个块碰巧相同,那么我们就知道他收到了左消息的加密(即
B=0)。如果它们不同,那么他知道B=1。因此,如果C[1](这个块)等于C1[1](那个块),他将输出0,否则输出1。
这基本上表明,使用固定IV的CBC是完全不安全的。第一个块可以很容易地被攻击。实际上,如果消息很短,你可以再次建立字典,并完全破坏那些希望使用固定IV的CBC来实现确定性CPA安全的系统。
所以不要这样做。我们实际上将在下一节中看到安全的确定性CPA构造。我再说一遍:如果你需要以一致的方式加密数据库索引,不要使用带固定IV的CBC来做,请使用我将在下一节中展示的方案。

另一个不安全的构造:固定IV的计数器(CTR)模式
让我问你一个关于固定IV的计数器(CTR)模式的同样问题:这会是一个确定性CPA安全的系统吗?在这里我提醒你,固定IV的CTR模式基本上是我们连接固定IV、固定IV+1、固定IV+L,我们加密所有这些计数器,连接结果,然后与消息进行异或操作得到密文。
你认为这会是确定性CPA安全的吗?
😊

我希望每个人都说“不”,因为这基本上是一种一次一密加密,如果我们用这个“一次一密”来加密不同的消息,基本上我们得到的是一个“两次一密”。
更精确地看,我在这里写下了确定性CPA游戏:
- 攻击者首先请求消息
M的加密,他会得到M XOR固定IV的加密。 - 然后他会请求两个不同的消息
M0和M1(它们都与M不同),所以M、M0和M1三者都是不同的消息。 - 然后他会得到
M的加密,现在他可以简单地发起标准的“两次一密”攻击。如果C XOR C'等于M XOR M0,他就知道C'是M0的加密;否则,他知道它是M1的加密。
同样,他将完全赢得这个游戏,优势通常为1。所以,固定IV的CTR模式作为确定性CPA密码也是完全不安全的。

所以,不要使用这些方案中的任何一个。相反,让我们使用我们将在下一节中描述的方案。

总结
在本节课中,我们一起学习了:
- 确定性加密的定义:对于固定密钥和消息,总是产生相同密文的加密方案。
- 它的一个主要应用场景:加密数据库索引查询,允许服务器在不暴露索引内容的情况下检索记录。
- 确定性加密的核心安全问题:它无法实现标准的选择明文攻击(CPA)安全,因为攻击者可以通过观察重复的密文推断出明文信息。
- 为了解决这个问题,引入了确定性CPA(Det-CPA)安全的概念,它限制攻击者不能查询同一密钥下相同消息的加密。
- 我们分析了两个不安全的常见构造:使用固定初始化向量(IV)的CBC模式和CTR模式,它们都不满足Det-CPA安全,因此不能用于需要确定性加密的场景(如加密数据库索引)。


关键要点是:当需要确定性加密时(例如为了可查询性),必须使用专门设计为Det-CPA安全的加密方案,而不是简单地重用标准CPA安全模式(如CBC或CTR)并固定其随机参数。
044:SIV与宽PRP 🔐
在本节课中,我们将学习两种提供确定性选择明文攻击(CPA)安全的加密方案构造方法。确定性加密在加密数据库索引等场景中非常有用,因为它能保证相同的明文总是产生相同的密文,从而支持基于加密索引的快速查询。我们将重点介绍SIV(合成初始化向量)模式和直接使用伪随机置换(PRP)的方法,并探讨如何为它们添加密文完整性。

确定性加密的需求与挑战
上一节我们介绍了确定性加密的概念。本节中,我们来看看如何构造能够抵抗确定性选择明文攻击的方案。
首先,确定性加密是必需的,例如在加密数据库索引时。之后,我们希望使用加密后的索引来查找记录。由于加密是确定性的,我们保证在进行查找时,加密后的索引与记录写入数据库时发送的加密索引完全相同。因此,这种确定性加密允许我们基于加密索引进行简单快速的查找。

问题在于,我们说过确定性加密不可能对一般的选择明文攻击安全,因为如果攻击者看到两个相同的密文,他就会知道底层加密的消息是相同的。因此,我们定义了“确定性选择明文安全”这一概念,这意味着只要加密者在使用给定密钥时,对同一消息的加密不超过一次,安全性就能得到保障。具体来说,每个密钥-消息对只用于一次加密,要么密钥改变,要么消息改变。
如前所述,我们正式定义了这个确定性CPA安全游戏。本小节的目标是实际给出确定性CPA安全的构造方案。
构造一:SIV(合成初始化向量)模式
我们首先要看的第一个构造叫做SIV(合成初始化向量)。其工作原理如下。
想象我们有一个通用的CPA安全加密系统。这里,K是密钥,M是消息,R表示加密算法使用的随机性。请记住,一个不使用“无意义”的CPA安全系统必须是随机的,因此我们明确写出变量R来表示加密算法执行加密时使用的随机字符串。例如,如果我们使用随机计数器模式,R就是随机计数器模式使用的随机IV(初始化向量)。当然,C是生成的密文。
此外,我们还需要一个伪随机函数F。它的作用是接收消息空间中的任意消息,并输出可用作CPA安全加密方案随机性的字符串R。这里的r实际上是集合R中的一个成员。我们假设F是一个将消息映射到随机字符串的伪随机函数。
SIV的工作方式如下。它使用两个密钥K1和K2来加密消息M。它首先将伪随机函数F应用于消息M,为CPA安全加密方案E导出随机性r。然后,它使用刚刚导出的随机性r来加密消息M,这将给我们一个密文C。最后,它输出这个密文C。
公式表示:
C = E(K2, M; r),其中 r = F(K1, M)
基本上,SIV模式首先从被加密的消息中导出随机性,然后使用这个导出的随机性实际加密消息以获得密文。
需要指出的是,例如,如果加密方案E恰好是随机计数器模式,那么随机性R就只是随机IV,它实际上会与密文一起输出。这意味着密文比明文略长,但这里的重点不是生成短密文,而是确保加密方案是确定性的。因此,如果我们多次加密相同的消息,每次都应该获得相同的密文。确实,每次我们都会获得相同的随机R,因此每次都会获得相同的密文C。
很容易证明,这种加密方案在确定性选择明文攻击下确实是语义安全的。原因在于,我们将伪随机函数F应用于不同的消息。如果我们对不同的消息应用F,那么F生成的随机字符串看起来就像是真正的随机字符串——每个消息对应一个不同的随机字符串。因此,CPA安全加密方案E总是使用真正的随机字符串来应用,而这正是它具备CPA安全性的情况。
因为这些R只是随机的、与随机字符串无法区分的值,所以最终的系统实际上将是CPA安全的。这只是其工作原理的直观解释,实际上可以相当直接地将其形式化为一个完整的证明。


现在我应该强调,这实际上非常适合长度超过一个AES块的消息。事实上,对于短消息,我们将看到一个稍微不同的、更适合这些短消息的加密方案。
SIV真正酷的地方在于,我们实际上免费获得了密文完整性。事实上,如果我们想添加完整性,我们不需要使用特殊的MAC(消息认证码);在某种意义上,SIV已经内置了一个完整性机制。让我解释一下我的意思。
首先,我们的目标是构建所谓的“确定性认证加密”(DE),它基本上意味着确定性CPA安全性和密文完整性。请记住,密文完整性意味着攻击者可以请求加密他选择的消息,然后他不应该能够产生另一个解密为有效消息的密文。
我想声称,事实上SIV自动提供了密文完整性,而无需嵌入MAC或其他任何东西。让我们看看为什么,特别是让我们看看当底层加密方案是随机计数器模式时SIV的一个特例。我们称之为SIV-CTR,以表示我们使用随机计数器模式的SIV。
让我再次提醒你SIV在这种情况下是如何工作的。我们取我们的消息,然后对其应用一个PRF,从而导出一个IV。然后,该IV将用于使用随机计数器模式加密消息。具体来说,我们将使用这个PRF F_CTR 用于随机计数器模式,我们基本上在IV、IV+1直到IV+L处评估这个F_CTR,然后将其与消息进行异或,从而得到最终的密文。这就是使用随机计数器模式的SIV。


现在让我们看看解密将如何工作。在解密过程中,我们将增加一个检查,这将提供密文完整性。
让我们看看解密将如何工作。这里我们有输入的密文,它包含IV和密文C。我们要做的第一件事是使用给定的IV解密密文,这将给我们一个候选明文M'。
现在,我们将从SIV的定义中重新应用PRF F到这个消息M'上。如果消息是有效的,我们应该得到与对手作为密文一部分提供的相同的IV。如果我们得到不同的IV,那么我们知道这个消息不是有效消息,我们直接拒绝该密文。
这真的很巧妙,它是一种非常简单的内置检查,以确保密文有效。我们只需检查解密后,如果我们重新推导IV,我们实际上会得到正确的IV;如果没有,我们就拒绝消息。我声称解密过程中的这个简单检查足以实际提供密文完整性,从而实现确定性认证加密。
我将在一个简单的定理中陈述这一点,基本上是说,如果F是一个安全的PRF,并且从F_CTR导出的计数器模式是CPA安全的,那么结果实际上就是一个确定性认证加密系统。
这个证明并不太难,用两句话让我向你展示为什么这是真的的粗略直觉。我们只需要论证密文完整性。我们之前已经论证过该系统具有确定性CPA安全性,我们只需要论证密文完整性。为了论证该系统具有密文完整性,让我们再次思考密文完整性游戏是如何工作的:对手请求加密一堆他选择的消息,他得到结果密文,然后他的目标是产生一个新的有效密文。
如果那个有效密文在解密时解密为某个全新的消息,那么当我们把这个新消息插入PRF F时,我们只会得到某个随机IV,并且极不可能与对手在他输出的密文中提供的IV匹配。因此,这表明当对手输出他的伪造密文时,该伪造密文中的消息基本上应该等于他的选择消息查询中的某个消息。否则,我们得到的IV将极不可能等于伪造密文中的IV。
但是,如果消息等于对手向我们查询的某个消息,那么我们得到的密文也将等于我们提供给对手的某个密文。那么他的伪造仅仅是我们给他的一个密文,因此这不是一个有效的伪造。他必须给我们一个新的密文才能赢得密文完整性游戏,但他给了我们一个旧的密文。这就是粗略的直觉。
我希望我快速地讲了一遍,希望它有点道理。如果没有,也不是世界末日,我将在模块末尾引用描述SIV的论文,如果你想更详细地了解,可以阅读并查看那篇论文。但无论如何,这是一个非常巧妙的想法,我想向你展示,即我们使用PRF为确定性计数器模式导出随机性这一事实,也在我们实际进行解密时为我们提供了一个完整性检查。
好的,所以当你需要时,这个SIV是进行确定性加密的一个好模式,特别是如果消息很长的话。如果消息很短,比如说小于16字节,实际上有更好的方法来做这件事,这就是我现在想向你展示的方法。
构造二:直接使用伪随机置换(PRP)
第二个构造实际上很简单。我们要做的就是直接使用一个PRP(伪随机置换)。具体做法如下。
假设(E, D)是一个安全的PRP。E用于加密,D用于解密。

我声称,如果我们直接使用E,那已经给了我们确定性CPA安全性。让我很快地告诉你为什么。
假设F是一个从集合X到X的真正随机可逆函数。请记住,我们的PRP与一个真正随机的可逆函数是无法区分的。让我们假装我们实际上有一个真正随机的可逆函数。

在实验0中,攻击者将看到他提交的一堆消息(左边的消息),他将看到的基本上是F在他提供的左边消息上的求值结果。在确定性CPA游戏中,所有这些消息都是不同的,所以他将得到的只是X中的Q个不同的随机值。是的,就是X中的Q个不同的随机值。
现在让我们想想实验1。在实验1中,他可以看到右边消息M_1^1到M_Q^1的加密结果。同样,所有这些消息都是不同的,所以他看到的只是X中的Q个随机不同的值。
因此,实验0和实验1中的这两个分布是相同的。基本上,在两种情况下,他看到的都只是X中的Q个不同的随机值。因此,他无法区分实验0和实验1。既然他无法对真正的随机函数做到这一点,他也无法对PRP做到这一点。
好的,这就解释了为什么直接用PRP加密已经给了我们一个CPA安全的系统,使用起来非常、非常简单。

这就是说,如果我们只想加密短消息,比如小于16字节,那么我们需要做的就是直接用AES加密它们,结果实际上将是确定性CPA安全的。所以,如果你的索引小于16字节,直接使用AES是很好的做法。
但是请注意,这不会提供完整性,我们马上就会看到如何添加完整性。但给你的问题是,如果我们有超过16字节的消息,我们该怎么办?一个选择是使用SIV,但如果我们也想使用这个构造呢?这实际上是一个有趣的问题:我们能否构造消息空间大于16字节的PRP?

如果你记得过去,我们从小消息空间的PRF构造了大消息空间的PRF。这里,我们将从小消息空间的PRP构造大消息空间的PRP。让我们看看如何做到。
宽块PRP:EME模式
假设(E, D)是一个在n位块上操作的安全PRP。
有一个称为EME的标准模式,它实际上将构造一个在N位块上操作的PRP,其中N远大于n。这将允许我们对远大于16字节的消息进行确定性加密,实际上,它可以和我们希望的一样长。
让我们看看EME是如何工作的,起初它可能有点令人生畏,但它不是一个困难的构造。让我们看看它是如何工作的。EME使用两个密钥K和L。实际上,在真实的EME中,L是从K派生出来的,但为了我们的目的,我们假设K和L是两个不同的密钥。


我们做的第一件事是取我们的消息X,将其分成块,然后用某个填充函数对每个块进行异或。我们使用密钥L通过函数P(我在这里不解释)导出一个填充,我们为每个块导出一个不同的填充,并将该填充异或到块中。
接下来,我们使用密钥K将PRP E应用于每个块,我们将这些输出称为PPP0、PPP1和PPP2。
接下来,我们将所有这些PPP异或在一起,并称结果为MP。然后我们用密钥K加密这个MP,并称这个加密的输出为MC。
然后,我们异或MP和MC,这给了我们另一个密钥M,我们用它来派生另一个填充。然后我们将这个填充的输出与所有的PPP进行异或,得到这些CC。
现在我们将所有这些CC异或在一起,得到一个值CC0,然后我们再用所有的E加密一次。我们用所有这些P再做一次填充,这实际上最终给出了EME的输出。
就像我说的,这可能看起来有点令人生畏,但实际上有一个定理表明,如果E的底层块大小是一个安全的PRP,那么实际上,这个构造EME在这个更大的块(N位)上是一个安全的PRP。这个构造的好处是你注意到它非常并行,实际上这就是为什么它有点复杂——每个块都是并行加密的。所以,如果你有一个多核处理器,你可以使用你所有的核心同时加密所有的块,然后会有一个顺序步骤来计算所有输出的校验和,然后再进行一轮并行加密,最后你得到输出。
顺便说一下,这些生成填充的函数P是非常、非常简单的函数,它们需要常数时间,所以在性能方面我们将忽略它们。
底线是这里的性能,你注意到每个输入块需要两次E的应用。事实证明,如果SIV使用非常快的PRF来导出随机性并正确实现,那么实际上SIV可能比这种特定的操作模式快两倍。
因此,我认为基于PRP的构造对于短消息非常好,而SIV则适用于如果你想以确定性方式加密较长的消息。
为PRP机制添加完整性
但是,如果我们想为这个基于PRP的机制添加完整性呢?那么,我们能否使用直接使用PRP加密消息的PRP机制来实现确定性认证加密?
事实证明答案是肯定的,并且这实际上又是一个你应该知道的非常简单的加密方案。基本上,我们要做的是取我们的消息,然后在其后附加一串零,然后应用PRP,就这样,这将给我们一个密文。

现在,非常非常简单,只需附加零并使用PRP加密。当我们解密时,我们将查看结果明文的这些最低有效位,如果它们不等于0,我们将直接拒绝密文;如果它们等于0,我们将输出消息有效。
就是这样,这就是整个系统,非常非常简单,在加密期间简单地附加零,然后在解密时检查零是否存在。我声称,这个非常简单的机制实际上提供了确定性认证加密,当然前提是你附加了足够的零。具体来说,如果你附加了n个零,我们需要1/2^n是可忽略的,如果是这样,那么实际上这种基于直接PRP的加密确实提供了确定性认证加密。

让我看看为什么。我们已经论证了该系统是CPA安全的,所以我们只需要论证它提供了密文完整性。这也很容易看到。
让我们看看密文完整性游戏。对手将选择一个真正随机的置换,即输入空间上的一个真正随机的可逆函数(在这种情况下,输入空间是消息空间加上n个零位)。现在对手能做什么?他可以提交Q条消息,然后他收到这些Q条消息的加密,即他收到PRP在他选择的点上的值(连接了n个零)。这基本上就是查询CPA挑战者的意思。
对于一个随机置换,他只能看到这个置换在他选择的Q个点上的值,但只是连接了n个零。现在他在密文完整性游戏中的目标是什么?他的目标是产生一些新的密文,不同于他给出的密文,并且能够正确解密。
正确解密意味着什么?这意味着当我们对密文C应用π的逆时,C的n个最低有效位最好都是零。问题是这发生的可能性有多大?
让我们想想。基本上,我们有一个真正随机的置换,对手看到了这个置换在Q个点上的值。他产生一个新点,当求逆时,其n个最低有效位恰好为零的可能性有多大?
我们在这里所做的本质上是评估置换π的逆在点C处的值。由于π的逆是一个随机置换,其n个最低有效位等于零的可能性有多大?
不难看出,答案最多是概率1/2^n。因为基本上,置换将在X × {0,1}^n内输出一个随机元素,而该元素将以n个零结尾的概率基本上是1/2^n。

因此,对手以可忽略的概率赢得游戏,因为这个值是可忽略的。

总结
本小节到此结束。我希望你看到了这两种非常巧妙的确定性认证加密方案。
第一种我们称之为SIV,我说你会使用随机计数器模式,并且你只是从一个应用于消息的PRF中为随机计数器模式导出随机性。这里非常巧妙的想法是,在解密过程中,你可以简单地从解密的消息中重新计算IV,并验证该IV是否与密文中提供给你的IV相同。这个简单的检查实际上足以保证认证加密,或者更准确地说,确定性认证加密。因此,如果索引很大,这种模式就是你加密数据库中索引的方式。
如果索引碰巧很短,比如它是一个8字节的用户ID,那么你可以直接使用一个PRP。你的做法是向那8个字节附加零,然后这些零将在解密时用作完整性检查。同样,如果你能够附加足够的零,那么实际上这也提供了确定性认证加密。
顺便提一下,我向你展示了有一种方法可以从窄PRP构建宽块PRP,这种特定的操作模式称为EME。我们实际上将在下一小节中引用EME。


045:可调加密 🔐
在本节中,我们将学习另一种加密形式,称为可调加密。我们将通过磁盘加密这一应用场景来引入可调加密的概念,并会看到这是确定性加密的另一个应用实例。

磁盘加密问题 💾
磁盘加密的目标是加密磁盘上的扇区。假设每个扇区大小为4KB。问题在于没有额外的空间用于扩展。换句话说,如果扇区大小是4KB,密文大小也必须恰好是4KB,因为没有地方写入额外的比特。如果密文比明文大,就无法存储。

因此,我们的基本目标是构建一种不扩展的加密方案,使得密文大小与明文大小完全相同。
从技术上讲,加密不能扩展意味着消息空间等于密文空间。消息空间是4KB的消息,密文空间也是4KB的消息。

在这种情况下,显然我们必须使用确定性加密,因为如果加密是随机的,就没有地方存储随机性。同样,我们也没有空间用于完整性保护,因为我们无法扩展密文并添加任何完整性比特。因此,我们最多只能实现确定性CPA安全。这就是我们的目标。
现在,有一个非常简单的引理可以证明:如果你给我一个确定性CPA安全的密码,其消息空间等于密文空间(即没有扩展),那么这个密码实际上是一个伪随机置换(PRP)。所以,我们在这里真正要说明的是,如果我们完全不希望扩展,我们唯一的加密选择就是上一节中提到的第二种构造方式,即我们必须使用PRP进行加密。

使用PRP加密的挑战
让我们看看如何使用PRP进行加密。我们获取磁盘并将其分成扇区。现在,如果我们使用同一个密钥下的PRP加密每个扇区,就会遇到确定性加密的标准问题:如果扇区1和扇区3恰好有相同的明文,那么加密后的扇区1将等于加密后的扇区3,攻击者就会知道对应的明文是相同的。
这实际上是一个现实问题。例如,如果你的某些扇区是空的(全部设置为零),那么加密后的扇区将是相同的。结果,攻击者就能确切知道你的磁盘上哪些扇区是空的,哪些不是。这确实很有问题。问题是,我们能做得更好吗?
答案是肯定的。首先想到的想法是:为什么不为每个扇区使用不同的密钥呢?你可以看到,扇区1用密钥K1加密,扇区2用密钥K2加密,依此类推。这样,即使扇区1的内容等于扇区3,扇区1和扇区3的密文也会不同,因为它们是在不同密钥下加密的。这实际上避免了我们之前讨论的信息泄露。

不过,我想指出,这种模式仍然存在一点信息泄露。例如,如果用户碰巧更改了扇区1中的一个比特,那么该扇区将被加密成一个不同的密文。由于这是一个伪随机置换,即使明文改变一个比特,整个扇区也会被映射到一个全新的随机输出。然而,如果用户撤销更改并恢复到原始扇区,那么加密后的扇区也会恢复到其原始状态,攻击者就能知道发生了更改并随后被撤销。所以仍然存在一点信息泄露,但这种类型的信息泄露是我们无法在不牺牲性能的情况下避免的,因此我们将忽略它,并认为它是可以接受的。

接下来的问题是,现在你意识到我们的磁盘实际上变得非常大,有很多扇区。这意味着我们需要生成大量的密钥。当然,我们不必存储所有这些密钥,我们可以简单地使用伪随机函数来生成这些密钥。
具体工作方式是:我们有一个主密钥,称之为K。然后,扇区编号(我用t表示)将使用主密钥进行加密,加密的结果就是特定的扇区密钥,我将其表示为K_T。这之所以安全,再次是因为PRF与随机函数无法区分。这意味着,如果我们对扇区编号1、2、3、4直到L应用一个随机函数,它们基本上会被映射到密钥空间中的完全随机元素。结果,我们使用一个新的、独立的随机密钥加密每个扇区。
这是一个不错的构造。然而,对于每个扇区,我们都必须应用这个PRF。所以很自然的问题是:我们能做得更好吗?事实证明我们可以,这就引入了可调分组密码的概念。我们真正想要的是拥有一个主密钥,并希望这个主密钥能派生出许多许多个PRP。我们说一种方法是简单地使用PRP编号加密密钥K,但正如我们将看到的,有一种更有效的方法。
这个PRP编号实际上被称为“调整值”,这就引入了可调分组密码的概念。
可调分组密码 🔧
在可调分组密码中,加密和解密算法通常以密钥作为输入,它们还以一个调整值作为输入(这个大写T被称为调整空间),当然,它们也以数据作为输入,输出数据在集合X中。
其特性是:对于调整空间中的每一个调整值和一个随机密钥,如果我们固定密钥和调整值,那么我们在集合X上得到一个可逆函数(一一映射)。因为密钥是随机的,所以该函数实际上与随机函数无法区分。换句话说,对于调整值的每一个设置,我们基本上都得到一个从X到X的独立PRP。
正如我所说,其应用是:现在我们将使用扇区编号作为调整值,结果,每个扇区都将获得自己独立的PRP。
让我非常快速地更精确地定义什么是安全的可调分组密码。正如我们所说,有一个调整空间、一个密钥调整空间和输入空间X。像往常一样,我们在这里定义两个实验。
在实验1中,我们将选择一组真正的随机置换。不仅仅是一个置换,我们将选择与调整值数量一样多的置换。你注意到,这就是为什么我们将此提升到调整空间的大小。如果调整空间的大小是5,这意味着我们在集合X上选择5个随机置换。


在另一种情况下,我们只是选择一个随机密钥,并将我们的置换集定义为由调整空间中的调整值定义的置换。现在,攻击者基本上可以提交一个调整值和一个x,然后看到由调整值T1定义的置换在点x1处的值。他可以一次又一次地看到这个值,看到由调整值T2定义的置换在点x2处的值,依此类推。然后他的目标是判断他是在与真正的随机置换交互,还是与伪随机置换交互。如果他做不到,我们就说这个可调分组密码是安全的。😊
与常规分组密码的区别在于:在常规分组密码中,你只能与一个置换交互,然后你的目标是判断你是在与伪随机置换交互还是与真正的随机置换交互。在这里,你可以与T个随机置换交互,你的目标同样是判断这T个随机置换是真正的随机置换还是伪随机置换。
😊,像往常一样,如果你无法区分,即攻击者在两个实验中的行为相同,我们就说这个PRP是一个安全的可调PRP。


示例与构造
很好。让我们看一些例子。我们已经看过了平凡的例子。在平凡的例子中,我们假设密钥空间等于输入空间。所以这个PRP实际上作用于x乘以x到x。😊,可以想象一下AES,其中密钥空间是128位,数据空间是128位,输出当然是128位。
然后,我们定义可调分组密码的方式(同样,输入是密钥、调整值和数据)基本上是:我们使用主密钥加密调整值,然后使用得到的随机密钥加密数据。
现在你意识到,如果我们想用这个可调分组密码加密n个块,这将需要2n次分组密码评估:n次评估用于加密给定的调整值,另外n次评估用于加密实际给定的数据。
所以我想向你展示一个很好的例子,表明我们实际上可以做得更好。这被称为XTS构造。它最初基于一种称为XEX的模式,工作原理如下:
假设你给了我一个常规的分组密码,它作用于n位块(不是可调分组密码,只是常规分组密码)。我们将定义一个可调分组密码。同样,这个可调分组密码将接受两个密钥作为输入。为了方便起见,调整空间(我们马上会看到)我们假设由两个值T和I组成。T将是某个调整值(我们稍后会看到),I将是某个索引。然后x将是一个n位字符串,我们将对其应用可调分组密码。
XTS的工作方式如下:
- 我们要做的第一件事是使用密钥K2加密调整值的左半部分,即T,我们将结果称为N。
- 现在我们要做的是,将消息M与应用于我们刚刚得到的值N和索引I的填充函数P进行异或。这个填充函数非常快,在运行时间上我们几乎可以忽略它。
- 接下来,我们使用密钥K1进行加密。
- 然后我们再次使用相同的填充进行异或。我们将再次使用应用于I的N的填充P进行异或,结果将是密文,我们将其表示为C。
好的,正如我所说,函数P是一个非常简单的函数,它只是有限域中的乘法,我在这里不详细解释,它非常快。所以运行时间实际上主要由分组密码E的运行时间决定。就是这样,这就是XTS。
这个构造的好处是:现在如果我们想加密n+1个块,我们所做的就是计算一次N值,然后对于索引1、2、3、4……,我们基本上每个块只需要评估一次PRP E。所以我们需要使用调整值(t,1)、(t,2)、(t,3)、(t,4)等加密n个块,我们只需要n+1次分组密码E的评估。所以它比平凡构造快两倍。
XTS构造的安全性思考
我想花一点时间审视一下这个XTS构造。我的问题是:在异或之前加密调整值真的有必要吗?也就是说,下面的构造是一个安全的可调PRP吗?你可以看到在这个构造中,调整值直接用作异或填充函数的输入。我的问题是,如果我们这样做,它会是一个安全的可调PRP吗?让我再次提醒你,这是密钥,这是调整值,这是数据。


我希望大家都说这是正确答案。基本上,如果我们设置数据为PT1,那么当我们与相应的调整值(也是PT1)进行异或时,我们在这里会得到0。所以实际被加密的将是0。无论结果是什么,假设是某个值C0。那么实际的输出将是C0异或P1。
当我们对PT2做同样的事情时,我们将得到C0异或PT2。当我们将这两个东西异或在一起时,我们只得到PT1异或PT2。
这个事实成立意味着攻击者可以简单地在这个调整值和这个数据处查询挑战者,然后计算两个响应的异或,并与适当填充值的异或进行比较。如果成立,我们正在与伪随机函数交互;否则,我们正在与真正的随机函数交互。这将允许攻击者以优势1击败这个构造。
XTS在磁盘加密中的应用
总结一下XTS用于磁盘加密的方式:我们查看扇区编号T,并将其分解为16字节的块。然后,块1使用调整值(T,1)加密,块2使用调整值(T,2)加密,依此类推。因此,每个块都获得自己的PRP,结果整个扇区使用一组PRP进行加密。
请注意,这是块级别的PRP,而不是扇区级别的PRP。实际上,并不是每个扇区都用自己的PRP加密,而是每个块用自己的PRP加密。扇区和块之间的区别有些人为,这种XTS模式实际上在块级别(160字节级别)提供了确定性CPA加密,这就是目标。
这种模式在磁盘加密产品中相当流行。我在这里只写了几个支持XTS的例子,我想让你知道,这实际上是实践中磁盘加密的常见做法。


总结
总结一下,可调加密是一个有用的概念,适用于当你需要从单个密钥派生出许多独立PRP的情况。需要记住的一件重要事情是,平凡的构造并不是最有效的。像XTS这样的构造实际上更高效,你可以重复使用一个调整值的加密来获得许多不同调整值的加密,因此这些是更好的使用方式。
平凡的构造和XTS构造都被称为窄块构造,即它们为16字节块提供可调分组密码。但正如我们所说,我们在上一节中看了EME构造,它为更大的块提供了PRP。实际上,EME是一种可调的操作模式。所以如果你需要用于更大块的PRP或可调PRP,那么你可以直接使用EME。但请注意,在EME中,每个输入块你必须应用两次分组密码,因此它的速度是XTS的一半,并不常用。
这就是我想说的关于可调加密的内容。在下一节中,我们将讨论格式保留加密。


046:格式保留加密 🔐

在本节中,我们将学习一种称为“格式保留加密”的加密形式。这种加密方式在实践中非常常见,尤其是在加密信用卡号等场景中。我们将探讨其工作原理、构建方法以及如何应用它来加密信用卡号,使得加密后的密文本身也像一个有效的信用卡号。
概述 📋
格式保留加密的目标是加密数据,同时保持加密后数据的格式与原始数据相同。例如,加密一个16位的信用卡号后,得到的密文仍然是一个16位的、看起来像信用卡号的字符串。这样,中间处理系统无需修改,因为它们接收到的数据格式保持不变,只有端点系统需要进行加密和解密操作。
信用卡号的结构 💳
一个典型的信用卡号由16位数字组成,分为四个4位数字的块。前6位是BIN号,代表发卡机构。例如,Visa卡以数字4开头,万事达卡以51到55开头。接下来的9位是特定客户的账号,最后一位是根据前15位数字计算出的校验和。
信用卡号的总数大约为2^42个,这意味着我们需要大约42位信息来紧凑地表示一个信用卡号。

格式保留加密的目标 🎯
我们的目标是构建一个伪随机置换,作用于集合 {0, 1, ..., S-1},其中S是信用卡号的总数(大约2^42)。我们有一个像AES这样的伪随机函数,它作用于更大的块(如128位)。我们需要“缩小”这个PRF的域,使其适应给定的数据。
构建步骤 🛠️
构建过程分为两个主要步骤:首先将PRF的域缩小到最接近S的2的幂次方,然后进一步将其缩小到目标集合 {0, 1, ..., S-1}。
第一步:缩小到2的幂次方
我们首先将AES这样的PRF从128位域缩小到T位域,其中T是大于S的最小2的幂次方。例如,如果S大约是2^42,那么T就是42。
一种简单的方法是取AES的输出,并截取其最低有效的T位。例如,对于21位输入,我们将其填充到128位,应用AES,然后截取最低21位作为输出。
代码示例:
def truncate_aes(input_bits, T):
# 假设 input_bits 是 T 位整数
padded_input = pad_to_128_bits(input_bits)
aes_output = aes_encrypt(padded_input)
truncated_output = aes_output & ((1 << T) - 1) # 取最低 T 位
return truncated_output
接下来,我们使用Luby-Rackoff构造(或更好的7轮Feistel网络)将这个PRF转换为一个PRP。Luby-Rackoff构造使用三轮Feistel网络,将安全的PRF转换为安全的PRP,其块大小是PRF的两倍。
公式:
- 设 F 为安全的PRF,作用于 T/2 位。
- Luby-Rackoff构造:
PRP(x) = F3(F2(F1(x))),其中F1、F2、F3是三轮Feistel网络。
第二步:缩小到目标集合
现在,我们有一个作用于2^T个元素的PRP,但我们需要作用于S个元素的PRP(其中S < 2^T)。以下是构造方法:
给定输入 x ∈ {0, 1, ..., S-1},我们重复以下过程:
- 计算 y = E(x),其中E是第一步中构建的PRP。
- 如果 y ∈ {0, 1, ..., S-1},则输出 y。
- 否则,令 x = y,重复步骤1。
这个过程会一直迭代,直到 y 落入目标集合中。由于 S > 2^(T-1),平均只需要两次迭代即可收敛。
逆操作:
要解密,我们只需反向操作:重复应用 D(y)(其中D是E的逆),直到结果落入目标集合中。
安全性分析 🔒
第二步的构造是紧密的,这意味着从2T域缩小到S域不会引入额外的安全损失。如果起始的PRP在2T域上是安全的,那么得到的PRP在S域上也是安全的。
然而,需要注意的是,这种加密方式只提供确定性CPA安全,不提供密文完整性,也没有随机性。
应用示例:加密信用卡号 💳
以下是使用格式保留加密加密信用卡号的步骤:
- 将信用卡号编码为 {0, 1, ..., S-1} 中的一个数字。
- 应用上述构建的PRP进行加密。
- 将加密后的数字映射回一个看起来像信用卡号的格式。
这样,加密后的密文仍然是一个有效的信用卡号,可以在不修改中间系统的情况下传输。
总结 📝
在本节中,我们学习了格式保留加密的概念和构建方法。通过两个步骤,我们可以将一个作用于大域的PRF转换为一个作用于特定小域的PRP,从而实现对信用卡号等格式敏感数据的加密。这种加密方式在保持数据格式不变的同时,提供了足够的安全性,适用于许多实际场景。
扩展阅读 📚
如果你对格式保留加密及相关主题感兴趣,可以参考以下论文:
- HKDF构造:Hugo Krawczyk, 2010。
- 确定性加密(SIV模式):相关论文。
- 宽块伪随机置换(EME模式):Olivier Raaway。
- 可调分组密码(XTS模式):相关论文。
- 格式保留加密:相关论文。

下一节中,我们将开始学习密钥交换的相关内容。
047:可信第三方与密钥交换
在本节课中,我们将学习如何通过一个可信第三方来帮助两个用户建立共享密钥。我们将探讨一个简单的密钥交换协议,理解其工作原理、安全假设以及局限性。

密钥管理问题
上一节我们讨论了如何使用共享密钥保护数据。本节中我们来看看,两个用户最初如何生成这个共享密钥。这个问题将引导我们进入公钥密码学的世界。在本模块中,我们将通过几个简单的密钥交换协议来介绍公钥密码学的核心思想。在我们构建更多公钥工具后,会再回来讨论并设计安全的密钥交换协议。
假设世界上有 n 个用户,问题在于这些用户如何管理他们彼此通信所需的秘密密钥。
例如,假设 n = 4,世界上有四个用户。一种方案是让每对用户共享一个秘密密钥。例如,U1 和 U3 共享密钥 K13,U1 和 U2 共享密钥 K12,以此类推,还有 K24、K34、K14 和 K23。
这种方法的问题是,现在存在大量需要用户管理的共享密钥。具体来说,如果每个用户需要与世界上其他 n 方通信,他必须存储大约 n 个密钥。

那么,我们能否做得比每个用户存储 n 个密钥更好呢?答案是肯定的。一种方法就是使用所谓的在线可信第三方。我们用 TTP 表示可信第三方。
可信第三方方案
使用可信第三方的方式是,每个用户现在只与这个可信第三方共享一个密钥。因此,用户一共享一个密钥,用户二共享一个密钥。我们的朋友 Alice 和 Bob 也有他们的秘密密钥,我们称之为 K_A 和 K_B。
现在,这种设计的好处是每个用户只需要记住一个秘密密钥。问题是,当 Alice 想与 Bob 通信时,他们俩必须执行某个协议,以便在该协议结束时,他们能拥有一个攻击者无法得知的共享秘密密钥 K_AB。
那么,Alice 和 Bob 如何生成这个共享秘密密钥 K_AB 呢?让我们看一个实现此目的的简单示例协议。
一个简单的密钥交换协议
以下是我们的朋友 Alice 和 Bob。Bob 有他与可信第三方共享的密钥 K_B。Alice 有她的秘密密钥 K_A,同样与可信第三方共享。因此,可信第三方同时拥有 K_A 和 K_B。问题在于,他们如何生成一个双方都同意、但攻击者完全不知道的共享密钥?
这里我们只讨论一个简单的协议。具体来说,这个协议仅能抵抗窃听攻击,无法抵抗篡改或主动攻击。正如之前所说,我们将在课程后期回来设计安全的密钥交换协议。但现在,为了介绍密钥交换的概念,让我们看看这个仅能抵抗窃听的最简单机制。换句话说,监听对话的对手将无法知道共享密钥 K_AB 是什么。
协议工作流程如下:
- Alice 首先向可信第三方发送一条消息,表示她想获得一个与 Bob 共享的秘密密钥。
- 可信第三方将实际生成一个随机的秘密密钥
K_AB。可信第三方是生成他们共享秘密密钥的一方。 - 可信第三方将向 Alice 发回一条消息,但这条消息由两部分组成:
- 第一部分是使用 Alice 的秘密密钥
K_A加密的消息。加密的内容是:这个密钥K_AB是供 Alice 和 Bob 双方使用的。 - 第二部分是一个称为“票据”的消息。这个票据是使用 Bob 的密钥
K_B加密的消息。加密的内容同样是:这个密钥K_AB是供 Alice 和 Bob 双方使用的。
- 第一部分是使用 Alice 的秘密密钥
- 这两条消息从可信第三方发送给 Alice。
- 当 Alice 稍后想与 Bob 安全通信时,她会解密用
K_A加密的部分,从而获得密钥K_AB。 - 然后,她将“票据”发送给 Bob。
- Bob 收到票据后,使用他自己的秘密密钥
K_B解密,从而也恢复出秘密密钥K_AB。
现在,他们拥有了可以用于安全通信的共享秘密密钥 K_AB。
这里使用的加密系统 E 是一个 CPA 安全的密码,我们稍后会看到为什么需要这个属性。
协议安全性分析(针对窃听)
第一个问题是,为什么这个协议是安全的?即使我们只考虑窃听对手。让我们思考一下。

目前,我们只关心对窃听者的安全性。那么,在这个协议中,窃听者 C 能看到什么?他基本上能看到这两个密文:加密给 Alice 的密文和加密给 Bob 的票据。事实上,他可能会看到许多这样的消息实例,特别是如果 Alice 和 Bob 持续以这种方式交换密钥。
我们的目标是证明他对交换的密钥 K_AB 没有任何信息。这直接源于密码 E 的 CPA 安全性。因为窃听者无法区分对秘密密钥 K_AB 的加密和对随机垃圾的加密,这就是 CPA 安全的定义。因此,他无法将 K_AB 与随机垃圾区分开来,这意味着他对 K_AB 一无所知。这是一个关于其窃听安全性的高层次论证,但对我们目前的目的来说已经足够。
总之,在这个协议中,窃听者实际上对 K_AB 没有任何信息,因此可以安全地使用 K_AB 作为 Alice 和 Bob 之间交换消息的秘密密钥。
关于可信第三方的讨论
现在,让我们思考一下可信第三方。首先,你注意到每次密钥交换都需要可信第三方。基本上,除非可信第三方在线并可用以帮助他们,否则 Alice 和 Bob 根本无法进行密钥交换。
这个协议的另一个特性是,可信第三方知道所有的会话密钥。因此,如果可信第三方被破坏或遭到入侵,攻击者可以非常容易地窃取系统中每个用户之间交换的所有秘密密钥。
这就是为什么这个 TTP 被称为可信第三方,因为它本质上知道系统中生成的所有会话密钥。
尽管如此,这个机制的优点在于它只使用了对称密码学,没有超出我们已经见过的内容,因此非常快速高效。唯一的问题是,你必须使用这个在线的可信方,并且不清楚如果要在万维网上使用,谁将充当这个在线可信第三方。然而,在企业环境中,这可能是有意义的。如果你有一个拥有单一信任点的公司,指定一台机器作为可信第三方可能是合理的。事实上,类似这样的机制(虽然不完全是我描述的方式)是 Kerberos 系统的基础。
协议的局限性(主动攻击)
然而,我必须明确指出,这只是一个非常简单的示例协议,仅能抵抗窃听攻击。它实际上对主动攻击者完全不安全。这里有一个主动攻击者如何破坏此协议的简单示例:重放攻击。
假设攻击者记录了 Alice 与某个在线商家(我们称之为商家 Bob)之间的对话。例如,想象 Alice 从商家 Bob 那里订购了一本书,交易完成,Bob 接受了付款并发送给 Alice 这本书的副本。

攻击者现在可以完全记录这次对话,并在稍后时间简单地向商家 Bob 重放这次对话。Bob 会认为 Alice 刚刚又订购了一本同样的书,他会再次向她收费并再次发送副本。因此,通过重放对话,可能会给 Alice 造成相当大的损害。这只是一个简单的例子,说明了为什么这个协议对主动攻击完全不安全,绝不应该在现实世界中使用。正如我所说,我们将在几周后回来讨论这个协议的安全版本,并了解如何在现实世界中使其工作。
迈向无第三方密钥交换
尽管如此,你可以看到这个非常简单的协议已经提出了一个非常有趣的问题:我们能否设计出安全的密钥交换协议,无论是对抗窃听还是对抗主动攻击者,并且不需要在线可信第三方?我们不想依赖任何完成密钥交换协议所必需的在线可信方。
令人惊讶的答案是,这实际上是可能的。我们可以在没有可信第三方的情况下进行密钥交换,而这正是公钥密码学的起点。我将向你展示实现这一点的三个想法,我们将在接下来的模块中更详细地讨论它们。
- 第一个想法 实际上来自 Ralph Merkle(1974 年)。当时他还只是一名本科生,就已经提出了这个无需在线可信第三方进行密钥交换的绝妙想法。这是我们将要研究的一个例子。
- 第二个例子 来自 Diffie 和 Hellman(1976 年)。他们当时都在斯坦福大学工作,提出了这个开启了现代公钥密码学世界的想法。
- 第三个想法 来自 Rivest、Shamir 和 Adleman(MIT)。我们将详细研究每个想法的工作原理。
我想指出的是,关于公钥管理的工作并没有在 70 年代末停止。事实上,多年来出现了许多想法。这里我只提两个近十年来的想法:一个是基于身份的加密,这是管理公钥的另一种方式;另一个是函数加密,这是一种仅能部分解密密文的密钥分发方式。
我们将在下周讨论公钥密码学的核心,并在课程后期讨论这些更高级的想法。
总结

本节课中,我们一起学习了如何通过一个在线可信第三方来帮助两个用户建立共享密钥。我们分析了一个简单的协议,它使用对称加密和可信第三方来分发密钥,并且仅能抵抗窃听攻击。我们认识到该协议对重放等主动攻击是不安全的,并且依赖于一个知晓所有会话密钥的可信中心。最后,我们了解到无需可信第三方的密钥交换是可能的,这引出了公钥密码学,并预告了后续课程将深入探讨的几种经典方案。
048:默克尔谜题

在本节课中,我们将学习第一个无需可信第三方的密钥交换协议。我们将从一种仅使用对称加密工具(如分组密码)的简单协议开始,了解其工作原理、效率以及局限性。
协议背景与目标
上一节我们介绍了密钥交换的基本概念。本节中,我们来看看一个具体的场景:爱丽丝和鲍勃从未见过面,但他们希望在不依赖任何可信第三方的情况下,通过公开的通信渠道协商出一个共享的密钥 K。我们假设攻击者只能窃听通信内容,而不能篡改。我们的目标是,即使攻击者窃听了所有通信,也无法得知最终的共享密钥 K。

一个核心问题是:仅使用对称密码学工具(如分组密码或哈希函数)能否实现这个目标?
令人惊讶的是,答案是肯定的。然而,由此产生的协议效率低下,在实践中并不使用。尽管如此,理解这些简单的协议有助于我们建立直觉,并为后续学习更高效的公钥密码学协议打下基础。
默克尔谜题协议详解
我们将要学习的简单协议被称为 默克尔谜题。它由拉尔夫·默克尔于1974年提出。该协议的核心工具是一个“谜题”。
什么是“谜题”?
一个“谜题”是一个难以解决但通过一定努力可以解决的问题。在密码学中,我们可以这样构造一个谜题:
假设我们使用一个128位的对称加密密钥(例如AES)。我们构造一个特殊的密钥:其前96位全部为0,只有后32位是随机选择的。然后,我们使用这个密钥加密一段固定的明文(例如,消息 "puzzle")。加密后的密文就是这个“谜题”。

公式表示:
密钥 = (96个0位) || (32位随机数 P)
密文(谜题)= Encrypt(密钥, 明文 "puzzle||X||K")
由于只有32位是随机的,攻击者最多只需要尝试 2^32 次(即暴力破解所有可能的32位组合)就能解密出明文,找到正确的密钥 P。因此,解决这个谜题的工作量是 O(2^32)。
协议步骤
以下是默克尔谜题协议的具体步骤,每个列表前都有简短介绍。
第一步:爱丽丝生成并发送谜题
爱丽丝首先生成大量(例如 n = 2^32 个)这样的谜题。
以下是爱丽丝为生成第 i 个谜题所执行的操作:
- 随机选择一个32位的谜题密钥
P_i。 - 随机选择两个128位的值:一个谜题标识符
X_i和一个将成为共享密钥的K_i。 - 构造一个128位的AES密钥:前96位为0,后32位为
P_i。 - 使用这个AES密钥加密以下拼接的明文:
"puzzle" || X_i || K_i。 - 得到的密文就是第
i个谜题。
爱丽丝将所有 n 个谜题发送给鲍勃。
第二步:鲍勃解决一个谜题
鲍勃从收到的 n 个谜题中随机选择一个(例如第 j 个)。

以下是鲍勃解决所选谜题的过程:
- 他需要尝试所有
2^32个可能的P值来解密这个谜题。 - 对于每个尝试的
P,他用构造出的密钥(96个0位 +P)解密密文。 - 检查解密出的明文是否以
"puzzle"开头。如果是,则说明找到了正确的P_j。 - 从明文中提取出标识符
X_j和共享密钥K_j。
鲍勃将解出的谜题标识符 X_j 发送回爱丽丝,并自己保存好 K_j。
第三步:爱丽丝恢复共享密钥
爱丽丝收到 X_j 后,在她本地的数据库中查找标识符为 X_j 的谜题记录。该记录中包含她之前为这个谜题生成的 K_j。现在,爱丽丝和鲍勃都知道了同一个共享密钥 K_j。
工作量和安全性分析
现在我们来分析协议参与方和攻击者的工作量。
参与方的工作量:
- 爱丽丝 需要生成
n个谜题。生成每个谜题是常数时间,所以总工作量是O(n)。 - 鲍勃 需要解决一个谜题。解决一个谜题需要
O(n)的工作量(因为n = 2^32,解决一个谜题就需要尝试约n次)。
因此,爱丽丝和鲍勃的工作量都是线性的,即 O(n)。
攻击者的工作量:
攻击者窃听到了爱丽丝发送的所有 n 个谜题,以及鲍勃返回的 X_j。为了找出共享密钥 K_j,攻击者必须:
- 尝试解决每一个谜题,直到找到那个内部明文包含
X_j的谜题。 - 解决一个谜题需要
O(n)的工作量。 - 在最坏情况下,他需要解决所有
n个谜题才能找到目标。
因此,攻击者的总工作量是 O(n) * O(n) = O(n^2)。
安全性总结:
在这个协议中,合法参与方的工作量是线性的(O(n)),而攻击者破解协议的工作量是二次方的(O(n^2))。这创造了一个二次方差距。例如,当 n = 2^32 时:
- 爱丽丝和鲍勃各需约
2^32步操作(生成或解决谜题)。 - 攻击者则需要约
2^64步操作来破解。

协议的局限性与启示
尽管默克尔谜题在概念上很巧妙,但它存在明显的效率问题。为了获得足够的安全性(例如让攻击者需要 2^128 步),参与方需要完成 2^64 步的操作,这在实践中是不可行的。因此,该协议从未被实际部署。
一个自然的问题是:仅使用对称密码学工具,我们能否做得比二次方差距更好?
答案是:我们相信不能。 有研究结果表明,如果我们将分组密码或哈希函数视为一个只能进行查询的“黑盒”,那么任何基于此类原语的密钥交换协议,都存在一个运行时间为 O(n^2) 的攻击。这意味着二次方差距可能是此类构造的理论极限。
这个局限性引出了密码学发展的一个关键转折点:为了实现高效且安全的密钥交换,我们不能仅仅依赖通用的对称密码原语。我们需要具有特殊数学性质的函数,这需要引入代数结构。
课程总结
本节课中我们一起学习了默克尔谜题协议。我们了解到:
- 仅使用对称加密工具可以实现无需可信第三方的密钥交换。
- 默克尔谜题通过让合法用户进行
O(n)的工作,而迫使窃听者进行O(n^2)的工作,从而在两者之间建立了安全差距。 - 该协议由于效率低下(合法用户的工作量随安全级别增长过快)而不实用。
- 理论研究表明,仅将对称密码作为黑盒使用,可能无法突破二次方差距的效率瓶颈。

这个结论为我们接下来的学习指明了方向:为了构建实用的密钥交换协议,我们必须超越对称密码学的范畴,进入公钥密码学的领域,这需要利用数论和代数中的特定难题。在接下来的几节中,我们将开始学习这些必要的数学基础。
049:迪菲-赫尔曼协议 🔑

在本节课中,我们将要学习迪菲-赫尔曼协议。这是我们遇到的第一个实用的密钥交换机制。我们将了解其工作原理、安全性基础,以及它如何让从未谋面的双方安全地建立一个共享密钥。
协议设置与目标

上一节我们介绍了梅克尔谜题,它利用分组密码或哈希函数实现密钥交换,但攻击者与参与者的工作量差距仅为平方级别,因此不够安全。本节中,我们来看看能否实现指数级别的安全差距,即参与者工作量为 O(N),而攻击者工作量需达到 O(exp(N))。
为了实现这种指数差距,我们不能仅依赖像AES或SHA-256这样的对称密码原语,因为它们缺乏必要的数学结构。因此,我们需要转向使用一些代数方法。
迪菲-赫尔曼协议详解
接下来,我们将具体且非正式地描述迪菲-赫尔曼协议。下周我们会更抽象、更严谨地分析它。
协议首先需要固定两个公开参数:
- 一个大素数
p:通常是一个非常大的素数,例如约600位十进制数(约2000比特)。 - 一个整数
g:范围在1到p-1之间。
参数 p 和 g 是协议的一部分,选定后长期固定。

以下是协议的具体步骤:
-
爱丽丝的操作:
- 随机选择一个秘密数字
a,范围在1到p-1之间。 - 计算
A = g^a mod p。 - 将计算结果
A发送给鲍勃。
- 随机选择一个秘密数字
-
鲍勃的操作:
- 随机选择一个秘密数字
b,范围在1到p-1之间。 - 计算
B = g^b mod p。 - 将计算结果
B发送给爱丽丝。
- 随机选择一个秘密数字

- 生成共享密钥:
- 共享密钥
K_AB定义为g^(a*b) mod p。 - 爱丽丝计算:她收到
B后,计算B^a mod p = (g^b)^a mod p = g^(a*b) mod p。 - 鲍勃计算:他收到
A后,计算A^b mod p = (g^a)^b mod p = g^(a*b) mod p。
- 共享密钥
由此可见,尽管双方看似计算了不同的值,但最终都得到了相同的共享密钥 g^(a*b) mod p。这个协议的核心在于指数运算的交换律:(g^b)^a = (g^a)^b = g^(a*b)。
协议安全性分析
理解协议为何有效相对简单,但更重要的是理解它为何安全。攻击者(窃听者)能看到什么?他能看到公开参数 p 和 g,以及双方交换的中间值 A = g^a mod p 和 B = g^b mod p。
关键问题是:给定 g^a mod p 和 g^b mod p,攻击者能否计算出 g^(a*b) mod p?这被称为计算迪菲-赫尔曼问题。
目前,解决此问题最著名的算法是一般数域筛法。假设素数 p 的长度为 n 比特,该算法的运行时间大致是 exp(O(n^(1/3)))。这是一个亚指数时间算法,而非真正的指数时间 exp(O(n))。这意味着,虽然问题很难,但由于立方根效应,其难度并未达到理想的指数级别。
以下是一个简化的对比表格,说明了为达到与特定密钥长度的分组密码相当的安全性,所需使用的模数 p 的大小:
| 分组密码密钥长度(比特) | 所需迪菲-赫尔曼模数大小(比特) |
|---|---|
| 80 | 约 1024 |
| 128 | 约 3072 |
| 256 | 约 15360 |
从表格可以看出,为了匹配高强度对称密钥的安全性,需要使用非常大的模数,这会导致计算效率低下。

椭圆曲线迪菲-赫尔曼
为了解决模数过大导致的效率问题,密码学家找到了更好的代数结构:椭圆曲线。迪菲-赫尔曼协议可以完全移植到椭圆曲线上运行。
在椭圆曲线上,计算迪菲-赫尔曼问题被认为困难得多(目前最好的算法是指数时间的)。因此,我们可以使用小得多的参数来达到相同的安全强度。例如,要达到80比特密钥的安全强度,仅需约160比特的椭圆曲线参数;要达到256比特密钥的安全强度,也仅需约512比特的参数。因此,业界正逐渐从传统的模素数迪菲-赫尔曼转向更高效的椭圆曲线迪菲-赫尔曼。
中间人攻击与协议局限性
需要强调的是,我们刚刚描述的基础迪菲-赫尔曼协议无法抵抗中间人攻击。在这种攻击下,攻击者可以拦截并篡改爱丽丝和鲍勃交换的中间值 A 和 B,分别替换成自己生成的 A' 和 B',从而与双方分别建立不同的共享密钥。这样,攻击者就能解密、阅读甚至修改双方通信的所有内容。
因此,基础协议仅能提供对抗被动窃听的安全性。要防御主动的中间人攻击,需要对协议进行增强(例如,结合数字签名或证书机制),我们将在后续课程中讨论。

协议的非交互特性与开放问题
迪菲-赫尔曼协议有一个有趣的特性:它可以被视为一种非交互式协议。想象一下,数百万用户(爱丽丝、鲍勃、查理等)各自选择一个秘密值,并将对应的公开值 g^a, g^b, g^c 发布在个人公开资料(如社交媒体主页)上。
此后,任意两个用户(如爱丽丝和查理)想要建立共享密钥,就无需再进行任何在线通信。爱丽丝只需读取查理的公开值 g^c,即可计算共享密钥 (g^c)^a = g^(a*c);查理同样读取爱丽丝的公开值 g^a 计算 (g^a)^c = g^(a*c)。他们立刻就能开始安全通信。
这引出了一个有趣的开放性问题:我们能否将这种非交互式密钥建立扩展到多于两方的群组?例如,四个用户各自发布一个公开值后,仅通过读取彼此的公开资料,能否让四方同时建立一个共享的群组密钥?
- 对于
n=2(两方),这就是迪菲-赫尔曼协议。 - 对于
n=3(三方),存在已知协议(如Joux协议),但需要更复杂的数学。 - 对于
n>=4(四方及以上),这仍然是一个未解决的公开问题,谁能解决它将在密码学界获得巨大声誉。
总结


本节课我们一起学习了迪菲-赫尔曼协议。我们了解了它如何利用模指数运算的代数性质,让双方在不安全的信道上建立一个共享的秘密密钥。我们分析了其安全性基于计算迪菲-赫尔曼问题的困难性,并指出了基础协议只能抵抗被动窃听,无法防御中间人攻击。最后,我们探讨了协议的非交互式特性以及一个关于多方密钥建立的开放性问题。在接下来的课程中,我们将继续学习其他的密钥交换机制。
050:公钥加密 🔐
在本节课中,我们将学习一种基于公钥加密概念的密钥交换方法。我们将了解公钥加密的定义、工作原理,以及如何利用它来建立共享密钥。课程最后,我们会探讨其安全性,并简要提及后续的学习方向。


公钥加密的定义与构成
上一节我们介绍了Diffie-Hellman密钥交换机制。本节中,我们来看看另一种基于公钥加密的密钥交换方法。
公钥加密与对称加密类似,包含加密和解密算法。但关键区别在于,加密和解密使用不同的密钥。加密算法使用一个公开的密钥(公钥),而解密算法使用一个保密的密钥(私钥)。这两个密钥合称为一个密钥对。
更精确地说,一个公钥加密系统由三个算法构成:G(密钥生成)、E(加密)和D(解密)。
- 密钥生成算法 G:运行时生成一个密钥对
(PK, SK),其中PK是公钥,SK是私钥。 - 加密算法 E:输入公钥
PK和明文消息M,输出对应的密文C。公式表示为:C = E(PK, M)。 - 解密算法 D:输入私钥
SK和密文C,输出原始明文M或一个错误标识⊥。公式表示为:M = D(SK, C)。

系统必须满足一致性:对于任何由 G 生成的 (PK, SK),解密用对应公钥加密的密文,必须得到原始消息。即:D(SK, E(PK, M)) = M。
公钥加密的安全性
理解了公钥加密的构成后,我们需要定义其安全性。我们沿用之前“语义安全”的概念,但游戏规则略有不同。
在公钥系统的语义安全游戏中:
- 挑战者运行
G生成密钥对(PK, SK),并将公钥PK交给攻击者。 - 攻击者输出两个等长的消息
M0和M1。 - 挑战者随机选择
b ∈ {0,1},将C = E(PK, Mb)发送给攻击者。 - 攻击者尝试猜测
b的值。
我们定义实验0(加密 M0)和实验1(加密 M1)。如果攻击者在两个实验中输出1的概率几乎相同,无法区分收到的是 M0 还是 M1 的加密结果,则该公钥系统是语义安全的。
需要强调的是,在公钥设置中,攻击者无需进行选择明文攻击的能力,因为他已经拥有公钥 PK,可以自行加密任何消息。
基于公钥加密的密钥交换协议
现在我们已经理解了公钥加密及其安全性,让我们看看如何利用它来建立一个共享密钥。

以下是协议的步骤:
- Alice 初始化:Alice 运行密钥生成算法
G,为自己生成一个公钥-私钥对(PKA, SKA)。 - Alice 发送公钥:Alice 将她的公钥
PKA发送给 Bob,并声明消息来自 Alice。 - Bob 加密共享秘密:Bob 生成一个随机的128位值
X。然后,他使用 Alice 的公钥PKA加密X,得到密文C = E(PKA, X)。 - Bob 发送密文:Bob 将密文
C发送回 Alice,并声明消息来自 Bob。 - Alice 解密获得密钥:Alice 收到密文后,使用自己的私钥
SKA进行解密,得到X = D(SKA, C)。
至此,Alice 和 Bob 共享了秘密值 X,可以将其用作会话密钥。
这个协议与上一节的 Diffie-Hellman 协议有一个重要区别:它需要交互顺序。Bob 必须等待收到 Alice 的公钥后才能发送他的消息。而 Diffie-Hellman 协议允许双方在任何时间发布自己的消息,无需严格顺序。
协议的安全性分析

我们分析了协议流程,接下来需要问:为什么这个协议是安全的?(我们目前仅考虑窃听安全性)
在协议中,攻击者能观察到公钥 PKA 和密文 C(即 X 的加密结果)。攻击者的目标是获取 X。
由于我们使用的公钥系统是语义安全的,这意味着攻击者无法区分 X 的加密结果与某个随机消息的加密结果。因此,在攻击者看来,X 与消息空间 M 中的一个随机元素是不可区分的。这样,X 就可以安全地作为双方之间的会话密钥。
可以证明,如果攻击者能区分上述两种分布,他就能直接破坏公钥加密的语义安全性。
然而,与许多基础协议一样,该协议无法抵抗中间人攻击。

中间人攻击示例
尽管协议能抵抗窃听,但在活跃攻击者面前非常脆弱。以下是简单的中间人攻击过程:
- Alice 生成她的密钥对
(PKA, SKA)。同时,中间人 Mallory 也生成自己的密钥对(PKM, SKM)。 - 当 Alice 发送她的公钥
PKA给 Bob 时,Mallory 拦截该消息。他将消息替换为PKM,但仍声称这是来自 Alice 的公钥。 - Bob 收到
PKM,信以为真。他生成随机值X,并发送密文C' = E(PKM, X)给“Alice”。 - Mallory 拦截
C'。他用自己的私钥SKM解密得到X。 - Mallory 然后用 Alice 的真实公钥
PKA加密X,得到C = E(PKA, X),并将其发送给 Alice。 - Alice 收到
C,用自己的私钥解密得到X。
攻击结果:Alice 和 Bob 都认为自己与对方成功交换了密钥 X,但实际上,中间人 Mallory 也完全掌握了 X。这使得协议在消息可被篡改时完全不安全。我们将在后续引入数字签名后,探讨如何加固此类协议。

总结与展望
本节课中我们一起学习了:
- 公钥加密的基本概念,它由密钥生成
G、加密E和解密D三个算法构成。 - 公钥加密的语义安全定义,其核心是攻击者在拥有公钥的情况下,仍无法区分不同明文的加密结果。
- 如何利用公钥加密构建一个密钥交换协议,使双方获得共享秘密
X。 - 该协议能抵抗窃听,但易受中间人攻击。
公钥加密本身可以实现抗窃听的密钥交换。接下来的问题是:如何构造公钥加密系统?与 Diffie-Hellman 协议类似,实际的构造通常依赖于数论和代数知识。
因此,在深入具体的公钥加密构造之前,我们将在下一个模块进行一次简短的“绕行”,快速回顾相关的数论背景知识。之后,我们再回来详细讨论基于数论的公钥加密构造以及更安全的密钥交换方法。
扩展阅读提示:
- 有论文证明,如果仅将对称密码和哈希函数视为黑盒,那么“Merkle谜题”式的密钥交换方案已达到最优,无法获得比二次方更好的安全间隙。
- 另有一些文献总结了使用公钥加密和Diffie-Hellman进行密钥交换的多种机制,并展望了如何使其抵抗中间人攻击,而不仅仅是窃听攻击。

本模块到此结束。下一模块,我们将简要概述代数与数论。
051:数论基础回顾

在本节课中,我们将要学习数论中的一些核心概念,这些概念是构建公钥密码系统(如密钥交换、数字签名和公钥加密)的基础。我们将从模运算开始,逐步介绍最大公约数、模逆元等关键知识。
模运算与集合 Zₙ
上一模块我们了解到数论对密钥交换很有用。本节中,我们来看看一些基本的数论事实,这些将帮助我们构建多种公钥系统。
我将使用以下符号表示法:
- 用大写 N 表示一个正整数。
- 用小写 p 表示一个正质数。
我将使用 Zₙ 来表示集合 {0, 1, 2, ..., N-1}。这不仅是整数集合,我们还可以在其中进行加法和乘法运算,只要运算结果总是对 N 取模。
对于了解一些代数的同学,可以说 Zₙ 表示一个环,其中的加法和乘法是模 N 运算。这是密码学中非常常见的符号。
为了确保大家都熟悉模运算,让我们以 N = 12 为例,看一些基本事实:
- 9 + 8 = 17,而 17 mod 12 = 5。所以我们写作在 Z₁₂ 中,9 + 8 = 5。
- 5 × 7 = 35,而 35 mod 12 = 11(因为 36 可被 12 整除,35 = 36 - 1)。所以在 Z₁₂ 中,5 × 7 = 11。
- 5 - 7 = -2,而 -2 mod 12 = 10。所以在 Z₁₂ 中,5 - 7 = 10。

总的来说,在 Zₙ 中进行的模 N 算术,其运算规则与你所熟悉的整数或实数算术规则基本一致。例如,分配律 x × (y + z) = x × y + x × z 在 Zₙ 中同样成立。
最大公约数与扩展欧几里得算法
接下来我们需要了解的概念是最大公约数。给定两个整数 x 和 y,它们的最大公约数是能同时整除 x 和 y 的最大整数。

例如,12 和 18 的 GCD 是 6。
关于 GCD 有一个重要事实:对于任意两个整数 x 和 y,总存在另外两个整数 a 和 b,使得 a × x + b × y = GCD(x, y)。也就是说,GCD 可以表示为 x 和 y 的一个线性组合。
以 12 和 18 为例,2 × 12 + (-1) × 18 = 24 - 18 = 6。这里的 a 和 b 分别是 2 和 -1。
不仅 a 和 b 存在,还有一个非常高效简单的算法可以找到它们,即扩展欧几里得算法。给定 x 和 y,该算法可以在时间复杂度约为 O((log N)²) 内找到 a 和 b。
如果 x 和 y 的 GCD 恰好为 1,即 1 是能同时整除它们的最大整数,那么我们称 x 和 y 互质。例如,3 和 5 互质。
模逆元
在有理数中,我们知道一个数的逆元是什么,例如 2 的逆元是 1/2。那么在模 N 运算中,逆元是什么呢?

元素 x 在 Zₙ 中的逆元,是另一个元素 y ∈ Zₙ,满足 x × y ≡ 1 (mod N)。这个数 y 记作 x⁻¹。如果 y 存在,那么它是唯一的。
来看一个简单例子:假设 N 是某个奇数,那么 2 在 Zₙ 中的逆元是 (N + 1) / 2。因为 2 × (N+1)/2 = N + 1 ≡ 1 (mod N)。
那么,Zₙ 中哪些元素拥有逆元呢?有一个非常简单的定理:
元素 x ∈ Zₙ 是可逆的,当且仅当 x 与模数 N 互质(即 GCD(x, N) = 1)。
证明:
- 充分性(如果 GCD(x, N)=1,则 x 可逆):
因为 GCD(x, N) = 1,根据 GCD 的性质,存在整数 a, b 使得a × x + b × N = 1。
将此等式模 N 化简,b × N项变为 0,得到a × x ≡ 1 (mod N)。因此,a 就是 x 在 Zₙ 中的逆元。 - 必要性(如果 x 可逆,则 GCD(x, N)=1):
用反证法。假设 GCD(x, N) = d > 1,且存在逆元 a 使得a × x ≡ 1 (mod N)。
这意味着a × x = k × N + 1对某个整数 k 成立。
由于 d 整除 x 和 N,那么 d 也整除等式左边a × x和右边k × N,但 d 不能整除 1(因为 d>1),矛盾。因此逆元不存在。
这个证明不仅是存在性证明,也给出了计算逆元的方法:使用扩展欧几里得算法找到满足 a × x + b × N = 1 的 a 和 b,那么 a 就是 x 的模逆元。

可逆元集合 Zₙ*
我们引入符号 Zₙ* 来表示 Zₙ 中所有可逆元素(即与 N 互质的元素)的集合。
以下是几个例子:
- 当 p 是质数时,Zₚ* 包含从 1 到 p-1 的所有整数,因为除了 0,它们都与 p 互质。所以 |Zₚ*| = p - 1。
- 对于 N = 12,Z₁₂* 包含所有与 12 互质的数:{1, 5, 7, 11}。像 2, 3, 4, 6 等与 12 有非平凡公约数的数则不可逆。
解模线性方程
基于以上知识,我们可以轻松解决模线性方程。给定方程 a × x ≡ b (mod N),其中 a 可逆(即 GCD(a, N)=1),解法如下:
- 两边同时乘以 a 的模逆元 a⁻¹。
- 得到
x ≡ b × a⁻¹ (mod N)。 - 使用扩展欧几里得算法高效计算出 a⁻¹,然后计算
b × a⁻¹ mod N即可得到解。
扩展欧几里得算法的时间复杂度是 O((log N)²),因此这是解决模线性方程的二次时间算法,也是目前已知的最佳算法。
回顾高中代数,学完线性方程后,下一个问题自然是:如何解二次方程?这个问题非常有趣,我们将在接下来的部分中探讨。
总结

本节课中我们一起学习了数论的基础知识,为后续构建密码系统做准备。我们回顾了模运算和集合 Zₙ 的概念,理解了最大公约数及其通过扩展欧几里得算法得到的线性组合表示。我们重点学习了模逆元的定义、存在条件(元素与模数互质)及其高效计算方法。最后,我们定义了可逆元集合 Zₙ*,并展示了如何利用模逆元来求解模线性方程。这些概念是理解后续公钥密码学内容的基石。
052:费马与欧拉定理 🔐

在本节课中,我们将学习两个在密码学中至关重要的数论定理:费马小定理和欧拉定理。我们将了解它们的定义、应用,以及它们如何帮助我们理解模运算中元素的结构。
回顾:模逆元与欧几里得算法
上一节我们介绍了模逆元的概念,并说明了欧几里得算法能高效地计算模 n 下元素的逆元。本节中,我们将时间快进到17和18世纪,探讨费马和欧拉的重要贡献。

在此之前,让我们快速回顾一下之前的内容。我们通常令 n 为一个正整数,且是一个 n 比特的整数(即介于 2^n 和 2^(n+1) 之间)。我们令 p 表示一个素数。
我们定义了集合 Z_n 为从 0 到 n-1 的整数集合,并可以在其中进行模 n 的加法和乘法运算。我们还定义了 Z_n* 为 Z_n 中所有可逆元素的集合,并证明了:元素 x 可逆的充要条件是 x 与 n 互质。
不仅如此,我们完全理解了哪些元素可逆,哪些不可逆。我们还展示了一个基于扩展欧几里得算法的高效算法,用于计算 Z_n 中元素 x 的逆元,其运行时间复杂度为 O(n^2),其中 n 是数字 N 的比特长度。
费马小定理
现在,让我们从古希腊时代跳到17世纪,谈谈费马。费马提出了许多重要定理,其中一个关键定理如下:
假设给定一个素数 p,那么对于 Z_p* 中的任意元素 x,都有 x^(p-1) ≡ 1 (mod p)。
让我们看一个简单的例子。设 p = 5,我们看 3^(p-1),即 3^4。3^4 = 81,而 81 mod 5 = 1。这个例子验证了费马定理。

有趣的是,费马本人并未证明这个定理。证明工作直到大约100年后才由欧拉完成,并且欧拉证明了一个更通用的版本。
费马定理的一个简单应用
假设我们有一个元素 x ∈ Z_p*(注意 p 必须是素数)。根据费马定理,我们知道 x^(p-1) ≡ 1 (mod p)。
我们可以将 x^(p-1) 写成 x * x^(p-2)。因此,x * x^(p-2) ≡ 1 (mod p)。这意味着 x 模 p 的逆元就是 x^(p-2)。
这为我们提供了另一种计算模素数逆元的算法:只需计算 x^(p-2) 即可得到 x 的逆元。然而,与欧几里得算法相比,这种方法有两个缺点:
- 它只适用于模素数的情况,而欧几里得算法也适用于模合数。
- 计算这个模幂运算的效率较低。我们将在本模块最后一节讨论模幂运算,届时你会看到计算
x^(p-2)的时间复杂度约为O(log^3 p),而欧几里得算法计算逆元的时间复杂度是O(log^2 p)。
因此,这个算法不仅通用性较差(仅适用于素数),效率也较低。尽管如此,关于素数的这个事实极其重要,我们将在接下来的几周内广泛使用它。
应用:生成大素数
让我们看一个费马定理的快速简单应用。假设我们需要生成一个大的随机素数,比如一个大约1024比特的素数(数量级约为 2^1024)。
以下是一个简单的概率算法:
- 在指定区间内随机选择一个整数
p。 - 测试这个整数是否满足费马定理。例如,以2为底数,测试
2^(p-1) ≡ 1 (mod p)是否成立。 - 如果等式不成立,那么我们肯定知道选择的数
p不是素数。此时,我们返回步骤1,尝试另一个数,如此反复。 - 一旦我们找到一个满足条件的整数,就输出它并停止。
事实证明,如果一个随机数通过了这个测试,那么它极有可能是一个素数。具体来说,对于1024比特的数,p 不是素数的概率非常小(小于 2^-60)。随着数字变大,通过测试但不是素数的概率会迅速趋近于零。
所以,这个算法并不保证输出一定是素数,但我们知道它非常、非常可能是一个素数。换句话说,它不是素数的唯一可能是我们遇到了概率极小的事件。
另一种说法是,在所有1024比特整数集合中,有一个素数子集,还有一小部分合数(我们称之为“伪素数”)会错误地通过测试。这个伪素数集合非常小,随机选择几乎不可能选中它们。
需要指出的是,这是一个非常简单的素数生成算法,远非最佳算法。我们现在有更好的算法。实际上,一旦你有了一个候选素数,我们现在有非常高效的算法可以毫无疑问地证明这个候选数确实是素数,因此我们甚至不必依赖概率性陈述。尽管如此,费马测试如此简单,我只是想向你展示它是生成素数的一种简单方法,尽管现实中并非如此生成素数。
最后一点,你可能会好奇这个迭代需要重复多少次才能找到一个素数。这是一个经典结果,称为素数定理,它表明经过几百次迭代后,我们大概率会找到一个素数。因此,期望的迭代次数是几百次,不会更多。
欧拉的工作与 Z_p* 的结构
理解了费马定理后,接下来我想谈谈 Z_p* 的结构。这里我们将时间快进100年到18世纪,看看欧拉的工作。欧拉首先用现代语言证明的一件事是:Z_p* 是一个循环群。
Z_p* 是循环群意味着存在某个元素 g ∈ Z_p*,使得如果我们取 g 并计算其一系列幂:g, g^2, g^3, g^4, ...,直到 g^(p-2),这些幂的集合恰好就是整个 Z_p* 群。
注意,没有必要计算超过 g^(p-2) 的幂,因为根据费马定理,g^(p-1) ≡ 1 (mod p)。如果我们继续计算 g^p,它将等于 g;g^(p+1) 将等于 g^2,依此类推,形成一个循环。因此,我们不妨在 g^(p-2) 处停止。
欧拉证明,确实存在这样一个元素 g,它的所有幂次能够生成整个 Z_p* 群。这种类型的元素称为生成元。

让我们看一个简单的例子。设 p = 7,看看3的所有幂次:
3^1 = 33^2 = 9 ≡ 2 (mod 7)3^3 = 27 ≡ 6 (mod 7)3^4 = 81 ≡ 4 (mod 7)3^5 = 243 ≡ 5 (mod 7)3^6 = 729 ≡ 1 (mod 7)(根据费马定理)
我们可以看到,幂次 3^1 到 3^5 给出了集合 {1, 2, 3, 4, 5, 6},即整个 Z_7*。因此,我们说3是 Z_7* 的一个生成元。
需要指出的是,并非每个元素都是生成元。例如,看看2的所有幂次:
2^0 = 12^1 = 22^2 = 42^3 = 8 ≡ 1 (mod 7)
之后开始循环:2^4 ≡ 2, 2^5 ≡ 4, ...。所以2的幂次只生成集合 {1, 2, 4},而不是整个群。因此,2不是 Z_7* 的生成元。
阶的概念
给定 Z_p* 中的一个元素 g,g 的所有幂次构成的集合称为由 g 生成的群,记作 <g>。这个群的大小称为元素 g 在 Z_p* 中的阶。
另一种理解阶的方式是:它是满足 g^a ≡ 1 (mod p) 的最小正整数 a。
很容易看出这两者是等价的。如果我们列出 g 的幂次:1, g, g^2, g^3, ..., g^(ord(g)-1),那么根据定义,g^(ord(g)) ≡ 1。因此,这个集合的大小正好是 g 的阶。
让我们看几个例子。设 p = 7:
- 3的阶:由于3是生成元,它生成整个
Z_7*,其中有6个元素,所以ord(3) = 6。 - 2的阶:如前所述,2的幂次生成集合
{1, 2, 4},大小为3,所以ord(2) = 3。 - 1的阶:由1生成的群只包含
{1},所以ord(1) = 1。实际上,1在任何模素数下的阶总是1。

拉格朗日定理(我们这里说的是一个非常特殊的情况)指出:元素 g 在模 p 下的阶总是整除 p-1。在我们的例子中,6 | (7-1),3 | (7-1)。实际上,费马小定理可以直接从这个事实推导出来。拉格朗日的工作是在19世纪,可以看到我们正沿着时间线前进:从古希腊开始,现在已经到了19世纪。更高级的密码学实际上广泛使用了20世纪的数学。
推广到合数:欧拉定理
理解了 Z_p* 的结构后,让我们将其推广到合数,看看 Z_n* 的结构。这里我想介绍的是欧拉定理,它是费马小定理的直接推广。
欧拉定义了以下函数:给定一个整数 n,他定义了欧拉φ函数 φ(n),其值为集合 Z_n* 的大小。
例如,我们之前看过 Z_12*,它包含元素 {1, 5, 7, 11},因此 φ(12) = 4。
作为一个思考题:φ(p) 是多少?它基本上是 Z_p* 的大小。Z_p* 包含 Z_p 中除0以外的所有元素,因此对于任何素数 p,φ(p) = p - 1。
这里有一个特殊情况,我们将在后续的RSA系统中用到:如果 n 恰好是两个素数 p 和 q 的乘积,那么 φ(n) = n - p - q + 1。让我解释一下为什么:
n是Z_n的大小。- 我们需要移除所有与
n不互质的元素。一个元素不与n互质,意味着它能被p或q整除。 - 在
0到n-1之间,有多少个元素能被p整除?恰好有q个。有多少个能被q整除?恰好有p个。 - 我们减去
p以移除能被q整除的数,减去q以移除能被p整除的数。注意,我们减了0两次,因为0同时能被p和q整除。因此,我们加回1,确保0只被减去一次。 - 所以,
φ(n) = n - p - q + 1。另一种写法是(p-1)(q-1)。
这个事实我们将在后面讨论RSA系统时用到。
到目前为止,这只是定义了欧拉的φ函数。但欧拉将这个函数真正派上了用场,他证明了一个惊人的事实:
对于 Z_n* 中的任意元素 x,都有 x^(φ(n)) ≡ 1 (mod n)。
你可以看到这是费马小定理的推广。特别地,费马定理只适用于素数。对于素数,我们知道 φ(p) = p-1,因此如果 n 是素数,我们只需将 φ(n) 替换为 p-1,就得到了费马小定理。欧拉定理的美妙之处在于它适用于合数,而不仅仅是素数。
让我们看一些例子。以 5 ∈ Z_12* 为例,计算 5^(φ(12))。我们知道 φ(12)=4,所以计算 5^4 = 625。很容易验证 625 mod 12 = 1。这只是一个示例验证,并非证明。实际上,欧拉定理也不难证明,并且它也是拉格朗日一般定理的一个特例。
我们说这是费马小定理的推广,实际上,正如我们将看到的,欧拉定理是RSA密码系统的基础。

总结
本节课中,我们一起学习了:
- 费马小定理:对于素数
p和任意x ∈ Z_p*,有x^(p-1) ≡ 1 (mod p)。我们看到了它在计算模逆元和概率性素数测试中的应用。 - 循环群与生成元:了解了
Z_p*是一个循环群,存在生成元能生成整个群,并引入了元素的“阶”的概念。 - 欧拉定理:这是费马定理的推广。我们定义了欧拉φ函数
φ(n),它表示Z_n*的大小。定理指出,对于任意x ∈ Z_n*,有x^(φ(n)) ≡ 1 (mod n)。这对于理解模合数下的运算至关重要,并且是RSA等密码系统的理论基础。

下一节,我们将继续探讨模二次方程。
053:模e次根 🔢


在本节课中,我们将学习如何求解模二次方程,并更一般地探讨如何计算模e次根。我们将从回顾线性方程求解开始,逐步深入到更高次多项式,特别是模素数下的求解方法。
回顾:模线性方程求解
上一节我们讨论了如何求解模线性方程。其核心是使用求逆算法计算 a 的逆元 a^{-1},然后乘以 -b 得到解。公式如下:
公式: x ≡ a^{-1} * (-b) (mod p)

模多项式求解简介
现在,我们关注更高次的多项式,特别是模素数 p 下的求解。我们感兴趣的多项式形式如下:
x^2 - c ≡ 0 (mod p):求解x即计算c的平方根。y^3 - c ≡ 0 (mod p):求解y即计算c的立方根。z^37 - e ≡ 0 (mod p):求解z即计算e的37次根。
我们固定一个素数 p,并设 c 是 Z_p 中的一个元素。如果存在 x ∈ Z_p 满足 x^e ≡ c (mod p),那么我们称 x 为 c 的模 e 次根。
示例:
- 在
Z_11中,7的立方根是6,因为6^3 = 216 ≡ 7 (mod 11)。 - 在
Z_11中,3的平方根是5,因为5^2 = 25 ≡ 3 (mod 11)。
需要注意的是,模 e 次根并不总是存在。例如,在 Z_11 中,2 的平方根就不存在。

简单情况:e 与 p-1 互质
当指数 e 与 p-1 互质时,情况非常简单。对于 Z_p* 中的任意 c,其模 e 次根总是存在,并且有一个高效的算法可以计算它。
以下是计算步骤:
- 由于
gcd(e, p-1) = 1,我们可以计算e在模p-1下的逆元d。即:d * e ≡ 1 (mod p-1)。 c的模e次根就是c^d mod p。
公式: c^{1/e} ≡ c^d (mod p),其中 d ≡ e^{-1} (mod p-1)。
证明思路: 验证 (c^d)^e ≡ c (mod p)。利用 d*e = k*(p-1) + 1 和费马小定理 c^{p-1} ≡ 1 (mod p),即可完成证明。
这个算法一举两得:既证明了根的存在性,又提供了高效的计算方法。

较难情况:e 与 p-1 不互质(以 e=2 为例)
当 e 与 p-1 不互质时,情况变得复杂。最典型的例子是计算模奇素数 p 下的平方根(此时 e=2 必然整除 p-1)。
在奇素数模下,平方函数 f(x) = x^2 是一个“二对一”的函数,它将 x 和 -x 映射到同一个值 x^2。
示例(模11):
1^2 ≡ 10^2 ≡ 1 (mod 11)2^2 ≡ 9^2 ≡ 4 (mod 11)3^2 ≡ 8^2 ≡ 9 (mod 11)
因此,并非所有元素都有平方根。这引出了以下定义:
- 二次剩余: 在
Z_p中有平方根的元素。 - 非二次剩余: 在
Z_p中没有平方根的元素。
对于奇素数 p,Z_p* 中恰好有一半的元素是二次剩余(加上元素 0,它总是二次剩余),另一半是非二次剩余。
欧拉准则:判断二次剩余
给定 x ∈ Z_p*,如何判断它是否是二次剩余?欧拉提供了一个简洁的判定准则:
定理(欧拉准则): x 是模 p 的二次剩余,当且仅当 x^{(p-1)/2} ≡ 1 (mod p)。否则,x^{(p-1)/2} ≡ -1 (mod p)。
示例(模11): 计算 x^5 mod 11。
- 当
x = 1, 3, 4, 5, 9时,结果为1,这些是二次剩余。 - 当
x = 2, 6, 7, 8, 10时,结果为10 (即 -1),这些是非二次剩余。
值 x^{(p-1)/2} mod p 被称为勒让德符号 (x/p),其结果只能是 1 或 -1。

然而,欧拉准则只解决了“存在性”判断问题,并没有告诉我们如何具体计算平方根。
计算模 p 下的平方根
计算平方根的算法分为两种情况:
情况一:p ≡ 3 (mod 4)
此时计算非常简单,有一个直接的公式:
公式: sqrt(c) ≡ c^{(p+1)/4} (mod p)
验证: (c^{(p+1)/4})^2 = c^{(p+1)/2} = c^{(p-1)/2} * c ≡ 1 * c ≡ c (mod p)。这里用到了对于二次剩余 c,有 c^{(p-1)/2} ≡ 1。
情况二:p ≡ 1 (mod 4)
此时没有像上面那样简单的确定性公式。但是,存在高效的随机化算法可以在多项式时间内计算出平方根。一个有趣的事实是,如果扩展黎曼猜想成立,那么将存在确定性的多项式时间算法。就目前所知,计算平方根的时间复杂度约为 O(log^3 p)。
求解模 p 下的二次方程

掌握了平方根的计算方法后,我们现在可以求解模 p 下的二次方程 ax^2 + bx + c ≡ 0 (mod p)。
求解过程与高中代数类似,使用求根公式:
公式: x ≡ [-b ± sqrt(b^2 - 4ac)] * (2a)^{-1} (mod p)
我们需要计算:
(2a)^{-1} mod p:使用扩展欧几里得算法求逆。sqrt(b^2 - 4ac) mod p:使用上一小节介绍的平方根算法。
方程有解的前提是b^2 - 4ac是模p下的二次剩余。
模合数 N 下的 e 次根
现在,我们将问题扩展到模合数 N 的情况:何时存在模 e 次根?如果存在,能否高效计算?
这个问题变得极其困难。据我们所知,对于一般的 e,计算模合数 N 的 e 次根的难度,与分解 N 的难度相当。
目前最好的算法都需要先分解模数 N。一旦成功分解 N = p * q,问题就简化为:
- 分别在模
p和模q下计算e次根(这相对容易)。 - 利用中国剩余定理将两个解组合成模
N下的解。

这是一个鲜明的对比:计算模素数的 e 次根通常很容易,但计算模合数的 e 次根则异常困难,其安全性依赖于大整数分解的困难性。
总结
本节课我们一起学习了模 e 次根的计算:
- 模素数 p:
- 简单情况 (
gcd(e, p-1)=1): 根恒存在,可通过c^{e^{-1} mod (p-1)}直接计算。 - 平方根情况 (
e=2): 仅一半元素有根。可用欧拉准则判断。计算时,若p≡3 mod 4有直接公式;若p≡1 mod 4需用随机化算法。 - 由此可求解模
p下的二次方程。
- 简单情况 (
- 模合数 N: 计算
e次根被认为与分解N一样困难,这是许多密码学方案安全性的基础。

下一节,我们将转向模运算算法,探讨模素数和合数下的加法、乘法及幂运算。
054:算术算法

在本节课中,我们将学习如何计算大整数的模运算。我们将从大整数在计算机中的表示方法开始,然后探讨加法、减法、乘法和除法的计算复杂度。最后,我们将重点学习一种称为“重复平方法”的高效算法,用于计算大指数幂。
大整数的表示

首先,我们来看看如何在计算机中表示大整数。这其实相当直接。想象我们在一台64位机器上,我们会将要表示的数字分解成多个32位的“桶”。这些32位的桶组合在一起,就代表了我们要存储在计算机中的数字。
需要说明的是,64位寄存器只是一个例子。实际上,许多现代处理器拥有128位甚至更宽的寄存器,并且可以在其上执行乘法运算。因此,通常我们会使用比32位大得多的块。之所以限制在32位,是为了确保两个块相乘的结果仍然小于机器的字长(例如64位)。
基本算术运算的复杂度
接下来,我们看看具体的算术运算,了解每种运算需要多长时间。
加法与减法
加法与减法基本上是带进位的加法或带借位的减法。这些是线性时间操作。换句话说,如果要相加或相减两个n位整数,运行时间基本上与n成线性关系。
乘法
朴素乘法需要二次方时间。这实际上就是所谓的“高中算法”,即你在学校学到的那种乘法。思考一下你会发现,该算法的运行时间与相乘数字的长度成二次方关系。
在20世纪60年代,卡拉楚巴算法带来了一个重大惊喜,它实现了比二次方时间好得多的性能,其运行时间为O(n^1.585)。这个1.585基本上是log₂3。

更令人惊讶的是,使用目前最好的乘法算法,实际上可以在大约O(n log n)的时间内完成乘法,几乎是线性时间。然而,这是一个极其渐近的结果,大O符号中隐藏了非常大的常数。因此,该算法仅在数字绝对巨大时才变得实用,所以并不常用。卡拉楚巴算法则非常实用,大多数密码学库都实现了它用于乘法。
为了简单起见,在这里我将忽略卡拉楚巴算法,并假设乘法以二次方时间运行。但在你的脑海中应该始终记得,乘法实际上比二次方要快一点。
带余除法
在乘法之后,下一个问题是带余除法。事实证明,这也是一个二次方时间算法。
指数运算与重复平方法
剩下的主要操作,也是我们迄今为止多次使用但从未解释如何计算的操作,就是指数运算问题。
让我们更抽象地解决这个指数运算问题。想象我们有一个有限循环群G。这意味着该群由某个生成元g的幂次生成。例如,可以将这个群视为Z_p^*,将小g视为大G的某个生成元。
我这样表述的原因,是希望你们开始习惯这种抽象,即我们处理一个通用群G,而Z_p*只是这种群的一个例子。实际上,还有许多其他有限循环群的例子。再次强调,群G基本上就是这个生成元的幂次,直到群的阶,我将其写作gq。
现在,我们的目标是给定这个元素g和某个指数x,计算g^x的值。

你可能会想,如果x等于3,我想计算g^3。这没什么可做的,我只需要做g * g * g,就得到了g^3。这确实很容易。但事实上,这不是我们感兴趣的情况。在我们的案例中,指数将是巨大的。想象一个500位的数字。如果你试图通过g乘以g再乘以g……来计算g的500次方,这将花费相当长的时间,实际上是指数时间,这不是我们想做的。
问题是,即使x是巨大的,我们是否仍然能相对快速地计算g^x?答案是肯定的,实现这一点的算法称为“重复平方法”。
让我展示重复平方法是如何工作的。我们以计算g^53为例。朴素方法需要53次g的连乘。但我想展示如何快速完成。
我们将把53写成二进制形式。53的二进制表示是110101。这意味着53等于32 + 16 + 4 + 1。
因此,g^53 = g(32+16+4+1)。利用指数运算规则,我们可以将其分解为g32 * g^16 * g^4 * g1。这应该让你开始明白如何快速计算g53了。
以下是计算步骤:
- 我们取g,开始将其平方。
- 平方一次得到g^2。
- 再平方得到g^4。
- 再平方得到g^8。
- 再平方得到g^16。
- 再平方得到g^32。
- 现在,我们只需将适当的幂次相乘,得到g^53。
所以,g^1 * g^4 * g^16 * g^32 就得到了我们想要的值g^53。
这里我们看到,我们只需要进行5次平方运算,加上4次乘法运算。总共9次乘法,我们就计算出了g^53。这非常有趣。事实证明,这是一种普遍现象,允许我们将g提升到非常高的幂次,并且非常快速地完成。
让我展示这个算法。输入是元素g和指数x,输出是g^x。
算法步骤如下:
- 将x写成二进制表示法。假设x有n位。
- 我们使用两个寄存器:Y寄存器不断被平方,Z寄存器是一个累加器,根据需要乘以g的适当幂次。
- 我们从最低有效位开始,循环遍历x的每一位。
- 在每次迭代中,我们平方y(即 y = y * y)。
- 每当指数x的对应位为1时,我们将y的当前值累加到累加器z中(即 z = z * y)。
- 循环结束后,输出z。

这就是整个重复平方法算法。
让我们用g53的例子来看一下。Y列在迭代过程中不断平方,遍历2的幂次。Z列更有趣,它在指数对应位为1时累加g的适当幂次。例如,指数的第一位是1,所以在第一次迭代后,z等于g。第二位是0,z不变。第三位是1,我们将g4累加到z中,以此类推,最终得到g^53。
这个算法的迭代次数基本上是log₂(x)。即使x是一个500位的数字,也只需要大约500次迭代(实际上是约1000次乘法,因为每次迭代需要一次平方和可能一次累乘),我们就能将g提升到500位指数的幂次。
运行时间总结
现在我们可以总结运行时间了。假设我们有一个n位模数N。
- 加法/减法:在Z_N中为线性时间 O(n)。
- 乘法:为简单起见,我们说是二次时间 O(n²)。(实际有更快的算法如卡拉楚巴)。
- 指数运算:基本上需要log(x)次迭代,每次迭代我们基本上进行两次乘法。所以运行时间是O(log(x) * T_multiply)。假设乘法时间是二次的,那么运行时间就是O(n² log x)。由于x总是小于N(根据费马小定理,将g提升到大于模数的幂次没有意义),假设x也是一个n位整数,那么指数运算实际上是一个立方时间算法 O(n³)。这就是我希望你记住的:指数运算实际上是一个相对较慢的操作。
如今,在现代计算机上这需要几微秒,但对于一个4GHz的处理器来说,几微秒也是相当多的工作量。请记住,我们讨论的所有指数运算操作,例如判断一个数是否为二次剩余,所有这些指数运算基本上都需要立方时间。

本节课中,我们一起学习了计算机中表示大整数的方法,回顾了基本算术运算(加、减、乘、除)的时间复杂度,并重点掌握了用于高效计算大指数幂的“重复平方法”算法。理解这些基础算术算法的效率,对于后续学习密码学中的困难问题至关重要。在下一节中,我们将开始讨论关于模数、素数与合数的困难问题。
055:难解问题

在本节课中,我们将学习模运算中的一些难解问题。这些问题将是下周我们构建密码系统的基础。

首先需要指出,模运算中存在许多简单问题。例如,给定一个整数 N 和 Z_N 中的一个元素 x,使用欧几里得算法求 x 的逆元实际上非常容易。同样,给定一个素数 p 和一个多项式 f,在 Z_p 中寻找该多项式的根也相对容易。事实上,存在一个高效算法,其运行时间与多项式的次数呈线性关系。因此,至少对于低次多项式,在模素数下求根是相当容易的。
然而,模运算中的许多问题实际上相当困难。正如我所说,这些难题构成了许多公钥密码系统的基础。
模素数下的经典难题
让我们来看一些模素数下的经典难题。固定一个大的素数 P,可以将其想象为一个600位的素数。然后固定 Z_P* 中的一个元素 G,并假设该元素 G 的阶恰好是一个数 Q。

现在考虑指数函数,它简单地将一个数 x 映射到元素 G^x。我们在上一节中已经证明,使用重复平方法可以轻松计算这个函数。因此,计算 G^x 实际上可以非常高效地完成。
但让我们看看逆问题。逆问题基本上是:给定 G^x,现在要求你找到它的对数,即值 x。在实数域中,给定 G^x 求 x 正是对数函数的定义。但这里我要求你求模素数 P 下的对数。这个问题被称为离散对数问题。G^x 以 G 为底的离散对数就是指数 x。因此,我们的目标是输出某个在 0 到 Q-2 范围内的指数 x,该指数恰好是 G^x 的对数。
让我们看一个例子。假设我们观察模11的整数,这里我写下了以2为底的 Z_11 中的离散对数函数。首先,1的离散对数是0,因为 2^0 = 1。类似地,2的离散对数是1,因为 2^1 = 2。4的离散对数是2,因为 2^2 = 4。8的离散对数是3,因为 2^3 = 8,依此类推。
这里我为你列出了离散对数值。让我问你一个谜题:5模11的离散对数是多少?答案是4,因为 2^4 = 16,而 16 ≡ 5 (mod 11)。这说明以2为底的5的离散对数是4。
一般来说,离散对数函数实际上相当难以计算。当然,对于小素数来说很容易,你可以制作一个表格并读出离散对数值。但如果素数 P 是一个大数,比如一个2000位的数,那么计算离散对数就相当困难,我们没有好的算法来做到这一点。
离散对数问题的通用定义
让我们更通用地定义离散对数问题。不局限于群 Z_P*,让我们抽象地看一个通用群 G。我们有一个有限循环群,其生成元为 g。这意味着这个群仅由 g 的所有幂次组成,我们取所有幂次直到其阶(在本例中,g 的阶为 Q)。这些幂次实际上构成了群 G。
如果实际上没有高效算法能够计算离散对数函数,我们就说离散对数问题在群 G 中是困难的。这意味着,如果你在群 G 中随机选择一个元素 g 和一个随机指数 x,我给算法 g 和 g^x(当然我还需要给它群的描述和群的阶),算法实际计算出离散对数的概率是可忽略的。如果这对所有高效算法都成立,那么我们就说离散对数问题在这个群 G 中是困难的。我们这样说是因为没有高效算法能够以不可忽略的概率计算离散对数。

正如我提到的,我们有几个候选群组,其中离散对数是困难的。典型例子是 Z_P*,这实际上是 Diffie-Hellman 在1974年提出的例子。但事实证明,还有其他候选群组,其中离散对数问题被认为是困难的。我之前提到的一个候选是所谓的椭圆曲线群,即椭圆曲线上的点集。我不在这里定义它,但正如我所说,如果你想让我讨论椭圆曲线密码学,我可以在课程的最后一周进行讲解。
这两个群组中,离散对数问题被认为是困难的。但就我们所知,离散对数问题在椭圆曲线上比在 Z_P* 中更难。换句话说,如果你给我两个大小相同的问题,一个在群 Z_P* 中,一个在椭圆曲线群中,椭圆曲线群中的问题将比 Z_P* 中的问题难得多。因为椭圆曲线上的离散对数问题比在 Z_P* 中更难,这意味着我们在使用椭圆曲线时可以使用更小的参数,而使用 Z_P* 则需要更大的参数。因此,基于椭圆曲线的系统将更高效,因为参数更小,而攻击者的工作难度却与 Z_P* 中使用大得多的素数时一样。
具体来说,在 Z_P* 中,存在一种用于离散对数的亚指数时间算法。如果你有一个 n 位的素数,该算法的运行时间大约为 e(n(1/3))。实际上,该算法的精确运行时间中还有许多其他项,但主导项是素数位数的立方根。由于这个算法的存在,如果我们希望离散对数问题与破解相应对称密钥一样困难,就必须使用相对较大的模数 P。

相比之下,如果你观察椭圆曲线群,情况要好得多。在椭圆曲线群上,我们拥有的最佳离散对数算法的运行时间为 e^(n/2)。这是一个真正的指数时间算法,因为对于大小为 n 的问题,运行时间大致是 e^n,是 n 的指数函数,而不是 n 的立方根的指数函数。因为问题困难得多,我们拥有的最佳算法实际上需要指数时间。你注意到,在椭圆曲线上,我们可以使用小得多的参数,同时仍然保持安全。顺便说一下,椭圆曲线大小恰好是对称密钥大小两倍的原因,正是由于这里指数中的因子 2。我们必须将椭圆曲线的大小加倍,才能使问题实际需要 e^n 的时间。实际上,我这里有一个小笔误,这应该是 2^(n/2),但对数的确切底数并不重要。
我之前提到过,由于椭圆曲线的参数比 Z_P* 小,因此从模 P 密码学过渡到椭圆曲线密码学是一个缓慢的过程。同样,我会提到,如果你想让我更详细地描述椭圆曲线,我可以在课程的最后一周进行讲解。
离散对数问题的应用:构造抗碰撞哈希函数
既然我们理解了什么是离散对数问题,让我给你一个离散对数困难性的直接应用。具体来说,让我们基于离散对数问题构建一个抗碰撞哈希函数。
选择一个离散对数问题困难的群 G。如果你愿意,可以将 G 视为群 Z_P*,并假设群 G 具有素数阶 Q。Q 是某个素数,恰好是群 G 的元素数量。

为了定义我们的哈希函数,我们将在群 G 中选择两个元素,称之为 g 和 h。然后我们如下定义哈希函数:哈希函数在输入 x 和 y 时,将输出 G 中的一个元素,定义为 g^x * h^y。就是这样,一个非常简单的哈希函数。如果你还记得,我们在之前讨论压缩函数时甚至提到过这个哈希函数。
我想向你展示,这个函数 H 实际上是抗碰撞的,其意义在于:找到 H 的碰撞与在群 G 中计算离散对数一样困难。具体来说,如果你能为 H 找到一个碰撞,你就可以计算以 g 为底的 h 的离散对数。既然我们说群 G 中的离散对数是困难的,计算离散对数应该是困难的,因此找到 H 的碰撞也将是困难的。
让我们看看如何证明这一点。这实际上是一个非常巧妙的证明。我们将这样做:假设我们得到了函数 H 上的一个碰撞。我们得到了两个不同的数对 (x0, y0) 和 (x1, y1),它们在函数 H 上发生碰撞。这意味着,如果我计算 H(x0, y0) 和 H(x1, y1),我会得到一个碰撞,即得到等式:g^x0 * h^y0 = g^x1 * h^y1。
现在我可以进行一些操作。我将所有 g 移到一边,所有 h 移到另一边。这意味着 g^(x0 - x1) = h^(y1 - y0)。现在我可以对两边取 (y1 - y0) 次方根。换句话说,我取两边的 (y1 - y0) 次方根。一边将变成简单的 h,另一边将变成 g 的 (x0 - x1)/(y1 - y0) 次方。看看我们刚刚得到了什么:我们基本上将 h 表示为 g 的某个已知幂次。我们所做的只是除法,然后我们就完成了——我们刚刚计算出了以 g 为底的 h 的离散对数。
你可能会问,指数中的除法是什么意思?除以 (y1 - y0) 在指数中意味着什么?它的意思是,我们计算 (y1 - y0) 模 Q 的逆元,然后将结果乘以 (x0 - x1),这就给出了清晰的指数。因此,我们刚刚得知了以 g 为底的 h 的离散对数。
这也说明了为什么我们希望 Q 是素数:我们需要确保 (y1 - y0) 总是可逆的。事实上,我们知道在模素数下,除了 0 之外,所有元素都是可逆的。这实际上引出了几个问题:如果 (y1 - y0) 恰好等于 0 怎么办?如果是这种情况,我们将无法得到离散对数,因为我们无法除以零。
但如果你思考一下,你会发现:如果 y1 - y0 = 0,那意味着 y1 = y0。但如果 y1 = y0,看看这个等式,那必然意味着 x0 也等于 x1。是的,这需要一点时间来理解:如果 y0 = y1,这两项基本上就抵消了,然后我们得到 x0 = x1。但如果 x0 = x1 且 y0 = y1,你给我的就不是一个碰撞,你只是给了我相同的数对两次,这是作弊,不被视为碰撞。因此,我不需要找到离散对数。但如果你给我一个碰撞,必然有 y0 ≠ y1,结果我就能得到以 g 为底的 h 的离散对数。
正如我们所说,由于离散对数在群 G 中被认为是困难的,这意味着这个非常简单的函数 H 必须是抗碰撞的。这是一个从寻找碰撞归约到计算离散对数的非常优雅的例子。
顺便说一下,我应该告诉你,这个函数并没有真正被使用。尽管这个函数有一个漂亮的抗碰撞性证明,但它并不常用,因为它相对较慢。本质上,每次哈希都必须计算两次指数运算,这比像 SHA-256 这样的函数以及其他类型的专用抗碰撞哈希函数要慢得多。
模合数下的难解问题

以上是关于模素数下难解问题的内容。现在让我们看看模合数下的一些难解问题。这里我们将再次观察,比如1024位的数,并定义集合 Z_{2N}。这将是所有整数的集合,这些整数恰好是两个素数的乘积,且这两个素数都是 n 位的素数。
这里的“2”对应于这样一个事实:这个集合中的数基本上有两个素因子,并且这两个素因子大小大致相同,都是 n 位的素数。在集合 Z_{2N} 中有两个经典的难解问题。
第一个问题是:如果我随机选择集合 Z_{2N} 中的一个整数,问题是分解它。对于1024位的数来说,这已经是一个相当困难的问题。虽然目前尚未实现,但很可能这种量级的数很快就会被分解。因此,目前推荐的值实际上是使用2048位的数,这仍然超出我们的能力范围,这些数我们仍然无法分解。
模合数下难解问题的另一个例子是:如果我给你一个非线性多项式(其次数大于1),并给你集合 Z_{2N} 中的一个随机合数,你的目标是找到该多项式的一个根,即找到一个 x,使其成为该多项式的根。同样,我们不知道如何做到这一点。当然,如果次数等于1,那只是解线性方程,我们已经知道这很容易。但是一旦次数变成非线性的,我们不知道如何在不首先分解模数然后计算群的情况下模 N 求解这个问题。
有一些系统,例如 RSA,依赖于特定多项式的这个问题的困难性,我们将在下周看到。作为一个例子,我应该提到,例如,在随机合数 Z_{2N} 下取平方根或立方根被认为是困难的。
关于分解问题,实际上有很多已知信息。这是一个非常古老的问题,古希腊人就对分解感兴趣。高斯实际上有一段精彩的引述,谈到了分解问题和素性测试问题。在他1805年的著名论文中,他写道:“区分素数和合数的问题(即素性测试)以及将后者(即合数)分解为其素因子的问题,被认为是算术中最重要和最有用的(问题)之一。”他有远见地意识到这些是极其有趣的问题。这些本质上是计算机科学问题:我们如何测试一个数是否为素数?如果一个数不是素数而是合数,我们如何分解它?高斯已经意识到这些是极其重要和有趣的问题,如今这些问题实际上在网络上被广泛使用。
事实上,测试一个数是否为素数已经完全解决了。我们现在知道如何使用随机算法高效地做到这一点,甚至知道如何使用确定性算法做到这一点。将合数分解为其素因子,这仍然被认为是一个难题。我鼓励你思考一下这个问题。这是一个值得思考的奇妙问题。如果你们中有人能解决它,提出一个将合数分解为素因子的算法,正如我所说,这将在密码学界立即成名,并对整个网络的安全产生巨大影响。所以,这是一个有趣的问题。
让我告诉你关于分解问题的已知情况。我们拥有的最佳算法称为数域筛法。它的运行时间也是这种指数形式,是指数的立方根,这就是为什么合数必须相当大才能使问题困难。尽管目前的世界纪录只是分解了一个768位的数,这被称为 RSA-768 挑战数,是一个最近被分解的约200位的数。分解这个数已经耗费了巨大的工作量,大约在数百台机器上花费了两年时间。最终他们成功分解了这个数。据估计,分解一个1024位的数比分解 RSA-768 大约困难1000倍。因此,不是两年,而是需要2000年。当然,计算机速度越来越快,我们有更多的核心和计算机可用。这个1000倍的因子,假设摩尔定律等,实际上只意味着一个十年。计算机速度大约每十年提高1000倍。因此,很可能在未来十年内,我们将看到1024位数的分解,这将意味着1024位公钥密码学的终结。
这就是分解领域的最新技术水平,本模块到此结束。我会提到,如果你想阅读更多关于我们讨论的任何主题的内容,网上有一本好书,是 Victor Shoup 写的免费电子书,你可以下载。特别是,我们讨论的主题涵盖在第1至4章、第11章和第12章。我鼓励你去看一看,希望这有助于理解材料。

下周,我们将开始使用刚刚学到的主题构建密码系统。
056:定义与安全性 🔐
在本节课中,我们将要学习公钥加密的基本定义及其安全性概念。我们将从回顾公钥加密的构成开始,然后详细探讨其安全性,特别是针对窃听攻击和更强大的选择密文攻击的安全性定义。

公钥加密系统回顾
上一周我们学习了许多公钥加密所需的理论。本节中,我们来看看如何将这些知识付诸实践,构建安全的公钥加密方案。但首先,我们需要明确什么是公钥加密,以及公钥加密的安全性意味着什么。

让我提醒你,在一个公钥加密方案中,有一个加密算法(我们通常用 E 表示)和一个解密算法(用 D 表示)。然而,这里的加密算法需要一个公钥,而解密算法需要一个私钥。

这对密钥被称为密钥对。公钥用于加密消息,而私钥用于解密消息。
因此,在这种情况下,消息 M 使用公钥进行加密,得到的结果是密文 C。
类似地,密文被输入到解密算法中,并使用私钥进行解密。解密算法输出的就是原始消息 M。

公钥加密的应用
公钥加密有许多应用。上周我们看到了经典应用——会话建立,即密钥交换。目前我们只关注仅能抵抗窃听的密钥交换。如果你还记得协议的工作方式,基本上爱丽丝会生成一个公钥-私钥对,并将公钥发送给鲍勃。鲍勃会生成一个随机的 x,这将作为他们的共享秘密,然后他将 x 用爱丽丝的公钥加密后发送给她。爱丽丝可以解密并恢复 x,现在他们双方都拥有了这个共享秘密 x,可以用来安全地通信。攻击者当然只能看到公钥和用公钥加密的 x,他应该无法从中获取关于 x 的任何信息。我们将更精确地定义这一点,以理解“无法了解关于 **x` 的任何信息”意味着什么。
公钥加密实际上还有许多其他应用。例如,它在非交互式应用中非常有用。以电子邮件系统为例。
这里,鲍勃想给爱丽丝发送邮件。当鲍勃发送邮件时,邮件会从一个邮件中继传递到另一个邮件中继,最终到达爱丽丝那里,此时爱丽丝应该能够解密。电子邮件系统的设置是为非交互式场景设计的,鲍勃发送邮件,然后爱丽丝接收,爱丽丝不需要与鲍勃通信来解密邮件。因此,在这种情况下,由于非交互性,爱丽丝和鲍勃之间没有机会建立共享秘密。所以,这里发生的情况是,鲍勃基本上会使用爱丽丝的公钥加密邮件并发送。世界上任何人都可以使用她的公钥加密邮件发送给爱丽丝。当爱丽丝收到这封邮件时,她使用自己的私钥解密密文并恢复明文消息。
当然,在这样的系统中有一个需要注意的地方是,鲍勃需要以某种方式获得爱丽丝的公钥。目前我们假设鲍勃已经拥有爱丽丝的公钥,但稍后当我们讨论数字签名时,我们将看到如何通过所谓的公钥管理非常高效地完成这项工作。正如我所说,我们稍后会再回到这个问题,但我想让你记住的主要一点是,公钥加密用于会话建立,这在网络上非常常见,公钥加密用于在网络浏览器和网络服务器之间建立安全密钥。

公钥加密对于非交互式应用也非常有用,世界上任何人非交互式地需要向爱丽丝发送消息时,都可以使用爱丽丝的公钥加密消息,而爱丽丝可以解密并恢复明文。

公钥加密系统的构成
让我更详细地提醒你公钥加密系统是什么。它由三个算法组成:G、E 和 D。

- G 被称为密钥生成算法。基本上,它会生成这个密钥对,即公钥和私钥。如上所述,G 不接受参数,但在现实生活中,G 实际上接受一个称为安全参数的参数,该参数指定此密钥生成算法生成的密钥大小。
- 然后是这些加密算法,像往常一样,它们接受一个公钥和一个消息,并产生一个密文。
- 解密算法接受相应的私钥和一个密文,并产生相应的消息。
像往常一样,为了保持一致性,我们说,如果我们在给定的公钥下加密一条消息,然后用相应的私钥解密,我们应该得到原始消息。

公钥加密的安全性定义
我将从定义针对窃听的安全性开始,然后我们将定义针对主动攻击的安全性。
定义针对窃听的安全性的方式与我们之前看到的对称加密情况非常相似,上周我们已经看到了这一点,所以我将快速回顾一下。基本上,攻击游戏定义如下:我们定义两个实验,实验0和实验1。在任一实验中,挑战者将生成一个公钥-私钥对,并将公钥交给对手。对手将输出两个等长的消息 M0 和 M1,然后他得到的是 M0 的加密或 M1 的加密。在实验0中,他得到 M0 的加密;在实验1中,他得到 M1 的加密。然后对手应该说出他得到的是哪一个:是 M0 的加密还是 M1 的加密。

在这个游戏中,攻击者只得到一个密文。这对应于一次窃听攻击,他只是窃听到了那个密文 C。现在他的目标是判断密文 C 是 M0 的加密还是 M1 的加密。目前还不允许对密文 C 进行篡改。
像往常一样,我们说一个公钥加密方案是语义安全的,如果攻击者无法区分实验0和实验1。换句话说,他无法分辨他得到的是 M0 的加密还是 M1 的加密。


与对称加密定义的比较
在我们转向主动攻击之前,我想提一下我们刚刚看到的定义与对称密码窃听安全定义之间的一个快速关系。如果你还记得,当我们讨论对称密码的窃听安全时,我们区分了密钥使用一次和密钥使用多次的情况。事实上,我们看到存在明显的区别。例如,如果密钥用于加密单个消息,一次一密是安全的,但如果密钥用于加密多个消息,则完全不安全。事实上,我们有两个不同的定义:一个是一次性安全的定义,另一个是当密钥多次使用时更强的单独定义。

我在上一张幻灯片上展示的定义与对称密码的一次性安全定义非常相似。事实上,对于公钥加密来说,如果一个系统在某种意义上对一次性密钥是安全的,那么它对多次使用密钥也是安全的。换句话说,我们不必明确赋予攻击者请求加密他选择的消息的能力,因为他可以自己创建这些加密。他被给予了公钥,因此他可以自己加密任何他喜欢的消息。
因此,在某种意义上,任何公钥-私钥对本质上都用于加密多个消息,因为攻击者可能已经使用我们在第一步中给他的公钥加密了许多他自己选择的消息。
所以,结果就是,事实上,一次性安全的定义足以暗示多次使用的安全性,这就是为什么我们将这个概念称为选择明文攻击下的不可区分性。
这只是一个小点,用于解释为什么在公钥加密的设置中,我们不需要更复杂的定义来捕捉窃听安全性。
主动攻击与选择密文安全
现在我们理解了窃听安全性,让我们看看能够发起主动攻击的更强大的对手。
特别是,让我们看看电子邮件示例。这里我们有我们的朋友鲍勃,他想给他的朋友卡罗琳发送邮件,而卡罗琳恰好有一个Gmail账户。其工作方式基本上是邮件被发送到Gmail服务器,经过加密,Gmail服务器解密邮件,查看预期收件人,然后如果预期收件人是卡罗琳,它将邮件转发给卡罗琳;如果预期收件人是攻击者,它将邮件转发给攻击者。
这类似于Gmail的实际工作方式,因为发件人会通过SSL加密将邮件发送到Gmail服务器,Gmail服务器会终止SSL,然后将邮件转发给适当的收件人。
现在假设鲍勃使用一个允许对手篡改密文而不被检测的系统加密邮件。例如,想象这封邮件是使用计数器模式之类的东西加密的。
那么当攻击者拦截这封邮件时,他可以更改收件人,使收件人现在显示为 attacker@gmail.com。我们知道,对于计数器模式来说,这很容易做到。攻击者知道邮件是发给卡罗琳的,他只对邮件正文感兴趣,因此他可以轻松地将邮件收件人更改为 attacker@gmail.com。现在当服务器收到邮件时,它会解密,看到收件人应该是攻击者,并将正文转发给攻击者。现在攻击者就能够阅读原本打算给卡罗琳的邮件正文了。

这是一个典型的主动攻击示例。你注意到攻击者在这里可以做的是,它可以解密任何预期收件人是“收件人:攻击者”的密文,即任何明文以“收件人:攻击者”开头的密文。
所以我们的目标是设计安全的公钥系统,即使攻击者可以篡改密文并可能解密某些密文。再次强调,这里的攻击者的目标是获取消息正文,攻击者已经知道邮件是发给卡罗琳的,他所要做的只是更改预期收件人。

选择密文安全定义
篡改攻击引出了选择密文安全的定义。事实上,这是公钥加密的标准安全概念。
让我解释一下攻击游戏如何进行。正如我所说,我们的目标是构建在这种非常保守的加密概念下安全的系统。这里我们有一个加密方案 G, E, D,假设它定义在消息空间和密文空间 M, C 上。像往常一样,我们将定义两个实验:实验0和实验1。这里的 b 表示挑战者是在实现实验0还是实验1。挑战者首先生成一个公钥-私钥对,然后将公钥交给对手。
现在对手可以说:“这里有一堆密文,请为我解密它们。” 因此,对手提交密文 C1,他得到密文 C1 的解密结果 M1。他可以一次又一次地这样做:提交密文 C2,得到解密结果 M2;提交密文 C3,得到解密结果 M3,依此类推。最终,对手表示查询阶段结束,现在他像往常一样提交两个等长的消息 M0 和 M1,并收到作为响应的挑战密文 C,该密文是 M0 的加密或 M1 的加密,具体取决于我们是在实验0还是实验1。
现在对手可以继续发出密文查询,即他可以继续发出解密请求。他提交一个密文,并得到该密文的解密结果。但当然,这里必须有一个限制:如果攻击者可以提交他选择的任意密文,他当然可以破解挑战。他会做的是将挑战密文 C 作为解密查询提交,然后他就会被告知在挑战阶段他得到的是 M0 的加密还是 M1 的加密。因此,我们在这里设置了限制,规定他实际上可以提交他选择的任何密文,除了挑战密文。
因此,攻击者可以请求解密他选择的任何密文,只要不是挑战密文。即使他得到了所有这些解密结果,他仍然不应该能够分辨出他得到的是 M0 的加密还是 M1 的加密。
你注意到这是一个非常保守的定义。它赋予了攻击者比我们在上一张幻灯片上看到的更多的权力。在上一张幻灯片中,攻击者只能解密那些明文以“收件人:攻击者”开头的消息。这里我们说,攻击者可以解密他选择的任何密文,只要它与挑战密文 C 不同。
然后他的目标是说出挑战密文是 M0 的加密还是 M1 的加密。像往常一样,如果他不能做到这一点,换句话说,他在实验0中的行为与在实验1中的行为基本相同,那么即使他拥有所有这些权力,他也无法区分 M0 的加密和 M1 的加密,我们就说该系统是选择密文安全的,即 CCA 安全。有时有一个缩写词,其缩写是“选择密文攻击下的不可区分性”,但我只说 CCA 安全性。

CCA 安全如何捕捉电子邮件示例

让我们看看这如何捕捉我们之前看到的电子邮件示例。假设使用的加密系统使得攻击者仅给定消息的加密,就可以将预期收件人从“给爱丽丝”更改为“给查理”。

以下是他在 CCA 游戏中获胜的方式:第一步,他被给予公钥。

然后攻击者会做的是,他会发出两个等长的消息,即在第一个消息中,正文是0,在第二个消息中,正文是1,但两个消息都是发给爱丽丝的。作为响应,他将被给予挑战密文 C。
现在,我们有了挑战密文 C。攻击者接下来要做的是,他将利用他在这里修改预期收件人的能力,发送回一个密文 C',其中 C' 是消息“给查理,正文为挑战正文 b”的加密。
记住,b 要么是0,要么是1。因为明文不同,我们知道密文也必然不同,所以特别是 C' 必须与挑战密文 C 不同。因此,这里的 C' 必须不同于 C。结果,根据 CCA 游戏的定义,可怜的挑战者现在必须解密。挑战者必须解密任何不等于挑战密文的密文。所以挑战者解密,给对手 M',基本上他给了对手 b,现在对手可以输出挑战 b,并以优势1赢得游戏。因此,对于这个特定方案,他的优势是1。仅仅因为攻击者能够将挑战密文从一个收件人更改为另一个收件人,就允许他以优势1赢得 CCA 游戏。
正如我所说,CCA 安全性实际上是公钥加密系统的正确安全概念。这是一个非常、非常有趣的概念。基本上,即使攻击者拥有解密他想要的任何东西的能力(除了挑战密文),他仍然无法了解挑战密文是什么。
因此,我们在本模块剩余部分以及下一个模块的目标是构建 CCA 安全的系统。实际上,这是可以实现的,这相当了不起,我将向你展示具体如何做到。事实上,我们构建的那些 CCA 安全系统正是现实世界中使用的系统。每当一个系统试图部署非 CCA 安全的公钥加密机制时,总会有人提出攻击并能够破解它。我们实际上将在接下来的几个部分中看到一些这样的攻击示例。

总结

本节课中我们一起学习了公钥加密的核心定义。我们回顾了公钥加密系统的构成,包括密钥生成算法 G、加密算法 E 和解密算法 D。我们定义了针对窃听攻击的语义安全性,并将其与对称加密的安全性概念进行了比较。更重要的是,我们深入探讨了针对主动攻击的选择密文安全概念,即 CCA 安全性。这是一个非常强大的安全概念,要求即使攻击者能够解密除挑战密文外的任意密文,也无法获取挑战密文所保护消息的任何信息。理解这些定义是设计和分析安全公钥加密方案的基础。
057:构造方法 🔐

在本节课中,我们将学习如何利用“陷门置换”这一核心概念来构建公钥加密系统。我们将首先定义陷门函数及其安全性,然后展示如何将其与对称加密方案结合,从而构造出安全的公钥加密方案。最后,我们会指出一个常见但完全不安全的错误构造方法。
陷门函数定义
上一节我们介绍了公钥加密系统及其安全目标。本节中,我们来看看构建安全公钥加密的一个基础工具:陷门函数。

陷门函数本质上是一个从集合 X 映射到集合 Y 的函数,它由三个算法定义:
- 密钥生成算法 G:生成一个密钥对
(PK, SK)。 - 函数 F:使用公钥
PK,可以轻松计算任何x ∈ X对应的y = F_PK(x) ∈ Y。 - 逆函数 F⁻¹:使用私钥
SK,可以轻松计算任何y ∈ Y对应的原像x = F⁻¹_SK(y) ∈ X。
其核心思想是:拥有公钥 PK 可以轻松正向计算函数,但只有拥有私钥 SK(即“陷门”)才能轻松反向计算(求逆)。
陷门函数的安全性
一个陷门函数是安全的,当且仅当函数 F_PK 是一个单向函数。这意味着在不知道私钥 SK 的情况下,即使知道公钥 PK 和函数输出 y,任何高效的攻击者也无法计算出其原像 x。
我们可以通过一个安全游戏来形式化定义:

- 挑战者运行
G()生成(PK, SK),并随机选取x ∈ X。 - 挑战者计算
y = F_PK(x),并将(PK, y)发送给攻击者。 - 攻击者输出一个猜测值
x‘。
如果对于所有高效的攻击者,其成功输出 x‘ = x 的概率都是可忽略的,那么这个陷门函数就是安全的。
从陷门函数构建公钥加密
理解了陷门函数后,我们就可以用它来构建公钥加密系统。除了陷门函数,我们还需要另外两个工具:
- 一个安全的对称加密方案:要求能抵抗主动攻击,即提供认证加密。
- 一个哈希函数 H:用于将陷门函数的输入域
X映射到对称加密的密钥空间K,即H: X → K。
以下是构建公钥加密系统的具体步骤:
密钥生成
公钥加密的密钥生成与陷门函数的密钥生成完全相同:
(PK, SK) ← G(),其中 PK 是公钥,SK 是私钥。

加密算法
加密算法输入公钥 PK 和明文消息 M,步骤如下:
- 随机选取
x ← X。 - 计算
y = F_PK(x)。 - 计算对称密钥
k = H(x)。 - 使用对称密钥加密明文:
c ← E_sym(k, M)。 - 输出密文
CT = (y, c)。
注意:陷门函数仅作用于随机值 x,而消息 M 本身是用由 x 派生的对称密钥加密的。
解密算法
解密算法输入私钥 SK 和密文 CT = (y, c),步骤如下:
- 使用陷门逆函数恢复
x:x = F⁻¹_SK(y)。 - 重新计算对称密钥:
k = H(x)。 - 使用对称密钥解密密文:
M = D_sym(k, c)。 - 输出明文
M。
安全性定理
如果满足以下三个条件,那么上述构造的公钥加密系统是CCA安全的:
- 陷门函数是安全的(即
F_PK是单向函数)。 - 对称加密方案提供认证加密。
- 哈希函数
H被建模为随机预言机(在实践中,可使用如SHA-256这样的强密码学哈希函数来近似)。
这个构造已被国际标准化组织(ISO)采纳为标准,因此可称为ISO加密方案。
一个错误的构造方法
在结束本节前,必须警告一个常见但完全错误的构造方法:直接使用陷门函数加密消息。

错误方法如下:
- 加密:
c = F_PK(M) - 解密:
M = F⁻¹_SK(c)
虽然从功能上看,解密确实是加密的逆过程,但这个方案是完全不安全的。最明显的原因是它是确定性的加密(没有引入随机性),因此连最基本的语义安全都无法满足。此外,当使用具体的陷门函数(如RSA)实例化时,会存在多种攻击。绝对不要使用这种构造。
总结
本节课中我们一起学习了:
- 陷门函数的定义及其作为单向函数的安全性要求。
- 如何结合陷门函数、认证对称加密和哈希函数,构造出CCA安全的公钥加密系统(ISO标准)。
- 一个必须避免的错误构造:直接使用陷门函数加密消息,因为它是确定性的且存在严重安全漏洞。


现在我们已经知道如何用陷门函数构建加密,下一个问题自然是:如何构造陷门函数本身?我们将在下一节探讨这个问题。
058:RSA陷门置换 🔐

在本节课中,我们将学习如何构建一个经典的陷门置换——RSA。我们将从回顾陷门置换的定义开始,然后介绍必要的算术背景知识,接着详细描述RSA的构造、工作原理及其安全性假设。最后,我们将探讨如何正确地将RSA用于公钥加密,并警示一种常见的错误用法。
回顾陷门置换

上一节我们学习了如何从陷门函数构建公钥加密。本节中,我们来看看一个经典的陷门置换——RSA。
首先,让我们快速回顾一下什么是陷门置换。陷门置换由三个算法组成:
- 密钥生成算法:输出一个公钥
PK和一个私钥SK。 - 函数
F:由公钥PK定义,将一个集合X映射到其自身(因此称为“置换”)。 - 反函数
F^{-1}:由私钥SK定义,用于计算函数F的逆。
我们称一个陷门置换是安全的,如果由公钥定义的函数是一个单向函数。这意味着在不知道陷门(私钥)的情况下,正向计算函数 F 很容易,但逆向计算(求逆)则非常困难。
必要的算术背景

在深入RSA之前,我们需要回顾一些必要的算术知识,特别是关于模合数的运算。
设模数 n 为两个大素数 p 和 q 的乘积,即 n = p * q。我们记 Z_n 为从 0 到 n-1 的整数集合,可以在其中进行模 n 的加法和乘法。
我们记 Z_n^* 为 Z_n 中所有可逆元素(即与 n 互素的元素)的集合。Z_n^* 中元素的数量由欧拉函数 φ(n) 表示。当 n 是两个不同素数的乘积时,有:
φ(n) = (p-1) * (q-1) = n - p - q + 1
由于 p 和 q 都大约为 √n 的数量级,因此 φ(n) 非常接近 n。这意味着在 Z_n 中随机选取一个元素,它极大概率属于 Z_n^*(即可逆)。
最后,我们需要欧拉定理:对于任意 x ∈ Z_n^*,有:
x^{φ(n)} ≡ 1 (mod n)
这个等式对于理解RSA的工作原理至关重要。
RSA陷门置换的构造
现在,我们准备描述RSA陷门置换。它由密钥生成、函数计算和函数求逆三部分组成。
密钥生成 (G)

以下是密钥生成的步骤:
- 生成两个大素数
p和q(例如,每个约1000比特)。 - 计算RSA模数
n = p * q。 - 选取两个指数
e和d,使得它们在模φ(n)下互为逆元,即:
e * d ≡ 1 (mod φ(n))
这意味着e和d必须与φ(n)互素。 - 公钥
PK是(n, e),私钥SK是(n, d)。e常被称为加密指数,d被称为解密指数。
函数计算 (F)
RSA函数定义在 Z_n^* 上。给定输入 x ∈ Z_n^*,函数计算为:
F(x) = x^e mod n
这非常简单,就是对输入 x 进行模 n 的 e 次幂运算。
函数求逆 (F^{-1})
给定输出 y ∈ Z_n^*,使用私钥进行求逆:
F^{-1}(y) = y^d mod n
即对 y 进行模 n 的 d 次幂运算。
正确性验证
为什么 y^d mod n 能恢复出 x 呢?让我们验证一下:
假设 y = x^e mod n,那么:
y^d mod n = (x^e)^d mod n = x^{e*d} mod n
根据密钥生成条件,存在某个整数 k,使得 e*d = k*φ(n) + 1。代入上式:
x^{e*d} mod n = x^{k*φ(n) + 1} mod n = (x^{φ(n)})^k * x mod n
根据欧拉定理,x^{φ(n)} ≡ 1 (mod n),所以 (x^{φ(n)})^k ≡ 1 (mod n)。因此:
y^d mod n ≡ 1 * x ≡ x (mod n)
这就证明了求逆操作的正确性。
RSA的安全性假设
RSA函数的安全性基于RSA假设。该假设声称:对于所有高效的算法 A,在给定公钥 (n, e) 和一个随机值 y ∈ Z_n^* 的情况下,算法 A 能够计算出使得 x^e ≡ y (mod n) 成立的 x 的概率是可忽略的。
换句话说,在只知道公钥而不知道私钥 d 的情况下,RSA函数是一个单向置换。正是由于私钥 d 提供了“陷门”,使得知道它的人可以轻松求逆,因此RSA构成了一个安全的陷门置换。
构建公钥加密系统

既然我们有了一个安全的陷门置换(RSA),就可以将其代入上一节介绍的ISO标准构造中,从而得到一个实用的公钥加密系统。
回顾一下,该构造需要一个提供认证加密的对称加密方案 (E, D),以及一个将陷门置换输出映射到对称密钥的哈希函数 H。
对于RSA,具体构造如下:
- 密钥生成:运行RSA密钥生成算法
G,得到公钥PK=(n, e)和私钥SK=(n, d)。 - 加密 (E):
- 随机选择
x ∈ Z_n^*。 - 计算
y = RSA(x) = x^e mod n。 - 通过哈希函数导出对称密钥:
k = H(x)。 - 输出密文:
(y, E_k(m)),其中E_k(m)是使用对称密钥k加密消息m的结果。
- 随机选择
- 解密 (D):
- 使用私钥恢复
x:x = y^d mod n。 - 重新导出对称密钥:
k = H(x)。 - 使用对称密钥解密:
m = D_k(c),其中c是接收到的对称加密密文。
- 使用私钥恢复
根据上一节的定理,如果RSA陷门置换是安全的,对称加密方案提供认证加密,并且哈希函数 H 被建模为随机预言机,那么这个公钥加密系统是选择密文攻击安全(CCA)的。

警告:教科书式RSA加密的错误用法
获得一个可用的公钥加密系统后,我们必须警惕RSA的一种错误用法。这通常被称为“教科书式RSA”加密。
错误做法是直接使用RSA函数加密消息 m:
- 加密:
c = m^e mod n - 解密:
m = c^d mod n
这是不安全的,绝不能在实际中使用! RSA本身只是一个陷门置换,并非一个完整的加密方案。直接加密会导致多种攻击,因为它不具备语义安全性等加密所需属性。
一个具体攻击示例
假设在一个SSL/TLS场景中,客户端需要生成一个64位的预主密钥 k 并发送给服务器。如果错误地使用教科书式RSA加密,即发送 c = k^e mod n。

攻击者可以实施“中间相遇攻击”:
- 假设
k可以分解为两个约34位的数之积,即k = k1 * k2。这种情况发生的概率约为20%。 - 那么密文满足:
c ≡ (k1 * k2)^e ≡ k1^e * k2^e (mod n)。 - 变形为:
c / k1^e ≡ k2^e (mod n)。 - 攻击者预先计算所有可能的
c / k1^e mod n(约2^34个值)并存入表。 - 然后计算所有可能的
k2^e mod n(约2^34个值),并检查是否存在于上述表中。 - 一旦找到匹配,就得到了
k1和k2,进而恢复出k = k1 * k2。
这个攻击的复杂度约为 2^40 量级,远低于暴力搜索 2^64 的复杂度。这清楚地展示了直接使用RSA加密的脆弱性。
核心要点:永远不要直接使用RSA函数加密数据。必须将其嵌入一个像ISO标准那样的安全加密框架中。
总结
本节课中,我们一起学习了经典的RSA陷门置换。我们从定义回顾和算术背景开始,详细描述了RSA的密钥生成、加密(幂运算)和解密(幂运算)过程,并验证了其正确性。我们了解到RSA的安全性基于一个计算性假设。更重要的是,我们学习了如何正确地将RSA陷门置换与对称加密和哈希函数结合,构建一个安全的公钥加密系统。最后,我们通过一个攻击实例,强调了绝不能直接使用“教科书式RSA”进行加密,而必须采用经过安全证明的构造方案。
059:RSA 实践与PKCS标准 🛡️

在本节中,我们将学习RSA在实际应用中的使用方法,特别是被称为公钥密码学标准一号(PKCS#1)的规范。我们将了解为什么不能直接使用“教科书式RSA”加密,以及如何通过预处理消息来确保安全。我们还将探讨一个著名的攻击案例,并学习更安全的替代方案。
为什么不能直接使用教科书式RSA? ⚠️

我们已经多次强调,永远不应该直接使用所谓的“教科书式RSA”来加密消息,因为这是不安全的。在实际应用RSA函数之前,必须对消息进行一些处理。
在之前的章节中,我们看到了ISO标准,其做法是:生成一个随机数 X,用RSA加密 X,然后从这个 X 派生出对称加密密钥。
然而,这并不是RSA在实际中的典型用法。
RSA在实际中如何工作? 🔧
在实际应用中,流程略有不同。通常,系统会先生成一个对称加密密钥(例如一个128位的AES密钥),然后要求RSA去加密这个给定的对称密钥,而不是将生成对称密钥作为RSA加密过程的一部分。
因此,RSA系统的输入是一个需要加密的对称密钥。为了用RSA加密这个较短的密钥(如128位),我们首先需要将其扩展为完整的模数大小(例如2048位),然后再应用RSA函数。

这就引出了两个核心问题:
- 应该如何进行这个预处理(即扩展)?
- 如何论证最终的系统是安全的?
PKCS#1 v1.5:一种广泛部署的标准 📜
一种古老但至今仍被广泛部署的方法是 PKCS#1 版本 1.5。PKCS代表公钥密码学标准。这里我们关注其模式2(用于加密),模式1用于签名。
以下是PKCS#1 v1.5的工作方式:
- 放置消息:将你的消息(例如128位AES密钥)放在要创建值的最低有效位。
- 添加固定标识:紧接着消息,添加16位全为1的字节(即
0xFF)。 - 添加随机填充:然后,添加一个不包含
0xFF字节的随机填充串。这部分大约有1900个随机比特。 - 添加模式标识:最后,在最高有效位放置数字
0x02,表示此明文是使用PKCS#1模式2编码的。
最终,这个完整的2048位字符串会被送入RSA函数:C = (这个值)^E mod N,结果就是PKCS#1密文。
解密时,接收方会:
- 对密文应用RSA逆函数,恢复出这个数据块。
- 检查最高有效位是否为
0x02。如果是,则说明是PKCS#1格式。 - 移除
0x02和其后的随机填充,直到遇到0xFF。 0xFF之后的部分就是原始消息。

Bleichenbacher 攻击:针对PKCS#1 v1.5的漏洞 🕵️♂️
有趣的是,PKCS#1 v1.5设计于80年代末,当时并没有严格的安全证明。1998年,Daniel Bleichenbacher 提出了一种非常巧妙的攻击,展示了如何攻击使用PKCS#1(例如在HTTPS中)的系统。
攻击原理如下:
假设攻击者截获了一个PKCS#1密文 C。他可以与一个Web服务器(拥有私钥)进行交互。服务器解密后,会检查解密结果的最高有效位是否为 0x02:
- 如果是
0x02,则继续正常协议。 - 如果不是
0x02,则返回一个错误信息。
这实际上为攻击者提供了一个“预言机”:攻击者可以提交任何密文 C‘ 给服务器,服务器会告诉他 C‘ 解密后的明文是否以 0x02 开头。
攻击者如何利用这一点?

攻击者拥有他想解密的密文 C。他会进行如下操作:
- 选择一个随机值
r。 - 构造一个新的密文
C‘ = (r^E * C) mod N。根据RSA的乘法同态性,这相当于使原始明文m乘以了r。 - 将
C‘发送给服务器,根据服务器的响应(是/否),他知道(r * m) mod N是否以0x02开头。
通过精心选择大量的 r 值(大约需要一百万次查询),并询问服务器对应的 C‘ 解密后是否以 0x02 开头,攻击者可以逐步缩小 m 的可能范围,最终完全恢复出原始明文 m。
一个简化示例(Baby Bleichenbacher):
为了理解核心思想,假设服务器只检查最高位是否为1(而不是 0x02),并且模数 N 是2的幂(仅为简化示例)。
- 查询
C本身,得知m的最高位。 - 查询
(2^E * C) mod N,这相当于查询(2*m) mod N的最高位,从而得知m的第二高位。 - 查询
(4^E * C) mod N,得知m的第三高位。 - …… 重复此过程,只需几千次查询即可恢复整个
m。
Bleichenbacher 攻击需要约一百万次查询,因为他检测的是更具体的 0x02 模式,但原理相同。这个攻击表明,即使只泄露关于RSA解密结果最高位的极少信息,也足以导致完全解密。
如何防御Bleichenbacher攻击? 🛑
SSL/TLS社区希望以最小的代码改动来防御此攻击。RFC中提出的方案是:
如果解密后得到的明文不是有效的PKCS#1格式(即不以 0x02 开头),服务器不会返回错误,而是生成一个随机的伪“主密钥”,并继续执行协议。
当然,由于客户端和服务器使用了不同的密钥,会话最终会失败。但关键在于,攻击者无法再区分“无效密文”和“有效但解密后格式错误”的密文,从而切断了信息泄露的渠道。
这个方案作为一个微小的代码补丁被广泛部署。然而,这引发了更深层的问题:我们是否应该彻底修改PKCS#1,以获得可证明的选择密文安全性(CCA)?
OAEP:更优的非对称加密填充 ✅
这引出了另一种使用RSA进行加密的方法:最优非对称加密填充(OAEP)。PKCS#1 版本 2.0 已加入对OAEP的支持。
OAEP 由 Bellare 和 Rogaway 于1994年提出,其工作原理如下:
- 准备消息:取要加密的消息
M(如AES密钥),并添加一个固定的填充(如0x01后跟一串0x00),使其达到一定长度。 - 选择随机数:选择一个随机值
R。 - 使用哈希函数进行变换:
- 将
R通过哈希函数H处理,结果与(填充后的)消息M进行异或,得到第一部分数据。 - 将上一步的结果通过另一个哈希函数
G处理,再与R异或,得到第二部分数据。
- 将
- 连接并加密:将这两部分数据连接起来,形成一个长度与RSA模数匹配的字符串,然后对其应用RSA加密函数。
解密过程则是逆向操作,并且必须验证填充的正确性。如果填充无效,则拒绝该密文。
为什么叫“最优”?
因为其密文长度就是单个RSA输出的长度,没有附加任何额外数据。相比之下,像ISO标准等方法,即使加密很短的消息,也会产生“一个RSA密文 + 一个对称密文”的输出。

安全性:在随机预言机模型下(假设哈希函数 H 和 G 是理想的),如果RSA函数是一个安全的陷门置换,那么OAEP能提供选择密文安全性。


OAEP的变体与实现注意事项 ⚠️
还存在一些OAEP的变体,如 OAEP+ 和 SAEP+,它们旨在提供更一般的安全性证明(不依赖于RSA的特定代数性质),但在实践中,标准化的OAEP使用更广泛。
实现OAEP时的关键陷阱:
即使OAEP的数学原理是安全的,错误的实现也会引入漏洞,特别是时序攻击。


考虑一个OAEP解密程序:
- 对密文应用RSA逆函数。
- 检查1:结果是否在有效范围内(例如,小于 2^{2047})?如果不是,则报错退出。
- 检查2:解包后,填充是否正确?如果不是,则报错退出。

如果这两个检查的失败路径执行时间不同,攻击者就可能通过精确测量响应时间,来判断错误是由于“范围过大”还是“填充错误”引起的。这种微小的信息泄露,结合类似Bleichenbacher的攻击技术,可能再次导致完全解密。
教训:不要自己实现密码学,尤其是像RSA-OAEP这样复杂的算法。应使用经过严格测试的标准库(如OpenSSL),它们会确保解密过程的运行时间是恒定的,不受错误类型影响。
本节总结 📝
在本节课中,我们一起学习了:
- RSA的实际使用:不能直接加密消息,必须使用填充方案。
- PKCS#1 v1.5:一种历史悠久、广泛部署但存在理论缺陷的填充标准。
- Bleichenbacher攻击:利用PKCS#1 v1.5解密过程中的错误信息反馈,可以逐步解密任意密文。
- 防御措施:通过不返回具体错误信息来掩盖解密状态。
- OAEP:一种更安全、可证明安全的RSA填充方案,是当前的标准推荐。
- 实现安全:即使算法安全,错误的实现(如引入时序侧信道)也会导致系统被攻破,因此务必使用权威的标准库。

下一节,我们将继续探讨RSA本身的安全性。
060:RSA是单向函数吗


在本节课中,我们将探讨RSA加密算法是否是一个真正的单向函数。我们将分析在没有陷门信息(私钥)的情况下,逆向计算RSA函数的难度,并讨论一些与RSA性能优化相关的安全陷阱。
RSA逆向计算的难度
上一节我们介绍了RSA加密的基本原理。本节中我们来看看,对于一个攻击者而言,在没有私钥的情况下,逆向计算RSA函数有多困难。

攻击者拥有公钥 (n, e),并看到了密文 c ≡ x^e (mod n)。他的目标是恢复明文 x。因此,核心问题是:给定 x^e mod n,计算 x 有多难?这等价于问,计算模合数 n 下的 e 次方根有多难?
如果这个问题被证明是困难的,那么RSA就是一个单向函数。如果它很容易(当然我们不相信它容易),那么RSA就被破解了。
目前,解决这个问题的最佳算法要求我们首先对模数 n 进行因式分解。一旦分解了 n,我们上周已经看到,计算模 p 和模 q 下的 e 次方根是容易的。然后,利用中国剩余定理,可以很容易地将这两个根组合起来,恢复出模 n 下的 e 次方根。
因此,一旦能够分解模数 n,计算模 n 下的 e 次方根就变得容易。但就我们所知,分解模数是一个非常、非常困难的问题。
逆向计算是否必须分解模数?
一个自然的问题是:为了计算模 n 下的 e 次方根,是否必须分解模数 n?就我们所知,计算模 n 下 e 次方根的最佳算法需要分解 n。但也许存在某种捷径,可以在不分解模数的情况下计算 e 次方根?
为了证明这是不可能的,我们需要展示一个规约。也就是说,我们必须证明,如果我给你一个计算模 n 下 e 次方根的高效算法,那么这个高效算法可以被转化为一个因式分解算法。这被称为规约:给定一个计算模 n 下 e 次方根的算法,我们得到一个因式分解算法。这将表明,计算模 n 下 e 次方根的速度不会比分解模数更快。
如果我们有这样的结果,它将表明破解RSA实际上和因式分解一样困难。但不幸的是,目前这尚未被证明。事实上,这是公钥密码学中最古老的问题之一。
让我给你一个具体的例子。假设我给你一个能计算模 n 下立方根的算法。对于 Z_n* 中的任何 x,该算法都能计算出 x 模 n 的立方根。我的问题是:你能证明使用这样的算法可以分解模数 n 吗?即使这个问题也未被完全解决。
已知的是,对于 e = 2(即计算模 n 下的平方根),确实意味着可以分解模数。因此,计算平方根实际上和分解模数一样困难。然而,回想RSA的定义,它要求 e * d ≡ 1 (mod φ(n))。这意味着 e 必须与 φ(n) 互质。如果 e 模 φ(n) 可逆,那么 e 必须与 φ(n) 互质。
但请记住,φ(n) = (p-1)(q-1)。由于 p 和 q 都是大素数,(p-1)(q-1) 总是偶数。因此,2 和 φ(n) 的最大公约数 GCD(2, φ(n)) = 2(因为 φ(n) 是偶数)。这意味着公钥指数 2 与 φ(n) 不互质,因此 e = 2 不能用作RSA指数。
所以,实际上最小的合法RSA指数是 e = 3。但对于 e = 3,计算立方根是否和因式分解一样困难,这是一个开放性问题。思考这个问题很有趣,我鼓励你思考一下:如果我给你一个计算模 n 下立方根的高效算法,你能用那个算法来分解模数 n 吗?

有一些微弱的证据表明,这样的规约可能不存在。但证据非常弱。基本上,如果你的规约是某种特定形式(例如“代数”规约),那么规约本身就会意味着存在一个因式分解算法。但这只是非常弱的证据,因为谁规定规约必须是代数的呢?也许存在我们尚未考虑的其他类型的规约。
RSA的性能优化与安全陷阱
如前所述,就我们所知,RSA是一个单向函数,破解RSA(计算 e 次方根)实际上需要分解模数。我们普遍相信这是真的,这也是目前的技术现状。
现在,有很多工作致力于提高RSA的性能,无论是RSA加密还是解密。事实证明,在这个方向上有一些错误的开端。我想展示一个绝佳的例子作为警告,这基本上是一个关于如何不提高RSA性能的例子。
你可能会想,如果我想加快RSA解密速度(解密是通过将密文提升到 d 次方来完成的,而求幂算法的时间复杂度与 d 的大小 log(d) 成线性关系),为什么不直接使用一个小的 d 呢?比如一个大约 2^128 数量级的解密指数 d?
这显然足够大,使得对 d 进行穷举搜索不可行。但通常解密指数 d 和模数 n 一样大,比如2000比特。通过使用一个只有128比特的 d,我基本上将RSA解密速度提高了约20倍(从2000比特降到约128比特)。
事实证明,这是一个糟糕透顶的主意。Michael Wiener提出的一种攻击表明,一旦私钥指数 d 小于模数 n 的四次方根(如果模数约为2048比特,这意味着如果 d 小于 2^512),那么RSA就完全不安全了,而且是以最糟糕的方式不安全:仅仅给定公钥 (n, e),你就可以非常快速地恢复出私钥 d。
有些人可能会说,这个攻击对高达512比特的 d 有效,那我们为什么不把模数 n 设为,比如530比特呢?这样攻击就不适用了,而我们仍然可以通过将指数从2000比特缩小到530比特来将RSA解密速度提高约4倍。
然而,事实证明即使这样也不安全。实际上,Wiener攻击有一个更复杂的扩展,表明如果 d 小于 n^0.292,那么RSA也不安全。并且猜想认为这个结论一直成立到 n^0.5。所以,即使 d 像 n^0.4999 一样大,RSA应该仍然不安全,尽管这是一个开放性问题。
这里的教训是:不应该为了改善RSA性能而对 d 施加任何结构限制。事实上,现在有一系列类似的结果表明,任何试图通过这类技巧来提高RSA性能的尝试都可能以灾难告终。这不是提高RSA性能的正确方法。
Wiener攻击细节(可选)
最初我不打算涵盖Wiener攻击的细节,但考虑到课堂上的讨论,我认为你们中的一些人会喜欢看到细节。它只涉及操作一些不等式。如果你对此感到不适应,可以跳过这部分,尽管我认为许多人会喜欢看到细节。

让我提醒你,在Wiener攻击中,我们被给定模数 n 和RSA公钥指数 e,我们的目标是恢复私钥指数 d。我们只知道 d 基本上小于 n 的四次方根。事实上,我将假设 d < (n^(1/4))/3。这个3并不重要,但主导项是 d < n^(1/4)。让我们看看如何做到。
首先,回想一下,因为 e 和 d 是RSA的公钥和私钥指数,我们知道 e * d ≡ 1 (mod φ(n))。这意味着存在某个整数 k,使得 e * d = k * φ(n) + 1。基本上,这就是 e * d 模 φ(n) 余1的含义。
现在,让我们稍微审视一下这个方程。事实上,这个方程是攻击中的关键方程。
我们要做的首先是两边同时除以 d * φ(n)。实际上,我将把这个项移到左边。除以 d * φ(n) 后,我得到:
e/φ(n) - k/d = 1/(d * φ(n))
好的,我所做的只是除以 d * φ(n),并把 k * φ(n) 项移到了左边。现在,为了完整性,我将在这里加上绝对值符号,这在一分钟后会变得有用,当然它们不会改变等式的成立。
现在,φ(n) 当然几乎等于 n。正如我们之前所说,φ(n) 非常接近 n。对于这个分数,我只需要说它小于 1/√n。实际上它比 1/√n 小得多,大约在 1/n 的数量级甚至更小,但为了我们的目的,我们只需要这个分数小于 1/√n。
现在,让我们稍微审视一下左边的这个分数。你意识到我们实际上并不知道这个分数。我们知道 e,但我们不知道 φ(n)。因此,我们不知道 e/φ(n)。但我们有一个很好的近似值:我们知道 φ(n) 非常接近 n,因此 e/φ(n) 非常接近 e/n。所以我们有一个很好的近似值来逼近左边的分数,即 e/n。
我们真正想要的是右边的分数,因为一旦我们得到右边的分数,基本上就涉及到了 d,然后我们就能恢复 d。让我们看看,如果我们用 e/n 替换 e/φ(n),我们会得到什么样的误差。
为了分析这一点,我们首先要提醒自己,φ(n) 基本上是 n - p - q + 1,这意味着 n - φ(n) < p + q(实际上我应该精确一点,应该是 p + q + 1,但加1不影响什么,为简单起见我就忽略它了)。所以 n - φ(n) < p + q。
p 和 q 都大约是 n 长度的一半,所以它们都在 √n 的数量级。基本上,p + q 小于 3√n。好的,我们稍后会用到这个不等式。
现在我们将开始利用我们知道 d 很小这一事实。如果我们看这个不等式 d < (n^(1/4))/3,很容易看出,如果我对两边平方并稍微操作一下,可以直接推导出以下关系:
1/(2d^2) - 1/√n > 3/√n
正如我所说,这基本上是通过两边平方,然后取倒数,然后我想是一边乘以二分之一推导出来的。好的,你可以很容易地推导出这个关系,我们马上会需要它。
现在,我们想做的是界定 e/n 和 k/d 之间的差。根据三角不等式,我们知道:
|e/n - k/d| ≤ |e/n - e/φ(n)| + |e/φ(n) - k/d|
这只是三角不等式的性质。这里的绝对值,我们已经知道如何界定。如果你考虑一下,它基本上是我们已经推导出的界限。所以我们知道这个绝对值小于 1/√n。
那么,这个绝对值 |e/n - e/φ(n)| 呢?让我们做通分看看得到什么。公分母是 n * φ(n)。分子是 e * |φ(n) - n|。根据上面的表达式,我们知道它小于 3√n。所以分子将小于 e * 3√n。
现在我知道 e < φ(n),所以我知道 e/φ(n) < 1。换句话说,如果我擦掉 e 和 φ(n),我只让分数变大了。所以最初的绝对值不会大于 (3√n)/n,也就是 3/√n。
好的,但我们知道 3/√n 小于 1/(2d^2) - 1/√n。好的,这就是我们推导的终点。
所以现在我们知道第一个绝对值小于 1/(2d^2) - 1/√n,第二个绝对值小于 1/√n,因此它们的和小于 1/(2d^2)。
这就是我希望你仔细审视的表达式。现在,让我们稍微审视一下这里。首先,和之前一样,我们知道 e/n 的值,我们想知道的是 k/d 的值。但我们知道这两个分数的差非常小,小于 1/(2d^2)。
事实证明,k/d 如此好地近似 e/n(以至于它们的差小于 k/d 分母平方的倒数)的情况很少发生。实际上,这种情况不会经常发生。事实证明,形式为 k/d 的分数中,能如此好地近似另一个分数(差小于 1/(2d^2))的分数非常少。实际上,这样的分数数量最多是 n 的对数级别。
现在有一个连分数算法,它是一个非常著名的算法,基本上它能从分数 e/n 中恢复出最多 log n 个可能的 k/d 候选值。所以我们只需一个一个地尝试它们,直到找到正确的 k/d,然后我们就完成了。我们完成了,因为我们知道 e * d ≡ 1 (mod k),因此 d 与 k 互质。
所以,如果我们只是将 k/d 表示为一个有理分数(分子除以分母),那么分母必须是 d。因此,我们刚刚尝试了所有近似 e/n 非常好(差小于 1/(2d^2))的 log n 个分数。然后我们查看所有这些分数的分母,其中一个分母必须是 d,然后我们就完成了,我们刚刚恢复了私钥。
这是一个相当巧妙的攻击,它基本上展示了如果私钥指数很小(小于 n 的四次方根),那么我们可以完全且相当容易地恢复 d。
总结
本节课中我们一起学习了RSA作为单向函数的强度问题。我们了解到,目前破解RSA(计算模 n 下的 e 次方根)的最佳已知方法需要分解模数 n,而分解大整数被认为是困难的。然而,严格证明“破解RSA等价于因式分解”对于常用指数(如 e=3)仍然是一个开放性问题。

我们还深入探讨了一个重要的安全教训:试图通过使用小的私钥指数 d 来优化RSA解密性能是极其危险的。Wiener攻击及其扩展表明,如果 d 小于模数 n 的某个界限(如 n^0.292),攻击者可以直接从公钥中恢复出私钥。这警示我们,在密码学中,性能优化绝不能以牺牲核心安全属性为代价。
061:RSA实践应用

在本节课中,我们将探讨RSA加密算法在实际应用中的关键要点,包括性能优化、实现攻击以及密钥生成的安全隐患。我们将学习如何安全高效地使用RSA。

加速RSA加密
上一节我们介绍了RSA的基本原理,本节中我们来看看如何优化其加密速度。为了加速RSA加密过程,使用一个较小的加密指数 e 是完全可行的。

以下是关于选择加密指数 e 的要点:
- 最小可用值:最小的可用加密指数是
e = 3。e = 1不安全,因为其逆运算过于简单;e = 2无效,因为它与偶数φ(n)不互质。 - 使用条件:当使用
e = 3时,需要确保素数p和q满足p ≡ 2 mod 3且q ≡ 2 mod 3,这样(p-1)(q-1)才不会被3整除。 - 推荐值:实际应用中,推荐使用
e = 2^16 + 1,即 65537。计算x^65537 mod n仅需约17次乘法运算(通过16次平方和1次乘法),这比使用一个随机的、需要约2000次乘法运算的大指数要快得多。
RSA的不对称性
上述优化引出了RSA的一个关键特性:不对称性。加密过程可以非常快,但解密过程则要慢得多。
- 加密速度:使用
e=65537时,加密仅需约17次乘法运算。 - 解密速度:解密使用私钥指数
d,其大小与模数n相当,因此需要约2000次乘法运算。 - 速度对比:RSA中加密与解密的速度比大约在10到30倍之间。这种加密远快于解密的特性是RSA所特有的,其他公钥系统(如下一模块将介绍的ElGamal加密)的加密和解密耗时通常相近。

密钥长度与实现攻击
我们之前讨论过RSA的密钥长度。为了匹配128位AES密钥的安全强度,应使用约3000位的RSA模数,但实践中普遍使用2048位。
接下来,我们关注针对RSA具体实现的攻击。这些攻击表明,即使数学原理正确,糟糕的实现也会导致系统完全不安全。
以下是几种著名的实现攻击:
- 时序攻击:通过精确测量RSA解密操作所花费的时间,攻击者可能推断出私钥
d。因此,实现必须确保解密时间与输入参数无关。 - 功耗分析攻击:通过测量智能卡等设备在执行RSA解密时的功耗波动,攻击者可以直接读取私钥
d的各个比特。 - 故障攻击:RSA对解密过程中的错误异常敏感。在特定情况下,单次计算错误就足以完全泄露私钥。
故障攻击实例分析
鉴于故障攻击的严重性,我们深入分析一个具体案例,即针对使用中国剩余定理加速的RSA解密的攻击。

RSA解密常通过CRT加速:分别计算 c^d mod p 和 c^d mod q,然后组合得到 c^d mod n。假设在计算 c^d mod q 时发生了一个错误,得到了错误结果 x_q_hat,而计算 c^d mod p 是正确的。
- 组合后的输出
x‘满足:x’ ≡ c^d (mod p)但x‘ ≠ c^d (mod q)。 - 将两边取
e次幂(因为d和e互为逆元):(x‘)^e ≡ c (mod p)但(x‘)^e ≠ c (mod q)。 - 因此,差值
(x‘)^e - c能被p整除,但不能被q整除。 - 计算
gcd((x‘)^e - c, n)。由于n = p * q,且p能整除该差值而q不能,这个最大公约数结果就是p。 - 得到
p后,即可分解n,计算出φ(n),进而从公钥(n, e)推导出私钥d。
防御措施:因此,在执行RSA解密(尤其是使用CRT加速时)后,验证结果是一个好习惯。可以通过计算 (解密结果)^e mod n 并检查是否等于原始密文 c 来实现。虽然这会引入约10%的性能开销,但至关重要。
核心建议:永远不要自己实现RSA。务必使用经过严格测试和防护的标准密码学库。

密钥生成与熵的重要性
最后,我们讨论一个近期的发现:不良的随机性(熵)会导致RSA密钥生成出现灾难性问题。
以OpenSSL的旧实现为例:
- 系统启动时,熵池可能不足。
- 生成第一个素数
p时,随机源熵值低,导致p的可能取值集合很小。 - 生成
p需要一些时间,期间系统积累了更多熵。 - 生成第二个素数
q时,熵池已较丰富,因此q通常是唯一的。
问题在于,大量设备在启动后立即生成RSA密钥,可能会使用相同的“低熵素数” p,但配以不同的 q。如果攻击者收集网络上的大量RSA公钥 n1, n2, ...,并通过计算 gcd(ni, nj) 来寻找公约数,就有可能分解那些共享了相同素数 p 的模数。在实际扫描中,研究者曾因此成功分解了约0.4%的SSL公钥。
教训:生成任何密码学密钥(无论是RSA、ElGamal还是对称密钥)时,确保随机数生成器已获得足够的、良好的熵源是至关重要的。避免在系统刚启动时立即生成密钥。

本节课中我们一起学习了RSA在实际应用中的多个方面。我们了解了如何通过选择小加密指数来优化加密速度,认识了RSA加解密的不对称性。更重要的是,我们探讨了时序攻击、功耗分析、故障攻击等多种针对实现的攻击手段,并通过实例深入分析了故障攻击的原理。最后,我们强调了在密钥生成过程中拥有高熵随机源的重要性。所有这些都指向一个核心原则:在实践中,应始终依赖久经考验的标准密码库来实现RSA,而非自行构建。
062:ElGamal公钥系统 🔐

在本节课中,我们将学习如何基于Diffie-Hellman协议构建公钥加密系统,即ElGamal加密方案。我们将了解其工作原理、应用场景以及性能特点。

在上一讲中,我们探讨了基于RSA或陷门函数构建的公钥加密系统。本节中,我们将关注基于Diffie-Hellman协议构建的公钥加密方案。
首先回顾一下,一个公钥加密系统由三个算法组成:
- 密钥生成算法:生成一个公钥和一个私钥。
- 加密算法:使用公钥对消息进行加密。
- 解密算法:使用私钥对密文进行解密。
公钥加密的物理世界类比是一个带锁的盒子。任何人都可以将消息放入盒子并锁上(这对应于使用公钥加密),但只有拥有钥匙(私钥)的人才能打开盒子并取出消息。
在上一讲中,我们看到了公钥加密的多种应用,特别是在密钥交换中的应用。然而,在许多场景中,交互是不可能的,此时公钥加密被直接用于加密消息。
以下是两个非交互式应用的例子:
1. 加密文件系统
想象一下,Bob想在一个存储服务器上存储一个加密文件。以下是他的操作步骤:
- 生成一个随机的文件加密密钥
K_F。 - 使用对称加密系统,用
K_F加密文件。 - 使用Bob自己的公钥加密
K_F,并将这个加密后的“头部”和加密后的文件一起存储。

这样,Bob以后可以用自己的私钥解密头部,得到 K_F,再解密文件。如果Bob想让Alice也能访问这个文件,他只需在文件头部额外添加一个用Alice公钥加密的 K_F。这样,无需与Alice交互,Bob就授权了Alice的访问权限。
2. 密钥托管
在企业环境中,公司可能需要访问员工的加密文件(例如,员工离职后)。密钥托管服务可以解决这个问题。以下是其工作方式:
- Bob在存储文件时,除了用自己的公钥加密
K_F,还会用托管服务的公钥加密K_F,并将两者都存入文件头部。 - 当公司需要访问Bob的文件时,可以联系托管服务。
- 托管服务使用自己的私钥解密其对应的头部,得到
K_F,从而解密文件。
托管服务在文件写入时是完全离线的,只有在需要时才被调用。


上一讲我们看到了基于陷门函数(如RSA)的公钥加密构造。本节我们将学习基于Diffie-Hellman协议的另一类公钥系统,即ElGamal公钥加密方案。

回顾:Diffie-Hellman协议 🔄
在介绍ElGamal系统之前,我们先简要回顾一下Diffie-Hellman协议。我们将使用有限循环群的概念进行抽象描述。
假设我们有一个有限循环群 G(例如 Z_p^* 或椭圆曲线点群),其阶为 n。我们固定该群的一个生成元 g。这意味着 g 的幂次可以生成群 G 中的所有元素,且 g^n = 1。
Diffie-Hellman协议的工作流程如下:
- Alice选择一个随机数
a,计算A = g^a并发送给Bob。 - Bob选择一个随机数
b,计算B = g^b并发送给Alice。 - 双方可以计算出共享密钥
s = g^(a*b)。- Alice计算:
s = B^a = (g^b)^a = g^(a*b) - Bob计算:
s = A^b = (g^a)^b = g^(a*b)
- Alice计算:
攻击者可以看到 A = g^a 和 B = g^b,但在类似 Z_p^* 的群中,从 g^a 和 g^b 计算 g^(a*b) 被认为是困难的(计算性Diffie-Hellman假设)。

ElGamal加密方案 🛡️
现在,我们来看看如何将Diffie-Hellman协议转化为一个公钥加密系统。这个巧妙的想法归功于Taher ElGamal。
我们仍然固定一个循环群 G 及其生成元 g。ElGamal方案可以看作是Diffie-Hellman协议在时间上的分离:
- 密钥生成:这对应于Diffie-Hellman中Alice的第一步。她选择随机私钥
a,计算公钥h = g^a。从公钥h推导私钥a是离散对数难题。 - 加密:当Bob想用Alice的公钥加密消息
m时,他执行类似Diffie-Hellman中Bob的步骤:- 选择随机数
b。 - 计算
u = g^b(这相当于他的临时公钥)。 - 计算共享密钥
v = h^b = (g^a)^b = g^(a*b)。 - 从
v派生出对称密钥k。 - 使用对称密钥
k加密消息m,得到密文c。 - 发送密文对
(u, c)给Alice。
- 选择随机数
- 解密:Alice收到
(u, c)后:- 使用她的私钥
a计算共享密钥v = u^a = (g^b)^a = g^(a*b)。 - 从
v派生出相同的对称密钥k。 - 使用
k解密密文c,恢复消息m。
- 使用她的私钥

详细算法描述 📝
为了使方案更严谨并实现选择密文安全,我们引入一个哈希函数和一个提供认证加密的对称加密方案。
系统参数
- 一个有限循环群
G,阶为n。 - 一个提供认证加密的对称加密方案
(E_s, D_s),其密钥空间为K。 - 一个哈希函数
H: G × G → K,将群元素对映射到对称密钥空间。
密钥生成算法 (Gen)
- 选择群
G的一个随机生成元g。 - 选择一个随机指数
a(私钥)。 - 计算
h = g^a。 - 输出公钥
pk = (G, g, h),私钥sk = a。
加密算法 (E)
输入:公钥 pk = (G, g, h),明文 m。
- 选择一个随机数
b。 - 计算
u = g^b。 - 计算
v = h^b。 - 计算对称密钥
k = H(u, v)。 - 计算对称密文
c = E_s(k, m)。 - 输出密文
(u, c)。

解密算法 (D)
输入:私钥 sk = a,密文 (u, c)。
- 计算
v = u^a。 - 计算对称密钥
k = H(u, v)。 - 解密明文
m = D_s(k, c)。 - 输出
m。
性能分析 ⚡
ElGamal加密的性能瓶颈在于群 G 中的模幂运算:
- 加密:需要计算两次模幂(
g^b和h^b)。 - 解密:需要计算一次模幂(
u^a)。

表面上看,加密比解密慢一倍。但有一个优化技巧:由于加密时底数 g 和 h 是固定的(来自公钥),加密方可以预先计算并存储 g 和 h 的许多幂次(例如所有2的幂次)。这样,在实际加密时,大部分耗时的平方运算已经完成,只需进行乘法累积,可以显著提升加密速度(甚至可能快于解密)。这种技术称为“固定基预计算”或“窗口指数运算”。
然而,如果加密方总是为不同的接收者加密(例如每次给不同的人发邮件),则无法利用此优化,加密速度约为解密的两倍。
安全性说明 🔒
一个自然的问题是:ElGamal系统是否安全?能否证明其在选择密文攻击下是安全的?这依赖于怎样的计算假设?
简而言之,在“决策性Diffie-Hellman假设”和哈希函数被建模为随机预言机的条件下,可以证明上述描述的ElGamal变体是IND-CCA安全的。我们将在下一节详细讨论其安全性证明。
总结 📚
本节课我们一起学习了基于Diffie-Hellman协议的ElGamal公钥加密方案。我们了解到:
- ElGamal方案可以视为将Diffie-Hellman密钥交换协议在时间上分离,从而构造出非交互式的公钥加密。
- 该方案包含三个核心算法:密钥生成、加密和解密,其中加密是随机化的。
- 我们看到了ElGamal在加密文件系统和密钥托管等非交互式场景下的应用。
- 方案性能依赖于群上的模幂运算,但可通过预计算进行优化。
- 通过引入哈希函数和认证加密,可以构建出满足现代安全标准(选择密文安全)的ElGamal变体。


ElGamal加密是密码学中的一个重要范例,展示了如何将交互式协议转化为非交互式的加密工具。
063:ElGamal加密系统的安全性分析

在本节课中,我们将要学习ElGamal公钥加密系统的安全性。我们将探讨其安全性所依赖的数学假设,并理解如何基于这些假设来证明其语义安全性和选择密文安全性。
概述
ElGamal加密系统的安全性建立在Diffie-Hellman问题的困难性之上。我们将首先回顾计算性Diffie-Hellman(CDH)假设,然后引入一个更强的假设——哈希Diffie-Hellman(HDH)假设,并展示如何利用HDH假设证明ElGamal的语义安全性。最后,我们将讨论为了证明选择密文安全性所需引入的交互式Diffie-Hellman假设。
计算性Diffie-Hellman(CDH)假设

上一节我们介绍了ElGamal加密系统的基本原理,本节中我们来看看其安全性的理论基础。首先,让我们回顾计算性Diffie-Hellman(CDH)假设。
CDH假设指出,在一个阶为N的有限循环群G中(例如Z_p*或椭圆曲线群),给定生成元g以及g^a和g^b(其中a和b是随机选择的指数),对于任何高效的算法,计算出Diffie-Hellman秘密g^(ab)的概率是可忽略的。
用公式描述,CDH假设成立的条件是:对于所有高效算法A,以下概率是可忽略的:
Pr[ A(g, g^a, g^b) = g^(ab) ]
其中,a和b是从Z_N中随机均匀选取的。
哈希Diffie-Hellman(HDH)假设
然而,CDH假设对于分析ElGamal系统的安全性并不理想。因此,我们引入一个更强的假设,称为哈希Diffie-Hellman(HDH)假设。

HDH假设不仅涉及一个群G,还引入一个哈希函数H,该函数将G中的元素对映射到某个对称加密系统的密钥空间K。HDH假设认为,以下两个分布在计算上是不可区分的:
- 真实分布:
(g, g^a, g^b, H(g^b, g^(ab))) - 随机分布:
(g, g^a, g^b, R),其中R是从密钥空间K中均匀随机选取的。
用代码描述这个假设的核心思想:
# 假设以下两个元组对于任何高效敌手都是不可区分的
tuple_real = (g, g_a, g_b, hash_func(g_b, g_ab))
tuple_random = (g, g_a, g_b, random_key)
HDH假设比CDH假设更强。如果CDH在某个群G中是容易的(即可以高效计算g^(ab)),那么敌手就能轻易计算出H(g^b, g^(ab)),从而区分上述两个分布,导致HDH假设不成立。因此,HDH成立意味着CDH也必须成立。

关于HDH假设的一个思考题
为了确保理解HDH假设,请考虑以下场景:假设我们的密钥空间是128位字符串({0,1}^128),而哈希函数H总是输出以0开头的字符串(即最高有效位总是0)。那么,HDH假设对于这对(G, H)还成立吗?
答案是否定的。因为一个真正随机的128位密钥,其最高位为0的概率是1/2。然而,来自“真实分布”的哈希值最高位总是0。因此,敌手可以通过检查最高位来以1/2的优势区分两个分布,从而打破HDH假设。这说明,即使CDH在群G中是困难的,如果选择了“坏”的哈希函数,HDH也可能不成立。
基于HDH假设证明ElGamal的语义安全性

现在,我们来看看如何利用HDH假设证明ElGamal加密系统是语义安全的。首先,快速回顾ElGamal的工作流程:
- 密钥生成:选择随机生成元
g和随机指数a。公钥是(g, g^a),私钥是a。 - 加密:为了加密消息
M,选择随机指数b,计算共享秘密(g^a)^b = g^(ab),然后通过哈希函数H导出对称密钥K = H(g^b, g^(ab))。最后,输出密文(g^b, E_K(M)),其中E是语义安全的对称加密算法。 - 解密:使用私钥
a从g^b计算共享秘密(g^b)^a = g^(ab),导出相同的密钥K,然后解密E_K(M)得到M。
证明思路如下:
- 在语义安全游戏中,敌手会获得公钥
(g, g^a)和一个挑战密文(要么是M0的加密,要么是M1的加密)。 - 挑战密文中包含
g^b和对称密文E_K(M),其中K = H(g^b, g^(ab))。 - 根据HDH假设,密钥
K = H(g^b, g^(ab))与一个完全独立、均匀随机的密钥R在计算上是不可区分的。因此,我们可以将游戏中的K替换为R,而敌手无法察觉。 - 替换后,游戏变为:敌手获得
(g, g^a, g^b, E_R(M)),其中R是随机密钥,与g^a和g^b无关。 - 在这种情况下,由于对称加密算法
E本身是语义安全的,敌手无法区分E_R(M0)和E_R(M1)。 - 通过这一系列不可区分的游戏变换,我们得出结论:敌手在原始的ElGamal语义安全游戏中也无法区分
M0和M1的加密。因此,ElGamal在HDH假设下是语义安全的。

选择密文安全性(CCA)与交互式Diffie-Hellman假设
然而,语义安全性并不足够,我们通常希望加密系统能够抵抗更强的选择密文攻击(CCA)。那么,ElGamal是否具有CCA安全性呢?
事实证明,要证明ElGamal的CCA安全性,CDH或HDH假设是不够的。我们需要一个更强的假设,称为交互式Diffie-Hellman(Interactive DH)假设。
在交互式DH游戏中:
- 挑战者生成
g、g^a和g^b,并将(g, g^a, g^b)发送给敌手。 - 敌手的目标仍然是输出Diffie-Hellman秘密
g^(ab)。 - 关键增强:敌手被允许多次向挑战者提交查询。每次查询可以提交一对群元素
(U, V),挑战者会回答1(如果U^a = V)或0(否则)。
这个假设更强,因为敌手拥有了“解密预言机”的某种能力(通过查询来测试某个值是否等于U^a)。假设声称,即使敌手可以进行任意多次这样的查询,他成功计算出g^(ab)的概率仍然是可以忽略的。
在交互式DH假设下,并且额外假设:
- 使用的对称加密方案提供“认证加密”功能。
- 哈希函数
H是“随机预言机”(一个理想化的哈希函数模型)。
那么,我们可以证明ElGamal加密系统是抵抗选择密文攻击(CCA安全)的。

总结与展望
本节课中我们一起学习了ElGamal加密系统的安全性分析:
- 我们回顾了计算性Diffie-Hellman(CDH)假设,它是许多密码学协议的基础。
- 我们引入了更强的哈希Diffie-Hellman(HDH)假设,并利用它简洁地证明了ElGamal的语义安全性。
- 我们了解到,为了证明更强的选择密文安全性(CCA),需要借助更强的交互式Diffie-Hellman假设以及随机预言机模型。

然而,交互式假设和随机预言机模型并非理想。一个自然的问题是:我们能否仅基于标准的CDH假设,并使用一个具体的、非理想化的哈希函数,来构造一个CCA安全的公钥加密方案呢?这将是下一节课我们要探讨的内容。
064:安全性更强的ElGamal变体 🔐

概述
在本节课程中,我们将探讨ElGamal公钥加密系统的变体,这些变体能够提供更强的选择密文攻击(CCA)安全性。我们将回顾标准ElGamal的局限性,并介绍几种改进方案,它们基于更自然或更弱的假设来证明安全性。课程最后会提供延伸阅读的论文列表。

ElGamal加密系统回顾
上一节我们介绍了ElGamal公钥加密系统,并提到它在一种较为特殊的交互式Diffie-Hellman假设下被证明是选择密文安全的。本节中,我们来看看如何改进ElGamal,使其安全性分析更优。
首先,让我们回顾一下标准ElGamal加密系统的工作原理。
密钥生成:
- 选择一个随机生成元
g。 - 选择一个随机指数
a ∈ Zn。 - 公钥
PK为(g, h = g^a)。 - 私钥
SK为a。
加密过程:
- 选择一个随机指数
b ∈ Zn。 - 计算
u = g^b。 - 计算共享秘密
s = h^b = g^(ab)。 - 使用哈希函数从
s派生对称密钥k = H(g^b, h^b)。 - 使用对称加密算法
E和密钥k加密消息m,得到c = E(k, m)。 - 输出密文
CT = (u, c)。

解密过程:
- 从密文
CT中提取u和c。 - 使用私钥
a计算共享秘密s = u^a = g^(ab)。 - 派生对称密钥
k = H(u, s)。 - 使用对称解密算法
D和密钥k解密密文c,得到m = D(k, c)。
基于标准CDH假设的改进:Twin ElGamal
标准ElGamal的CCA安全性证明依赖于交互式Diffie-Hellman(IDH)假设。一个自然的问题是:能否仅基于标准的计算性Diffie-Hellman(CDH)假设来证明其安全性?答案是肯定的,这可以通过一个优雅的构造——Twin ElGamal(双生ElGamal)来实现。
以下是Twin ElGamal的工作流程,它是对标准ElGamal的一个简单修改。
密钥生成:
- 选择一个随机生成元
g。 - 选择两个随机指数
a1, a2 ∈ Zn作为私钥SK = (a1, a2)。 - 公钥
PK为(g, h1 = g^a1, h2 = g^a2)。可以看到,公钥比标准ElGamal多了一个元素。

加密过程:
- 选择一个随机指数
b ∈ Zn。 - 计算
u = g^b。 - 计算三个元素的哈希值来派生密钥:
k = H(g^b, h1^b, h2^b)。 - 使用对称加密算法
E和密钥k加密消息m,得到c = E(k, m)。 - 输出密文
CT = (u, c)。密文长度与标准版相同。
解密过程:
- 从密文
CT中提取u和c。 - 使用私钥
(a1, a2)计算u^a1 = g^(b*a1) = h1^b和u^a2 = g^(b*a2) = h2^b。 - 现在,解密方拥有了加密方哈希过的三个值:
u,h1^b,h2^b。因此可以计算相同的对称密钥k = H(u, h1^b, h2^b)。 - 使用对称解密算法
D和密钥k解密密文c,得到m = D(k, c)。
安全性分析:
这个简单的修改(公钥多一个元素,加/解密时多哈希一个值)使得我们能够仅基于标准的CDH假设(在随机预言机模型下)证明其CCA安全性。代价是加密方需要多进行一次指数运算(共3次),解密方也需要多进行一次指数运算(共2次)。

超越随机预言机模型
Twin ElGamal的证明仍然依赖于“哈希函数是理想的随机预言机”这一假设。密码学界一个非常活跃的研究领域就是构建不依赖随机预言机的、高效的CCA安全公钥加密方案。针对ElGamal框架,主要有两类构造:
- 使用双线性群:这类特殊群具有额外的代数结构(通常基于椭圆曲线),使得CDH假设和IDH假设被证明是等价的。利用这种结构,可以构造出非常高效且不依赖随机预言机的CCA安全方案。
- 使用判定性Diffie-Hellman(DDH)假设:在某些群中(如ZP*的素数阶子群),DDH假设被认为是困难的。基于DDH假设,可以构造一个称为Cramer-Shoup系统的ElGamal变体,它可以在不依赖随机预言机的情况下被证明是CCA安全的。
这两类构造都非常精妙,但需要更深入的背景知识才能详细展开。

延伸阅读
以下是一些推荐阅读的论文,它们深入探讨了本节提到的各种概念和构造:
- 关于Diffie-Hellman假设的综述:Dan Boneh的论文《The Decision Diffie-Hellman Problem》详细讨论了与Diffie-Hellman相关的各种计算假设。
- 基于DDH的CCA安全构造:Cramer和Shoup于2002年发表的论文《A Practical Public Key Cryptosystem Provably Secure Against Adaptive Chosen Ciphertext Attack》是经典之作。
- 基于双线性群的CCA安全构造:Boneh和Franklin的论文《Identity-Based Encryption from the Weil Pairing》介绍了基于身份的加密,该方案可以自然地导出CCA安全的公钥加密。
- Twin ElGamal构造及其证明:Cash, Kiltz和Shoup的论文《The Twin Diffie-Hellman Problem and Applications》详细阐述了Twin ElGamal。
- 可抽取哈希证明的通用框架:Jutla的近期论文《Encryption Schemes with Post-Challenge Auxiliary Inputs》提供了一个构建CCA安全系统的新颖通用框架。
总结
本节课我们一起学习了如何增强ElGamal加密系统的安全性。我们首先回顾了标准ElGamal在CCA安全性上的局限,然后介绍了Twin ElGamal变体,它仅基于标准的CDH假设即可实现CCA安全。最后,我们概述了密码学界为摆脱随机预言机假设所做的努力,主要分为利用双线性群和基于更强的DDH假设(如Cramer-Shoup系统)两条路径。这些工作展示了公钥加密设计领域在过去几十年的丰硕成果。
065:统一主题


在本节课中,我们将探讨公钥加密系统背后的统一核心概念——单向函数。我们将了解单向函数的定义,并通过几个具体例子来理解它们如何成为构建公钥密码学(如Diffie-Hellman密钥交换和RSA加密)的基础。
单向函数的概念
上一节我们介绍了两种公钥加密体系。本节中,我们来看看它们共同遵循的一个更普遍的原则。
这个统一主题被称为单向函数。那么,什么是单向函数呢?我们之前已经简要接触过这个概念。本质上,一个从集合X映射到集合Y的函数F被称为单向的,如果存在一个高效算法可以让我们计算函数F(即任何人都能计算任意输入对应的F值),但反推函数F却非常困难。这就是函数“单向”的含义。
更具体地说,我们可以将函数F视为一个映射。但需要注意的是,X中的多个点可能被映射到Y中的同一个点。

当我说函数难以求逆时,意思是:当我给你Y中的某个点,并要求你找出这个点的一个原像时,你无法高效地找到任何一个原像。换句话说,没有高效算法能找到给定点Y的逆。
更精确的定义是:对于所有高效算法A,如果我随机从集合X中选择一个x,并将F(x)给算法A,然后要求算法A产生F(x)的一个原像。算法A产生了一个点,如果我将函数F应用于A的输出,得到的结果恰好是F(x)的概率是可忽略的。

这旨在捕捉一个事实:给定F(x),很难找到F(x)的某个原像。

以下是几个不是单向函数的简单例子:
- 恒等函数:
f(x) = x。这显然不是单向的,因为给定f(x),我可以轻易找到其逆,即x本身。 - 映射到常数的函数:例如将所有输入映射到0的函数。如下图所示,它将所有点映射到同一个点。

这个函数不是单向的,因为如果给你这个像中的点(0),很容易找到一个原像,只需选择X中的任意一点即可。
需要指出的是,如果我们更形式化地定义单向函数,那么证明单向函数的存在性将等价于证明P不等于NP。由于我们目前无法证明P是否等于NP,我们基本上也无法证明单向函数的存在,只能假设它们存在。
从伪随机生成器构建单向函数
现在,让我们看第一个单向函数的例子(或者说,我们假设的单向函数)。我们将从一个伪随机生成器来构建它。
假设我有一个从X到Y的函数F,它是一个安全的伪随机生成器。伪随机生成器接受一个小的种子,并将其扩展成一个更大的输出。例如,你可以想象这个伪随机生成器是使用AES的确定性计数器模式构建的。
可以很容易地看出,如果F是一个安全的伪随机生成器,那么F实际上就是一个单向函数。因此,我们的第一个单向函数例子直接构建于伪随机生成器之上。这个证明比较简单,我们采用反证法。
假设存在一个能高效反演F的算法A(即F不是单向的)。那么我需要构建一个算法B来攻破这个伪随机生成器。
算法B的工作方式如下:当给定集合Y中的某个y时,它会尝试在输入y上运行算法A。如果y是伪随机生成器的输出,那么算法A将以不可忽略的概率输出种子(即X中的某个元素)。然后,我们再次将伪随机生成器应用于算法A的输出。如果y确实是生成器的输出,那么A的输出就是种子,再次应用生成器应该得到我们最初开始的y。如果这个条件成立,算法B就输出0;否则输出1。

这就是一个针对伪随机生成器的区分器。如果我们的区分器B得到的是伪随机生成器的输出y,那么它以不可忽略的概率输出0。然而,如果B得到的是一个真正的随机字符串,由于真正的随机字符串几乎不可能是生成器的输出(即没有对应的种子),因此我们的区分器将以极高的概率输出1。
因此,如果算法A能够反演F,那么算法B就能攻破生成器。既然生成器是安全的,算法A就不能反演F。所以,没有高效算法能反演F,因此F是一个单向函数。

这个讨论虽然较长,但核心是想表明:伪随机生成器直接给出了一个单向函数。不幸的是,这种单向函数没有特殊属性,这意味着很难用它来进行密钥交换或公钥加密。就我们所知,用它实现的最佳密钥交换是Merkle谜题。
离散对数单向函数
接下来,让我们看第二个例子。第二个例子我称之为离散对数单向函数。
我们固定一个阶为n的循环群G,并让g作为群G的一个生成元。这意味着g的所有幂次能生成整个群G。
现在定义以下函数:f: Z_n -> G,定义为 f(x) = g^x。它将0到n-1之间的任何元素映射到群G中的一个元素,方法是将g提升到相应的幂次。
显然,如果群G上的离散对数问题是困难的,那么f就是单向的。实际上,f的单向性就是离散对数假设。
这个单向函数的有趣之处在于它具有一些特殊属性。具体来说,即使我不知道x和y是什么,只要给我f(x)和f(y),我就能很容易地计算出f(x + y)。这是如何做到的呢?
根据指数运算规则,f(x + y) 就是 f(x) * f(y)(在群G中进行运算)。如果你不确定,只需回想一下:f(x + y) = g^(x+y) = g^x * g^y,这正是我们这里的结果。
这个简单的属性——函数具有这种加法同态性——正是实现密钥交换和公钥加密的关键所在。

RSA单向函数

现在让我们看下一个例子:RSA单向函数。
我们选择两个素数p和q,设 n = p * q。然后选择一个与φ(n)互质的数e。接着定义函数:f: Z_n* -> Z_n*,定义为 f(x) = x^e mod n。同样,在RSA假设下,我们认为这个函数是单向的。
这个函数的有趣之处在于它具有与上一张幻灯片中看到的函数相似的属性。给定f(x)和f(y),我们现在可以计算f(x * y)(而不是f(x + y))。我们说这个函数具有乘法属性,而不是上一张幻灯片的加法属性。
但更重要的是,这个函数最令人兴奋的地方在于它有一个陷门。换句话说,存在一个秘密密钥,可以让我们突然反演这个函数,而在没有这个陷门的情况下,据我们所知该函数是单向的。
这个陷门属性同样使得RSA函数特别适合构建数字签名。在第7周,我们将看到RSA函数和离散对数函数都能让我们构建数字签名,但RSA函数因为有陷门,使得构建数字签名变得非常、非常简单。事实上,世界上大多数数字签名都依赖于RSA函数,正是因为用它构建数字签名如此简单。

总结
本节课中我们一起学习了公钥密码学的统一核心——单向函数及其特殊属性。
我们能够构建公钥密码学(即能够进行密钥交换、公钥加密等)的原因,是因为我们能够构造具有非常特殊属性的单向函数。特别是,它们具有这些有时被称为同态的属性,即给定f(x)和f(y),我们可以构造出f(x + y)或f(x * y)。而像RSA这样的函数甚至还有陷门,这让我们可以非常轻松地构建数字签名。
我想展示的主要观点是:公钥密码学的魔力之所以成为可能,正是得益于这些具有特殊属性的单向函数。

本模块到此结束,在第7周我们将开始学习数字签名。
066:课程回顾与暂别寄语 🎓

在本节课中,我们将对为期六周的《密码学1》课程进行回顾与总结,并展望后续内容。
课程内容回顾 📚
上一节我们介绍了公钥加密的构建,本节中我们来整体回顾一下本课程所涵盖的核心主题。
以下是我们在课程中讨论的密码学原语概览图:

- 第一周:我们首先讨论了伪随机数生成器(PRG)和流密码。
- 第二周:我们讨论了分组密码。理解分组密码的正确方式是将其视为伪随机置换(PRP)和伪随机函数(PRF)。我们提到,使用计数器模式(CTR),可以将分组密码转换为PRG。同时,通过GGM构造,可以从伪随机生成器构建分组密码。
- 第三周:我们讨论了数据完整性,特别是消息认证码(MAC)。我们研究了从伪随机函数构建MAC的各种方法,例如CBC-MAC、HMAC、PMAC等。我们还讨论了抗碰撞性,并指出抗碰撞哈希函数可用于数据完整性保护,尤其是在只读存储器场景中:先将数据的哈希值写入只读存储器,之后通过比较哈希值来验证数据的真实性。
- 第四周:我们讨论了如何结合完整性与机密性。具体来说,我们探讨了如何结合加密和MAC来构建我们所说的认证加密。我告诉过你们,实际上在实践中唯一允许使用的对称加密形式就是认证加密。仅能抵御窃听攻击的加密通常并不安全,必须同时防范篡改。因此,进行对称加密时应只使用认证加密模式。
- 第五周和第六周:我们转换主题,讨论了密钥交换和公钥加密。第五周我们讲解了陷门函数和Diffie-Hellman协议,并解释了其背后的数学原理。第六周我们讨论了如何基于陷门函数和Diffie-Hellman协议构建公钥加密。
重要说明与未来展望 🔮
需要强调的是,我们在第五周看到的密钥交换协议仅能抵御窃听攻击,绝不应在实践中使用。实际上,在后续的第八周,我们将学习认证密钥交换协议,这些才是实际中(例如在SSL等协议中)真正使用的协议。我们学习那些简单密钥交换机制的唯一原因,是为了引出陷门函数和Diffie-Hellman群组的概念。
完整的密码学课程还有四周内容,我们将在稍后继续。以下是后续内容的简要介绍:


- 第七周:我们将讨论数字签名,以及如何以任何人都能验证的方式认证数据。
- 第八周:如前所述,我们将讨论认证密钥交换。
- 第九周:我们将讨论用户认证,包括密码管理、一次性密码、挑战-响应协议等。
- 第十周:我们将讨论各种隐私保护机制,例如如何在不暴露位置的情况下进行认证,如何在不透露身份的情况下进行签名等。作为其中一些机制的构建模块,我们还将讨论零知识证明协议,这是一种在密码学中应用非常广泛的通用工具。
密码学的广阔天地 🌌
但必须说明,密码学远不止这些核心主题。实际上,如果有足够的时间,我还有很多很多主题想与大家分享。

这里列出的只是一个简短的清单,甚至不是一个详尽的清单。还有许多其他内容我希望介绍。因此,如果有足够的需求,我甚至可能会开设一门高级密码学课程(通常是为研究生开设的),涵盖这些更高级的主题。这可能会在明年某个时候进行,请保持关注,届时我会发布相关通知。
核心建议与最终提醒 ⚠️

最后,请务必记住我从这门课程中传达的主要信息:密码学是一个强大的工具,但使用时必须始终谨慎。


如果你错误地实现了密码学,系统可能仍会正常运行,你无法察觉任何问题,直到攻击者尝试攻击时,系统可能轻易被攻破。因此,密码学是那种“一知半解相当危险”的领域。确保实现正确至关重要。实现这一点的一种方法是,确保始终有其他人审查你的代码和设计,以发现密码学实现中的任何缺陷,甚至是系统设计中更普遍的缺陷。
最后,留给大家的临别赠言是:永远不要发明自己的密码算法或操作模式,也永远不要去实现自己的密码算法或操作模式。尽可能遵循标准,尽可能使用这些算法的标准实现。如果不得不偏离标准,请确保有足够的第三方对你所做的工作进行审查。
课程结束安排 📝
那么,我就在这里和大家说再见了。


期末考试将在第七周发布,基本上是第六周讲座公开一周后。期末考试将涵盖全部六周的内容,其形式与习题集大致相同。
我希望大家都能在考试中取得好成绩。在所有课程作业完成后,我们将发放证书。我期待在下一期课程中与大家再见。请一如既往地在论坛上提交您的评论和建议,我会阅读所有的帖子,它们对改进课程非常有帮助。期待在秋季与大家相见。
本节课中我们一起学习了《密码学1》六周课程的核心内容回顾,了解了从伪随机生成器、分组密码、数据完整性、认证加密到公钥加密与密钥交换的知识脉络。我们明确了已学密钥交换协议的局限性,并展望了后续关于数字签名、认证密钥交换、用户认证和隐私机制等主题。最重要的是,我们牢记了密码学实践中的核心准则:谨慎使用标准实现,并寻求第三方审查,切勿自行发明或实现密码算法。

浙公网安备 33010602011771号