真实世界的密码学-全-
真实世界的密码学(全)
原文:
annas-archive.org/md5/655c944001312f47533514408a1a919a译者:飞龙
前言
序言
当你拿起这本书时,你可能会想,为什么又一本关于密码学的书?甚至,为什么我要读这本书?要回答这个问题,你必须了解它是什么时候开始的。
一本经过多年打磨的书
如今,如果你想学习几乎任何东西,你会谷歌它,或者必应它,或者百度它——你懂的。然而,对于密码学,取决于你要找的是什么,资源可能相当匮乏。这是我很久以前就遇到的问题,从那时起一直是持续的挫败感的来源。
在我还在学校时,我曾经为一门课程实现过差分功率分析攻击。那时,这种攻击是密码分析的一个突破,因为它是第一个公开发表的侧信道攻击。差分功率分析攻击是一种神奇的技术:通过在设备加密或解密时测量其功耗,你能够提取它的秘密。我意识到,优秀的论文可以传达出伟大的思想,而在清晰度和可理解性上却付出了很少的努力。我记得为了弄清作者想要表达的意思而一头撞在墙上。更糟糕的是,我找不到解释这篇论文的好的在线资源。所以我又撞了一会儿头,最终搞明白了。然后,我想,也许我可以帮助其他像我一样将要经历这一磨难的人。
受到鼓舞,我画了一些图表,加以动画处理,并录制了自己讲解它们的视频。那是我关于密码学的第一段 YouTube 视频:www.youtube.com/watch?v=gbqNCgVcXsM。
几年后,我上传了视频之后,我仍然从互联网上的陌生人那里收到赞扬。就在昨天,当我写这个序言时,有人发帖说:“谢谢你,真的是一个很好的解释,可能省了我好几个小时来理解那篇论文。”
这是多么大的一份奖励啊!在冒险探索教育领域的另一边迈出的这一小步足以让我想要做更多的事情。我开始录制更多这样的视频,然后我开始写一篇关于密码学的博客。你可以在这里查看:cryptologie.net。
在开始写这本书之前,我已经积累了将近 500 篇解释这个介绍之外许多概念的文章。这一切都只是练习。在我心里,写一本书的想法在 Manning Publications 与我联系提出书籍提案的几年前就在慢慢成熟。
现实世界的密码学课程
我完成了理论数学学士学位,不知道接下来该做什么。我一直在编程,而我想要协调这两者。自然而然地,我对密码学产生了兴趣,它似乎兼具了两者的优点,并开始阅读我能够得到的不同的书籍。我很快发现了自己的人生使命。
有些事情很让人烦恼,尤其是那些以历史开始的长篇介绍;我只对技术性感兴趣,一直以来都是如此。我发誓,如果我写一本关于密码学的书,我不会写一行有关维吉尼亚密码、凯撒密码和其他历史遗留物的内容。于是,在波尔多大学获得密码学硕士学位后,我以为我已经为现实世界做好了准备。我还是太天真了。
我相信我的学位足够了,但我的教育缺乏关于我即将攻击的真实世界协议的许多知识。我花了很多时间学习椭圆曲线的数学知识,但对这些在密码算法中如何使用却一无所知。我学习了有关 LFSR、ElGamal 和 DES 等一系列其他密码学原语,但我永远不会再见到它们。
在我开始在 Matasano(后来成为 NCC Group)工作时,我的第一个任务是审核 OpenSSL,这是最流行的 SSL/TLS 实现——这个代码基本上加密了整个互联网。哦,我的脑袋痛啊。我记得每天回家时头痛欲裂。这简直是一个灾难般的库和协议!当时我完全不知道,几年后我会成为 TLS 1.3 的合著者,这是该协议的最新版本。
但是,那时候,我已经在想,“这才是我应该在学校学到的东西。我现在获得的知识才是为了让我为真实世界做准备!”毕竟,我现在是一个专门从事密码学的安全从业者。我在审核真实世界的加密应用。我正在做人们在完成密码学学位后希望自己能做的工作。我实现了、验证了、使用了,并就应该使用哪些密码算法提供建议。这就是为什么我是我正在写的书的第一位读者。这就是我会写给过去的自己的东西,以便为他准备好面对现实世界。
大多数漏洞所在
我的咨询工作让我审核了许多真实世界的加密应用,如 OpenSSL、Google 的加密备份系统、Cloudflare 的 TLS 1.3 实现、Let’s Encrypt 的证书颁发协议、Zcash 加密货币的 sapling 协议、NuCypher 的门限代理重加密方案,以及其他许多真实世界的加密应用,可惜我不能在公开场合提及。
在我的职业生涯早期,我被指派审计一个知名公司编写的自定义协议以加密其通信。结果发现几乎所有内容都使用了签名,但暂时的密钥却没有,这完全破坏了整个协议,因为任何有一些安全传输协议经验的人都可以轻易地替换它们——这是一个新手的错误,但被认为有足够经验编写自己的加密协议的人却忽略了这一点。我记得在最后解释这个漏洞的时候,整个工程师室安静了好几十秒钟。
在我的职业生涯中,这个故事反复出现了很多次。有一次,当我为另一家客户审计加密货币时,我发现了一种方法可以伪造已经存在的交易,因为签名的内容有些模糊不清。在为另一家客户查看 TLS 实现时,我发现了一些微妙的方法来破坏 RSA 实现,结果,与 RSA 的一位发明者合作撰写了一篇白皮书,导致向十几个开源项目报告了许多公共漏洞和曝光(CVE)。最近,在写作我的书的过程中阅读了关于更新的 Matrix 聊天协议时,我意识到他们的身份验证协议存在问题,导致了他们端到端加密的破解。在使用密码学时,不幸的是,有很多细节可能会导致错误。在这一点上,我知道我必须写点东西来解决这些问题。这就是为什么我的书包含了这么多这样的轶事。
作为工作的一部分,我会审查多种编程语言中的密码学库和应用程序。我发现了一些漏洞(例如,Golang 标准库中的 CVE-2016-3959),研究了库如何欺骗您误用这些漏洞(例如,我的论文《如何植入迪菲-赫尔曼后门》),并就应该使用哪些库提供建议。开发人员从未知道该使用哪个库,而我总是发现答案很棘手。
我继续发明了 Disco 协议(discocrypto.com; embeddeddisco.com),并在不到 1000 行代码的情况下编写了其功能齐全的密码库,而且还涉及了多种语言。Disco 只依赖于两个密码学原语:SHA-3 的置换和 Curve25519。是的,仅凭这两个在 1000 行代码中实现的东西,开发人员就可以进行任何类型的身份验证密钥交换、签名、加密、MAC、哈希、密钥派生等等。这使我对一个好的密码库应该是什么有了独特的看法。
我想让我的书包含这些实用的见解。因此,不同的章节自然包含了如何在不同的编程语言中应用“密码学”的示例,使用备受尊敬的密码学库。
需要一本新书吗?
当我在著名的安全会议 Black Hat(黑帽大会)上举办我的年度密码学培训课程时,一名学生来找我,问我能否推荐一本好书或在线课程关于密码学。我记得建议学生阅读 Boneh 和 Shoup 的一本书,并参加 Coursera 上的 Boneh 的密码学 I 课程。(我也在本书的结尾推荐了这两个资源。)
学生告诉我,“啊,我尝试过,这太理论化了!” 这个回答一直留在我心里。起初我不同意,但慢慢意识到他们是对的。大多数资源在数学上非常深奥,而大多数与密码学交互的开发人员不想涉及数学。对他们来说还有什么选择呢?
当时另外两个颇受尊重的资源是(布鲁斯·施奈尔的两本书)。但这些书开始变得相当过时。应用密码学花了四章讨论分组密码,还有一整章讨论密码模式,但没有提及认证加密。更新一些的密码工程只在脚注中简单提到了椭圆曲线密码。另一方面,我的许多视频或博客文章正在成为一些密码概念的良好主要参考资料。我知道我可以做一些特别的事情。
渐渐地,我的许多学生开始对加密货币产生兴趣,对这个主题提出越来越多的问题。与此同时,我开始审计越来越多的加密货币应用程序。后来,我转到 Facebook 工作,负责领导 Libra 加密货币(现在称为 Diem)的安全工作。当时,加密货币是最热门的领域之一,混合了许多极其有趣的密码原语,迄今为止几乎没有在现实世界中使用过(零知识证明、聚合签名、门限密码学、多方计算、共识协议、密码累加器、可验证随机函数、可验证延迟函数等等……清单还在继续)。然而,没有一本密码学书籍包含了关于加密货币的章节。我现在处于一个独特的位置。
我知道我可以写一本书,告诉学生、开发人员、顾问、安全工程师以及其他人现代应用密码学究竟是什么。这将是一本几乎没有公式但充满许多图表的书。这将是一本几乎没有历史但充满我亲眼目睹的现代密码失败故事的书。这将是一本几乎没有关于传统算法的书,但充满我个人见过的大规模使用的密码学:TLS、Noise 协议框架、Signal 协议、加密货币、HSM、门限密码学等等。这将是一本几乎没有理论密码学但充满可能变得相关的内容:密码认证密钥交换、零知识证明、后量子密码学等等。
当 Manning 出版社在 2018 年联系我,问我是否想要写一本关于密码学的书时,我已经知道了答案。我已经知道我想写什么了。我只是在等待着有人给我机会和借口来花时间写我心中的那本书。巧合的是,Manning 有一系列“真实世界”的书籍,所以自然而然地,我建议我的书扩展它。你眼前的是两年多的辛勤工作和大量热爱的成果。我希望你喜欢。
致谢
感谢 Marina Michaels 一直以来的帮助和见解,如果没有她,这本书可能不会完成。
感谢 Frances Buran,Sam Zaydel,Michael Rosenberg,Pascal Knecht,Seth David Schoen,Eyal Ronen,Saralynn Chick,Robert Seacord,Eloi Manuel,Rob Wood,Hunter Monk,Jean-Christophe Forest,Liviu Bartha,Mattia Reggiani,Olivier Guerra,Andrey Labunov,Carl Littke,Yan Ivnitskiy,Keller Fuchs,Roman Zabicki,M K Saravanan,Sarah Zennou,Daniel Bourdrez,Jason Noll,Ilias Cherkaoui,Felipe De Lima,Raul Siles,Matteo Bocchi,John Woods,Kostas Chalkias,Yolan Romailler,Gerardo Di Giacomo,Gregory Nazario,Rob Stubbs,Ján Jancˇár,Gabe Pike,Kiran Tummala,Stephen Singam,Jeremy O’Donoghue,Jeremy Boone,Thomas Duboucher,Charles Guillemet,Ryan Sleevi,Lionel Rivière,Benjamin Larsen,Gabriel Giono,Daan Sprenkels,Andreas Krogen,Vadim Lyubashevsky,Samuel Neves,Steven(Dongze)Yue,Tony Patti,Graham Steel,以及所有 livebook 评论者的许多讨论和纠正,以及技术和编辑反馈。
感谢所有审阅者:Adhir Ramjiawan,Al Pezewski,Al Rahimi,Alessandro Campeis,Bobby Lin,Chad Davis,David T Kerns,Domingo Salazar,Eddy Vluggen,Gábor László Hajba,Geert Van Laethem,Grzegorz Bernas´,Harald Kuhn,Hugo Durana,Jan Pieter Herweijer,Jeff Smith,Jim Karabatsos,Joel Kotarski,John Paraskevopoulos,Matt Van Winkle,Michal Rutka,Paul Grebenc,Richard Lebel,Ruslan Shevchenko,Sanjeev Jaiswal,Shawn P Bolan,Thomas Doylend,William Rudenmalm,感谢您的建议,这使得这本书更加完善。
关于本书
现在距我开始写《真实世界密码学》已经过去两年多了。我最初打算让它成为一本介绍实际世界中使用的密码学的所有内容的入门书。但是,当然,这是一个不可能的任务。没有一个领域可以用一本书来总结。因此,我不得不在我想给读者的详细程度和我想覆盖的领域之间取得平衡。我希望你发现自己和我一样陷入了同样的困境。如果你正在寻找一本实用的书,教你企业和产品实现和使用的密码学,以及如果你对实际世界的密码学是如何在表面下工作感兴趣,但不是在寻找一个包含所有实现细节的参考书,那么这本书适合你。
谁应该读这本书
这是我认为会受益于这本书的人群类型的列表(尽管请不要让任何人将你归类为某种类型)。
学生
如果你正在学习计算机科学、安全或密码学,并希望了解现实世界中的密码学(因为你要么在瞄准行业工作,要么想在学术界从事应用主题),那么我认为这本教材适合你。为什么呢?因为,正如我在前言中所说的,我曾经也是这样的学生,我写了我那时希望有的那本书。
安全从业者
渗透测试人员,安全顾问,安全工程师,安全架构师以及其他安全角色组成了我教应用密码学时大多数学生的人群。因此,这些材料是通过我在试图向非密码学家解释复杂的密码学概念时收到的许多问题加以完善的。作为一个安全从业者,这本书也受到了我为大公司审计的密码学以及我在途中学到或发现的错误的影响。
直接或间接使用密码学的开发人员
这项工作也受到了我与客户和同事之间的许多讨论的影响,这些人大多既不是安全从业者也不是密码学家。如今,编写代码而不涉及密码学越来越困难,因此,你需要对你所使用的东西有一定的了解。这本书通过不同编程语言的编码示例以及其他更多内容来给你这种理解,如果你感兴趣的话。
密码学家对其他领域感兴趣
这本书是应用密码学的介绍,对像我这样的人很有用。首先我是为自己写的,记住这一点。如果我做得不错,一个理论密码学家应该能够快速理解应用密码学世界的样貌;另一个在对称加密上工作的人应该能够通过阅读相关章节迅速掌握密码验证密钥交换;一个与协议工作的人应该能够迅速地了解量子密码学;依此类推。
工程和产品经理想要了解更多的人
这本书还试图回答我认为更注重产品的问题:这些方法的权衡和限制是什么?我面临什么风险?这条路会帮我遵守法规吗?我需要这样做那样做才能与政府合作吗?
好奇的人想要了解现实世界的加密
你不需要是我之前列出的任何一种类型才能读这本书。你只需要对现实世界中的密码学感兴趣。请记住,我不教授密码学的历史,也不教授计算机科学的基础知识,所以至少,在阅读这样一本书之前,你应该听说过密码学。
假定的知识,长版本
为了最大限度地利用这本书,你需要什么?你应该知道这本书假设你对你的笔记本电脑或互联网的工作方式有一些基本的了解,至少,你应该听说过加密。这本书是关于真实世界的密码学,所以如果你不熟悉计算机或者以前从未听说过encryption这个词,那么很难将事情放在上下文中。
假设你在某种程度上知道你在做什么,如果你知道比特和字节是什么,如果你看过甚至使用过像异或、左移这样的位运算,那就是一个真正的优势。如果你没有?不,但这可能意味着你需要不时停下来花几分钟在读书之前进行一些谷歌搜索。
实际上,无论你有多么有资格,当阅读这本书时,你可能会不时停下来,以获取更多来自互联网的信息。要么是因为我(真是可耻)在使用术语之前忘记了定义它,要么是因为我错误地认为你会了解它。无论如何,这应该不是什么大事,因为我尽力以最简单的方式解释我介绍的不同概念。
最后,当我使用cryptography这个词时,你的大脑可能会想到数学。如果除了那个想法,你的脸还皱起了,那么你会很高兴地知道你不必太担心。真实世界的密码学是关于教授见解,以便你对所有这些是如何运作有直观的理解,并且尽可能避免数学细节。
当然,如果我说制作这本书没有涉及数学,那就是在撒谎了。没有数学就没有教授密码学。所以我要说的是:如果你在数学方面取得了良好的水平,那会有所帮助,但如果没有,这不应该阻止你阅读这本书的大部分内容。除非你对数学有更高级的理解,否则一些章节对你来说可能不友好,特别是最后两章(第十四章和第十五章)关于量子密码学和下一代密码学,但没有什么是不可能的,你可以通过意志力和谷歌搜索矩阵乘法和其他你可能不了解的事物来应对这些章节。如果你决定跳过这些章节,请确保你不要跳过第十六章,因为那是锦上添花。
这本书的组织方式:一份路线图
真实世界的密码学分为两部分。第一部分应该从第一页读到最后一页,涵盖了大部分密码学的成分:像乐高一样的东西,用来构建更复杂的系统和协议。
-
第一章是关于真实世界密码学的介绍,给你一些你将学到的内容的概念。
-
第二章讨论了哈希函数,这是密码学中使用的一种基本算法,用于从字节串创建唯一标识符。
-
第三章讲述了数据认证以及你如何确保没有人修改你的消息。
-
第四章讲述了加密,它允许两个参与者将他们的通信隐藏起来,使观察者无法看到。
-
第五章介绍了密钥交换,它允许你与他人进行交互地协商一个共同的秘密。
-
第六章描述了非对称加密,它允许多人将消息加密给单个人。
-
第七章讨论了签名,密码学等效于笔和纸签名。
-
第八章讲述了随机性以及如何管理你的秘密。
本书的第二部分包含了由这些成分构建的系统。
-
第九章教你如何使用加密和认证来保护机器之间的连接(通过 SSL/TLS 协议)。
-
第十章描述了端对端加密,这实际上是关于像你和我这样的人如何相互信任的。
-
第十一章展示了机器如何认证人,并且人们如何帮助机器相互同步。
-
第十二章介绍了新兴的加密货币领域。
-
第十三章重点介绍了硬件密码学,这些设备可以用来防止你的密钥被提取。
有两个附加章节:第十四章介绍了后量子密码学,第十五章介绍了下一代密码学。这两个领域开始进入产品和公司,要么是因为它们变得更加相关,要么是因为它们变得更加实用和高效。虽然如果你跳过这最后两章我不会责怪你,但你必须在把这本书放回书架之前仔细阅读第十六章(结语)。第十六章总结了密码学从业者(也就是你,一旦你完成这本书)必须牢记的不同挑战和不同教训。就像蜘蛛侠的本·帕克叔叔说的:“伴随着伟大的力量而来的是伟大的责任。”
关于代码
本书包含许多源代码示例,既在编号列表中,也在普通文本中。在这两种情况下,源代码都以fixed-width font like this的格式进行排版,以区分它与普通文本。有时,代码也会以in bold的形式呈现,以突出显示从章节中的先前步骤中改变的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已经重新格式化;我们添加了换行符和重新调整了缩进,以适应书中可用的页面空间。在罕见情况下,甚至这都不够,列表包括行连续标记(➥)。此外,在文本中描述代码时,源代码中的注释通常已被从列表中删除。代码注释伴随着许多列表,突出显示重要概念。
liveBook 讨论论坛
购买包括免费访问由 Manning Publications 运行的私人网络论坛,您可以在那里对这本书发表评论,提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请转到livebook.manning.com/book/real-world-cryptography/discussion。您还可以在livebook.manning.com/discussion了解有关 Manning 论坛和行为规则的更多信息。
Manning 对我们的读者的承诺是提供一个场所,让个别读者之间以及读者与作者之间进行有意义的对话。这不是对作者参与的任何特定数量的承诺,作者对论坛的贡献仍然是自愿的(且未付费的)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他失去兴趣!只要这本书还在印刷中,您可以从出版商的网站访问论坛和以前的讨论档案。
关于作者
David Wong是 O(1) Labs 的高级密码学工程师,负责 Mina 加密货币。在此之前,他是 Facebook 的 Diem(原名 Libra)加密货币的安全主管,之前是 NCC Group 的密码服务实践的安全顾问。David 还是《现实世界的密码学》一书的作者。
在他的职业生涯中,David 参与了几个公开资助的开源审计项目,如 OpenSSL 和 Let’s Encrypt。他曾在各种会议上发表演讲,包括黑帽大会和 DEF CON,并在黑帽大会上教授了一门关于密码学的课程。他为 TLS 1.3 和 Noise 协议框架做出了贡献。他发现了许多系统中的漏洞,包括 Golang 标准库中的 CVE-2016-3959,以及各种 TLS 库中的 CVE-2018-12404、CVE-2018-19608、CVE-2018-16868、CVE-2018-16869 和 CVE-2018-16870。
他是 Disco 协议(www.discocrypto.com和www.embeddeddisco.com)和智能合约的去中心化应用安全项目(www.dasp.co)的作者之一。他的研究包括对 RSA 的缓存攻击(cat.eyalro.net/)、基于 QUIC 的协议(eprint.iacr.org/2019/028)、对 ECDSA 的时序攻击(eprint.iacr.org/2015/839)或者 Diffie-Hellman 中的后门(eprint.iacr.org/2016/644)。您可以在他的博客www.cryptologie.net上看到并阅读有关他的信息。
关于封面插图
《现实世界的加密学》 封面上的图画标题为“Indienne de quito”,即基多印第安人。这幅插图取自雅克·格拉塞·德·圣索维尔(Jacques Grasset de Saint-Sauveur,1757–1810)收集的各国服装装束,题为 《不同国家的服装》 ,于 1797 年在法国出版。每幅插图都是精细绘制并手工着色的。格拉塞·德·圣索维尔的收藏丰富多样,生动地提醒我们,仅 200 年前,世界各地的城镇和地区在文化上是多么的不同。人们相互隔离,说着不同的方言和语言。在街道上或乡间,仅凭着他们的服装就能轻易地辨认出他们的居住地和他们的职业或社会地位。
自那时以来,我们的穿着方式已经发生了变化,地区间的多样性,当时如此丰富,已经消失了。如今很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。也许我们已经用更多样化的个人生活,甚至是更多样化和快节奏的技术生活来交换了文化多样性。
在如今难以区分一本计算机书籍与另一本之际,曼宁(Manning)通过基于格拉塞·德·圣索维尔的图片再现两个世纪前丰富多样的地区生活,来庆祝计算机行业的创造力和主动性。
第一部分:原语:密码学的基本组成部分
欢迎来到密码学的现实世界!你手中的这本书(如果你选择获取印刷版本)分为两个相等的部分,共有八章。通过阅读整本书,你将了解几乎所有关于现实世界密码学的知识——就是你所处的这个世界。
请注意,本书的第一部分是按顺序阅读的,尽管每一章都会告诉你前提条件是什么,所以不要把这看作是一种强制性的限制。前八章带你了解基础知识——密码学的基本构建块。每一章介绍一个新的原语,并教你它的作用、工作原理以及它如何与其他元素结合使用。本书的第一部分旨在在我们开始利用全部内容之前,为你提供良好的抽象和见解。
祝你好运!
第一章:引言
本章涵盖
-
密码学的内涵
-
理论与现实中的密码学
-
你将在这次冒险中学到什么
问候,旅行者;坐稳了。你即将进入一个充满奇迹和神秘的世界——密码学的世界。密码学是一门古老的学科,旨在保护受到恶意人物侵扰的情况。这本书包括了我们需要防御自己免受恶意的咒语。许多人尝试学习这门技艺,但很少有人能在掌握之前生存下来,因为掌握这门技艺面临着重重挑战。的确,令人兴奋的冒险在等待着!
在这本书中,我们将揭示密码算法如何保护我们的信件,识别我们的盟友,并保护我们的宝藏免受敌人的侵害。穿越密码学的海洋将不会是一次最顺利的旅程,因为密码学是我们世界安全和隐私的基础——最轻微的错误都可能致命。
注意 如果你发现自己迷失了方向,请记得继续向前走。一切最终都会变得清晰起来。

1.1 密码学是关于保护协议的
我们的旅程从介绍密码学开始,这是一门旨在防御协议免受破坏者侵害的科学。但首先,什么是协议?简单来说,它是一个(或多个人)必须遵循一系列步骤以实现某事的步骤列表。例如,想象一下以下假设:你想将你的魔剑无人看管几个小时,这样你就可以小睡一会儿。一个做到这一点的协议可能是这样的:
-
将武器放在地上
-
在树下小睡一会儿
-
从地上取回武器
当然,这并不是一个很好的协议,因为任何人都可以在你小睡时偷走你的剑……因此,密码学是考虑到那些想要利用你的对手的。
在古代,当统治者和将军们忙于背叛彼此和策划政变时,他们最大的问题之一就是如何与他们信任的人分享机密信息。从这里,密码学的概念诞生了。经过几个世纪的努力和辛勤工作,密码学才成为今天严肃的学科。现在,它在我们周围被广泛使用,以在我们混乱和不利的世界中提供最基本的服务。
这本书的故事是关于密码学的实践。它带领你在整个计算机世界中进行探险,介绍了当今正在使用的密码协议;它还向你展示了它们由什么部分组成以及如何组合在一起。虽然一本典型的密码学书通常是从密码学的发现开始,然后带你穿越它的历史,但我认为用这种方式开始事情没有太多意义。我想告诉你实用的东西。我想告诉你我亲眼所见的,作为一家大公司的顾问审查密码应用,或者作为领域工程师自己使用的密码学。
几乎不会有可怕的数学公式。本书的目的是揭示密码学的神秘,调查当今被认为有用的内容,并提供关于你周围事物是如何构建的直觉。本书面向对此感兴趣的人,有冒险精神的工程师,冒险的开发者和好奇的研究人员。第一章,本章,开始了对密码学世界的探索之旅。我们将发现不同类型的密码学,哪些对我们重要,以及世界是如何同意使用这些的。
1.2 对称加密:什么是对称加密?
密码学的一个基本概念是对称加密。它在本书中的大多数密码算法中使用,因此非常重要。我通过我们的第一个协议在这里介绍这个新概念。
让我们想象一下,女王爱丽丝需要给住在几个城堡之外的鲍勃勋爵发送一封信。她请求她忠诚的信使骑着他可靠的坐骑,冒着前方危险的土地,为了将珍贵的消息送到鲍勃勋爵手中。然而,她心存疑虑;即使她的忠诚信使为她服务多年,她希望在传输过程中的消息对所有被动观察者保密,包括信使!你看,这封信很可能包含了一些关于途中王国的有争议的八卦。

女王爱丽丝需要的是一个模拟将消息直接交给鲍勃勋爵而没有中间人的协议。这在实践中是一个几乎不可能解决的问题,除非我们引入密码学(或者传送)到方程中。这就是我们很久以前通过发明一种新类型的加密算法——称为对称加密算法(也称为密码)来解决的问题。
顺便说一下,一种加密算法通常被称为原语。你可以把原语看作是密码学中最小的、有用的构造,通常与其他原语一起使用以构建协议。这主要是一个术语,没有特别重要的意义,尽管它在文献中经常出现,但了解它是很好的。
让我们看看如何使用加密原语来隐藏女王爱丽丝的消息免受信使的干扰。现在想象一下,这个原语是一个黑匣子(我们看不到里面或者它内部在做什么),提供两个函数:
-
ENCRYPT
-
DECRYPT
第一个函数,ENCRYPT,通过取一个秘钥(通常是一个大数)和一个消息来工作。然后输出一系列看起来像随机数的数字,一些嘈杂的数据。我们将称这个输出为加密消息。我在图 1.1 中说明了这一点。

图 1.1 ENCRYPT 函数接受一个消息和一个秘钥,并输出加密消息——一长串看起来像随机噪音的数字。
第二个函数 DECRYPT 是第一个函数的反函数。它使用相同的秘钥和第一个函数的随机输出(加密的消息),然后找到原始消息。我在图 1.2 中进行了说明。

图 1.2 DECRYPT 函数接收一个加密的消息和一个秘钥,并返回原始消息。
要使用这个新的原语,女王艾丽斯和鲍勃勋爵必须首先在现实生活中见面并决定使用什么秘钥。稍后,女王艾丽斯可以使用提供的 ENCRYPT 函数,借助秘钥保护消息。然后,她将加密的消息传递给她的信使,最终将其传递给鲍勃勋爵。然后,鲍勃勋爵使用相同的秘钥对加密的消息使用 DECRYPT 函数来恢复原始消息。图 1.3 显示了这个过程。

图 1.3(1)艾丽斯使用带有秘钥的 ENCRYPT 函数将她的消息转换为噪音。(2)然后她将加密的消息传递给她的信使,后者不会了解到底层消息的任何信息。(3)一旦鲍勃收到加密的消息,他可以使用与艾丽斯相同的秘钥使用 DECRYPT 函数来恢复原始内容。
在这个交换过程中,信使所拥有的只是看起来随机的东西,它对隐藏消息的内容没有任何有意义的见解。实际上,借助密码学的帮助,我们将我们的不安全协议增强为安全协议。新的协议使女王艾丽斯能够向鲍勃勋爵发送一封机密信件,而没有任何人(除了鲍勃勋爵)了解其内容。
使用秘钥将事物渲染成噪音,使其与随机无法区分的过程,在密码学中是一种常见的安全协议保护方式。随着你在接下来的章节中学习更多密码算法,你会看到更多这样的内容。
顺便说一句,对称加密是密码学算法的一个更大类别的一部分,称为对称密码学或秘密密钥密码学。这是由于密码原语暴露的不同函数使用相同的密钥。后面你会看到,有时会有不止一个密钥。
1.3 克尔克霍夫原则:只有密钥是保密的
设计一个密码算法(就像我们的加密原语)是一件容易的事情,但设计一个安全的密码算法并不是胆小之人能够做到的。虽然我们在这本书中避免创建这样的算法,但我们确实学会了如何识别优秀的算法。这可能会很困难,因为选择太多,超出了任务所需。在密码学的历史中反复失败的经验教训以及社区从中学到的教训中可以找到一些提示。当我们回顾过去时,我们将领会到什么将密码算法变成一个值得信赖的安全算法。
数百年过去了,许多皇后和领主被埋葬了。从那时起,纸张被放弃作为我们主要的交流方式,转而采用更好更实用的技术。如今,我们可以接触到强大的计算机以及互联网。更实用,当然,但这也意味着我们之前的恶意传送者变得更加强大。他现在无处不在:你所在的星巴克咖啡厅的 Wi-Fi、构成互联网并转发你的消息的不同服务器,甚至在运行我们算法的机器上。我们的敌人现在能够观察到更多的消息,因为你向网站发出的每个请求都可能通过错误的线路,并在几纳秒内被改变或复制,而没有人注意到。
在我们之前,我们可以看到最近的历史中有许多加密算法失效的例子,被秘密国家组织或独立研究人员破解,并未能保护其消息或实现其声明。我们吸取了许多教训,并逐渐了解了如何制造良好的密码学。
注意,密码算法可以通过多种方式被视为破解。对于加密算法,你可以想象到多种攻击算法的方法:秘密密钥可以泄露给攻击者,消息可以在没有密钥的情况下解密,仅通过查看加密消息就可以透露一些关于消息的信息,等等。任何会削弱我们对算法做出的假设的事情都可以被视为破解。
密码学经历了漫长的试验和错误过程后,产生了一个强有力的概念:要对密码原语所声称的安全性进行信任,就必须由专家公开分析该原语。否则,你就是在依赖安全性通过模糊性,这在历史上并不奏效。这就是为什么密码学家(构建者)通常会寻求密码分析家(破解者)的帮助来分析一个构造的安全性。(尽管密码学家经常自己也是密码分析家,反之亦然。)

让我们以高级加密标准(AES)加密算法为例。AES 是由美国国家标准与技术研究院(NIST)组织的国际竞赛的产物。
注意,NIST 是一个美国机构,其角色是定义政府相关职能以及其他公共或私营组织使用的标准并制定指南。像 AES 一样,它标准化了许多广泛使用的密码原语。
AES 竞赛持续了数年,期间来自世界各地的许多志愿密码分析师聚集在一起,试图打破各种候选结构。几年后,通过这个过程建立了足够的信心后,一个单一的竞争性加密算法被提名为成为高级加密标准本身。现在,大多数人相信 AES 是一种可靠的加密算法,并且被广泛用于几乎所有的加密。例如,当你浏览网页时,你每天都在使用它。
在公开建立加密标准的想法与一个经常被称为Kerckhoffs' principle的概念有关,可以理解为这样一种情况:依赖于我们的敌人不会发现我们使用的算法是愚蠢的,因为他们很可能会发现。相反,让我们对此持开放态度。
如果女王艾丽斯和鲍勃勋爵的敌人确切地知道他们是如何加密消息的,那么他们的加密算法如何安全?答案是秘钥!秘钥的保密性使得协议安全,而不是算法本身的保密性。这是本书的一个常见主题:我们将要学习的所有加密算法,以及实际世界中使用的加密算法,大多数情况下是可以自由学习和使用的。只有作为这些算法输入的秘钥是保密的。1644 年,让·罗伯特·迪·卡莱特(Jean Robert du Carlet)说:“Ars ipsi secreta magistro”(即使对于大师来说也是秘密的艺术)。在接下来的部分中,我将谈论一种完全不同类型的密码原语。现在,让我们使用图 1.4 来整理我们迄今为止学到的知识。

图 1.4 到目前为止你学到的密码算法。AES 是对称加密算法的一个实例,它是更广泛的对称加密算法类别的一部分。
1.4 非对称加密:两把钥匙比一把好
在我们关于对称加密的讨论中,我们说女王艾丽斯和鲍勃勋爵首先见面商定一个对称秘钥。这是一个合理的情景,实际上很多协议确实是这样工作的。然而,在有许多参与者的协议中,这很快就变得不太实际:我们需要我们的网络浏览器与谷歌、Facebook、亚马逊以及其他数十亿个网站见面,然后才能安全地连接到它们吗?
这个问题,通常称为密钥分配,在很长一段时间内一直很难解决,至少直到 20 世纪 70 年代末另一类大而有用的密码算法被发现,称为非对称密码学或公钥密码学。非对称密码学通常使用不同的密钥来执行不同的功能(与对称密码学中使用的单个密钥相对),或者为不同的参与者提供不同的观点。为了说明这意味着什么以及公钥密码学如何帮助建立人与人之间的信任,我将在本节中介绍一些非对称原语。请注意,这只是你将在本书中学到的内容的一个概述,因为我将在随后的章节中更详细地讨论这些密码原语中的每一个。
1.4.1 密钥交换或如何获得共享秘密
我们将要看的第一个非对称密码学原语是密钥交换。首次发现并发布的公钥算法是一种以其作者命名的密钥交换算法,称为 Diffie-Hellman(DH)。DH 密钥交换算法的主要目的是在两个参与方之间建立一个共同的秘密。然后可以将这个共同的秘密用于不同的目的(例如,作为对称加密原语的密钥)。
在第五章中,我将解释 Diffie-Hellman 的工作原理,但在此简介中,让我们使用一个简单的类比来理解密钥交换提供了什么。像密码学中的许多算法一样,密钥交换必须从参与者使用的一组共同参数开始。在我们的类比中,我们简单地让 Alice 女王和 Bob 勋爵同意使用一个正方形(■)。接下来的步骤是让他们选择自己的随机形状。他们俩都去各自的秘密地点,在不被看到的情况下,Alice 女王选择了一个三角形(▲),而 Bob 勋爵选择了一个星形(★)。他们选择的对象必须以任何代价保持秘密!这些对象代表他们的私钥(见图 1.5)。

图 1.5 DH(Diffie-Hellman)密钥交换的第一步是让两个参与者生成一个私钥。在我们的类比中,Alice 女王选择一个三角形作为她的私钥,而 Bob 勋爵选择一个星形作为他的私钥。
一旦他们选择了他们的私钥,他们都会单独将他们的秘密形状与他们最初同意使用的共同形状(正方形)相结合。这些组合产生了代表他们的公钥的独特形状。Alice 女王和 Bob 勋爵现在可以交换他们的公钥(因此称为密钥交换),因为公钥被视为公共信息。我在图 1.6 中说明了这一点。

图 1.6 DH 密钥交换的第二步,两个参与者交换他们的公钥。参与者通过将他们的私钥与一个共同形状相结合来导出他们的公钥。
现在我们开始看到为什么这个算法被称为公钥算法。这是因为它需要一个由私钥和公钥组成的 密钥对。DH 密钥交换算法的最后一步非常简单:Alice 女王取 Bob 男爵的公钥并与她的私钥结合。Bob 男爵也同样对待 Alice 女王的公钥,并将其与自己的私钥结合。结果现在应该在每一方都是相同的;在我们的示例中,是一个形状结合了星形、正方形和三角形(见图 1.7)。

图 1.7 DH 密钥交换的最后一步,两个参与者产生相同的共享密钥。为此,Alice 女王将她的私钥与 Bob 男爵的公钥结合,而 Bob 男爵则将他的私钥与 Alice 女王的公钥结合。仅观察公钥无法获取共享密钥。
现在由协议参与者决定如何使用这个共享密钥。在本书中,你会看到几个示例,但最明显的场景是在需要共享密钥的算法中使用它。例如,Alice 女王和 Bob 男爵现在可以使用共享密钥作为对称加密原语进一步加密消息的密钥。概括一下
-
Alice 和 Bob 交换他们的公钥,这掩盖了他们各自的私钥。
-
使用另一方的公钥和各自的私钥,他们可以计算出一个共享密钥。
-
观察公钥交换的对手没有足够的信息来计算共享密钥。
注意 在我们的示例中,最后一点很容易被绕过。实际上,在没有任何私钥知识的情况下,我们可以将公钥组合在一起生成共享密钥。幸运的是,这只是我们比喻的一个局限性,但它足以帮助我们理解密钥交换的作用。
实际上,DH 密钥交换非常不安全。你能花几秒钟想出为什么吗?
因为 Alice 女王接受她收到的任何公钥都是 Bob 男爵的公钥,所以我可以拦截交换并用我的公钥替换它,这样我就可以冒充 Bob 男爵向 Alice 女王发起攻击(同样也可以对 Bob 男爵进行相同操作)。我们称之为 中间人 (MITM)攻击者可以成功攻击协议。我们如何解决这个问题?在后面的章节中,我们将看到我们要么需要用另一个加密原语增强此协议,要么需要事先知道 Bob 男爵的公钥是什么。但那样的话,我们不是回到了原点吗?
以前,Alice 女王和 Bob 男爵需要知道一个共享密钥;现在 Alice 女王和 Bob 男爵需要知道各自的公钥。他们如何知道呢?这是不是又是一个鸡生蛋蛋生鸡的问题?嗯,有点像。正如我们将看到的,实际上,公钥密码学并不能解决信任问题,但它简化了其建立(特别是当参与者数量很多时)。
让我们暂停一下,继续下一节,因为你将在第五章了解更多关于密钥交换的知识。我们还有一些非对称加密原语需要揭示(见图 1.8),以完成我们对现实世界密码学的概览。

图 1.8 我们到目前为止学到的加密算法。两大类加密算法是对称加密(使用对称加密)和非对称加密(使用密钥交换)。
1.4.2 非对称加密,不像对称加密那样
DH 密钥交换算法的发明很快被 RSA 算法的发明紧随其后,该算法以 Ron Rivest、Adi Shamir 和 Leonard Adleman 命名。RSA 包含两种不同的原语:公钥加密算法(或非对称加密)和(数字)签名方案。这两种原语都是更大类别的加密算法称为非对称加密的一部分。在本节中,我们将解释这些原语的作用以及它们如何有用。
第一个,非对称加密,与我们之前讨论的对称加密算法有类似的目的:它允许加密消息以获得机密性。然而,与对称加密不同,对称加密是完全不同的:
-
它使用两个不同的密钥:一个公钥和一个私钥。
-
它提供了一个不对称的观点:任何人都可以使用公钥加密,但只有私钥的所有者可以解密消息。
现在让我们用一个简单的类比来解释如何使用非对称加密。我们再次从我们的朋友女王爱丽丝开始,她持有一个私钥(及其相关的公钥)。让我们把她的公钥想象成一个她向公众发布供任何人使用的开放箱子(见图 1.9)。

图 1.9 为了使用非对称加密,女王爱丽丝需要先发布她的公钥(这里用一个开放的盒子表示)。现在,任何人都可以使用这个公钥来加密发送消息给她。而且她应该能够使用相关的私钥解密它们。
现在,你、我和每个想要的人都可以使用她的公钥加密一条消息给她。在我们的类比中,想象一下你会把你的消息插入到开放的箱子里,然后关闭它。一旦箱子关闭了,除了女王爱丽丝之外,没有人能够打开它。这个盒子有效地保护了消息的保密性免受观察者的观察。然后,关闭的盒子(或加密内容)可以发送给女王爱丽丝,她可以使用她的私钥(只有她知道的)来解密它们(见图 1.10)。

图 1.10 非对称加密:(1)任何人都可以使用艾丽丝女王的公钥将消息加密给她。(2)接收后,(3)她可以使用相关联的私钥解密内容。在消息发送给艾丽丝女王时,没有人能够观察到。
让我们在图 1.11 中总结到目前为止我们学到的加密原语。我们只需要再学习一个就可以完成我们的真实世界加密之旅了!

图 1.11 到目前为止我们学到的加密算法:两类大型加密算法是对称加密(使用对称加密)和非对称加密(使用密钥交换和非对称加密)。
1.4.3 数字签名,就像你的纸笔签名一样
我们看到 RSA 提供了一种非对称加密算法,但正如我们之前提到的,它也提供了一种数字签名算法。这一数字签名的加密原语的发明在建立我们世界的爱丽丝和鲍勃之间的信任方面帮助巨大。它类似于真实的签名;你知道的,比如当你试图租一间公寓时,你被要求在合同上签字的那种。
“如果他们伪造我的签名怎么办?”你可能会问,实际上,真实的签名在现实世界中并不提供太多安全性。另一方面,加密签名可以以同样的方式使用,但提供带有你名字的加密证书。你的加密签名是无法伪造的,并且可以很容易地被其他人验证。与你过去在支票上写的古老签名相比,非常有用!
在图 1.12 中,我们可以想象一个协议,艾丽丝女王想向大卫勋爵表明她信任鲍勃勋爵。这是一个典型的多参与者环境下建立信任的例子,以及非对称加密如何帮助。通过签署一份包含“我,艾丽丝女王,信任鲍勃勋爵”的文件,艾丽丝女王可以表明立场并通知大卫勋爵要信任鲍勃勋爵。如果大卫勋爵已经信任艾丽丝女王及其签名算法,那么他可以选择相应地信任鲍勃勋爵。

图 1.12 大卫勋爵已经信任艾丽丝女王。因为艾丽丝女王信任鲍勃勋爵,所以大卫勋爵能够安全地信任鲍勃勋爵吗?
更详细地说,艾丽丝女王可以使用 RSA 签名方案和她的私钥签署消息“我,艾丽丝女王,信任鲍勃勋爵”。这将生成一个看起来像随机噪音的签名(见图 1.13)。

图 1.13 要签署一条消息,艾丽丝女王使用她的私钥并生成一个签名。
任何人都可以通过组合验证签名:
-
艾丽丝的公钥
-
签名的消息
-
签名
结果要么是true(签名有效),要么是false(签名无效),如图 1.14 所示。

图 1.14 要验证来自阿丽斯女王的签名,还需要被签名的消息和阿丽斯女王的公钥。结果要么验证签名,要么无效化签名。
我们现在学到了三种不同的非对称基元:
-
与迪菲-赫尔曼(Diffie-Hellman)进行密钥交换
-
非对称加密
-
使用 RSA 进行数字签名
这三种密码算法是非对称加密中最知名和常用的基元。它们如何帮助解决现实问题可能并不完全明显,但请放心,它们每天都被许多应用程序用来保护周围的事物。现在是时候用我们迄今学到的所有密码算法来完整地描绘我们的图景了(见图 1.15)。

图 1.15 我们迄今学到的对称和非对称算法
1.5 对加密进行分类和抽象
在前一节中,我们调查了两类大型算法:
-
对称加密(或秘密密钥加密)—使用单个秘密。如果有多个参与者知道秘密,则称为共享秘密。
-
非对称加密(或公钥加密)—参与者对于密钥有不对称的视角。例如,有些人会知道公钥,而有些人会同时知道公钥和私钥。
对称和非对称加密不是加密中唯一的两类基元,而且很难对不同的子领域进行分类。但是,正如你将意识到的那样,我们书中的很大一部分内容都是关于(并且利用了)对称和非对称基元。这是因为当今加密中有用的很大一部分内容都包含在这些子领域中。另一种划分加密的方式可能是
-
基于数学的构建—这些依赖于数学问题,如分解数字。(RSA 算法用于数字签名和非对称加密就是这种构建的一个例子。)
-
启发式构建—这些依赖于密码分析人员的观察和统计分析。(对称加密的 AES 就是这种构建的一个例子。)
这种分类还涉及速度因素,因为基于数学的构建通常比基于启发式的构建慢得多。给你一个概念,对称构建通常基于启发式(看起来有效的东西),而大多数非对称构建基于数学问题(被认为是困难的问题)。
对我们来说,严格分类密码学所能提供的一切是很困难的。事实上,关于这个主题的每本书或课程都给出了不同的定义和分类。最终,这些区别对我们来说并不太有用,因为我们将看到大多数密码原语都是具有独特 安全声明 的独特工具。我们反过来可以使用这些工具中的许多作为构建协议的基础模块。因此,了解这些工具的每一个是如何工作的,以及它们提供了哪些安全声明,以便理解它们如何保护我们周围的协议,是非常重要的。因此,这本书的第一部分将介绍最有用的密码原语及其安全属性。
书中的很多概念第一次接触时可能会相当复杂。但像一切事物一样,我们越是阅读它们,越是在上下文中看到它们,它们就越自然,我们就能越抽象它们。这本书的作用是帮助你创建抽象,让你建立这些构造所做的事情的心理模型,并理解它们如何组合在一起产生安全协议。我经常会谈论构造的接口,并给出使用和组合的现实示例。
以前,密码学的定义很简单:女王爱丽丝和博伯爵想要交换秘密信息。现在不再是这样了。如今的密码学描述相当复杂,是围绕着发现、突破和实际需求有机发展起来的。归根结底,密码学是帮助增强协议以使其在对抗环境中运行的东西。
要准确理解密码学如何帮助,对我们而言最重要的是这些协议旨在实现的一系列目标。那才是有用的部分。本书中我们将了解的大多数密码原语和协议提供以下一种或两种属性:
-
保密性 —— 它关乎掩盖和保护一些信息免受错误眼睛的侵害。例如,加密掩盖了传输中的消息。
-
认证 —— 它关乎我们正在与谁交谈。例如,这有助于确保我们收到的消息确实来自女王爱丽丝。
当然,这仍然是密码学可以提供的内容的一个重要简化。在大多数情况下,细节都在原语的安全声明中。根据我们在协议中如何使用密码原语,它将实现不同的安全属性。
在本书中,我们将学习新的加密原语以及它们如何结合起来以暴露诸如保密性和认证等安全属性。目前,我们要欣赏的是,密码学是关于在对抗性环境中为协议提供保险的。虽然“对手”并没有明确定义,但我们可以想象他们是试图破坏我们协议的人:参与者、观察者、中间人。他们反映了真实生活中对手可能的情况。因为最终,密码学是一个实践领域,旨在防御血肉和骨头以及位的不良行为者。
1.6 理论密码学与现实世界密码学
1993 年,布鲁斯·施奈尔(Bruce Schneier)发布了《应用密码学》(Wiley),这是一本针对希望构建涉及密码学应用的开发人员和工程师的书籍。大约在 2012 年,肯尼·帕特森(Kenny Paterson)和奈杰尔·斯马特(Nigel Smart)开始了一年一度的名为真实世界密码(Real World Crypto)的会议,针对的是同一群人。但是应用密码学和现实世界密码学是指什么?是否有多种类型的密码学?
要回答这些问题,我们必须从定义理论密码学开始,这是密码学家和密码分析家从事的密码学。这些密码学人大多来自学术界,在大学工作,但有时来自行业或政府的特定部门。他们研究密码学的一切。结果通过国际期刊和会议的出版物和演示分享。然而,他们所做的并不是显然有用或实用的。通常情况下,没有“概念证明”或代码被发布。无论如何,这是毫无意义的,因为没有计算机足够强大来运行他们的研究。话虽如此,理论密码学有时变得如此有用和实用,以至于它会进入另一边。
另一方面是应用密码学或现实世界密码学的世界。它是你周围所有应用程序中发现的安全的基础。虽然它通常看起来好像不存在,几乎是透明的,但当你在互联网上登录银行账户时它就在那里;当你给朋友发消息时它与你同在;当你丢失手机时它帮助保护你。它是无处不在的,因为不幸的是,攻击者无处不在,并积极尝试观察和伤害我们的系统。从业者通常来自行业,但有时会与学术界合作评估算法并设计协议。结果通常通过会议、博客文章和开源软件分享。
现实世界的密码学通常非常关注现实世界的考虑因素:算法提供的确切安全级别是多少?运行算法需要多长时间?原语需要的输入和输出大小是多少?现实世界的密码学就是这本书的主题。虽然理论密码学是其他书的主题,但我们仍将在本书的最后几章中窥探一下那里正在酝酿的东西。准备好被惊讶吧,因为你可能会一瞥到明天的现实世界的密码学。
现在你可能想知道:开发人员和工程师如何选择用于他们的现实世界应用的密码学?
1.7 从理论到实践:选择你自己的冒险
位于顶部的是提出并解决难题的密码分析师[...] 而在底部的是希望加密一些数据的软件工程师。
—Thai Duong(“那么你想自己设计密码学?”,2020)
在我花费在研究和从事密码学的所有年份中,我从未注意到一个密码学原语在实际应用中被使用的单一模式。情况相当混乱。在一个理论原语被采用之前,有一长串人要处理这个原语,并将其塑造成某种可消费的东西,有时对大众更安全。我该如何向你解释呢?
你听说过选择你自己的冒险吗?这是一个旧书系列,你可以选择如何沿着故事前进。原则很简单:你读完书的第一部分;在部分结束时,书让你通过给出不同的选项来决定未来的道路。每个选项都与一个不同的部分号码相关,如果你愿意,可以直接跳转到该部分。所以,在这里我也做了同样的事情!从阅读下一段开始,按照它给出的方向前进。
一切始于此。 你是谁?你是爱丽丝,一位密码学家吗?你是大卫,私营行业工作者,需要解决问题吗?还是你是伊娃,工作在政府部门,忙于密码学?
-
你是爱丽丝,前往步骤 1。
-
你是大卫,前往步骤 2。
-
你是伊娃,前往步骤 3。
步骤 1:研究员得研究。 你是一名在大学工作的研究人员,或者在私营公司或非营利组织的研究团队工作,或者在像 NIST 或 NSA 这样的政府研究机构工作。因此,你的资金可能来自不同的地方,并可能激励你研究不同的东西。
-
你发明了一个新的原语,前往步骤 4。
-
你发明了一种新的结构,前往步骤 5。
-
你开始了一场公开竞赛,前往步骤 6。
第 2 步:行业有需求。 作为你的工作的一部分,出现了一些问题,你需要一个新的标准。例如,Wi-Fi 联盟是由感兴趣的公司资助的非营利组织,制定了围绕 Wi-Fi 协议的一套标准。另一个例子是银行联合起来制定了支付卡行业数据安全标准(PCI-DSS),该标准强制执行处理信用卡号码时要使用的算法和协议。
-
你决定资助一些急需的研究,前往第 1 步。
-
你决定标准化一个新的基元或协议,前往第 5 步。
-
你发起了一场公开竞赛,前往第 6 步。
第 3 步:政府有需求。 你为你国家的政府工作,需要推出一些新的加密。例如,NIST 负责发布联邦信息处理标准(FIPS),规定了哪些加密算法可以被与美国政府打交道的公司使用。虽然许多这些标准都是成功案例,人们倾向于对政府机构推动的标准有很多信任,但(不幸的是)对于失败还有很多话要说。
在 2013 年,根据爱德华·斯诺登的披露,发现 NSA 故意并成功地推动在标准中包含后门算法(参见 Bernstein 等人的“Dual EC: A Standardized Back Door”),其中包括一个隐藏的开关,允许 NSA,仅限 NSA,预测你的秘密。这些后门可以被视为魔法密码,允许政府(仅限政府,据说)颠覆你的加密。在此之后,密码学界对来自政府机构的标准和建议失去了很多信心。最近,在 2019 年,发现俄罗斯标准 GOST 也遭受了同样的对待。
密码学家长期以来一直怀疑,该机构在 2006 年被国家标准技术研究所采纳并后来被国际标准化组织采纳的标准中植入了漏洞,该组织有 163 个成员国。机密的 N.S.A.备忘录似乎证实了这个致命弱点,由 2007 年两名微软密码学家发现,由该机构设计。N.S.A.编写了这一标准,并积极推动国际组织,私下称这一努力为“一项精湛的挑战”。。
—纽约时报(“N.S.A. Able to Foil Basic Safeguards of Privacy on Web,” 2013)
-
你资助了一些研究,前往第 1 步。
-
你组织了一场公开竞赛,前往第 6 步。
-
你推动正在使用的基元或协议的标准化,前往第 7 步。
第 4 步:提出一个新概念。 作为研究人员,你成功做到了不可能的事情;你发明了一个新概念。当然,有人已经想到了加密,但在密码学领域每年仍然会提出一些新的原语。其中一些将被证明无法实现,而一些将最终得以解决。也许你的提议中有一个实际的构造,或者也许你得等待看看是否有人能想出有效的方法。
-
你的原始构造被实施,前往第 5 步。
-
你的原始构造最终无法实施,回到起点。
第 5 步:提出新的构造或协议。 一个密码学家或密码学家团队提出了一个实现概念的新算法。例如,AES 是加密方案的一种实例化。(AES 最初由文森特·瑞曼和约安·达门提出,他们将他们的构造命名为他们的名字的缩写,Rijndael。)接下来呢?
-
有人在你的构造基础上进行改进,前往第 5 步。
-
你参加了一个公开竞赛并赢得了胜利!前往第 6 步。
-
对你的工作有很多炒作;你将获得一个标准!前往第 7 步。
-
你决定对你的构造进行专利申请,前往第 8 步。
-
你或其他人决定实现你的构造会很有趣。前往第 9 步。
第 6 步:一个算法赢得了比赛。 密码学家们最喜欢的过程是一个公开的竞赛!例如,AES 就是一个邀请全球研究者参与的竞赛。经过数十次提交和分析以及密码分析师的帮助(可能需要数年),候选名单被缩减到几个(在 AES 的情况下,只有一个),然后开始标准化过程。
-
你很幸运,在多年的竞争后,你的构造赢得了胜利!前往第 7 步。
-
不幸的是,你失败了。回到起点。
第 7 步:一个算法或协议被标准化。 标准通常由政府或标准化机构发布。目的是确保每个人都在同一页面上,以最大限度地提高互操作性。例如,NIST 定期发布密码学标准。密码学中的一个知名标准化机构是互联网工程任务组(IETF),它是互联网上许多标准(如 TCP、UDP、TLS 等)的背后推动者,在本书中你会经常听到。在 IETF 中,标准称为 请求评论(RFC),几乎任何想要制定标准的人都可以编写。
为了加强我们不投票的观念,我们还采用了“哼声”的传统:例如,当我们面对面开会时,工作组主席想要了解“房间的感觉”时,主席有时会要求每一方对特定问题哼唱,“支持”还是“反对”。
—RFC 7282(《关于 IETF 中的共识和哼声》,2014)
有时,一家公司直接发布一个标准。例如,RSA 安全有限责任公司(由 RSA 算法的创建者资助)发布了一系列 15 个名为 公钥密码学标准(PKCS)的文档,以合法化该公司当时使用的算法和技术。如今,这种情况非常罕见,许多公司通过 IETF 来标准化他们的协议或算法,而不是通过自定义文档。
-
你的算法或协议得到了实现,前往步骤 9。
-
没有人关心你的标准,回到起点。
步骤 8:专利到期。 在密码学中,专利通常意味着没有人会使用该算法。一旦专利过期,重新对原语产生兴趣并不罕见。最著名的例子可能是 Schnorr 签名,它曾是最受欢迎的签名方案之一,直到 Schnorr 本人在 1989 年对算法进行了专利。这导致 NIST 标准化了一个较差的算法,称为数字签名算法(DSA),它成为了当时的首选签名方案,但现在已经不太使用。Schnorr 签名的专利在 2008 年到期,自那时起该算法开始重新受到关注。
-
时间过去太久了,你的算法将永远被遗忘。回到起点。
-
你的构建启发了更多的构建被发明在其之上,前往步骤 5。
-
现在人们想要使用你的构建,但在真正标准化之前不会这样。前往步骤 7。
-
一些开发人员正在实现你的算法!前往步骤 9。
步骤 9:构建或协议被实现。 实现者不仅要解读论文或标准(尽管标准 应该 面向实现者),而且还必须使他们的实现易于使用和安全。这并不总是一件简单的任务,因为在使用密码学的方式上可能会出现许多灾难性的错误。
-
有人决定是时候为这些实现提供一个标准支持了。没有标准支持是令人尴尬的。前往步骤 7。
-
炒作正落在你的加密库头上!前往步骤 10。
步骤 10:开发人员在应用程序中使用协议或原语。 开发人员有一个需求,而你的加密库似乎解决了这个需求 —— 轻而易举!
-
原语解决了需求,但它没有一个标准。不太好。前往步骤 7。
-
我希望这是用我的编程语言编写的。前往步骤 9。
-
我滥用了库或者构建已经损坏。游戏结束。
你成功了!原语实现到真实世界有很多方式。最好的方式涉及多年的分析,一个友好的实现标准和优秀的库。更糟糕的方式涉及一个糟糕的算法和糟糕的实现。在图 1.16 中,我说明了首选路径。

图 1.16 理想的加密算法生命周期始于密码专家在白皮书中实例化一个概念。例如,AES 是对称加密概念的一个实例化(还有许多其他对称加密算法)。然后可以对构造进行标准化:每个人都同意以某种方式实现它以最大程度地实现互操作性。然后通过在不同语言中实现标准来创建支持。
1.8 一个警告
任何人,从最无知的业余爱好者到最优秀的密码学家,都可以创建一个他自己无法破解的算法。
—布鲁斯·施奈尔(“给业余密码设计者的备忘录”,1998 年)
我必须警告您,密码学的艺术很难掌握。一旦您完成了这本书,就假设您可以构建复杂的密码协议是不明智的。这段旅程应该启发您,向您展示可能性,并向您展示事物是如何运作的,但它不会使您成为密码学大师。
本书并非圣杯。事实上,本书的最后几页将带你走过最重要的一课——不要独自踏上真正的冒险。龙可以杀人,你需要一些支持来陪伴你以便击败它们。换句话说,密码学很复杂,而仅凭本书无法让您滥用所学知识。要构建复杂的系统,需要研究自己的行业多年的专家。相反,您将学到的是如何识别何时应使用密码学,或者,如果有什么不对劲,可以使用什么密码原语和协议来解决您面临的问题,以及所有这些密码算法在表面下是如何工作的。既然您已经被警告,请转到下一章。
摘要
-
协议是一个逐步指南,在其中多个参与者尝试实现类似交换保密消息之类的目标。
-
密码学是关于增强协议以在对抗性环境中保护它们。它通常需要秘密。
-
密码原语是一种密码算法类型。例如,对称加密是一种密码原语,而 AES 是一种特定的对称加密算法。
-
对不同的密码原语进行分类的一种方法是将它们分为两种类型:对称和非对称密码学。对称密码学使用单一密钥(正如您在对称加密中所看到的),而非对称密码学则使用不同的密钥(正如您在密钥交换、非对称加密和数字签名中所看到的)。
-
密码学属性难以分类,但它们通常旨在提供以下两种属性之一:身份验证或保密性。身份验证涉及验证某物或某人的真实性,而保密性涉及数据或身份的隐私。
-
现实世界的密码学很重要,因为它在技术应用中无处不在,而理论密码学在实践中通常不那么有用。
-
本书中包含的大多数密码学基元是经过长时间的标准化过程达成一致的。
-
密码学很复杂,在实施或使用密码学基元时存在许多危险。
第二章:哈希函数
本章内容包括
-
哈希函数及其安全属性
-
当今广泛采用的哈希函数
-
存在的其他类型的哈希函数
将全局唯一标识符分配给任何东西,这就是您将在本章中学到的第一个密码构造的承诺——哈希函数。哈希函数在密码学中随处可见——随处可见!非正式地说,它们以您希望的任何数据作为输入,并返回一个唯一的字节串。给定相同的输入,哈希函数始终会产生相同的字节串。这可能看起来像无足轻重的事情,但这个简单的构造在构建密码学中的许多其他构造时非常有用。在本章中,您将学到有关哈希函数的一切以及它们为什么如此多才多艺。
2.1 什么是哈希函数?
在你面前,一个下载按钮占据了页面的很大一部分。您可以读到字母DOWNLOAD,点击这个似乎会将您重定向到包含文件的不同网站。下面是一长串难以理解的字母:
f63e68ac0bf052ae923c03f5b12aedc6cca49874c1c9b0ccf3f39b662d1f487b
紧接着是一个看起来像是某种首字母缩写的东西:sha256sum。听起来耳熟吗?你可能在以前的生活中下载过一些伴随着这样奇怪字符串的东西(见图 2.1)。

图 2.1 一个链接到包含文件的外部网站的网页。外部网站无法修改文件的内容,因为第一页提供了文件的哈希或摘要,这确保了下载的文件的完整性。
如果您曾经想知道那串长字符串要做什么:
-
点击按钮下载文件
-
使用 SHA-256 算法对下载的文件进行哈希处理
-
将输出(摘要)与网页上显示的长字符串进行比较
这使您能够验证您下载了正确的文件。
注 哈希函数的输出通常称为摘要或HASH。我在本书中交替使用这两个词。其他人可能称其为校验和或和,但我避免使用这些术语,因为这些术语主要用于非密码哈希函数,可能会导致更多的混淆。当不同的代码库或文档使用不同的术语时,请记住这一点。
要尝试对某些东西进行哈希处理,您可以使用流行的 OpenSSL 库。它提供了一个多功能的命令行界面(CLI),在包括 macOS 在内的许多系统中默认提供。例如,可以通过打开终端并输入以下命令来完成这项工作:
$ openssl dgst -sha256 downloaded_file
f63e68ac0bf052ae923c03f5b12aedc6cca49874c1c9b0ccf3f39b662d1f487b
通过该命令,我们使用了 SHA-256 哈希函数将输入(下载的文件)转换为一个唯一标识符(命令所回显的值)。这些额外的步骤提供了什么?它们提供了完整性和真实性。它告诉你,你下载的确实是你打算下载的文件。
所有这些都是由哈希函数的一个安全属性实现的,称为第二原像抗性。这个数学启发的术语意味着从哈希函数的长输出中,f63e...,你不能找到另一个会散列到相同输出f63e....的文件。在实践中,这意味着这个摘要与您正在下载的文件紧密相关,并且没有攻击者应该能够通过给您不同的文件来愚弄您。
十六进制表示
顺便说一句,长输出字符串f63e...表示十六进制中显示的二进制数据(一种基于 16 的编码,使用数字 0 到 9 和字母 a 到 f 来表示几位数据)。我们可以用 0 和 1(基数 2)来显示二进制数据,但这将占用更多的空间。相反,十六进制编码允许我们为每 8 位(1 字节)遇到的数据写 2 个字母数字字符。它对人类来说有点可读,并且占用更少的空间。有其他方式来为人类消费编码二进制数据,但最广泛使用的两种编码是十六进制和 base64。基数越大,显示二进制字符串所需的空间就越少,但在某些时候,我们会用完人类可读的字符。
请注意,这个长摘要由网页的所有者(们)控制,任何可以修改网页的人都可以轻松地替换它。(如果你还不相信,请花点时间考虑一下。)这意味着我们需要信任给我们摘要的页面,它的所有者以及用于检索页面的机制(尽管我们不需要信任给我们下载文件的页面)。在这个意义上,哈希函数本身并不提供完整性。下载文件的完整性和真实性来自于与给出摘要的受信任机制的摘要结合起来(在本例中是 HTTPS)。我们将在第九章讨论 HTTPS,但现在,想象它神奇地允许你与网站安全通信。
回到我们的哈希函数,它可以被可视化为图 2.2 中的黑匣子。我们的黑匣子接受单个输入并产生单个输出。

图 2.2 哈希函数接受任意长度的输入(文件、消息、视频等)并产生固定长度的输出(例如,SHA-256 的 256 位)。对相同输入进行哈希处理会产生相同的摘要或哈希。
这个函数的输入可以是任意大小。它甚至可以是空的。输出始终具有相同的长度和确定性:如果给定相同的输入,它总是产生相同的结果。在我们的例子中,SHA-256 始终提供 256 位(32 字节)的输出,始终以十六进制的 64 个字母数字字符编码。哈希函数的一个主要特性是不能逆转算法,这意味着不能仅凭输出就找到输入。我们说哈希函数是单向的。
为了说明哈希函数在实践中是如何工作的,我们将使用相同的 OpenSSL CLI 对不同的输入使用 SHA-256 哈希函数进行哈希。以下终端会话显示了这一点。
$ echo -n "hello" | openssl dgst -sha256
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
$ echo -n "hello" | openssl dgst -sha256 // ❶
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
$ echo -n "hella" | openssl dgst -sha256 // ❷
70de66401b1399d79b843521ee726dcec1e9a8cb5708ec1520f1f3bb4b1dd984
$ echo -n "this is a very very very very very very // ❸
➥ very very very long sentence" | openssl dgst -sha256 // ❸
1166e94d8c45fd8b269ae9451c51547dddec4fc09a91f15a9e27b14afee30006
❶ 对相同输入进行哈希会产生相同的结果。
❷ 输入中的微小变化会完全改变输出。
❸ 输出始终相同大小,无论输入大小如何。
在下一节中,我们将看到哈希函数的确切安全性质是什么。
2.2 哈希函数的安全性质
应用密码学中的哈希函数是常见的构造,通常被定义为提供三种特定安全性质。随着我们将在接下来的部分中看到的,这个定义随着时间的推移而改变。但是现在,让我们定义构成哈希函数的三个强基础。这很重要,因为您需要了解哈希函数可以在哪里有用,以及它们不起作用的地方。
第一个是原像抗性。这个属性确保没有人应该能够反转哈希函数以恢复给定输出的输入。在图 2.3 中,我们通过想象我们的哈希函数就像一个搅拌机,使得从制作的冰沙中恢复原料变得不可能。

图 2.3 给定哈希函数产生的摘要(这里表示为搅拌机),逆转它并找到使用的原始输入是不可能的(或者在技术上如此困难,我们假设永远不会发生)。这种安全性质被称为原像抗性。
警告 如果您的输入很小,这是真的吗?假设它是oui或non,那么有人很容易对所有可能的 3 个字母单词进行哈希,找出输入是什么。如果您的输入空间很小呢?意味着您总是对句子的变体进行哈希,例如,“我将在星期一凌晨 3 点回家。”在这种情况下,一个人可以预测这一点,但不知道确切的星期几或小时,仍然可以对所有可能的句子进行哈希,直到产生正确的输出。因此,这个第一个原像安全性质有一个明显的警告:您无法隐藏太小或可预测的东西。
第二个属性是第二原像抗性。当我们想要保护文件的完整性时,我们已经看到了这个安全性质。该属性表明:如果我给你一个输入和它哈希到的摘要,你不应该能够找到一个不同的输入,其哈希到相同的摘要。图 2.4 说明了这个原则。

图 2.4 考虑一个输入及其相关的摘要,一个人永远不应该能够找到一个不同的输入,其哈希值相同。这种安全性质被称为第二原像抗性。
请注意我们无法控制第一个输入。这种强调对于理解哈希函数的下一个安全性质非常重要。
最后,第三个属性是碰撞抗性。它保证没有人能够产生两个不同的输入,其哈希为相同的输出(如图 2.5 所示)。在这里,攻击者可以选择两个输入,不像前一个属性固定了其中一个输入。

图 2.5 永远不应该找到两个输入(在左侧表示为两个随机数据块)哈希为相同的输出值(在右侧)。这种安全属性称为碰撞抗性。
人们经常混淆碰撞抗性和第二原像抗性。花点时间理解它们的区别。
随机预言机
另外,哈希函数通常设计成它们的摘要是不可预测和随机的。这是有用的,因为人们并不总是能够证明一个协议是安全的,多亏了我们讨论过的哈希函数的安全属性之一(比如碰撞抗性,例如)。许多协议实际上是在随机预言机模型中被证明的,在这种协议中,一个名为随机预言机的虚构和理想的参与者被使用。在这种类型的协议中,可以将任何输入作为请求发送给该随机预言机,据说它以完全随机的输出作为响应返回,并且像哈希函数一样,给它相同的输入两次会返回相同的输出两次。
在这个模型中的证明有时是有争议的,因为我们不确定我们是否可以用真实的哈希函数(在实践中)替换这些随机预言机。然而,许多合法的协议是通过这种方法被证明是安全的,其中哈希函数被视为比它们实际上更理想的东西。
2.3 哈希函数的安全考虑
到目前为止,我们看到了哈希函数的三个安全属性:
-
原像抗性
-
第二原像抗性
-
碰撞抗性
这些安全属性通常单独来看毫无意义;一切都取决于你如何使用哈希函数。尽管如此,重要的是在我们看一些现实世界的哈希函数之前,我们要理解这里的一些限制。
首先,这些安全属性假定你(合理地)使用哈希函数。想象一下,我要么对单词yes进行哈希,要么对单词no进行哈希,然后我发布摘要。如果你对我在做什么有一些想法,你可以简单地对这两个词进行哈希,并将结果与我给你的结果进行比较。因为没有涉及到任何秘密,而且我们使用的哈希算法是公开的,所以你可以这样做。事实上,你可能会认为这会破坏哈希函数的前像抗性,但我会认为你的输入不够“随机”。此外,由于哈希函数接受任意长度的输入并始终产生相同长度的输出,也有无限数量的输入哈希到相同的输出。同样,你可能会说,“嗯,这不是在破坏第二前像抗性吗?”第二前像抗性仅仅是说找到另一个输入是极其困难的,困难到我们认为在实践中是不可能的,但在理论上是可能的。
其次,摘要的大小确实很重要。这并不是哈希函数的奇特之处。在实践中,所有的加密算法都必须关心它们参数的大小。让我们想象一下以下极端情况。我们有一个哈希函数,它以均匀随机的方式生成长度为 2 位的输出(意味着它将00作为输出的概率为 25%,01为 25%,以此类推)。你不需要做太多工作就能产生碰撞:在对几个随机输入字符串进行哈希之后,你应该能够找到两个哈希到相同输出的字符串。因此,哈希函数在实践中必须产生的最小输出大小是 256 位(或 32 字节)。有了这么大的输出,除非计算方面发生突破,否则碰撞应该是不可能的。
这个数字是怎么得到的呢?在实际的密码学中,算法的目标是提供至少 128 位的安全性。这意味着一个想要攻破算法(提供 128 位安全性)的攻击者必须执行大约 2128 次操作(例如,尝试所有长度为 128 位的可能输入字符串将需要 2128 次操作)。为了让哈希函数提供前面提到的所有三个安全属性,它需要提供至少 128 位的安全性来抵御所有三种攻击。通常,最简单的攻击是由于生日悖论导致的碰撞查找。
生日悖论
生日悖论根源于概率论,其中生日问题揭示了一些不直观的结果。在一个房间里,至少需要多少人才能有至少 50%的机会两人拥有相同的生日(即发生碰撞)。事实证明,随机选择的 23 人就足够达到这个概率!很奇怪,对吧?
这被称为 生日悖论。实际上,当我们从 2^N 个可能性中随机生成字符串时,你可以期望有 50% 的概率在生成约 2^(N/2) 个字符串后发现冲突。
如果我们的哈希函数生成了 256 位的随机输出,所有输出的空间大小为 2²⁵⁶。这意味着在生成了 2¹²⁸ 个摘要之后,可以以较高的概率找到冲突(由于生日悖论)。这是我们的目标数字,这也是哈希函数最少必须提供 256 位输出的原因。
有时某些约束会迫使开发人员通过 截断(移除其中的一些字节)来减小摘要的大小。理论上,这是可能的,但会大大降低安全性。为了至少实现 128 位安全性,摘要不得被截断为:
-
256 位用于防冲突
-
128 位用于前像和第二前像防护
这意味着根据依赖的属性,哈希函数的输出可以被截断以获得更短的摘要。
2.4 实践中的哈希函数
正如我们之前所说的,在实践中,哈希函数很少单独使用。它们通常与其他元素结合在一起,以创建密码原语或密码协议。在本书中,我们将看到许多使用哈希函数构建更复杂对象的例子,但本节描述了在现实世界中哈希函数的几种不同用法。
2.4.1 承诺
想象一下,你知道市场上的一只股票将会增值并在未来一个月达到 50 美元,但由于某种法律原因,你真的不能告诉你的朋友(也许是出于某种法律原因)。尽管如此,你仍然希望能够事后告诉你的朋友你知道这件事,因为你自以为是(不要否认)。你可以做的是承诺一句话,如“股票 X 下个月将达到 50 美元。” 为此,将该句话哈希,并将输出给你的朋友。一个月后,揭示这句话。你的朋友将能够哈希这句话以观察到确实产生了相同的输出。
这就是我们所说的 承诺方案。密码学中的承诺通常试图实现两个属性:
-
隐藏 —— 一个承诺必须隐藏底层值。
-
绑定 —— 一个承诺必须隐藏一个单一值。换句话说,如果你承诺一个值 x,你不应该能够后来成功地透露一个不同的值 y。
练习
如果将哈希函数用作承诺方案,你能否判断它是否提供了隐藏和绑定?
2.4.2 子资源完整性
有时(经常)网页会导入外部 JavaScript 文件。例如,很多网站使用内容传送网络(CDN)在其页面中导入 JavaScript 库或与 web 框架相关的文件。这些 CDN 被放置在战略位置,以便快速向访问者传递这些文件。然而,如果 CDN 走向歧途并决定提供恶意的 JavaScript 文件,这可能是一个真正的问题。为了应对这种情况,网页可以使用一个名为子资源完整性的功能,允许在导入标签中包含摘要:
<script src="https://code.jquery.com/jquery-2.1.4.min.js"
integrity="sha256-8WqyJLuWKRBVhxXIL1jBDD7SDxU936oZkCnxQbWwJVw="></script>
这恰好是我们在本章开头讨论过的情景。一旦检索到 JavaScript 文件,浏览器对其进行哈希处理(使用 SHA-256),并验证其是否与页面中硬编码的摘要相对应。如果验证通过,JavaScript 文件将被执行,因为其完整性已经得到验证。
2.4.3 BitTorrent
世界各地的用户(称为对等体)使用 BitTorrent 协议直接在彼此之间共享文件(我们也称之为点对点)。为了分发一个文件,它被切成块,每个块都被单独散列。然后这些哈希值作为信任源被共享以代表要下载的文件。
BitTorrent 有几种机制允许对等体从不同的对等体获取文件的不同块。最后,通过对下载的每个块进行哈希处理并将输出与各自已知的摘要(在重新组装文件之前)匹配来验证整个文件的完整性。例如,以下“磁铁链接”代表 Ubuntu 操作系统,v19.04。它是通过对文件的元数据以及所有块的摘要进行哈希处理而获得的摘要(以十六进制表示)。
magnet:?xt=urn:btih:b7b0fbab74a85d4ac170662c645982a862826455
2.4.4 Tor
Tor 浏览器的目标是让个人能够匿名浏览互联网。另一个特性是,可以创建隐藏的网页,其物理位置难以追踪。与这些页面的连接通过使用网页的公钥进行保护的协议来保护。(我们将在第九章讨论会话加密时详细了解其工作原理。)例如,Silk Road 曾是毒品的 eBay,直到被 FBI 查封,可以通过 Tor 浏览器中的silkroad6ownowfk .onion访问。这个 base32 字符串实际上代表了 Silk Road 的公钥的哈希值。因此,通过知道洋葱地址,你可以验证你正在访问的隐藏网页的公钥,并确保你正在与正确的页面交流(而不是冒充者)。如果这不清楚,不用担心,我会在第九章再次提到这一点。
练习
顺便说一句,这个字符串不可能代表 256 位(32 字节),对吗?那么根据你在第 2.3 节学到的内容,这样是安全的吗?另外,你能猜到 Dread Pirate Roberts(Silk Road 的网站管理员的化名)是如何获得一个包含网站名称的哈希值的吗?
在本节的所有示例中,哈希函数提供了内容完整性或真实性,用于以下情况:
-
有人可能会篡改被哈希的内容。
-
哈希已经安全地传达给你。
我们有时也会说我们认证某物或某人。重要的是要理解,如果哈希不是安全地获取的,那么任何人都可以用其他内容的哈希替换它!因此,它本身不提供完整性。下一章关于消息认证码将通过引入密钥来修复这一点。现在让我们看看你可以使用哪些实际的哈希函数算法。
2.5 标准化的哈希函数
在我们之前的示例中提到了 SHA-256,这只是我们可以使用的哈希函数之一。在我们继续列出我们这个时代推荐的哈希函数之前,让我们先提到其他在实际应用中人们使用但不被视为加密哈希函数的算法。
首先,像 CRC32 这样的函数不是加密哈希函数,而是错误检测代码函数。虽然它们有助于检测一些简单的错误,但它们没有提供先前提到的任何安全属性,并且不应与我们正在讨论的哈希函数混淆(尽管它们有时可能会共享名称)。它们的输出通常称为校验和。
其次,像 MD5 和 SHA-1 这样的流行哈希函数如今被认为是不安全的。尽管它们曾经是 20 世纪 90 年代的标准和广泛接受的哈希函数,但 MD5 和 SHA-1 在 2004 年和 2016 年被不同研究团队发布的碰撞攻击显示出是不安全的。这些攻击部分成功是因为计算机技术的进步,但主要是因为在哈希函数设计中发现了缺陷。
废弃是困难的
直到研究人员展示了它们缺乏抵抗碰撞的能力之前,MD5 和 SHA-1 都被认为是良好的哈希函数。尽管如今它们的原像和第二原像抵抗力尚未受到任何攻击的影响,但这对我们并不重要,因为我们只想在本书中谈论安全算法。尽管如此,你仍然会看到一些人在只依赖这些算法的原像抵抗力而不依赖它们的碰撞抵抗力的系统中使用 MD5 和 SHA-1。这些人经常争辩说,由于遗留和向后兼容性原因,他们无法将哈希函数升级为更安全的函数。由于本书意在长期存在并成为真实世界密码学未来的一束明亮光芒,这将是我最后一次提到这些哈希函数。
接下来的两个小节介绍了 SHA-2 和 SHA-3,这是两个最广泛使用的哈希函数。图 2.6 介绍了这些函数。

图 2.6 SHA-2 和 SHA-3,两种最广泛采用的哈希函数。SHA-2 基于 Merkle–Damgård 构造,而 SHA-3 基于海绵构造。
2.5.1 SHA-2 哈希函数
现在我们已经了解了哈希函数是什么,也瞥见了它们的潜在用途,接下来需要看看在实践中我们可以使用哪些哈希函数。在接下来的两节中,我介绍了两种广泛接受的哈希函数,并且从内部给出了它们工作的高级解释。这些高级解释不应该提供关于如何使用哈希函数的更深入见解,因为我给出的黑盒描述应该足够了。但尽管如此,了解这些加密原语是如何由密码学家设计的还是很有趣的。
最广泛采用的哈希函数是 安全哈希算法 2(SHA-2)。SHA-2 由 NSA 发明,并于 2001 年由 NIST 标准化。它旨在添加到 NIST 已经标准化的老化的安全哈希算法 1(SHA-1)中。SHA-2 提供了 4 个不同的版本,分别产生输出长度为 224、256、384 或 512 位。它们各自的名称省略了算法的版本:SHA-224、SHA-256、SHA-384 和 SHA-512。此外,另外两个版本,SHA-512/224 和 SHA-512/256,通过截断 SHA-512 的结果分别提供了 224 位和 256 位的输出。
在以下的终端会话中,我们使用 OpenSSL CLI 调用了 SHA-2 的各个变种。请注意,使用相同的输入调用不同的变种会产生完全不同长度的输出。
$ echo -n "hello world" | openssl dgst -sha224
2f05477fc24bb4faefd86517156dafdecec45b8ad3cf2522a563582b
$ echo -n "hello world" | openssl dgst -sha256
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
$ echo -n "hello world" | openssl dgst -sha384
fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3
➥ e417cb71ce646efd0819dd8c088de1bd
$ echo -n "hello world" | openssl dgst -sha512
309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f
➥ 989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f
现在,人们主要使用 SHA-256,它提供了我们三个安全性属性所需的最低 128 位安全性,而更多防范性的应用则使用 SHA-512。现在,让我们看一下 SHA-2 如何工作的简化解释。
异或运算
要理解接下来的内容,你需要理解 XOR(异或)操作。XOR 是位操作,意味着它在位上操作。下图显示了它的工作原理。XOR 在密码学中无处不在,所以确保你记住它。

异或(Exclusive OR 或 XOR,通常表示为 ⊕)操作作用于 2 位。它类似于 OR 操作,但当两个操作数都是 1 时不同。
一切都始于一个称为 压缩函数 的特殊函数。压缩函数接受某些大小的两个输入,并产生一个输入大小的输出。简而言之,它接受一些数据并返回较少的数据。图 2.7 说明了这一点。

图 2.7 压缩函数接受两个不同大小的输入 X 和 Y(这里都是 16 字节),并返回一个大小为 X 或 Y 的输出。
虽然构建压缩函数有不同的方法,但 SHA-2 使用了 Davies–Meyer 方法(见图 2.8),它依赖于一个 分组密码(可以加密固定大小的数据块的密码)。我在第一章中提到了 AES 分组密码,但你还没有学习过它。目前,接受压缩函数是一个黑盒,直到你在第四章中学习了认证加密。

图 2.8 展示了通过戴维斯-迈耶构造构建的压缩函数的示意图。压缩函数的第一个输入(输入块)用作块密码的密钥。第二个输入(中间值)用作要由块密码加密的输入。然后,它再次通过与块密码的输出进行异或来使用自身。
SHA-2 是一种默克尔-达姆高构造,这是一种通过迭代调用这样的压缩函数对消息进行哈希的算法(由拉尔夫·默克尔和伊万·达姆高独立发明)。具体而言,它通过以下两个步骤进行。
首先,它对我们要哈希的输入应用填充,然后将输入切成可以适应压缩函数的块。填充意味着向输入附加特定字节,以使其长度成为某个块大小的倍数。将填充后的输入切成相同块大小的块使我们能够将这些块放入压缩函数的第一个参数中。例如,SHA-256 的块大小为 512 位。图 2.9 说明了这一步骤。

图 2.9 默克尔-达姆高构造的第一步是向输入消息添加一些填充。此步骤完成后,输入长度应为所使用压缩函数的输入大小的倍数(例如,8 字节)。为此,我们在末尾添加 5 字节填充使其为 32 字节。然后,我们将消息分割成 4 个 8 字节的块。
其次,它迭代地将压缩函数应用于消息块,使用前一个压缩函数的输出作为压缩函数的第二个参数。最终输出是摘要。图 2.10 说明了这一步骤。

图 2.10 默克尔-达姆高构造迭代地对要散列的每个输入块和上一个压缩函数的输出应用压缩函数。对压缩函数的最终调用直接返回摘要。
这就是 SHA-2 的工作原理,通过迭代调用其压缩函数对输入的片段进行处理,直到全部处理为最终摘要。
注意,如果压缩函数本身是证明碰撞抵抗的,那么默克尔-达姆高构造就被证明是抗碰撞的。因此,任意长度输入的哈希函数的安全性降低到一个固定大小的压缩函数的安全性,这更容易设计和分析。这就是默克尔-达姆高构造的巧妙之处。
起初,压缩函数的第二个参数通常被固定和标准化为“无暗藏的”值。具体来说,SHA-256 使用第一个质数的平方根来导出这个值。无暗藏值的目的是让密码学界相信它不是为了使哈希函数更弱(例如,为了创建后门)而选择的。这是密码学中的一个流行概念。
警告:虽然 SHA-2 是一个完全可以使用的哈希函数,但不适合用于哈希秘密信息。这是因为 Merkle–Damgård 结构的一个缺点,使得 SHA-2 在处理秘密信息时容易受到攻击(称为长度扩展攻击)。我们将在下一章节中更详细地讨论这个问题。
2.5.2 SHA-3 哈希函数
正如我之前提到的,最近一段时间,MD5 和 SHA-1 哈希函数都被破解了。这两个函数都使用了我在前一节中描述的相同的 Merkle–Damgård 结构。因此,由于 SHA-2 容易受到长度扩展攻击的影响,NIST 于 2007 年决定组织一个新标准的公开竞赛:SHA-3。本节介绍了这个更新的标准,并试图对其内部工作原理进行高层次的解释。
2007 年,来自不同国际研究团队的 64 个不同候选者参加了 SHA-3 竞赛。五年后,其中一个提交的算法 Keccak 被提名为获胜者,并取名为 SHA-3。2015 年,SHA-3 被标准化为 FIPS 出版物 202(nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf)。
SHA-3 遵循我们之前讨论的三个安全属性,并提供与 SHA-2 变种一样多的安全性。此外,它不容易受到长度扩展攻击,并且可以用于哈希秘密信息。因此,现在推荐使用它作为哈希函数。它提供与 SHA-2 相同的变种,这次在它们的命名变种中标明了全名 SHA-3:SHA-3-224、SHA-3-256、SHA-3-384 和 SHA-3-512。因此,类似于 SHA-2,例如,SHA-3-256 提供 256 位的输出。现在让我花几页的篇幅来解释 SHA-3 的工作原理。
SHA-3 是建立在置换之上的密码算法。理解置换的最简单方法是想象以下:你在左边有一组元素,在右边也有同样的一组元素。现在画箭头从左边的每个元素指向右边。每个元素只能有一个从它开始并结束的箭头。现在你有了一个置换。图 2.11 说明了这个原理。根据定义,任何置换也是可逆的,这意味着从输出我们可以找到输入。

图 2.11 一个作用在四种不同形状上的示例置换。您可以使用中间图片中描述的置换来转换给定的形状。
SHA-3 是建立在海绵构造上的,这是 SHA-3 竞赛中发明的一种不同于 Merkle–Damgård 的构造。它基于一个特定的置换称为keccak-f,它接受一个输入并返回相同大小的输出。
注意 我们不会解释 keccak-f 是如何设计的,但是你将在第四章中对此有一个概念,因为它与 AES 算法实质上是相似的(除了它没有密钥)。这不是偶然的,因为 AES 的发明者之一也是 SHA-3 的发明者之一。
在接下来的几页中,我将使用一个 8 位排列来说明海绵结构的工作原理。因为排列已经固定,你可以想象图 2.12 很好地说明了这个排列在所有可能的 8 位输入上创建的映射。与我们之前对排列的解释相比,你也可以想象每个可能的 8 位字符串是我们所代表的不同形状(000...是一个三角形,100...是一个正方形,等等)。

图 2.12 一个海绵结构利用了一个指定的排列 f。通过作用于输入,我们的示例排列创建了一个映射,将所有可能的 8 位输入和所有可能的 8 位输出联系起来。
要在我们的海绵结构中使用一个排列,我们还需要定义一个将输入和输出分成 速率 和 容量 的任意划分。这有点奇怪,但请坚持。图 2.13 说明了这个过程。

图 2.13 排列 f 将一个大小为 8 位的输入随机化为一个相同大小的输出。在海绵结构中,这个排列的输入和输出被分成两部分:速率(大小为 r)和容量(大小为 c)。
我们设置速率和容量之间的限制是任意的。不同版本的 SHA-3 使用不同的参数。我们非正式地指出容量应该像一个秘密一样对待,而且容量越大,海绵结构就越安全。
现在,像所有良好的哈希函数一样,我们需要能够哈希一些东西,对吧?否则,它有点没用。为了做到这一点,我们简单地将输入与排列的输入速率进行异或(⊕)。起初,这只是一堆 0。正如我们之前指出的,容量被视为一个秘密,所以我们不会与之进行任何异或运算。图 2.14 说明了这一点。

图 2.14 要吸收输入的 5 位 00101,一个速率为 5 位的海绵结构可以简单地将这 5 位与速率进行异或(其初始化为 0)。然后置换混淆状态。
现在获得的输出看起来应该是随机的(尽管我们可以轻松地找到输入,因为按照定义排列是可逆的)。如果我们想要输入更大的输入呢?嗯,类似于我们对 SHA-2 所做的,我们会
-
如果需要,填充输入,然后将输入分成速率大小的块。
-
迭代地调用排列,同时对每个块与排列的输入进行异或,并在每个块与排列的输入进行异或后对 状态(上次操作输出的中间值)进行排列。
为了简化起见,我在其余的解释中忽略了填充,但填充是区分诸如0和00之类的输入的重要步骤。图 2.15 展示了这两个步骤。

图 2.15 为了吸收比速率大小更大的输入,海绵构造会迭代地对输入块与速率进行异或运算,并对结果进行排列。
到目前为止一切顺利,但我们还没有生成摘要。为了做到这一点,我们可以简单地使用海绵的最后状态的速率(再次强调,我们不会触及容量)。要获得更长的摘要,我们可以继续对状态的速率部分进行排列和读取,正如图 2.16 所示。

图 2.16 为了使用海绵构造获取摘要,需要迭代地排列状态并检索尽可能多的速率(状态的上部)。
这就是 SHA-3 的工作原理。因为它是一个海绵构造,所以摄取输入自然被称为吸收,创建摘要被称为挤压。海绵采用一个 1,600 比特的置换,根据 SHA-3 不同版本广告的安全性,使用不同的r和c值进行规定。
SHA-3 是一个随机神谕
我之前提到过随机神谕:这是一个理想的虚构构造,对查询返回完全随机的响应,并且如果我们用相同的输入两次查询它,它会重复自己。事实证明,只要构造所使用的置换看起来足够随机,海绵构造的行为就会接近随机神谕。我们如何证明这种置换的安全性质?我们最好的方法是尝试多次破解它,直到我们对其设计有了强烈的信心(这就是 SHA-3 竞赛期间发生的情况)。SHA-3 可以被建模为随机神谕的事实立即赋予了它我们期望从哈希函数得到的安全属性。
2.5.3 SHAKE 和 cSHAKE:两个可扩展输出函数(XOF)
我介绍了两个主要的哈希函数标准:SHA-2 和 SHA-3。这些都是明确定义的哈希函数,可以接受任意长度的输入,并产生看起来随机而固定长度的输出。正如您将在后面的章节中看到的,加密协议经常需要这种类型的原语,但不希望受到哈希函数摘要的固定大小的限制。因此,SHA-3 标准引入了一个更具多功能性的原语,称为可扩展输出函数或XOF(发音为“zoff”)。本节介绍了两个标准化的 XOF:SHAKE 和 cSHAKE。
SHAKE,在 FIPS 202 中规定,可以看作是返回任意长度输出的哈希函数。SHAKE 基本上与 SHA-3 相同的构造,只是它更快,并在挤压阶段中可以排列任意数量的排列。生成不同大小的输出非常有用,不仅用于创建摘要,还用于创建随机数,派生密钥等。我将在本书中再次讨论 SHAKE 的不同应用;现在,想象一下 SHAKE 就像 SHA-3,只是它提供任何你想要的长度的输出。
这种构造在密码学中非常有用,以至于在 SHA-3 标准化一年后,NIST 发布了其特殊出版物 800-185,其中包含了一个称为cSHAKE的可定制 SHAKE。cSHAKE 与 SHAKE 几乎完全相同,只是它还接受一个自定义字符串。这个自定义字符串可以为空,也可以是任何您想要的字符串。让我们首先看一个在伪代码中使用 cSHAKE 的示例:
cSHAKE(input="hello world", output_length=256, custom_string="my_hash")
-> 72444fde79690f0cac19e866d7e6505c
cSHAKE(input="hello world", output_length=256, custom_string="your_hash")
-> 688a49e8c2a1e1ab4e78f887c1c73957
正如您所看到的,尽管 cSHAKE 与 SHAKE 和 SHA-3 一样确定性,但两个摘要不同。这是因为使用了不同的自定义字符串。自定义字符串允许您自定义您的 XOF!这在一些协议中非常有用,例如,必须使用不同的哈希函数才能使证明有效。我们称之为领域分离。
作为密码学中的黄金法则:如果相同的密码原语用于不同的用例,不要使用相同的密钥(如果需要密钥)和/或应用领域分离。在后续章节中,我们将在调查密码协议时看到更多领域分离的示例。
警告 NIST 倾向于指定以比特为单位而不是字节的参数算法。在示例中,请求了 256 比特的长度。想象一下,如果您请求了 16 字节的长度,却得到了 2 字节,因为程序认为您请求了 16 比特的输出。这个问题有时被称为比特攻击。
与密码学中的一切一样,像密钥、参数和输出这样的密码字符串的长度与系统的安全性密切相关。重要的是不要从 SHAKE 或 cSHAKE 请求太短的输出。使用 256 比特的输出永远不会错,因为它提供了 128 比特的安全性,可以抵御碰撞攻击。但是,现实世界的密码学有时在可能使用较短的密码值的受限环境中运行。如果系统的安全性经过仔细分析,可以这样做。例如,如果协议中不需要碰撞抵抗,只需要从 SHAKE 或 cSHAKE 获得 128 比特长的输出。
2.5.4 避免使用 TupleHash 进行模糊哈希
在本章中,我已经讨论了不同类型的密码原语和密码算法。这包括
-
SHA-2 哈希函数,容易受到长度扩展攻击的攻击,但在没有秘密被哈希时仍然广泛使用
-
SHA-3 哈希函数,现在是推荐使用的哈希函数
-
SHAKE 和 cSHAKE XOFs,比哈希函数更多变的工具,因为它们提供可变的输出长度
我将讨论另一个方便的函数,TupleHash,它基于 cSHAKE 并在与 cSHAKE 相同的标准中指定。TupleHash 是一个有趣的函数,它允许对元组(一些东西的列表)进行哈希。为了解释 TupleHash 是什么以及它为什么有用,让我给你讲个故事。
几年前,作为工作的一部分,我被委派审查一个加密货币。它包括一个加密货币应有的基本功能:账户、支付等。用户之间的交易将包含有关谁向谁发送多少的元数据。它还包括一笔小费用以补偿网络处理交易所需的成本。
例如,艾丽斯可以向网络发送交易,但要让它们被接受,她需要包含证明该交易来自她的证据。为此,她可以对交易进行哈希并签名(我在第一章中给出了一个类似的例子)。任何人都可以对交易进行哈希并验证哈希上的签名,以查看这是否是艾丽斯打算发送的交易。图 2.17 说明了一个中间人(MITM)攻击者在交易到达网络之前截取交易后,将无法篡改交易。这是因为哈希将会改变,然后签名将不会验证新的交易摘要。

图 2.17 艾丽斯发送一个交易以及对交易哈希的签名。如果中间人攻击者试图篡改交易,哈希将会不同,因此附加的签名也将不正确。
你将在第七章看到,这样的攻击者当然无法伪造艾丽斯在新摘要上的签名。并且由于所使用的哈希函数的第二预像抗性,攻击者也无法找到一个完全不同的交易,其哈希为相同的摘要。
我们的中间人攻击者无害吗?我们还没有脱离危险。不幸的是,对于我审计的加密货币,交易是通过简单地串联每个字段进行哈希的:
$ echo -n "Alice""Bob""100""15" | openssl dgst -sha3-256
34d6b397c7f2e8a303fc8e39d283771c0397dad74cef08376e27483efc29bb02
表面上看似乎一切正常,实际上完全破坏了加密货币的支付系统。这样做很容易使攻击者打破哈希函数的第二预像抗性。花点时间思考一下你如何找到一个不同的交易,其哈希为34d6...。
如果我们将一个数字从fee字段移到amount字段会发生什么?人们可以看到以下交易哈希到了同样的摘要,艾丽斯签署了:
$ echo -n "Alice""Bob""1001""5" | openssl dgst -sha3-256
34d6b397c7f2e8a303fc8e39d283771c0397dad74cef08376e27483efc29bb02
因此,一个想让鲍勃获得更多钱的中间人攻击者可以修改交易而不使签名无效。正如您可能已经猜到的那样,这就是 TupleHash 解决的问题。它允许您通过使用非模糊编码来明确地对字段列表进行哈希处理。实际上发生的情况与以下内容类似(使用||字符串连接操作):
cSHAKE(input="5"||"Alice"||"3"||"Bob"||"3"||"100"||"2"||"10",
➥ output_length=256, custom_string="TupleHash"+"anything you want")
这次输入是通过在交易的每个字段前加上其长度来构造的。花点时间理解为什么这样解决了我们的问题。一般来说,通过在对其进行哈希处理之前始终确保序列化输入,可以安全地使用任何哈希函数。序列化输入意味着始终存在一种方法来反序列化它(即恢复原始输入)。如果可以反序列化数据,则字段分隔就不会存在任何歧义。
2.6 密码哈希
在本章中,您已经看到了几个有用的函数,这些函数要么是哈希函数,要么是扩展哈希函数。但在您跳转到下一章之前,我需要提到密码哈希。
想象一下以下情景:您有一个网站(这将使您成为网站管理员),您希望用户注册并登录到该网站,因此您为这两个功能分别创建了两个网页。突然间,您想知道,您将如何存储他们的密码?您在数据库中以明文存储这些密码吗?起初似乎没有什么问题,您认为。但这并不完美。人们倾向于在各处重复使用相同的密码,如果(或者当)您遭受攻击并且攻击者设法倾倒所有用户的密码,这对您的用户来说将是不好的,对您的平台声誉也将是不好的。您再仔细考虑一下,您意识到一个攻击者如果能够窃取这个数据库,那么他将能够以任何用户的身份登录。以明文存储密码现在不再理想,您希望有更好的处理方式。
一种解决方案可能是对密码进行哈希处理,然后仅存储摘要。当有人登录到您的网站时,流程将类似于以下内容:
-
您收到用户的密码。
-
您对用户提供的密码进行哈希处理并丢弃密码。
-
您将摘要与先前存储的内容进行比较;如果匹配,则用户已登录。
该流程允许您在有限时间内处理用户的密码。但是,一旦攻击者进入您的服务器,就可以悄悄地保留从该流程中记录密码,直到您察觉到其存在。我们承认这仍然不是一个完美的情况,但我们仍然改进了网站的安全性。在安全领域,我们也称之为深度防御,即通过分层不完美的防御措施,希望攻击者无法击败所有这些层次。这也是现实世界的加密学所涉及的内容。但是,这种解决方案还存在其他问题:
-
如果攻击者获取了哈希密码,可以进行暴力破解攻击或穷举搜索(尝试所有可能的密码)。 这将针对整个数据库测试每个尝试。理想情况下,我们希望攻击者只能一次攻击一个哈希密码。
-
哈希函数应该尽可能快。 攻击者可以利用这一点进行暴力破解(每秒尝试许多密码)。理想情况下,我们应该有一种机制来减慢这种攻击。
第一个问题通常通过使用 盐 解决,盐是公开的随机值,每个用户都不同。当我们对用户的密码进行哈希处理时,我们使用一个盐,这在某种程度上类似于在 cSHAKE 中使用每个用户的自定义字符串:它实际上为每个用户创建了一个不同的哈希函数。由于每个用户使用不同的哈希函数,攻击者无法预先计算大量密码(称为 彩虹表),希望将其与窃取的密码哈希数据库整个测试相匹配。
第二个问题通过 密码哈希 解决,这些密码哈希设计为缓慢。目前用于此的最先进选择是 Argon2,它是从 2013 年到 2015 年举办的密码哈希竞赛 (password-hashing.net) 的获胜者。本文撰写时(2021 年),Argon2 正在按计划标准化为 RFC (datatracker.ietf.org/doc/draft-irtf-cfrg-argon2/)。在实践中,还使用其他非标准算法,如 PBKDF2、bcrypt 和 scrypt。问题在于这些算法可能使用不安全的参数,并且因此在实践中配置起来并不简单。
此外,只有 Argon2 和 scrypt 能够抵御来自攻击者的重度优化,因为其他方案不是内存硬化的。术语 内存硬化 意味着算法只能通过内存访问的优化来进行优化。换句话说,优化其余部分并不会带来太大好处。由于即使使用专用硬件(CPU 周围只能放置那么多缓存),内存硬化函数在任何类型的设备上运行速度都很慢。当你想要防止攻击者在评估函数时获得非可忽略的速度优势时,这是一种期望的属性。
图 2.18 回顾了本章中你所看到的不同类型的哈希函数。

图 2.18 在本章中,你看到了四种类型的哈希函数:(1) 普通类型的哈希函数为任意长度的输入提供了一个唯一的随机标识符;(2) 可扩展输出函数与普通类型类似,但提供了任意长度的输出;(3) 元组哈希函数清楚地列出了哈希值;以及 (4) 无法轻易优化以安全存储密码的密码哈希函数。
摘要
-
哈希函数提供碰撞抗性、前像抗性和第二前像抗性。
-
前像抗性意味着不应该能够找到产生摘要的输入。
-
第二前像抗性意味着从一个输入及其摘要出发,不应该能够找到一个产生相同摘要的不同输入。
-
碰撞抗性意味着不应该能够找到两个随机输入,它们的哈希值相同。
-
-
最广泛采用的哈希函数是 SHA-2,而推荐的哈希函数是 SHA-3,因为 SHA-2 缺乏对长度扩展攻击的抵抗力。
-
SHAKE 是一种可扩展输出函数(XOF),它的行为类似于哈希函数,但提供任意长度的摘要。
-
cSHAKE(用于定制 SHAKE)允许轻松创建行为像不同 XOF 的 SHAKE 实例。这被称为域分离。
-
对象在进行哈希处理之前应进行序列化,以避免破坏哈希函数的第二前像抗性。像 TupleHash 这样的算法会自动处理这一点。
-
对密码进行哈希处理时使用的是专门设计用于此目的的较慢的哈希函数。Argon2 是最先进的选择。
第三章:消息认证码
本章涵盖
-
消息认证码(MACs)
-
MAC 的安全性属性和陷阱
-
广泛采用的 MAC 标准
将哈希函数与秘密密钥混合在一起,你就得到了一种称为消息认证码(MAC)的东西,它是一种用于保护数据完整性的密码原语。添加秘密密钥是任何类型安全的基础:没有密钥就没有机密性,也没有认证。虽然哈希函数可以为任意数据提供认证或完整性,但这要归功于一个不可篡改的额外受信任的通道。在本章中,你将看到 MAC 如何用于创建这样一个受信任的通道,以及它还能做些什么。
注意 对于本章,您需要已阅读了第二章的哈希函数。
无状态 cookies,MAC 的一个激励示例
假设下面的场景:你是一个网页。你色彩明亮,充满活力,最重要的是,你以为一群忠实用户提供服务感到自豪。要与你互动,访客必须首先通过发送他们的凭据来登录,然后你必须验证这些凭据。如果凭据与用户首次注册时使用的凭据匹配,那么你已成功验证了用户。
当然,Web 浏览体验不仅仅是一个请求,而是多个请求的组合。为了避免用户在每个请求中重新进行身份验证,你可以让他们的浏览器存储用户凭据,并在每个请求中自动重新发送。浏览器就有一个专门的功能——cookies! Cookies 不仅仅用于凭据。它们可以存储任何你希望用户在每个请求中发送给你的内容。
尽管这种天真的方法效果很好,但通常你不希望在浏览器中以明文形式存储诸如用户密码之类的敏感信息。相反,会话 cookie 最常携带一个随机字符串,在用户登录后立即生成。Web 服务器将随机字符串存储在临时数据库中,使用用户的昵称作为标识。如果浏览器以某种方式发布了会话 cookie,就不会泄露有关用户密码的任何信息(尽管可以使用它来冒充用户)。Web 服务器还有可能通过在其端删除 cookie 来终止会话,这很好。

这种方法没有问题,但在某些情况下,它可能不太适合扩展。如果你有许多服务器,让所有服务器共享用户和随机字符串之间的关联可能会很麻烦。相反,你可以在浏览器端存储更多信息。让我们看看如何做到这一点。
天真地,你可以让 cookie 包含一个用户名而不是一个随机字符串,但这显然是一个问题,因为我现在可以通过手动修改 cookie 中包含的用户名来冒充任何用户。也许你在第二章学到的哈希函数能帮助我们。花几分钟想想哈希函数如何防止用户篡改自己的 cookie。
第二种天真的方法可能是不仅在 cookie 中存储一个用户名,还存储该用户名的摘要。你可以使用像 SHA-3 这样的哈希函数来对用户名进行哈希。我在图 3.1 中说明了这一点。你认为这个方法可行吗?

图 3.1 为了验证浏览器的请求,web 服务器要求浏览器存储一个用户名和该用户名的哈希,并在每个后续请求中发送这些信息。
这种方法有一个很大的问题。请记住,哈希函数是一个公开的算法,恶意用户可以重新计算新数据上的哈希。如果你不信任哈希的来源,它就无法提供数据完整性!的确,图 3.2 显示,如果恶意用户修改了其 cookie 中的用户名,他们也可以简单地重新计算 cookie 的摘要部分。

图 3.2 恶意用户可以修改其 cookie 中包含的信息。如果一个 cookie 包含一个用户名和一个哈希,两者都可以被修改以冒充不同的用户。
使用哈希仍然不是一个愚蠢的想法。我们还能做什么?事实证明,有一种与哈希函数类似的原始方法,叫做 MAC,它正好可以满足我们的需求。
MAC是一个秘密密钥算法,它像哈希函数一样接受一个输入,但它还接受一个秘密密钥(谁会想到呢?)然后产生一个称为认证标签的唯一输出。这个过程是确定性的;给定相同的秘密密钥和相同的消息,MAC 会产生相同的认证标签。我在图 3.3 中说明了这一点。

图 3.3 一个消息认证码(MAC)的接口。该算法接受一个秘密密钥和一个消息,并确定性地生成一个唯一的认证标签。没有密钥的话,应该无法再现那个认证标签。
为了确保用户无法篡改他们的 cookie,让我们现在利用这种新的原始方法。当用户第一次登录时,你使用你的秘密密钥和他们的用户名生成一个认证标签,并要求他们将他们的用户名和认证标签存储在cookie中。因为他们不知道秘密密钥,所以他们将无法伪造出不同用户名的有效认证标签。
要验证他们的 cookie,你做同样的事情:使用你的秘密密钥和 cookie 中包含的用户名生成一个身份验证标签,并检查它是否与 cookie 中包含的身份验证标签匹配。如果匹配,那么它必定来自你,因为只有你能够生成有效的身份验证标签(在你的秘密密钥下)。我在图 3.4 中说明了这一点。

图 3.4 一个恶意用户篡改了他的 cookie,但无法伪造新 cookie 的有效身份验证标签。随后,网页无法验证 cookie 的真实性和完整性,因此丢弃了请求。
MAC 就像一个私有的哈希函数,只有你知道密钥才能计算出来。在某种意义上,你可以用密钥个性化一个哈希函数。与哈希函数的关系并不止于此。你将在本章后面看到,MAC 经常是从哈希函数构建的。接下来,让我们看一个使用真实代码的不同示例。
3.2 一个代码示例
到目前为止,只有你在使用 MAC。让我们增加参与者的数量,并以此为动机编写一些代码,看看 MAC 在实践中是如何使用的。想象一下,你想与其他人通信,而不在乎其他人是否阅读你的消息。但你真正关心的是消息的完整性:它们不能被修改!一个解决方案是你和你的通信对象使用相同的秘密密钥和 MAC 来保护通信的完整性。
对于这个示例,我们将使用最流行的 MAC 函数之一——基于哈希的消息认证码(HMAC)与 Rust 编程语言一起使用。HMAC 是一种使用哈希函数作为核心的消息认证码。它与不同的哈希函数兼容,但主要与 SHA-2 一起使用。如下列表所示,发送部分只需接受一个密钥和一个消息,然后返回一个身份验证标签。
列表 3.1 在 Rust 中发送经过身份验证的消息
use sha2::Sha256;
use hmac::{Hmac, Mac, NewMac};
fn send_message(key: &[u8], message: &[u8]) -> Vec<u8> {
let mut mac = Hmac::<Sha256>::new(key.into()); // ❶
mac.update(message); // ❷
mac.finalize().into_bytes().to_vec() // ❸
}
❶ 使用秘密密钥和 SHA-256 哈希函数实例化 HMAC
❷ 为 HMAC 缓冲更多输入
❸ 返回身份验证标签
另一方面,流程类似。在接收到消息和身份验证标签后,你的朋友可以使用相同的秘密密钥生成自己的标签,然后进行比较。与加密类似,双方需要共享相同的秘密密钥才能使其正常工作。以下列表显示了这是如何工作的。
列表 3.2 在 Rust 中接收经过身份验证的消息
use sha2::Sha256;
use hmac::{Hmac, Mac, NewMac};
fn receive_message(key: &[u8], message: &[u8],
authentication_tag: &[u8]) -> bool {
let mut mac = Hmac::<Sha256>::new(key); // ❶
mac.update(message); // ❷
mac.verify(&authentication_tag).is_ok()
}
❶ 接收方需要从相同的密钥和消息中重新创建身份验证标签。
❷ 检查重现的身份验证标签是否与接收到的标签匹配
注意,这个协议并不完美:它允许重放攻击。如果一个消息及其认证标签在以后的某个时间点被重放,它们仍然是真实的,但你将无法检测到它是一条旧消息被重新发送给你。本章后面,我会告诉你一个解决方案。现在你知道了 MAC 可以用来做什么,我会在下一节谈谈 MAC 的一些“坑”。
3.3 MAC 的安全属性
MACs,像所有的密码学原语一样,有它们的怪异之处和陷阱。在继续之前,我将对 MAC 提供的安全属性以及如何正确使用它们提供一些解释。你会依次学到(按顺序):
-
MACs 抵抗认证标签的伪造。
-
一个认证标签需要有足够的长度才能保证安全。
-
如果简单地进行认证,消息可以被重放。
-
验证认证标签容易出现错误。
3.3.1 认证标签伪造
一个 MAC 的一般安全目标是防止在新消息上伪造认证标签。这意味着在不知道秘钥 k 的情况下,无法计算出认证标签 t = MAC(k, m) 在用户选择的消息 m 上。这听起来合理,对吧?如果我们缺少一个参数,我们就不能计算出一个函数。
然而,MACs 提供了比这更多的保证。现实世界的应用程序通常会让攻击者获取一些受限制的消息的认证标签。例如,在我们的介绍场景中,这就是问题所在,用户可以通过注册一个可用的昵称来获得几乎任意的认证标签。因此,MACs 必须甚至对这些更强大的攻击者也是安全的。一个 MAC 通常附带一个证明,即使攻击者可以要求你为大量的任意消息产生认证标签,攻击者也不能自己伪造一个以前从未见过的消息的认证标签。
注意有人可能会想知道证明这样一个极端性质的用处是什么。如果攻击者可以直接请求任意消息的认证标签,那么还剩下什么需要保护的呢?但这就是密码学中安全证明的工作原理:它们考虑到最强大的攻击者,甚至在那种情况下,攻击者也是无能为力的。在实践中,攻击者通常不那么强大,因此,我们相信如果一个强大的攻击者无法做出恶意行为,一个不那么强大的攻击者就更加无能为力了。
因此,只要与 MAC 一起使用的秘钥保持秘密,你就应该受到保护。这意味着秘钥必须足够随机(在第八章中详细讨论)和足够大(通常为 16 字节)。此外,一个 MAC 对于我们在第二章中看到的相同类型的模糊攻击也是脆弱的。如果你试图验证结构,请确保在用 MAC 验证之前将它们序列化;否则,伪造可能是微不足道的。
3.3.2 认证标签的长度
针对 MAC 的另一个可能攻击是碰撞。记住,找到哈希函数的碰撞意味着找到两个不同的输入 X 和 Y,使得 HASH(X) = HASH(Y)。我们可以通过定义当 MAC(k, X) = MAC(k, Y) 时输入 X 和 Y 发生碰撞来将此定义扩展到 MAC。
正如我们在第二章学到的生日攻击边界一样,如果我们算法的输出长度较小,则可以高概率地找到碰撞。例如,对于 MAC,如果攻击者可以访问生成 64 位认证标签的服务,则可以通过请求较少的标签数(232)高概率地找到碰撞。在实践中,这样的碰撞很少能够被利用,但存在一些碰撞抗性很重要的情况。因此,我们希望认证标签大小能够限制此类攻击。一般来说,使用 128 位认证标签是因为它们提供足够的抗性。
[请求 2⁶⁴ 个认证标签] 在连续 1Gbps 链路上需要 250,000 年,并且在此期间不更改秘密密钥 K。
—RFC 2104(“HMAC:用于消息认证的键控哈希”,1997)
使用 128 位认证标签可能看起来有些反直觉,因为我们希望哈希函数的输出为 256 位。但是哈希函数是公开算法,攻击者可以离线计算,这使得攻击者能够对攻击进行优化和并行化。使用像 MAC 这样的密钥函数,攻击者无法有效地离线优化攻击,而是被迫直接向您请求认证标签,这通常会使攻击速度变慢。128 位认证标签需要攻击者在线查询 2⁶⁴ 次,才有 50% 的机会找到碰撞,这被认为足够大。尽管如此,某些情况下可能仍希望将认证标签增加到 256 位,这也是可能的。
3.3.3 重播攻击
我还没提到的一件事是重播攻击。让我们看一个容易受到此类攻击的场景。假设 Alice 和 Bob 使用不安全的连接在公开场合进行通信。为了防止消息篡改,他们在每条消息后附上认证标签。更具体地说,他们都使用两个不同的秘密密钥来保护连接的不同侧面(按最佳实践)。我在图 3.5 中说明了这一点。

图 3.5 两个用户共享两个密钥 k1 和 k2,并随消息一起交换认证标签。这些标签是根据消息的方向从 k1 或 k2 计算出来的。恶意观察者会重播其中一条消息给用户。
在这种情况下,没有任何东西能阻止恶意观察者向其接收者重播其中一条消息。依赖于 MAC 的协议必须意识到这一点,并构建对抗措施。一种方法是像图 3.6 中所示,向 MAC 的输入添加一个递增计数器。

图 3.6 两个用户共享两个密钥 k1 和 k2,并与身份验证标签一起交换消息。这些标签是根据消息的方向从 k1 或 k2 计算的。恶意观察者向用户重播其中一个消息。因为受害者已经增加了他的计数器,标签将被计算为 2, fine and you?,并且不会与攻击者发送的标签匹配。这使得受害者能够成功拒绝重放的消息。
在实践中,计数器通常是固定的 64 位长度。这允许在填满计数器之前发送 2⁶⁴ 条消息(并且有风险包装和重复自身)。当然,如果共享的密钥经常旋转(意味着在X条消息后,参与者同意使用新的共享密钥),那么计数器的大小可以缩小,并且在密钥旋转后重置为 0。(你应该确信重复使用相同的计数器与两个不同的密钥是可以的。)再次强调,由于存在歧义攻击,计数器永远不是可变长度的。
练习
你能想象出一个可变长度计数器如何可能允许攻击者伪造身份验证标签吗?
3.3.4 在恒定时间内验证身份验证标签
最后一个注意事项对我来说很重要,因为我在审计的应用程序中多次发现了这个漏洞。在验证身份验证标签时,接收到的身份验证标签和你计算的标签之间的比较必须在恒定时间内完成。这意味着比较应该始终花费相同的时间,假设接收到的标签是正确大小的。如果比较两个身份验证标签所花费的时间不是恒定时间,那么很可能是因为它在两个标签不同时返回。这通常提供了足够的信息,以启用通过测量验证完成所需时间来逐字节重新创建有效身份验证标签的攻击。我在以下漫画中解释了这一点。我们将这类攻击称为时序攻击。
幸运的是,实现 MAC 的加密库还提供了方便的函数,以恒定的时间验证身份验证标签。如果你想知道这是如何做到的,清单 3.3 展示了 Golang 如何在恒定时间代码中实现身份验证标签比较。

清单 3.3 Golang 中的常量时间比较
for i := 0; i < len(x); i++ {
v |= x[i] ^ y[i]
}
窍门在于从不采取任何分支。具体工作原理留给读者作为练习。
3.4 现实世界中的 MAC
现在我已经介绍了 MAC 是什么以及它们提供的安全属性,让我们看看人们在实际环境中如何使用它们。以下章节将讨论这一点。
3.4.1 消息认证
MACs 被广泛用于确保两台机器或两个用户之间的通信不被篡改。这在通信以明文传输和通信以加密方式传输的情况下都是必要的。我已经解释了当通信以明文传输时会发生什么,而在第四章中,我将解释在通信加密时如何实现这一点。
3.4.2 密钥派生
MACs 的一个特点是它们通常被设计为生成看起来随机的字节(就像哈希函数)。您可以利用这个特性实现一个单一的密钥来生成随机数,或者生成更多的密钥。在第八章关于秘密和随机性中,我将介绍基于 HMAC 的密钥派生函数(HKDF),它通过使用 HMAC 来实现这一点,HMAC 是我们将在本章中讨论的 MAC 算法之一。
伪随机函数(PRF)
想象一下,所有接受可变长度输入并生成固定大小随机输出的函数的集合。如果我们可以从这个集合中随机选择一个函数并将其用作 MAC(没有密钥),那就太好了。我们只需就选择哪个函数达成一致(有点像达成一致选择密钥)。不幸的是,我们不能拥有这样的集合,因为它太大了,但我们可以通过设计一些接近的东西来模拟选择这样一个随机函数:我们称这样的构造为伪随机函数(PRFs)。HMAC 和大多数实用的 MAC 都是这样的构造。它们通过一个密钥参数进行随机化。选择不同的密钥就像选择一个随机函数。
练习
注意:并非所有的 MAC 都是 PRF。你能看出为什么吗?
3.4.3 Cookie 的完整性
要追踪用户的浏览器会话,您可以向他们发送一个随机字符串(与他们的元数据相关联)或直接发送元数据,附带身份验证标签,以便他们无法修改它。这就是我在引言例子中解释的内容。
3.4.4 哈希表
编程语言通常公开称为哈希表(也称为哈希映射、字典、关联数组等)的数据结构,这些数据结构使用非密码散列函数。如果一个服务以这样一种方式公开此数据结构,使得攻击者可以控制非密码散列函数的输入,这可能导致拒绝服务(DoS)攻击,意味着攻击者可以使服务无法使用。为了避免这种情况,非密码散列函数通常在程序启动时进行随机化。
许多主要的应用程序使用一个随机密钥的 MAC 代替非密码散列函数。这适用于许多编程语言(如 Rust、Python 和 Ruby)或主要应用程序(如 Linux 内核)。它们都使用SipHash,一个针对短身份验证标签进行优化的 MAC,该标签在程序启动时生成随机密钥。
3.5 实践中的消息认证码(MACs)
你已经了解到 MAC 是一种加密算法,可以在一个或多个参与方之间使用,以保护信息的完整性和真实性。由于广泛使用的 MAC 也表现出良好的随机性,MAC 也经常被用于在不同类型的算法中确定性地产生随机数(例如,你将在第十一章学习的基于时间的一次性密码[TOTP]算法)。在本节中,我们将介绍两种现在可以使用的标准化的 MAC 算法——HMAC 和 KMAC。
3.5.1 HMAC,一种基于哈希的 MAC
最广泛使用的 MAC 是 HMAC(基于哈希的 MAC),由 M. Bellare、R. Canetti 和 H. Krawczyk 于 1996 年发明,并在 RFC 2104、FIPS 出版物 198 和 ANSI X9.71 中指定。HMAC,正如其名称所示,是一种使用哈希函数和密钥的方法。使用哈希函数构建 MAC 的概念是一个流行的概念,因为哈希函数有广泛可用的实现,在软件中速度快,并且在大多数系统上也受到硬件支持。记得我在第二章提到过,由于长度扩展攻击(本章末尾将详细介绍),SHA-2 不应直接用于对秘密进行哈希处理。那么如何将哈希函数转换为带密钥的函数呢?这就是 HMAC 为我们解决的问题。在幕后,HMAC 遵循以下步骤,我在图 3.7 中通过可视化方式说明:
-
它首先从主密钥中创建两个密钥:k1 = k ⊕ ipad 和 k2 = k ⊕ opad,其中ipad(内部填充)和opad(外部填充)是常数, ⊕ 是异或操作的符号。
-
然后将一个密钥
k1与消息进行串联并对其进行哈希运算。 -
结果与一个密钥
k2进行串联,并再进行一次哈希运算。 -
这产生了最终的认证标签。

图 3.7 HMAC 通过对一个密钥k1和输入消息的串联(||)进行哈希运算,然后再对第一次操作的输出与另一个密钥k2的串联进行哈希运算来工作。k1和k2都是从一个秘密密钥k派生出来的确定性密钥。
由于 HMAC 是可定制的,其认证标签的大小取决于所使用的哈希函数。例如,HMAC-SHA256 使用 SHA-256 并产生 256 位的认证标签,HMAC-SHA512 产生 512 位的认证标签,依此类推。
警告虽然可以截断 HMAC 的输出以减小其大小,但认证标签应至少为 128 位,正如我们之前讨论的那样。这并不总是得到尊重,一些应用会降低到 64 位,因为明确处理了有限数量的查询。这种方法存在权衡,再次强调,在执行非标准操作之前,仔细阅读细则是很重要的。
HMAC 是这样构建的,以方便证明。在几篇论文中,已经证明 HMAC 在底层哈希函数具有一些良好属性时是安全的,而所有的密码学安全哈希函数都应该具备这些属性。由于这一点,我们可以将 HMAC 与大量的哈希函数结合使用。今天,HMAC 主要与 SHA-2 一起使用。
3.5.2 KMAC,基于 cSHAKE 的 MAC
由于 SHA-3 不容易受到长度扩展攻击的影响(这实际上是 SHA-3 竞赛的要求之一),在实践中使用 SHA-3 与 HMAC 相比,使用像SHA-3-256(key || message)这样的方法更合理。这正是KMAC所做的。
KMAC 利用了 cSHAKE,即您在第二章中看到的可定制版本的 SHAKE 可扩展输出函数(XOF)。KMAC 以一种明确的方式对 MAC 密钥、输入和请求的输出长度进行编码(KMAC 是一种可扩展输出 MAC),并将其作为 cSHAKE 的输入来吸收(参见图 3.8)。KMAC 还使用“KMAC”作为函数名称(以定制 cSHAKE),并且还可以接受用户定义的定制字符串。

图 3.8 KMAC 只是 cSHAKE 的一个包装器。为了使用密钥,它对密钥、输入和输出长度进行编码(以一种明确的方式),然后将其作为 cSHAKE 的输入。
有趣的是,由于 KMAC 还吸收了请求的输出长度,使用不同输出长度进行多次调用会得到完全不同的结果,这在一般情况下很少见于 XOFs。这使得 KMAC 在实践中成为一种非常多功能的函数。
3.6 SHA-2 和长度扩展攻击
我们已经多次提到,不应该使用 SHA-2 来哈希秘密,因为它对长度扩展攻击不具有抵抗力。在本节中,我们旨在对这种攻击进行简单解释。
让我们回到我们的引言情景,回到我们尝试简单地使用 SHA-2 来保护 cookie 的完整性的步骤。请记住,这还不够好,因为用户可以篡改 cookie(例如,添加一个admin=true字段)并重新计算 cookie 的哈希。确实,SHA-2 是一个公共函数,没有任何东西阻止用户这样做。图 3.9 说明了这一点。

图 3.9 一个网页发送一个 cookie,然后跟随着该 cookie 的哈希给一个用户。然后,要求用户在每次后续请求中发送 cookie 以验证自己。不幸的是,一个恶意用户可以篡改 cookie 并重新计算哈希,从而破坏完整性检查。然后网页接受该 cookie 为有效。
接下来最好的想法是在我们哈希的内容中添加一个秘钥。这样,用户无法重新计算摘要,因为需要秘钥,就像 MAC 一样。在接收到篡改的 cookie 时,页面计算SHA-256(key || tampered_cookie),其中||表示两个值的连接,并得到一个与恶意用户可能发送的内容不匹配的结果。图 3.10 说明了这种方法。

图 3.10 通过在计算 cookie 的哈希时使用一个秘钥,人们可能会认为想要篡改自己的 cookie 的恶意用户无法计算出新 cookie 的正确摘要。我们将在后面看到,对于 SHA-256 来说这并不成立。
不幸的是,SHA-2 有一个令人讨厌的特点:从一个输入的摘要中,可以计算出输入的摘要以及更多内容。这是什么意思呢?让我们看看图 3.11,其中使用 SHA-256 作为SHA-256(secret || input1)。

图 3.11 SHA-256 对一个与 cookie(这里命名为input1)连接的秘密进行哈希。请记住,SHA-256 通过使用 Merkle–Damgård 构造来迭代地调用压缩函数对输入的块进行处理,从初始化向量(IV)开始。
图 3.11 非常简化,但想象一下input1是字符串user=bob。请注意,获得的摘要实际上是哈希函数在这一点的完整中间状态。没有什么可以阻止假装填充部分是输入的一部分,继续 Merkle–Damgård 舞蹈。在图 3.12 中,我们说明了这种攻击,其中一个人会取得摘要并计算input1 || padding || input2的哈希。在我们的例子中,input2是&admin=true。

图 3.12 SHA-256 对 cookie 的哈希输出(中间摘要)用于扩展哈希到更多数据,创建一个哈希(右侧摘要),其中包括秘密与input1、第一个填充字节和input2的连接。
这个漏洞允许从给定的摘要继续哈希,就好像操作还没有完成一样。这打破了我们先前的协议,正如图 3.13 所示。

图 3.13 攻击者成功使用长度扩展攻击篡改他们的 cookie,并使用先前的哈希计算出正确的哈希。
现在第一个填充需要成为输入的一部分,这可能会阻止一些协议被利用。但是,最小的更改可能会重新引入漏洞。因此,永远不要使用 SHA-2 对秘密信息进行哈希。当然,还有几种正确的方法(例如,SHA-256(k || message || k)),这就是 HMAC 提供的功能。因此,如果要使用 SHA-2,请使用 HMAC,如果更喜欢 SHA-3,请使用 KMAC。
总结
-
消息验证码(MACs)是对称加密算法,允许共享相同密钥的一个或多个参与方验证消息的完整性和真实性。
-
要验证消息及其相关的认证标签的真实性,可以重新计算消息和一个秘密密钥的认证标签,然后比较这两个认证标签。如果它们不同,则消息已被篡改。
-
总是在恒定时间内将接收到的认证标签与计算得到的标签进行比较。
-
-
虽然消息验证码默认保护消息的完整性,但它们不能检测到消息被重播的情况。
-
标准化和广受认可的消息验证码包括 HMAC 和 KMAC 标准。
-
可以使用不同的哈希函数来进行 HMAC。实际上,HMAC 常与 SHA-2 哈希函数一起使用。
-
认证标签的最小长度应为 128 位,以防止认证标签的碰撞和伪造。
-
永远不要直接使用 SHA-256 来构建消息验证码,因为可能会出错。始终使用像 HMAC 这样的函数来完成这个任务。
第四章:认证加密
本章涵盖
-
对称加密与认证加密的区别
-
流行的认证加密算法
-
其他类型的对称加密
保密性是关于隐藏数据不被未经授权的人看到,而加密是实现这一目标的方法。加密是密码学最初被发明的目的;它是早期密码学家最关心的问题。他们会问自己,“我们如何防止观察者理解我们的对话?”虽然最初科学及其进展是在闭门之后蓬勃发展的,只有政府和军队受益,但现在已经向全世界开放。今天,加密在现代生活的各个方面被广泛使用以增加隐私和安全性。在本章中,我们将了解加密的真正含义,它解决了哪些问题,以及当今的应用程序如何大量使用这种密码原语。
注意 对于本章,您需要已经阅读了第三章关于消息认证码的内容。
4.1 什么是密码?
就像当你用俚语与兄弟姐妹谈论放学后要做什么,这样你的妈妈就不知道你在干什么。
—Natanael L. (2020, twitter.com/Natanael_L)
让我们想象一下我们的两个角色,爱丽丝和鲍勃,想要私下交换一些消息。在实践中,他们有许多可供选择的媒介(邮件、电话、互联网等),每个媒介默认都是不安全的。邮递员可能会打开他们的信件;电信运营商可以窥探他们的通话和短信;互联网服务提供商或者在爱丽丝和鲍勃之间的网络中的任何服务器都可以访问正在交换的数据包的内容。
不再拖延,让我们介绍一下爱丽丝和鲍勃的救星:加密算法(也称为密码)。现在,让我们把这个新算法想象成爱丽丝可以用来加密她发送给鲍勃的消息的黑匣子。通过对消息进行加密,爱丽丝将其转换为看起来随机的内容。这个加密算法需要
-
一个秘钥—这个元素的不可预测性、随机性和良好的保护至关重要,因为加密算法的安全性直接依赖于密钥的保密性。我将在第八章关于秘密和随机性中更多地讨论这一点。
-
一些明文—这是你想要加密的内容。它可以是一些文本、一张图片、一个视频,或者任何可以转换为比特的东西。
这个加密过程产生了一个密文,即加密后的内容。爱丽丝可以安全地使用之前列出的媒介之一将该密文发送给鲍勃。对于不知道秘钥的任何人来说,密文看起来是随机的,消息内容(明文)的任何信息都不会泄露。一旦鲍勃收到这个密文,他可以使用一个解密算法将密文恢复为原始明文。解密需要
-
一个秘密密钥 — 这是艾丽丝用于创建密文的相同秘密密钥。因为同一密钥用于两种算法,所以我们有时将密钥称为对称密钥。这也是为什么我们有时指定我们使用对称加密而不仅仅是加密。
-
一些密文 — 这是鲍勃从艾丽丝那里收到的加密消息。
然后该过程显示出原始明文。图 4.1 说明了此流程。

图 4.1 艾丽丝(右上)用密钥 0x8866...(一个缩写的十六进制数)加密明文 hello,然后将密文发送给鲍勃。鲍勃(右下)使用相同的密钥和解密算法解密收到的密文。
加密允许艾丽丝将她的消息转换成看起来随机的内容,并可以安全地传输给鲍勃。解密允许鲍勃将加密消息还原为原始消息。这种新的加密原语为他们的消息提供了保密性(或秘密性或隐私性)。
注意 艾丽丝和鲍勃如何同意使用相同的对称密钥?现在,我们假设其中一个人有权访问一个生成不可预测密钥的算法,并且他们亲自见面交换密钥。实际上,如何用共享的秘密来启动这样的协议通常是公司需要解决的重大挑战之一。在本书中,您将看到许多解决这个问题的不同方法。
请注意,我尚未介绍本章标题“认证加密”指的是什么。到目前为止,我只谈到了单独的加密。虽然单独的加密并不安全(稍后再说),但我必须先解释它是如何工作的,然后才能介绍认证加密原语。所以请容我先讲解加密的主要标准:高级加密标准(AES)。
4.2 高级加密标准(AES)块密码
1997 年,NIST 启动了一个旨在取代数据加密标准(DES)算法的高级加密标准(AES)的公开竞赛,他们以前的加密标准开始显露老化迹象。竞赛持续了三年,期间,来自不同国家的密码学家团队提交了 15 种不同的设计。竞赛结束时,只有一个提交作品,由文森特·赖曼和约翰·达曼设计的 Rijndael 被提名为获胜者。2001 年,NIST 发布了 AES 作为 FIPS(联邦信息处理标准)197 出版物的一部分。 AES,即 FIPS 标准中描述的算法,仍然是今天主要使用的密码。在本节中,我将解释 AES 的工作原理。
4.2.1 AES 提供了多少安全性?
AES 提供了三个不同版本:AES-128 使用 128 位(16 字节)密钥,AES-192 使用 192 位(24 字节)密钥,AES-256 使用 256 位(32 字节)密钥。密钥的长度决定了安全级别—越大越强。尽管如此,大多数应用都使用 AES-128,因为它提供足够的安全性(128 位安全性)。
术语 位安全性 常用来指示密码算法的安全性。例如,AES-128 指定我们已知的最佳攻击需要大约 2¹²⁸ 次操作。这个数字是巨大的,它是大多数应用所追求的安全级别。
位安全性是一个上限
128 位密钥提供 128 位安全性的事实是特定于 AES 的;这不是一个黄金法则。在某些其他算法中使用的 128 位密钥理论上可能提供的安全性不到 128 位。虽然 128 位密钥可以提供不到 128 位的安全性,但永远不会提供更多(总是有暴力破解攻击)。尝试所有可能的密钥最多需要 2¹²⁸ 次操作,将安全性至少降低到 128 位。
2¹²⁸ 有多大?注意两个 2 的幂之间的数量加倍。例如,2³ 是 2² 的两倍。如果说 2¹⁰⁰ 次操作几乎是不可能实现的,想象一下达到其两倍(2¹⁰¹)。要达到 2¹²⁸,你需要将你的初始数量加倍 128 次!简单来说,2¹²⁸ 是 340 个无法想象的无穷大。这个数字是相当巨大的,但你可以假设我们在实践中永远不可能达到这样的数字。我们也没有考虑到任何大规模复杂攻击所需的空间量,实际上同样是巨大的。
可预见的是,AES-128 将在很长一段时间内保持安全。除非密码分析方面的进展发现尚未发现的漏洞,这会减少攻击算法所需的操作数。
4.2.2 AES 的接口
查看 AES 加密的接口,我们可以看到以下内容:
-
正如前面讨论过的,该算法接受可变长度的密钥。
-
它还需要准确的 128 位纯文本。
-
它输出准确的 128 位密文。
因为 AES 加密了固定大小的纯文本,我们称其为 分组密码。后面你将在本章中看到,一些其他密码可以加密任意长度的纯文本。
解密操作恰好与此相反:它使用相同的密钥,一个 128 位密文,并返回原始的 128 位纯文本。实际上,解密是加密的逆过程。这是因为加密和解密操作是 确定性 的;无论你调用它们多少次,它们都会产生相同的结果。
从技术上讲,具有密钥的分组密码是一种置换:它将所有可能的明文映射到所有可能的密文(请参见图 4.2 中的示例)。更改密钥会更改该映射。置换也是可逆的。从密文,您可以得到回到其相应明文的映射(否则,解密将无法工作)。

图 4.2 具有密钥的密码可以被视为一种置换:它将所有可能的明文映射到所有可能的密文。
当然,我们没有空间列出所有可能的明文及其相关的密文。对于 128 位分组密码,这将是 2¹²⁸ 个映射。相反,我们设计像 AES 这样的结构,它们的行为类似于置换,并由密钥随机化。我们说它们是伪随机置换(PRPs)。
4.2.3 AES 的内部结构
让我们深入了解 AES 的内部。请注意,在加密过程中,AES 将明文的状态视为一个 4×4 字节矩阵(正如您在图 4.3 中所看到的)。

图 4.3 当进入 AES 算法时,16 字节的明文被转换为一个 4×4 矩阵。然后将对此状态进行加密,最终将其转换为 16 字节的密文。
实际上这并不重要,但这就是 AES 的定义方式。在幕后,AES 的工作方式类似于许多类似的对称密码原语,称为分组密码,它们是加密固定大小的块的密码。AES 还有一个轮函数,它会多次迭代,从原始输入(明文)开始。我在图 4.4 中对此进行了说明。

图 4.4 AES 通过对状态迭代一个轮函数来对其进行加密。轮函数接受多个参数,包括一个秘密密钥。(这些参数在图表中被省略以简化。)
每次调用轮函数都会进一步转换状态,最终产生密文。每个轮使用一个不同的轮密钥,它是从主对称密钥派生的(在所谓的密钥调度期间)。这允许对对称密钥位的细微更改产生完全不同的加密(这被称为扩散原理)。
轮函数由多个操作组成,这些操作混合和转换状态的字节。AES 的轮函数特别使用了四种不同的子函数。虽然我们会避免详细解释子函数的工作原理(您可以在任何关于 AES 的书籍中找到这些信息),但它们被命名为SubBytes、ShiftRows、MixColumns和AddRoundKey。前三者是容易可逆的(您可以从操作的输出中找到输入),但最后一个不是。它执行轮密钥和状态的异或(XOR)操作,因此需要轮密钥的知识才能反转。我在图 4.5 中说明了轮函数的内部。

图 4.5 AES 的典型轮次。(第一轮和最后一轮省略了一些操作。)四种不同的函数转换状态。每个函数都是可逆的,否则解密就不起作用。圆圈内的加法符号(⊕)是 XOR 操作的符号。
在 AES 中,轮函数的迭代次数,通常在减少的轮次上是实用的,被选择用来阻止密码分析。例如,在 AES-128 的三轮变种上存在非常有效的 总破解(恢复密钥的攻击)。通过多次迭代,密码将明文转换为看起来与原始明文完全不同的东西。明文中最微小的变化也会返回完全不同的密文。这个原则被称为 雪崩效应。
注意 现实世界中的加密算法通常通过它们提供的安全性、大小和速度进行比较。我们已经讨论了 AES 的安全性和大小;它的安全性取决于密钥大小,并且可以一次加密 128 位的数据块。就速度而言,许多 CPU 厂商已经在硬件中实现了 AES。例如,AES 新指令(AES-NI)是一组可在英特尔和 AMD CPU 中使用的指令,可用于有效地实现 AES 的加密和解密。这些特殊指令使 AES 在实践中变得极快。
你可能仍然有一个问题,那就是如何用 AES 加密超过或少于 128 位的内容?我下面会回答这个问题。
4.3 加密企鹅和 CBC 操作模式
现在我们已经介绍了 AES 分组密码并解释了它的内部工作原理,让我们看看如何在实践中使用它。分组密码的问题在于它只能单独加密一个块。要加密不是完全 128 位的内容,我们必须使用 填充 以及 操作模式。所以让我们看看这两个概念是什么。
想象一下,你想加密一条长消息。天真地,你可以将消息分成 16 字节的块(AES 的块大小)。然后,如果明文的最后一个块小于 16 字节,你可以在末尾添加一些字节,直到明文变成 16 字节长。这就是填充的目的!
有几种方式可以指定如何选择这些 填充字节,但填充的最重要方面是它必须是可逆的。一旦我们解密了密文,我们应该能够去除填充以检索原始的未填充消息。例如,简单地添加随机字节是行不通的,因为你无法辨别随机字节是否是原始消息的一部分。
最流行的填充机制通常被称为PKCS#7 填充,它首次出现在 RSA(一家公司)于 1990 年代末发布的 PKCS#7 标准中。PKCS#7 填充规定一条规则:每个填充字节的值必须设置为所需填充的长度。如果明文已经是 16 字节了怎么办?那么我们添加一个完整块的填充,设置为值 16。我在图 4.6 中用图示说明了这一点。要移除填充,你可以轻松地检查明文的最后一个字节的值,并将其解释为要移除的填充长度。

图 4.6 如果明文不是块大小的倍数,则填充所需长度以达到块大小的倍数。在图中,明文为 8 字节,因此我们使用 8 个字节(包含值 8)来填充明文,使其达到 AES 所需的 16 字节。
现在,有一个大问题我需要谈论。到目前为止,为了加密一条长消息,你只需将其分成 16 字节的块(也许你会填充最后一个块)。这种天真的方式被称为电子密码本(ECB)操作模式。正如你所学到的,加密是确定性的,因此对相同的明文块进行两次加密会导致相同的密文。这意味着通过单独加密每个块,生成的密文可能会有重复的模式。
这可能看起来没问题,但允许这些重复会导致许多问题。最明显的问题是它们泄露了有关明文的信息。其中最著名的例子是图 4.7 中的ECB 企鹅。

图 4.7 著名的 ECB 企鹅是使用电子密码本(ECB)操作模式加密的企鹅图像。由于 ECB 不隐藏重复模式,仅仅通过查看密文就可以猜出最初加密的内容。(图片来源于维基百科。)
为了安全地加密超过 128 位的明文,存在更好的操作模式可以“随机化”加密。对于 AES 来说,最流行的操作模式之一是密码块链接(CBC)。CBC 适用于任何确定性块密码(不仅仅是 AES),通过使用称为初始化向量(IV)的附加值来随机化加密。因此,IV 的长度为块大小(AES 为 16 字节)并且必须是随机且不可预测的。
要使用 CBC 操作模式加密,首先生成一个 16 字节的随机 IV(第八章告诉你如何做到这一点),然后在加密之前将生成的 IV 与明文的前 16 字节进行异或。这有效地随机化了加密。实际上,如果相同的明文使用不同的 IV 加密两次,操作模式会生成两个不同的密文。
如果有更多明文需要加密,使用前一个密文(就像我们之前使用 IV 一样)与下一个明文块进行异或运算,然后再加密。这样也会使下一个加密块变得随机。记住,加密的内容是不可预测的,应该和我们用来创建真正 IV 的随机性一样好。图 4.8 说明了 CBC 加密。

图 4.8 使用 AES 的 CBC 模式。为了加密,我们使用一个随机的初始化向量(IV),以及填充的明文(分成多个 16 字节的块)。
要使用 CBC 模式进行解密,需要反向操作。由于需要 IV,因此必须将其明文传输,与密文一起。由于 IV 应该是随机的,因此观察其值不会泄露任何信息。我在图 4.9 中说明了 CBC 解密。

图 4.9 使用 AES 的 CBC 模式。为了解密,需要相关的初始化向量(IV)。
附加参数如 IV 在密码学中很常见。然而,这些参数通常被理解不清楚,是漏洞的主要来源。在 CBC 模式下,IV 需要是唯一的(不能重复)以及不可预测的(真的需要是随机的)。这些要求可能由于多种原因而失败。因为开发人员经常对 IV 感到困惑,一些密码库已经删除了在使用 CBC 加密时指定 IV 的可能性,并自动生成一个随机的 IV。
警告 当 IV 重复或可预测时,加密再次变得确定性,并且可能出现许多巧妙的攻击。这就是著名的 BEAST 攻击(针对 TLS 协议的浏览器利用)在 TLS 协议上的情况。还要注意,其他算法可能对 IV 有不同的要求。这就是阅读手册总是很重要的原因。危险的细节隐藏在小字里。
请注意,仅仅使用一种操作模式和填充还不足以使密码可用。在下一节中,您将看到原因。
4.4 缺乏真实性,因此 AES-CBC-HMAC
到目前为止,我们未能解决一个根本性的缺陷:在 CBC 模式下,密文以及 IV 仍然可以被攻击者修改。实际上,没有完整性机制来防止这种情况!密文或 IV 的更改可能导致解密时出现意外的变化。例如,在 AES-CBC(使用 CBC 模式的 AES),攻击者可以通过翻转 IV 和密文中的特定位来翻转明文的特定位。我在图 4.10 中说明了这种攻击。

图 4.10 拦截 AES-CBC 密文的攻击者可以执行以下操作:(1)因为 IV 是公开的,所以将 IV 的位(例如从 1 到 0)进行翻转,也会(2)翻转第一个明文块的位。 (3)密文块上也可能发生位的修改。 (4)这样的更改会影响解密后的下一个明文块。 (5)请注意,篡改密文块会直接影响到该块的解密。
因此,密码或操作模式不能直接使用。它们缺乏某种完整性保护,以确保密文及其关联参数(这里是 IV)在没有触发警报的情况下无法修改。
为了防止对密文的修改,我们可以使用我们在第三章中看到的 消息认证码(MAC)。对于 AES-CBC,我们通常使用 HMAC(用于 基于哈希的 MAC )与 SHA-256 哈希函数结合使用来提供完整性。然后我们在对明文进行填充并将其加密后,将 MAC 应用于密文和 IV 上;否则,攻击者仍然可以修改 IV 而不被发现。
警告 这种构造称为 加密后进行认证。替代方案(如 认证后进行加密)有时可能会导致巧妙的攻击(如著名的 Vaudenay 填充预言攻击),因此在实践中要避免使用。
创建的认证标签可以与 IV 和密文一起传输。通常,它们全部连接在一起,如图 4.11 所示。此外,最佳实践是为 AES-CBC 和 HMAC 使用不同的密钥。

图 4.11 AES-CBC-HMAC 构造产生三个参数,通常按以下顺序连接:公共 IV、密文和认证标签。
在解密之前,需要验证标签(正如您在第三章中看到的那样,以恒定时间)。所有这些算法的组合被称为 AES-CBC-HMAC,直到我们开始采用更现代的一体化构造为止,它是最广泛使用的经过身份验证的加密模式之一。
警告 AES-CBC-HMAC 不是最开发者友好的构造。它经常实现不良,而且在使用不正确时存在一些危险的陷阱(例如,每次加密的 IV 必须 是不可预测的)。我花了几页的篇幅介绍这个算法,因为它仍然被广泛使用且仍然有效,但我建议不要使用它,而是使用我接下来介绍的更现代的构造。
4.5 一体化构造:经过身份验证的加密
加密的历史并不美好。不仅人们很少意识到没有认证的加密是危险的,而且错误地应用认证也是开发人员经常犯的系统性错误。因此,出现了大量研究,旨在标准化简化开发人员使用加密的全合一构造。在本节的其余部分,我将介绍这个新概念以及两种广泛采用的标准:AES-GCM 和 ChaCha20-Poly1305。
4.5.1 什么是带有关联数据的认证加密(AEAD)?
目前加密数据的最新方式是使用一种名为带有关联数据的认证加密(AEAD)的全合一构造。该构造与 AES-CBC-HMAC 提供的内容极为接近,因为它在保护明文的同时检测可能发生在密文上的任何修改。此外,它提供了一种验证关联数据的方法。
关联数据参数是可选的,可以为空,也可以包含与明文的加密和解密相关的元数据。这些数据不会被加密,要么是隐含的,要么与密文一起传输。此外,密文的大小比明文大,因为现在它包含了一个额外的认证标签(通常附加在密文的末尾)。
要解密密文,我们需要使用相同的隐含或传输的关联数据。结果要么是错误,表示密文在传输过程中被修改,要么是原始明文。我在图 4.12 中说明了这个新的原语。

图 4.12 Alice 和 Bob 亲自会面以达成共享密钥。然后 Alice 可以使用密钥使用 AEAD 加密算法将她的消息加密给 Bob。她可以选择验证一些关联数据(ad);例如,消息的发送者。收到密文和认证标签后,Bob 可以使用相同的密钥和关联数据解密。如果关联数据不正确或密文在传输过程中被修改,解密将失败。
让我们看看如何使用加密库来使用认证加密原语进行加密和解密。为此,我们将使用 JavaScript 编程语言和 Web Crypto API(大多数浏览器支持的官方接口,提供低级加密功能),如下列表所示。
列表 4.1 在 JavaScript 中使用 AES-GCM 进行认证加密
let config = {
name: 'AES-GCM',
length: 128 // ❶
};
let keyUsages = ['encrypt', 'decrypt'];
let key = await crypto.subtle.generateKey(config, false, keyUsages);
let iv = new Uint8Array(12);
await crypto.getRandomValues(iv); // ❷
let te = new TextEncoder();
let ad = te.encode("some associated data"); // ❸
let plaintext = te.encode("hello world");
let param = {
name: 'AES-GCM',
iv: iv,
additionalData: ad
};
let ciphertext = await crypto.subtle.encrypt(param, key, plaintext);
let result = await window.crypto.subtle.decrypt( // ❹
param, key, ciphertext); // ❹
new TextDecoder("utf-8").decode(result);
❶ 生成一个 128 位密钥,提供 128 位的安全性
❷ 随机生成一个 12 字节的 IV
❸ 使用一些关联数据来加密我们的明文。解密必须使用相同的 IV 和关联数据。
❹ 如果 IV、密文或关联数据被篡改,解密将抛出异常。
请注意,Web Crypto API 是一个低级 API,因此并不会帮助开发人员避免错误。例如,它让我们指定 IV,这是一种危险的模式。在此列表中,我使用了 AES-GCM,这是最广泛使用的 AEAD。接下来,让我们更多地了解 AES-GCM。
4.5.2 AES-GCM AEAD
最广泛使用的 AEAD 是 Galois/Counter Mode (缩写为 AES-GCM) 的 AES。它通过利用 AES 的硬件支持以及使用可以有效实现的 MAC(GMAC),被设计为高性能。
AES-GCM 自 2007 年起已被包括在 NIST 的特殊出版物(SP 800-38D)中,它是用于加密协议的主要密码,包括 TLS 协议的多个版本,该协议用于安全连接到互联网上的网站。实际上,我们可以说 AES-GCM 加密了网络。
AES-GCM 结合了 AES 中的 Counter (CTR) 模式和 GMAC 消息认证码。首先,让我们看看 CTR 模式如何与 AES 结合使用。图 4.13 展示了 AES 如何与 CTR 模式一起使用。

图 4.13 将 AES 密码与操作模式 Counter(CTR 模式)结合使用的 AES-CTR 算法。将唯一的随机数与计数器串联,并加密以产生密钥流。然后,将密钥流与实际的明文字节进行异或运算以产生加密。
AES-CTR 使用 AES 来加密一个随机数和一个数字(从 1 开始),而不是明文。这个额外的参数,“一个用于数字一次的随机数”,起到与 IV 相同的作用:它允许操作模式对 AES 加密进行随机化。然而,其要求与 CBC 模式的 IV 有些不同。一个随机数需要是唯一的,但不需要是不可预测的。一旦这个 16 字节的块被加密,结果被称为密钥流,它与实际的明文进行异或运算以产生加密结果。
非分裂密钥 (IVs) 一样,随机数(nonces)是密码学中常见的术语,在不同的密码学原语中都有出现。随机数可能有不同的要求,尽管其名称通常暗示着不应该重复使用。但通常情况下,重要的是手册上说了什么,而不是参数名称暗示了什么。事实上,AES-GCM 的随机数有时被称为 IV。
AES-CTR 中的随机数为 96 位(12 字节),大部分用于加密 16 字节的内容。剩下的 32 位(4 字节)作为计数器,从 1 开始,并在每个块加密时递增,直到达到其最大值为 2^(4×8) – 1 = 4,294,967,295. 这意味着,最多可以使用相同的随机数加密 4,294,967,295 个 128 位块(少于 69 GB)。
如果相同的随机数被使用两次,将创建相同的密钥流。通过对两个密文进行异或运算,可以取消密钥流,并且可以恢复两个明文的异或结果。这可能是毁灭性的,特别是如果你对两个明文的内容有一些了解。

图 4.14 如果 AES-CTR 的密钥流比明文长,则在与明文进行异或之前将其截断为与明文相同的长度。这使得 AES-CTR 可以在不填充的情况下工作。
图 4.14 展示了 CTR 模式的一个有趣特点:不需要填充。我们说它将分组密码(AES)转变为流密码。它按字节加密明文。
流密码
流密码是密码的另一类。它们与分组密码不同,因为我们可以直接使用它们通过与密钥流进行异或来加密密文。无需填充或操作模式,允许密文与明文长度相同。
在实践中,这两类密码之间没有太大的区别,因为通过 CTR 操作模式,分组密码很容易转换为流密码。但是,在理论上,分组密码具有优势,因为它们在构建其他类别的基元时可能会有用(类似于第二章中所见的哈希函数)。
此时也是值得注意的好时机,默认情况下,加密不会(或很差地)隐藏您正在加密的内容的长度。因此,在加密之前使用压缩可能会导致攻击,如果攻击者可以影响正在加密的部分。
AES-GCM 的第二部分是GMAC。它是从带有密钥散列(称为GHASH)构造的 MAC。从技术角度来看,GHASH 是几乎异或的通用哈希(AXU),也称为差异不可预测函数(DUF)。这样的函数的要求比哈希要弱。例如,AXU 不需要抗碰撞性。由于这个原因,GHASH 可以显着加快速度。图 4.15 说明了 GHASH 算法。

图 4.15 GHASH 使用密钥并以类似 CBC 模式的方式逐块吸收输入。它产生一个 16 字节的摘要。
使用 GHASH 进行哈希时,我们将输入分成 16 字节的块,然后以类似 CBC 模式的方式对它们进行哈希。由于此哈希需要一个密钥作为输入,因此理论上可以用作 MAC,但只能用一次(否则,算法就会破坏)—这是一次性 MAC。由于这对我们来说不理想,我们使用一种技术(由 Wegman-Carter 提出)将 GHASH 转换为多次 MAC。我在图 4.16 中进行了说明。

图 4.16 GMAC 使用带有密钥的 GHASH 对输入进行哈希,然后使用不同的密钥和 AES-CTR 进行加密,以生成认证标签。
GMAC 实际上是使用 AES-CTR(和不同的密钥)加密 GHASH 输出。再次强调,随机数必须是唯一的;否则,聪明的攻击者可以恢复 GHASH 使用的认证密钥,这将是灾难性的,并且将允许轻松伪造认证标签。
最后,AES-GCM 可以被看作是 CTR 模式和 GMAC 的交织组合,类似于我们之前讨论的加密-然后-MAC 构造。我在图 4.17 中说明了整个算法。

图 4.17 AES-GCM 通过使用对称密钥K的 AES-CTR 来加密明文,并使用 GMAC 来使用认证密钥H对相关数据和密文进行认证。
计数器从 1 开始加密,将 0 计数器留给由 GHASH 创建的加密标签。GHASH 反过来使用独立密钥H,这是使用密钥K对全零块进行加密。这样,一个密钥K就足以派生另一个密钥,不需要携带两个不同的密钥。
正如我之前所说,AES-GCM 的 12 字节 nonce 需要是唯一的,因此永远不会重复。请注意,它不需要是随机的。因此,一些人喜欢将其用作计数器,从 1 开始逐个加密。在这种情况下,必须使用一个允许用户选择 nonce 的加密库。这样可以在达到 nonce 的最大值之前加密 2^(12×8) - 1 条消息。可以说,这是一个在实践中无法达到的消息数量。
另一方面,拥有计数器意味着需要保持状态。如果一台机器在错误的时间崩溃,可能会发生 nonce 重用。因此,有时候更倾向于使用随机 nonce。实际上,一些库不允许开发人员选择 nonce,并会随机生成 nonce。这样做可以避免高概率重复,实际上不应该发生这种情况。然而,加密的消息越多,使用的 nonce 越多,发生碰撞的几率就越高。由于我们在第二章讨论的生日界限,建议在随机生成 nonce 时不要使用相同密钥加密超过 2^(92/3) ≈ 2³⁰ 条消息。
超越生日界限安全性
2³⁰ 条消息是相当大量的消息。在许多情况下可能永远不会达到这个数量,但现实世界的加密通常会推动被认为是合理的极限。一些长期存在的系统需要每秒加密许多消息,最终达到这些极限。例如,Visa 每天处理 1.5 亿笔交易。如果需要用唯一密钥加密这些交易,它将在仅一周内达到 2³⁰ 条消息的限制。在这些极端情况下,重新生成密钥(更改用于加密的密钥)可能是一个解决方案。还存在一个名为超越生日界限安全性的研究领域,旨在提高可以使用相同密钥加密的最大消息数量。
4.5.3 ChaCha20-Poly1305
我将要讨论的第二个 AEAD 是ChaCha20-Poly1305。它是两个算法的组合:ChaCha20 流密码和 Poly1305 MAC。这两个算法分别由 Daniel J. Bernstein 设计,用于在软件中快速使用,与 AES 相反,当硬件支持不可用时速度较慢。2013 年,Google 标准化了 ChaCha20-Poly1305 AEAD,以便在依赖低端处理器的 Android 手机中使用。如今,它被广泛应用于像 OpenSSH、TLS 和 Noise 这样的互联网协议中。
ChaCha20 是 Salsa20 流密码的修改版,最初由 Daniel J. Bernstein 在 2005 年左右设计。它是 ESTREAM 竞赛中的提名算法之一(www.ecrypt.eu.org/stream/)。与所有流密码一样,该算法生成一个密钥流,一个与明文长度相同的随机字节序列。然后将其与明文进行异或运算以创建密文。要解密,使用相同的算法生成相同的密钥流,将其与密文进行异或运算以还原明文。我在图 4.18 中说明了这两个流程。

图 4.18 ChaCha20 通过使用对称密钥和唯一随机数生成密钥流,然后将其与明文(或密文)进行异或运算以生成密文(或明文)。加密是保持长度不变的,因为密文和明文长度相同。
在内部,ChaCha20 通过反复调用块函数生成许多 64 字节的密钥流块来生成密钥流。
-
一个 256 位(32 字节)的类似 AES-256 的密钥
-
一个 92 位(12 字节)的类似 AES-GCM 的随机数
-
一个 32 位(4 字节)的类似 AES-GCM 的计数器
加密过程与 AES-CTR 相同。(我在图 4.19 中说明了这个流程。)
-
运行块函数,每次递增计数器,直到产生足够的密钥流
-
将密钥流截断到与明文长度相同
-
将密钥流与明文进行异或运算

图 4.19 ChaCha20 的密钥流是通过调用内部块函数生成足够的字节而创建的。一个块函数调用会创建 64 字节的随机密钥流。
由于计数器的上限,你可以使用 ChaCha20 加密与 AES-GCM 相同数量的消息(因为它是由类似的随机数参数化的)。由于这个块函数创建的输出要大得多,你可以加密的消息大小也会受到影响。你可以加密大小为 232 × 64 字节 ≈ 274 GB 的消息。如果重复使用一个随机数来加密明文,会出现与 AES-GCM 类似的问题。观察者可以通过对两个密文进行异或运算来获取两个明文的异或结果,并且还可以恢复随机数的认证密钥。这些是严重的问题,可能导致攻击者能够伪造消息!
随机数和计数器的大小
Nonce 和计数器的大小实际上并不总是相同(对于 AES-GCM 和 ChaCha20-Poly1305 都是如此),但它们是采用的标准推荐值。尽管如此,一些加密库接受不同大小的 nonce,一些应用程序增加计数器(或 nonce)的大小以允许加密更大的消息(或更多的消息)。增加一个组件的大小必然会减少另一个组件的大小。
为了防止这种情况,同时允许在单个密钥下加密大量消息,还有其他标准可用,例如 XChaCha20-Poly1305。这些标准增加了 nonce 的大小,同时保持其余部分不变,这在需要随机生成 nonce 而不是在系统中跟踪计数器的情况下很重要。
在 ChaCha20 块函数内部,形成一个状态。图 4.20 说明了这个状态。

图 4.20 ChaCha20 块函数的状态。它由 16 个字(每个字 32 字节)组成。第一行存储一个常量,第二和第三行存储 32 字节的对称密钥,接下来的一个字存储一个 4 字节的计数器,最后 3 个字存储 12 字节的 nonce。
这个状态然后通过将一个轮函数迭代 20 次(因此算法名称中有 20)转换为 64 字节的密钥流。这类似于 AES 及其轮函数的处理方式。轮函数本身每轮调用一次 Quarter Round(QR)函数,每次在内部状态的不同字上操作,具体取决于轮数是奇数还是偶数。图 4.21 展示了这个过程。

图 4.21 ChaCha20 中的一轮影响状态中包含的所有字。由于 Quarter Round (QR) 函数只接受 4 个参数,所以必须至少在不同的字上调用 4 次(在图表中显示为灰色)才能修改状态的所有 16 个字。
QR 函数接受四个不同的参数,并仅使用加法、旋转和异或操作来更新它们。我们说这是一个 ARX 流密码。这使得 ChaCha20 在软件中非常容易实现且速度快。
Poly1305 是通过 Wegman-Carter 技术创建的 MAC,与我们之前讨论的 GMAC 类似。图 4.22 说明了这个加密 MAC。

图 4.22 Poly1305 的核心函数通过每次接收一个输入块并取一个额外的累加器(最初设置为 0)和一个认证密钥 r 来吸收输入。输出被作为累加器馈送到下一个核心函数的调用。最终输出加上一个随机值 s 以成为认证标签。
在图中,r 可以看作是方案的认证密钥,就像 GMAC 的认证密钥 H 一样。而 s 通过加密结果使得 MAC 对多次使用具有安全性,因此它必须对每次使用都是唯一的。
Poly1305 核心函数将密钥与累加器(初始设置为 0)和要认证的消息混合在一起。操作是简单的乘法,对一个常数P取模。
注意 显然,我们的描述中缺少很多细节。我很少提到如何对数据进行编码或如何在执行之前对某些参数进行填充。这些都是实现特定的细节,对我们来说并不重要,因为我们正在努力理解这些事物的工作原理。
最终,我们可以使用 ChaCha20 和计数器设置为 0 来生成一个密钥流,并推导出我们需要的 16 字节r和 16 字节s值,以用于 Poly1305。我在图 4.23 中展示了结果的 AEAD 密码。

图 4.23 ChaCha20-Poly1305 通过使用 ChaCha20 加密明文并推导出 Poly1305 MAC 所需的密钥来工作。然后 Poly1305 用于认证密文以及相关数据。
首先使用普通的 ChaCha20 算法推导出 Poly1305 所需的认证密钥r和s。然后,计数器递增,并使用 ChaCha20 加密明文。之后,相关数据和密文(以及它们各自的长度)被传递给 Poly1305 以创建认证标签。
要解密,将应用完全相同的过程。ChaCha20 首先通过收到的标签验证密文和相关数据的认证。然后解密密文。
4.6 其他类型的对称加密
让我们暂停一下,回顾一下你迄今学到的对称加密算法:
-
非认证加密—带有操作模式但不带 MAC 的 AES。在实践中不安全,因为密文可能会被篡改。
-
认证加密—AES-GCM 和 ChaCha20-Poly1305 是两种最广泛采用的密码。
章节到此结束也没有问题。然而,现实世界的密码学并不总是遵循约定的标准;它还涉及到大小、速度、格式等方面的限制。因此,让我简要介绍一下当 AES-GCM 和 ChaCha20-Poly1305 不适用时可以有用的其他类型的对称加密。
4.6.1 密钥包装
基于 Nonce 的 AEAD 的问题之一是它们都需要一个 Nonce,这需要额外的空间。注意,当加密密钥时,您可能并不一定需要随机化,因为加密的内容已经是随机的,并且不会以高概率重复(或者如果它确实重复,这并不重要)。一个众所周知的密钥包装标准是 NIST 的 Special Publication 800-38F:“Recommendation for Block Cipher Modes of Operation: Methods for Key Wrapping。”这些密钥包装算法不需要额外的 Nonce 或 IV,并且根据它们加密的内容进行随机化。由于这一点,它们不必在密文旁边存储额外的 Nonce 或 IV。
4.6.2 防止滥用 Nonce 的认证加密
2006 年,菲利普·罗加韦(Phillip Rogaway)发布了一种名为合成初始化向量(SIV)的新密钥包装算法。作为提案的一部分,罗加韦指出,SIV 不仅对加密密钥有用,而且作为一种更能容忍重复 nonce 的通用 AEAD 方案。正如你在本章中学到的那样,在 AES-GCM 或 ChaCha20-Poly1305 中重复的 nonce 可能会导致灾难性后果。它不仅会揭示两个明文的异或,还允许攻击者恢复身份验证密钥并伪造消息的有效加密。
防止 nonce 误用的算法的要点是,使用相同的 nonce 加密两个明文只会显示两个明文是否相等,仅此而已。这并不理想,但显然不像泄漏身份验证密钥那样糟糕。该方案引起了很多关注,并且自那时起已被标准化为 RFC 8452:“AES-GCM-SIV:防止 nonce 误用的身份验证加密”。SIV 背后的诀窍是 AEAD 中使用的 nonce 是从明文本身生成的,这使得两个不同的明文最终被加密为相同的 nonce 的可能性极小。
4.6.3 磁盘加密
加密笔记本电脑或手机的存储有一些严重的限制:它必须快速(否则用户会注意到),而且只能在原地执行(对于大量设备来说,节省空间很重要)。由于加密不能扩展,需要一个 nonce 和身份验证标签的 AEAD 并不适合。相反,使用未经身份验证的加密。
为了防止位翻转攻击,大块(数千字节)数据的加密方式是,单个位翻转会使整个块的解密混乱。这样一来,攻击更有可能导致设备崩溃而不是达到其目标。这些构造被称为宽块密码,尽管这种方法也被称为穷人的身份验证。
Linux 系统和一些 Android 设备采用了这种方法,使用了 Adiantum,这是一种包装 ChaCha 密码的宽块构造,并于 2019 年由 Google 标准化。尽管如此,大多数设备仍然使用非理想的解决方案:微软和苹果都使用 AES-XTS,这是未经身份验证的,也不是宽块密码。
4.6.4 数据库加密
在数据库中加密数据很棘手。因为整个目的是防止数据库泄漏数据,所以用于加密和解密数据的密钥必须远离数据库服务器。因为客户端没有数据本身,所以它们在查询数据的方式上受到严重限制。
最简单的解决方案称为透明数据加密(TDE),只需加密选择的列。在某些情况下,这种方法效果很好,尽管需要小心对待用于标识正在加密的行和列的相关数据进行认证;否则,加密内容可能会被替换。但是,不能通过加密的数据进行搜索,因此查询必须使用未加密的列。
可搜索加密是旨在解决此问题的研究领域。已经提出了许多不同的方案,但似乎没有灵丹妙药。不同的方案提出了不同级别的“可搜索性”以及不同程度的安全降级。例如,盲目索引仅允许您搜索完全匹配,而保序和透露排序的加密允许您对结果进行排序。总的来说,这些解决方案的安全性需要仔细考虑,因为它们确实是一种权衡。
摘要
-
加密(或对称加密)是一种加密原语,可用于保护数据的机密性。安全性依赖于一个需要保密的对称密钥。
-
对称加密需要经过身份验证(之后我们称之为认证加密)才能确保安全,否则密文可能会被篡改。
-
认证加密可以通过使用消息认证码从对称加密算法构建。但最佳做法是使用关联数据认证加密(AEAD)算法,因为它们是一体化构造,更难被误用。
-
两个参与方可以使用认证加密来隐藏他们的通信,只要他们都知道相同的对称密钥。
-
AES-GCM 和 ChaCha20-Poly1305 是目前广泛采用的两种 AEAD(Authenticated Encryption with Associated Data)。如今大多数应用程序都使用其中一种。
-
重用一次性密码会破坏 AES-GCM 和 ChaCha20-Poly1305 的认证。诸如 AES-GCM-SIV 这样的方案是免受一次性密码误用的,而加密密钥可以避免该问题,因为一次性密码不是必需的。
-
现实世界的密码学涉及到约束,AEAD 并不总能适用于每种情况。例如,数据库或磁盘加密就需要开发新的构造。
第五章:密钥交换
本章涵盖
-
密钥交换是什么以及它们如何有用
-
Diffie-Hellman 和椭圆曲线 Diffie-Hellman 密钥交换
-
使用密钥交换时的安全考虑
现在我们进入了 非对称加密 领域(也称为 公钥加密 ) ,我们的第一个非对称加密原语:密钥交换。密钥交换正如其名称所示,是密钥的交换。例如,Alice 发送一个密钥给 Bob,Bob 发送一个密钥给 Alice。这使得两个对等方可以达成共识,产生一个共享密钥,然后可以使用认证加密算法对通信进行加密。
警告 正如我在本书引言中所暗示的,非对称加密涉及更多的数学;因此,接下来的章节对某些读者来说可能会更加困难。不要气馁!本章学到的内容将有助于理解基于相同基础的许多其他原语。
注意 对于本章,你需要已经阅读了第三章关于消息认证码和第四章关于认证加密。
5.1 什么是密钥交换?
让我们首先看一个场景,Alice 和 Bob 都想要私下交流,但之前从未互相交谈过。这将激发出在最简单的情况下密钥交换可以解锁什么。
要加密通信,Alice 可以使用你在第四章中了解到的认证加密原语。为此,Bob 需要知道相同的对称密钥,以便 Alice 可以生成一个并将其发送给 Bob。之后,他们可以简单地使用该密钥来加密他们的通信。但是,如果有人窃听他们的对话怎么办?现在敌人拥有对称密钥,可以解密 Alice 和 Bob 互相发送的所有加密内容!这就是在这种情况下使用密钥交换可以对 Alice 和 Bob(以及我们将来自己)有趣的地方。通过使用密钥交换,他们可以获得一个被动观察者无法复制的对称密钥。
密钥交换 从 Alice 和 Bob 生成一些密钥开始。为此,他们都使用一个密钥生成算法,生成一个密钥对:一个私钥(或秘密密钥)和一个公钥。然后 Alice 和 Bob 将各自的公钥发送给对方。这里的 公开 意味着敌人可以观察到,但不会产生后果。然后 Alice 使用 Bob 的公钥和她自己的私钥计算共享密钥。同样地,Bob 可以使用他的私钥和 Alice 的公钥来获得相同的共享密钥。我在图 5.1 中说明了这一点。

图 5.1 一个密钥交换提供以下接口:它采用你的对等方的公钥和你的私钥生成一个共享密钥。你的对等方可以通过使用你的公钥和他们自己的私钥获得相同的共享密钥。
从高层次了解密钥交换的工作原理后,我们现在可以回到我们的初始情景,看看这如何帮助。通过以密钥交换开始他们的通信,Alice 和 Bob 生成了一个共享的密钥,用作身份验证加密原语的密钥。因为任何观察交换的中间人(MITM)对手都无法推导出相同的共享密钥,他们将无法解密通信。我在图 5.2 中说明了这一点。

图 5.2 两个参与者之间的密钥交换使他们能够就一个密钥达成一致,而中间人(MITM)对手无法通过被动观察密钥交换来推导出相同的密钥。
请注意,这里的 MITM 是被动的;一个主动的MITM 将没有问题拦截密钥交换并冒充双方。在这种攻击中,Alice 和 Bob 实际上将与 MITM 执行密钥交换,都认为他们已经就密钥达成了一致。之所以可能是因为我们的任何一个角色都没有办法验证他们收到的公钥真正属于谁。这个密钥交换是未经身份验证的!我在图 5.3 中说明了这次攻击。

图 5.3 一个未经身份验证的密钥交换容易受到主动的中间人攻击。事实上,攻击者可以简单地冒充连接的双方并执行两次单独的密钥交换。
让我们看一个不同的情景来激发经过身份验证的密钥交换。想象一下,你想运行一个给你提供当天时间的服务。然而,你不希望这个信息被中间人攻击者修改。你最好的选择是使用你在第三章学到的消息认证码(MACs)对你的响应进行身份验证。由于 MACs 需要一个密钥,你可以简单地生成一个并手动与所有用户共享。但是,现在任何用户都拥有与其他用户一起使用的 MAC 密钥,并且可能有一天会利用它对其他人执行前面讨论的 MITM 攻击。你可以为每个用户设置不同的密钥,但这也不理想。对于想要连接到你的服务的每个新用户,你都需要手动为你的服务和用户提供一个新的 MAC 密钥。如果服务器端不需要做任何事情就好多了,是不是?
密钥交换可以在这里发挥作用!你可以做的是让你的服务生成一个密钥交换密钥对,并向服务的任何新用户提供服务的公钥。这被称为身份验证密钥交换;你的用户知道服务器的公钥,因此,主动的中间人对手无法冒充该密钥交换的一方。然而,一个恶意的人可以做的是执行他们自己的密钥交换(因为连接的客户端未经身份验证)。顺便说一句,当双方都经过身份验证时,我们称之为双向身份验证密钥交换。
这种情况非常普遍,密钥交换原语使其能够随着用户数量的增加而扩展得很好。但是,如果服务数量也增加,这种情况就不容易扩展!互联网就是一个很好的例子。我们有许多浏览器试图与许多网站进行安全通信。想象一下,如果你不得不在浏览器中硬编码你可能有一天会访问的所有网站的公钥,以及当更多的网站上线时会发生什么?
虽然密钥交换很有用,但在没有姊妹原语——数字签名的情况下,并不是所有情况下都能很好地扩展。不过这只是一个引子。在第七章中,你将了解到有关这种新的密码原语以及它如何帮助系统中的信任扩展的信息。密钥交换在实践中很少直接使用。它们通常只是更复杂协议的组成部分。话虽如此,在某些情况下它们仍然可以是有用的(例如,正如我们之前对抗被动对手时看到的)。
现在让我们看看在实践中如何使用密钥交换密码原语。libsodium 是最知名和广泛使用的 C/C++ 库之一。以下示例显示了在实践中如何使用 libsodium 来执行密钥交换。
5.1 C 语言中的密钥交换示例
unsigned char client_pk[crypto_kx_PUBLICKEYBYTES]; // ❶
unsigned char client_sk[crypto_kx_SECRETKEYBYTES]; // ❶
crypto_kx_keypair(client_pk, client_sk); // ❶
unsigned char server_pk[crypto_kx_PUBLICKEYBYTES]; // ❷
obtain(server_pk); // ❷
unsigned char decrypt_key[crypto_kx_SESSIONKEYBYTES]; // ❸
unsigned char encrypt_key[crypto_kx_SESSIONKEYBYTES]; // ❸
if (crypto_kx_client_session_keys(decrypt_key, encrypt_key,
client_pk, client_sk, server_pk) != 0) { // ❹
abort_session(); // ❺
}
❶ 生成客户端的密钥对
❷ 我们假设我们有一种获取服务器公钥的方式。
❸ libsodium 根据最佳实践派生两个对称密钥,而不是一个;每个密钥用于加密单个方向。
❹ 我们使用我们的秘密密钥和服务器的公钥进行密钥交换。
❺ 如果公钥格式错误,则函数返回错误。
libsodium 将许多细节隐藏在开发者之外,同时还公开了安全可用的接口。在这种情况下,libsodium 使用 X25519 密钥交换算法,你将在本章后面更多了解这个算法。在本章的其余部分,你将了解有关密钥交换的不同标准以及它们在幕后的工作原理。
5.2 Diffie-Hellman(DH)密钥交换
1976 年,Whitfield Diffie 和 Martin E. Hellman 发表了题为“密码学的新方向”的关键论文,介绍了 Diffie-Hellman(DH)密钥交换算法。多么响亮的标题啊!DH 是第一个发明的密钥交换算法,也是第一个公钥加密算法的正式化之一。在本节中,我将阐述该算法的数学基础,解释其工作原理,并最终讨论规定如何在加密应用中使用它的标准。
5.2.1 群论
DH 密钥交换建立在一种称为群论的数学领域之上,这是当今大多数公钥加密的基础。因此,在本章中,我将花一些时间向你介绍群论的基础知识。我将尽力提供有关这些算法如何工作的深入见解,但无论如何,这都将涉及到一些数学。
让我们从一个显而易见的问题开始:什么是群?它有两个方面:
-
一组元素
-
在这些元素上定义的特殊二元运算(例如 + 或 ×)
如果集合和运算能够满足一些属性,那么我们就有了一个群。如果我们有一个群,那么我们就可以做出神奇的事情......(稍后详述)。注意,DH 工作在一个乘法群中:一种使用乘法作为定义的二元运算的群。由于这一点,其余的解释使用乘法群作为示例。我也经常省略乘号符号(例如,我会将 a × b 写成 ab)。
我需要在这里更加具体。为了使集合及其运算成为一个群,它们需要具有以下特性。(和往常一样,我会在图 5.4 中以更加视觉化的方式来说明这些特性,以提供更多材料来理解这个新概念。)
-
封闭性——对两个元素进行操作会得到同一集合的另一个元素。例如,对于群的两个元素 a 和 b,a × b 会得到另一个群元素。
-
结合性——同时对几个元素进行操作可以按任意顺序进行。例如,对于群的三个元素 a、b 和 c,那么 a(bc) 和 (ab)c 会得到相同的群元素。
-
单位元素——与此元素进行运算不会改变另一个操作数的结果。例如,我们可以在我们的乘法群中将单位元素定义为 1。对于任何群元素 a,我们有 a × 1 = a。
-
逆元素——存在一个逆元素与所有群元素相对应。例如,对于任何群元素 a,都存在一个逆元素 a^(–1)(也写作 1/a),使得 a × a^(–1) = 1(也写作 a × 1/a = 1)。

图 5.4 群的四个属性:封闭性、结合性、单位元素和逆元素。
我可以想象我对群的解释可能有点抽象,所以让我们看看 DH 在实践中使用的群是什么。首先,DH 使用由严格正整数集合组成的群:1、2、3、4、····、p – 1,其中 p 是素数,1 是单位元素。不同的标准为 p 指定不同的数字,但直观地说,它必须是一个大素数,以确保群的安全性。
素数
素数 是只能被 1 或它本身整除的数字。前几个素数是 2、3、5、7、11 等等。素数在非对称密码学中随处可见!而且,幸运的是,我们有高效的算法来找到大素数。为了加快速度,大多数密码库会寻找伪素数(有很高概率是素数的数字)。有趣的是,此类优化在过去几次被打破过;最臭名昭著的一次发生在 2017 年,当时 ROCA 漏洞发现了超过一百万台设备为其密码应用生成了不正确的素数。
第二,DH 使用 模乘法 作为一种特殊操作。在我解释模乘法是什么之前,我需要解释什么是 模算术。直观地说,模算术是关于在达到一个称为模数的某个数后“环绕”的数字。例如,如果我们将模数设置为 5,我们说超过 5 的数字回到 1;例如,6 变成 1,7 变成 2,依此类推。(我们也将 5 记为 0,但因为它不在我们的乘法群中,所以我们不太在乎它。)
表达模算术的数学方式是取一个数与其模数的欧几里得除法的余数。让我们以数字 7 为例,并将其与 5 进行欧几里得除法得到 7 = 5 × 1 + 2。注意余数为 2。然后我们说 7 = 2 mod 5(有时写成 7 ≡ 2 (mod 5))。这个方程可以读作 7 对模 5 同余于 2。同样地
-
8 = 1 mod 7
-
54 = 2 mod 13
-
170 = 0 mod 17
-
等等
描绘这样一个概念的传统方式是用时钟。图 5.5 说明了这个概念。

图 5.5 整数模素数 5 的群可以被描绘成一个时钟,在数字 4 之后重新归零。因此 5 被表示为 0,6 被表示为 1,7 被表示为 2,8 被表示为 3,9 被表示为 4,10 被表示为 0,依此类推。
在这样一组数字上定义模乘法是相当自然的。让我们以以下乘法为例:
3 × 2 = 6
根据你之前学到的知识,你知道 6 对模 5 同余于 1,因此方程可以重写为:
3 × 2 = 1 mod 5
相当直接,是吗?请注意,前一个方程告诉我们 3 是 2 的倒数,反之亦然。我们也可以写成以下形式:
3^(–1) = 2 mod 5
当上下文清晰时,方程中的模数部分(此处为 mod 5)通常会被省略。所以如果我在这本书中有时省略了它,请不要感到惊讶。
注 事实上,当我们使用素数模下的正数时,只有零元素缺乏逆元。(确实,你能找到一个元素 b 使得 0 × b = 1 mod 5 吗?)这就是为什么我们不将零包含在群的元素中的原因。
好的,现在我们有了一个组,其中包括严格正整数 1、2、···、p – 1,p 是一个素数,以及模乘法。我们形成的组也恰好是两者:
-
交换性——操作的顺序不重要。例如,给定两个群元素 a 和 b,则 ab = ba。具有此属性的群通常被称为伽罗瓦群。
-
有限域——具有更多属性的伽罗瓦群,以及一个额外的运算(在我们的例子中,我们也可以将数字相加)。
由于最后一点,DH 定义在这种类型的群上有时被称为有限域 Diffie-Hellman(FFDH)。如果你理解什么是群(并确保在继续阅读之前理解),那么子群只是原始群中包含的一个群。也就是说,它是群元素的子集。在子群元素上操作会产生另一个子群元素,并且每个子群元素在子群中都有一个逆元素,等等。
循环子群是可以从单个生成器(或基数)生成的子群。生成器通过反复相乘来生成循环子群。例如,生成器 4 定义了由数字 1 和 4 组成的子群:
-
4 mod 5 = 4
-
4 × 4 mod 5 = 1
-
4 × 4 × 4 mod 5 = 4(我们从头开始)
-
4 × 4 × 4 × 4 mod 5 = 1
-
以此类推
注意 我们也可以将 4 × 4 × 4 写为 43。
恰好当我们的模数是素数时,我们群中的每个元素都是一个子群的生成器。这些不同的子群可以有不同的大小,我们称之为阶。我在图 5.6 中进行了说明。

图 5.6 不同的模 5 乘法群的子群。这些都包括数字 1(称为单位元素)并且具有不同的阶(元素数量)。
好了,现在你明白了
-
一个群是一组具有二元运算的数字集合,遵守一些性质(封闭性,结合性,单位元素,逆元素)。
-
DH 在 Galois 群(一个具有交换性的群)中运行,由严格正数组成,直到一个素数(不包括在内)和模乘法形成。
-
在 DH 群中,每个元素都是一个子群的生成器。
群是大量不同加密原语的中心。如果你想要理解其他加密原语的工作原理,对群论有良好的直觉是很重要的。
5.2.2 离散对数问题:Diffie-Hellman 的基础
DH 密钥交换的安全性依赖于群中的离散对数问题,这是一个被认为难以解决的问题。在本节中,我简要介绍这个问题。
想象一下,我拿一个生成器,比如说 3,然后给你一个它可以生成的随机元素,比如说 2 = 3^x mod 5,其中x对你来说是未知的。问你“x是多少?”就等同于让你找到基于 3 的 2 的离散对数。因此,在我们的群中,离散对数问题就是找出我们将生成器与自身相乘多少次才能产生给定的群元素。这是一个重要的概念!在继续之前花几分钟思考一下。
在我们的示例群中,你可以快速发现答案是 3(确实,3³ = 2 mod 5)。但是,如果我们选择一个比 5 大得多的素数,事情就变得复杂得多:变得难以解决。这就是 Diffie-Hellman 背后的秘密。现在你已经了解如何在 DH 中生成密钥对了:
-
所有参与者都必须就一个大素数 p 和一个生成器 g 达成一致。
-
每个参与者都生成一个随机数 x,这个数就成了他们的私钥。
-
每个参与者都根据 g^x mod p 推导出他们的公钥。
离散对数问题的“困难”意味着没有人应该能够从公钥中恢复出私钥。我在图 5.7 中进行了说明。

图 5.7 在 Diffie-Hellman 中选择私钥就像在生成器 g 产生的数字列表中选择索引一样。离散对数问题就是仅凭数字找到索引的问题。
尽管我们有算法来计算离散对数,但在实践中它们并不高效。另一方面,如果我给你问题的解 x,你就可以利用你手头上非常高效的算法来验证,确实我给你的是正确的解:g^x mod p。如果你感兴趣,计算模幂的最先进技术被称为“平方乘”。它通过逐位地遍历 x 来高效地计算结果。
注意就像密码学中的一切一样,仅仅通过猜测来找到解决方案是不可能的。然而,通过选择足够大的参数(在这里,一个大素数),可以将寻找解决方案的效果降低到可以忽略的几率。这意味着即使经过数百年的随机尝试,你找到解决方案的几率仍然在统计上接近于零。
很好。我们如何利用所有这些数学知识来进行 DH 密钥交换算法呢?想象一下
-
Alice 有一个私钥 a 和一个公钥 A = g^a mod p。
-
Bob 有一个私钥 b 和一个公钥 B = g^b mod p。
借助 Bob 的公钥,Alice 可以计算出共享秘密 B^a mod p。Bob 也可以利用 Alice 的公钥和他自己的私钥进行类似的计算:A^b mod p。自然地,我们可以看到这两个计算最终得到的结果是相同的:
B^a = (gb)a = g^(ab) = (ga)b = A^b mod p
这就是 DH 的魔力。从外部人士的角度来看,仅观察公钥 A 和 B 并不能以任何方式计算出密钥交换的结果 g^(ab) mod p。接下来,你将了解到现实世界的应用是如何利用这个算法以及存在的不同标准的。
计算和决策 Diffie-Hellman
顺便说一句,在理论密码学中,观察g^a mod p和g^b mod p并不会帮助你计算g^(ab) mod p的想法被称为计算 Diffie-Hellman 假设(CDH)。它经常与更强的决策 Diffie-Hellman 假设(DDH)混淆,直观地说明了在给定g^a mod p,g^b mod p和z mod p的情况下,没有人应该能够自信地猜测后者是否是两个公钥之间的密钥交换结果(g^(ab) mod p)还是组中的随机元素。这两者都是有用的理论假设,已被用于构建密码学中的许多不同算法。
5.2.3 Diffie-Hellman 标准
现在您已经了解了 DH 的工作原理,您可以理解参与者需要在一组参数上达成一致,具体来说是质数p和生成器g。在本节中,您将了解现实世界应用是如何选择这些参数以及存在的不同标准的。
首先是质数p。正如我之前所述,数字越大,效果越好。因为 DH 基于离散对数问题,其安全性与该问题已知的最佳攻击直接相关。该领域的任何进展都可能削弱算法。随着时间的推移,我们成功地对这些进展的速度(或缓慢程度)以及足够的安全性有了相当好的了解。目前已知的最佳实践是使用 2048 位的质数。
注意 一般来说,keylength.com 总结了常见加密算法的参数长度建议。结果来自研究组织或政府机构(如法国国家信息安全局(ANSSI)、美国国家标准与技术研究所(NIST)和德国联邦信息安全办公室(BSI))发布的权威文件。虽然它们并不总是一致,但它们通常会趋于类似数量级。
在过去,许多库和软件通常会生成和硬编码自己的参数。不幸的是,有时会发现它们要么是薄弱的,要么更糟,完全是破碎的。在 2016 年,有人发现了 Socat,一个流行的命令行工具,一年前已经使用了一个损坏的默认 DH 组,这就引发了一个问题,这是一个错误还是一个有意的后门。使用标准化的 DH 组可能看起来像一个更好的主意,但是 DH 是不幸的反例之一。在 Socat 问题发生几个月后,安东尼奥·桑索(Antonio Sanso)在阅读 RFC 5114 时发现,该标准也指定了损坏的 DH 组。
由于所有这些问题,更新的协议和库已经趋于要么弃用 DH 以支持椭圆曲线 Diffie-Hellman(ECDH),要么使用更好的标准 RFC 7919(www.rfc-editor.org/info/rfc7919)定义的群。因此,现在的最佳实践是使用 RFC 7919,它定义了几种不同大小和安全性的群。例如,ffdhe2048 是由 2,048 位素数模定义的群:
p = 3231700607131100730015351347782516336248805713348907517458843413926980683413621000279205636264016468
54585563579353308169288290230805734726252735547424612457410262025279165729728627063003252634282131457669
31414223654220941111348629991657478268034230553086349050635557712219187890332729569696129743856241741236
23722519734640269185579776797682301462539793305801522685873076119753243646747585546071504389684494036613
04976978128542959586595975670512838521327844685229255045682728791137200989318739591433741758378260002780
34973198552060607533234122603254684088120031105907484281003994966956119696956248629032338072839127039
以及生成器g = 2
注意 选择生成器的数字 2 是很常见的,因为计算机在使用简单的左移(<<)指令与 2 相乘时非常高效。
群大小(或order)也被指定为q = (p – 1)/2。这意味着私钥和公钥在大小上都会在 2,048 位左右。实际上,对于密钥来说,这些都是相当大的尺寸(例如,与通常为 128 位长的对称密钥相比)。您将在下一节中看到,通过定义椭圆曲线上的群,我们可以在相同的安全性下获得更小的密钥。
5.3 椭圆曲线 Diffie-Hellman(ECDH)密钥交换
结果证明,我们刚刚讨论的 DH 算法可以在不同类型的群中实现,而不仅仅是模素数的乘法群。事实证明,一个群可以由数学中研究的一种曲线——椭圆曲线构成。这个想法是由 Neal Koblitz 和 Victor S. Miller 在 1985 年独立提出的,而在 2000 年,当基于椭圆曲线的加密算法开始标准化时,这个想法得到了采纳。
应用密码学领域很快就采用了椭圆曲线密码学,因为它提供的密钥比上一代公钥密码学要小得多。与 DH 中建议的 2,048 位参数相比,椭圆曲线变体算法可以使用 256 位的参数。
5.3.1 什么是椭圆曲线?
现在让我们解释一下椭圆曲线是如何工作的。首先,首要的是要理解椭圆曲线只是曲线!这意味着它们由解方程的所有坐标x和y定义。具体来说,这个方程
y² + a[1]xy + a[3]y = x³ + a[2]x² + a[4]x + a[6]
对于一些a[1]、a[2]、a[3]、a[4]和a[6]。注意,对于今天的大多数实用曲线,这个方程可以简化为短 Weierstrass 方程:
y² = x³ + ax + b(其中 4a³ + 27b² ≠ 0)
虽然对于两种类型的曲线(称为二进制曲线和特征 3 的曲线),这种简化是不可能的,但这些曲线的使用频率很低,因此在本章的其余部分中我们将使用 Weierstrass 形式。图 5.8 显示了一个随机选取两个点的椭圆曲线示例。

图 5.8 一个由方程定义的椭圆曲线示例。
在椭圆曲线的历史上的某个时候,人们发现可以在其上构建一个群。从那时起,在这些群上实现 DH 就变得简单了。我将利用这一节来解释椭圆曲线密码学背后的直觉。
椭圆曲线上的群通常被定义为加法群。与前一节中定义的乘法群不同,这里使用的是+号。
注意 在实践中,使用加法或乘法都没有太大关系,这只是一种偏好。虽然大多数密码学使用乘法符号,但围绕椭圆曲线的文献更倾向于使用加法符号,因此,在本书中提及椭圆曲线群时,我将使用这种表示法。
这一次,我会在定义群的元素之前定义操作。我们的加法操作定义如下。图 5.9 说明了这个过程。
-
画一条穿过你想要相加的两个点的直线。这条直线在曲线上又碰到另一个点。
-
从这个新找到的点画一条垂直线。垂直线在曲线上又碰到另一个点。
-
这一点是将原始两点相加的结果。

图 5.9 通过几何方法可以在椭圆曲线的点上定义加法操作。
有两种特殊情况,这个规则不适用。我们也定义一下这两种情况:
-
我们如何将一个点加到自身?答案是画出该点的切线(而不是在两点之间画一条线)。
-
如果我们在第 1 步(或第 2 步)画的线不在曲线上碰到任何其他点会发生什么?嗯,这很尴尬,我们需要这种特殊情况来产生一个结果。解决方案是将结果定义为一个虚构的点(我们自己编造的)。这个新发明的点称为无穷远点(通常用大写字母O表示)。图 5.10 说明了这些特殊情况。

图 5.10 在图 5.9 的基础上构建,当将一个点与自身相加或当两个点相互抵消以得到无穷远点(O)时,也定义了在椭圆曲线上的加法。
我知道这个无穷点有些超级奇怪,但不要太担心。它实际上只是我们为了使加法运算有效而想出来的东西。哦,顺便说一下,它的行为就像一个零,它是我们的恒等元素:
O + O = O
对于曲线上的任意点P
P + O = P
一切都很好。到目前为止,我们看到要在椭圆曲线上创建一个群,我们需要
-
定义一组有效点的椭圆曲线方程。
-
在这个集合中定义加法的定义。
-
一个称为无穷点的虚拟点。
我知道这是很多需要理解的信息,但我们还缺少最后一点。椭圆曲线密码学利用之前讨论过的在有限域上定义的一种群类型。在实践中,这意味着我们的坐标是数字 1、2、···、p – 1,其中p是某个大素数。这应该听起来很熟悉!因此,当考虑椭圆曲线密码学时,您应该想象一个图形,它看起来更像图 5.11 右侧的图形。

图 5.11 椭圆曲线密码学(ECC)在实践中,主要是通过模一个大素数p的坐标椭圆曲线来指定。这意味着在密码学中使用的内容更像右图而不是左图。
就是这样!我们现在有了一个可以进行密码学运算的群,就像我们之前使用的是模一个素数的数字(排除 0)和 Diffie-Hellman 的乘法运算一样。我们如何在椭圆曲线上定义的这个群上进行 Diffie-Hellman 呢?现在让我们来看看在这个群中离散对数是如何工作的。
让我们取一个点G,并将其加上自身x次以通过我们定义的加法操作产生另一个点P。我们可以写成P = G + ··· + G(x次)或者使用一些数学上的糖来写成P = [x]G,读作x倍的G。椭圆曲线离散对数问题(ECDLP)就是要从仅知道P和G的情况下找到数字x。
注释 我们将[x]G标量乘法称为在这种群中通常称为标量的x。
5.3.2 椭圆曲线 Diffie-Hellman(ECDH)密钥交换如何工作?
现在我们在椭圆曲线上构建了一个群,我们可以在其上实例化相同的 Diffie-Hellman 密钥交换算法。要在 ECDH 中生成密钥对:
-
所有参与者都同意一个椭圆曲线方程,一个有限域(很可能是一个素数),以及一个生成元G(在椭圆曲线密码学中通常称为基点)。
-
每个参与者生成一个随机数x,这个随机数成为他们的私钥。
-
每个参与者将他们的公钥派生为[x]G。
因为椭圆曲线离散对数问题很困难,你猜对了,没有人应该能够仅仅通过查看你的公钥就恢复出你的私钥。我在图 5.12 中有例证。

图 5.12 在 ECDH 中选择一个私钥就像在由生成器(或基点)G 产生的数字列表中选择一个索引一样。椭圆曲线离散对数问题(ECDLP)是仅通过数字找到索引。
所有这些可能有点令人困惑,因为我们为 DH 群定义的操作是乘法,而对于椭圆曲线,我们现在使用加法。再次强调,这些区别完全不重要,因为它们是等价的。您可以在图 5.13 中看到比较。

图 5.13 比较了在 Diffie-Hellman 中使用的群与在椭圆曲线 Diffie-Hellman(ECDH)中使用的群。
现在你应该相信,对于密码学来说唯一重要的是我们有一个定义了操作的群,并且该群的离散对数是困难的。为了完整起见,图 5.14 展示了我们所见过的两种类型群中离散对数问题的差异。

图 5.14 在大质数模下的离散对数问题与椭圆曲线密码学(ECC)中的离散对数问题的比较。它们都与 DH 密钥交换有关,因为问题是从公钥中找到私钥。
对于理论的最后一点说明,我们在椭圆曲线之上形成的群与我们在严格正整数模素数之上形成的群不同。由于一些差异,已知的对 DH 的最强攻击(称为索引演算法或数域筛攻击)在椭圆曲线群上并不起作用。这是为什么 ECDH 的参数可以远远低于相同安全级别下 DH 的参数的主要原因。
好了,我们已经结束了理论部分。让我们回到定义 ECDH。想象一下
-
Alice 有一个私钥 a 和一个公钥 A = [a]G。
-
Bob 有一个私钥 b 和一个公钥 B = [b]G。
拥有 Bob 的公钥知识后,Alice 可以计算出共享密钥为 [a]B。Bob 可以用 Alice 的公钥和他自己的私钥进行类似的计算:[b]A。自然地,我们可以看到这两个计算最终得到相同的数字:
[a]B = [a][b]G = [ab]G = [b][a]G = [b]A
没有被动的对手应该能够仅通过观察公钥来推导出共享点。听起来很熟悉,对吧?接下来,让我们谈谈标准。
5.3.3 椭圆曲线 Diffie-Hellman 的标准
自 1985 年首次提出以来,椭圆曲线密码学一直保持着它的完整性。[...] 美国、英国、加拿大和某些其他北约国家都已经采用了某种形式的椭圆曲线密码学来保护政府之间和之内的机密信息。
—NSA(《椭圆曲线密码学的理由》,2005 年)
ECDH 的标准化过程相当混乱。许多标准化机构努力指定许多不同的曲线,然后引发了许多关于哪个曲线更安全或更高效的争论。由 Daniel J. Bernstein 领导的大量研究指出了 NIST 标准化的一些曲线可能属于 NSA 所知的更弱的曲线类别。
我不再相信这些常数。我相信美国国家安全局通过与行业的关系来操纵它们。
——Bruce Schneier(“美国国家安全局正在破解互联网上的大多数加密”,2013)
如今,大多数使用的曲线都来自一对标准,大多数应用都固定在两条曲线上:P-256 和 Curve25519。在本节的其余部分,我将介绍这些曲线。
NIST FIPS 186-4,“数字签名标准”,最初作为 2000 年签名的标准发布,其中包含一个附录,指定了 15 个用于 ECDH 的曲线。其中一条曲线,P-256,在互联网上是最广泛使用的曲线。该曲线还在 2010 年以不同名称 secp256r1 发布的 “高效密码标准” (SEC) 2,v2 中指定。P-256 使用短 Weierstrass 方程定义:
y² = x³ + ax + b mod p
其中 a = –3,而
b = 41058363725152142129326129780047268409114441015993725554835256314039467401291
和
p = 2²⁵⁶ – 2²²⁴ + 2¹⁹² + 2⁹⁶ – 1
这定义了一个素数阶的曲线:
n = 115792089210356248762697446949407573529996955224135760342422259061068512044369
这意味着曲线上确切有 n 个点(包括无穷远处的点)。基点被指定为
G = (48439561293906451759052585252797914202762949526041747995844080717082404635286, 36134250956749795798585127919587881956611106672985015071877198253568414405109)
该曲线提供了 128 位的安全性。对于使用其他提供 256 位安全性而不是 128 位安全性的密码算法(例如,具有 256 位密钥的 AES)的应用程序,同样标准中还提供了 P-521,以匹配安全级别。
我们能相信 P-256 吗?
有趣的是,FIPS 186-4 中定义的 P-256 和其他曲线据说是从一个 seed 生成的。对于 P-256,种子已知为字节字符串
0xc49d360886e704936a6678e1139d26b7819f7e90
我之前谈过“什么都没有藏在我袖子里”的概念——旨在证明算法设计没有后门的常数。不幸的是,除了指定沿曲线参数的事实之外,对 P-256 种子几乎没有解释。
RFC 7748,“用于安全的椭圆曲线”,于 2016 年发布,规定了两个曲线:Curve25519 和 Curve448。Curve25519 提供了大约 128 位的安全性,而 Curve448 则提供了大约 224 位的安全性,用于协议希望对椭圆曲线的攻击潜力进行防范。我这里只会谈论 Curve25519,它是由以下方程定义的蒙哥马利曲线:
y² = x³ + 486662 x² + x mod p,其中 p = 2²⁵⁵ – 19
Curve25519 的阶数为
n = 2²⁵² + 27742317777372353535851937790883648493
使用的基点为
G = (9, 14781619447589544791020593568409986887264606134616475288964881837755586237401)
ECDH 与 Curve25519 的结合常被称为 X25519。
5.4 小子群攻击和其他安全考虑
今天,我建议您使用 ECDH 而不是 DH,原因是密钥的大小、已知强攻击的缺乏、可用实现的质量,以及椭圆曲线的固定性和良好的标准化(与 DH 群相反,后者随处可见)。后者一点非常重要!使用 DH 可能意味着使用破损的标准(如前面提到的 RFC 5114),过于松散的协议(许多协议,如较旧版本的 TLS,不强制使用什么样的 DH 群),使用破损的自定义 DH 群的软件(前面提到的 socat 问题),等等。
如果您确实必须使用 Diffie-Hellman,请确保遵循标准。我之前提到的标准使用安全素数作为模数:形式为 p = 2q + 1 的素数,其中 q 是另一个素数。关键是,这种形式的群只有两个子群:大小为 2 的小子群(由–1 生成)和大小为 q 的大子群。(顺便说一句,这是您能得到的最好的结果;在 DH 中不存在素数阶群。)小子群的稀缺性防止了一种被称为小子群攻击的攻击(稍后详细说明)。安全素数创建了安全群,因为有两个因素:
-
模素数 p 的乘法群的阶数计算为 p – 1。
-
一个群的子群的阶数是该群的阶数的因数(这就是拉格朗日定理)。
因此,我们模素数的乘法群的阶数是 p – 1 = (2q + 1) – 1 = 2q,它的因数为 2 和 q,这意味着它的子群只能是阶数为 2 或 q 的子群。在这样的群中,小子群攻击是不可能的,因为没有足够的小子群。小子群攻击 是一种针对密钥交换的攻击,攻击者会逐渐发送几个无效的公钥来逐渐泄漏您的私钥的位,而无效的公钥是小子群的生成器。
例如,攻击者可以选择–1(大小为 2 的子群的生成器)作为公钥,并将其发送给你。通过执行你的密钥交换部分,结果的共享密钥是小子群的一个元素(–1 或 1)。这是因为你只是将小子群的生成器(攻击者的公钥)提升到你的私钥的幂。根据你对共享密钥的处理方式,攻击者可以猜测它是什么,并泄露关于你的私钥的一些信息。
对于我们恶意公钥的示例,如果你的私钥是偶数,则共享密钥将为 1,如果你的私钥是奇数,则共享密钥将为–1。因此,攻击者了解了一个信息位:你的私钥的最低有效位。许多不同大小的子群可以导致攻击者有更多机会了解你的私钥,直到整个密钥被恢复。我在图 5.15 中说明了这个问题。

图 5.15 小子群攻击影响具有许多子群的 DH 群。通过选择小子群的生成器作为公钥,攻击者可以逐渐泄露某人的私钥的位。
虽然始终验证接收到的公钥是否位于正确的子群中是一个好主意,但并不是所有的实现都这样做。2016 年,一组研究人员分析了 20 种不同的 DH 实现,并发现没有一个在验证公钥(参见 Valenta 等人的“Measuring small subgroup attacks against Diffie-Hellman”)。确保你正在使用的 DH 实现是这样做的!你可以通过将公钥提升到子群的阶,如果它是该子群的元素,那么应该返回恒等元。
另一方面,椭圆曲线允许素数阶的群。也就是说,它们没有小子群(除了由恒等元素生成的大小为 1 的子群),因此它们对小子群攻击是安全的。好吧,不要那么快……在 2000 年,Biehl、Meyer 和 Muller 发现即使在这样的素数阶椭圆曲线群中,也可能发生小子群攻击,原因是一种被称为无效曲线攻击的攻击。
无效曲线攻击背后的思想是这样的。首先,为了实现使用短 Weierstrass 方程y² = x³ + ax + b(如 NIST 的 P-256)的椭圆曲线的标量乘法,与变量b无关。这意味着攻击者可以找到具有相同方程的不同曲线,除了值b之外,其中一些曲线将具有许多小的子群。你可能知道这将导致什么:攻击者选择另一个曲线中具有小子群的点,并将其发送到目标服务器。服务器通过对给定点执行标量乘法来继续进行密钥交换,从而有效地在不同的曲线上进行密钥交换。这个技巧最终重新启用了小子群攻击,即使在素数阶曲线上也是如此。
修复这个问题的明显方法是再次验证公钥。这可以通过检查公钥不是无穷远点,并将接收到的坐标插入曲线方程中来轻松完成。看看它是否描述了定义曲线上的一个点。不幸的是,在 2015 年,Jager、Schwenk 和 Somorovsky 在“Practical Invalid Curve Attacks on TLS-ECDH”中展示了几个流行实现没有执行这些检查。如果使用 ECDH,我建议你使用 X25519 密钥交换,因为它考虑了无效曲线攻击,可用实现的质量以及设计上对抗时序攻击的抵抗力。
Curve25519 有一个警告,即它不是一个素数阶群。该曲线有两个子群:一个大小为 8 的小子群和一个用于 ECDH 的大子群。此外,原始设计没有规定验证接收到的点,并且库也没有实现这些检查。这导致在使用原语的不同类型协议中发现问题。 (其中一个我在 Matrix 消息协议中发现的问题,在第十一章中有讨论。)
不验证公钥可能会导致与 X25519 不符合预期的行为。原因在于密钥交换算法没有贡献行为:它不允许双方共同为密钥交换的最终结果做出贡献。具体来说,参与者之一可以通过发送一个小子群中的点作为公钥,强制密钥交换的结果为全零。RFC 7748 确实提到了这个问题,并建议检查生成的共享秘钥不是全零输出,但让实现者决定是否进行检查!我建议确保你的实现执行这个检查,尽管除非你以非标准方式使用 X25519,否则不太可能遇到任何问题。
由于许多协议依赖于 Curve25519,这不仅仅是密钥交换的问题。Ristretto,即将成为 RFC 的互联网草案,是一种为 Curve25519 添加额外编码层的构造,有效模拟了一个素数阶曲线(参见 tools.ietf.org/html/draft-hdevalence-cfrg-ristretto-01)。这种构造已经开始受到关注,因为它简化了其他类型的加密原语对 Curve25519 的安全假设,但又希望得到素数阶域的好处。
概要
-
未经身份验证的密钥交换允许两方达成共享秘钥,同时阻止任何被动中间人攻击者能够推导出它。
-
身份验证的密钥交换阻止主动中间人攻击者冒充连接的一方,而双向身份验证的密钥交换阻止主动中间人攻击者冒充双方。
-
通过了解对方的公钥,可以执行经过身份验证的密钥交换,但这并不总是可扩展的,而签名将解锁更复杂的场景(见第七章)。
-
迪菲-赫尔曼(DH)是第一个发明的密钥交换算法,仍然被广泛使用。
-
用于 DH 的推荐标准是 RFC 7919,其中包括几个可供选择的参数。最小选项是推荐的 2,048 位素数参数。
-
椭圆曲线迪菲-赫尔曼(ECDH)的密钥尺寸比 DH 小得多。对于 128 位的安全性,DH 需要 2,048 位的参数,而 ECDH 只需要 256 位的参数。
-
ECDH 最广泛使用的曲线是 P-256 和 Curve25519。两者都提供 128 位的安全性。对于 256 位的安全性,同一标准中还提供了 P-521 和 Curve448。
-
确保实现验证你接收到的公钥的有效性,因为无效的密钥是许多错误的源头。
第六章:非对称加密和混合加密
本章内容包括
-
对秘密信息进行加密的非对称加密方法
-
对数据进行加密到公钥的混合加密方法
-
非对称和混合加密的标准
在第四章中,您了解到了认证加密,这是一种用于加密数据的加密原语,但受到对称性的限制(连接的两侧必须共享相同的密钥)。在本章中,我将通过介绍非对称加密来解除此限制,这是一种加密到其他人的密钥而无需知道密钥的原语。毫不奇怪,非对称加密利用密钥对,加密将使用公钥而不是对称密钥。
在本章的中间部分,您将看到非对称加密受其可以加密的数据量以及加密速率的限制。为了消除这一障碍,我将向您展示如何将非对称加密与认证加密混合在一起,形成我们所称的混合加密。让我们开始吧!
注意 对于本章,您需要已经阅读过第四章关于认证加密和第五章关于密钥交换。
6.1 什么是非对称加密?
了解如何加密消息的第一步是理解非对称加密(也称为公钥加密)。在本节中,您将了解此加密原语及其属性。让我们看一个以下真实场景:加密电子邮件。
您可能知道,您发送的所有电子邮件都是“明文”发送的,任何坐在您和您收件人的电子邮件提供商之间的人都可以阅读。这不太好。你该怎么解决这个问题?您可以使用像 AES-GCM 这样的加密原语,这是您在第四章学到的。为此,您需要为想要给您发消息的每个人设置一个不同的共享对称密钥。
练习
使用相同的共享密钥与所有人将非常糟糕;您能理解为什么吗?
但是您不能指望提前知道谁会给您发送消息,随着越来越多的人想要给您加密消息,生成和交换新的对称密钥会变得繁琐。这就是非对称加密的帮助所在,它允许拥有您公钥的任何人向您加密消息。此外,您是唯一能够使用您拥有的相关私钥解密这些消息的人。请参见图 6.1,了解非对称加密的示意图。

图 6.1 使用非对称加密,任何人都可以使用爱丽丝的公钥向她发送加密消息。只有拥有相关私钥的爱丽丝才能解密这些消息。
要设置非对称加密,首先需要通过某种算法生成一对密钥。与任何加密算法的设置函数一样,密钥生成算法接受一个安全参数。这个安全参数通常被翻译为“你想要多大的密钥?”更大意味着更安全。图 6.2 说明了这一步骤。

图 6.2 要使用非对称加密,首先需要生成一对密钥。根据您提供的安全参数,您可以生成不同安全强度的密钥。
密钥生成算法生成由两个不同部分组成的密钥对:公钥部分(如名称所示)可以在不太担心的情况下发布和共享,而私钥必须保持秘密。与其他加密原语的密钥生成算法类似,需要一个安全参数来决定算法的位安全性。然后任何人都可以使用公钥部分加密消息,您可以使用私钥部分解密,就像图 6.3 所示。与经过认证的解密类似,如果提供不一致的密文,解密可能会失败。

图 6.3 非对称加密允许使用接收者的公钥加密消息(明文)。接收者随后可以使用与先前使用的公钥相关的私钥使用不同的算法解密加密的消息(密文)。
请注意,到目前为止我们还没有讨论认证问题。考虑电线的两侧:
-
您正在使用您认为是 Alice 拥有的公钥进行加密。
-
Alice 并不确定是谁发送了这条消息。
现在,我们将假设我们以一种非常安全的方式获得了 Alice 的公钥。在涵盖数字签名的第七章中,您将了解现实世界协议如何解决这个实践中的引导问题。您还将在第七章中学习如何以加密方式向 Alice 传达您的真实身份。剧透警告:您将签署您的消息。
让我们继续下一节,您将了解非对称加密在实践中的应用(以及为什么在实践中很少直接使用)。
6.2 实践中的非对称加密和混合加密
您可能认为非对称加密可能足以开始加密您的电子邮件。实际上,由于它可以加密的消息长度受限,非对称加密相当受限。与对称加密相比,非对称加密和解密的速度也较慢。这是由于非对称构造实施数学运算,而对称原语通常只是操作位。
在本节中,你将了解这些限制,实际上非对称加密用于什么,最后,密码学是如何克服这些障碍的。本节分为两个部分,分别介绍了非对称加密的两个主要用例:
-
密钥交换 —— 你会发现使用非对称加密原语执行密钥交换(或密钥协商)是相当自然的。
-
混合加密 —— 你会发现由于你可以加密的最大大小的限制,非对称加密的用例相当有限。为了加密更大的消息,你将了解到一种更有用的原语,称为 混合加密。
6.2.1 密钥交换和密钥封装
原来非对称加密可以用于执行密钥交换——与我们在第五章中看到的一样!为了做到这一点,你可以开始生成一个对称密钥,并用 Alice 的公钥对其进行加密——我们也称之为 封装密钥 ——就像图 6.4 所示。

图 6.4 要将非对称加密用作密钥交换原语,你需要(1)生成一个对称密钥,然后(2)用 Alice 的公钥对其进行加密。
你随后可以将密文发送给 Alice,她将能够解密它并学习对称密钥。接下来,你们将有一个共享的秘密!图 6.5 展示了完整的流程。

图 6.5 要将非对称加密用作密钥交换原语,你可以(1)生成一个对称密钥,然后(2)用 Alice 的公钥对其进行加密。之后(3)将其发送给 Alice,她可以(4)用她关联的私钥对其进行解密。在协议结束时,你们都拥有共享的秘密,而其他人无法仅从观察到的加密对称密钥中推导出它。
使用非对称加密执行密钥交换通常使用一种称为 RSA 的算法(按照其发明者 Rivest、Shamir 和 Adleman 的名字命名),并在许多互联网协议中使用。今天,RSA 通常不是进行密钥交换的首选方式,并且在协议中的使用越来越少,而更偏爱椭圆曲线 Diffie-Hellman(ECDH)。这主要是出于历史原因(发现了许多与 RSA 实现和标准相关的漏洞)和 ECDH 提供的更小参数大小的吸引力。
6.2.2 混合加密
实际上,非对称加密只能加密长度不超过一定限制的消息。例如,可以通过 RSA 加密的明文消息的大小受到生成密钥对时使用的安全参数的限制(更具体地说,是模数的大小)。现今,使用的安全参数(4,096 位模数),限制约为 500 个 ASCII 字符 —— 相当小。因此,大多数应用程序使用混合加密,其限制与使用的认证加密算法的加密限制相关联。
混合加密在实践中与非对称加密具有相同的接口(见图 6.6)。人们可以使用公钥加密消息,拥有相关私钥的人可以解密加密的消息。真正的区别在于您可以加密的消息的大小限制。

图 6.6 混合加密与非对称加密具有相同的接口,只是可以加密的消息大小要大得多。
在幕后,混合加密只是一个非对称加密原语与一个对称加密原语的结合(因此得名)。具体来说,它是与接收者进行的非交互式密钥交换,然后使用经过身份验证的加密算法加密消息。
警告 您也可以使用简单的对称加密原语,而不是经过身份验证的加密原语,但对称加密无法防止有人篡改您的加密消息。这就是为什么在实践中我们从不单独使用对称加密的原因(如第四章所示)。
让我们了解一下混合加密的工作原理!如果您想将消息加密给爱丽丝,您首先生成一个对称密钥并使用它加密您的消息,然后使用一个经过身份验证的加密算法,正如图 6.7 所示。

图 6.7 使用混合加密和非对称加密将消息加密给爱丽丝,您(1)为经过身份验证的加密算法生成对称密钥,然后您(2)使用对称密钥将消息加密给爱丽丝。
一旦您加密了您的消息,爱丽丝仍然无法在不知道对称密钥的情况下解密它。我们如何向爱丽丝提供对称密钥?使用爱丽丝的公钥对对称密钥进行非对称加密,就像图 6.8 中所示的那样。

图 6.8 在图 6.7 的基础上,(3)你使用爱丽丝的公钥和非对称加密算法加密对称密钥本身。
最后,你可以将两个结果都发送给爱丽丝。这些包括
-
非对称加密的对称密钥
-
对称加密的消息
这对于爱丽丝解密消息已经足够了。我在图 6.9 中详细说明了整个流程。

图 6.9 在图 6.8 的基础上,(4)在你将加密的对称密钥和加密的消息都发送给爱丽丝后,(5)爱丽丝使用她的私钥解密对称密钥。(6)然后她使用对称密钥解密加密的消息。(请注意,如果在步骤 4 时通信被中间人攻击者篡改,步骤 5 和 6 都可能失败并返回错误。)
这就是我们如何利用两者之间的最佳特性:将非对称加密和对称加密混合以向公钥加密大量数据。我们通常将算法的第一个非对称部分称为密钥封装机制(KEM),将第二个对称部分称为数据封装机制(DEM)。
在我们转向下一节并学习存在的不同算法和标准以及非对称加密和混合加密的方法之前,让我们看看(实践中)如何使用加密库执行混合加密。为此,我选择了 Tink 加密库。Tink 是由 Google 的一组密码学家开发的,以支持公司内外的大型团队。由于项目的规模,进行了有意识的设计选择,并暴露了健全的功能,以防止开发人员误用密码原语。此外,Tink 可在几种编程语言中使用(Java、C++、Obj-C 和 Golang)。
列表 6.1 Java 中的混合加密
import com.google.crypto.tink.HybridDecrypt;
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.hybrid.HybridKeyTemplates
➥ .ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM;
import com.google.crypto.tink.KeysetHandle;
KeysetHandle privkey = KeysetHandle.generateNew( // ❶
ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM); // ❶
KeysetHandle publicKeysetHandle = // ❷
privkey.getPublicKeysetHandle(); // ❷
HybridEncrypt hybridEncrypt = // ❸
publicKeysetHandle.getPrimitive( // ❸
HybridEncrypt.class); // ❸
byte[] ciphertext = hybridEncrypt.encrypt( // ❸
plaintext, associatedData); // ❸
HybridDecrypt hybridDecrypt = // ❹
privkey.getPrimitive(HybridDecrypt.class); // ❹
byte[] plaintext = hybridDecrypt.decrypt( // ❹
ciphertext, associatedData); // ❹
❶ 为特定混合加密方案生成密钥
❷ 获取我们可以发布或共享的公钥部分
❸ 任何知道此公钥的人都可以用它加密明文,并可以验证一些关联数据。
❹ 使用相同的关联数据解密加密消息。如果解密失败,它会抛出异常。
为了帮助你理解ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM字符串:ECIES(椭圆曲线集成加密方案)是要使用的混合加密标准。你将在本章后面学到这一点。字符串的其余部分列出了用于实例化 ECIES 的算法:
-
P256 是你在第五章学到的 NIST 标准化椭圆曲线。
-
HKDF 是一个密钥派生函数,你将在第八章学习它。
-
HMAC 是你在第三章学到的消息认证码。
-
SHA-256 是你在第二章学到的哈希函数。
-
AES-128-GCM 是你在第四章学到的使用 128 位密钥的 AES-GCM 验证加密算法。
看到一切是如何开始拼凑在一起的了吗?在下一节中,你将学习 RSA 和 ECIES,这两种广泛采用的非对称加密和混合加密标准。
6.3 使用 RSA 进行非对称加密:好的和不那么好的
是时候让我们来看一下在实践中定义了非对称加密和混合加密的标准了。在历史上,这两个原语都未能幸免于密码分析家的手,许多漏洞和弱点都被发现在这些标准和实现中。这就是为什么我将从介绍 RSA 非对称加密算法及其不正确使用方式开始这一节。本章的其余部分将介绍你可以遵循的实际标准来使用非对称和混合加密:
-
RSA-OAEP — 使用 RSA 进行非对称加密的主要标准
-
ECIES — 使用椭圆曲线 Diffie-Hellman(ECDH)进行混合加密的主要标准
6.3.1 教科书 RSA
在本节中,你将了解 RSA 公钥加密算法及其在多年来的标准化。这对理解基于 RSA 的其他安全方案很有用。
不幸的是,自从 1977 年首次发布以来,RSA 一直受到了相当大的诟病。流行的理论之一是 RSA 太容易理解和实现,因此许多人自行实施,这导致了许多易受攻击的实现。这是一个有趣的想法,但它没有抓住整个故事的要点。尽管 RSA 的概念(通常称为教科书 RSA)如果被天真地实现是不安全的,但甚至标准也被发现是不安全的!但是不要那么快,要理解这些问题,您首先需要了解 RSA 的工作原理。
还记得模素数p的乘法群吗?(我们在第五章中谈论过。)它是严格正整数的集合:
1, 2, 3, 4, ···, p – 1
让我们假设其中一个数字是我们的消息。对于足够大的p,比如 4,096 位,我们的消息最多可以包含约 500 个字符。
注意对于计算机来说,一条消息只是一系列字节,也可以解释为一个数字。
我们已经看到通过对一个数字进行幂运算(比如我们的消息),我们可以生成其他形成一个子群的数字。我在图 6.10 中进行了说明。

图 6.10 对于模素数(这里为 5)的整数被划分为不同的子群。通过选择一个元素作为生成器(假设是数字 2)并对其进行指数运算,我们可以生成一个子群。对于 RSA,生成器就是消息。
当我们定义如何使用 RSA 加密时,这对我们很有用。为此,我们发布一个公共指数e(用于加密)和一个素数p。(实际上p不能是素数,但我们暂时忽略这一点。)要加密一个消息m,需要计算
密文 = m^e mod p
例如,要使用e = 2 和p = 5 加密消息m = 2,我们计算
密文 = 2² mod 5 = 4
这就是使用 RSA 加密的理念背后的想法!
注意通常情况下,会选择一个小的数作为公共指数e,以便加密速度更快。从历史上看,标准和实现似乎已经确定了素数 65,537 作为公共指数。
太棒了!现在你有了一种让人们向你加密消息的方法。但是如何解密呢?记住,如果你继续对一个生成器进行幂运算,你实际上会回到原始数字(见图 6.11)。

图 6.11 假设我们的消息是数字 2。通过对其进行幂运算,我们可以获得我们群中的其他数字。如果我们对其进行足够多次幂运算,我们将回到我们的原始消息 2。我们称该群是循环的。这个属性可以用来在将消息提升到某个幂之后恢复消息。
这应该让你有一个实现解密的思路:找出你需要对密文进行多少次幂运算才能恢复原始生成器(即消息)。假设你知道这样一个数字,我们将其称为私有指数 d(d表示解密)。如果你收到
密文 = 消息^e mod p
你应该能够将其提升到幂次d以恢复消息:
密文^d = (消息e)d = 消息^(e×d) = 模p的消息
找到这个私有指数d的实际数学有点棘手。简单来说,你计算群的阶(元素数量)对公共指数取模的逆元:
d = e^(–1) mod order
我们有一个有效的算法来计算模反函数(如扩展欧几里得算法),所以这不是问题。不过我们有另一个问题!对于一个素数p,阶很简单,就是p – 1,因此,任何人都可以很容易地计算出私有指数。这是因为除了d之外,这个方程中的每个元素都是公开的。
欧拉的定理
我们如何得到前述方程以计算私有指数d?欧拉定理说明,对于与p互质的m(意味着它们没有公共因数):
m^(order) = 1 mod p
对于order,即整数对p取模创建的乘法群中的元素数。这又意味着,对于任何整数multiple
m(1+)(multiple×order) = m × (m(order))(multiple) mod p = m mod p
这告诉我们我们要解决的方程
m^(e × d) = m mod p
可以简化为
e × d = 1 + multiple × order
这可以重写为
e × d = 1 mod order
这意味着d是模order下的e的逆元。
我们可以防止他人从公共指数计算私有指数的一种方法是隐藏我们群的阶。这是 RSA 背后的精妙思想:如果我们的模数不再是一个素数而是一个素数p × q的乘积(其中p和q是只有你知道的大素数),那么我们的乘法群的阶就不容易计算,只要我们不知道p和q!
RSA 群的阶
你可以用欧拉的欧拉函数ϕ(N)计算模数N的乘法群的阶,它返回与N互质的数字的计数。例如,5 和 6 是互质的,因为唯一能够同时整除它们的正整数是 1。另一方面,10 和 15 不是,因为 1 和 5 分别能整除它们。对于 RSA 模数N = p × q的乘法群的阶是
ϕ(N) = (p – 1) × (q – 1)
这太难计算了,除非你知道N的因数。
我们都搞定了!总结一下,这就是 RSA 的工作原理:
-
用于密钥生成
-
生成两个大素数p和q。
-
选择一个随机的公共指数e或一个固定的像e = 65537 这样的。
-
你的公钥是公共指数e和公共模数N = p × q。
-
求得你的私有指数d = e^(–1) mod (p – 1) (q – 1)。
-
你的私钥是私有指数d。
-
-
用于加密,计算消息^e mod N。
-
用于密文的解密,计算密文^d mod N。
图 6.12 回顾了 RSA 如何在实践中工作。

图 6.12 RSA 加密通过使用公共指数 e 对消息进行模公共模数 N = p × q 进行指数运算。RSA 解密通过使用私有指数 d 对加密数字进行模公共模数 N 进行指数运算。
我们说 RSA 依赖于因子分解问题。没有 p 和 q 的知识,没有人可以计算出顺序;因此,除了你之外,没有人可以从公共指数计算出私有指数。这类似于迪菲-赫尔曼基于离散对数问题的方式(见图 6.13)。

图 6.13 迪菲-赫尔曼(DH)、椭圆曲线迪菲-赫尔曼(ECDH)和 RSA 是依赖于数学中三个我们认为难以解决的问题的非对称算法。难以解决 意味着我们不知道如何用大数实例化时解决它们的高效算法。
因此,教科书上的 RSA 在一个复合数 N = p × q 上运行,其中 p 和 q 是两个需要保持秘密的大素数。现在你了解了 RSA 的工作原理,让我们看看它在实践中有多不安全以及标准如何使其安全。
6.3.2 为什么不使用 RSA PKCS#1 v1.5
你了解了“教科书上的 RSA”,由于许多原因,默认情况下是不安全的。在学习 RSA 的安全版本之前,让我们看看你需要避免的内容。
有许多原因导致你不能直接使用教科书上的 RSA。一个例子是,如果你加密小消息(例如 m = 2),那么一些恶意行为者可以加密 0 到 100 之间的所有小数字,然后迅速观察他们的加密数字是否与你的密文匹配。如果匹配,他们将知道你加密了什么。
标准通过使你的消息变得过大,以至于无法以这种方式暴力破解来解决这个问题。具体来说,它们通过添加一个 非确定性 填充来最大化消息(加密前)的大小。例如,RSA PKCS#1 v1.5 标准定义了一个填充,向消息添加一些随机字节。我在图 6.14 中进行了说明。

图 6.14 RSA PKCS#1 v1.5 标准指定了在加密之前应用于消息的填充。填充必须是可逆的(以便解密可以去除它),并且必须向消息添加足够的随机字节,以避免暴力破解攻击。
PKCS#1 标准实际上是基于 RSA 的第一个标准,是 RSA 公司在 90 年代初撰写的一系列公钥密码标准(PKCS)文件的一部分。尽管 PKCS#1 标准修复了一些已知问题,但在 1998 年,Bleichenbacher 发现了对 PKCS#1 v1.5 的实际攻击,允许攻击者解密使用标准指定的填充加密的消息。由于需要百万条消息,因此这个攻击被恶名昭彰地称为百万消息攻击。后来找到了一些缓解方法,但有趣的是,多年来,攻击一再被重新发现,因为研究人员发现这些缓解方法过于难以安全地实现(如果可能的话)。
自适应选择密文攻击
Bleichenbacher 的百万消息攻击是理论密码学中一种称为自适应选择密文攻击(CCA2)的攻击类型。CCA2 意味着为了执行此攻击,攻击者可以提交任意的 RSA 加密消息(选择密文),观察它如何影响解密,并根据先前的观察继续攻击(自适应部分)。CCA2 经常用于模拟密码学安全证明中的攻击者。
要理解攻击为何可能,您需要了解 RSA 密文是可塑的:您可以篡改 RSA 密文而不使其解密无效。如果我观察到密文 c = m^e mod N,那么我可以提交以下密文:
3^e × m^e = (3m)^e mod N
解密结果将为
((3m)e)d = (3m)^(e×d) = 3m mod N
我在这里以数字 3 作为示例,但我可以用任意数字乘以原始消息。在实践中,消息必须格式良好(由于填充),因此,篡改密文应该会破坏解密。然而,有时,即使在恶意修改之后,解密后仍然接受填充。
Bleichenbacher 在他对 RSA PKCS#1 v1.5 的百万消息攻击中利用了这个属性。他的攻击是通过截获加密消息,修改它,并发送给负责解密的人。通过观察那个人是否能解密它(填充仍然有效),我们可以获得关于消息范围的一些信息。因为前两个字节是 0x0002,所以我们知道解密结果小于某个值。通过迭代执行此操作,我们可以将该范围缩小到原始消息本身。
尽管 Bleichenbacher 攻击是众所周知的,但今天仍然有许多系统使用 RSA PKCS#1 v1.5 进行加密。作为安全顾问工作的一部分,我发现许多应用程序容易受到此攻击的影响——所以要小心!
6.3.3 使用 RSA-OAEP 进行非对称加密
1998 年,同一 PKCS#1 标准的 2.0 版本发布了一个名为Optimal Asymmetric Encryption Padding(OAEP)的 RSA 新填充方案。与其前身 PKCS#1 v1.5 不同,OAEP 不容易受到 Bleichenbacher 的攻击,因此是目前用于 RSA 加密的强标准。让我们看看 OAEP 是如何工作并防止先前讨论的攻击。
首先,让我们提到,像大多数加密算法一样,OAEP 带有一个密钥生成算法。这需要一个安全参数,如图 6.15 所示。

图 6.15 RSA-OAEP,像许多公钥算法一样,首先需要生成一个密钥对,以便后来在加密原语提供的其他算法中使用。
此算法需要一个安全参数,即位数。与 Diffie-Hellman 一样,操作发生在模一个大数的数字集合中。当我们谈论 RSA 的一个实例的安全性时,我们通常指的是那个大模数的大小。如果你记得的话,这与 Diffie-Hellman 类似。
目前,大多数指南(参见keylength.com)估计模数在 2,048 到 4,096 位之间,以提供 128 位安全性。由于这些估计相当不同,大多数应用程序似乎保守地选择了 4,096 位参数。
注意我们看到 RSA 的大模数不是一个素数,而是两个大素数p和q的乘积N = p × q。对于 4,096 位模数,密钥生成算法通常将事情一分为二,并生成大约 2,048 位大小的p和q。
加密时,算法首先对消息进行填充,并与每次加密生成的随机数混合。然后使用 RSA 对结果进行加密。解密密文时,过程与图 6.16 所示相反。

图 6.16 RSA-OAEP 通过在加密之前将消息与随机数混合来工作。混合可以在解密后恢复。在算法的中心,使用掩码生成函数(MGF)来随机化和扩大或缩小输入。
RSA-OAEP 使用这种混合方式,以确保如果 RSA 加密的几位泄漏,就无法获取有关明文的任何信息。实际上,要撤销 OAEP 填充,您需要获取(接近)OAEP 填充明文的所有字节!此外,Bleichenbacher 的攻击不应再起作用,因为该方案使得通过修改密文无法获得格式良好的明文。
注意明文感知性是一种属性,使得攻击者很难创建一个成功解密的密文(当然没有加密的帮助)。由于 OAEP 提供的明文感知性,Bleichenbacher 的攻击对该方案不起作用。
在 OAEP 内部,MGF代表掩码生成函数。在实践中,MGF 是一个可扩展输出函数(XOF);你在第二章已经了解了 XOF。由于 MGF 是在 XOF 之前发明的,因此它们是使用散列函数反复散列输入与计数器的输入来构建的(见图 6.17)。这就是 OAEP 的工作原理!

图 6.17 掩码生成函数(MGF)只是一个接受任意长度输入并产生随机外观任意长度输出的函数。它通过对输入和计数器进行散列,将摘要连接在一起,并截断结果以获得所需长度来工作。
Manger 的填充预言攻击
OAEP 标准发布仅三年后,James Manger 发现了一个与 Bleichenbacher 的百万消息攻击类似但更加实用的 OAEP 定时攻击,如果实现不正确的话。幸运的是,与 PKCS#1 v1.5 相比,安全地实现 OAEP 要简单得多,并且对该方案实现中的漏洞要少得多。
此外,OAEP 的设计并不完美;多年来已经提出并标准化了更好的构造。一个例子是 RSA-KEM,它具有更强的安全性证明,并且要安全地实现要简单得多。您可以观察到设计在图 6.18 中更加优雅。

图 6.18 RSA-KEM 是一种通过简单地使用 RSA 加密随机数来工作的加密方案。不需要填充。我们可以通过密钥派生函数(KDF)传递随机数以获得对称密钥。然后,我们使用对称密钥通过身份验证加密算法加密消息。
注意这里使用的密钥派生函数(KDF)。这是另一个可以用 MGF 或 XOF 替换的加密原语。我将在第八章关于随机性和机密性中更多地谈到 KDF 是什么。
如今,大多数使用 RSA 的协议和应用程序仍然实现不安全的 PKCS#1 v1.5 或 OAEP。另一方面,越来越多的协议正在摒弃 RSA 加密,转而采用椭圆曲线 Diffie-Hellman(ECDH)进行密钥交换和混合加密。这是可以理解的,因为 ECDH 提供更短的公钥,并且通常从更好的标准和更安全的实现中受益。
6.4 使用 ECIES 进行混合加密
虽然存在许多混合加密方案,但最广泛采用的标准是椭圆曲线整合加密方案(ECIES)。该方案已被指定用于与 ECDH 一起使用,并包含在许多标准中,如 ANSI X9.63,ISO/IEC 18033-2,IEEE 1363a 和 SECG SEC 1。不幸的是,每个标准似乎都实现了不同的变体,并且不同的加密库以不同的方式实现混合加密,部分原因是如此。
出于这个原因,在野外我很少看到两个相似的混合加密实现。重要的是要理解,虽然这很烦人,但如果协议的所有参与者使用相同的实现或记录了他们实现的混合加密方案的详细信息,那么就不会有问题。
ECIES 的工作方式与第 6.2 节中解释的混合加密方案类似。不同之处在于,我们使用 ECDH 密钥交换实现了 KEM 部分,而不是使用非对称加密原语。让我们逐步解释这一点。
首先,如果你想将消息加密给 Alice,你使用基于(EC)DH 的密钥交换与 Alice 的公钥以及你为此生成的密钥对(这称为临时密钥对)。然后你可以使用获得的共享秘密与像 AES-GCM 这样的认证对称加密算法加密一个更长的消息给她。图 6.19 说明了这一点。

图 6.19 使用混合加密将消息加密给 Alice,使用(EC)DH,你(1)生成一个临时(椭圆曲线)DH 密钥对。然后(2)使用你的临时私钥和 Alice 的公钥进行密钥交换。(3)使用生成的共享秘密作为对称密钥,使用认证加密算法加密你的消息。
之后,你可以将临时公钥和密文发送给 Alice。Alice 可以使用你的临时公钥与自己的密钥对进行密钥交换。然后她可以使用结果来解密密文并恢复原始消息。结果要么是原始消息,要么是错误,如果公钥或加密消息在传输中被篡改。图 6.20 说明了完整的流程。

图 6.20 在图 6.19 的基础上构建,(4)在你将你的临时公钥和你的加密消息发送给 Alice 后,(5)Alice 可以使用她的私钥和你的临时公钥进行密钥交换。(6)最后,她使用生成的共享秘密作为对称密钥,使用相同的认证加密算法解密加密消息。
这基本上就是 ECIES 的工作原理。还有一种使用 Diffie-Hellman 的 ECIES 变体,称为 IES,工作方式基本相同,但似乎没有多少人使用它。
消除密钥交换输出中的偏差
注意,我简化了图 6.20。大多数认证加密原语期望一个均匀随机的对称密钥。因为密钥交换的输出通常不是均匀随机的,所以我们需要先通过 KDF 或 XOF(在第二章中见过)传递共享秘密。你将在第八章中了解更多相关内容。
这里的不是均匀随机*意味着从统计上看,密钥交换结果的某些比特可能更多地是 0,或者相反。例如,前几位可能总是被设置为 0。
练习
你看出为什么不能立即使用密钥交换输出了吗?
这就是你可以使用的不同标准。在下一章中,你将学习关于签名的内容,这将是第一部分中最后,也许是最重要的公钥密码算法。
摘要
-
我们很少使用非对称加密直接加密消息。这是因为非对称加密可以加密的数据相对较小。
-
混合加密可以通过将非对称加密(或密钥交换)与对称认证加密算法结合来加密更大的消息。
-
RSA PKCS#1 版本 1.5 标准用于非对称加密在大多数情况下已经被破解。建议使用在 RSA PKCS#1 版本 2.2 中标准化的 RSA-OAEP 算法。
-
ECIES 是最广泛使用的混合加密方案。由于其参数大小和对坚实标准的依赖,它比基于 RSA 的方案更受青睐。
-
不同的加密库可能以不同的方式实现混合加密。如果可互操作的应用程序使用相同的实现,这在实践中并不是问题。
第七章:签名和零知识证明
本章包括
-
零知识证明和数字签名
-
密码签名的现有标准
-
签名的微妙行为和避免它们的陷阱
你即将学到一种最普遍和最强大的密码原语——数字签名。简单来说,数字签名类似于你习惯的现实生活中的签名,就像你在支票和合同上写的那种。当然,数字签名是密码学的,所以它们提供比纸笔等价物更多的保证。
在协议的世界里,数字签名解锁了许多不同的可能性,你将会在本书的第二部分中反复遇到它们。在这一章中,我将介绍这个新原语是什么,它如何在现实世界中使用,以及现代数字签名标准是什么。最后,我将谈论安全考虑和使用数字签名的危险。
注:在密码学中,签名经常被称为数字签名或签名方案。在本书中,我会交替使用这些术语。
对于本章,你需要阅读
-
第二章关于哈希函数
-
第五章关于密钥交换
-
第六章关于非对称加密
7.1 什么是签名?
我在第一章解释过,密码签名基本上就像现实生活中的签名一样。因此,它们通常是最直观的密码原语之一:
-
只有你可以使用你的签名来签署任意消息。
-
任何人都可以验证你在消息上的签名。
因为我们处于非对称密码学的领域,你可能已经猜到了这种不对称性会如何发挥作用。一个签名方案通常由三种不同的算法组成:
-
一个签名者用来创建新的私钥和公钥的密钥对生成算法(然后可以将公钥与任何人分享)。
-
一个签名算法,它接受一个私钥和一个消息,然后产生一个签名。
-
一个验证算法,它接受一个公钥、一个消息和一个签名,并返回一个成功或错误的消息。
有时私钥也被称为签名密钥,公钥被称为验证密钥。有道理吧?我在图 7.1 中总结了这三个算法。

图 7.1 数字签名的接口。像其他公钥密码算法一样,你首先需要通过一个接受安全参数和一些随机性的密钥生成算法生成密钥对。然后你可以使用一个带有私钥的签名算法对消息进行签名,并使用带有公钥的验证算法验证消息上的签名。如果你没有访问其关联私钥,你就无法伪造一个验证公钥的签名。
签名有什么用?它们用于验证消息的来源以及消息的完整性:
-
原始性 —— 如果我的签名在上面,那么它来自我。
-
完整性 —— 如果有人修改了消息,则签名将失效。
注意:虽然这两个属性与认证相关联,但通常被区分为两个单独的属性:原始认证 和 消息认证(或完整性)。
从某种意义上说,签名类似于第三章中您了解到的消息认证码(MACs)。但与 MAC 不同的是,它们允许我们对消息进行非对称认证:参与者可以验证消息未被篡改,而无需私钥或签名密钥的知识。接下来,我将向您展示这些算法如何在实践中使用。
练习
正如您在第三章中看到的那样,MAC 生成的认证标签必须以恒定时间验证,以避免时间攻击。您认为我们需要对验证签名做同样的事情吗?
7.1.1 如何在实践中签名和验证签名
让我们看一个实际的例子。为此,我使用了 pyca/cryptography(cryptography.io),一个广受尊敬的 Python 库。以下清单简单地生成一个密钥对,使用私钥部分签名消息,然后使用公钥部分验证签名。
代码清单 7.1 在 Python 中签名和验证签名
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey // ❶
)
private_key = Ed25519PrivateKey.generate() // ❷
public_key = private_key.public_key() // ❷
message = b"example.com has the public key 0xab70..." // ❸
signature = private_key.sign(message) // ❸
try: // ❹
public_key.verify(signature, message) // ❹
print("valid signature") // ❹
except InvalidSignature: // ❹
print("invalid signature") // ❹
❶ 使用 Ed25519 签名算法,这是一种流行的签名方案
❷ 首先生成私钥,然后生成公钥
❸ 使用私钥对消息进行签名并获得签名
❹ 使用公钥验证消息上的签名
正如我之前所说,数字签名在现实世界中解锁了许多用例。让我们在下一节中看一个例子。
7.1.2 签名的主要用例:认证密钥交换
第 5 和 6 章介绍了两个参与者之间执行密钥交换的不同方法。在同一章节中,您了解到这些密钥交换对于协商一个共享密钥是有用的,然后可以使用该密钥来使用经过身份验证的加密算法来保护通信。然而,密钥交换并未完全解决在两个参与者之间建立安全连接的问题,因为主动的中间人(MITM)攻击者可以轻易地冒充密钥交换的双方。这就是签名的用武之地。
假设 Alice 和 Bob 正试图在它们之间建立安全通信渠道,并且 Bob 知道 Alice 的验证密钥。知道这一点,Alice 可以使用她的签名密钥来认证她的密钥交换的一面:她生成一个密钥交换密钥对,用她的签名密钥对公钥部分进行签名,然后发送密钥交换的公钥以及签名。Bob 可以使用他已经知道的关联验证密钥验证签名是否有效,然后使用密钥交换的公钥执行密钥交换。
我们称这样的密钥交换为身份验证密钥交换。如果签名无效,鲍勃可以知道有人正在积极地中间人攻击密钥交换。我在图 7.2 中说明了身份验证密钥交换。

图 7.2 第一张图片(顶部)代表了一个未经身份验证的密钥交换,这对于一个可以轻松伪装成交换双方的主动中间人攻击者来说是不安全的,因为他可以用自己的公钥与双方交换公钥。第二张图片(底部)代表了一个密钥交换的开始,通过爱丽丝对她的公钥签名来进行身份验证。由于被主动中间人攻击者篡改了消息,鲍勃(知道爱丽丝的验证密钥)无法验证签名,于是他中止了密钥交换。
请注意,在此示例中,密钥交换只在一侧进行了身份验证:尽管爱丽丝无法被冒充,但鲍勃可以。如果双方都经过了身份验证(鲍勃会签署他的密钥交换的一部分),我们称这种密钥交换为相互身份验证密钥交换。签署密钥交换可能看起来并不是很有用。我们似乎是把事先不知道爱丽丝的密钥交换公钥的问题转移到了事先不知道她的验证密钥的问题上。下一节将介绍身份验证密钥交换的一个实际应用,这将更容易理解。
7.1.3 实际应用:公钥基础设施
如果您假设信任是传递的,签名就会变得更加强大。我的意思是,如果您信任我,我信任爱丽丝,那么您就可以信任爱丽丝。她很酷。
信任的传递允许您以极端的方式扩展系统中的信任。想象一下,您对某个权威及其验证密钥有信心。此外,想象一下,这个权威已经签署了指示查尔斯公钥是什么、戴维公钥是什么等消息。然后,您可以选择相信这个映射!这样的映射称为公钥基础设施。例如,如果您尝试与查尔斯进行密钥交换,并且他声称他的公钥是一个看起来像 3848... 的大数,您可以通过检查您“心爱的”权威是否已签署了类似“查尔斯的公钥是 3848...”的消息来验证。
这个概念的一个现实应用是网络公钥基础设施(web PKI)。Web PKI 是您的网络浏览器用来验证其与您每天访问的众多网站执行的密钥交换的机制。Web PKI 的简化解释(如图 7.3 所示)如下:当您下载浏览器时,它会带有一些验证密钥嵌入到程序中。这个验证密钥与一个权威机构相关联,其责任是签署成千上万个网站的公钥,以便您可以信任这些而不必了解它们。您看不到的是这些网站必须向权威机构证明他们真正拥有自己的域名,然后才能获得对其公钥的签名。(实际上,您的浏览器信任许多权威机构来执行这项工作,而不仅仅是一个。)

图 7.3 在网络 PKI 中,浏览器信任一个权威机构来证明某些域名与某些公钥相关联。当安全访问网站时,您的浏览器可以通过验证来自权威机构的签名来验证网站的公钥确实属于他们自己(而不是来自某个中间人)。
在本节中,您从高层次的角度了解了签名。让我们深入了解签名的实际工作原理。但是为此,我们首先需要绕个弯,看看称为零知识证明(ZKP)的东西。
7.2 零知识证明(ZKP):签名的起源
理解密码学中签名工作原理的最佳方法是了解它们的来源。因此,让我们花点时间简要介绍 ZKP,然后我会回到签名。
想象一下,佩姬想向维克多证明某事。例如,她想证明自己知道某个群元素的离散对数。换句话说,她想证明自己知道x,给定Y = g^x,其中g是某个群的生成元。

当然,最简单的解决方案是佩姬简单地发送值x(称为见证)。这个解决方案将是一个简单的知识证明,这样就可以了,除非佩姬不希望维克多知道它。
注意 在理论上,我们说用于生成证明的协议如果完备,那么佩姬可以使用它向维克多证明她知道见证。如果她无法使用它证明自己所知,那么这个方案就是无用的,对吧?
在密码学中,我们主要关注不向验证者泄露见证的知识证明。这样的证明被称为零知识证明(ZKP)。
7.2.1 Schnorr 身份验证协议:一个交互式零知识证明
在接下来的页面中,我将逐步从破损的协议构建一个 ZKP,以向您展示爱丽丝如何证明她知道x而不泄露x。
在密码学中解决这种问题的典型方法是用一些随机性“隐藏”这个值(例如,通过加密)。但我们不仅仅是隐藏:我们还想证明它是存在的。为此,我们需要一种代数方法来隐藏它。一个简单的解决方案是简单地将一个随机生成的值 k 添加到证人中。
s = k + x
佩姬随后可以将隐藏的证人 s 与随机值 k 一起发送给维克多。此时,维克多没有理由相信佩姬确实将证人隐藏在 s 中。实际上,如果她不知道证人 x,那么 s 可能只是一些随机值。维克多知道的是,证人 x 正隐藏在 g 的指数中,因为他知道 Y = g^x。
为了确定佩姬是否真的知道这个证人,维克多可以检查她给他的东西是否与他所知的相匹配,这也必须在 g 的指数中进行(因为这是证人所在的地方)。换句话说,维克多检查这两个数字是否相等:
-
g^s (= g^(k+x))
-
Y × g^k (= g^x × g^k = g^(x+k))
思路是只有知道证人 x 的人才能构造出满足这个方程的“蒙眼”证人 s。因此,这是一种知识证明。我在图 7.4 中重述了这个零知识证明系统。

图 7.4 为了向维克多证明她知道证人 x,佩姬隐藏它(通过将其添加到随机值 k)并发送隐藏的证人 s。
不要那么快。这个方案有一个问题——显然不安全!实际上,由于隐藏证人 x 的方程只有一个未知数(x 本身),维克多可以简单地反转方程以检索证人:
x = s – k
为了解决这个问题,佩姬可以隐藏随机值 k 本身!这次,她必须将随机值隐藏在指数中(而不是将其加到另一个随机值中),以确保维克多的等式仍然成立:
R = g^k
这样,维克多就不会得知值 k(这是第五章介绍的离散对数问题),因此无法恢复证人 x。然而,他仍然拥有足够的信息来验证佩姬是否知道 x!维克多只需检查 g^s (= g^(k+x) = g^k × g^x) 是否等于 Y × R (= g^x × g^k)。我在图 7.5 中审查了这个第二次尝试的零知识证明协议。

图 7.5 为了使知识证明零知识,证明者可以用随机值 k 隐藏证人 x,然后隐藏随机值本身。
我们方案还有一个问题——佩姬可以欺骗。她可以让维克多相信她知道 x,而实际上并不知道 x!她所要做的就是反转她计算证明的步骤。她首先生成一个随机值 s,然后基于 s 计算值 R:
R = g^s × Y^(–1)
维克托然后计算Y × R = Y × g^s × Y(–1),这确实与*g*s 匹配。(佩吉使用逆来计算值的技巧在密码学中的许多攻击中都有所应用。)
注意 在理论上,我们说方案“可靠”,如果佩吉无法作弊(如果她不知道x,那么她无法愚弄维克托)。
为了使 ZKP 协议可靠,维克托必须确保佩吉从R计算出s而不是反向计算。为此,维克托使协议交互式:
-
佩吉必须对她的随机值k进行承诺,以便以后无法更改。
-
在收到佩吉的承诺后,维克托在协议中引入了一些自己的随机性。他生成一个随机值c(称为挑战)并将其发送给佩吉。
-
佩吉随后可以根据随机值k和挑战c计算她的隐藏承诺。
注意 在第二章中,您学习了承诺方案,我们使用哈希函数对我们可以稍后揭示的值进行承诺。但基于哈希函数的承诺方案不允许我们对隐藏值进行有趣的算术运算。相反,我们可以简单地将我们的生成器提升到该值,g^k,这是我们已经在做的事情。
因为佩吉无法在没有维克托的挑战c的情况下执行最后一步,而维克托又不会在看到随机值k的承诺之前发送挑战给她,所以佩吉被迫根据k计算s。获得的协议,我在图 7.6 中说明,通常被称为Schnorr 身份验证协议。

图 7.6 Schnorr 身份验证协议是一个完备的(佩吉可以证明她知道某个见证人)、可靠的(如果佩吉不知道见证人,她无法证明任何事情)和零知识的(维克托对见证人一无所知)交互式 ZKP。
所谓的交互式 ZKP 系统遵循三个步骤(承诺、挑战和证明)的模式,在文献中通常被称为Sigma 协议,有时写作Σ协议(因为希腊字母的形状具有说明性)。但这与数字签名有什么关系呢?
注意 Schnorr 身份验证协议在诚实验证者零知识(HVZK)模型中运作:如果验证者(维克托)表现不诚实并且不随机选择挑战,他们可以了解见证人的一些信息。一些更强大的 ZKP 方案在验证者恶意时仍然是零知识的。
7.2.2 签名作为非交互式零知识证明
以前的交互式 ZKP 的问题在于,嗯,它是交互式的,而现实世界的协议通常不喜欢交互性。交互式协议会增加一些不可忽略的开销,因为它们需要多个消息(可能通过网络)并且会增加无限延迟,除非两个参与者同时在线。由于这个原因,交互式 ZKP 在应用密码学领域中大多缺席。
所有这些讨论都不是毫无意义的!在 1986 年,Amos Fiat 和 Adi Shamir 发表了一种技术,允许将一个交互式的零知识证明(ZKP)轻松转换为一个非交互式的 ZKP。他们引入的技巧(称为费曼-沙米尔启发式或费曼-沙米尔变换)是让证明者自己计算挑战,以一种他们无法控制的方式。
这是一个诀窍——将挑战计算为到目前为止协议中发送和接收的所有消息的哈希(我们称之为转录)。如果我们假设哈希函数产生的输出与真正的随机数不可区分(换句话说,看起来是随机的),那么它可以成功模拟验证者的角色。
Schnorr 更进一步。他注意到任何东西都可以包含在那个哈希中!例如,如果我们在其中包含一条消息会怎样?我们得到的不仅是一个证明我们知道某个见证者x的证据,而且还是与证据密切相关的密码学链接的消息承诺。换句话说,如果证据是正确的,那么只有知道见证者的人(它变成签名密钥)才能承诺那条消息。
这就是一个签名!数字签名只是非交互式 ZKP。将 Fiat-Shamir 转换应用到 Schnorr 识别协议,我们得到了Schnorr 签名方案,我在图 7.7 中进行了说明。

图 7.7 左边的协议是之前讨论过的 Schnorr 识别协议,这是一个交互式协议。右边的协议是 Schnorr 签名,是左边协议的非交互式版本(其中验证者消息被替换为对转录进行哈希的调用)。
总结一下,Schnorr 签名基本上是两个值,R和s,其中R是对某个秘密随机值的承诺(通常称为nonce,因为它每个签名需要是唯一的),而s是通过承诺R、私钥(见证者x)和一条消息的帮助计算得出的值。接下来,让我们看一下签名算法的现代标准。
7.3 你应该使用(或不使用)的签名算法
像密码学中的其他领域一样,数字签名有许多标准,有时很难理解应该使用哪一个。这就是我在这里的原因!幸运的是,签名算法的类型与密钥交换的类型类似:有基于大数算术模的算法,如 Diffie-Hellman(DH)和 RSA,也有基于椭圆曲线的算法,如椭圆曲线 Diffie-Hellman(ECDH)。
请确保你对第五章和第六章的算法了解足够深入,因为我们现在要基于这些内容进行讨论。有趣的是,引入 DH 密钥交换的论文也提出了数字签名的概念(没有给出解决方案):
为了开发一种能够用一些纯电子形式的通信替代当前书面合同的系统,我们必须发现一个具有与书面签名相同属性的数字现象。 任何人都必须能够轻松识别签名的真实性,但除了合法签署者之外,任何其他人都不可能产生签名。 我们将称这样的技术为单向认证。 由于任何数字信号都可以精确复制,真正的数字签名必须在不被知道的情况下识别。
——Diffie 和 Hellman(《密码学的新方向》,1976 年)
一年后(1977 年),第一个签名算法(称为 RSA)与 RSA 非对称加密算法一起被引入(您在第六章中学到了)。 RSA 用于签名是我们将学习的第一个算法。
1991 年,NIST 提出了数字签名算法(DSA),试图避开 Schnorr 签名的专利。 出于这个原因,DSA 是 Schnorr 签名的一种奇怪的变体,发布时没有安全性证明(尽管目前尚未发现任何攻击)。 该算法被许多人采用,但很快被一个称为ECDSA(代表椭圆曲线数字签名算法)的椭圆曲线版本取代,就像椭圆曲线 Diffie-Hellman(ECDH)取代 Diffie-Hellman(DH)一样,由于其更小的密钥(请参见第五章)。 ECDSA 是我将在本节中讨论的第二种签名算法。
在 2008 年,Schnorr 签名的专利过期后,Daniel J. Bernstein,也就是 ChaCha20-Poly1305(在第四章中介绍)和 X25519(在第五章中介绍)的发明者,推出了一种新的签名方案,称为EdDSA(代表 Edwards 曲线数字签名算法),基于 Schnorr 签名。 自推出以来,EdDSA 迅速获得了采用,并且现在被认为是实际应用中数字签名的最新技术。 EdDSA 是我将在本节中讨论的第三种也是最后一种签名算法。
7.3.1 RSA PKCS#1 v1.5:一个糟糕的标准
RSA 签名目前被广泛应用,尽管它们不应该被使用(正如您将在本节中看到的,它们存在许多问题)。 这是因为该算法是第一个被标准化的签名方案,并且实际应用领域迟迟未能转向更新更好的算法。 因此,在您的学习过程中很可能会遇到 RSA 签名,我无法避免解释它们的工作原理和采用的标准。 但让我说,如果您理解了第六章中 RSA 加密的工作原理,那么本节应该很简单,因为使用 RSA 进行签名与使用 RSA 进行加密相反:
-
要进行签名,您需要使用私钥(而不是公钥)对消息进行加密,这将生成一个签名(组中的随机元素)。
-
要验证签名,您需要使用公钥(而不是私钥)对签名进行解密。 如果它将原始消息还原出来,则签名有效。
注意 实际上,在签名之前,消息通常会被散列,因为这样会占用更少的空间(RSA 只能签署比其模数小的消息)。结果也被解释为一个大数,以便可以在数学运算中使用。
如果你的私钥是私钥指数d,公钥是公钥指数e和公共模数N,你可以
-
通过计算signature = message^d mod N来签署消息
-
通过计算signature^e mod N来验证签名,并检查它是否等于消息
我在图 7.8 中以图示方式说明了这一点。

图 7.8 要使用 RSA 签名,我们只需对 RSA 加密算法进行逆操作:我们使用私钥指数对消息进行指数运算,然后进行验证,我们使用公钥指数对签名进行指数运算,返回到消息。
这样做的原因是只有了解私钥指数d的人才能对消息产生签名。与 RSA 加密一样,安全性与因子分解问题的难度紧密相连。
那么用 RSA 进行签名的标准是什么?幸运的是,它们遵循与 RSA 加密相同的模式:
-
RSA 用于加密在 PKCS#1 v1.5 文档中松散标准化。同一文档还包含了 RSA 签名的规范(没有安全证明)。
-
然后在 PKCS#1 v2 文档中对 RSA 进行了重新标准化,采用了更好的构造方法(称为 RSA-OAEP)。同一文档中也对 RSA 签名进行了标准化,RSA-PSS 方案也在其中标准化(附带安全证明)。
我在第六章关于非对称加密中讨论了 RSA PKCS#1 v1.5。在该文档中标准化的签名方案与加密方案几乎相同。要签名,首先使用所选的哈希函数对消息进行哈希,然后根据 PKCS#1 v1.5 的签名填充进行填充(这与相同标准中的加密填充类似)。接下来,使用私钥指数对填充和散列消息进行加密。我在图 7.9 中说明了这一点。

图 7.9 RSA PKCS#1 v1.5 用于签名。要签名,先使用 PKCS#1 v1.5 填充方案对消息进行哈希和填充。最后一步使用私钥指数d对填充的哈希消息进行指数运算取模N。要验证,只需使用公钥指数e对签名进行指数运算取模N,并验证它是否与填充的哈希消息匹配。
多个 RSAs
顺便说一句,不要被 RSA 周围的不同术语搞混了。有 RSA(非对称加密原语)和 RSA(签名原语)。此外,还有 RSA(公司),由 RSA 的发明者创立。提到用 RSA 加密时,大多数人指的是 RSA PKCS#1 v1.5 和 RSA-OAEP 方案。提到用 RSA 签名时,大多数人指的是 RSA PKCS#1 v1.5 和 RSA-PSS 方案。
我知道这可能会让人感到困惑,特别是对于 PKCS#1 v1.5 标准。 尽管在 PKCS#1 v1.5 中有官方名称来区分加密和签名算法(RSAES-PKCS1-v1_5 用于加密,RSASSA-PKCS1-v1_5 用于签名),但我很少看到这些名称被使用。
在第六章中,我提到了对 RSA PKCS#1 v1.5 进行加密的破坏性攻击;不幸的是,对于 RSA PKCS#1 v1.5 签名也是如此。 1998 年,Bleichenbacher 发现了对 RSA PKCS#1 v1.5 加密的毁灭性攻击后,他决定看看签名标准。 Bleichenbacher 在 2006 年提出了对 RSA PKCS#1 v1.5 的签名伪造攻击,这是对签名的最灾难性的攻击类型之一——攻击者可以在不知道私钥的情况下伪造签名! 与直接破解加密算法的第一次攻击不同,第二次攻击是一种实现攻击。 这意味着如果签名方案按照规范正确实现,攻击就不会奏效。
实现缺陷听起来不像算法缺陷那么糟糕,也就是说,如果很容易避免并且不影响许多实现。 不幸的是,2019 年已经表明,尴尬的是,许多开源实现的 RSA PKCS#1 v1.5 签名实际上陷入了这个陷阱,并且错误地实现了标准(参见 Chau 等人的“使用符号执行分析语义正确性的案例研究:PKCS#1 v1.5 签名验证”)。 各种实现缺陷最终导致了不同变体的 Bleichenbacher 的伪造攻击。
不幸的是,RSA PKCS#1 v1.5 签名仍然被广泛使用。 如果您真的必须出于向后兼容性原因使用此算法,请注意这些问题。 话虽如此,这并不意味着 RSA 签名是不安全的。 故事并没有在这里结束。
7.3.2 RSA-PSS:更好的标准
RSA-PSS 在更新的 PKCS#1 v2.1 中标准化,并包括了安全性证明(与之前的 PKCS#1 v1.5 中标准化的签名方案不同)。 新规范的工作方式如下:
-
使用 PSS 编码算法对消息进行编码
-
使用 RSA 对编码消息进行签名(就像在 PKCS#1 v1.5 标准中所做的那样)
PSS 编码稍微复杂,类似于 OAEP(Optimal Asymmetric Encryption Padding)。 我在图 7.10 中进行了说明。

图 7.10 RSA-PSS 签名方案使用掩码生成函数(MGF)对消息进行编码,就像你在第六章中学到的 RSA-OAEP 算法一样,然后以通常的 RSA 方式进行签名。
验证由 RSA-PSS 产生的签名只是在将签名提升到公共模数的公共指数模下,反转编码的问题。
PSS 的可证明安全性
PSS(概率签名方案)是可证明安全的,意味着没有人应该能够在不知道私钥的情况下伪造签名。 PSS 并非证明了如果 RSA 安全则 RSA-PSS 安全,而是证明了逆否命题:如果有人能够破解 RSA-PSS,那么该人也能够破解 RSA。这是密码学中证明事物的一种常见方式。当然,这仅在 RSA 安全时才有效,这是我们在证明中假设的。
如果你还记得,我在第六章也谈到了 RSA 加密的第三种算法(称为 RSA-KEM)——这是一种没有任何人使用但被证明安全的更简单的算法。有趣的是,RSA 签名也反映了 RSA 加密历史的这一部分,并且有一个几乎没有人使用的更简单的算法;它被称为完全域哈希(FDH)。 FDH 通过简单地对消息进行哈希,然后使用 RSA 签名(通过将摘要解释为数字)来工作。
尽管 RSA-PSS 和 FDH 都具有安全性证明并且更容易正确实现,但今天大多数协议仍然使用 RSA PKCS#1 v1.5 进行签名。这只是加密算法淘汰通常发生的缓慢的又一个例子。由于旧的实现仍然必须与新的实现一起工作,因此删除或替换算法变得困难。考虑一下不更新应用程序的用户、不提供软件新版本的供应商、无法更新的硬件设备等等。接下来,让我们看看一个更现代的算法。
7.3.3 椭圆曲线数字签名算法(ECDSA)
在本节中,让我们看看 ECDSA,这是 DSA 的椭圆曲线变体,它本身只是为了规避 Schnorr 签名的专利而发明的。该签名方案在许多标准中指定,包括 ISO 14888-3、ANSI X9.62、NIST 的 FIPS 186-2、IEEE P1363 等等。并非所有标准都兼容,希望进行互操作的应用程序必须确保它们使用相同的标准。
不幸的是,与 DSA 一样,ECDSA 没有安全性证明,而 Schnorr 签名却有。尽管如此,ECDSA 已被广泛采用,并且是最常用的签名方案之一。在本节中,我将解释 ECDSA 的工作原理以及如何使用它。与所有这些方案一样,公钥几乎总是根据相同的公式生成:
-
私钥是一个随机生成的大数x。
-
公钥是通过将x视为椭圆曲线密码学中的一个生成器(称为基点)中的索引而获得的。
更具体地说,在 ECDSA 中,公钥是使用[x]G计算的,其中x与基点G的标量乘积。
加法还是乘法符号?
请注意,我使用加法符号(在标量周围放置括号的椭圆曲线语法),但如果我想使用乘法符号,我可以写public_key = G^x。这些差异在实践中并不重要。大多数时候,不关心群的基础性质的加密协议使用乘法符号编写,而专门在椭圆曲线群中定义的协议倾向于使用加法符号编写。
要计算 ECDSA 签名,你需要与 Schnorr 签名所需的相同输入:签署消息的哈希值(H(m)),你的私钥x,以及每个签名唯一的随机数k。ECDSA 签名是两个整数,r和s,计算如下:
-
r是[k] G的 x 坐标
-
s等于k^(–1) (H(m) + xr) mod p
要验证 ECDSA 签名,验证者需要使用相同的哈希消息H(m),签名者的公钥,以及签名数值r和s。验证者然后
-
计算[H(m) s^(–1)]G + [rs^(–1)]public_key
-
验证所得点的 x 坐标是否与签名值r相同
你肯定能够认识到与 Schnorr 签名有一些相似之处。随机数k有时被称为nonce,因为它是一个只能使用一次的数字,有时也被称为ephemeral key,因为它必须保持秘密。
警告我再次强调:k绝对不能重复或可预测!没有这一点,恢复私钥就变得微不足道。
一般来说,加密库在幕后执行此 nonce(k值)的生成,但有时不会让调用者提供它。这当然是一场灾难。例如,在 2010 年,索尼的 Playstation 3 被发现使用重复 nonce 的 ECDSA(泄漏了他们的私钥)。
警告更加微妙的是,如果 nonce k不是均匀和随机选择的(特别是,如果你可以预测前几位),仍然存在可以在瞬间恢复私钥的强大攻击(所谓的格攻击)。在理论上,我们称这种密钥检索攻击为全面破解(因为它们破坏了一切!)。这种全面破解在实践中非常罕见,这使得 ECDSA 算法可能以惊人的方式失败。
存在避免 nonce 问题的尝试。例如,RFC 6979 指定了一个基于消息和私钥生成 nonce 的确定性 ECDSA方案。这意味着两次签署相同消息涉及两次相同的 nonce,因此产生两次相同的签名(这显然不是问题)。
倾向于与 ECDSA 一起使用的椭圆曲线基本上与椭圆曲线 Diffie-Hellman(ECDH)算法(参见第五章)中流行的曲线相同,但有一个显着的例外:Secp256k1。Secp256k1 曲线在 SEC 2 中定义:“推荐的椭圆曲线域参数” (secg.org/sec2-v2.pdf),由高效密码学标准组(SECG)编写。在比特币决定使用它而不是更流行的 NIST 曲线之后,它受到了很多关注,原因是我在第五章中提到的对 NIST 曲线的不信任。
Secp256k1 是一种称为 Koblitz 曲线 的椭圆曲线类型。Koblitz 曲线只是具有一些参数约束的椭圆曲线,这些约束允许在曲线上优化一些操作。椭圆曲线具有以下方程式:
y² = x³ + ax + b
其中 a = 0 和 b = 7 是常数,x 和 y 定义在模素数 p 上:
p = 2¹⁹² – 2³² – 2¹² – 2⁸ – 2⁷ – 2⁶ – 2³ – 1
这定义了一个素数阶的群,与 NIST 曲线相似。今天,我们有有效的公式来计算椭圆曲线上点的数量。这是 Secp256k1 曲线中点的数量(包括无穷远点)的素数:
115792089237316195423570985008687907852837564279074904382605163141518161494337
我们使用固定点 G 作为生成器(或基点)的坐标
x = 55066263022277343669578718895168534326250603453777594175500187360389116729240
和
y = 32670510020758816978083085130507043184471273380659243275938904335757337482424
尽管如此,今天 ECDSA 大多数与 NIST 曲线 P-256(有时称为 Secp256r1;注意区别)一起使用。接下来让我们看另一种广泛流行的签名方案。
7.3.4 Edwards 曲线数字签名算法(EdDSA)
让我介绍一下本章的最后一个签名算法,Edwards 曲线数字签名算法(EdDSA),由 Daniel J. Bernstein 于 2011 年发布,以回应对 NIST 和其他政府机构创建的曲线的不信任。EdDSA 这个名字似乎表明它基于 DSA 算法,就像 ECDSA 一样,但这是误导的。EdDSA 实际上基于 Schnorr 签名,这是由于 Schnorr 签名专利在 2008 年早些时候到期而可能的。
EdDSA 的一个特殊之处在于该方案不需要每次签名操作都产生新的随机数。EdDSA 确定性地生成签名。这使得该算法相当具有吸引力,并且已被许多协议和标准采用。
EdDSA 正在着手包括在 NIST 的即将更新的 FIPS 186-5 标准中(截至 2021 年初仍是草案)。当前的官方标准是 RFC 8032,它定义了两个不同安全级别的曲线,可用于 EdDSA。所定义的两个曲线都是 扭曲的 Edwards 曲线(一种启用有趣的实现优化的椭圆曲线类型):
-
Edwards25519 基于 Daniel J. Bernstein 的 Curve25519(在第五章中介绍)。由于椭圆曲线的类型所启用的优化,其曲线操作可以比 Curve25519 更快地实现。由于它是在 Curve25519 之后发明的,基于 Curve25519 的密钥交换 X25519 并未从这些速度改进中受益。与 Curve25519 一样,Edwards25519 提供了 128 位安全性。
-
Edwards448 基于 Mike Hamburg 的 Ed448-Goldilocks 曲线。它提供了 224 位安全性。
在实践中,EdDSA 主要使用 Edwards25519 曲线实例化,该组合被称为 Ed25519(而带有 Edwards448 的 EdDSA 则缩写为 Ed448)。与现有方案不同,EdDSA 的密钥生成略有不同。EdDSA 不直接生成签名密钥,而是生成一个秘密密钥,然后用于派生实际的签名密钥和我们称之为随机数密钥的另一个密钥。那个随机数密钥很重要!它是用于确定性地生成所需每个签名的随机数的密钥。
注意 根据您使用的加密库,您可能正在存储秘密密钥或两个派生密钥:签名密钥和随机数密钥。不是这很重要,但如果您不知道这一点,那么如果遇到将 Ed25519 秘密密钥存储为 32 字节或 64 字节,具体取决于所使用的实现,则可能会感到困惑。
要签名,EdDSA 首先通过将随机数密钥与要签名的消息进行哈希运算来确定性地生成随机数。之后,类似于 Schnorr 签名的过程如下进行:
-
计算随机数为 HASH(nonce key || message)
-
计算承诺 R 为 [nonce]G,其中 G 是群的基点
-
计算挑战为 HASH(commitment || public key || message)
-
计算证明 S 为 nonce + challenge × signing key
签名是(R,S)。我在图 7.11 中说明了 EdDSA 的重要部分。

Figure 7.11 EdDSA 密钥生成产生一个秘密密钥,然后用于派生另外两个密钥。第一个派生密钥是实际的签名密钥,因此可用于派生公钥;另一个派生密钥是随机数密钥,在签名操作期间用于确定性地派生随机数。然后,EdDSA 签名类似于 Schnorr 签名,唯一的异常是(1)随机数是根据随机数密钥和消息确定性生成的,并且(2)签名者的公钥包含在挑战的一部分中。
注意随机数(或临时密钥)如何确定性地而不是概率性地从随机数密钥和给定的消息中派生出来。这意味着签署两个不同的消息应该涉及两个不同的随机数,巧妙地防止签署者重复使用随机数,从而泄漏密钥(就像 ECDSA 可能发生的情况一样)。两次签署相同的消息会产生两次相同的随机数,然后也会产生两次相同的签名。这显然不是问题。可以通过计算以下两个方程式来验证签名:
[S]G
R + [HASH(R || public key || message)] public key
如果这两个值匹配,则签名有效。这与 Schnorr 签名的工作方式完全相同,只是现在我们处于一个椭圆曲线组中,我在这里使用了加法表示法。
EdDSA 的最广泛使用的实例是 Ed25519,它使用 Edwards25519 曲线和 SHA-512 作为哈希函数进行定义。 Edwards25519 曲线的定义包含满足以下方程的所有点:
–x² + y² = 1 + d × x² × y² mod p
其中值 d 是大数
37095705934669439343138083508754565189542113879843219016388785533085940283555
变量 x 和 y 取模 p,即大数 2²⁵⁵ – 19(用于 Curve25519 的相同素数)。基点是坐标为 G
x = 15112221349535400772501151409588531511454012693041857206046113283949847762202
和
y = 46316835694926478169428394003475163141307993866256225615783033603165251855960
RFC 8032 实际上定义了三种使用 Edwards25519 曲线的 EdDSA 变体。所有三种变体都遵循相同的密钥生成算法,但具有不同的签名和验证算法:
-
Ed25519(或 pureEd25519) —— 这就是我之前解释过的算法。
-
Ed25519ctx —— 此算法引入了一个强制的定制字符串,并且在实践中很少被实现,甚至很少被使用。唯一的区别是在每次调用哈希函数时都添加了一些用户选择的前缀。
-
Ed25519ph(或 HashEd25519) —— 这允许应用程序在签名之前对消息进行预哈希(因此名称中有 ph)。它还基于 Ed25519ctx,允许调用者包含一个可选的自定义字符串。
在密码学中增加一个 定制字符串 是相当常见的,就像你在第二章中看到的某些哈希函数,或者在第八章中看到的密钥派生函数一样。当协议中的参与者在不同的上下文中使用相同的密钥对消息进行签名时,这是一个有用的补充。例如,你可以想象一个应用程序,允许你使用私钥签名交易,也可以向你交谈的人签署私人消息。如果你错误地签署并发送了一个看起来像交易的消息给你的邪恶朋友 Eve,她可能会尝试将其重新发布为有效的交易,如果无法区分你签署的两种类型的有效载荷的话。
Ed25519ph 仅为了满足需要签署大型消息的调用者而引入。正如您在第二章中看到的,哈希函数通常提供“初始化-更新-完成”接口,允许您连续哈希数据流,而无需将整个输入保留在内存中。
现在您已经完成了对实际应用中使用的签名方案的介绍。接下来,让我们看看在使用这些签名算法时可能如何自掘坟墓。但首先,让我们回顾一下:
-
RSA PKCS#1 v1.5 仍然被广泛使用,但正确实现很困难,许多实现已被发现存在问题。
-
RSA-PSS 具有安全性证明,更易于实现,但由于基于椭圆曲线的新方案而受到较少采用。
-
ECDSA 是 RSA PKCS#1 v1.5 的主要竞争对手,大多数情况下与 NIST 的曲线 P-256 一起使用,除了在加密货币世界中,Secp256k1 似乎占主导地位。
-
Ed25519 基于 Schnorr 签名,已经得到广泛采用,并且与 ECDSA 相比更容易实现;它不需要每次签名操作都产生新的随机数。如果可以的话,这是您应该使用的算法。
7.4 签名方案的微妙行为
签名方案可能具有一些微妙的特性。虽然它们在大多数协议中可能并不重要,但在处理更复杂和非常规的协议时,不了解这些“陷阱”可能会给您带来麻烦。本章的最后部分重点介绍了数字签名的已知问题。
7.4.1 签名替换攻击
数字签名并不能唯一地识别密钥或消息。
——Andrew Ayer(《让我们加密中的重复签名密钥选择攻击》,2015)
替换攻击,也称为重复签名密钥选择(DSKS),对 RSA PKCS#1 v1.5 和 RSA-PSS 都是可能的。存在两种 DSKS 变体:
-
密钥替换攻击——使用不同的密钥对或公钥来验证给定消息上的给定签名。
-
消息密钥替换攻击——使用不同的密钥对或公钥来验证给定消息上的新签名。
再说一遍:第一次攻击同时修复了消息和签名;第二次攻击只修复了签名。我在图 7.12 中总结了这一点。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

图 10.20 有两种方法可以实现群聊的端到端加密。客户端端点扩散方法意味着客户端必须使用其已经存在的加密通道向每个接收者单独发送消息。这是一个很好的方法,可以隐藏群组成员。服务器端端点扩散方法允许服务器将消息转发给每个群聊参与者。从客户端的角度来看,这是一种减少发送消息数量的好方法。
另一方面,WhatsApp 使用 Signal 协议的变体,其中服务器知道群聊成员。这一变化允许参与者向服务器发送单个加密消息,服务器负责将其转发给群成员。这称为服务器端端点扩散。
群聊的另一个问题是扩展性,可以扩展到大量成员。为此,行业中的许多参与者最近围绕消息层安全(MLS)标准聚集在一起,以应对大规模安全的群组消息传递。但是似乎还有很多工作要做,人们可以想象一下,在拥有一百多名参与者的群聊中是否真的有保密性?
注意 这仍然是一个积极研究的领域,不同的方法具有安全性和可用性方面的不同权衡。例如,在 2021 年,似乎没有任何群聊协议提供转录一致性,这是一种确保群聊所有参与者以相同顺序看到相同消息的加密属性。
支持多个设备要么根本不存在,要么以各种方式实现,最常见的是假装你的不同设备是群聊的不同参与者。TOFU 模型可以使处理多个设备变得非常复杂,因为每个设备具有不同的身份密钥可能会成为一个真正的密钥管理问题。想象一下,为每个设备以及每个朋友的设备验证指纹。例如,Matrix 让用户签署自己的设备。然后其他用户可以通过验证其关联的签名来信任所有你的设备作为一个实体。
最后,我提到 TOFU 模型也不是最好的,因为它是基于第一次看到公钥时信任它,而大多数用户后来并不验证指纹是否匹配。这个问题能做些什么?如果服务器决定只向 Alice 冒充 Bob 怎么办?这是密钥透明度试图解决的问题。密钥透明度是 Google 提出的一个协议,类似于我在第九章讨论过的证书透明度协议。还有一些研究利用区块链技术,我将在第十二章关于加密货币的部分谈到。
摘要
-
端到端加密是为了保护真实人类之间的通信。实施端到端加密的协议对于服务器之间发生的漏洞更具弹性,并且可以极大地简化公司的法律要求。
-
端到端加密系统需要一种方法来在用户之间建立信任。这种信任可以来自我们已经知道的公钥,或者是我们信任的带外信道。
-
PGP 和 S/MIME 是今天用于加密电子邮件的主要协议,然而它们都不被认为是安全的使用方式,因为它们使用了旧的密码算法和实践。它们还与已经被证明在实践中容易受到不同攻击的电子邮件客户端集成很差。
-
PGP 使用信任网(WOT)模型,用户相互签署公钥以便让其他人信任他们。
-
S/MIME 使用公钥基础设施来建立参与者之间的信任。它最常用于公司和大学。
-
-
PGP 的一种替代方案是 saltpack,它修复了一些问题,同时依赖社交网络来发现其他人的公钥。
-
电子邮件在加密方面始终存在问题,因为该协议并未考虑加密。另一方面,现代消息传递协议和应用程序被认为是加密电子邮件的更好选择,因为它们在设计时考虑了端到端加密。
-
Signal 协议被大多数消息传递应用程序用于保护用户之间的端到端通信。Signal Messenger、WhatsApp、Facebook Messenger 和 Skype 都宣称他们使用 Signal 协议来保护消息。
-
其他协议,如 Matrix,试图标准化端到端加密消息传递的联邦协议。联邦协议是任何人都可以与之交互操作的开放协议(与限制在单个应用程序中的集中协议相对)。
-
第十一章:用户认证
本章涵盖了
-
认证人员和数据之间的区别
-
用户认证,根据密码或密钥对用户进行身份验证。
-
用户辅助认证以保护用户设备之间的连接
在本书的介绍中,我将密码学简化为两个概念:机密性和认证。 在实际应用中,保密性(通常)不是你的问题所在; 认证是大部分复杂性产生的地方。 我知道我已经在整本书中大量谈论了认证,但是它在密码学中使用的含义不同,可能是一个令人困惑的概念。 因此,本章从介绍认证的真正含义开始。 与密码学一样,没有协议是万能药,本章的其余部分将教你大量在实际应用中使用的认证协议。
11.1 认证回顾
到目前为止,你已经多次听说过认证,所以让我们回顾一下。 你学到了关于
-
在密码学原语中的认证,如消息认证码(在第三章中介绍)和认证加密(在第四章中介绍)
-
密码协议中的认证,如 TLS(在第九章中介绍)和 Signal(在第十章中介绍),其中协议的一个或多个参与者可以被认证
在第一种情况下,认证指的是消息的真实性(或完整性)。 在后一种情况下,认证是指向别人证明自己是谁。 这些是由相同单词涵盖的不同概念,这可能会相当令人困惑! 但是牛津英语词典(www.oed.com/)指出,这两种用法都是正确的:
认证。证明或展示某事物为真实、真正或有效的过程或行为。
由于这个原因,你应该将认证视为密码学术语,根据上下文传达两个不同的概念:
-
消息/有效载荷认证—你正在证明一条消息是真实的,并且自其创建以来没有被修改过。 (例如,这些消息是否已经认证,还是有人可能篡改它们?)
-
源/实体/身份认证—你正在证明一个实体确实是他们声称的那个人。 (例如,我是否真的在与google.com通信?)
底线:认证是关于证明某物是它应该是的,而某物可以是人、消息或其他东西。 在本章中,我将仅使用术语认证来指代识别人或机器。 换句话说,身份认证。 顺便说一句,你已经看到了很多关于这种类型认证的内容:
-
在第九章,关于安全传输,你学到了机器如何通过使用公钥基础设施(PKI)来大规模认证其他机器。
-
在第十章中,关于端到端加密,您了解了人类如何通过使用首次使用信任(TOFU)(然后稍后验证)或使用信任网络(WOT)技术来规模化认证彼此。
在本章中,您将学习到前面未提及的其他两种情况。(我在图 11.1 中进行了回顾。)
-
用户认证,或者说机器如何认证人类——滴滴滴滴
-
用户辅助认证,或者说人类如何帮助机器认证彼此

图 11.1 在本书中,我讨论了三种情景下的源认证。当设备认证人类时,就发生了用户认证。当机器认证另一台机器时,就发生了机器认证。当一个人参与机器认证另一台机器时,就发生了用户辅助认证。
身份认证的另一个方面是身份部分。确实,我们如何在密码协议中定义像爱丽丝这样的人?机器如何认证你和我?遗憾的是(或者幸运的是),肉体和比特之间存在固有的差距。为了弥合现实和数字世界之间的鸿沟,我们总是假设爱丽丝是唯一知道一些秘密数据的人,为了证明她的身份,她必须证明她知道那些秘密数据。例如,她可以发送她的密码,或者她可以使用与她的公钥相关联的私钥签署一个随机挑战。
好了,介绍就到此为止。如果这一部分让你感到有些迷糊,接下来的众多例子会让你明白的。现在让我们首先看看机器找到的认证我们人类的多种方式!
11.2 用户认证,或摆脱密码的追求
本章的第一部分是关于机器如何认证人类,换句话说就是用户认证。有很多方法可以做到这一点,没有一种解决方案是万能的。但在大多数用户认证场景中,我们假设
-
服务器已经经过认证。
-
用户与之共享安全连接。
例如,您可以想象服务器通过网络公钥基础设施(PKI)对用户进行了认证,并且连接通过 TLS(在第九章中都有涉及)。在某种意义上,本节的大部分内容都是关于将单向认证连接升级为双向认证连接,就像图 11.2 所示的那样。

图 11.2 用户认证通常发生在已经安全的通道上,但只有服务器经过认证的情况。一个典型的例子是当您使用 HTTPS 浏览网页并使用您的凭据登录网页时。
我必须警告你:用户认证是一个充满了空头支票的广阔领域。您肯定已经多次使用密码对不同的网页进行认证,而您自己的经历可能类似于这样:
-
您在网站上注册了用户名和密码。
-
使用新凭据登录网站。
-
当您恢复您的账户或者因为网站强制要求时,您需要更改密码。
-
如果您运气不佳,您的密码(或其哈希值)可能会在一系列数据库泄露中泄露。
听起来耳熟吗?
请注意,本章节将忽略密码/账户恢复,因为它们与密码学关系不大。只需知道它们通常与您最初注册的方式相关联。例如,如果您在工作场所的 IT 部门注册,那么如果您忘记密码,您可能需要去找他们,如果不小心的话,他们可能是系统中最薄弱的环节。事实上,如果我可以通过拨打一个号码并告诉某人您的生日来恢复您的账户,那么在登录时进行的任何酷炫的密码学都无济于事。
实现先前用户认证流程的一个天真的方法是在注册时存储用户密码,然后在登录时要求用户输入密码。正如第三章所述,一旦成功认证,用户通常会获得一个可以在每个后续请求中发送的 cookie,而不是用户名和密码。但是等等;如果服务器以明文形式存储您的密码,那么其数据库的任何泄露都会将您的密码暴露给攻击者。这些攻击者随后将能够使用它登录您在其中使用相同密码注册的任何网站。
更好的存储密码的方法是使用像您在第二章中学到的标准化的 Argon2 这样的密码哈希算法。这将有效地防止对数据库进行的一种破坏性攻击,以泄露您的密码,尽管一个过度进入的入侵者仍然可以在您每次登录时看到您的密码。然而,许多网站和公司仍然以明文形式存储密码。
练习
有时,应用程序试图通过让客户端在发送密码之前对密码进行哈希处理(也许使用密码哈希)来解决服务器在注册时了解用户密码的问题。您能确定这是否真的有效吗?
此外,人类在密码方面天生就不擅长。我们通常更喜欢使用简单易记的短密码。而且,如果可能的话,我们希望可以在所有地方都重复使用相同的密码。
81%的黑客入侵事件利用了被盗或弱密码。
—Verizon 数据泄露报告(2017)
弱密码和密码重用的问题导致了许多愚蠢和恼人的设计模式,试图迫使用户更加认真对待密码。例如,一些网站要求您在密码中使用特殊字符,或者强制您每 6 个月更改一次密码,等等。此外,许多协议试图“修复”密码或完全摆脱它们。每年,新的安全专家似乎都认为“密码”这个概念已经过时。然而,它仍然是最广泛使用的用户认证机制。

所以在这里,密码可能会一直存在。然而,存在许多可以改进或替代密码的协议。让我们看看这些。
11.2.1 一切由一个密码控制:单点登录(SSO)和密码管理器
好的,密码重用是不好的,那我们能做些什么呢?天真地,用户可以为不同的网站使用不同的密码,但这种方法有两个问题:
-
用户不擅长创建许多不同的密码。
-
记住多个密码所需的心理负担是不切实际的。
为了缓解这些问题,已经广泛采用了两种解决方案:
-
单点登录(SSO)—SSO 的理念是允许用户通过证明他们拥有单个服务的帐户来连接到许多不同的服务。这样,用户只需记住与该服务关联的密码,就能连接到许多服务。想象一下“使用 Facebook 登录”类型的按钮,正如图 11.3 所示。
-
密码管理器—如果您使用的不同服务都支持前述 SSO 方法,那么这种方法很方便,但显然对于像网页这样的场景来说并不可扩展。在这些极端情况下,一个更好的方法是改进客户端,而不是试图在服务器端解决问题。如今,现代浏览器内置了密码管理器,当您在新网站注册时可以建议复杂的密码,并且只要您记住一个主密码,它们就可以记住您的所有密码。

图 11.3 网页上单点登录(SSO)的示例。通过在 Facebook 或 Google 上拥有帐户,用户可以连接到新服务(在此示例中是 Airbnb),而无需考虑新密码。
单点登录(SSO)的概念在企业世界并不新鲜,但它在普通终端用户中的成功是比较近期的。如今,在建立 SSO 时,有两个主要竞争者协议:
-
安全断言标记语言 2.0(SAML)—一种使用可扩展标记语言(XML)编码的协议。
-
OpenID Connect(OIDC)—OAuth 2.0(RFC 6749)授权协议的扩展,使用 JavaScript 对象表示法(JSON)编码。
SAML 仍然被广泛使用,主要是在企业环境中,但(目前)它是一种遗留协议。另一方面,OIDC 可以在网页和移动应用程序中随处可见。你很可能已经使用过它!
认证协议通常被认为很难正确使用。OIDC 依赖的协议 OAuth2 以易被滥用而臭名昭著。另一方面,OIDC 被很好地规范化(参见openid.net)。确保你遵循标准并查看最佳实践,因为这可以避免许多麻烦。
注意 这里有另一个例子,一个相当大的公司决定不遵循这些建议。2020 年 5 月,苹果登录 SSO 流程不遵循 OIDC 的做法被发现存在漏洞。任何人都可以通过查询苹果的服务器获得任何苹果账户的有效 ID 令牌。
SSO 对用户很有好处,因为它减少了他们需要管理的密码数量,但它并没有完全消除密码。用户仍然必须使用密码连接到 OIDC 提供者。接下来,让我们看看密码学如何帮助隐藏密码。
11.2.2 不想看到他们的密码?使用非对称密码身份验证密钥交换
前一节调查了试图简化用户身份管理的解决方案,允许他们使用仅链接到单个服务的一个帐户来认证到多个服务。虽然 OIDC 等协议很好,因为它们有效地减少了用户需要管理的密码数量,但它们并不改变某些服务仍然需要以明文形式查看用户密码的事实。即使密码在密码哈希后存储,每次用户注册、更改密码或登录时仍然以明文形式发送。
称为非对称(或增强型)密码身份验证密钥交换(PAKEs)的加密协议试图提供用户身份验证,而无需用户直接将其密码传递给服务器。这与对称或平衡的 PAKEs协议形成对比,后者双方都知道密码。
目前最流行的非对称 PAKE 是安全远程密码(SRP)协议,该协议于 2000 年首次标准化于 RFC 2944(“Telnet Authentication: SRP”),后来通过 RFC 5054(“Using the Secure Remote Password (SRP) Protocol for TLS Authentication”)集成到 TLS 中。它是一个相当古老的协议,并且存在许多缺陷。例如,如果注册流程被中间人攻击者拦截,那么攻击者将能够冒充并登录为受害者。它还不能很好地与现代协议配合使用,因为它无法在椭圆曲线上实例化,更糟糕的是,它与 TLS 1.3 不兼容。
自 SRP 发明以来,已经提出并标准化了许多非对称 PAKE。2019 年夏季,IETF 的 Crypto Forum Research Group(CFRG)开始了一个 PAKE 选择过程,目标是为每个 PAKE 类别(对称/平衡和非对称/增强型)选择一个算法进行标准化。2020 年 3 月,CFRG 宣布 PAKE 选择过程结束,选择
-
CPace——由 Haase 和 Benoît Labrique 发明的推荐的对称/平衡 PAKE
-
OPAQUE——由 Stanislaw Jarecki、Hugo Krawczyk 和 Jiayu Xu 发明的推荐的非对称/增强型 PAKE
在本节中,我将讨论 OPAQUE,在 2021 年初仍在标准化过程中。在本章的第二节中,您将了解更多关于对称 PAKEs 和 CPace 的信息。
OPAQUE 从同音异义词 O-PAKE 中取其名称,其中 O 指的是术语 oblivious。这是因为 OPAQUE 依赖于本书中尚未提到的密码原语:一个 oblivious pseudorandom function(OPRF)。
无意识伪随机函数(OPRFs)
OPRFs 是一个模拟第三章中所学的 PRFs 的两方协议。作为提醒,PRF 在某种程度上等同于人们对 MAC 的预期:它接受一个密钥和一个输入,并给出一个固定长度的完全随机输出。
注意:密码学中的术语 oblivious 通常指的是一个参与方计算加密操作而不知道另一方提供的输入的协议。
下面是 OPRF 的高层次工作方式:
-
Alice 想要对输入计算一个 PRF,但希望输入保持秘密。她使用一个随机值(称为 blinding factor) “盲化” 她的输入,并将其发送给 Bob。
-
Bob 使用他的秘钥在这个被盲化的数值上运行 OPRFs,但输出仍然被盲化,所以对 Bob 毫无用处。Bob 然后将这个返回给 Alice。
-
Alice 最后使用与之前用于获取真实输出相同的盲化因子 “解盲” 结果。
需要注意的是,每次 Alice 想要执行这个协议时,她都必须创建一个不同的盲化因子。但无论她使用什么盲化因子,只要她使用相同的输入,她总是会得到相同的结果。我在图 11.4 中进行了说明。

图 11.4 一个无意识 PRF(OPRF)是一种构造,允许一个参与方在不了解该参与方输入的情况下计算 PRF。为了实现这一点,Alice 首先生成一个随机的盲化因子,然后使用它来盲化她的输入,然后发送给 Bob。Bob 使用他的秘密密钥在被盲化的数值上计算 PRF,然后将盲化输出发送给 Alice,Alice 可以对其进行解盲。结果不依赖于盲化因子的值。
这是在离散对数问题难度高的群中实现的 OPRF 协议的一个示例:
-
Alice 将她的输入转换为一个群元素 x。
-
Alice 生成一个随机的盲化因子 r。
-
Alice 通过计算 blinded_input = x^r 来盲化她的输入。
-
Alice 将 blinded_input 发送给 Bob。
-
Bob 计算 blinded_output = blinded_input^k,其中 k 是秘密密钥。
-
Bob 将结果发送回给 Alice。
-
Alice 然后可以通过计算 output = blinded_output^(1/r) = x^k 来解盲产生的结果,其中 1/r 是 r 的倒数。
OPAQUE 如何使用这个有趣的构造是非对称 PAKE 的整个技巧。
OPAQUE 非对称 PAKE,它是如何工作的?
我们的想法是,我们希望客户端,比如 Alice,能够与某个服务器进行经过身份验证的密钥交换。我们还假设 Alice 已经知道服务器的公钥或已经有一种方法对其进行身份验证(服务器可以是 HTTPS 网站,因此 Alice 可以使用 Web PKI)。让我们看看如何逐步构建这个来逐渐理解 OPAQUE 的工作原理。
第一个想法: 使用公钥密码学来验证 Alice 的连接一侧。如果 Alice 拥有长期密钥对并且服务器知道公钥,她可以简单地使用她的私钥与服务器进行相互验证的密钥交换,或者她可以签署服务器给出的挑战。不幸的是,非对称私钥太长了,Alice 只能记住她的密码。她可以在当前设备上存储一对密钥,但她也想以后能够从另一台设备登录。
第二个想法: Alice 可以使用类似 Argon2 这样的基于密码的密钥派生函数(KDF)从她的密码派生非对称私钥,你在第二章和第八章学到了。然后,Alice 的公钥可以存储在服务器上。如果我们想要在数据库泄露的情况下避免有人对整个数据库进行密码测试,我们可以让服务器为每个用户提供一个不同的盐,他们必须将其与基于密码的 KDF 一起使用。
这已经相当不错了,但 OPAQUE 想要抛弃一种攻击:预计算攻击。我可以尝试以你的身份登录,接收到你的盐,然后离线预计算大量非对称私钥及其关联的公钥。在数据库被破坏的那一天,我可以迅速查看是否可以在我的大量预计算的非对称公钥列表中找到您的公钥和关联的密码。
第三个想法: 这就是 OPAQUE 的主要技巧所在!我们可以使用 OPRF 协议和 Alice 的密码来派生非对称私钥。如果服务器为每个用户使用不同的密钥,那么这就等同于有盐(攻击只能针对一个用户)。这样,想要基于我们密码的猜测预先计算非对称私钥的攻击者必须执行在线查询(防止离线暴力攻击)。在线查询很好,因为它们可以进行速率限制(例如,每小时不超过 10 次登录尝试),以防止这种类型的在线暴力攻击。
注意,这实际上并不是 OPAQUE 的工作方式:与其让用户派生非对称私钥,OPAQUE 让用户派生对称密钥。然后,对称密钥用于加密您的非对称密钥对的备份和一些附加数据(例如可以包括服务器的公钥)。我在图 11.5 中说明了算法。

图 11.5 使用 OPAQUE 注册到服务器时,Alice 生成一个长期密钥对并将其公钥发送到服务器,服务器将其存储并与 Alice 的身份关联起来。然后,她使用 OPRF 协议从她的密码获取一个强对称密钥,并将密钥对的加密备份发送给服务器。要登录,她从服务器获取她的加密密钥对,然后使用她的密码执行 OPRF 协议以获取能够解密她的密钥对的对称密钥。现在只需使用这个密钥执行一个互认证的密钥交换(或者可能签署一个挑战)。
在进入下一节之前,让我们回顾一下你在这里学到的内容。图 11.6 对此进行了说明。

图 11.6 密码是验证用户身份的方便方式,因为它们存在于某人的头脑中,并且可以在任何设备上使用。另一方面,用户很难创建强密码,因为用户往往在网站之间重复使用密码,密码泄漏可能会造成严重损失。SSO 允许您使用一个(或几个)服务连接到多个服务,而不对称(或增强)密码验证密钥交换允许您在服务器不了解真实密码的情况下进行身份验证。
11.2.3 一次性密码并不真的是密码:使用对称密钥实现无密码登录
好了,到目前为止一切都很好。你已经了解了应用程序可以利用的不同协议来使用密码对用户进行身份验证。但是,你可能已经听说了,密码也不是那么好。它们容易受到暴力破解攻击,往往被重复使用,被窃取等等。如果我们可以避免使用密码,我们可以使用什么?
答案就是——密钥!正如你所知,在密码学中有两种类型的密钥,而且两种类型都可能很有用:
-
对称密钥
-
不对称密钥
本节介绍基于对称密钥的解决方案,而下一节介绍基于不对称密钥的解决方案。让我们想象一下,Alice 使用对称密钥(通常由服务器生成并通过 QR 码传输给你)注册了一个服务。后来验证 Alice 的一个天真的方法是简单地要求她发送对称密钥。当然,这并不好,因为她的秘密泄漏将给攻击者无限制的访问权限。相反,Alice 可以从对称密钥中派生出所谓的一次性密码(OTPs),并在长期对称密钥的位置发送这些 OTP。尽管 OTP 不是密码,但其名称表明 OTP 可以代替密码使用,并警告不应重复使用。
基于 OTP 的用户身份验证背后的想法很简单:你的安全性来自于一个(通常是)16 到 32 字节的均匀随机对称密钥的知识,而不是一个低熵密码。这个对称密钥允许你按需生成 OTP,如图 11.7 所示。

图 11.7 一次性密码(OTP)算法允许您从对称密钥和一些附加数据创建任意数量的一次性密码。附加数据不同,取决于 OTP 算法。
OTP 身份验证通常在移动应用程序中实现(请参见图 11.8 中的一个热门示例)或安全密钥中(这是一个可以插入计算机 USB 端口的小设备)。可以使用两种主要方案来生成 OTP:
-
基于 HMAC 的一次性密码(HOTP)算法,标准化在 RFC 4226 中,这是一种额外数据为计数器的 OTP 算法。
-
基于时间的一次性密码(TOTP)算法,标准化在 RFC 6238 中,这是一种额外数据为时间的 OTP 算法。
大多数应用程序现在使用 TOTP,因为 HOTP 需要客户端和服务器都存储状态(计数器)。如果一方失去同步,无法再生成(或验证)合法的 OTP,存储状态可能会导致问题。

图 11.8 Google Authenticator 移动应用程序的屏幕截图。该应用程序允许您存储唯一的应用程序专用对称密钥,然后可与 TOTP 方案一起使用生成 6 位数的一次性密码(OTP),仅有效 30 秒。
在大多数情况下,这就是 TOTP 的工作方式:
-
在注册时,服务向用户通信一个对称密钥(也许使用 QR 码)。 然后,用户将此密钥添加到 TOTP 应用程序中。
-
在登录时,用户可以使用 TOTP 应用程序计算一次性密码。 这是通过计算HMAC(symmetric_key,time)来完成的,其中time表示当前时间(四舍五入到分钟,以使一次性密码在 60 秒内有效)。然后
a) TOTP 应用程序向用户显示派生的一次性密码,截断并以人类可读的基数显示(例如,缩减为 10 进制的 6 个字符,使其全部为数字)。
b) 用户将一次性密码复制或键入到相关应用程序中。
c) 应用程序检索用户关联的对称密钥,并以与用户相同的方式计算一次性密码。如果结果与接收到的一次性密码匹配,则用户成功验证身份。
当然,用户的一次性密码(OTP)与服务器计算的密码必须在恒定时间内进行比较。这类似于 MAC 身份验证标签检查。我在图 11.9 中展示了这个流程。

图 11.9 Alice 使用 TOTP 作为认证在 example.com 上注册。她将网站的对称密钥导入到她的 TOTP 应用程序中。稍后,她可以要求应用程序为 example.com 计算一次性密码,并将其用于与网站进行身份验证。网站 example.com 获取与 Alice 关联的对称密钥,并使用 HMAC 和当前时间计算一次性密码。网站接下来以常量时间比较 Alice 发送的一次性密码。
这种基于 TOTP 的身份验证流程并不理想。有许多可以改进的地方,例如:
-
由于服务器也拥有对称密钥,认证可以被伪造。
-
您可以被社会工程学方式获得您的一次性密码。
因此,对称密钥是另一种不完美的密码替代方案。接下来,让我们看看如何使用非对称密钥来解决这些缺点。
钓鱼
钓鱼(或社会工程学)是一种不针对软件漏洞而是针对人的漏洞的攻击。想象一下,一个应用程序要求您输入一次性密码进行身份验证。在这种情况下,攻击者可能会尝试以您的身份登录应用程序,并在提示输入一次性密码时,给您打电话要求您提供有效的密码(假装他们为该应用程序工作)。
你在告诉我你不会上当吗!优秀的社会工程师擅长编织可信度很高的故事,并制造出一种紧迫感,使我们中的大多数人都会不假思索地泄露秘密。如果你仔细想想,我们之前谈论过的所有协议都容易受到这些类型的攻击的影响。
11.2.4 用非对称密钥替换密码
现在我们正在处理公钥密码学,有多种方法可以使用非对称密钥对服务器进行身份验证。我们可以
-
在密钥交换中使用我们的非对称密钥来验证连接的我们这一侧
-
在已经获得验证的连接中使用我们的非对称密钥与认证的服务器
让我们看看每种方法。
密钥交换中的双向认证
你已经听说过第一种方法了:使用密钥交换中的非对称密钥。在第九章中,我提到 TLS 服务器可以要求客户端在握手中使用证书。通常,公司会向其员工的设备配备唯一的员工证书,允许他们对内部服务进行身份验证。图 11.10 从用户的角度提供了一个大致的外观。

图 11.10 一页提示用户的浏览器获取客户端证书。用户然后可以从本地已安装的证书列表中选择要使用的证书。在 TLS 握手中,客户端证书的密钥用于签署握手记录,包括客户端的临时公钥,该公钥用作握手的一部分。
客户端证书相当简单。例如,在 TLS 1.3 中,服务器可以在握手期间通过发送 CertificateRequest 消息来请求客户端进行身份验证。然后,客户端通过在 Certificate 消息中发送其证书,然后在 CertificateVerify 消息中对发送和接收的所有消息进行签名(其中包括用于密钥交换的临时公钥)。如果服务器能够识别证书并成功验证客户端的签名,则客户端经过身份验证。另一个例子是安全外壳(SSH)协议,该协议也要求客户端使用服务器已知的公钥对握手的部分进行签名。
请注意,在握手阶段使用公钥加密进行身份验证的方法不仅限于签名。Noise 协议框架(在第九章中也有介绍)有几种握手模式,可以仅使用 DH 密钥交换进行客户端身份验证。
使用 FIDO2 进行握手后的用户身份验证
使用非对称密钥的第二种身份验证类型使用已经 安全 的连接,仅服务器被验证。为此,服务器可以简单地要求客户端对一个 随机 挑战进行签名。这样,重放攻击就被防止了。
在这个领域有一个有趣的标准是快速身份在线 2(FIDO2)。FIDO2 是一个开放标准,定义了如何使用非对称密钥对用户进行身份验证。该标准专门针对钓鱼攻击,并且为此,FIDO2 只能与硬件认证器一起使用。硬件认证器只是可以生成和存储签名密钥,并能签署任意挑战的物理组件。FIDO2 分为两个规范(图 11.11):
-
客户端到认证器协议(CTAP)—CTAP 指定了漫游认证器和客户端可以使用的通信协议。漫游认证器是外部于您的主设备的硬件认证器。在 CTAP 规范中,客户端被定义为要查询这些认证器的软件的一部分,作为身份验证协议的一部分。因此,客户端可以是操作系统、本地应用程序(如浏览器)等。
-
Web 身份验证(WebAuthn)—WebAuthn 是 Web 浏览器和 Web 应用程序可以使用的协议,用于使用硬件认证器对用户进行身份验证。因此,必须由浏览器来实现它以支持认证器。如果您正在构建一个 Web 应用程序,并希望支持通过硬件认证器进行用户身份验证,则需要使用 WebAuthn。

图 11.11 FIDO2 可用的两种硬件认证器类型:(左侧)Yubikey,一种漫游认证器,以及(右侧)TouchID,一种内置认证器。
WebAuthn 允许网站不仅使用漫游验证器,还可以使用平台验证器。平台验证器是设备提供的内置验证器。它们在各种平台上实现不同,并且通常受生物识别技术保护(例如,指纹识别器、面部识别等)。
我们现在结束了本章的第一部分。但在我这样做之前,图 11.12 总结了我谈论过的非基于密码的认证协议。

图 11.12 要进行无密码认证,应用程序可以允许用户通过基于 OTP 的协议使用对称密钥,或者通过 FIDO2 标准使用非对称密钥。FIDO2 支持不同类型的验证器,例如漫游验证器(通过 CTAP 标准)或内置验证器。
现在,您已经了解了许多不同的技术和协议,这些技术和协议旨在改善密码或将其替换为更强大的加密解决方案,您可能会想知道,应该使用哪一种?每种解决方案都有其自己的局限性,可能没有一种解决方案能够胜任。如果没有,那就结合多种解决方案吧!这个想法被称为多因素认证(MFA)。实际上,很有可能您已经在密码之外使用了 OTP 或 FIDO2 作为第二个身份验证因素。
这结束了本章关于用户身份验证的前半部分。接下来,让我们看看人类如何帮助设备相互认证。
11.3 用户辅助认证:使用人类帮助配对设备
人类每天都在帮助机器相互认证 —— 每一天!您通过将无线耳机与手机配对,或者将手机与汽车配对,或者将某个设备连接到家庭 WiFi,等等来完成了这一点。而且与任何配对一样,底层很可能是密钥交换。
上一节中的身份验证协议是在已经安全的通道中进行的(可能是通过 TLS),服务器进行了身份验证。与之相反,本节大部分内容试图为两个不知道如何相互认证的设备提供一个安全通道。在这个意义上,您将在本节中学到的内容是人类如何帮助将一个不安全的连接升级为一个相互认证的连接。因此,接下来您将学到的技术应该让您想起第十章端到端协议中的一些建立信任的技术,只是在那里,两个人试图相互认证。
如今,你将遇到的最常见的不安全连接(不通过互联网),都是基于短程无线电频率的协议,如蓝牙、WiFi 和近场通信(NFC)。 NFC 是你用来用手机或银行卡的“非接触”支付。使用这些通信协议的设备通常从低功耗电子设备到功能齐全的计算机都有。这已经给我们设置了一些限制:
-
您正在尝试连接的设备可能没有屏幕来显示密钥或手动输入密钥的方法。我们称之为配置该设备。例如,今天大多数无线音频耳机只有几个按钮而已。
-
由于人类是验证过程的一部分,必须键入或比较长字符串通常被认为是不切实际和不用户友好的。因此,许多协议试图将安全相关字符串缩短为 4 位或 6 位数字密码。
练习
想象一种协议,你必须输入正确的 4 位数字密码才能安全连接到设备。只通过猜测选择正确密码的机会有多大?
你可能会回想起你的一些设备配对经历,并且意识到现在很多情况都是自动完成的。例如
-
你按下了设备上的一个按钮。
-
设备进入配对模式。
-
你接着试图在手机的蓝牙列表中找到设备。
-
在点击设备图标后,它成功地将设备与您的手机配对。
如果你读过第十章,这应该让你想起了第一次使用时信任(TOFU)。不过,这次我们手头还有一些其他的牌:
-
接近性——两个设备必须彼此靠近,特别是如果使用 NFC 协议。
-
时间——设备配对通常受时间限制。如果,例如,在 30 秒的时间窗口内,配对不成功,必须手动重新启动该过程是很常见的。
与 TOFU 不同,这些真实场景通常不允许你事后验证你是否已连接到正确的设备。这并不理想,如果可能的话,应该努力提升安全性。
注意顺便提一下,这就是蓝牙核心规范实际上将类似 TOFU 的协议称为的内容:“Just Works”。我应该提到,目前所有内置的蓝牙安全协议都因许多攻击而破坏,包括 2019 年发布的最新 KNOB 攻击(knobattack.com)。尽管如此,本章介绍的技术如果设计和实施正确,仍然是安全的。
我们工具箱中的下一步是什么?这就是我们将在本节中看到的内容:人类帮助设备进行身份验证的方法。剧透:
-
你会发现,加密密钥始终是最安全的方法,但不一定是最用户友好的。
-
您将了解关于对称 PAKE 和如何在两个设备上输入相同密码以安全连接它们的内容。
-
你将了解基于短认证字符串(SAS)的协议,这些协议通过让你比较和匹配两个设备显示的两个短字符串来验证密钥交换的有效性。
让我们开始吧!
11.3.1 预共享密钥
幼稚地,将用户连接到设备的第一种方法将是重用你在第九章或第十章学到的协议(例如,TLS 或 Noise),并向两个设备提供对称共享密钥或更好地,提供长期公钥以为将来的会话提供前向保密性。这意味着每个设备学习另一个设备的公钥需要两件事:
-
你需要一种方法来导出设备的公钥。
-
你需要一种方法来导入公钥。
正如我们将看到的,这并不总是简单或用户友好的。但请记住,我们有一个参与的人可以观察和(也许)操纵这两个设备。这与我们以前见过的其他场景不同,我们可以利用这一点!
认证问题 - 密码学中的一个主要问题是在不安全的通道上建立安全的点对点(或群组)通信。在没有额外安全通道的假设下,这个任务是不可能的。但是,假设有一些前提条件,存在许多建立安全通信的方法。
—Sylvain Pasini(《使用认证通道进行安全通信》,2009 年)
所有接下来的协议都基于这样一个事实:你(负责人类)拥有一个额外的带外通道。这使您可以安全地通信一些信息。添加此带外通道可以被建模为两个设备可以访问两种类型的通道(如图 11.13 所示):
-
一个不安全的通道——想象一下与设备连接的蓝牙或 WiFi 连接。默认情况下,用户无法对设备进行身份验证,因此可能会受到中间人攻击(MITM)。
-
一个经过认证的通道——想象一下设备上的屏幕。该通道提供了所传输信息的完整性/真实性,但机密性较差(有人可能在你旁边偷看)。

图 11.13 用户辅助身份验证协议允许用户配对两个设备,这些协议模拟了设备之间的两种类型的通道:一个不安全的通道(例如,NFC、蓝牙、WiFi 等),我们假设该通道由对手控制,以及一个经过认证的通道(例如,现实生活中的通道),该通道不提供机密性,但可以用于交换相对较小的信息量。
由于这种带外通道提供的保密性较差,我们通常不希望使用它来导出机密信息,而是用于公共数据。例如,设备的屏幕可以显示公钥或某些摘要。但是一旦您导出了一个公钥,您仍然需要另一个设备来导入它。例如,如果密钥是一个二维码,那么另一个设备可能能够扫描它,或者如果密钥以人类可读的格式编码,那么用户可以使用键盘在另一个设备上手动输入它。一旦两个设备都配置了彼此的公钥,您可以使用我在第九章中提到的任何协议来执行两个设备之间的相互认证密钥交换。
我希望您从本节中了解到,在您的协议中使用加密密钥始终是实现某些目标的最安全方式,但并不总是最用户友好的方式。然而,现实世界的密码学充满了妥协和权衡,这就是为什么下面的两种方案不仅存在,而且是认证设备最流行的方式。
让我们看看在无法导出和导入长公钥的情况下如何使用密码启动双向认证密钥交换。然后我们将看看短认证字符串如何在无法将数据导入到一个或两个设备的情况下提供帮助。
11.3.2 使用 CPace 进行对称密码认证密钥交换
如果可能的话,您应该采用上述解决方案,因为它依赖于强大的非对称密钥作为信任的根源。然而,实践中发现,手动使用一串长字符串表示的密钥在一些笨重的键盘上输入是很繁琐的。那么这些亲爱的密码呢?它们要短得多,因此更容易处理。我们喜欢密码对吧?也许我们不喜欢,但用户喜欢,而现实世界的密码学充满了妥协。所以就这样吧。
在关于对称密码认证密钥交换的部分中,我提到了存在一个对称(或平衡)版本,其中知道共同密码的两个对等方可以执行相互认证密钥交换。这正是我们需要的。
可组合密码认证连接建立(CPace)于 2008 年由 Björn Haase 和 Benoît Labrique 提出,并于 2020 年初被 CFRG(密码论坛研究小组)选为官方推荐。该算法目前正在作为 RFC 标准化。简化的协议看起来类似于以下内容(图 11.14 说明了该算法):
-
两个设备基于一个共同的密码派生一个生成器(用于某个预定循环群)。
-
然后两个设备使用这个生成器在其上执行临时 DH 密钥交换。

图 11.14 CPace PAKE 的工作原理是让两个设备基于一个密码创建一个生成器,然后将其用作通常的临时 DH 密钥交换的基础。
当然,魔鬼在细节中,作为一个现代规范,CPace 针对椭圆曲线的“陷阱”,并定义了何时必须验证接收到的点是否在正确的群中(由于时髦的 Curve25519,不幸的是,它不构成一个素数群)。它还指定了如何基于密码在椭圆曲线群中派生生成器(使用所谓的哈希到曲线算法)以及如何做到这一点(不仅使用普通密码,还使用唯一的会话 ID 和一些附加的上下文元数据,比如对等方 IP 地址等等)。
这些步骤很重要,因为双方都必须以防止它们知道其离散对数 x 的方式派生生成器 h,使得 g^x = h。最后,会话密钥是从 DH 密钥交换输出、记录(临时公钥)和唯一的会话 ID 派生的。
直觉上,你可以看到冒充其中一方并在握手的一部分发送一个群元素意味着你发送了一个公钥,这个公钥与你无法知道的私钥相关联。这意味着如果你不知道密码,你永远无法执行 DH 密钥交换。记录看起来就像一个正常的 DH 密钥交换,所以,没有运气(只要 DH 是安全的)。
11.3.3 我的密钥交换被 MITM 攻击了吗?只需检查一个短认证字符串(SAS)。
在本章的第二部分中,你看到了不同的协议,它们允许两个设备在人类的帮助下配对。然而,我提到有些设备受限制以至于无法使用这些协议。让我们来看看一种方案,当两个设备无法导入密钥但可以向用户显示一些有限的数据时使用(也许通过屏幕、或者通过打开一些 LED、或者通过发出一些声音等等)。
首先,记住在第十章中,你学到了如何在 握手后(密钥交换后)使用 指纹(传输的哈希)对会话进行认证。我们可以像这样使用一些东西,因为我们有我们的带外信道来传递这些指纹。如果用户能够成功比较和匹配从两台设备获取的指纹,那么用户就知道密钥交换没有被 MITM 攻击。
指纹的问题在于它们是长字节串(通常为 32 个字节长),可能难以显示给用户。它们也很笨重,难以比较。但对于设备配对,我们可以使用更短的字节串,因为我们在实时进行比较!我们称这些为 短认证字符串(SAS)。SAS 被广泛使用,特别是由于它们相当用户友好(请参见图 11.15 中的示例)。

图 11.15 要通过蓝牙将手机与汽车配对,可以使用数字比对模式生成一个短的经过身份验证的字符串(SAS),该字符串是两个设备之间协商的安全连接的一部分。不幸的是,正如我在本章早些时候所述,由于 KNOB 攻击,蓝牙的安全协议目前已经破解(截至 2021 年)。如果你控制着这两个设备,你需要实现自己的 SAS 协议。
SAS-based schemes 没有任何标准,但大多数协议(包括蓝牙的数字比对)实现了一种变种的手动认证迪菲-赫尔曼(MA-DH)。MA-DH 是一种简单的密钥交换协议,其附加的技巧使得攻击者很难在中间人攻击中主动干预协议。你可能会问,为什么不只是从截断的指纹中创建 SAS?为什么需要一种技巧?
SAS 通常是一个 6 位数,可以通过将传输的哈希值截断为少于 20 位并将其转换为十进制数字来获得。因此,SAS 实际上非常小,这使得攻击者更容易在截断的哈希上获取第二个前像。在图 11.16 中,我们以两个设备为例(尽管我们使用了 Alice 和 Bob),执行一个未经身份验证的密钥交换。一个主动的 MITM 攻击者可以在第一个消息中用他们自己的公钥替换 Alice 的公钥。一旦攻击者收到 Bob 的公钥,他们就会知道 Bob 将计算什么样的 SAS(基于攻击者的公钥和 Bob 的公钥的截断哈希)。攻击者只需生成许多公钥,以找到一个(public_key[E]2),使得 Alice 的 SAS 与 Bob 的匹配。

图 11.16 典型的未经身份验证的密钥交换(左侧)可以被主动的 MITM 攻击者(右侧)拦截,后者可以替换 Alice 和 Bob 的公钥。如果 Alice 和 Bob 都生成相同的短的经过身份验证的字符串,则 MITM 攻击成功。也就是说,如果 HASH(public_key[A] || public_key[E2]) 和 HASH(public_key[E2] || public_key[B]) 匹配。
生成一个公钥以使两个 SAS 匹配实际上相当容易。想象一下 SAS 是 20 位,那么只需要 2²⁰ 次计算,你就应该能够找到一个第二个前像,使得 Alice 和 Bob 都生成相同的 SAS。即使在一部廉价手机上,这也应该是相当快速的计算。
SAS-based key exchanges 的技巧在于防止攻击者能够选择他们的第二个公钥,从而强制两个 SAS 匹配。为了做到这一点,Alice 在看到 Bob 的公钥之前简单地发送了她的公钥的一个承诺(如图 11.17 所示)。

图 11.17 左侧的图示了一个安全的 SAS-based 协议,其中 Alice 首先发送她的公钥的承诺。在收到 Bob 的公钥后,她只在之后才揭示自己的公钥。因为她已经对其进行了承诺,所以她不能根据 Bob 的密钥自由选择她的密钥对。如果交换被主动进行了 MITM 攻击(右侧的图示),攻击者将无法选择任何密钥对以强制 Alice 和 Bob 的 SAS 匹配。
与以前的不安全方案一样,攻击者选择的 public_key[E1] 不会给他们任何优势。但现在,他们也不能选择一个 public_key[E2] 来帮助,因为在协议的这一点上他们不知道 Bob 的 SAS。他们被迫“盲目射击”,希望 Alice 和 Bob 的 SAS 会匹配。
如果 SAS 是 20 位,那么概率是 1,048,576 中的 1。攻击者可以通过多次运行协议来增加机会,但请记住,协议的每个实例都必须由用户手动匹配 SAS。实际上,这种摩擦自然地防止了攻击者获得过多的彩票。

图 11.18 你学到了关于配对两台设备的三种技术:(1)用户可以帮助设备获取彼此的公钥,以便它们可以执行密钥交换;(2)用户可以在两台设备上输入相同的密码,以便它们可以执行对称密码认证密钥交换;或者(3)用户可以事后验证密钥交换的指纹,以确认没有 MITM 攻击者拦截了配对。
故事时间
有趣的是,当我写第十章关于端对端加密时,我开始研究 Matrix 端对端加密聊天协议的用户是如何验证他们的通信的。为了使验证更加用户友好,Matrix 创建了自己的 SAS-based 协议变种。不幸的是,它对 X25519 密钥交换的共享密钥进行了哈希处理,但在哈希中没有包含要交换的公钥。
在第五章中,我提到验证 X25519 公钥是很重要的。Matrix 没有这样做,这使得 MITM 攻击者能够向用户发送不正确的公钥,迫使他们最终得到相同的可预测的共享密钥,进而得到相同的 SAS。这完全破坏了协议的端对端加密声明,并且很快由 Matrix 进行了修复。
这就是全部内容!图 11.18 回顾了本章第二部分中学到的不同技术。下次见在第十二章。
摘要
-
用户身份验证协议(机器验证人类的协议)通常在安全连接上进行,只有机器(服务器)已经通过验证。在这个意义上,它将单向验证连接升级为双向验证连接。
-
用户认证协议大量使用密码。密码已被证明是一种相对实用的解决方案,并被用户广泛接受。但由于密码卫生不佳、熵值低和密码数据库泄露等问题,密码也导致了许多问题。
-
有两种方法可以避免用户携带多个密码(并可能重复使用密码):
-
密码管理器—用户可以使用的工具,用于为他们使用的每个应用程序生成和管理强密码。
-
单点登录(SSO)—一种联合协议,允许用户使用一个帐户注册并登录其他服务。
-
-
服务器避免了解和存储其用户密码的一个解决方案是使用非对称密码认证密钥交换(非对称 PAKE)。非对称 PAKE(如 OPAQUE)允许用户使用密码对已知服务器进行身份验证,但无需实际向服务器透露密码。
-
避免密码的解决方案包括用户通过一次性密码(OTP)算法使用对称密钥或通过 FIDO2 等标准使用非对称密钥。
-
用户辅助认证协议通常在不安全的连接(WiFi,蓝牙,NFC)上进行,并帮助两个设备相互认证。为了在这些情景下保护连接,用户辅助协议假设两个参与者拥有一个额外的经过身份验证的(但不保密的)通道可供使用(例如,设备上的屏幕)。
-
将设备的公钥导出到另一个设备可以允许进行强相互认证的密钥交换。不幸的是,这些流程不够用户友好,有时由于设备限制(例如无法导出或导入密钥)而不可能。
-
对称密码认证密钥交换(对称 PAKEs)如 CPace 可以通过只需用户手动输入密码而无需导入长公钥来减轻用户的负担。例如,大多数人已经使用对称 PAKEs 来连接到他们的家庭 WiFi。
-
基于短身份验证字符串(SAS)的协议可以为无法导入密钥或密码但能够在密钥交换后显示短字符串的设备提供安全性。为了确保未经认证的密钥交换未被主动中间人攻击,这个短字符串必须在两个设备上相同。
第十二章:加密货币是指加密货币吗?
本章包括
-
共识协议及其如何使加密货币可能
-
不同类型的加密货币
-
比特币和 Diem 加密货币如何在实践中运作
密码学能否成为新金融系统的基础?这是自 2008 年以来加密货币一直在试图回答的问题,当时比特币是由中本聪提出的(至今仍未透露他或他们的身份)。在那之前,术语加密始终是用于指涉密码学领域。但自从比特币的创建以来,我看到它的含义迅速改变,现在也用于指代加密货币。加密货币爱好者反过来越来越有兴趣学习密码学。这是有道理的,因为密码学是加密货币的核心。
什么是加密货币?它有两个方面:
-
它是一种数字货币。简单来说,它允许人们以电子方式交易货币。有时会使用由政府支持的货币(如美元),有时会使用虚拟货币(如比特币)。你很可能已经在使用数字货币——每当你在互联网上向某人汇款或使用支票账户时,你都在使用数字货币!事实上,你不再需要通过邮件寄送现金,今天大多数货币交易只是数据库中行的更新。
-
它是一种严重依赖密码学来避免使用信任第三方和提供透明度的货币。在加密货币中,没有必须盲目信任的中央权威,如政府或银行。我们经常将这种属性称为去中心化(就像“我们正在去中心化信任”)。因此,正如你将在本章中看到的那样,加密货币被设计为容忍一定数量的恶意行为者,并允许人们验证它们是否正常运作。
加密货币相对较新,因为第一个成功的实验是比特币,它是在 2008 年提出的,当时正值全球金融危机中。虽然危机始于美国,但很快就传播到世界其他地区,侵蚀了人们对金融体系的信任,并为比特币等更透明的倡议提供了平台。那时,许多人开始意识到金融交易的现状是低效、昂贵且大多数人不透明的。其余的就是历史,我相信这本书是第一本包含有关加密货币章节的密码学书籍。
12.1 一种温和的拜占庭容错(BFT)共识算法简介
想象一下,你想要创建一种新的数字货币。构建一个运作良好的系统实际上并不复杂。你可以在专用服务器上设置一个数据库,用于跟踪用户及其余额。通过这样做,你可以为人们提供一个界面,让他们查询余额或允许他们发送支付请求,这将在数据库中减少他们的余额并增加另一行中的余额。最初,你也可以随机将一些虚拟货币分配给你的朋友,以便他们可以开始向你的系统转账。但是这样一个简单的系统有一些缺陷。
12.1.1 弹性问题:分布式协议来拯救
我们刚刚看到的系统是一个单点故障。如果停电,你的用户将无法使用系统。更糟糕的是,如果某种自然灾害意外摧毁了你的服务器,每个人可能会永久丢失他们的余额。为了解决这个问题,存在着一些众所周知的技术,你可以用来为你的系统提供更强大的弹性。分布式系统领域研究了这些技术。
在这种情况下,大多数大型应用程序使用的常见解决方案是将数据库内容(在某种程度上)实时地复制到其他备份服务器上。这些服务器可以分布在各个地理位置,随时准备作为备份使用,甚至在主服务器故障时接管。这被称为高可用性。现在你拥有了分布式数据库。
对于服务大量查询的大型系统,备份数据库通常不仅仅是闲置在一旁等待发挥作用,而是被用于提供状态读取。很难让超过一个数据库接受写入和更新,因为这样可能会引发冲突(就像两个人同时编辑同一份文件一样危险)。因此,你通常希望一个数据库充当领导者,并对所有写入和更新操作进行排序,而其他数据库则用于读取状态。
数据库内容的复制可能会很慢,预计你的一些数据库会落后于主数据库,直到它们追赶上去。这在使用复制数据库读取状态时尤其如此。(想象一下,你和你的朋友查询不同的服务器,因此看到了不同的账户余额。)
在这些情况下,应用程序通常被编写以容忍这种滞后。这被称为最终一致性,因为最终数据库的状态会变得一致。(存在更强的一致性模型,但它们通常速度较慢且不切实际。)这样的系统也存在其他问题:如果主数据库崩溃,哪个数据库将成为主数据库?另一个问题是,如果备份数据库在主数据库崩溃时落后,我们会丢失一些最新的更改吗?
这就是在需要整个系统就某个决定达成一致意见时,更强大的算法—共识算法(也称为日志复制、状态机复制或原子广播)—发挥作用的地方。想象一下,一个共识算法解决了一群人试图就要点什么披萨达成一致意见的问题。如果每个人都在同一个房间里,很容易看出大多数人想要什么。但如果每个人都通过网络进行通信,消息可能会延迟、丢失、被拦截和修改,那么就需要一个更复杂的协议。

让我们看看共识如何用来回答前两个问题。在崩溃的情况下哪个数据库可以接管的第一个问题被称为领导者选举,通常使用共识算法来确定哪个将成为下一个领导者。第二个问题通常通过将数据库更改视为两个不同步骤来解决:待定和已提交。对数据库状态的更改始终首先是待定的,只有足够多的数据库同意提交它才能被设置为已提交(这也是共识协议可以使用的地方)。一旦提交,对状态的更新不容易丢失,因为大多数参与的数据库已经提交了更改。
一些知名的共识算法包括 Paxos(由 Lamport 于 1989 年发表)及其后续简化版本 Raft(由 Ongaro 和 Ousterhout 于 2013 年发表)。您可以在大多数分布式数据库系统中使用这些算法来解决不同的问题。(要了解关于 Raft 的出色互动解释,请查看thesecretlivesofdata.com/raft。)
12.1.2 信任的问题?分权有助于解决
分布式系统(从操作角度)为那些充当单点故障的系统提供了一个弹性的替代方案。大多数分布式数据库系统使用的共识算法不够容错。一旦机器开始崩溃,或由于硬件故障而开始表现不良,或开始与某些其他机器断开连接,比如网络分区,问题就会出现。此外,从用户角度来看,没有办法检测到这一点,如果服务器被入侵,这就更成问题了。
如果我向服务器查询,它告诉我 Alice 的账户里有 50 亿美元,我只能相信它。如果服务器在响应中包含了自她账户开户以来所收到和发送的所有货币转账,并将它们全部加起来,我可以验证她账户中确实有 50 亿美元是正确的。但是谁能保证服务器没有对我撒谎呢?也许当 Bob 询问另一个服务器时,它返回的是完全不同的余额和/或 Alice 账户的历史记录。我们称之为分叉(以两种相互矛盾的状态呈现为有效),这是历史中不应该发生的一个分支。因此,你可以想象,其中一个复制的数据库的妥协可能会导致相当严重的后果。
在第九章中,我提到了证书透明性,这是一种旨在检测 Web 公钥基础设施(PKI)中这种分叉的八卦协议。金钱的问题在于仅仅检测是不够的。你希望首先防止分叉发生!1982 年,Paxos 共识算法的作者 Lamport 提出了拜占庭容错(BFT)共识算法的概念。
我们想象拜占庭军队的几个师分驻扎在一座敌方城市外面,每个师分都由自己的将军指挥。将军们只能通过信使相互通信。观察敌人之后,他们必须决定一个共同的行动计划。然而,一些将军可能是叛徒,试图阻止忠诚的将军达成一致意见。
——Lamport 等人(《拜占庭将军问题》,1982 年)
Lamport 通过他的拜占庭类比开启了 BFT 共识算法领域,旨在防止不良共识参与者在达成决策时对系统产生不同的冲突观点。这些 BFT 共识算法高度类似于之前的共识算法,如 Paxos 和 Raft,只是复制的数据库(协议参与者)不再盲目地相互信任了。BFT 协议通常大量使用密码学来验证消息和决策的真实性,而这反过来可以被其他人用来对共识协议输出的决策进行密码学验证。
这些 BFT 共识协议因此解决了我们的韧性和信任问题。不同的复制数据库可以运行这些 BFT 算法,以便在新系统状态(例如用户余额)上达成一致,同时通过验证状态转换(用户之间的交易)是否有效,并获得大多数参与者的同意来相互监督。我们说信任现在是分散的。
第一个真实世界的 BFT 共识算法是 1999 年发表的实用 BFT(PBFT)。PBFT 是一种基于领导者的算法,类似于 Paxos 和 Raft,其中一个领导者负责提出提案,而其他人试图就提案达成一致。不幸的是,PBFT 相当复杂,缓慢,并且在超过十几个参与者后无法很好地扩展。如今,大多数现代加密货币使用更高效的 PBFT 变体。例如,Facebook 于 2019 年推出的加密货币 Diem 基于 HotStuff,这是一种受 PBFT 启发的协议。
12.1.3 规模问题:无许可和抗审查网络
这些基于 PBFT 的共识算法的一个局限性是它们都需要一个已知且固定的参与者集合。更为棘手的是,超过一定数量的参与者后,它们开始分崩离析:通信复杂性急剧增加,变得极其缓慢,选举领导者变得复杂等等。
加密货币如何决定共识参与者?有几种方式,但最常见的两种方式是
-
权威证明(PoA)—共识参与者事先确定。
-
权益证明(PoS)—共识参与者是动态选择的,基于谁拥有的权益最大(因此,更不愿意攻击协议)。一般来说,基于 PoS 的加密货币根据持有的数字货币数量选举参与者。
话虽如此,并非所有的共识协议都是经典的 BFT 共识协议。例如,比特币在提出一种没有已知参与者名单的共识机制时采取了不同的方法。这在当时是一个相当新颖的想法,比特币通过放宽经典 BFT 共识协议的约束来实现这一点。正如你将在本章后面看到的,由于这种方法,比特币可以分叉,这带来了自己的一系列挑战。
没有参与者,你如何选择领导者?您可以使用 PoS 系统(例如,Ouroboros 共识协议就是这样做的)。相反,比特币的共识依赖于一种称为工作量证明(PoW)的概率机制。在比特币中,这意味着人们试图找到解决方案来成为参与者和领导者。正如你将在本章后面看到的,这个谜题是一个密码学谜题。
由于缺乏已知参与者,比特币被称为无许可网络。在无许可网络中,您无需额外权限即可参与共识;任何人都可以参与。这与有许可网络形成对比,后者有一个固定的参与者集合。我在图 12.1 中总结了一些这些新概念。

图 12.1 一个集中式网络可以被视为单点故障,而分布式和去中心化网络对一些服务器关闭或恶意行为具有弹性。在许可网络中,有一组已知和固定的主要参与者,而在无许可网络中,任何人都可以参与。
直到最近,人们还不知道如何将经典的 BFT 共识协议与允许任何人加入的无许可网络一起使用。如今,存在许多使用 PoS 动态选择参与者子集作为共识参与者的方法。其中最值得注意的是 2017 年发布的 Algorand,它根据持有的货币数量动态选择参与者和领导者。
比特币还声称对审查具有抵抗力,因为你无法预先知道谁将成为下一个领导者,因此无法阻止系统选举新领导者。在 PoS 系统中是否可能实现这一点尚不太清楚,因为在这种系统中更容易确定大量货币背后的身份。
我应该提到,并非所有的 BFT 共识协议都是基于领导者的。有些是无领导者的,它们不是通过选举领导者决定下一个状态转换的。相反,每个人都可以提出变更,共识协议帮助每个人就下一个状态达成一致。2019 年,Avalanche 推出了这样一种加密货币,允许任何人提出变更并参与共识。
最后,如果你认为共识对于去中心化支付系统是必要的,那也不完全正确。2018 年,Guerraoui、Kuznetsov、Monti、Pavlovic 和 Seredinschi 提出了“AT2: 异步可信转账”中的无共识协议。考虑到这一点,我在本章中不会讨论无共识协议,因为它们是相对较新的,尚未经过实战测试。在本章的其余部分,我将介绍两种不同的加密货币,以展示该领域的不同方面:
-
比特币—基于 PoW 的最流行的加密货币,于 2008 年推出。
-
Diem—一种基于 BFT 共识协议的加密货币,由 Facebook 和一群其他公司在 2019 年宣布。
12.2 比特币是如何运作的?
2008 年 10 月 31 日,一位匿名研究人员以化名 Satoshi Nakamoto 发布了“比特币:一个点对点的电子现金系统”。直至今日,仍然不知道 Satoshi Nakamoto 是谁。不久之后,“他们”发布了比特币核心客户端,这是任何人都可以运行以加入和参与比特币网络的软件。比特币所需要的唯一一件事情就是足够多的用户运行相同的软件或至少相同的算法。第一个加密货币诞生了——比特币(或 BTC)。
比特币是一个真正的成功故事。这种加密货币已经运行了十多年(截至撰写本文时),并且已经允许来自世界各地的用户使用数字货币进行交易。2010 年,开发者拉斯洛·汉野奇(Laszlo Hanyecz)用 10,000 BTC 买了两块披萨。当我写下这些文字时(2021 年 2 月),BTC 几乎价值 57,000 美元。因此,我们已经可以得出结论,加密货币有时可能极度波动。
12.2.1 比特币如何处理用户余额和交易
让我们深入了解比特币的内部结构,首先看看比特币如何处理用户余额和交易。作为比特币的用户,您直接处理密码学。您不像在任何银行网站上一样有用户名和密码登录;相反,您有自己生成的椭圆曲线数字签名算法(ECDSA)密钥对。用户的余额只是与公钥关联的一定数量的 BTC,因此,要接收 BTC,您只需与他人共享您的公钥。
要使用您的 BTC,您需要使用您的私钥签署交易。交易基本上说明了您认为的内容,“我将 X BTC 发送到公钥 Y”,忽略了一些我稍后会解释的细节。
注意:在第七章中,我提到比特币使用带有 ECDSA 的 secp256k1 曲线。不要将此曲线与 NIST 的 P-256 曲线混淆,后者被称为 secp256r1。
您的资金安全直接与您的私钥安全性相关。而且,正如您所知,密钥管理很困难。在过去的十年中,加密货币中的密钥管理问题导致了价值数百万美元的密钥的意外丢失(或盗窃)。小心!
比特币存在不同类型的交易,实际上,在网络上看到的大多数交易都通过对其进行哈希来隐藏接收方的公钥。在这些情况下,公钥的哈希被称为帐户的地址。(例如,这是我的比特币地址:bc1q8y6p4x3rp32dz80etpyffh6764ray9842egchy。)地址有效地隐藏了帐户的实际公钥,直到帐户所有者决定花费 BTC(在这种情况下,需要揭示地址的预图,以便其他人可以验证签名)。这使地址的大小更短,并防止有人在某天破解 ECDSA 后检索您的私钥。
不同类型的交易存在是比特币的一个有趣细节。交易不仅仅是包含一些信息的有效载荷;它们实际上是用虚构和相当有限的指令集编写的简短脚本。当交易被处理时,必须执行脚本,然后生成的输出才能确定交易是否有效,以及如果有效,则需要采取哪些步骤来修改所有帐户的状态。
像以太坊这样的加密货币通过允许在执行交易时运行更复杂的程序(所谓的智能合约)将这个脚本思想推向了极限。这里有几件事情我没有触及到:
-
一个交易中包含什么?
-
交易执行意味着什么?谁来执行它?
我将在下一节解释第二项内容。现在,让我们看看一个交易中有什么。
比特币的一个特点是没有真正的账户余额数据库。相反,用户拥有的是可供支出的比特币“零钱”,称为未花费交易输出(UTXOs)。你可以将 UTXOs 的概念想象成一个大碗,对所有人可见,里面装满了只有它们的所有者才能花费的硬币。当一笔交易花费了一些硬币时,这些硬币就会从碗里消失,同时为同一交易的收款方产生新的硬币。这些新硬币就是交易中列出的输出。
要知道你账户里有多少比特币,你需要数一下分配给你地址的所有 UTXOs。换句话说,你需要数一下所有发给你但你尚未花费的钱。图 12.2 举例说明了 UTXOs 在交易中的使用方式。

图 12.2 交易 1 由 Alice 签名,将 1 BTC 转给 Bob。由于它使用了 5 BTC 的 UTXO,该交易还需要将找零发送回 Alice,并保留一些找零作为费用。交易 2 由 Bob 签名,合并了两个 UTXO 以将 2 BTC 转给 Felix。(请注意,实际中,费用要低得多。)
现在有一个先有鸡还是先有蛋的问题:第一个 UTXO 从哪里来?这个问题我将在下一节中回答。
12.2.2 在数字黄金时代挖掘比特币
你现在了解了比特币交易中的内容以及如何管理你的账户或查明某人的余额。但是实际上是谁跟踪所有这些交易的呢?答案是每个人!
实际上,使用比特币意味着每笔交易都必须公开共享并记录在历史中。比特币是一个只追加的分类帐——一本交易记录的书,每页都与上一页相连。我在这里要强调的是,只追加意味着你不能回去修改书中的某一页。还要注意的是,因为每笔交易都是公开的,你唯一能得到的匿名性只是可能很难弄清谁是谁(换句话说,实际上什么公钥与什么人联系在一起)。
任何人都可以通过下载比特币客户端并使用它下载整个历史来轻松检查自比特币创立以来发生的任何交易。通过这样做,你成为了网络的一部分,并且必须根据比特币客户端中编码的规则重新执行每个交易。当然,比特币的历史非常庞大:在撰写本文时,它大约是 300 GB,根据你的连接速度,可能需要几天的时间来下载整个比特币分类账。你可以通过使用一个为你做繁重工作的在线服务更轻松地检查交易(只要你信任在线服务)。我在图 12.3 中给出了这些所谓的区块链浏览器的一个例子。

图 12.3 我选择在blockchain.com上分析的一笔随机交易(mng.bz/n295)。该交易使用一个输入(约 1.976 BTC)并将其分成两个输出(约 0.009 BTC 和 1.967 BTC)。总输入金额与总输出金额之间的差额是交易费(不作为输出表示)。其他字段是使用比特币脚本语言编写的脚本,以便花费输入中的 UTXO 或使输出中的 UTXO 可花费。
比特币实际上只是自其创世以来已处理的所有交易的列表(我们称之为起源)直到现在。这应该让你思考:谁负责选择和排序交易在这个分类账中?
为了就交易的排序达成一致,比特币允许任何人(甚至是你)提出要包含在下一个分类账页面中的交易列表。包含交易列表的这个提案在比特币的术语中被称为一个块。但是,让任何人提出一个块是灾难的预兆,因为比特币中有很多参与者。相反,我们希望只有一个人提出下一个交易块的提案。为了做到这一点,比特币让每个人都在一些概率谜题上工作,并且只允许第一个解决谜题的人提出他们的块。这就是我之前谈到的工作证明(PoW)机制。比特币的 PoW 是基于找到一个哈希值小于某个值的块。换句话说,块的哈希值必须具有以一些给定的零开始的二进制表示。
除了你想要包含的交易之外,块还必须包含上一个块的哈希值。因此,比特币分类账实际上是一系列块,其中每个块都指向前一个块,一直到第一个块,即创世块。这就是比特币所谓的区块链。区块链的美妙之处在于,对块的最轻微修改都会使链无效,因为块的哈希值也会改变,从而破坏下一个块对它的引用。
请注意,作为一个寻求提出下一个区块的参与者,你不需要对你的区块做太多更改来从中派生一个新的哈希。你可以首先固定它的大部分内容(包括其中的交易、它扩展的区块的哈希等),然后仅修改一个字段(称为区块的 nonce),以影响区块的哈希。你可以将这个字段视为一个计数器,递增其值直到找到符合游戏规则的摘要,或者你可以生成一个随机值。我在图 12.4 中阐述了区块链的这个概念。

图 12.4 在andersbrownworth.com/blockchain/blockchain上,人们可以与一个玩具区块链进行互动。每个区块都包含其父区块的摘要,每个区块都包含一个允许其摘要以四个 0 开头的随机 nonce。注意,对于顶部的区块链是如此,但是底部的区块链包含一个已经被修改的区块(编号为 2)(其数据最初为空)。由于修改改变了区块的摘要,所以它不再被后续区块认证。
所有这一切都是因为每个人都在运行相同的协议,使用相同的规则。当你与区块链同步时,你从其他节点下载每个区块,并验证:
-
对每个区块进行哈希确实会产生一个比某个预期值更小的摘要。
-
每个区块都指向历史中的前一个区块。
并非每个人都必须提出区块,但如果你愿意,你可以这样做。如果你这样做,你就被称为矿工。这意味着为了让你的交易进入区块链,你需要矿工的帮助(正如图 12.5 所示)。

图 12.5 比特币网络是许多节点(矿工或其他)相互连接的网络。要提交一个交易,你必须将其发送给一个能够将其放入区块链中的矿工(通过将其包含在一个区块中)。由于你不知道哪个矿工将成功地挖掘一个区块,你必须通过网络传播你的交易,以尽可能多地达到矿工。
矿工不是无偿工作的。如果一个矿工找到了一个区块,他们会收集:
-
奖励 —— 一定数量的比特币将被创建并发送到你的地址。一开始,每个挖掘的区块都会获得 50 个比特币。但是奖励值会在每挖掘 210,000 个区块时减半,并最终减少到 0,限制可以创建的比特币总量为 2100 万。
-
包含在区块中的所有交易费 ——这就是为什么在你的交易中增加费用可以让它们更快被接受,因为矿工倾向于在他们挖掘的区块中包含费用更高的交易。
这就是比特币用户被激励推动协议向前发展的方式。一个区块总是包含所谓的coinbase,即收集奖励和费用的地址。矿工通常将 coinbase 设置为他们自己的地址。
现在我们可以回答本节开头提出的问题:第一个 UTXO 是从哪里来的?答案是,历史上的所有比特币在某个时候都是作为矿工的区块奖励的一部分而创建的。
12.2.3 分叉地狱!解决挖矿中的冲突
比特币通过基于 PoW 的系统分配选择下一组要处理的交易的任务。你挖掘一个区块的机会与你能计算的哈希数量直接相关,因此,你可以产生的计算量。如今,很多计算能力都被用于在比特币或其他基于 PoW 的加密货币中挖矿。
注意:PoW 可以被视为比特币应对西比尔攻击的方式,这些攻击利用了你可以在协议中创建任意多个账户的事实,给不诚实的参与者带来了不对称的优势。在比特币中,获得更多算力的唯一途径实际上是购买更多硬件来计算哈希值,而不是在网络中创建更多地址。
然而,仍然存在一个问题:找到一个低于某个值的哈希的难度不能太低。如果太容易,那么网络中将有太多参与者同时挖掘一个有效的区块。如果发生这种情况,那么在链中哪个被挖掘的区块是合法的下一个区块呢?这本质上就是我们所说的分叉。
为了解决分叉问题,比特币有两种机制。第一种是保持 PoW 的难度。如果区块挖掘得太快或太慢,那么每个人都在运行的比特币算法会根据网络条件动态调整,并增加或减少 PoW 的难度。简单来说,矿工必须找到一个具有更多或更少零的区块摘要。
注意:如果难度要求区块摘要以 0 字节开头,你需要尝试 2⁸个不同的区块(更具体地说是不同的 nonce,如前面所述),直到找到有效的摘要。将这个数字提高到 2 字节,你现在需要尝试 2¹⁶个不同的区块。你达到这个目标所需的时间取决于你拥有的计算能力以及是否有专门的硬件来更快地计算这些哈希值。目前,比特币的算法会动态调整难度,以确保每 10 分钟挖出一个区块。
我们的第二个机制是确保每个人在发生分叉时都有相同的前进方式。为了做到这一点,规则是遵循工作量最大的链。2008 年的比特币白皮书指出,“最长的链不仅作为事件序列的证明,还证明它来自 CPU 算力最大的池”,规定参与者应该尊重他们认为是最长链的链。协议后来更新为遵循具有最高累积工作量的链,但在这里这个区别并不太重要。我在图 12.6 中进行了说明。

图 12.6 区块链中的分叉:两个矿工在高度 3 发布了一个有效区块(意味着创世区块之后的第 3 个区块)。后来,另一个矿工在高度 4 挖掘了一个指向高度 3 的第二个区块的区块。由于第二个分叉现在更长,这是矿工应该继续扩展的有效分叉。请注意,指向区块的箭头指向父区块(它们扩展的区块)。
我之前说过比特币的共识算法不是 BFT 协议。这是因为共识算法允许这样的分叉。因此,如果你正在等待你的交易被处理,绝对不应该仅仅依靠观察你的交易是否被包含在一个区块中!观察到的区块实际上可能是一个分叉,而且是一个失败的分叉(相对于更长的分叉)。
你需要更多的保证来决定你的交易是否已经真正被处理。大多数钱包和交易平台都等待一定数量的确认区块在你的区块之上被挖掘出来。在包含你的交易的那个区块之上挖掘出来的区块越多,链被重新组织成另一条链的机会就越小,因为已存在的分叉更长。
确认数通常设置为 6 个区块,这使得你的交易确认时间大约为一小时。话虽如此,比特币仍然不能提供 100% 的保证,即在 6 个区块之后不会发生分叉。如果挖矿难度调整得很好,那么应该没问题,我们有理由相信比特币是这样的。
随着加密货币变得越来越流行,比特币的 PoW 难度逐渐增加。难度现在已经非常高,大多数人无法负担得起所需的硬件来有机会挖掘一个区块。如今,大多数矿工会聚集在所谓的挖矿池中,以分配挖掘一个区块所需的工作。然后,他们分享奖励。
在区块 632874 [. . .] 中,比特币区块链的预期累积工作量超过了 2⁹² 次双 SHA256 哈希运算。
—Pieter Wuille (2020, mng.bz/aZNJ)
要理解分叉为何具有破坏性,让我们想象以下情景。Alice 从你这里购买了一瓶葡萄酒,而你一直在等待她将她账户中的 5 BTC 发送给你。最终,你观察到高度为 10 的新区块(意味着创世区块之后的第 10 个区块)包含了她的交易。谨慎起见,你决定等待再添加 6 个区块在其上。等待了一段时间后,你最终看到了高度为 16 的区块,延伸了包含你的高度为 10 的区块的链。你将葡萄酒送给了 Alice,并称其为一天结束。但这还不是故事的结束。
后来,高度为 30 的区块突然出现,延伸了一个刚在你的区块之前(高度为 9)分叉出来的不同区块链。由于新链更长,最终被所有人接受为合法链。你之前所在的链(从你的高度为 10 的区块开始)被丢弃,网络中的参与者简单地重新组织他们的链,指向新的最长链。正如你所猜测的,这个新链中没有包含爱丽丝的交易。相反,它包含一笔交易,将她所有的资金转移到另一个地址,阻止你重新发布将她的资金转移到你地址的原始交易。爱丽丝有效地双重花费了她的钱。
这就是51%攻击。这个名称来自爱丽丝执行攻击所需的计算能力的数量;她只需要比其他人多一点点。(crypto51.app有一张有趣的表格,列出了根据 PoW 在不同加密货币上执行 51%攻击的成本。)这不仅仅是一个理论上的攻击!51%攻击在现实世界中发生。例如,在 2018 年,一名攻击者成功地在 Vertcoin 货币上进行了 51%攻击,双重花费了一些资金。
攻击者实质上重写了账本的部分历史,然后利用他们的主导哈希算力生成最长链,说服其他矿工验证这个新版本的区块链。有了这个,他或她可以实施终极的加密犯罪:对先前交易进行双重花费,让先前的收款人持有无效的硬币。
—迈克尔·J·凯西(“Vertcoin 的困境是真实的:为什么最新的加密 51%攻击很重要”,2018)
在 2019 年,以太坊经典(以太坊的一个变种)发生了同样的事情,导致当时损失超过 100 万美元,出现了超过 100 个区块深度的多次重组。2020 年,比特币黄金(比特币的一个变种)也遭受了 51%攻击,从加密货币的历史中删除了 29 个区块,并在不到两天内双重花费了超过 7 万美元。
12.2.4 通过使用默克尔树来减小区块的大小
我想谈谈比特币的另一个有趣方面,即它如何压缩部分可用信息。比特币中的一个区块实际上不包含任何交易!交易是单独共享的,而一个区块包含一个认证一系列交易的单一摘要。该摘要可以简单地是区块中包含的所有交易的哈希值,但它比那更聪明。相反,该摘要是一个Merkle 树的根。
什么是默克尔树?简单来说,它是一个树(数据结构),其中内部节点是它们子节点的哈希值。这可能有点令人困惑,一幅图值千言,所以看看图 12.7。

图 12.7 梅克尔树,一种验证其叶子元素的数据结构。在树中,内部节点是其子节点的哈希值。根哈希可以用来验证整个结构。在图中,H()表示哈希函数,逗号分隔的输入可以实现为连接(只要没有歧义)。
梅克尔树是有用的结构,你会在各种实际协议中找到它们。它们可以将大量数据压缩为一个小的、固定大小的值——树的根。不仅如此,你不一定需要所有的叶子来重建根。
例如,想象一下,你知道梅克尔树的根是因为它包含在一个比特币区块中,你想知道一个交易(树中的一个叶子)是否包含在该区块中。如果它在树中,我可以与你分享路径上的相邻节点作为成员证明。(一种在树的深度上对数大小的证明。)你需要做的是通过对路径中的每一对进行哈希运算,计算出根节点的内部节点直到根节点。在文字上解释这个过程有点复杂,所以我在图 12.8 中用图示来说明这个证明。

图 12.8 知道梅克尔树的根,可以通过重构所有叶子的根哈希来验证一个叶子是否属于树。为此,你首先需要所有叶子,在我们的图中是 8 个摘要(假设叶子是某个对象的哈希)。如果你不需要所有其他叶子,还有一种更有效的方法来构建成员证明:你只需要路径中从叶子到根的相邻节点,这包括你的叶子在内的 4 个摘要。验证者可以使用这些相邻节点来计算路径上所有缺失节点的哈希,直到重建根哈希并查看它是否与他们期望的相匹配。
在一个区块中使用梅克尔树而不是直接列出所有交易的原因是为了减轻下载所需信息以执行对区块链的简单查询。例如,想象一下,你想要检查你最近的交易是否包含在一个区块中,而不必下载比特币区块链的整个历史记录。你可以做的是仅下载区块头,因为它们不包含交易而更轻,一旦你拥有了它,就可以询问一个节点告诉你哪个区块包含了你的交易。如果有这样的一个区块,他们应该能够提供一个证明,证明你的交易在你在区块头中拥有的摘要所认证的树中。
对于比特币还有很多要讲,但这本书的页数有限。因此,我将利用本章剩余的空间带你了解这个领域,并解释经典的 BFT 共识协议是如何工作的。
12.3 加密货币的概览
比特币是第一个成功的加密货币,尽管已经创建了数百种其他加密货币,但比特币仍然保持着最大市场份额和价值。有趣的是,比特币存在许多问题,其他加密货币已经尝试解决(有些成功)。更有趣的是,加密货币领域利用了许多直到现在都没有许多实际应用或甚至不存在的加密原语!所以,话不多说,以下部分列出了自比特币诞生以来已经研究的问题。
12.3.1 波动性
目前大多数人使用加密货币作为投机工具。比特币的价格显然有助于这个故事,因为它已经证明它可以在一天内轻松地上千美元地上下波动。有些人声称稳定性将随着时间的推移而来,但事实仍然是,比特币现在不能用作货币。其他加密货币已经尝试使用稳定币的概念,将其代币的价格与现有的法定货币(如美元)挂钩。
12.3.2 延迟
您可以通过许多方式来衡量加密货币的效率。加密货币的吞吐量是它可以处理的每秒交易数量。例如,比特币的吞吐量相当低,每秒只有 7 笔交易。另一方面,确定性是一旦您的交易被包含在区块链中就被视为已确定的时间。由于分叉,比特币的确定性永远无法完全实现。被认为是在交易被包含在新区块中的至少一小时后,交易被撤销的概率变得可接受。这两个数字都极大地影响了延迟,延迟是从用户的角度来看,交易被最终确认所需的时间。在比特币中,延迟包括交易的创建,将其传播到网络的时间,将其包含在区块中的时间,最后是等待区块确认的时间。
这些速度问题的解决方案可以通过 BFT 协议解决,这些协议通常提供仅需几秒钟即可完成的确定性,并保证不会发生分叉,并且每秒可处理数千笔交易。然而,有时这仍然不够,正在探索不同的技术。所谓的第二层协议尝试提供额外的解决方案,可以在链下更快地进行支付,并周期性地将进度保存在主区块链上(与第一层相比称为层 1)。
12.3.3 区块链大小
比特币和其他加密货币的另一个常见问题是区块链的大小可能迅速增长到不切实际的大小。当用户想要使用加密货币(例如查询其账户余额)时,会出现可用性问题,因为他们预期必须首先下载整个链才能与网络交互。处理大量交易每秒的基于 BFT 的加密货币预计将在几个月甚至几周内轻松达到几 TB 的数据。存在几种解决方案。
其中最有趣的之一是 Mina,它不需要您下载整个区块链的历史记录才能到达最新状态。相反,Mina 使用零知识证明(ZKPs),在第七章中提到,我将在第十五章中更深入地介绍,将所有历史记录压缩成固定大小的 11 KB 证明。这对于像手机这样的轻客户端特别有用,通常必须信任第三方服务器才能查询区块链。
12.3.4 保密性
比特币提供了伪匿名性,因为账户仅与公钥相关联。只要没有人能将公钥与个人联系起来,相关账户就保持匿名。请记住,与该账户有关的所有交易都是公开的,社交图仍然可以创建,以了解谁倾向于与谁更频繁地交易,以及谁拥有多少货币。
有许多加密货币尝试使用 ZKPs 或其他技术来解决这些问题。Zcash是最知名的保密加密货币之一,因为其交易可以加密发送者地址、接收者地址和交易金额。所有这些都使用 ZKPs!
12.3.5 能源效率
比特币因为在电力消耗方面过于庞大而受到了严厉批评。事实上,剑桥大学最近评估,挖掘比特币所花费的所有能源使比特币成为世界前 30 大能源使用国(如果视为一个国家),在一年内消耗的能源比阿根廷还多(2021 年 2 月;cbeci.org/)。另一方面,BFT 协议不依赖于 PoW,因此避免了这种沉重的开销。这无疑是为什么任何现代加密货币似乎都避免基于 PoW 的共识,甚至像以太坊这样重要的 PoW-based 加密货币也宣布计划转向更环保的共识协议。在进入下一章之前,让我们看看基于 BFT 共识协议的这些加密货币。
12.4 DiemBFT:拜占庭容错(BFT)共识协议
许多现代加密货币已经放弃了比特币的 PoW 方面,转而采用更环保和更高效的共识协议。这些共识协议大多基于经典的 BFT 共识协议,这些协议大多是原始 PBFT 协议的变体。在本节中,我将使用 Diem 来说明这种 BFT 协议。
Diem(之前称为 Libra)是一种数字货币,最初由 Facebook 在 2019 年宣布,由 Diem 协会管理,该协会是由公司、大学和非营利组织组成,旨在推动开放和全球支付网络。Diem 的一个特点是它由真实货币支持,使用法定货币储备。这使得数字货币稳定,不像它的老表兄比特币。为了以安全和开放的方式运行支付网络,使用了一种 BFT 共识协议称为 DiemBFT,这是 HotStuff 的一个变种。在本节中,让我们看看 DiemBFT 是如何工作的。
12.4.1 安全性和活性:BFT 共识协议的两个属性
BFT 共识协议旨在在容忍一定比例的恶意参与者的情况下实现两个属性。这些属性包括
-
安全性—不会达成矛盾的状态,意味着不应该发生分叉(或以极小的概率发生)。
-
活性—当人们提交交易时,状态最终会处理它们。换句话说,没有人可以阻止协议完成其任务。
请注意,如果参与者不按照协议行事,则通常被视为恶意(也称为 拜占庭)。这可能意味着他们什么也不做,或者他们没有按照正确顺序执行协议的步骤,或者他们没有遵守一些旨在确保没有分叉的强制性规则,等等。
BFT 共识协议通常很容易实现安全性,而活性则被认为更加困难。事实上,1985 年由 Fischer、Lync 和 Paterson 提出的著名不可能结果(“一个故障进程下的分布式共识不可能性”)与 BFT 协议相关,指出在 异步 网络(消息可以花费任意时间到达)中,没有 确定性 共识协议能够容忍故障。大多数 BFT 协议通过将网络视为某种程度上的 同步(事实上,如果你的网络长时间宕机,任何协议都是无用的)或者在算法中引入随机性来避免这一不可能结果。
出于这个原因,即使在极端网络条件下,DiemBFT 也永远不会分叉。此外,即使存在网络分区,即网络的不同部分无法到达其他部分,只要网络最终恢复和稳定足够长的时间,它总是会取得进展。
12.4.2 DiemBFT 协议中的一轮
Diem 在一个预先知道参与者(称为 验证者)的许可设置中运行。协议在严格递增的轮次(第 1 轮、第 2 轮、第 3 轮等)中前进,在此期间验证者轮流提出交易块。在每一轮中
-
被选择为领导者(确定性地)的验证者收集一定数量的交易,将它们组合成一个新的区块,延伸区块链,然后对区块进行签名并将其发送给所有其他验证者。
-
在收到建议的区块后,其他验证者可以通过签名并将签名发送给下一轮的领导者来对其进行认证。
-
如果下一轮的领导者收到足够多的选票支持该区块,他们可以将所有这些选票捆绑在一个称为quorum certificate(QC)的证书中,该证书证明了该区块,并使用该 QC 提出一个新的区块(在下一轮中)来延伸现在已经被认证的区块。
另一种看待这个问题的方式是,在比特币中,一个区块只包含它所延伸的区块的哈希值,而在 DiemBFT 中,一个区块还包含对该哈希值的一定数量的签名。(签名的数量很重要,但稍后再详细说明。)
请注意,如果验证者在一轮中没有看到建议(例如,因为领导者离线了),他们可以超时并警告其他验证者没有发生任何事情。在这种情况下,将触发下一轮,提议者可以延伸他们已看到的最高认证区块。我在图 12.9 中总结了这一点。

图 12.9 DiemBFT 的每一轮都是由指定的领导者提出延伸他们所见到的最后一个区块的区块开始的。其他验证者随后可以对该区块进行投票,将他们的投票发送给下一轮的领导者。如果下一轮的领导者收集到足够的选票以形成一个 quorum certificate(QC),他们可以提出一个包含 QC 的新区块,有效地延伸之前看到的区块。
12.4.3 协议能容忍多少不诚实行为?
假设我们希望在最多容忍f个恶意验证者的情况下(即使他们全部串通作恶),那么 DiemBFT 规定协议需要至少有 3f + 1 个验证者参与(换句话说,对于f个恶意验证者,至少需要有 2f + 1 个诚实验证者)。只要这个假设成立,该协议就能提供安全性和活力。
有了这个前提,只有获得大多数诚实验证者的投票才能形成 QC,即使有 3f + 1 个参与者,这也需要 2f + 1 个签名。这些数字可能有点难以想象,因此我展示了它们对我们观察到的投票信心的影响,见图 12.10。

图 12.10 在 DiemBFT 协议中,至少有三分之二的验证者必须是诚实的,协议才能安全(不会分叉)和活跃(会取得进展)。换句话说,只要有 2f + 1 个验证者是诚实的,该协议就能容忍f个不诚实的验证者。一个已认证的区块至少收到了 2f + 1 个投票,因为这是能够代表大多数诚实验证者的最低投票数。
12.4.4 DiemBFT 投票规则
验证者必须始终遵循两个投票规则,否则将被视为拜占庭式的:
-
他们不能在过去投票(例如,如果你刚刚在第 3 轮投票,你只能在第 4 轮及以上投票)。
-
他们只能为延伸到他们首选轮次或更高轮次的区块投票。
什么是首选轮次?默认情况下是 0,但如果你为一个延伸到一个延伸到一个区块的区块投票(我的意思是你为一个有祖父区块的区块投票),那么那个祖父区块的轮次就成为你的首选轮次,除非你之前的首选轮次更高。复杂吗?我知道,这就是为什么我制作了图 12.11。

图 12.11 在为一个区块投票后,验证者将他们的首选轮次设置为祖父区块的轮次,如果它高于他们当前的首选轮次。要为一个区块投票,其父区块的轮次必须大于或等于首选轮次。
12.4.5 交易何时被视为最终确定?
请注意,已认证的区块尚未最终确定,或者我们也可以说是已提交。没有人应该假设包含在待处理区块中的交易不会被撤销。只有当提交规则被触发时,区块和其中包含的交易才能被视为最终确定。提交规则(在图 12.12 中说明)表示,如果:
-
区块开始了一个由 3 个在连续轮次(例如,在第 1、2 和 3 轮)中提出的区块组成的链。
-
这个 3 个区块链的最后一个区块被认证。

图 12.12 三个连续轮次(3、4、5)恰好有一条链由认证的区块组成。观察到第 5 轮的最后一个区块被第 9 轮的 QC 认证的任何验证者可以提交第 3 轮的链的第一个区块,以及其所有祖先(这里是第 1 轮的区块)。任何相矛盾的分支(例如第 2 轮的区块)都会被丢弃。
这就是协议的高层次内容。但是,当然,细节才是关键。
12.4.6 DiemBFT 安全性背后的直觉
虽然我鼓励你阅读 DiemBFT 论文中的一页安全性证明,但我想在这里用几页来让你直观地理解它为什么有效。首先,我们注意到在同一轮中不能认证两个不同的区块。这是一个重要的特性,我在图 12.13 中用视觉方式解释。

图 12.13 假设在一个由 3f + 1 个验证者组成的协议中只能有最多f个恶意验证者,并且一个法定证书是由 2f + 1 个签名投票创建的,那么每轮只能有一个经过认证的区块。图中展示了一个反证法,证明这是不可能的,因为那样会与我们最初的假设相矛盾。
利用只有一个区块可以在给定轮次获得认证的属性,我们可以简化我们讨论区块的方式:区块 3 在第 3 轮,区块 6 在第 6 轮,依此类推。现在,看一下图 12.14,并花点时间弄清楚为什么一个经过认证的区块,或两个经过认证的区块,或三个非连续轮次的经过认证的区块不能在不冒风险的情况下导致提交。

图 12.14 在所有这些场景中,提交区块 5 可能导致分叉。只有在第 4 个场景中提交区块 5 是安全的。你能告诉为什么在所有场景中除了第 4 个场景提交区块 5 是危险的吗?
你能找出所有场景的答案吗?简短的答案是,除了最后一个场景外,所有场景都留有一个区块可以延伸到第 1 轮。这个晚到的区块实际上会分叉并根据共识协议的规则进一步延伸。如果发生这种情况,区块 5 和其他延伸它的区块将被丢弃,因为另一个更早的分支被提交。对于场景 1 和 2,这可能是由于提议者没有看到之前的区块。在场景 3 中,一个更早的区块可能出现得比预期晚,可能是由于网络延迟,或者更糟糕的是,由于验证者在合适的时机才公布它。我在图 12.15 中进一步解释这一点。

图 12.15 在图 12.14 的基础上,除了最后一个场景外,所有场景都允许一个可以最终获胜并丢弃区块 5 分支的并行链。最后一个场景有一个由三个连续轮次的经过认证的区块组成的链。这意味着区块 7 有大多数诚实选民,他们反过来更新了他们的首选轮次到第 5 轮。之后,没有区块可以在区块 5 之前分叉并同时获得 QC。最糟糕的情况是一个区块延伸区块 5 或区块 6,这最终会导致相同的结果—区块 5 被提交。
摘要
-
加密货币是关于去中心化支付网络,以避免单点故障。
-
为了让每个人对加密货币的状态达成一致,我们可以使用共识算法。
-
拜占庭容错(BFT)共识协议于 1982 年发明,并已发展成为更快速和更简单易懂的形式。
-
BFT 共识协议需要一个已知且固定的参与者集合(许可网络)。这样的协议可以决定谁是这个参与者集合的一部分(权威证明或 PoA),或者根据他们持有的货币数量动态选举参与者集合(权益证明或 PoS)。
-
比特币的共识算法(中本聪共识)使用工作量证明(PoW)来验证正确的链并允许任何人参与(无许可网络)。
-
比特币的 PoW 让参与者(称为矿工)计算大量哈希以找到具有特定前缀的哈希。成功找到有效摘要允许矿工决定下一个交易区块并收取奖励以及交易费。
-
比特币中的账户只是使用 secp256k1 曲线的 ECDSA 密钥对。用户可以通过查看尚未花费的所有交易输出(UTXO)知道他们的账户持有多少比特币。因此,交易是一条已签名的消息,授权将一定数量的旧交易输出移动到新输出,可以花费给不同的公钥。
-
比特币使用默克尔树来压缩区块的大小,并允许交易包含验证的大小较小。
-
稳定币是一种加密货币,试图通过将其代币与美元等法定货币的价值挂钩来稳定其价值。
-
加密货币使用所谓的第二层协议,以减少其延迟,通过在链下处理交易并周期性地保存进度在链上。
-
零知识证明(ZKPs)在许多不同的区块链应用中使用(例如,在 Zcash 中提供保密性,在 Coda 中将整个区块链压缩为短的有效性证明)。
-
Diem 是一种稳定币,它使用称为 DiemBFT 的 BFT 共识协议。只要 3f + 1 参与者中不超过 f 个恶意参与者存在,它就保持安全(没有分叉)和活跃(总是取得进展)。
-
DiemBFT 通过在轮次中让参与者提议延伸先前区块的交易的区块来运作。其他参与者随后可以为该区块投票,如果收集到足够的票数(2f + 1),可能会创建一个法定证书(QC)。
-
在 DiemBFT 中,当触发提交规则(一系列连续轮次的 3 个已认证区块)时,区块及其交易将被最终确定。发生这种情况时,链的第一个区块及其延伸的区块将被提交。
第十三章:硬件加密
本章内容包括
-
高度对抗性环境中的密码学问题
-
增加攻击者成本的硬件解决方案
-
侧信道攻击和软件缓解措施
密码学原语和协议经常被描述为孤立的构建模块,仿佛它们在远离任何对手的星系中运行。实际上,这是一个不切实际的假设,经常被证明是错误的。在现实世界中,密码学在各种环境中运行,并受到各种威胁的影响。在本章中,我们将研究更极端的场景——高度对抗性环境——以及您在这些情况下可以采取的措施以保护您的密钥和数据。(剧透警告:这涉及使用专门的硬件。)
13.1 现代密码学攻击模型
当今的计算机和网络安全始于这样一个假设,即存在一个我们可以信任的域。例如:如果我们为了在互联网上传输数据而加密数据,我们通常假设进行加密的计算机没有被损害,并且存在一些其他的“终点”,可以在那里安全地解密它。
— Joanna Rutkowska(《Intel x86 可恶之处》,2015)
密码学曾经是关于“爱丽丝想要将消息加密发送给鲍勃,而不让伊娃能够截获它”。如今,它的大部分内容已经转移到了更类似于“爱丽丝想要将消息加密发送给鲍勃,但爱丽丝已经受到了损害。”这是一个完全不同的攻击者模型,通常在理论密码学中没有被预料到。我这是什么意思?让我给你一些例子:
-
在可能装有读取器假面(skimmer)的自动取款机(ATM)上使用信用卡。读取器假面是窃贼可以放置在读卡器顶部的设备,用于复制您银行卡的内容(见图 13.1)
-
在您的手机上下载一个破坏操作系统(OS)的应用程序
-
在共享网络托管服务中托管网站,另一个恶意客户可能与您共用同一台机器
-
在被来自不同国家间谍访问的数据中心中管理高度敏感的机密

图 13.1 读取器假面,一种恶意设备,可放置在 ATM 或付款终端的读卡器前,以复制磁条中的数据。磁条通常包含账号、到期日期和其他元数据,您用于在线支付或在许多付款终端上支付。假面有时伴随着隐藏摄像头一起使用,以获取您的个人识别码(PIN),从而潜在地使窃贼能够进行取款和要求输入 PIN 的付款终端。
所有这些示例都是在许多密码学家忽视或完全不了解的威胁模型中对密码学的现代应用。事实上,您在文献中读到的大多数密码学原语都假设例如,艾丽丝完全控制她的执行环境,只有当密文(或签名或公钥或……)离开她的计算机进入网络时,中间人攻击者才能执行他们的技巧。但是,在现实和现代,我们经常在更具对抗性的模型中使用密码学。
警告 安全性毕竟是您的假设和对潜在攻击者的期望的产物。如果您的假设是错误的,那么您将度过糟糕的时光。
现实世界中的应用如何将理论加密与这些更强大的攻击者相协调?它们做出妥协。换句话说,他们试图让攻击者的生活更加困难。这些系统的安全性通常是以成本(攻击者需要花费多少来破解系统?)而不是计算复杂性来计算的。
在本章中,您将学到很多不完美的加密技术,这在现实世界中我们称之为深度防御。有很多东西需要学习,这一章带来了许多新的缩略词和不同的解决方案,不同的供应商以及他们的营销团队和销售人员提出了。所以让我们开始学习在不受信任的环境中的可信系统。
13.2 不受信任的环境:硬件拯救
实践中攻击系统有不同的方法。将它们归类的一种方式是这样思考:
-
软件攻击—利用在您设备上运行的代码的攻击。
-
硬件攻击—需要攻击者物理接近您的设备的攻击方式。
在之前的章节中,我已经反复讨论了针对加密的软件攻击以及如何减轻它们的影响,但是如果利用硬件解决方案,有些软件攻击会更容易防御。例如,通过在连接到您计算机的独立设备上生成和使用加密密钥,一个感染您计算机的病毒将无法提取密钥。
然而,硬件攻击更加棘手,因为获得设备访问权限的攻击者几乎可以为所欲为:磁盘上的数据可以任意修改,激光可以瞄准特定位置以迫使计算产生错误值(所谓的故障攻击),芯片可以打开以显示其部件,聚焦离子束(FIB)显微镜可用于逆向工程组件等等。天空是极限,保护免受这种有动机的攻击者是很困难的。通常,可用的不同解决方案归结为尽可能添加更多层次的防御以使攻击者的生活更加困难。这一切都是关于提高成本!
恶意女佣攻击
并非所有的硬件攻击者都是一样的。例如,有些攻击者可以花费一些时间与您的设备相处,而其他人可能只有有限的时间。想象一下以下情景:您把手机或笔记本电脑放在酒店房间里不管,一个“恶意”的女佣进来,打开设备,使用低成本的现成工具修改系统,然后离开设备看起来未经触碰就回到您的房间之前的地方。在文献中,这被称为恶毒女佣攻击,并且可以推广到许多情况(例如,携带设备在飞行时的托运行李中,将敏感密钥存储在不安全的数据中心中等)。
当然,并非所有系统都必须防范最强大的硬件攻击,也不是所有应用程序都面对相同级别的威胁。不同的硬件解决方案适用于不同的情境,因此本节剩余内容是关于理解“这样那样”的区别。
13.2.1 白盒密码学,一个糟糕的想法
在涉及不受信任环境的硬件解决方案之前,为什么不使用软件解决方案呢?密码学能否提供不泄露自己密钥的原语?
白盒密码学正是这样:密码学的一个领域,试图将其使用的密钥与加密实现混合在一起。目标是防止观察者从中提取密钥。攻击者获取了某个带有固定密钥的白盒 AES 实现的源代码,它可以很好地加密和解密,但是密钥与实现混合得太好了,以至于任何人都很难从算法中提取它。这至少是理论上的。在实践中,尚未发现任何已发布的白盒密码算法是安全的,大多数商业解决方案由于这个原因是闭源的。
注意安全通过模糊和混淆(将代码混淆以使其看起来难以理解)是一种通常不受欢迎的技术,因为它们尚未被证明有效。尽管如此,在现实世界中,这些技术有时会有用,并且可以用来延迟和挫败对手。
总的来说,白盒密码学是一个大行业,向需要数字版权管理(DRM)解决方案的企业销售可疑的产品(控制客户对其购买的产品的访问权限的工具)。例如,您可以在播放您在商店购买的电影的硬件中找到这些白盒解决方案,或者在您正在观看的流媒体服务中播放电影的软件中找到这些解决方案。实际上,DRM 并不能强力阻止这些攻击;它只是让他们的客户的生活变得更加困难。更严肃的是,有一个称为不可区分混淆(iO)的密码学分支试图在密码学上实现这一点。iO 是一个理论上的、不切实际的、到目前为止还没有真正被证明的研究领域。我们将看看这个领域的发展如何,但我不会抱太大希望。
13.2.2 它们在你的钱包里:智能卡和安全元素
白盒密码学并不是很好,但这几乎是对抗强大对手的最佳软件解决方案。因此,让我们转向硬件方面寻找解决方案。(剧透警告:事情即将变得更加复杂和令人困惑。)如果您认为现实世界的密码学很混乱,有太多的标准或做同样事情的方法,那么等到您了解硬件世界正在发生的事情时,您会感到更加惊讶。不同的术语已经被创造并以不同的方式使用,标准不幸地像密码学标准一样多样化(如果不是更多)。
要了解所有这些硬件解决方案以及它们之间的区别,让我们从一些必要的历史开始。智能卡是通常包装在塑料卡(如银行卡)内的小芯片,于 20 世纪 70 年代初在微电子技术的进步之后发明。智能卡最初是让每个人都有一个口袋计算机的实用方式!事实上,现代智能卡嵌入了自己的 CPU、不同类型的可编程或不可编程存储器(ROM、RAM 和 EEPROM)、输入和输出、硬件随机数生成器(也称为 TRNG,正如你在第八章中学到的),等等。
他们在“智能”方面是指它们可以运行程序,不像以前的不那么智能的卡片只能通过磁条存储数据,这些数据可以很容易通过我之前提到的偷取器复制。大多数智能卡允许开发人员编写可以在卡上运行的小型、独立的应用程序。智能卡支持的最流行的标准是JavaCard,它允许开发人员编写类似于 Java 的应用程序。
要使用智能卡,您首先需要通过将其插入读卡器来激活它。最近,卡片已经通过近场通信(NFC)协议进行了增强,以通过无线电频率实现相同的结果。这使您可以通过靠近读卡器来使用卡片,而不是物理接触。
银行和传统密码学
顺便说一句,银行利用智能卡存储每张卡的唯一卡密,能够表明:“我确实是您给这位客户的卡。”直觉上,您可能认为这是通过公钥加密实现的,但银行业仍然停留在过去,使用对称加密(由于仍在使用的大量传统软件和硬件)!
更具体地说,大多数银行卡存储着一个三重 DES(3DES)对称密钥,这是一个旧的 64 位分组密码,旨在使不安全的数据加密标准(DES)安全。该算法用于生成 MAC(消息认证码)而不是加密,用于对某些挑战生成 MAC。持有每位客户当前 3DES 对称密钥的银行可以验证 MAC。这是现实世界加密通常涉及的一个绝佳例子:在许多地方以一种危险的方式使用的传统算法。(这也是为什么密钥轮换是一个如此重要的概念,以及为什么你必须定期更换银行卡。)
智能卡结合了许多物理和逻辑技术,以防止其执行环境和存储秘密的部分的观察、提取和修改。存在许多试图破解这些卡片和硬件设备的攻击。这些攻击可以分为三种不同的类别:
-
非侵入式攻击—不会影响目标设备的攻击。例如,差分功耗分析(DPA)攻击评估智能卡在执行加密操作时的功耗,以提取其密钥。
-
半侵入式攻击—利用对芯片表面的访问以非破坏性方式进行攻击以实施利用。例如,差分故障分析(DFA)攻击利用热量、激光等技术修改智能卡上运行的程序的执行,以泄露密钥。
-
侵入式攻击—打开芯片以探测或修改硅片电路,以改变芯片的功能并揭示其秘密的攻击。这些攻击是显著的,因为它们可能损坏设备,并且有更大的可能性使设备无法使用。
硬件芯片非常小且紧密封装的事实可能使攻击变得困难。但专门的硬件通常通过使用不同层次的材料防止解封和物理观察,并使用硬件技术增加已知攻击的不准确性而进一步防范。
智能卡迅速变得非常流行,很快就变得明显,将这样一个安全的黑匣子放入其他设备中可能是有用的。一个安全元件的概念诞生了:一个防篡改的微控制器,可以以可插拔的形式找到(例如,您手机中用于访问运营商网络所需的 SIM 卡)或直接粘贴在芯片和主板上(例如,连接到 iPhone NFC 芯片进行支付的嵌入式安全元件)。安全元件实际上只是一个小型、独立的硬件部件,旨在保护您的机密信息及其在加密操作中的使用。
安全元件是保护物联网(IoT)中的加密操作的重要概念,这是一个口头上的(并且有点过载的)术语,指的是可以与其他设备通信的设备(比如信用卡、手机、生物识别护照、车库钥匙、智能家居传感器等等)。您可以将本节中的所有解决方案视为以不同形式实现的安全元素,使用不同的技术来实现几乎相同的功能,但提供不同级别的安全性和速度。
关于安全元素的主要定义和标准是由 Global Platform 制定的,这是一个由行业内不同参与者的需求而创建的非营利性协会,旨在促进不同供应商和系统之间的互操作性。还有更多关于安全元素的安全声明的标准和认证,来自 Common Criteria(CC)、NIST 或 EMV(欧洲支付、万事达卡和 Visa)等标准机构。
由于安全元素是高度保密的配方,将它们集成到您的产品中意味着您将不得不签署保密协议并使用闭源硬件和固件。对于许多项目来说,这被视为透明度的严重限制,但可以理解,因为这些芯片的安全性部分来自于其设计的模糊性。
13.2.3 银行喜欢它们:硬件安全模块(HSM)
如果你了解什么是安全元件,那么硬件安全模块(HSM)基本上就是一个更大更快的安全元件,而且像一些安全元件一样,一些 HSM 也可以运行任意代码。然而,这并不总是正确的。一些 HSM 很小(比如 YubiHSM,一个微型 USB dongle,类似于 YubiKey),而术语硬件安全模块可能会因人而异地被用来表示不同的事物。
许多人会认为到目前为止讨论的所有硬件解决方案都是不同形式的 HSM,并且安全元素只是由 GlobalPlatform 指定的 HSM,而 TPM(可信平台模块)是由 Trusted Computing Group 指定的 HSM。但大多数时候,当人们谈论 HSM 时,他们指的是大型设备。
HSM 经常根据 FIPS 140-2 进行分类,“加密模块的安全要求”。该文档相当古老,于 2001 年出版,自然而然地,并未考虑在其出版后发现的许多攻击。幸运的是,在 2019 年,它被更现代的版本 FIPS 140-3 所取代。FIPS 140-3 现在依赖于两个国际标准:
-
ISO/IEC 19790:2012—为硬件安全模块定义了四个安全等级。一级 HSM 不提供任何防御措施(你可以将其视为纯软件实现),而三级 HSM 如果检测到任何入侵,就会擦除其秘密!
-
ISO 24759:2017—定义了 HSM 必须如何测试以标准化 HSM 产品的认证。
不幸的是,这两个标准都不是免费的。如果你想阅读它们,就得付费。
美国、加拿大和一些其他国家规定某些行业(如银行)必须使用根据 FIPS 140 等级认证的设备。全球许多公司也遵循这些建议。
注意:擦除秘密是一种叫做 零化 的做法。与三级 HSM 不同,四级 HSM 可以多次覆盖秘密数据,即使在停电情况下也是如此,这要归功于备份内部电池。
通常,你会发现 HSM 是一个外部设备,有自己的货架放在机架上(见图 13.2),插入到数据中心中的企业服务器上,作为插入到服务器主板上的 PCIe 卡,或者甚至是类似硬件安全令牌的小型 dongle。它们可以通过 USB 设备插入到你的硬件中(如果你不介意较低的性能)。回到原点,其中一些 HSM 可以使用智能卡进行管理,用于安装应用程序,备份密钥等等。

图 13.2 作为 PCI 卡的 IBM 4767 HSM。来自维基百科的照片(mng.bz/XrAG)。
一些行业高度利用 HSM。例如,每当你在 ATM 中输入你的 PIN 时,PIN 最终都会由某个地方的 HSM 进行验证。每当你通过 HTTPS 连接到网站时,信任的根源来自存储其私钥在 HSM 中的证书颁发机构(CA),而 TLS 连接可能是由 HSM 终止的。你有安卓手机或 iPhone 吗?谷歌或苹果很有可能使用一批 HSM 来安全地备份你的手机。最后一种情况很有趣,因为威胁模型被颠倒了:用户不信任云端的数据,因此,云服务提供商声称其服务无法查看用户的加密备份,也无法访问用于加密的密钥。
HSM 实际上没有标准的接口,但其中大多数至少会实现公钥密码标准 11(PKCS#11),这是由 RSA 公司发起的一个古老标准,2012 年逐渐转移到 OASIS 组织,以促进标准的采用。虽然 PKCS#11 的最新版本(v2.40)发布于 2015 年,但它只是一个标准的更新,最初始于 1994 年。因此,它规定了许多旧的加密算法或旧的操作方式,这可能会导致漏洞。尽管如此,对于许多用途来说它已经足够好,并且指定了一个允许不同系统轻松互操作的接口。好消息是,PKCS#11 v3.0 在 2020 年发布,包括许多现代加密算法,例如 Curve25519、EdDSA 和 SHAKE 等。
HSM 的真正目标是确保没有人可以从中提取密钥材料,但它们的安全性并不总是闪耀的。关于这些硬件解决方案的安全性很大程度上依赖于它们的高价格、未公开的硬件防御技术以及主要关注硬件方面的认证(如 FIPS 和 Common Criteria)。实际上,已经发现了严重的软件漏洞,而且你使用的 HSM 是否受到这些漏洞的威胁并不总是一目了然。2018 年,Jean-Baptiste Bédrune 和 Gabriel Campana 在他们的研究中展示了一种软件攻击方法(“Everybody be Cool, This is a Robbery”),可以从流行的 HSM 中提取密钥。
注意一个 HSM 的价格不仅高(根据安全级别,它可能轻松达到数万美元),而且除了一个 HSM 外,您通常至少还有另一个用于测试的 HSM,以及至少还有一个用于备份(以防您的第一个 HSM 因密钥而损坏)。这可能会加起来!
此外,我还没有涉及所有这些解决方案中的“大象在房间里”:虽然你可能会阻止大多数攻击者获取你的秘密密钥,但你无法阻止攻击者破坏系统并对 HSM 进行自己的调用(除非 HSM 具有需要多个签名或存在阈值智能卡的逻辑才能运行)。但是,在大多数情况下,HSM 提供的唯一服务是防止攻击者偷偷窃取秘密并在其他时间使用它们。在集成像 HSM 这样的硬件解决方案时,首先了解您的威胁模型、您要防范的攻击类型以及我在第八章中提到的多签名等阈值方案是否更好。
13.2.4 可信平台模块(TPM):安全元素的有用标准化
尽管安全元件和 HSM(硬件安全模块)被证明是有用的,但它们仅限于特定用例,并且编写自定义应用程序的过程被认为是乏味的。 出于这个原因,可信计算组(TCG)(由行业参与者组成的另一个非营利组织)提出了一个可用的替代方案,旨在面向个人和企业计算机。 这就是可信平台模块(TPM)。
TPM 不是芯片,而是一个标准(TPM 2.0 标准);任何选择都可以实现它的供应商。 符合 TPM 2.0 标准的 TPM 是一个安全微控制器,具有硬件随机数生成器、用于存储机密的安全存储器,可以执行加密操作,整个系统是防篡改的。 这个描述可能听起来很熟悉,确实,常见的 TPM 实现方式是作为安全元件的重新打包。 通常情况下,您会在企业服务器、笔记本电脑和台式电脑的主板上直接焊接或插入一个 TPM(见图 13.3)。

图 13.3 实现 TPM 2.0 标准的芯片,插入主板。 该芯片可以被系统的主板组件以及运行在计算机操作系统上的用户应用程序调用。 来自维基百科的照片(mng.bz/Q2je)。
与智能卡和安全元件不同,TPM 不运行任意代码。 相反,它提供了一个明确定义的接口,一个更大的系统可以利用它。 TPM 通常相当便宜,今天许多普通笔记本电脑都携带一个。
现在让我们看看坏消息:TPM 和处理器之间的通信渠道通常只是一个总线接口,如果您设法窃取或获得临时物理访问权限,这个通道很容易被截取。 尽管许多 TPM 提供了高度抵抗物理攻击的水平,但它们的通信渠道有些开放的事实确实将它们的用例大部分限制在防御软件攻击上。
为了解决这些问题,已经出现了将类似 TPM 的芯片直接集成到主处理器中的趋势。 例如,苹果有安全信封,微软有 Pluton。 不幸的是,这些安全处理器似乎没有遵循标准,这意味着用户应用程序可能很难,甚至不可能利用它们的功能。 让我们看一些例子,了解像 TPM 这样的硬件安全芯片可以做些什么。
TPM 的最简单的用例是保护数据。要保护密钥很简单:只需在安全芯片中生成它们,并禁止提取。如果您需要密钥,请要求芯片执行加密操作。要保护数据,就对其进行加密。如果你加密单个文件,那概念就叫做基于文件的加密(FBE);如果是整个磁盘,那就叫做全盘加密(FDE)。FDE 听起来要好得多,因为它是一种全盘加密的方法。这是大多数笔记本电脑和台式机使用的方式。但实际上,FDE 并不那么好:它没有考虑到我们人类如何使用我们的设备。我们经常将设备锁定,而不是关闭,以便后台功能可以继续运行。计算机通过保留数据加密密钥(DEK)来处理这一点,即使您的计算机已锁定也是如此。 (下次你在星巴克上厕所时,留下你锁定的电脑不受监管时,请考虑一下这一点。)现代手机提供了更多的安全性,根据手机是锁定还是关闭,对不同类型的文件进行加密。
实际上,FDE 和 FBE 都有许多实施问题。2019 年,Meijer 和 Gastel(在“自我加密的欺骗:固态硬盘(SSD)加密中的弱点”中)表明,几个 SSD 供应商完全没有安全的解决方案。2021 年,Zinkus 等人(在“移动设备上的数据安全:现状、存在的问题和提出的解决方案”中)发现手机磁盘加密也存在许多问题。
当然,在解密数据之前,用户应该经过身份验证。通常通过要求用户输入 PIN 码或密码来实现。但是仅仅使用 PIN 码或密码是不够的,因为这会导致简单的暴力攻击(尤其是对于 4 位或 6 位 PIN 码)。一般来说,解决方案尝试将 DEK 与用户凭据和保留在围栏上的对称密钥绑定起来。
但是芯片制造商不能在他们生产的每个设备中硬编码相同的密钥;这会导致像 DUHK 攻击(duhkattack.com)这样的攻击,其中发现数千个设备都硬编码了相同的秘密。这反过来意味着一个设备的妥协会导致所有设备的妥协!解决方案是每个设备都有一个设备密钥,该密钥可以在制造时被熔入芯片中,或者由芯片自己通过称为物理不可克隆函数的硬件组件创建。例如,每个苹果安全围栏都有一个 UID,每个 TPM 都有一个唯一的认证密钥和证书密钥,等等。为了防止暴力攻击,苹果的安全围栏将 UID 密钥和用户 PIN 与基于密码的密钥导出函数混合(我们在第二章中介绍了这一点)以导出 DEK。除了我撒了个谎:为了允许用户快速更改他们的 PIN,DEK 并不直接派生,而是由一个密钥加密密钥(KEK)加密。
另一个例子是安全启动。当启动计算机时,会经过不同的阶段,直到最终进入想要的屏幕。用户面临的一个问题是病毒和恶意软件,如果它们感染了启动过程,那么你就会运行在一个邪恶的操作系统上。
为了保护引导的完整性,TPM 和集成的安全芯片提供了一个信任根,这是我们百分之百信任的东西,它使我们能够信任后续的其他东西。这个信任根通常是一些只读存储器(ROM),无法被覆盖(也称为一次可编程存储器,因为它在制造过程中被写入,不能更改)。例如,当最近的苹果设备上电时,首先执行的代码是位于苹果安全区 ROM 内部的引导 ROM。这个引导 ROM 非常小,所以通常它所做的唯一的事情就是:
-
准备一些受保护的内存,并加载下一个要运行的程序(通常是另一个引导加载程序)
-
对程序进行哈希处理,并针对 ROM 中的硬编码公钥验证其签名
-
执行程序
下一个引导加载程序也会执行相同的操作,依此类推,直到最终一个引导加载程序启动操作系统。顺便说一句,这就是为什么没有经过苹果签名的操作系统更新无法安装到您的手机上的原因。
TPM 和集成了类似 TPM 的芯片是一个有趣的发展,它们在最近几年极大地增加了我们设备的安全性。随着它们变得更便宜,以及一个胜出的标准出现,越来越多的设备将能够从中受益。
13.2.5 受信执行环境(TEE)的机密计算
智能卡、安全元件、HSM 和 TPM 是独立的芯片或模块;它们带有自己的 CPU、内存、TRNG 等,其他组件可以通过一些导线或 NFC 启用芯片中的无线电频率与它们通信。类似 TPM 的芯片(微软的 Pluton 和苹果的安全区)也是独立的芯片,尽管与系统片上的主处理器紧密耦合。在本节中,我将讨论在这种安全硬件分类法中您可以采取的下一个逻辑步骤,集成安全,硬件强制执行安全性在主处理器内部。
集成安全功能的处理器被称为为用户代码创建了一个受信任执行环境(TEE),通过扩展处理器的指令集,允许程序在一个单独的安全环境中运行。这个安全环境与我们通常处理的环境(通常称为富执行环境)之间的分离是通过硬件实现的。最终发生的是,现代 CPU 同时运行正常的操作系统和安全操作系统。两者都有自己的寄存器集,但大部分 CPU 结构是共享的。通过使用 CPU 强制逻辑,来自安全世界的数据无法从正常世界访问。例如,CPU 通常会分割其内存,将一部分专门用于 TEE 的专用。因为 TEE 直接在主处理器上实现,这不仅意味着 TEE 比 TPM 或安全元件更快、更便宜,而且在许多现代 CPU 中都是免费的。
与所有其他硬件解决方案一样,TEE 是由不同供应商独立开发的概念,标准(由全球平台)试图追赶发展。最知名的 TEE 是英特尔的软件保护扩展(SGX)和 ARM 的 TrustZone。
TEE 有什么用?让我们举个例子。在过去的几年里,有了一个新的范式——云计算——大公司运行服务器来托管您的数据。亚马逊有 AWS,谷歌有 GCP,微软有 Azure。换句话说,人们正在从自己运行事物转向在别人的计算机上运行事物。在一些需要保护隐私的场景中,这会带来一些问题。为了解决这个问题,机密计算试图提供解决方案,以便运行客户端代码而无法查看或修改其行为。SGX 的主要用例似乎正是这些天的客户端运行代码,而服务器不能查看或篡改。
一个有趣的问题是,如何确信响应来自 SGX,例如,而不是来自某个冒充者。这就是 认证试图解决的问题。认证有两种类型:
-
本地认证——在同一平台上运行的两个隔离区需要进行通信并向对方证明它们是安全的隔离区。
-
远程认证——客户端查询远程隔离区,并需要确保它是生成请求结果的合法隔离区。
每个 SGX 芯片在制造时都提供了唯一的密钥对(根密封密钥)。公钥部分然后由一些英特尔 CA 签名。首先假设,如果忽略硬件安全的假设,那么就是 Intel 正确地为安全 SGX 芯片签署公钥。有了这个前提,现在您可以从 Intel 的 CA 获取签名的认证,证明您正在与真实的 SGX 隔离区通信,并且它正在运行某些特定的代码。
TEE 的首要目标是防止软件攻击。虽然声称的软件安全看起来很吸引人,但实际上,由于现代 CPU 的极端复杂性和动态状态,难以在同一芯片上分隔执行。这可以通过针对 SGX 和 TrustZone 的许多软件攻击来证明(foreshadowattack.eu,mdsattacks.com,plundervolt.com和sgaxe.com)。
作为概念的 TEE 提供了一定程度的抵抗物理攻击,因为在这个微观层面上的东西太小、太紧密地包装在一起,以至于没有昂贵的设备无法分析。对于一个积极进取的攻击者,情况可能会不同。
13.3 什么解决方案适合我?
在本章中,你已经了解了许多硬件产品。作为总结,这里是列表,我也在图 13.4 中加以说明:
-
智能卡是需要外部设备(如支付终端)打开的微型计算机。 它们可以运行小型自定义类似 Java 的应用程序。银行卡就是广泛使用的智能卡的一个例子。
-
安全元件是智能卡的一种泛化,依赖于一组全球平台标准。 SIM 卡是安全元件的一个例子。
-
HSMs(硬件安全模块)可以看作是企业服务器的较大的可插拔安全元件。 它们更快、更灵活,主要用于数据中心存储秘密密钥,使密钥攻击更加明显。
-
TPM(可信平台模块)是插入个人和企业计算机主板的重新打包的安全元件。 它们遵循由可信计算组织制定的标准 API,可以为操作系统和最终用户提供功能。
-
安全处理器是建立在主处理器极为接近的 TPM 样式芯片,不可编程。 它们不遵循任何标准,不同的参与者推出了不同的技术。
-
信任执行环境(TEEs,如 TrustZone 和 SGX)可以被视为实现在 CPU 指令集内的可编程安全元件。 它们更快、更便宜,主要提供对软件攻击的抵抗。大多数现代 CPU 都配备了 TEE,并提供各种级别的硬件攻击防御。

图 13.4 你在本章学到的不同硬件解决方案以及它们的外观概念。
什么是最适合你的解决方案?通过自问一些问题来缩小你的选择范围:
-
采用何种形态? 例如,在小型设备中需要安全元件的需求决定了你不能使用哪些解决方案。
-
你需要多快的速度? 需要每秒执行大量加密操作的应用程序将在可以使用的解决方案上受到严格的限制,可能仅限于 HSM 和 TEE。
-
你需要多少安全性? 供应商的认证和声明对应于不同级别的软件或硬件安全性。天空是极限。
请记住,没有硬件解决方案是万灵药;你只是增加了攻击的成本。对于一个复杂的攻击者来说,所有这些都几乎没有用。设计你的系统,以便一个被 compromized 的设备不意味着所有的设备都被 compromized。
13.4 泄漏弹性密码学或如何在软件中减轻侧信道攻击
我们看到硬件试图防止直接观察和提取秘密密钥,但硬件能做的事情有限。归根结底,软件可能会不在乎并且尽管所有这些硬件加固,仍然提供密钥。软件可以直接这样做 (像后门) ,或者它可以间接地泄漏足够的信息,让某人重构密钥。后者被称为 侧信道,侧信道漏洞大多数情况下是不经意的漏洞 (至少人们希望如此)。
我在第三章提到了定时攻击,你在那里学到了 MAC 认证标签必须在恒定的时间内进行比较;否则,攻击者可以在发送了许多不正确的标签并测量等待你回复的时间后,推断出正确的标签。定时攻击在现实世界中的所有领域通常都受到严肃对待,因为它们可以在网络上潜在地远程执行,而不像物理侧信道那样。
最重要的和已知的侧信道是 电源消耗,我在本章前面提到过。这被发现是一种攻击,称为 差分电源分析 (DPA),由 Kocher、Jaffe 和 Jun 在 1998 年发现,当他们意识到他们可以将示波器连接到设备并观察设备随时间变化的电力消耗,同时执行已知明文的加密。这种变化显然取决于所使用的密钥位,以及像异或这样的操作是否会消耗更多或更少的电力,这取决于操作数位是否设置。这个观察结果导致了一种 密钥提取攻击 (所谓的 完全破解)。
这个概念可以用 简单的功耗分析 (SPA) 攻击来说明。在理想的情况下,并且没有硬件或软件对抗功耗分析攻击的实施,只需测量和分析涉及秘密密钥的单个加密操作的功耗消耗即可。我在图 13.5 中说明了这一点。

图 13.5 一些加密算法通过其功耗泄露了大量信息,以至于对单个功耗跟踪(一段时间内的功耗测量)进行简单的功耗分析就可以泄漏算法的私钥。例如,本图表示了 RSA 指数运算的跟踪(消息被指数化为私钥指数;见第六章)。RSA 指数运算采用了一个通过私钥指数的位来迭代的平方乘算法;对于每一位,它只在该位被设置时应用一个平方运算,然后是一个乘法运算。在这个例子中,乘法显然消耗了更多的功耗;因此,功耗跟踪的清晰度。
电源并不是唯一的物理侧信道。一些攻击依赖于电磁辐射、振动,甚至是硬件发出的声音。让我再提到另外两种非物理侧信道。我知道我们处于一个硬件为主的章节,但这些非物理侧信道攻击同样重要,因为它们需要在许多现实世界的加密应用中得到缓解。
首先,返回的错误有时可能泄漏关键信息。例如,在 2018 年,ROBOT 攻击找到了一种利用 Bleichenbacher 攻击(在第六章提到)的方法,攻击了许多实现 RSA PKCS#1 v1.5 解密的服务器,在 TLS 协议中(在第九章中有所涉及)。Bleichenbacher 的攻击只在你可以区分 RSA 密文是否具有有效填充时才有效。为了防止该攻击,安全实现在常量时间内执行填充验证,并且在检测到填充无效时避免提前返回。例如,在 TLS 中的 RSA 密钥交换中,如果 RSA 载荷的填充不正确,服务器必须伪造其响应,使其看起来像是已经完成了成功的握手。然而,如果在填充验证的最后,实现决定根据填充的有效性向客户端返回不同的错误,那么这一切都是徒劳的。
第二,访问内存可能需要更多或更少的时间,这取决于数据是否之前已被访问过。这是由于计算机中存在着众多层次的缓存。例如,如果 CPU 需要某些东西,它首先会检查它是否已经被缓存在其内部存储器中。如果没有,它就会到更远的缓存中去寻找。缓存越远,花费的时间就越长。不仅如此,一些缓存是特定于核心的(例如 L1 缓存),而一些缓存是在多核机器中共享的(例如 L3 缓存、RAM、磁盘)。
缓存攻击利用了一个事实:恶意程序有可能在同一台计算机上运行,使用与敏感密码程序相同的密码库。例如,许多云服务在同一台计算机上托管不同的虚拟服务器,许多服务器使用 OpenSSL 库进行密码操作或提供 TLS 页面。恶意程序找到方法将已加载到与受害者进程共享的缓存中的库的部分逐出,然后定期测量重新读取该库的某些部分所需的时间。如果花费了很长时间,那么受害者没有执行该程序的这部分;如果不花费很长时间,则受害者访问了该程序的这部分并重新填充了缓存,以避免再次将程序从远处的缓存中获取或者更糟的是从磁盘获取。您获得的是类似于功率跟踪的跟踪,而且确实可以以类似的方式进行利用!
好了,侧信道攻击就说到这里。如果您对通过这些侧信道攻击攻击密码学感兴趣,那么有比本书更好的资源。在本节中,我只想讨论密码实现可以和应该实施的软件缓解措施,以保护免受侧信道攻击的影响。这个研究领域整体被称为泄漏韧性密码学,因为密码学家在这里的目标是不泄漏任何信息。
防御物理攻击者是一场永无止境的战斗,这解释了为什么许多这些缓解措施是专有的且类似于混淆。这一部分显然不是详尽无遗的,但应该让您了解应用密码学家正在致力于解决侧信道攻击的类型。
常量时间编程
任何密码学实现的第一道防线是在常量时间内实现其密码学敏感部分(考虑任何涉及秘密的计算)。显而易见,以常量时间实现某事会取消时间攻击,但这也会摆脱许多攻击类别,如缓存攻击和简单的电源分析攻击。
如何以常量时间实现某事?永远不要分支。换句话说,无论输入是什么,始终执行相同的操作。例如,列表 13.1 显示了 Golang 语言如何实现 HMAC 算法的认证标签的常量时间比较。直觉上,如果两个字节相等,那么它们的异或将是 0。如果我们比较的每一对字节都满足这个属性,那么对它们进行 OR 运算也将导致一个 0 值(否则是一个非零值)。请注意,如果这是您第一次看到常量时间技巧,那么阅读这段代码可能会令人困惑。
func ConstantTimeCompare(x, y []byte) byte {
if len(x) != len(y) { // ❶
return 0 // ❶
} // ❶
var v byte // ❷
for i := 0; i < len(x); i++ { // ❷
v |= x[i] ^ y[i] // ❷
} // ❷
return v // ❸
}
❶ 如果两个字符串长度不同,那么在常量时间内比较它们就没有意义。
❷ 这就是魔法发生的地方。循环 OR 将每个字节的异或值累加到一个值 v 中。
❸ 当 v 等于 0 时仅返回 0,否则返回非零值
对于 MAC 身份验证标签比较,仅需要在此处停止通过分支(使用条件表达式,如if)来检查结果是否为 0 或非 0。另一个有趣的例子是椭圆曲线密码中的标量乘法,正如你在第五章中学到的那样,它包括将一个点添加到自身x次数,其中x是我们称之为标量的值。这个过程可能有点慢,因此存在一些聪明的算法来加速这部分。其中一个流行的算法称为蒙哥马利阶梯,基本上等同于我之前提到的 RSA 的平方乘算法(但在不同的群中)。
蒙哥马利阶梯算法在两点相加和一个点加倍之间交替进行(将点加到自身)。RSA 的平方乘和蒙哥马利阶梯算法都有一种简单的方法来缓解时间攻击:它们不分支并且总是执行两个操作。 (这就是为什么 RSA 指数算法通常称为square and multiply always的原因。)
注意 在第七章中,我提到签名方案可能以多种方式出错,并且针对泄漏它们使用的一些字节(在 ECDSA 等签名方案中)的非 ces 存在密钥恢复攻击。这就是 Minerva 和 TPM-Fail 攻击发生的情况,它们发生在同一时间。这两次攻击发现了许多设备由于签名操作所花费的时间变化量而易受攻击。
在实践中,缓解时间攻击并不总是直截了当的,因为 CPU 指令是否用于乘法或条件移动并不总是在恒定的时间内。此外,当使用不同的编译标志时,高级代码如何被编译成机器代码并不总是清楚的。因此,有时会对生成的汇编进行手动审核,以便更加信任编写的恒定时间代码。存在用于分析恒定时间代码的不同工具(如 ducdect、ct-verif、SideTrail 等),但它们在实践中很少被使用。
13.4.2 不要使用秘密!屏蔽和 blinding
另一种常见的阻止或至少混淆攻击者的方法是在涉及秘密的任何操作中添加间接层。其中一种技术称为blinding,这通常得益于公钥密码算法的算术结构。你在第十一章看到了类似于密码认证密钥交换算法的遗忘算法中使用了 blinding,我们可以在我们想要让遗忘的一方成为攻击者观察我们计算中的泄漏的地方同样使用 blinding。让我们以 RSA 为例。
记住,RSA 解密是通过将密文c提升到私有指数d来完成的,其中私有指数d取消了用于计算密文的公共指数e,该公共指数e被用于计算密文为m^e mod N。如果你不记得细节,请务必查阅第六章。增加间接性的一种方法是在攻击者所知的不是密文的值上执行解密操作。这种方法称为基础掩码,操作如下:
-
生成一个随机的掩盲因子r。
-
计算message = (ciphertext × re)d mod N。
-
通过计算real_message = message × r^(–1) mod N来解除掩码,其中r^(–1)是r的逆。
这种方法对正在使用的值进行了掩盲,但我们也可以对秘密本身进行掩盲。例如,椭圆曲线标量乘法通常与秘密标量一起使用。但是由于计算发生在一个循环群中,将阶的倍数添加到该秘密中不会改变计算结果。这种技术称为标量掩码,操作如下:
-
生成随机值k[1]。
-
计算标量k[2] = d + k[1] × order,其中d是原始的秘密标量,order是它的阶数。
-
要计算Q = P,而不是计算Q = [k[2]] P,结果是相同的点。
所有这些技术都被证明多多少少是有效的,并且通常与其他软件和硬件缓解措施组合使用。在对称密码中,另一种有些类似的技术称为掩码。
掩码的概念是在将输入(在密码中为明文或密文)传递给算法之前对其进行转换。例如,通过将输入与随机值进行异或操作。然后解除掩码以获得最终正确的输出。由于任何中间状态都被掩码,因此这为密码计算提供了一定程度的与输入数据的去相关性,并使得侧信道攻击变得更加困难。算法必须意识到这种掩码,以便在保持原始算法的正确行为的同时正确执行内部操作。
13.4.3 关于故障攻击的问题?
我之前谈到了故障攻击,这是一种更具侵入性的侧信道攻击类型,它通过引入故障来修改算法的执行。注入故障可以通过许多创造性的方式来实现,例如通过物理方式,例如增加系统的热量,或者甚至通过向目标芯片的计算点发射激光。
令人惊讶的是,故障也可以通过软件引起。例如,在 Plundervolt 和 V0LTpwn 攻击中独立发现了一个例子,它们成功地改变了 CPU 的电压以引入自然故障。这也发生在臭名昭著的 rowhammer 攻击中,该攻击发现了在一些 DRAM 设备上重复访问内存可以翻转附近的位。这些类型的攻击可能很难实现,但非常强大。在密码学中,计算出错误结果有时可能会泄漏密钥。这就是例如使用一些特定优化实现的 RSA 签名的情况。
虽然无法完全消除这些攻击,但存在一些技术可以增加成功攻击的复杂性;例如,多次计算相同的操作并在发布之前比较结果以确保它们匹配,或者在发布之前验证结果。对于签名,可以在返回之前通过公钥验证签名。
故障攻击也可能对随机数生成器产生严重后果。一个简单的解决方案是使用不在每次运行时使用新随机性的算法。例如,在第七章中,您了解到了 EdDSA,这是一种签名算法,签名时不需要新的随机性,与 ECDSA 签名算法相比。
总的来说,这些技术都不是绝对可靠的。在高度敌对的环境中进行密码学始终是关于你能承受多大成本以应对攻击者的问题。
总结
-
今天的威胁不仅是攻击者拦截信息传输,而且是攻击者窃取或篡改运行您的密码学的设备。所谓的物联网设备通常会遇到威胁,并且默认情况下不受复杂攻击者的保护。最近,云服务也被认为是用户威胁模型的一部分。
-
硬件可以帮助保护密码应用程序及其密钥在高度敌对的环境中。其中一个想法是提供一个具有防篡改芯片的设备来存储和执行加密操作。也就是说,如果设备落入攻击者手中,提取密钥或修改芯片行为将会很困难。
-
人们普遍认为,必须结合不同的软件和硬件技术来加固密码学以应对敌对环境。但硬件保护的密码学并非万能药;它只是多层防御,有效地减慢和增加了攻击的成本。有无限时间和金钱的对手总是能够破坏你的硬件。
-
减少攻击的影响也可以帮助吓阻攻击者。这必须通过良好设计系统来完成(例如,确保一个设备的妥协不意味着所有设备都妥协)。
-
虽然有很多硬件解决方案,但最流行的是以下几种:
-
智能卡是最早的这种安全微控制器之一,可用作微型计算机来存储机密信息并执行加密操作。 它们应该使用多种技术来阻止物理攻击者。 智能卡的概念被概括为安全元素,这是一个在不同领域中以不同方式使用的术语,但归结为可以用作辅助处理器的智能卡,用于已经具有主处理器的更大系统中。
-
硬件安全模块(HSM)通常被称为行为类似安全元素的可插拔卡。 它们不遵循任何标准接口,但通常实现 PKCS#11 标准用于加密操作。 HSM 可以通过一些 NIST 标准(FIPS 140-3)获得不同级别的安全认证。
-
受信平台模块(TPM)类似于具有规定接口的安全元素,标准化为 TPM 2.0。 TPM 通常被视为插入笔记本电脑或服务器主板中。
-
受信执行环境(TEE)是在安全环境和潜在不安全环境之间隔离执行环境的一种方式。 TEE 通常作为 CPU 指令集的扩展来实现。
-
-
在高度对抗环境中,硬件不足以保护加密操作,因为软件和硬件侧信道攻击可以利用以不同方式发生的泄漏(时间、功耗、电磁辐射等)。 为了抵御侧信道攻击,加密算法实施软件减轻措施:
-
严肃的加密实现基于恒定时间算法,并避免所有分支以及依赖于秘密数据的内存访问。
-
基于模糊和掩蔽的减轻技术使敏感操作与秘密或已知要操作的数据脱钩。
-
故障攻击更难以防范。 减轻措施包括多次计算一个操作,并在发布结果之前比较和验证操作的结果(例如,使用公钥验证签名)。
-
-
在对抗环境中加固加密是一场永无止境的战斗。 人们应该使用软件和硬件减轻措施的组合,将成功攻击的成本和时间增加到可接受的风险水平。 人们还应该通过为每个设备使用唯一密钥以及可能为每个加密操作使用唯一密钥来减少攻击的影响。
第十四章:后量子密码学
本章包括
-
量子计算机及其对密码学的影响
-
后量子密码学抵御量子计算机的攻击
-
今天和明天的后量子算法
“量子计算机可以破解密码学,”麻省理工学院数学教授彼得·肖尔暗示道。那是 1994 年,肖尔刚刚提出了一个新算法。他的发现解锁了整数的高效因式分解,如果量子计算机真的成为现实,将摧毁像 RSA 这样的密码算法。当时,量子计算机只是一个理论,一个基于量子物理的新型计算机概念。这个想法仍然有待证明。2015 年中期,国家安全局(NSA)在宣布他们计划过渡到量子抗性算法(不易受量子计算机攻击的密码算法)后,让所有人都感到意外。
对于那些尚未过渡到 Suite B 椭圆曲线算法的合作伙伴和供应商,我们建议暂时不要在这一点上进行重大支出,而是准备好迎接即将到来的量子抗性算法过渡。[...] 不幸的是,椭圆曲线的使用增长与量子计算研究的持续进展相冲突,这清楚地表明椭圆曲线密码学并不是许多人曾经希望的长期解决方案。因此,我们不得不更新我们的策略。
—国家安全局(“今日密码学”,2015 年)
尽管量子计算的概念(基于量子力学领域研究的物理现象构建计算机)并不新鲜,但近年来在研究资助和实验突破方面都经历了巨大提升。然而,至今还没有人能够展示用量子计算机破解密码学。国家安全局知道我们不知道的事情吗?量子计算机真的会破解密码学吗?量子抗性密码学又是什么?在本章中,我将尝试回答你所有的问题!
14.1 量子计算机是什么,为什么会让密码学家感到恐慌?
自国家安全局的宣布以来,量子计算机已经多次成为新闻头条,因为诸如 IBM、Google、阿里巴巴、微软、英特尔等许多大公司已经投入了大量资源进行研究。但这些量子计算机到底是什么,为什么这么令人担忧?一切都始于量子力学(也称为量子物理学),这是一门研究小东西行为的物理学领域(想想原子和更小的东西)。由于这是量子计算机的基础,这就是我们调查的起点。
曾经有一段时间,报纸上说只有十二个人理解相对论的理论。我不相信真有这样的时候。可能有一个人理解了,因为他是唯一一个在写论文之前就理解了的人。但是在人们读了这篇论文之后,许多人以某种方式理解了相对论,肯定不止十二个。另一方面,我可以肯定地说,没有人理解量子力学。
—— 理查德·费曼(《物理定律的性质》,麻省理工学院出版社,1965 年)
14.1.1 量子力学,研究微观世界
物理学家长期以来一直认为整个世界是确定性的,就像我们的加密伪随机数生成器一样:如果你知道宇宙是如何运作的,如果你有一台足够大的计算机来计算“宇宙函数”,那么你所需要的只是种子(大爆炸中所包含的信息),然后你就可以从那里预测一切。是的,一切,甚至是宇宙开始后仅仅 137 亿年,你将要读到这行文字的事实。在这样一个世界中,没有随机性的余地。你所做的每个决定都是被过去事件所确定的,甚至是在你出生之前发生的事件。
虽然这种对世界的看法令许多哲学家感到困惑——“那我们真的有自由意志吗?” 他们问道——但在 1990 年代开始出现了一个有趣的物理领域,从那时起就困扰着许多科学家,我们称之为量子物理学(也称为量子力学)。原来,非常小的物体(想想原子和更小的物体)的行为往往与我们迄今使用的所谓的古典物理学观察到的和理论化的行为大不相同。在这个(亚)原子尺度上,粒子有时候似乎像波一样行动,就像不同的波可以相叠加到一起形成一个更大的波,或者在短暂的时刻互相抵消。
我们可以对电子等粒子进行的一种测量是它们的自旋。例如,我们可以测量电子是自旋向上还是向下旋转。到目前为止,还没什么太奇怪的。奇怪的是,量子力学说一个粒子可以同时处于这两种状态中,旋转上升和下降。我们称这种粒子处于量子叠加态。
这种特殊状态可以通过不同的技术手段人工诱导,具体取决于粒子的类型。一个粒子可以保持在叠加态直到我们对其进行测量;在这种情况下,粒子将坍缩成这些可能状态中的一个(自旋向上或向下)。这种量子叠加态就是量子计算机最终使用的:与其拥有可以是 1 或 0 的位,一个量子位或qubit可以同时是 0 和 1。
-
更怪异的是,量子理论认为只有当测量发生时,而不是之前,粒子才会随机决定取哪种状态(每种状态有 50%的概率被观察到)。如果这看起来很奇怪,你并不孤单。许多物理学家无法想象这在他们描绘的确定性世界中是如何工作的。爱因斯坦坚信这个新理论有问题,曾经说过“上帝不玩骰子”。然而,密码学家们很感兴趣,因为这是获得真正随机数的方法!这就是量子随机数生成器(QRNGs)通过不断将光子等粒子设置在超定态然后测量它们所做的事情。
-
物理学家还推测了如果我们的尺度上有物体时量子力学会是什么样子。这导致了薛定谔的猫的著名实验:一个盒子里的猫同时死和活,直到有人观察里面(这引发了关于什么才算观察者的许多争论)。
-
一只猫被困在一个钢制容器里,与以下装置一同(必须防止猫直接干扰):在一个盖革计数器中,有一小片放射性物质,可能在一个小时内,一个原子会衰变,但也有可能一个都不衰变;如果发生衰变,计数器管就会放电,通过继电器释放一个锤子,破坏一小瓶氰化氢。如果一个小时都让这整个系统自己,而与此同时没有原子衰变,那么我们会说猫还活着。第一个原子的衰变就会毒死它。整个系统的Ψ-函数将通过将活猫和死猫(原谅这个表达)混合或平均分布来表达这一点。
-
—艾尔温·薛定谔(“量子力学中的现状”,1935 年)
-
这一切对我们来说都是非常不直观的,因为我们在日常生活中从未遇到过量子行为。现在,让我们再增加一些怪异吧!
-
有时粒子相互作用(例如相互碰撞)并最终处于强烈相关状态,其中描述一个粒子而不包括其他粒子是不可能的。这种现象被称为量子纠缠,它是量子计算机性能提升的秘密之一。比如说,如果两个粒子被纠缠在一起,那么当其中一个被测量时,两个粒子都会坍缩,其中一个的状态被完全地与另一个的状态相关联。好了,那太令人困惑了。让我们举个例子:如果两个电子被纠缠在一起,然后测量其中一个发现它自旋向上,那么我们知道另一个此时是自旋向下的(但在第一个被测量之前是不知道的)。此外,任何这样的实验结果都是一样的。
这很难相信,但更令人震惊的是,已经证明纠缠甚至可以在非常长的距离上起作用。爱因斯坦、波多尔斯基和罗森曾经争论说量子力学的描述是不完整的,很可能缺少隐藏变量,这将解释纠缠(也就是说,一旦粒子分开,它们就知道它们的测量结果将是什么)。
爱因斯坦、波多尔斯基和罗森还描述了一个思想实验(EPR 悖论,以他们姓氏的首字母命名),在这个实验中,两个纠缠粒子被分开了很大的距离(想象一下光年的距离),然后几乎同时被测量。根据量子力学,对其中一个粒子的测量会立即影响另一个粒子,这是不可能的,因为根据相对论的理论,没有信息可以传播得比光速更快(因此产生了悖论)。这个奇怪的思想实验就是爱因斯坦著名地称之为“远距离的诡异作用”。
约翰·贝尔后来提出了一个被称为贝尔定理的概率不等式;如果该定理被证明为真,将证明 EPR 悖论的作者提到的隐藏变量的存在。这个不等式后来在实验中被违反(很多很多次),足以让我们相信纠缠是真实的,排除了任何隐藏变量的存在。
今天,我们说对纠缠粒子的测量会导致粒子相互协调,这就绕过了相对论的预测,即通信不能比光速更快。事实上,试着想想你如何利用纠缠来设计一个通信渠道,你会发现这是不可能的。然而,对于密码学家来说,远距离的诡异作用意味着我们可以开发新颖的方法来进行密钥交换;这个想法被称为量子密钥分发(QKD)。
想象一下将两个纠缠粒子分发给两个同行:然后他们测量各自的粒子以开始形成相同的密钥(因为测量一个粒子会给你关于另一个粒子的测量信息)?量子密钥分发的概念更加吸引人的地方在于不可克隆定理,该定理指出你不能被动地观察这样的交换并创建被发送在通道上的粒子的精确副本。然而,这些协议容易受到简单的中间人攻击,并且在没有已经有一种验证数据的方法的情况下几乎是无用的。这个缺陷导致一些密码学家如布鲁斯·施奈尔声称“量子密钥分发作为一种产品没有未来”。
至于量子物理,这就是我对密码学书籍的讲解。如果你不相信刚刚读到的所有奇异的事情,那你并不孤单。在他的书《工程师的量子力学》中,里昂·范·多姆伦写道:“物理学最终采用了量子力学,并不是因为它似乎是最合乎逻辑的解释,而是因为无数的观察使其不可避免。”
14.1.2 从量子计算机诞生到量子霸权
1980 年,量子计算的概念诞生了。是保罗·贝尼奥夫首次描述了量子计算机可能的样子:一台由量子力学最近几十年的观察结果构建的计算机。同年晚些时候,保罗·贝尼奥夫和理查德·费曼认为,这是模拟和分析量子系统的唯一方法,而不受经典计算机的限制。
仅仅 18 年后,IBM 首次演示了在实际量子计算机上运行的量子算法。快进到 2011 年,量子计算机公司 D-Wave Systems 宣布推出了第一台商用量子计算机,推动整个行业向前迈进,致力于创建第一台可扩展的量子计算机。
目前仍然有很长的路要走,而且有用的量子计算机还没有实现。在撰写本文时(2021 年),最近一个引人注目的成果是谷歌声称在 2019 年用一台 53 量子比特的量子计算机实现了量子霸权。量子霸权意味着,首次有一台量子计算机做到了经典计算机无法做到的事情。在 3 分 20 秒内,它完成了一些分析,而这些分析如果由经典计算机完成,需要大约 1 万年的时间。也就是说,在你激动得太早之前,它在一个无用的任务上胜过了经典计算机。然而,这是一个令人难以置信的里程碑,人们只能想象这将引领我们走向何方。
量子计算机基本上使用量子物理现象(如叠加和纠缠)进行计算,就像经典计算机使用电来进行计算一样。量子计算机不使用比特,而是使用量子比特或qubit,可以通过量子门转换它们以设置特定值或使它们处于叠加状态,甚至是纠缠状态。这在某种程度上类似于经典计算机电路中使用门的方式。计算完成后,可以测量量子比特以经典方式解释它们——作为 0 和 1。此时,可以进一步使用经典计算机解释结果,以完成有用的计算。
一般来说,N个纠缠的量子比特包含等价于 2^N 个经典比特的信息。但是,在计算结束时测量量子比特只会给出N个 0 或 1。因此,并不总是清楚量子计算机如何帮助,量子计算机只对有限数量的应用程序有用。随着人们找到巧妙的方法利用它们的力量,它们可能会变得越来越有用。
今天,你已经可以在家中舒适地使用量子计算机了。像 IBM 量子计算(quantum-computing.ibm.com)这样的服务允许你构建量子电路并在托管在云中的真实量子计算机上执行。当然,目前(2021 年初)这类服务相当有限,只有少量的量子比特可用。但是,创建自己的电路并等待其在真实量子计算机上运行的体验令人叹为观止,而且这一切都是免费的。
14.1.3 Grover 和 Shor 算法对密码学的影响
不幸的是,正如我之前所说的,量子计算机并不适用于每一种类型的计算,因此,它们不是我们经典计算机的更强大的替代品。但是,它们究竟有什么用处呢?
1994 年,在量子计算机的概念只是一种思想实验的时候,彼得·肖尔提出了一个解决离散对数和因子化问题的量子算法。肖尔有洞察力地认为量子计算机可以用于快速计算与密码学中看到的难题相关的问题的解决方案。事实证明,存在一种高效的量子算法,可以帮助找到一个周期,使得f(x + period) = f(x)对于任何给定的x成立。例如,找到值为period的周期,使得 g^(x+period) = g^x mod N。这反过来导致可以有效地解决因子化和离散对数问题的算法,从而影响了像 RSA(在第六章中讨论)和 Diffie-Hellman(在第五章中讨论)这样的算法。
Shor 算法对非对称密码学具有毁灭性影响,因为今天大多数使用的非对称算法依赖于离散对数或因子化问题——实际上,你在本书中看到的大部分内容都是如此。你可能会认为离散对数和因子化仍然是困难的数学问题,而我们可以(也许)增加算法参数的大小以提升其抵抗量子计算机的能力。不幸的是,由 Bernstein 等人于 2017 年证明,尽管提高参数是有效的,但这是极不切实际的。研究估计,将 RSA 的参数增加到 1 TB 可以使其对抗量子计算机。说实话,这是不现实的。
Shor 算法颠覆了部署的公钥加密技术的基础:RSA 和有限域和椭圆曲线上的离散对数问题。长期保密的文件,如患者医疗记录和国家机密,必须在多年内保证安全性,但今天使用 RSA 或椭圆曲线加密的信息,存储到量子计算机可用时,将像今天的 Enigma 加密消息一样容易解密。
—PQCRYPTO:长期安全后量子系统的初始建议(2015)
对于对称加密来说,情况要好得多。Grover 算法于 1996 年由 Lov Grover 提出,作为优化无序列表中搜索的一种方式。在经典计算机上,对 N 个项目的无序列表进行搜索平均需要 N/2 次操作;而在量子计算机上,只需要 √N 次操作。这是相当大的加速!
Grover 算法是一种非常多才多艺的工具,可以在许多密码学领域应用,例如提取密码的对称密钥或在哈希函数中找到碰撞。要搜索一个 128 位密钥,Grover 算法在量子计算机上运行的操作次数为 2⁶⁴,而在经典计算机上为 2¹²⁷。对于我们所有的对称加密算法来说,这是一个相当可怕的说法,但我们只需将安全参数从 128 位提升到 256 位,就足以抵御 Grover 的攻击。因此,如果你想保护对称加密技术免受量子计算机的攻击,你可以简单地使用 SHA-3-512 替代 SHA-3-256,使用 AES-256-GCM 替代 AES-128-GCM,等等。
总结一下,对称加密大部分情况下是安全的,而非对称加密则不然。这甚至比你一开始想象的更糟:对称加密通常需要进行密钥交换,而这一过程容易受到量子计算机的攻击。那么,这是否意味着我们所熟知的加密技术将走向终结呢?
14.1.4 后量子密码学,对抗量子计算机的防御
幸运的是,这并不是加密技术的终结。社区迅速应对量子威胁,通过组织自身并研究新旧算法,以抵御 Shor 和 Grover 的攻击。抗量子密码学领域,也被称为后量子密码学,因此诞生了。互联网上存在着不同地方的标准化努力,但最受尊敬的努力来自 NIST,该机构于 2016 年启动了后量子密码学的标准化进程。
似乎过渡到后量子密码学并不简单,因为我们当前的公钥密码算法不太可能有一个简单的“即插即用”替代方案。将需要进行大量的工作来开发、标准化和部署新的后量子密码系统。此外,这种过渡需要在任何大规模量子计算机建成之前进行,以便任何后期由量子密码分析而泄露的信息在泄露时不再敏感。因此,尽早为这种过渡做好规划是可取的。
—NIST 标准化过程的后量子密码学页面 (2016)
自 NIST 开始这个过程以来,有 82 个候选者申请,并且经过了 3 轮,将候选者名单缩减到了 7 个决赛选手和 8 个备用决赛选手(不太可能被考虑用于标准化,但如果决赛选手中的某个范式被破解了,则是一个很好的选择)。NIST 的标准化工作旨在替换最常见的非对称密码学基元,其中包括签名方案和非对称加密。后者也可以轻松地作为密钥交换基元,正如你在第六章中所学到的。
在本章的其余部分,我将介绍正在考虑标准化的不同类型的后量子密码算法,并指出你今天可以使用哪些算法。
14.2 基于哈希的签名:除了哈希函数外不需要任何东西
尽管所有实际的签名方案似乎都使用哈希函数,但存在一种方法可以构建只使用哈希函数而不使用其他东西的签名方案。更好的是,这些方案倾向于仅依赖于哈希函数的前像抗性而不是它们的碰撞抗性。这是一个相当吸引人的建议,因为应用密码学的巨大部分已经基于稳固和被充分理解的哈希函数。
现代哈希函数也能抵抗量子计算机,这使得这些基于哈希的签名方案自然而然地具有量子抗性。让我们看看这些基于哈希的签名是什么,以及它们是如何工作的。
14.2.1 一次性签名 (OTS) 使用 Lamport 签名
1979 年 10 月 18 日,莱斯利·兰波特(Leslie Lamport)发布了他的 一次性签名 (OTS) 的概念:只能用于签名一次的密钥对。大多数签名方案(部分地)依赖于单向函数(通常是哈希函数)来进行安全证明。兰波特方案的美妙之处在于,他的签名完全依赖于这样的单向函数的安全性。
假设你想要签名一个单个比特。首先,通过生成密钥对
-
生成两个随机数 x 和 y,它们将作为私钥
-
对 x 和 y 进行哈希运算得到两个摘要 h(x) 和 h(y),你可以将其公开为公钥
要将一个比特设为 0,揭示你的私钥的x部分;要将一个比特设为 1,揭示y部分。要验证一个签名,只需对其进行哈希,以检查其是否与公钥的正确部分匹配。我在图 14.1 中说明了这一点。

图 14.1 Lamport 签名是一种仅基于哈希函数的一次性签名(OTS)。为了生成一个可以签署一个比特的密钥对,生成两个随机数,这将是你的私钥,并分别对这两个数进行哈希,以产生你的公钥的两个摘要。要将一个比特设为 0,揭示第一个随机数;要将一个比特设为 1,揭示第二个随机数。
签署一个比特并不那么有用,你说。没问题;Lamport 签名通过为每个比特创建更多的秘密对,一个比特一个秘密对,来对更大的输入进行签名(参见图 14.2)。显然,如果你的输入大于 256 位,你首先会对其进行哈希,然后再对其进行签名。

图 14.2 为了生成一个 Lamport 签名密钥对,可以签署一个n位消息,生成 2n个随机数,这将是你的私钥,并分别对这些数进行哈希,以产生你的公钥的 2n个摘要。要签名,遍历秘密和n位的对,揭示第一个元素以将一个比特设为 0,或揭示第二个元素以将一个比特设为 1。
这种方案的一个主要限制是你只能使用它签名一次;如果你用它签名两次,你最终会授权别人混合这两个签名来伪造其他有效的签名。我们可以通过天真地生成大量一次性密钥对而不是单个密钥对来改善这种情况,然后确保在使用后丢弃一个密钥对。这不仅使你的公钥变得像你认为你可能会使用的签名数量一样大,而且还意味着你必须跟踪你使用过的密钥对(或者更好的是,丢弃你使用过的私钥)。例如,如果你知道你将要使用具有 256 位输出大小的哈希函数签署最大为 256 位的 1,000 个消息,那么你的私钥和公钥都必须是 1000 × (256 × 2 × 256)位,约为 16 兆字节。对于只有 1,000 个签名来说,这是相当多的。
今天提出的大多数基于哈希的签名方案都建立在 Lamport 创造的基础上,以允许更多的签名(有时是几乎无限量的签名),无状态的私钥(尽管一些提出的方案仍然是有状态的),以及更实用的参数大小。
14.2.2 Winternitz 一次签名(WOTS)的较小密钥
在 Lamport 发表几个月后,斯坦福大学数学系的 Robert Winternitz 提出了发布一个秘密的哈希的哈希的哈希* h ( h (... h ( x *)))= * h ^ w ( x *)而不是发布多个秘密的多个摘要,以优化私钥的大小(参见图 14.3)。这个方案被称为作者之后的 Winternitz 一次签名(WOTS)。
例如,选择 w = 16 允许你签署 16 个不同的值,换句话说,4 位输入。你首先生成一个作为私钥的随机值 x,并将其哈希 16 次以获得你的公钥,h¹⁶(x)。现在想象一下,你想要签署位 1001(十进制中的 9);你发布哈希的第九次迭代,h⁹(x)。我在图 14.3 中进行了说明。

图 14.3 Winternitz 一次签名(WOTS)方案通过仅使用一个被迭代哈希的秘密来优化 Lamport 签名,以获得许多其他秘密和最终的公钥。揭示不同的秘密允许签署不同的数字。
花几分钟时间来理解这个方案是如何工作的。你看到其中有什么问题吗?一个主要问题是这个方案允许伪造签名。想象一下,你看到别人对位 1001 的签名,根据我们之前的例子,这将是 h⁹(x)。你可以简单地对其进行哈希以检索任何其他迭代,比如 h¹⁰(x) 或 h¹¹(x),这将为你提供位 1010 或 1011 的有效签名。这可以通过在消息后添加一个短的认证标签来规避,你也必须对其进行签名。我在图 14.4 中进行了说明。为了说服自己这解决了伪造问题,请尝试从另一个签名中伪造一个签名。

图 14.4 WOTS 使用额外的签名密钥来验证签名,以防篡改。它的工作原理是:在签名时,第一个私钥用于签署消息,第二个私钥用于签署消息的补码。很明显,在所示的任何情况下,篡改签名都不会导致新的有效签名。
14.2.3 使用 XMSS 和 SPHINCS+ 的多次签名
到目前为止,你已经看到了只使用哈希函数签名的方法。虽然 Lamport 签名是有效的,但它们具有较大的密钥大小,因此 WOTS 通过减小密钥大小改进了这一点。然而,这两种方案仍然不具有良好的可扩展性,因为它们都是一次性签名(重复使用密钥对会破坏方案),因此,它们的参数会随着你认为需要的签名数量的增加而线性增加。
一些方案允许密钥对重复使用几次(而不是一次)。这些方案被称为少次签名(FTS),如果重复使用次数太多,将会破坏,从而允许伪造签名。FTS 依赖于从秘密池中重复使用相同组合秘密的低概率。这是对一次性签名的小改进,允许减少密钥重用的风险。但我们可以做得更好。
在这本书中,你学到的一个技术是将许多事物压缩成一个事物的技术是什么?答案是默克尔树。正如你可能还记得第十二章所述,默克尔树是一种数据结构,为诸如我的数据是否在这个集合中之类的问题提供简短的证明。在 1990 年代,提出默克尔树的默克尔还发明了一种基于哈希函数的签名方案,将多个一次性签名压缩成一个默克尔树。
这个想法非常简单:你的树的每个叶子都是一次性签名的哈希,而根哈希可以用作公钥,将其大小减小到哈希函数的输出大小。要签名,你选择一个之前未使用过的一次性签名,然后按照第 14.2.2 节中的说明应用它。签名是一次性签名,以及证明它属于你的默克尔树的默克尔证明(所有邻居)。这个方案显然是有状态的,因为人们应该小心不要在树中重复使用一次性签名之一。我在图 14.5 中说明了这一点。

图 14.5 默克尔签名方案是一种有状态的基于哈希的算法,利用默克尔树将许多 OTS 公钥压缩为更小的公钥(根哈希)。树越大,它可以产生的签名就越多。请注意,签名现在具有成员证明的开销,这是一些邻居节点,允许验证签名的相关 OTS 是否属于树。
扩展默克尔签名方案(XMSS),在 RFC 8391 中标准化,旨在通过向默克尔方案添加一些优化来实现默克尔签名的生产。例如,为了生成能够签署N条消息的密钥对,你必须生成N个 OTS 私钥。虽然公钥现在只是一个根哈希,但你仍然必须存储N个 OTS 私钥。XMSS 通过使用种子和树中的叶子位置确定性地生成树中的每个 OTS,从而减小了你持有的私钥的大小。这样,你只需要将种子存储为私钥,而不是所有 OTS 私钥,并且可以快速从树中的位置和种子重新生成任何 OTS 密钥对。为了跟踪上次使用的叶子/OTS,私钥还包含一个计数器,每次用于签名时都会递增。
提到这一点,Merkle 树中只能容纳有限的 OTS。树越大,重建树以签署消息就越耗时(因为您需要重建所有叶子以生成 Merkle 证明)。树越小,在签名时需要重建的 OTS 私钥就越少,但这显然违背了初衷:我们现在又回到了有限数量的签名。解决方案是使用一个较小的树,其中叶子中的 OTS 不用于签署消息,而是用于签署其他 OTS 的 Merkle 树的根哈希。这将我们的初始树转换为超树——树的树——是 XMSS 的一种变体称为 XMSS^(MT)。对于 XMSS^(MT),仅需要基于相同的技术重建与 OTS 路径有关的树。我在图 14.6 中进行了说明。

图 14.6 XMSS^(MT) 有状态基于哈希的签名方案使用多个树来增加方案支持的签名数量,同时减少密钥生成和签名时间的工作量。只有当它们在路径到包含用于签署消息的 OTS 的最终叶子中使用时,每个树才会被确定性地生成。
请注意,XMSS 和 XMSS^(MT) 的状态性在某些情况下可能不是问题,但总的来说不是一种理想的属性。必须跟踪一个计数器是反直觉的,因为我们不希望从主流签名方案的用户那里期待这种行为。这种实践的变化可能会导致 OTS 的重用(从而导致签名伪造),如果不当使用,例如,回滚到文件系统的先前状态或在多个服务器上使用相同的签名密钥可能导致超树中的相同路径两次用于签署消息。
为了解决 XMSS 最大的缺点之一(其状态性)并展现类似于我们习惯的签名方案的接口,SPHINCS+ 签名方案作为 NIST 的后量子密码竞赛的一部分提出。这个无状态签名方案通过三个主要变化来增强 XMSS^(MT):
-
两次签署相同的消息会导致相同的签名。与第七章介绍的 EdDSA 类似,超树中使用的路径是根据私钥和消息确定性地派生的。这确保了两次签署相同的消息会导致相同的 OTS,从而导致相同的签名;由于使用了私钥,攻击者也无法预测您在签署其他人消息时将采取哪条路径。
-
使用更多的树. XMSS^(MT)通过追踪上次使用的 OTS 来避免两次重复使用相同的 OTS。由于 SPHINCS+的整个目的是避免追踪状态,因此它需要在选择伪随机路径时避免冲突。为此,SPHINCS+简单地使用了更多的 OTS,减少了两次重复使用相同 OTS 的概率。由于 SPHINCS+还使用了超树,这意味着更多的树。
-
使用少次签名(FTS)。由于方案的安全性是基于两次重复使用相同路径的概率,因此 SPHINCS+还用我之前提到的 FTS 替换了用于签署消息的最终 OTS。这样,重复使用相同路径来签署两个不同的消息仍然不会直接导致签名方案的破坏。
虽然 SPHINCS+正在考虑在 NIST 后量子密码竞赛中进行标准化,但它并不是主要的竞争者。SPHINCS+不仅速度慢,而且与提议的替代方案(如基于格的方案,在本章后面将会介绍)相比,其签名大小也较大。基于状态的哈希签名方案(如 XMSS)提供更快的速度和更好的签名大小(小于 3 KB,而 SPHINCS+的最小签名大小为 8 KB)。(在公钥大小方面,这两种方案提供了与预量子签名方案(如 ECDSA 和 Ed25519)类似的大小。)由于更现实的参数大小和良好理解的安全性,NIST 在 SP 800-208 中推荐了 XMSS 作为早期标准,“基于状态的哈希签名方案的建议”。
接下来,让我们来看看另外两种构建抗量子密码原语的方法。温柔的警告:它们的数学内容要多得多!
14.3 基于格的密码学中的更短密钥和签名
大量的后量子密码方案基于格,这是一种您将在本节中了解的数学结构。NIST 后量子密码竞赛本身已经选定了基于格的方案作为其半决赛选手的一半。这使得基于格的密码学成为最有可能赢得并从 NIST 获得标准的范式。在本节中,我将告诉您关于两种基于格的算法:Dilithium,一种签名方案,以及 Kyber,一种公钥加密原语。但在此之前,让我们先了解一下什么是格。
14.3.1 什么是格?
首先,基于格的概念可能不是你想象的那样。以 RSA 为例(第六章讨论过),我们说 RSA 是基于因子分解问题的。这并不意味着我们在 RSA 中使用了因子分解,而是意味着因子分解是你攻击 RSA 的方法,并且因为因子分解很难,所以我们说 RSA 是安全的。基于格的密码系统也是如此:格是具有困难问题的结构,只要这些问题保持困难,这些密码系统就是安全的。
说到这里,什么是格?嗯,它就像一个向量空间,但带有整数。如果你不记得向量空间是什么,那就是所有可以使用以下内容创建的向量的集合:
-
基—一组向量;例如,(0,1) 和 (1,0)。
-
向量之间的运算—向量可以相加;例如,(0,1) + (1,0) = (1,1)。
-
标量运算—向量可以乘以我们称为标量的东西;例如,3 × (1,2) = (3,6)。
在我们的例子中,向量空间包含所有可以表示为基的线性组合的向量,这意味着任何可以写为a × (0,1) + b × (1,0)的向量。例如,0.5 × (0,1) + 3.87 × (1,0) = (3.87,0.5)在我们的向量空间中,99 × (0,1) + 0 × (1,0) = (0,99)也是如此,等等。
格是一个向量空间,其中涉及的所有数字都是整数。是的,在密码学中,我们喜欢整数。我在图 14.7 中说明了这一点。

图 14.7 左侧绘制了两个向量的基。通过取这两个向量的所有可能的整数线性组合可以形成一个格点(中间图)。所得的格点可以被解释为空间中永远重复的点的模式(右图)。
在格空间中存在几个众所周知的难题,针对这些问题,我们有解决方案。这些算法通常是我们能想到的最好的,但这并不一定意味着它们是高效的,甚至是实用的。因此,这些问题至少被认为是困难的,直到找到更高效的解决方案。最知名的两个困难问题如下。(我在图 14.8 中说明了这两个问题。)
-
最短向量问题(SVP)—回答这个问题:你的格点中最短的非零向量是什么?
-
最近向量问题(CVP)—给定一个不在格点上的坐标,找到该坐标在格点上最近的点。

图 14.8 展示了密码学中使用的两个主要格问题:最短向量问题(SVP)和最近向量问题(CVP)
通常,我们使用像 LLL(Lenstra–Lenstra–Lovász 算法)或 BKZ(Block-Korkine-Zolotarev 算法)这样的算法来解决这两个问题(CVP 可以归约为 SVP)。这些算法会缩减格点的基,意味着它们试图找到一组比给定的更短的向量,并且能够产生完全相同的格点。
14.3.2 含有错误的学习(LWE),密码学的基础?
在 2005 年,Oded Regev 提出了含有错误的学习(LWE)问题,它成为了许多密码方案的基础,包括本章中的一些算法。在继续之前,让我们看看 LWE 问题是什么。让我们从以下方程式开始,它们是相同整数s[0]和s[1]的线性组合:
-
5 s[0] + 2 s[1] = 27
-
2 s[0] + 0 s[1] = 6
我们知道,通过使用高斯消元算法,只要我们有足够多的这些方程,我们就可以快速有效地学到s[0]和s[1]是什么。现在有趣的是,如果我们给这些方程添加一些噪音,问题就变得更加困难:
-
5 s[0] + 2 s[1] = 28
-
2 s[0] + 0 s[1] = 5
当你增加所涉及的数字的大小和s[i]的数量时,通过更多嘈杂的方程可能并不太难找到答案,但一旦增加了这些因素,问题就变得更加困难。
这本质上就是 LWE 问题,尽管通常用向量来表述。想象一下,你有一个带有模一些大数的秘密向量s。给定相同大小的任意数量的随机向量a[i]和计算a[i]s + e[i]的结果,其中e[i]是一个随机小误差,你能找到值s吗?
注意 对于两个向量v和w,可以使用点积计算乘积vw,即每对坐标的乘积之和。让我们看一个例子:如果v = (v[0], v[1]),而w = (w[0], w[1]),那么vw = v[0] × w[0] + v[1] × w[1]。
例如,如果我使用秘密s = (3,6)并给你随机向量a[0] = (5,2)和a[1] = (2,0),我得到了我在例子中开始的方程。正如我之前所说,基于格的方案实际上并不使用格;相反,它们被证明是安全的,如果 SVP 保持困难(对于某些定义的困难)。只有在我们将以前的方程以矩阵形式写出时,才能看到归约,如图 14.9 所示。

图 14.9 学习中的错误问题(LWE)被认为是基于格的构造,因为存在到格问题的归约:CVP。换句话说,如果我们可以找到 CVP 的解,那么我们就可以找到 LWE 问题的解。
这种矩阵形式很重要,因为大多数基于 LWE 的方案都是以这种形式表达和解释的。花几分钟时间复习矩阵乘法。另外,如果你还没有注意到,我使用了一些常见的符号技巧,这些技巧对于阅读涉及矩阵和向量的方程非常有帮助:两者都以粗体字体书写,矩阵始终是大写字母。例如,A是矩阵,a是向量,b只是一个数字。
注意 LWE 问题存在几个变体(例如,环-LWE 或模-LWE 问题),它们基本上是相同的问题,但坐标位于不同类型的群中。由于它们的紧凑性和解锁的优化,通常更喜欢这些变体。LWE 变体之间的差异不影响接下来的解释。
现在你知道 LWE 问题是什么了,让我们学习一些基于它的后量子密码学:代数格密码套件(CRYSTALS)。 方便的是,CRYSTALS 包含两个密码学原语:一个称为 Kyber 的密钥交换和一个称为 Dilithium 的签名方案。
14.3.3 Kyber,基于格的密钥交换
两个 NIST 最终方案密切相关: CRYSTALS-Kyber 和 CRYSTALS-Dilithium,这两个方案都是来自同一研究团队的候选方案,都基于 LWE 问题。 Kyber 是一个可以用作密钥交换原语的公钥加密原语,在本节中我将解释。 Dilithium 是一个签名方案,我将在下一节中解释。 还要注意,由于这些算法仍在变化中,我将只写出这两个方案背后的思想和直觉。
首先,让我们假设所有操作都在模一个大数 q 的整数群中进行。 我们还假设错误和私钥是从以 0 为中心的小范围内(均匀随机选择)采样的。 具体来说,错误范围是范围 [–B, B],其中 B 远远小于 q。 这很重要,因为某些项需要比某个值小才能被视为错误。
要生成私钥,只需生成一个随机向量 s,其中每个系数都在错误范围内。 公钥的第一部分是相同大小的随机向量 a[i] 的列表,第二部分是相关的噪声点乘列表 t[i] = a[i] s + e[i] mod q。 这正是我们之前学到的 LWE 问题。 对于其余部分很重要的是,我们可以用矩阵来重写这个问题:
t = As + e
其中矩阵 A 包含随机向量 a[i] 作为行,而错误向量 e 包含单个错误 e[i]。
要使用 Kyber 进行密钥交换,我们使用方案加密一个 1 位的对称密钥(是的,只有一个位!)。 这类似于您在第六章中看到的 RSA 密钥封装机制。 以下四个步骤显示了加密的工作原理:
-
生成一个短暂的私钥向量 r(系数在错误范围内),及其关联的短暂公钥 rA + e[1] 与一些随机错误向量 e[1],使用对等方的 A 矩阵作为公共参数。 注意,矩阵乘法在右侧执行,这涉及将向量 r 与 A 的列相乘,而不是计算 Ar(向量 r 与 A 的行的乘积)。 这是一个细节,但对解密步骤的工作是必要的。
-
我们通过将其与 q/2 相乘将消息向左移动,以避免小错误影响我们的消息。 注意,q/2 模 q 通常意味着 q 乘以 2 模 q 的倒数,但这里它只是意味着 q/2 的最接近的整数。
-
用我们的临时私钥和对等方的公钥的点积计算共享密钥。
-
通过将其添加到共享密钥以及随机错误e[2]来加密您的(移位的)消息。这将产生一个密文。
执行完这些步骤后,我们可以将临时公钥和密文都发送给另一个对等方。收到临时公钥和密文后,我们可以按照以下步骤解密消息:
-
通过计算您的秘密与收到的临时公钥的点积来获得共享密钥。
-
将共享密钥从密文中减去(结果包含移位的消息和一些错误)。
-
通过将其除以q/2,将消息移回原始位置,有效地消除错误。
-
如果消息接近q/2,则为 1,否则为 0。
当然,1 位是不够的,所以当前方案采用不同的技术来克服这个限制。我在图 14.10 中总结了所有三个算法。

图 14.10 Kyber 公钥加密方案。请注意,在加密和解密过程中,共享密钥几乎相同,因为r、s和错误远小于q/2。因此,解密的最后一步(除以q/2,可以看作是向右的位移)消除了两个共享密钥之间的任何差异。请注意,所有操作都是模q进行的。
在实践中,对于密钥交换,您加密到对等方的公钥的消息是一个随机密钥。然后,结果是从密钥交换的记录和对等方的公钥、您的临时密钥和密文中确定性地派生出来的。
Kyber 的推荐参数导致公钥和密文约为 1 千字节,这比我们使用的预量子方案要大得多,但对于大多数用例来说仍然是实用的范畴。虽然时间会告诉我们是否可以进一步减少这些方案的通信开销,但迄今为止,量子后的韵律似乎与更大的尺寸相呼应。
14.3.4 Dilithium,一个基于格的签名方案
我将解释的下一个方案Dilithium也是基于 LWE 的,是 Kyber 的姊妹候选方案。与我们见过的其他类型的签名(如第七章中的 Schnorr 签名)一样,Dilithium 基于一个零知识证明,通过 Fiat-Shamir 技巧使其非交互式。
对于密钥生成,Dilithium 与先前的方案类似,只是我们将错误作为私钥的一部分保留。我们首先生成两个作为私钥的随机向量s[1]和s[2],然后计算公钥为t = As[1] + s[2],其中A是以与 Kyber 类似的方式获得的矩阵。公钥是t和A。请注意,我们将错误s[2]视为私钥的一部分,因为我们需要在每次签署消息时重复使用它(不像在 Kyber 中,错误可以在密钥生成步骤之后丢弃)。
要签署,我们创建一个 sigma 协议,然后通过费亚特-沙米尔转换将其转换为非交互式、零知识证明,这类似于第七章中 Schnorr 识别协议转换为 Schnorr 签名的方式。交互式协议如下:
-
证明者通过发送Ay[1] + y[2]对两个随机向量y[1]和y[2]进行承诺。
-
在收到此提交后,验证者会回复一个随机挑战c。
-
然后,证明者计算两个向量z[1] = c s[1] + y[1]和z[2] = c s[2] + y[2],并仅在它们是小值时将它们发送给验证者。
-
验证者检查Az[1] + z[2] – c t和Ay[1] + y[2]是否为相同的值。
费亚特-沙米尔技巧通过让证明者从要签名的消息和已承诺的Ay[1] + y[2]值的哈希中生成挑战,取代了第 2 步中验证者的角色。我在图 14.11 中总结了这个转换,使用了第七章中类似的图表。

图 14.11 Dilithium 签名是对秘密向量s的知识证明,通过费亚特-沙米尔转换变为非交互式。左侧的图表显示了交互式证明协议,而右侧的图表显示了一个非交互式版本,其中挑战被计算为y和要签名的消息的承诺。
再次强调,这只是对签名方案的粗略简化。实际上会使用更多的优化来减少密钥和签名的大小。通常,这些优化会尝试通过从较小的随机值确定性生成随机数据来减少任何随机数据,并通过自定义方法压缩非随机数据来减少非随机数据,不一定通过已知的压缩算法。由于 LWE 的独特结构,还有许多其他可能的优化。
在推荐的安全级别下,Dilithium 提供约 3 KB 的签名和不到 2 KB 的公钥。这显然远远超过了前量子方案的 32 字节公钥和 64 字节签名,但也比无状态基于哈希的签名要好得多。值得注意的是,这些方案仍然相当新颖,可能会找到更好的算法来解决 LWE 问题,潜在地增加公钥和签名的大小。同时,我们也可能找到更好的技术来减小这些参数的大小。总的来说,量子抗性很可能总是伴随着尺寸成本。
后量子密码学并不仅仅是这些;NIST 后量子密码学竞赛还有许多基于不同范式的构造。NIST 已宣布将于 2022 年发布初步标准,但我预计这个领域将继续快速发展,至少在后量子计算机被视为合法威胁的情况下。虽然仍有许多未知因素,但这也意味着有很多令人兴奋的研究空间。如果你对此感兴趣,我建议查看 NIST 报告(nist.gov/pqcrypto)。
14.4 我需要恐慌吗?
总结一下,如果量子计算机得以实现,对于密码学来说将是一个巨大的挑战。这里的要点是什么?你需要放弃手头的一切,转向后量子算法吗?嗯,事情并不那么简单。
询问任何专家,你会得到不同类型的答案。对于一些人来说,这可能是 5 到 50 年的事情;对于其他人来说,这永远不会发生。量子计算研究所所长米歇尔·莫斯卡估计“到 2026 年有 1/7 的机会破解 RSA-2048,到 2031 年有 1/2 的机会”,而法国国家科学研究中心研究员米哈伊尔·迪亚科诺夫公开表示“我们能否学会控制定义这种系统量子状态的超过 10³⁰⁰个连续可变参数?我的答案很简单。不,永远不会。”虽然物理学家而非密码学家更了解情况,但他们仍可能被激励夸大自己的研究以获取资金。作为一个非物理学家,我只能说我们应该对不寻常的声明保持怀疑,同时做最坏的准备。问题不是“它会起作用吗?”;而是“它会扩展吗?”
要使可扩展的量子计算机(可能破坏密码学)成为现实存在许多挑战;最大的挑战似乎在于难以减少或纠正的噪声和错误量。德克萨斯大学计算机科学家斯科特·亚伦森将其描述为“你试图建造一艘保持不变的船,即使其中的每块木板都腐烂并需要更换。”
那么 NSA 说了什么呢?人们需要记住政府对保密性的需求往往超过个人和私营公司的需求。认为政府可能希望将一些绝密数据保密超过 50 年并不是疯狂的想法。然而,这让许多密码学家感到困惑(例如,参见 Neal Koblitz 和 Alfred J. Menezes 的“A Riddle Wrapped In An Enigma”),他们一直在思考为什么我们要保护自己免受尚不存在或可能永远不会存在的东西的威胁。
无论如何,如果你真的担心并且你的资产的保密性需要长时间保持,增加你正在使用的每个对称加密算法的参数并不是疯狂的,而且相对容易。话虽如此,如果你正在进行密钥交换以获得 AES-256-GCM 密钥,那么非对称加密部分仍然容易受到量子计算机的攻击,仅保护对称加密是不够的。
对于非对称加密,现在还为时过早真正知道什么是安全的。最好等待 NIST 竞赛结束,以获得更多的密码分析,进而对这些新算法更有信心。
目前,已经提出了几种后量子密码系统,包括基于格的密码系统、基于码的密码系统、多元密码系统、基于哈希的签名等。然而,对于大多数这些提议,需要进一步研究以获得对其安全性(特别是针对拥有量子计算机的对手)更多的信心,并改进其性能。
—NIST 后量子密码学提案征集(2017)
如果你太不耐烦,无法等待 NIST 竞赛结果,你可以做的一件事是在你的协议中同时使用当前方案和后量子方案。例如,你可以使用 Ed25519 和 Dilithium 交叉签署消息,换句话说,附加一条消息,带有来自两种不同签名方案的两个签名。如果 Dilithium 被破解,攻击者仍然需要破解 Ed25519,如果量子计算机真的存在,那么攻击者仍然拥有无法伪造的 Dilithium 签名。
注意:这就是 Google 在 2018 年以及 2019 年与 Cloudflare 一起所做的,尝试在 Google Chrome 用户和 Google 以及 Cloudflare 的服务器之间的 TLS 连接中使用混合密钥交换方案。混合方案是 X25519 和一个后量子密钥交换(2018 年的 New Hope,2019 年的 HRSS 和 SIKE)的混合,其中当前密钥交换和后量子密钥交换的输出被混合在一起进入 HKDF 以产生一个共享密钥。
最后,我将再次强调基于哈希的签名已经得到充分研究和理解。尽管它们存在一些开销,像 XMSS 和 SPHINCS+这样的方案现在就可以使用,而且 XMSS 具有即用的标准(RFC 8391 和 NIST SP 800-208)。
概要
-
量子计算机基于量子物理学,可以为特定的计算提供非常大的加速。
-
并非所有算法都可以在量子计算机上运行,也不是所有算法都能与经典计算机竞争。令密码学家担心的两个显著算法是
-
Shor 算法可以高效地解决离散对数问题和因子分解问题。它破坏了大多数当今的非对称密码学。
-
Grover 算法可以有效地搜索 2¹²⁸个值的空间中的密钥或值,影响大多数具有 128 位安全性的对称算法。将对称算法的参数提升到提供 256 位安全性足以抵御量子攻击。
-
-
后量子密码学领域旨在寻找新的加密算法来替代今天的非对称加密原语(例如,非对称加密、密钥交换和数字签名)。
-
NIST 于 2016 年启动了后量子密码学标准化工作。目前有七个决赛选手,该工作现已进入最后一轮选拔阶段。
-
基于哈希的签名是仅基于哈希函数的签名方案。两个主要标准是 XMSS(一种有状态的签名方案)和 SPHINCS+(一种无状态的签名方案)。
-
基于格的密码学是有希望的,因为它提供了较短的密钥和签名。最有前途的两个候选方案基于 LWE 问题:Kyber 是一种非对称加密和密钥交换原语,而 Dilithium 是一种签名方案。
-
还存在其他后量子方案,并作为 NIST 后量子密码学竞赛的一部分提出。这些包括基于代码理论、同源性、对称密钥密码学和多项式的方案。NIST 的竞赛计划于 2022 年结束,这仍然为发现新攻击或优化留下了很大的空间。
-
尚不清楚量子计算机何时能够足够高效地破坏密码学,或者它们是否能够达到那个水平。
-
如果您有长期保护数据的需求,应考虑过渡到后量子密码学:
-
将所有对称加密算法的使用升级为提供 256 位安全性的参数(例如,从 AES-128-GCM 迁移到 AES-256-GCM,从 SHA-3-256 迁移到 SHA-3-512)。
-
使用混合方案将后量子算法与前量子算法混合。例如,始终使用 Ed25519 和 Dilithium 对消息进行签名,或始终使用 X25519 和 Kyber 进行密钥交换(从获得的两个密钥交换输出中派生出共享密钥)。
-
使用像 XMSS 和 SPHINCS+这样基于哈希的签名算法,这些算法经过了深入研究和广泛理解。XMSS 的优势在于已经被 NIST 标准化和批准。
-
第十五章:这就是全部吗?下一代密码学
本章涵盖
-
通过安全多方计算(MPC)摆脱信任的第三方
-
允许他人对加密数据进行操作通过全同态加密(FHE)
-
通过零知识证明(ZKPs)隐藏程序执行的部分
我开始写这本书的时候,认为大部分章节读完的读者也会对现实世界密码学的未来感兴趣。虽然您正在阅读一个侧重于当今使用的应用和实用书籍,但密码学领域正在迅速变化(例如,最近几年我们看到的加密货币)。
当您阅读本书时,许多理论密码原语和协议正在进入应用密码学领域——也许是因为这些理论原语终于找到了用例,或者是因为它们终于变得足够高效,可以用于实际应用。无论原因是什么,密码学的现实世界肯定在增长并变得更加令人兴奋。在本章中,我通过简要介绍三种原语,为您展示了未来实际密码学可能会是什么样子(也许在未来 10 到 20 年内):
-
安全多方计算(MPC)—密码学的一个子领域,允许不同参与者一起执行程序,而不必向程序透露自己的输入。
-
全同态加密(FHE)—密码学的圣杯,一种用于允许对加密数据进行任意计算的原语。
-
通用零知识证明(ZKPs)—您在第七章学到的原语,允许您证明自己知道某事而不泄露该事情,但这次更普遍地应用于更复杂的程序。
本章包含了本书中最先进和复杂的概念。因此,我建议您先浏览一下,然后转到第十六章阅读结论。当您有动力想要了解这些更高级概念的内部机制时,请回到这一章。让我们开始吧!
15.1 人越多越热闹:安全多方计算(MPC)
安全多方计算(MPC)是密码学领域的一个子领域,始于 1982 年的著名百万富翁问题。在他 1982 年的论文“用于安全计算的协议”中,Andrew C. Yao 写道:“两位百万富翁想知道谁更富有;然而,他们不想无意中获得有关对方财富的任何额外信息。他们如何进行这样的对话?”简而言之,MPC 是多个参与者一起计算程序的一种方式。但在了解更多关于这种新原语之前,让我们看看它为什么有用。
我们知道,在可信第三方的帮助下,任何分布式计算都可以轻松解决。这个可信第三方可以可能维护每个参与者输入的隐私,同时可能限制计算所透露给特定参与者的信息量。然而,在现实世界中,我们不太喜欢可信第三方;我们知道它们很难找到,并且它们并不总是遵守自己的承诺。
MPC 允许我们完全将可信第三方从分布式计算中移除,并使参与者能够自行计算计算,而不会向彼此透露各自的输入。这是通过一个加密协议完成的。考虑到这一点,在系统中使用 MPC 基本上等同于使用一个可信第三方(见图 15.1)。

图 15.1 安全多方计算(MPC)协议将一个可以通过可信第三方进行计算的分布式计算(左侧图像)转变为一个不需要可信第三方帮助的计算(右侧图像)。
请注意,您已经看到了一些 MPC 协议。阈值签名和分布式密钥生成,涵盖在第八章中,是 MPC 的示例。更具体地说,这些示例是 MPC 的一个子领域,称为阈值密码学,在近年来受到了很多关注,例如,2019 年中期 NIST 启动了阈值密码学的标准化过程。
15.1.1 私有集合交集(PSI)
另一个众所周知的 MPC 子领域是私有集合交集(PSI)的领域,它提出了以下问题:Alice 和 Bob 有一组单词,他们想知道他们有哪些单词(或者可能只是有多少)是共同的,而不暴露各自的单词列表。解决这个问题的一种方法是使用你在第十一章学到的遗忘伪随机函数(OPFR)构造。(我在图 15.2 中说明了这个协议。)如果您回忆起来
-
Bob 为 OPRF 生成一个密钥。
-
Alice 使用 OPRF 协议为她列表中的每个单词获取随机值PRF(key,word)(因此她不会得知 PRF 密钥,Bob 也不会得知这些单词)。
-
然后 Bob 可以计算他自己单词的PRF(key,word)值列表,并将其发送给 Alice,然后 Alice 可以将其与她自己的 PRF 输出进行比较,以查看 Bob 的任何 PRF 输出是否匹配。

图 15.2 私有集合交集(PSI)允许 Alice 了解她与 Bob 有哪些共同的单词。首先,她会对她列表中的每个单词进行盲化,并使用 OPRF 协议与 Bob 一起对每个单词应用 PRF,使用 Bob 的密钥。最后,Bob 发送给她他的单词与其密钥的 PRF。然后,Alice 可以查看是否有任何匹配项,以了解他们共有哪些单词。
PSI 是一个前景广阔的领域,近年来开始越来越多地得到应用,因为它显示出比过去更实用的特点。例如,Google 的 Chrome 浏览器集成的密码检查功能使用 PSI 来在密码泄露后的密码转储中检测到您的某些密码时向您发出警告,而不实际看到您的密码。有趣的是,微软也为其 Edge 浏览器做了同样的事情,但使用全同态加密(我将在下一节介绍)执行私有集交。另一方面,Signal 消息应用的开发人员(在第十章讨论)认为 PSI 速度太慢,无法执行联系发现,以便根据您手机的联系人列表确定可以与您交谈的人,并且改用了 SGX(在第十三章介绍)作为可信第三方。
15.1.2 通用型多方计算
总的来说,MPC 有许多不同的解决方案,旨在计算任意程序。通用型 MPC 解决方案提供不同级别的效率(从几小时到几毫秒)和不同类型的属性。例如,协议可以容忍多少不诚实的参与者?参与者是恶意的还是只是诚实但好奇(也称为半诚实,是 MPC 协议中的一种参与者类型,他们愿意正确执行协议,但可能试图了解其他参与者的输入)?如果其中一些参与者提前终止协议,那么所有参与者是否都公平?
在使用 MPC 可以安全计算程序之前,需要将其转换为算术电路。算术电路是一系列的加法和乘法,因为它们是图灵完备的,所以它们可以表示任何程序!有关算术电路的说明,请参见图 15.3。

图 15.3 一个算术电路是一些将输入与输出连接起来的加法或乘法门。在图中,数值从左到右传播。例如,d = a + b。在这里,电路只输出一个值 f = a + b + bc,但理论上它可以有多个输出值。请注意,电路的不同输入由不同的参与者提供,但它们也可以是公共输入(为所有人所知)。
在看下一个原语之前,让我给你一个通过 Shamir 的秘密共享构建的(诚实多数)通用型多方计算的简化示例。存在许多更多的方案,但这个方案足够简单,可以在这里进行三步解释:在电路中共享每个输入的足够信息,评估电路中的每个门,以及重构输出。让我们更详细地看看每一步。
第一步是每个参与者对电路的每个输入都有足够的信息。公共输入是公开分享的,而私有输入是通过 Shamir 的秘密共享(在第八章中介绍)分享的。我在图 15.4 中说明了这一点。

图 15.4 通用 MPC 的第一步是使用 Shamir 的秘密共享方案,让参与者分割各自的秘密输入并将部分分发给所有参与者。例如,在这里,Alice 将她的输入 a 分割成 a[1] 和 a[2]。因为在这个例子中只有两个参与者,她将第一个份额给了自己,将第二个份额给了 Bob。
第二步是评估电路的每个门。由于技术原因,我将在这里省略,加法门可以在本地计算,而乘法门必须通过交互方式计算(参与者必须交换一些消息)。对于加法门,只需将您拥有的输入份额相加;对于乘法门,将输入份额相乘。您得到的是一个结果份额,如图 15.5 所示。此时,份额可以交换(以重建输出)或保持分开以继续计算(如果它们代表中间值)。

图 15.5 通用 MPC 的第二步是让参与者计算电路中的每个门。例如,参与者可以通过添加他们拥有的两个输入 Shamir 份额来计算一个加法门,这将产生一个输出的 Shamir 份额。
最后一步是重建输出。此时,参与者应该都拥有输出的一个份额,他们可以使用这个份额来使用 Shamir 的秘密共享方案的最后一步来重建最终输出。
15.1.3 MPC 的现状
在过去的十年中,MPC 的实用性取得了巨大进展。这是一个涵盖许多不同用例的领域,人们应该密切关注可以从这种新型原语中受益的潜在应用。需要注意的是,不幸的是,目前并没有真正的标准化努力,虽然今天有几种 MPC 实现可以被认为对许多用例来说是实用的,但它们并不容易使用。
顺便提一下,在本节前面我解释的通用 MPC 构造是基于秘密共享的,但构造 MPC 协议的方法还有很多。一个著名的替代方案叫做加密电路,这是姚期智在他 1982 年的论文中首次提出 MPC 时提出的一种构造类型。另一个选择是基于全同态加密,这是你将在下一节中了解的一种基本原语。
15.2 完全同态加密(FHE)和加密云的承诺
在密码学领域长期以来,一个问题困扰着许多密码学家:是否可能在加密数据上计算任意程序?想象一下,您可以分别加密值 a、b 和 c,将密文发送到一个服务,然后要求该服务返回 a × 3b + 2c + 3 的加密结果,然后您可以解密它。这里的重要思想是服务永远不会了解您的值,并始终处理密文。这个计算可能不太有用,但是通过加法和乘法,可以在加密数据上计算实际程序。
这个有趣的概念最初是由 Rivest、Adleman 和 Dertouzos 在 1978 年提出的,我们称之为 完全同态加密(FHE)(或者以前被称为 密码学圣杯)。我在图 15.6 中说明了这个密码学原语。

图 15.6 完全同态加密(FHE)是一种加密方案,允许对加密内容进行任意计算。只有密钥的所有者可以解密计算结果。
15.2.1 使用 RSA 加密的同态加密示例
顺便说一句,您已经看到了一些加密方案,应该让您感觉自己知道我在说什么。想想 RSA(在第六章中讨论过):给定一个 ciphertext = message^e mod N,某人可以轻松计算密文的某些受限函数
n^e × ciphertext = (n × message)^e mod N
他们想要的任何数字(虽然不能太大)。结果是一个解密为
n × message
当然,这并不是 RSA 想要的行为,这导致了一些攻击(例如第六章提到的 Bleichenbacher 的攻击)。在实践中,RSA 通过使用填充方案来打破同态性质。请注意,RSA 仅对乘法同态,这对于计算任意函数是不够的,因为这需要乘法和加法。由于这个限制,我们说 RSA 是 部分同态。
15.2.2 不同类型的同态加密
其他类型的同态加密包括
-
部分同态—意味着对于一种操作(加法或乘法)部分同态,另一种操作在有限的方式上是同态的。例如,加法在一定数量上是无限制的,但只能进行少量乘法。
-
分层同态—可以进行一定次数的加法和乘法。
-
完全同态—加法和乘法无限制(这才是真正的东西)。
在 FHE 的发明之前,提出了几种类型的同态加密方案,但没有一种能够实现完全同态加密所承诺的功能。原因是通过在加密数据上评估电路,一些噪声会增加;在某个点之后,噪声已经达到了使解密变得不可能的阈值。多年来,一些研究人员试图证明也许有一些信息理论可以表明完全同态加密是不可能的;直到被证明是可能的。
15.2.3 启动引导,完全同态加密的关键
一天晚上,爱丽丝梦见了巨大的财富,洞穴里堆满了银、金和钻石。然后,一只巨龙吞噬了财富,并开始吃自己的尾巴!她醒来时感到平静。当她试图理解她的梦时,她意识到她已经找到了解决问题的方法。
—克雷格·根特里(“计算加密数据的任意函数”,2009)
2009 年,丹·博内的博士生克雷格·根特里提出了有史以来第一个完全同态加密构造。根特里的解决方案被称为启动引导,实际上是每隔一段时间在密文上评估解密电路,以将噪声降低到可管理的阈值。有趣的是,解密电路本身不会泄露私钥,并且可以由不受信任的一方计算。启动引导允许将分层的 FHE 方案转变为 FHE 方案。根特里的构造速度很慢且相当不切实际,每个基本位操作大约需要 30 分钟,但与任何突破一样,随着时间的推移变得更好。它还表明完全同态加密是可能的。
启动引导是如何工作的?让我们看看是否能获得一些见解。首先,我需要提到我们不需要对称加密系统,而是需要一个公钥加密系统,其中公钥可用于加密,私钥可用于解密。现在,想象一下,你在一个密文上执行了一定数量的加法和乘法运算,并达到了一定的噪声水平。噪声足够低,仍然可以正确解密密文,但太高了,不会让你执行更多同态操作而不破坏加密内容。我在图 15.7 中说明了这一点。

图 15.7 在使用完全同态加密算法加密消息后,对其进行操作会将其噪声增加到危险的阈值,解密变得不可能。
你可能认为自己陷入了困境,但启动引导通过从那个密文中去除噪声来解决问题。为此,你需要使用另一个公钥(通常称为启动引导密钥)重新加密有噪声的密文,以获得该有噪声的密文的加密。请注意,新的密文没有噪声。我在图 15.8 中说明了这一点。

图 15.8 在图 15.7 的基础上,为了消除密文的噪声,你可以对其进行解密。但是因为你没有秘密密钥,所以你将嘈杂的密文重新加密在另一个公钥下(称为引导密钥)以获得一个新的密文,该密文是没有错误的嘈杂密文。
现在来到了魔法的部分:你被提供了初始的私钥,但不是以明文形式,而是在那个引导密钥下加密的。这意味着你可以使用解密电路与同态地解密内部带有噪声的密文。如果解密电路产生的噪声量是可接受的,那么它就起作用了,你将得到第一个同态操作的结果,其密文是在引导密钥下加密的。我在图 15.9 中进行了说明。

图 15.9 在图 15.9 的基础上,你使用了初始的加密到引导密钥的秘密密钥来对新的密文应用解密电路。这有效地在原地解密了带有噪声的密文,消除了错误。由于解密电路,会产生一定数量的错误。
如果剩余的错误量允许你至少进行一次同态操作(+或×),那么你就成功了:你拥有一个完全同态加密算法,因为你可以始终在实践中在每个操作之后或之前运行引导。请注意,你可以将引导密钥对设置为与初始密钥对相同。这有点奇怪,因为你会得到一些循环安全问题,但似乎它可以运行,且没有已知的安全问题。
15.2.4 基于学习中的错误问题的 FHE 方案
在继续之前,让我们看一个基于我们在第十四章中看到的学习中的错误问题的 FHE 方案的示例。我将解释一个简化版本的 GSW 方案,以作者 Craig Gentry、Amit Sahai 和 Brent Waters 的名字命名。为了保持简单,我将介绍算法的秘密密钥版本,但请记住,将这样的方案转换为我们需要的公钥变体是相对简单的,这是我们用于引导的。看看下面的方程式,其中C是一个方阵,s是一个向量,而m是一个标量(一个数字):
Cs = ms
在这个方程中,s被称为eigenvector而m被称为eigenvalue。如果这些词对你来说很陌生,不用担心;它们在这里并不重要。
我们 FHE 方案中的第一个直觉是通过观察特征向量和特征值得到的。观察到的是,如果我们将m设置为要加密的单个位,C设置为密文,s设置为秘密密钥,则我们有一个(不安全的)同态加密方案来加密一个位。 (当然,我们假设存在一种方法从固定位m和固定秘密密钥s获取随机密文C。)我在图 15.10 中以一种乐高方式进行了说明。

图 15.10 我们可以通过将 m 解释为一个特征值,s 解释为一个特征向量,然后找到关联矩阵 C 来生成一个不安全的同态加密方案,该方案将密文。
要解密密文,您需要用秘密向量 s 乘以矩阵,然后查看是否获得了秘密向量或 0。您可以验证该方案是完全同态的,方法是检查两个密文加在一起的解密结果是否是相应位相加的结果:
(C[1] + C[2])s = C[1]s + C[2]s = b[1]s + b[2]s = (b[1] + b[2])s
此外,两个密文相乘的解密结果 (C[1] × C[2]) 是相应位相乘的结果:
(C[1] × C[2])s = C[1] (C[2]s) = C[1] (b[2]s) = b[2]C[1]s = (b[2] × b[1]) s
不幸的是,该方案不安全,因为很容易从 C 中检索出特征向量(秘密向量 s)。增加一点噪声呢?我们可以稍微改变这个方程,使它看起来像我们的误差学习问题:
Cs = ms + e
这应该更加熟悉了。同样,我们可以验证加法仍然是同态的:
(C[1] + C[2])s = C[1]s + C[2]s = b[1]s + e[1] + b[2]s + e[2] = (b[1] + b[2])s + (e[1]+e[2])
在这里,注意到误差正在增加(e[1] + e[2]),这正是我们预期的。我们还可以验证乘法仍然有效:
(C[1] × C[2])s = C[1] (C[2]s) = C[1] (b[2]s + e[2]) = b[2]C[1]s + C[1]e[2] = b[2] (b[1]s + e[1]) + C[1]e[2]
= (b[2] × b[1]) s + b[2]e[1] + C[1]e[2]
在这里,b[2]e[1] 很小(因为它要么是 e[1] 要么是 0),但 C[1]e[2] 可能很大。这显然是一个问题,我会忽略它以避免深入研究细节。如果你有兴趣了解更多,请务必阅读沙伊·哈莱维(Shai Halevi)于 2017 年发表的《同态加密》报告,该报告在解释所有这些内容及更多内容方面做得非常出色。
15.2.5 它在哪里使用?
FHE 最被吹捧的用例一直是云。如果我可以继续将我的数据存储在云中而不被看到怎么办?而且,此外,如果云可以在加密数据上提供有用的计算呢?事实上,人们可以想到许多应用场景可以使用 FHE。一些例子包括
-
垃圾邮件检测器可以扫描您的电子邮件而不看这些内容。
-
可以对您的 DNA 进行遗传研究,而无需实际存储和保护您的隐私敏感人类编码。
-
数据库可以加密存储,并且在服务器端进行查询而不泄露任何数据。
然而,菲利普·罗加韦在他 2015 年关于“密码工作的道德性质”的开创性论文中指出,“全同态加密[...]引发了一波新的狂热。在资助提案、媒体采访和演讲中,领先的理论家们谈论全同态加密[...]作为我们取得进展的标志性迹象。没有人似乎强调这种假设性的东西是否会对实践产生任何影响。”
尽管罗加韦并没有错,全同态加密仍然非常缓慢,但该领域的进展令人兴奋。截至撰写本文时(2021 年),操作速度比正常操作慢约十亿倍,但自 2009 年以来,已经有了 10⁹倍的加速。毫无疑问,我们正在朝着未来的方向前进,至少对于某些有限的应用来说,全同态加密将成为可能。
此外,并非每个应用都需要全面的原语;有些同态加密可以在广泛的应用中使用,比全同态加密更有效。一个理论密码原语进入现实世界的一个良好指标是标准化,而事实上,全同态加密并不陌生于此。homomorphicencryption.org的标准化工作包括许多大公司和大学。目前尚不清楚全同态加密何时、何地以及以何种形式进入现实世界。但可以肯定的是,它将会发生,敬请关注!
15.3 通用零知识证明(ZKPs)
第七章中我谈到了零知识证明(ZKPs)与签名有关。在那里,我指出签名类似于离散对数的非交互式 ZKPs。这种 ZKPs 是由沙菲·戈德瓦塞、席尔维奥·米卡利和查尔斯·拉科夫教授于上世纪 80 年代中期发明的。不久之后,戈德雷希、米卡利和威格德森发现我们不仅可以证明离散对数或其他类型的难题,还可以证明任何程序的正确执行,即使我们删除了一些输入或输出(参见图 15.11 的示例)。本节重点讨论这种通用类型的 ZKP。

图 15.11 通用 ZKPs 允许证明者说服验证者关于执行轨迹的完整性(程序的输入以及执行后获得的输出),同时隐藏了计算中涉及的一些输入或输出。一个例子是证明者试图证明数独可以被解决。
自其早期以来,ZKP 作为一个领域已经有了巨大的增长。这种增长的一个主要原因是加密货币的繁荣以及对链上交易提供更多保密性以及优化空间的需求。截至撰写本文时,ZKP 领域仍然以极快的速度增长,很难跟上现代方案的发展以及通用 ZKPs 的类型。
幸运的是,这个问题变得足够严重,已经触发了标准化阈值,一条虚拟线,一旦达到,几乎总是会激励一些人共同努力,以澄清该领域。2018 年,来自行业和学术界的人士联合起来,成立了 ZKProof 标准化工作组,旨在“标准化使用加密零知识证明”。直至今日,这仍然是一个正在进行的工作。您可以在zkproof.org上阅读更多信息。
您可以在许多不同的情况下使用通用型 ZKPs,但据我所知,迄今为止它们主要已被用于加密货币领域,可能是因为对密码学感兴趣并愿意尝试最前沿技术的人数众多。尽管如此,通用型 ZKPs 在许多领域都有潜在应用:身份管理(能够证明您的年龄而不暴露它)、压缩(能够隐藏大部分计算)、机密性(能够隐藏协议的某些部分)等等。更多应用采用通用型 ZKPs 的最大障碍似乎是以下几点:
-
大量的 ZKP 方案以及每年都有更多方案被提出。
-
理解这些系统如何工作以及如何将它们用于特定用例的困难。
不同提议方案之间的区别非常重要。由于这是一个极易引起混淆的来源,这是一些方案被分割的方式:
-
零知识或非零知识—如果某些信息需要对某些参与者保密,那么我们需要零知识性。注意,无秘密的证明也可能有用。例如,您可能想将一些密集计算委托给一个服务,而该服务又必须向您证明他们提供的结果是正确的。
-
交互式或非交互式—大多数 ZKP 方案可以变成非交互式的(有时使用我在第七章中讨论的 Fiat-Shamir 变换),而协议设计者似乎最感兴趣的是该方案的非交互式版本。这是因为在协议中来回传输的信息可能会耗费时间,但也因为有时交互性可能不可行。所谓的非交互式证明通常被称为NIZKs,代表非交互式 ZKPs。
-
简洁证明或非简洁证明—聚光灯下的大多数 ZKP 方案通常被称为zk-SNARKs,代表零知识简洁非交互知识证明。尽管定义可能有所不同,但它着重于这些系统生成的证明的大小(通常在数百字节的数量级),以及验证这些证明所需的时间(在毫秒级范围内)。zk-SNARKs 因此简短且易于用于验证 ZKPs。请注意,一个方案不是 zk-SNARK 并不意味着它在现实世界中无法使用,因为通常在不同的用例中可能有用的不同属性。
-
透明的设置或不是—像每个密码原语一样,ZKP 需要一种设置来同意一组参数和公共值。这称为共同参考字符串(CRS)。但是,ZKP 的设置可能比最初想象的限制或危险得多。有三种类型的设置:
-
可信任的—意味着创建 CRS 的人也可以访问允许他们伪造证明的秘密(因此,这就是为什么这些秘密有时被称为“有毒废物”的原因)。这是一个相当严重的问题,因为我们又回到了需要信任的第三方,然而具有这种属性的方案通常是效率最高且证明大小最短的。为了降低风险,MPC 可以用于让许多参与者帮助创建这些危险的参数。如果单个参与者是诚实的,并在典礼结束后删除他们的密钥,那么有毒废物就会被清除。
-
通用的—如果信任的设置被称为通用,则您可以使用它来证明任何电路的执行(受某些大小限制)。否则它是特定于单个电路的。
-
透明的—对于我们来说,幸运的是,许多方案也提供透明的设置,这意味着不需要存在信任的第三方来创建系统的参数。透明的方案按设计是通用的。
-
-
是否抗量子—一些 ZKP 利用公钥加密和高级原语,如双线性配对(稍后我会解释),而另一些则仅依赖对称加密(如哈希函数),这使它们在本质上抗量子计算(通常以更大的证明为代价)。
由于在撰写本文时,zk-SNARKs 备受关注,让我向您解释一下它们的工作原理。
15.3.1 zk-SNARKs 的工作原理
首先,有许多许多 zk-SNARK 方案—实际上有太多了。大多数都建立在这种类型的构造之上:
-
一个证明系统,允许证明者向验证者证明某些事情。
-
程序的翻译或编译为证明系统可以证明的东西。
第一部分并不太难理解,而第二部分在某种程度上需要一个研究生课程的知识。首先,让我们来看看第一部分。
zk-SNARKs 的主要思想是证明您知道一些多项式f(x)具有一些根。根的意思是验证者心中有一些值(例如,1 和 2),证明者必须证明他们心中的秘密多项式对这些值(例如,f(1) = f(2) = 0)进行评估为 0。顺便说一句,一个具有 1 和 2 作为根的多项式(如我们的例子)可以写为f(x) = (x – 1)(x – 2)h(x),其中h(x)是某个多项式。 (如果你不相信,请尝试在x = 1 和x = 2 处评估它。)我们说证明者必须证明他们知道一个f(x)和h(x),使得f(x) = t(x)h(x),其中t(x) = (x – 1)(x – 2)是目标多项式。在这个例子中,1 和 2 是验证者想要检查的根。
但就是这样!这就是 zk-SNARKs 证明系统通常提供的东西:证明你知道某些多项式。我再次强调这一点,因为我第一次了解这个概念时觉得很难理解。如果你只能证明你知道一个多项式,你怎么能证明你知道程序的某个秘密输入呢?好吧,这就是 zk-SNARK 的第二部分如此困难的原因。它涉及将程序转化为多项式。但稍后再详述。
回到我们的证明系统,如何证明他们知道这样一个函数f(x)?他们必须证明他们知道一个h(x),使得你可以将f(x)写成f(x) = t(x)h(x)。啊,... 别那么快。我们说的是零知识证明,对吧?我们怎么能在不泄露f(x)的情况下证明这一点?答案就在以下三个技巧中:
-
同态承诺——类似于我们在其他零知识证明中使用的承诺方案(在第七章中讨论)
-
双线性配对——一种具有一些有趣特性的数学构造;稍后详述
-
大多数情况下,不同的多项式求值不同
所以让我们逐个看看这些,好吗?
15.3.2 同态承诺以隐藏证明的部分
第一个技巧是使用承诺来隐藏我们发送给证明者的值。但我们不仅要隐藏它们,还要允许验证者对它们执行一些操作,以便他们可以验证证明。具体来说,他们需要验证,如果证明者对他们的多项式f(x)以及h(x)进行承诺,那么我们就有
com(f(x)) = com(t(x)) com(h(x)) = com(t(x)h(x))
其中承诺com(t(x))由验证者计算为多项式上的约束。这些操作称为同态操作,如果我们使用哈希函数作为承诺机制(如第二章所述),我们无法执行这些操作。由于这些同态承诺,我们可以“隐藏指数中的值”(例如,对于值v,发送承诺g^v mod p)并执行有用的身份验证:
-
承诺的等式——等式g^a = g^b 意味着 a = b
-
承诺的添加——等式g^a = gb*g*c 意味着 a = b + c
-
承诺的缩放——等式g^a = (gb)c 意味着 a = bc
注意,最后一个检查只有在c是公共值而不是一个承诺(gc)时才有效。仅通过同态承诺,我们无法检查承诺的乘法,这正是我们所需的。幸运的是,密码学有另一个工具可以将这样的方程式隐藏在指数中——双线性配对。
15.3.3 双线性配对以改进我们的同态承诺
双线性配对可用于解除我们的阻碍,这是我们在 zk-SNARK 中使用双线性配对的唯一原因(真的,只是为了能够在承诺内部相乘的值)。我不想深入讨论什么是双线性配对,但只需知道它是我们工具箱中的另一个工具,允许我们将以前无法相乘的元素从一个群移到另一个群。
使用 e 作为写双线性配对的典型方式,我们有 e(g[1], g[2]) = h[3],其中 g[1]、g[2] 和 h[3] 是不同群的生成器。在这里,我们将在左侧使用相同的生成器(g[1] = g[2]),这使得配对对称。我们可以使用双线性配对通过这个方程来执行指数中隐藏的乘法:
e(g^b, g^c) = e(g)^(bc)
再次,我们使用双线性配对使得我们的承诺不仅对加法是同态的,而且对乘法也是同态的。(请注意,这不是一个完全同态的方案,因为乘法仅限于单个乘法。)双线性配对也用于密码学的其他地方,并且正在逐渐成为更常见的构建块。它们可以在同态加密方案中看到,也可以在像 BLS(我在第八章提到的)这样的签名方案中看到。
15.3.4 紧凑性来自何处?
最后,zk-SNARK 的紧凑性来自于两个不同函数的评估大多数情况下会得到不同的点。这对我们意味着什么?假设我没有一个多项式 f(x) 真正具有我们与验证者选择的根,这意味着 f(x) 不等于 t(x)h(x)。然后,在随机点 r 上评估 f(x) 和 t(x)h(x) 不会大部分时间返回相同的结果。对于几乎所有的 r,f(r)≠ t(r)h(r)。这就是Schwartz-Zippel 引理,我在图 15.12 中有所说明。

图 15.12 Schwartz-Zippel 引理指出,两个不同的 n 次多项式最多可以在 n 个点相交。换句话说,两个不同的多项式在大多数点上会有所不同。
了解这一点,证明 com(f(r)) = com(t(r)h(r)) 对于某个随机点 r 就足够了。这就是为什么 zk-SNARK 证明如此小的原因:通过比较群中的点,最终你会比较更大的多项式。但这也是大多数 zk-SNARK 构造中需要可信设置的原因。如果证明者知道将用于检查等式的随机点 r,那么他们可以伪造一个无效的多项式,仍然会验证相等。因此,可信设置就是
-
创建一个随机值 r
-
承诺不同幂次的 r(例如,g, g^r, g^(r²), g^(r³),等等),以便证明者可以计算他们的多项式而不知道点 r
-
销毁值 r
第二点有意义吗?如果我作为证明者的多项式是f(x) = 3x² + x + 2,那么我所要做的就是计算(g^(r²))³ g^r g²,以获得我在那个随机点r处评估的多项式的承诺(不知道r)。
15.3.5 从程序到多项式
到目前为止,证明者必须找到的多项式的约束是它需要有一些根:一些用我们的多项式计算为 0 的值。但是我们如何将一个更一般的语句转换成多项式知识证明?目前最多使用 zk-SNARKs 的应用是加密货币,其中典型的语句的形式为:
-
证明一个值在范围[0, 2⁶⁴]内(这被称为范围证明)
-
证明一个(秘密)值包含在给定的(公开)Merkle 树中
-
证明某些值的和等于另一些值的和
-
依此类推
这就是困难的部分所在。正如我之前所说,将程序执行转换为多项式的知识是非常困难的。好消息是我不会告诉你所有的细节,但我会告诉你足够让你了解事情的工作原理。从那里开始,你应该能够理解我解释中缺少的部分,并根据自己的需要填补缺漏。接下来将会发生以下事情:
-
我们的程序将首先被转换成算术电路,就像我们在 MPC 部分看到的那样。
-
那个算术电路将被转换成一种特定形式的方程组(称为秩-1 约束系统或 R1CS)。
-
然后我们使用一个技巧将我们的方程组转换为一个多项式。
15.3.6 程序适用于计算机;我们需要算术电路而不是
首先,让我们假设几乎任何程序都可以更多或更少地轻松地用数学重写。为什么我们想这样做的原因应该是显而易见的:我们不能证明代码,但我们可以证明数学。例如,以下列表提供了一个函数,其中除了a是我们的秘密输入之外,每个输入都是公开的。
列表 15.1 一个简单的函数
fn my_function(w, a, b) {
if w == true {
return a * (b + 3);
} else {
return a + b;
}
}
在这个简单的例子中,如果除了a之外的每个输入和输出都是公开的,人们仍然可以推断出a是什么。这个列表也是一个例子,说明你不应该试图在零知识中证明什么。无论如何,该程序可以用以下方程重写成数学形式:
w × (a × (b + 3)) + (1 – w) × (a + b) = v
其中v是输出,w要么是 0(false)要么是 1(true)。请注意,这个方程实际上不是一个程序或电路,它只是一个约束条件。如果你正确执行程序,然后填写方程中得到的输入和输出,等式应该是正确的。如果等式不正确,那么你的输入和输出就不对应于程序的有效执行。
这就是你必须思考这些通用零知识证明的方式。我们不是在零知识中执行一个函数(实际上这并没有什么意义),而是使用 zk-SNARKs 来证明一些给定的输入和输出正确地匹配了程序的执行,即使其中一些输入或输出被省略了。
15.3.7 从算术电路到 rank-1 约束系统(R1CS)
无论如何,我们只是将我们的执行转换为可以用 zk-SNARKs 证明的东西的过程中的一步。下一步是将其转换为一系列约束条件,然后可以将其转换为证明某个多项式的知识。zk-SNARKs 想要的是rank-1 约束系统(R1CS)。一个 R1CS 实际上只是一系列形式为L × R = O的方程,其中L、R和O只能是一些变量的加法,因此唯一的乘法是在L和R之间。我们为什么需要将我们的算术电路转换为这样的方程系统其实并不重要,除了在将其转换为最终可以证明的东西时会有所帮助。尝试用我们的方程做这个,我们得到的东西就像是
-
a × (b + 3) = m
-
w × (m – a – b) = v – a – b
我们实际上忘记了w只能是 0 或 1 的约束条件,我们可以通过一个巧妙的技巧来将其添加到我们的系统中:
-
a × (b + 3) = m
-
w × (m – a – b) = v – a – b
-
w × w = w
有意义吗?你真的应该将这个系统看作一组约束条件:如果你给我一组值,声称这些值与我程序的输入和输出匹配,那么我应该能够验证这些值也正确地验证了等式。如果其中一个等式是错误的,那么这必须意味着该程序没有输出你给我的这些输入的值。另一种思考方式是,zk-SNARKs 允许你可验证地删除程序正确执行的传输的输入或输出。
15.3.8 从 R1CS 到多项式
问题仍然是:我们如何将这个系统转化为一个多项式?我们已经快要完成了,而且一如既往的答案是通过一系列的技巧!因为我们的系统中有三个不同的方程,第一步是为我们的多项式确定三个根。我们可以简单地选择 1、2、3 作为根,这意味着我们的多项式对x = 1、x = 2 和x = 3 都解出了f(x) = 0。为什么这样做?通过这样做,我们可以使我们的多项式同时代表我们系统中的所有方程,通过在 1 处求值时代表第一个方程,在 2 处求值时代表第二个方程,依此类推。现在验证者的工作是创建一个多项式f(x),使得:
-
f(1) = a × (b + 3) – m
-
f(2) = w × (m – a – b) – (v – a – b)
-
f(3) = w × w – w
请注意,如果这些方程的值正确匹配我们原始程序的执行,所有这些方程应该评估为 0。换句话说,我们的多项式f(x)只有在我们正确创建它时才有根 1、2、3。记住,这就是 zk-SNARK 的全部意义:我们有协议来证明,确实,我们的多项式f(x)有这些根(由证明者和验证者都知道)。
如果我的解释到此为止就太简单了,因为现在的问题是证明者在选择他们的多项式f(x)时有太多的自由。他们可以简单地找到一个具有根 1、2、3 的多项式,而不关心值a、b、m、v和w。他们可以做任何他们想做的事情!相反,我们想要的是一个系统,锁定多项式的每个部分,除了验证者必须不了解的秘密值。
15.3.9 两个人评估隐藏在指数中的多项式
让我们回顾一下,我们希望一个证明者必须使用他们的秘密值a和公共值b和w正确执行程序,并获得他们可以发布的输出v。然后,证明者必须创建一个多项式,只填写验证者不应该了解的部分:值a和m。因此,在一个真正的 zk-SNARK 协议中,当证明者创建他们的多项式并将其评估到一个随机点时,你希望证明者在创建他们的多项式时拥有尽可能少的自由。
为了做到这一点,多项式是通过让证明者只填写他们的部分而在一定程度上动态地创建的,然后让验证者填写其他部分。例如,让我们以第一个方程为例,f(1) = a × (b + 3) – m,表示为
f1 = aL1 × (b + 3)R1 – mO1
其中L1、R1、O1 是多项式,当x = 1 时评估为 1,当x = 2 和x = 3 时评估为 0。这是必要的,以便它们只影响我们的第一个方程。(请注意,通过拉格朗日插值等算法很容易找到这样的多项式。)现在,请注意另外两点:
-
我们的多项式的系数是输入、中间值和输出。
-
多项式f(x)是和 f1 + f2 + f3 的总和,其中我们可以定义f2 和f3 来表示方程 2 和 3,类似于f1。
正如你所看到的,我们的第一个方程仍然在点x = 1 处表示:
f(1) = f1 + f2 + f3
= f1
= aL1 × (b + 3)R1 – mO1
= a × (b + 3) – m
采用这种新的表示方程的方式(记住,表示我们程序执行的方程),证明者现在可以通过以下方式评估与他们相关的多项式的部分:
-
对隐藏在指数中的随机点r进行指数运算,以重构多项式L1 和O1
-
对秘密值a进行指数运算 g(L1)以获得(g(L1))^a = g^(aL1),表示a × L1 在未知且随机点x = r处的评估,并隐藏在指数中。
-
对秘密中间值m进行指数运算 g(O1)以获得(g(O1))^m = g^(mO1),表示在随机点r处评估mO1,并隐藏在指数中。
验证者可以通过使用与证明者相同的技术,重建(g(R1))b 和(g^(R0))³来填补缺失的部分,其中b是一个已同意的值。将两者相加,验证者得到 g^(bR1) + g^(3R1),表示在未知且随机点x = r处(b + 3) × R1 的(隐藏)评估。最后,验证者可以通过使用双线性配对重建隐藏在指数中的f1:
e(g^(aL1), g^((b+3)R1)) – e(g, g^(mO1)) = e(g, g)^(aL1) × (b + 3)R1 – mO1
如果将这些技术推广到整个多项式f(x),你可以了解最终的协议。当然,这仍然是对真实 zk-SNARK 协议的严重简化;这仍然给证明者留下了太多的权力。
zk-SNARKs 中使用的所有其他技巧都旨在进一步限制证明者的行为,确保他们正确且一致地填补缺失的部分,并优化可以优化的部分。顺便说一句,我读过的最好的解释是 Maksym Petkus 的论文《zk-SNARK 为什么以及如何工作:权威解释》,该论文深入探讨了我忽视的所有部分。
这就是 zk-SNARKs 的全部内容。这实际上只是一个介绍;在实践中,了解和使用 zk-SNARKs 要复杂得多!将程序转换为可被证明的东西的工作量不仅仅是非平凡的,有时还会对密码协议添加新的约束。例如,主流的哈希函数和签名方案通常对于通用 ZKP 系统来说过于重型,这导致许多协议设计者研究了不同的 ZKP 友好方案。此外,正如我之前所说的,有许多不同的 zk-SNARKs 构造,还有许多不同的非 zk-SNARKs 构造,根据您的用例,后者可能更相关作为通用 ZKP 构造。
不幸的是,并不存在一种大小适合所有的 ZKP 方案(例如,具有透明设置、简洁、通用和抗量子的 ZKP 方案),目前尚不清楚在哪些情况下使用哪种方案。该领域仍然很年轻,每年都会发布新的更好的方案。也许几年后会出现更好的标准和易于使用的库,所以如果你对这个领域感兴趣,继续关注吧!
总结
-
在过去的十年中,许多理论密码学原语在效率和实用性方面取得了巨大进步;其中一些正在逐渐走向现实世界。
-
安全多方计算(MPC)是一种原语,允许多个参与者共同正确执行程序,而不暴露各自的输入。阈值签名正在开始在加密货币中被采用,而私密集合交集(PSI)协议正在被用于现代和大规模协议,如谷歌的密码检查。
-
完全同态加密(FHE)允许在不解密的情况下对加密数据进行任意函数计算。它在云中具有潜在应用,可以防止除用户之外的任何人访问数据,同时允许云平台对数据进行用户有用的计算。
-
通用零知识证明(ZKPs)已经找到了许多用例,并且在快速验证小证明方面取得了最近的突破。它们主要用于加密货币,以增加隐私或压缩区块链的大小。然而,它们的用例似乎更广泛,随着更好的标准和更易于使用的库进入现实世界,我们可能会看到它们被越来越广泛地使用。
第十六章:加密何时何地失败
本章涵盖
-
使用加密时可能遇到的一般问题
-
遵循烘烤良好的加密的要点
-
加密从业者的危险和责任
问候,旅行者;你走了很长的路。虽然这是最后一章,但重要的是旅程,而不是终点。你现在已经装备好了进入真正的加密世界所需的装备和技能。剩下的就是应用你所学到的知识。
在我们分道扬镳之前,我想给你一些提示和工具,这些对接下来的事情会有用。你将面临的任务经常遵循相同的模式:它始于一个挑战,将你引向一个现有的加密原语或协议的追求。从那里,你会寻找一个标准和一个良好的实现,然后你会尽可能地利用它。这是如果一切按计划进行的话。 . . .
在我们分别之前
试图弥合理论与实践之间差距的人将不得不打倒许多龙。这是你的剑 —— 拿去吧。

找到正确的加密原语或协议是一项无聊的工作
你面对的是未加密的流量,或者需要相互认证的多个服务器,或者需要存储而不会成为单点故障的一些秘密。你会怎么做?
你可以使用 TLS 或噪声(在第九章提到)来加密你的流量。你可以建立一个公钥基础设施(在第九章提到)来通过某个证书颁发机构的签名来验证服务器,你还可以使用阈值方案(在第八章中涵盖)来分发一个秘密,以避免一个秘密的泄露导致整个系统的崩溃。这些将是很好的答案。
如果你面临的问题是常见的,那么你很有可能会发现一个已经存在的加密原语或协议直接解决你的使用情况。本书给出了标准原语和常见协议的良好概念,所以在这一点上,当你面临加密问题时,你应该很清楚你可以使用什么。
加密是一个非常有趣的领域,随着新的发现和原语的提出,它遍布各个地方。虽然你可能会被诱惑去探索奇特的加密来解决你的问题,但你的责任是保持保守。原因是复杂性是安全的敌人。无论何时做某事,尽可能简单是更容易的。尝试过于炫耀已经引入了太多的漏洞。这个概念被 Bernstein 在 2015 年称为无聊的加密,并且是 Google 的 TLS 库 BoringSSL 命名背后的灵感来源。
加密提议需要经受多年的仔细审查,才能成为可信任的字段使用候选人,尤其是当提议基于新颖的数学问题时。
—Rivest 等人(“对 NIST 提案的回应”,1992)
如果找不到解决您问题的加密原语或协议怎么办? 这就是您必须踏入 理论 密码学世界的地方,显然这不是本书的主题。 我只能给出建议。
我将给你的第一个建议是免费书籍 A Graduate Course in Applied Cryptography,由 Dan Boneh 和 Victor Shoup 撰写,可在 cryptobook.us 获取。 这本书提供了出色的支持,涵盖了我在这本书中涵盖的所有内容,但更加深入。 Dan Boneh 还有一个令人惊叹的在线课程,“Cryptography I”,也可在 www.coursera.org/learn/crypto 免费获得。 这是一个更温和的理论密码学入门。 如果你想阅读介于这本书和理论密码学世界之间的东西,我强烈推荐 Serious Cryptography: A Practical Introduction to Modern Encryption(No Starch Press,2017),作者 Jean-Philippe Aumasson。
现在,让我们想象一下,您确实有一个现有的解决方案解决了您的问题。 加密原语或协议仍然是一个非常理论的东西。 如果它有一个您可以立即使用的实用标准,那不是很棒吗?

16.2 我如何使用加密原语或协议? 有礼貌的标准和形式验证
您意识到存在符合您需求的解决方案,那么它是否有标准呢? 没有标准,原语往往是在不考虑其真实世界使用的情况下提出的。 密码学家通常不考虑使用其原语或协议的不同陷阱以及实施它们的细节。 有礼貌的密码学 是 Riad S. Wahby 曾称之为关心其实现并且不留给实施者多少错误空间的标准。
可怜的用户被给予足够的绳子来上吊自己——这是标准不应该做的事情。
—Rivest 等人(“对 NIST 提案的回应”,1992)
一个有礼貌的标准是一项旨在通过提供安全且易于使用的接口来解决所有边缘情况和潜在安全问题的规范,并提供了关于如何实现以及如何使用原语或协议的良好指导。 此外,良好的标准还具有相应的测试向量:匹配输入和输出的列表,您可以将其馈送到您的实现中以测试其正确性。
不幸的是,并非所有标准都是“友好的”,它们所造成的密码学陷阱是我在本书中谈论的大多数漏洞的原因。有时标准过于模糊,缺乏测试向量,或者试图一次做太多事情。例如,密码学灵活性是指协议在支持的密码算法方面的灵活性。支持不同的密码算法可以使一个标准具有优势,因为有时一个算法被破解和废弃,而其他算法没有。在这种情况下,一个不灵活的协议会阻止客户端和服务方轻松迁移。另一方面,过多的灵活性也会严重影响标准的复杂性,有时甚至会导致漏洞,正如 TLS 上的许多降级攻击所证明的那样。
不幸的是,更多的时候,密码学家不愿承认,当你的问题遇到主流原语或协议没有解决的边缘情况,或者当你的问题与标准化解决方案不匹配时,你会遇到麻烦。因此,看到开发人员创建自己的迷你协议或迷你标准是极其常见的。这就是麻烦开始的时候。
当对原语的威胁模型(它所保护的内容)或其可组合性(如何在协议中使用)做出错误假设时,就会出现问题。这些特定于上下文的问题被放大,因为加密原语通常是在一个独立的环境中构建的,设计者并没有必然考虑一旦原语在多种方式或在另一个原语或协议中使用时可能出现的所有问题。我举了很多例子:X25519 在边缘情况协议中破解(第十一章),签名被假定为唯一(第七章),以及在谁与谁通信方面的模糊性(第十章)。这并不一定是你的错!开发人员已经比密码学家聪明,揭示了没有人知道存在的陷阱。这就是发生的事情。
如果你发现自己处于这种情况下,密码学家的首选工具是纸笔证明。对于我们这些从业者来说,这并不是很有帮助,因为我们要么没有时间去做这项工作(确实需要很多时间),要么没有专业知识。不过,我们并不无助。我们可以利用计算机来简化分析迷你协议的任务。这被称为形式验证,可以很好地利用你的时间。
形式化验证允许您在某种中间语言中编写协议并对其进行一些属性测试。例如,Tamarin 协议证明器(见图 16.1)是一款形式验证工具,已经(并且正在)被用来发现许多不同协议中的微妙攻击。要了解更多信息,请参阅论文“Prime, Order Please! Revisiting Small Subgroup and Invalid Curve Attacks on Protocols using Diffie-Hellman”(2019)和“Seems Legit: Automated Analysis of Subtle Attacks on Protocols that Use Signatures”(2019)。

图 16.1 Tamarin 协议证明器是一款免费的形式验证工具,您可以使用它来对加密协议进行建模并找到其中的攻击。
另一面硬币是,使用形式化验证工具通常很困难。第一步是理解如何将协议转换为工具使用的语言和概念,这通常并不直观。在用形式语言描述了一个协议之后,您仍然需要弄清楚您想要证明什么以及如何在形式语言中表达它。
实际上,见到一个证明实际上证明了错误的事情并不少见,所以人们甚至可以问,谁来验证形式化验证?在这个领域的一些有希望的研究致力于让开发者更容易地形式化验证他们的协议。例如,工具 Verifpal (verifpal.com) 通过简化使用来换取声音(能够找到所有攻击)。
KRACK 攻击
在编写协议的形式描述与实际实现的协议之间,可能会产生关键性差异,从而导致现实世界的攻击和漏洞。这就是 2017 年发生的情况,当时 KRACK 攻击 (krackattacks.com) 破解了 Wi-Fi 协议 WPA2,尽管它先前已经经过形式验证。
你也可以使用形式化验证来验证密码学原语的安全性证明,使用像 Coq、CryptoVerif 和 ProVerif 这样的形式化验证工具,甚至可以在不同的语言中生成“经过形式验证”的实现(参见像 HACL*、Vale 和 fiat-crypto 这样的项目,这些项目实现了具有验证属性(如正确性、内存安全等)的主流密码学原语)。话虽如此,形式化验证并非万无一失的技术;论文协议与其形式描述之间或形式描述与实现之间的差距总是存在的,并且看起来无害,直到被发现是致命的。
研究其他协议失败的方式是避免犯同样的错误的绝佳方法。cryptopals.com或cryptohack.org挑战是了解在使用和组合加密原语和协议中可能出错的内容的好方法。底线——你需要彻底了解你正在使用的东西!如果你正在构建一个迷你协议,那么你需要小心,要么正式验证该协议,要么向专家寻求帮助。好了,我们有了一个标准,或者看起来像是标准,现在谁负责实现它呢?

16.3 哪里有好的库?
你离解决问题更近了一步。你知道你想使用的原语或协议,你也有了一个标准。同时,你也离规范更远了一步,这意味着你可能会产生错误。但首先,代码在哪里?
你四处看看,发现有许多可供您使用的库或框架。这是一个好问题。但是,你应该选择哪个库?哪一个最安全?这是一个难以回答的问题。一些库备受尊敬,我在本书中列出了一些:谷歌的 Tink、libsodium、cryptography.io 等等。
有时,找到一个好的库使用是困难的。也许你使用的编程语言对加密支持不多,或者你想使用的原语或协议没有那么多的实现。在这些情况下,谨慎一些并向加密社区寻求建议是很好的,看看库背后的作者,也许甚至向专家请求代码审查。例如,Reddit 上的 r/crypto 社区非常乐意帮助;直接给作者发邮件有时会奏效;在会议的开放麦克风环节询问观众也可能有所作用。
如果你处于绝望的境地,甚至可能不得不自己实现加密原语或协议。此时可能会出现许多问题,检查加密实现中常见问题是个好主意。幸运的是,如果你遵循一个好的标准,那么犯错就不那么容易了。但是,实现加密是一种艺术,如果可以避免的话,你不应该涉足其中。
一种有趣的测试加密实现的方法是使用工具。虽然没有单一的工具适用于所有加密算法,但谷歌的 Wycheproof 值得一提。Wycheproof 是一套测试向量,您可以使用它来查找常见加密算法(如 ECDSA、AES-GCM 等)中的棘手错误。该框架已被用于发现不同加密实现中令人印象深刻的大量错误。接下来,假设您没有自己实现加密,并找到了一个加密库。

16.4 加密的错误使用:开发者是敌人
你找到了一些可以使用的代码,你又前进了一步,然而你发现还有更多的机会引入错误。这就是应用密码学中大多数错误发生的地方。我们在本书中一再看到了错误使用密码学的例子:在 ECDSA(第七章)和 AES-GCM(第四章)等算法中重用 nonce 是不好的,当滥用哈希函数(第二章)时会出现碰撞,由于缺乏源身份验证(第九章),参与方可能会被冒充,等等。
结果显示,仅有 17%的错误出现在加密库中(这往往会产生严重后果),而其余 83%是由个别应用程序错误使用加密库造成的。
—David Lazar, Haogang Chen, Xi Wang, and Nickolai Zeldovich(“加密软件为什么会失败?案例研究和未解决的问题”,2014 年)
一般来说,原语或协议越抽象,使用起来就越安全。例如,AWS 提供了一个密钥管理服务(KMS),可以将您的密钥托管在 HSM 中,并按需执行加密计算。这样,加密就在应用程序级别上抽象化了。另一个例子是编程语言在其标准库中提供加密功能,这些功能通常比第三方库更可信。例如,Golang 的标准库非常出色。
加密库对可用性的关注通常可以概括为“将开发者视为敌人”。这是许多加密库采取的方法。例如,Google 的 Tink 不允许您在 AES-GCM(见第四章)中选择 nonce/IV 值,以避免意外的 nonce 重用。为了避免复杂性,libsodium 库只提供了一组固定的原语,而没有给您任何自由。一些签名库在签名中包装消息,强制您在发布消息之前验证签名,等等。在这个意义上,加密协议和库有责任尽可能地使其接口对误用具有抵抗力。
我以前说过,我会再次说一遍——确保您理解您正在使用的所有细节。正如您在本书中所看到的,错误使用加密原语或协议可能以灾难性的方式失败。阅读标准,阅读安全注意事项,阅读您加密库的手册和文档。这就是全部吗?嗯,并不完全是这样……您不是唯一的用户。

16.5 你在做错事:可用安全
使用加密可以在很多时候以透明的方式解决应用程序的问题,但并非总是如此!有时,加密的使用会泄露给应用程序的用户。
通常,教育只能起到有限的帮助作用。因此,当发生不良事件时,指责用户绝非明智之举。相关的研究领域被称为可用安全性,在这个领域里,人们致力于使安全和与密码学相关的功能对用户尽可能透明化,尽量消除用户滥用的机会。一个很好的例子是浏览器逐渐改变了在 SSL/TLS 证书无效时发出简单警告的方式,而是使用户更难接受风险。
我们观察到的行为与警告疲劳理论相一致。在 Google Chrome 中,用户对最常见的 SSL 错误的点击速度更快、频率更高。 [. . .] 我们还发现,对于 Google Chrome 的 SSL 警告,点击通过率高达 70.2%,这表明警告的用户体验对用户行为有巨大影响。
——Devdatta Akhawe 和 Adrienne Porter Felt(《警告国的爱丽丝:浏览器安全警告有效性的大规模现场研究》,2013)
另一个很好的例子是,安全敏感的服务已经从密码转向支持第二因素认证(在第十一章介绍)。因为强制用户使用强大的每服务密码太困难了,所以找到了另一个解决方案来消除密码泄露的风险。端到端加密也是一个很好的例子,因为用户始终很难理解他们的对话是端到端加密的意义,以及安全性有多大程度来自于他们主动验证指纹(在第十章介绍)。每当将密码学推给用户时,都必须付出巨大的努力来减少用户错误的风险。
故事时间
几年前,有人请我审查一个广泛使用的消息应用的端到端加密方案。该方案包括了常见的最先进协议,即 Signal 协议(在第十章介绍),但却没有提供用户验证其他用户的公钥(或会话密钥)的功能。这意味着,虽然在被动攻击者存在时,您的通信是端到端加密的,但恶意员工却可以轻易更新用户的公钥(或某些用户的会话密钥),而您却无法检测到中间人攻击。

16.6 密码学不是孤立存在的
密码学通常被用作更复杂系统的一部分,而这些系统也可能存在漏洞。实际上,大多数漏洞存在于与密码学本身无关的部分。攻击者通常寻找链条中最脆弱的环节,最容易攻击的目标,而密码学往往在提高门槛方面表现良好。涵盖系统可能更大更复杂,往往会产生更多可访问的攻击向量。阿迪·沙密尔曾经说过:“密码学通常是被绕过,而不是被突破。”
虽然努力确保系统中的密码学是保守的、实现良好且经过充分测试是好事,但也有益于确保对系统的其余部分也应用了同样程度的审查。否则,你可能白费了所有努力。

16.7 作为密码学从业者,不要自己设计加密算法
就是这样,这本书到此结束,你现在可以在荒野中自由驰骋。但我必须警告你,阅读这本书并不赋予你超能力;它只应该让你感到脆弱。让你意识到密码学很容易被误用,最简单的错误可能导致灾难性后果。请谨慎前行!
现在你已经掌握了丰富的密码学工具。你应该能够识别周围使用的密码学类型,甚至可能识别出可疑之处。你应该能够做出一些设计决策,知道如何在应用程序中使用密码学,并了解何时你或他人开始做一些可能需要更多关注的危险行为。永远不要犹豫寻求专家的意见。
“不要自己设计加密算法”可能是软件工程中最被滥用的密码学说法。然而,这些人在某种程度上是正确的。虽然你应该有能力实现甚至创建自己的加密原语和协议,但不应在生产环境中使用它。制作密码学需要多年才能做到正确:多年来学习该领域的方方面面,不仅从设计的角度,还从密码分析的角度。即使是终身研究密码学的专家也会构建破损的密码系统。Bruce Schneier 曾经著名地说过:“任何人,从最无知的业余爱好者到最优秀的密码学家,都可以创建一个他自己无法破解的算法。”此时,继续学习密码学就取决于你。这些最后的页面并不是旅程的终点。
我希望你意识到你处于一个特权地位。密码学起初是一个在闭门之后进行的领域,只限于政府成员或学术界保密,慢慢地演变成了今天的样子:一门在全世界公开研究的科学。但对于一些人来说,我们仍然处于(冷)战争时期。
2015 年,Rogaway 对密码学和物理学两个研究领域进行了有趣的比较。他指出,物理学在二战结束后不久就变成了一个高度政治化的领域。研究人员开始感到深刻的责任,因为物理学开始明显且直接地与许多人的死亡以及可能更多人的死亡相关联。不久之后,切尔诺贝利核事故将加剧这种感觉。
另一方面,密码学是一个经常被讨论隐私问题的领域,使密码学研究成为无政治性。然而,您和我做出的决定可能会对我们的社会产生长远影响。下次设计或实施使用密码学的系统时,请考虑您将使用的威胁模型。您是将自己视为可信方,还是以一种即使您也无法访问用户数据或影响其安全性的方式设计事物?您如何通过密码学赋予用户权力?您加密了什么?前 NSA 局长迈克尔·海登曾说:“我们根据元数据杀人” (mng .bz/PX19)。

2012 年,在圣巴巴拉海岸附近,数百名密码学家聚集在一个黑暗的讲堂里,聆听乔纳森·齐特兰的演讲,“加密的终结” (www.youtube.com/watch?v=3ijjHZHNIbU)。这是世界上最受尊敬的密码学会议 Crypto。乔纳森向房间里播放了来自电视剧《权力的游戏》的片段。在视频中,一个阉人瓦里斯向国王之手提利昂提出了一个谜语。这是谜语:
三位伟大的人坐在一个房间里:一个国王,一个神父和一个富人。他们之间站着一个普通的雇佣兵。每位伟大的人都让雇佣兵杀死其他两个。谁活着,谁死了?提利昂迅速回答:“取决于雇佣兵”,阉人回答:“如果是剑客统治,为什么我们要假装国王拥有所有权力?”。
乔纳森停止了视频并指向观众,对他们大喊道:“你们明白你们是雇佣兵,对吧?”
摘要
-
真实世界的密码学在应用方面往往失败。我们已经知道在大多数用例中使用的好原语和好协议,这使得它们的误用成为大多数错误的根源。
-
大多数典型用例已经通过加密原语和协议解决。大多数情况下,您只需找到一个受人尊敬的实现来解决您的问题。确保阅读手册并了解在什么情况下可以使用原语或协议。
-
真实世界的协议是通过像乐高积木一样组合加密原语构建的。当没有受人尊敬的协议解决您的问题时,您将不得不自己组装这些部件。这是极其危险的,因为加密原语有时在特定情况下使用或与其他原语或协议组合时会出现问题。在这些情况下,形式验证是发现问题的绝佳工具,尽管可能难以使用。
-
实施加密不仅仅是困难的;您还必须考虑难以误用的接口(好的加密代码留给用户的空间很小,不容易出错)。
-
保持保守,并使用经过验证的加密技术是避免后续问题的好方法。源自复杂性的问题(例如,支持过多的加密算法)是社区中的一个重要话题,远离过度设计的系统被称为“无聊的加密”。尽可能无聊。
-
加密原语和标准都可能由于过于复杂难以实现或者对实现者应该注意的事项描述不清而导致实现中的漏洞。有礼貌的加密是指一种难以糟糕实现的加密原语或标准的概念。要有礼貌。
-
应用中使用的加密有时会泄漏给用户。可用安全性是确保用户了解如何处理加密并且不会误用它的一种方式。
-
加密并不是孤立存在的。如果你遵循本书给出的所有建议,那么你的大多数错误可能会发生在系统的非加密部分。不要忽视这些!
-
通过这本书所学到的知识,确保对自己的工作负责,并且认真思考工作带来的后果。
附录:练习答案
第二章
如果哈希函数用作承诺方案,你能判断它是否提供隐藏和绑定吗?。
一个哈希函数 隐藏 在于其预像抗性属性;即,如果你的输入足够随机,以至于没有人能够猜测到它。为了解决这个问题,你可以生成一个随机数,并将其与你的输入进行哈希,稍后你可以公开你的输入和随机数来 打开 你的承诺。哈希函数 绑定 在于第二预像抗性属性。
顺便问一下,这个字符串有没有表示 256 位(32 字节)的方法?这样安全吗?。
我们不关心碰撞抗性。我们只关心第二预像抗性。因此,我们可以截断摘要以减小其大小。
你能猜到 Dread Pirate Roberts(Silk Road 网站管理员的化名)是如何得到包含网站名称的哈希的吗?。
Dread Pirate Roberts 创建了许多密钥,直到其中一个以那个酷炫的 base32 表示形式进行哈希。Facebook 也是这样做的,并且可以从 facebookcorewwwi.onion (facebook.com/notes/protect-the-graph/making-connections-to-facebook-more-secure/1526085754298237) 访问。这些被称为 虚荣地址。
第三章
你能想出可变长度计数器可能如何允许攻击者伪造身份认证标签吗?。
通过观察以下消息,其中 || 表示字符串连接,MAC(k, "1" || "1 is my favorite number"),攻击者可以伪造第十一条消息的有效认证标签,MAC(k, "11" | " is my favorite number")。
注意:并非所有的 MAC 都是 PRF。你能明白为什么吗?。
假设以下函数是有效的 MAC 和 PRF:MAC(key, input),那么以下函数是有效的 MAC 吗?NEW_MAC = MAC(key, input) || 0x01?它是有效的 PRF 吗?它是有效的 MAC,因为它防止伪造,但它不是有效的 PRF,因为你可以很容易地区分输出和完全随机的字符串(因为最后一个字节总是设置为 1)。
第六章
如果每个人都使用相同的共享秘密会很糟糕;你能明白为什么吗?。
如果我能用这个共享的秘密给你加密消息,我也可以解密其他人的消息。
你知道为什么不能立即使用密钥交换的输出吗?。
记住你在第五章关于密钥交换学到的知识。在 (FF)DH 中,计算是在一个大素数 p 的模下进行的。让我们以小素数为例,65,537. 以十六进制表示,我们的 p 写为 0x010001,而在二进制中,它写为 0000 0001 0000 0000 0000 0001。在二进制中,注意到第一个 1 之前的零,因为我们将我们的数字表示为字节(8 位的倍数)。
如果你了解模算术,你会知道对于这个质数p的模数永远不会更大,这意味着前 7 位将始终被设置为 0。此外,第八位大多数情况下会设置为 0 而不是 1。这不是均匀随机的。理想情况下,每一位都应该有相同的概率被设置为 1 或 0。
第七章
正如你在第三章中所看到的,由 MAC 产生的认证标签必须以恒定时间验证,以避免时间攻击。你认为我们需要对验证签名做同样的事情吗?。
不需要。这是因为验证认证标签涉及到一个秘密密钥。验证签名仅涉及公钥,因此不需要以恒定时间验证。
第八章
想象一下,将不同熵源混合在一起的方法只是简单地将它们进行异或。你能看到这可能会导致不可贡献吗?。
一个带有后门的熵源可以将其输出设置为所有其他熵源的异或,有效地将所有熵取消为 0。
BLS 等签名方案(见图 8.5 和第七章中提到)会产生唯一的签名,但对于 ECDSA 和 EdDSA 却不成立。你明白为什么吗?。
在 ECDSA 中,签名者可以选择不同的随机数产生相同密钥对和消息的不同签名。而 EdDSA 是一种签名算法,根据要签名的消息确定地推导出随机数,但这并不意味着签名者不能选择任何随机数。
第九章
如果服务器的私钥在某个时间点被泄露,那么中间人攻击者随后将能够解密所有先前记录的对话。你明白这是如何发生的吗?。
攻击者随后将能够倒带历史并在握手时模拟服务器。实际上,攻击者现在拥有了服务器的私钥。执行密钥交换和推导握手后对称密钥的所有其他信息都是公开的。
实际证书tbsCertificate中不包含值signatureAlgorithm和signatureValue。你知道为什么吗?。
认证机构(CA)需要签署证书,这导致了一个悖论:签名不能成为签名本身的一部分。因此,CA 必须将签名附加到证书上。其他标准和协议可能使用不同的技术。例如,你可以将签名作为tbsCertificate的一部分,并在签署或验证证书时假装它由全 0 组成。
第十章
你知道为什么电子邮件的内容在加密之前被压缩而不是之后吗?。
根据密码的定义,密文无法与随机字符串区分开来。因此,压缩算法无法找到有效压缩加密数据的模式。因此,压缩总是在加密之前应用。
你能想到一种明确的签署消息的方法吗?。
一句话:验证上下文。一种方法是在签名中包含发送者和接收者的姓名和他们的公钥,然后对其进行加密。
第十一章
有时应用程序试图通过让客户端在将密码发送到服务器之前进行哈希(也许使用密码哈希)来解决服务器在注册时了解用户密码的问题。你能确定这是否有效吗?。
仅仅使用客户端哈希无法防止像臭名昭著的传递哈希攻击那样的攻击(en.wikipedia.org/wiki/Pass_the_hash);如果服务器直接存储 Alice 的哈希密码,那么任何窃取它的人也可以将其用作 Alice 的密码进行身份验证。一些应用程序同时进行客户端哈希和服务器端哈希,这种情况下或许可以防止主动攻击者知道原始密码(尽管主动攻击者可能通过更新客户端应用程序的代码来禁用客户端哈希)。
想象一种协议,你必须输入正确的 4 位 PIN 码才能安全连接到设备。仅仅猜测正确的 PIN 码的机会是多少?。
这是一万分之一的机会猜对某件事。如果你在乐透中有这样的几率,你会很高兴的。


浙公网安备 33010602011771号