当前位置:   article > 正文

Python 全栈安全(一)

Python 全栈安全(一)

原文:annas-archive.org/md5/712ab41a4ed6036d0e8214d788514d6b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

序言

多年前,我在亚马逊搜索了一本基于 Python 的应用程序安全书。我以为会有多本书可供选择。已经有了很多其他主题的 Python 书籍,如性能、机器学习和 Web 开发。

令我惊讶的是,我搜索的那本书并不存在。我找不到一本关于我和我的同事们正在解决的日常问题的书。我们如何确保所有网络流量都是加密的?我们应该使用哪些框架来保护 Web 应用程序?我们应该用什么算法来对数据进行哈希或签名?

在接下来的几年里,我和我的同事们在确定一套标准的开源工具和最佳实践的同时,找到了这些问题的答案。在此期间,我们设计并实施了几个系统,保护了数百万新用户的数据和隐私。与此同时,有三家竞争对手遭受了黑客攻击。

像世界上其他人一样,我的生活在 2020 年初发生了变化。每条头条新闻都是关于 COVID-19 的,突然间远程工作成为了新常态。我认为可以说,每个人对这场大流行都有自己独特的反应;对我来说,是严重的无聊。

写这本书让我一举两得。首先,在疫情封锁的一年里,这是一种避免无聊的好方法。作为硅谷的居民,在 2020 年秋天,这个银色光辉在我这里被放大了。在此期间,一连串附近的山火破坏了该州大部分地区的空气质量,许多居民被困在家中。

其次,更重要的是,写这本书是非常令人满意的,因为我找不到可以购买的这本书。像许多硅谷初创公司一样,很多书最初的目的就是获得“作者”或“创始人”这样的头衔。但是,如果一家初创公司或一本书永远不会为他人产生价值,它必须解决现实世界的问题。

我希望这本书能让您解决许多您面临的真实安全问题。

致谢

写作需要很多孤独的努力。因此很容易忘记谁帮助了你。我想感谢以下人员帮助我(按我遇到他们的顺序)。

致 Kathryn Berkowitz,感谢你是世界上最好的高中英语老师。对于我造成的麻烦,我深感抱歉。致 Amit Rathore,我的 ThoughtQuitter 同伴,感谢你介绍我认识 Manning。我要感谢 Jay Fields、Brian Goetz 和 Dean Wampler,在我寻找出版商时给予的建议和支持。致 Cary Kempston,感谢你对认证团队的支持。没有实际工作经验,我就不应该写出这样的书。致 Mike Stephens,感谢你看过我的原始“手稿”并看到了潜力。我要感谢 Toni Arritola,我的开发编辑,教会我许多知识。我非常感谢你的反馈,通过它我学到了很多关于技术写作的东西。致我的技术编辑 Michael Jensen,感谢你周到的反馈和快速的交稿时间。你的评论和建议帮助这本书取得了成功。

最后,我要感谢在这一努力的开发阶段给予我时间和反馈的所有 Manning 评论员:Aaron Barton,Adriaan Beiertz,Bobby Lin,Daivid Morgan,Daniel Vasquez,Domingo Salazar,Grzegorz Mika,Håvard Wall,Igor van Oostveen,Jens Christian Bredahl Madsen,Kamesh Ganesan,Manu Sareena,Marc-Anthony Taylor,Marco Simone Zuppone,Mary Anne Thygesen,Nicolas Acton,Ninoslav Cerkez,Patrick Regan,Richard Vaughan,Tim van Deurzen,Veena Garapaty 和 William Jamir Silva,你们的建议帮助这本书更上一层楼。

关于本书

我用 Python 来教授安全,而不是反过来。换句话说,当你阅读本书时,你将学到更多关于安全而不是 Python 的知识。这有两个原因。首先,安全是复杂的,而 Python 并不是。其次,编写大量的自定义安全代码并不是保护系统的最佳方式;几乎总是应该把繁重的工作委托给 Python、一个库或一个工具。

本书涵盖了初学者和中级安全概念。这些概念是用初级 Python 代码实现的。无论是安全还是 Python 的材料都不是高级的。

谁应该阅读这本书

本书中的所有示例都模拟了在现实世界中开发和保护系统的挑战。因此,将代码推送到生产环境的程序员将学到最多。需要具备初级 Python 技能,或者对任何其他主要语言有中级经验。你当然不必是 web 开发人员才能从这本书中学习,但对 web 的基本了解会使吸收后半部分内容变得更容易。

也许你不是构建或维护系统的人;相反,你是测试它们。如果是这样,你将更深入地了解什么要测试,但我甚至不会试图教你如何测试。你知道,这是两种不同的技能。

与一些安全书籍不同,这里的例子都不假设攻击者的视角。因此,这个群体将学到最少。如果对他们来说有任何安慰的话,在一些章节中,我会让坏人获胜。

本书的组织方式:一份路线图

本书的第一章通过简要介绍安全标准、最佳实践和基础知识来设定期望。剩下的 17 章分为三个部分。

第 1 部分,“密码学基础”,通过少量的密码学概念奠定了基础。这部分内容在第 2 部分和第 3 部分中反复出现。

  • 第二章直接介绍了散列和数据完整性的密码学。在讲解的过程中,我介绍了一小组在全书中频繁出现的字符。

  • 第三章是从第二章中提取出来的。该章涉及到使用密钥生成和键控散列进行数据验证。

  • 第四章涵盖了任何安全书籍都必不可少的两个主题:对称加密和保密性。

  • 类似于第三章,第五章也是从其前一章中提取出来的。该章涵盖了非对称加密、数字签名和不可否认性。

  • 第六章将前几章的许多主要思想结合起来,形成了一个无处不在的网络协议,传输层安全性。

第 2 部分,“认证和授权”,包含了本书中最具商业价值的材料。该部分以大量关于与安全相关的常见用户工作流的实际操作指导为特点。

  • 第七章介绍了 HTTP 会话管理和 Cookies,为后续章节讨论的许多攻击做好了铺垫。

  • 第八章完全关于身份,介绍了用户注册和用户认证的工作流程。

  • 第九章介绍了密码管理,是我写作过程中最有趣的一章。这部分内容在之前的章节中得到了大量的延伸。

  • 第十章从认证转向了授权,介绍了关于权限和用户组的另一个工作流程。

  • 第十一章通过 OAuth 结束了第 2 部分,OAuth 是一个为了共享受保护资源而设计的行业标准授权协议。

读者们发现第 3 部分,“抵御攻击”,是本书中最具对抗性的部分。这部分内容更易于理解,也更加刺激。

  • 第十二章深入操作系统,涉及到文件系统、外部可执行文件和 Shell 等主题。

  • 第十三章教会你如何通过各种输入验证策略来抵制众多的注入攻击。

  • 第十四章完全专注于最臭名昭著的所有注入攻击,即跨站脚本攻击。你可能早就预料到了这一点。

  • 第十五章介绍了内容安全策略。在某种程度上,这可以被视为关于跨站脚本攻击的附加章节。

  • 第十六章介绍了跨站请求伪造。这一章结合了前几章的几个主题,并且遵循了 REST 的最佳实践。

  • 第十七章解释了同源策略,以及我们为什么会不时地使用跨源资源共享来放宽同源策略。

  • 第十八章以关于点击劫持的内容结束了本书,并提供了一些资源来保持你的技能更新。

关于代码

本书包含许多源代码示例,包括编号列表和与普通文本一致的代码。在这两种情况下,源代码都以固定宽度字体格式显示,以将其与普通文本分隔开。有时代码也会以**粗体**显示,以突出显示从本章的先前步骤更改的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已经重新格式化;我们添加了换行符并重新调整了缩进以适应书中可用的页面空间。在少数情况下,即使这样还不够,列表中也包含行续标记(➥)。此外,当文本描述代码时,源代码中的注释通常已从列表中删除。代码注释伴随着许多列表,突出显示重要概念。

liveBook 讨论论坛

购买《全栈 Python 安全》包括免费访问 Manning Publications 维护的私人网络论坛,您可以在论坛上发表对书籍的评论,提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问livebook.manning.com/book/practical-python-security/welcome/v-4/。您还可以在livebook.manning.com/#!/discussion了解有关 Manning 论坛和行为规则的更多信息。

Manning 对我们的读者的承诺是提供一个场所,个人读者和读者与作者之间可以进行有意义的对话。这并不意味着作者需要承诺任何特定数量的参与,作者对论坛的贡献仍然是自愿的(并且未付费的)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他失去兴趣!只要书籍在印刷中,论坛和以前讨论的存档将可以从出版商的网站访问。

关于作者

Dennis Byrne 是 23andMe 架构团队的成员,保护着超过 1000 万客户的基因数据和隐私。在加入 23andMe 之前,Dennis 曾是 LinkedIn 的软件工程师。Dennis 是一名健美运动员和全球水下探险者(GUE)洞穴潜水员。他目前住在硅谷,远离他成长并上学的阿拉斯加。

关于封面插图

全栈 Python 安全封面上的人物题为“Homme Touralinze”,是西伯利亚一个地区的图茂人。这幅插图选自雅克·格拉塞·德·圣索维尔(Jacques Grasset de Saint-Sauveur)(1757-1810)收集的各国服装集《不同国家的服装》,该书于 1797 年在法国出版。每幅插图都经过精细绘制和手工上色。格拉塞·德·圣索维尔收集的丰富多样性生动地提醒我们,仅仅 200 年前,世界各地的城镇和地区在文化上是多么分隔开的。人们彼此孤立,说着不同的方言和语言。在街上或乡间,仅仅通过他们的着装就能轻易地辨认出他们住在哪里,以及他们的贸易或社会地位。

我们的着装方式自那时起已经发生了变化,地区之间的多样性在当时是如此丰富,但现在已经逐渐消失了。现在很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。也许我们已经用更加多样化的个人生活换取了文化多样性——当然也换来了更加多样化和快节奏的技术生活。

在如今很难辨别一本电脑书和另一本电脑书的时代,曼宁庆祝计算机行业的创造力和主动性,其书籍封面基于两个世纪前丰富多样的地区生活,由格拉塞·德·圣索维尔(Grasset de Saint-Sauveur)的图片带回到人们的视线中。

第一章:深度防御

本章涵盖

  • 定义你的攻击面

  • 引入深度防御

  • 遵守标准、最佳实践和基本原则

  • 识别 Python 安全工具

你现在比以往任何时候都更加信任组织保管你的个人信息。不幸的是,其中一些组织已经将你的信息交给了攻击者。如果你觉得难以置信,可以访问haveibeenpwned.com。这个网站允许你轻松搜索一个包含数十亿被入侵账户的电子邮件地址的数据库。随着时间的推移,这个数据库只会变得更大。作为软件用户,通过这种共同经历,我们对安全有了一定的认识。

因为你打开了这本书,我敢打赌你对安全有了额外的认识。和我一样,你不仅想使用安全系统;你也想创建它们。大多数程序员重视安全,但他们并不总是有能力实现。我写这本书是为了为你提供一个建立这种背景的工具集。

安全是抵抗攻击的能力。本章从外部向内部分解安全性,从攻击开始。随后的章节涵盖了你在 Python 中实现防御层所需的工具。

每次攻击都始于一个入口点。一个特定系统的所有入口点的总和被称为攻击面。在一个安全系统的攻击面下面是安全层,一种被称为深度防御的架构设计。防御层遵循标准和最佳实践,以确保安全基础。

1.1 攻击面

信息安全已经从一小部分的做和不做发展成为一个复杂的学科。是什么驱使了这种复杂性?安全是复杂的,因为攻击是复杂的;出于必要性,它是复杂的。如今的攻击形式多种多样。在我们能够开发安全系统之前,我们必须对攻击有所了解。

正如我在前一节中所指出的,每次攻击都始于一个易受攻击的入口点,所有潜在入口点的总和就是你的攻击面。每个系统都有一个独特的攻击面。

攻击和攻击面处于不断变化的状态。攻击者随着时间变得更加复杂,新的漏洞也会定期被发现。保护你的攻击面因此是一个永无止境的过程,一个组织对这一过程的承诺应该是持续的。

攻击的入口点可以是系统的用户、系统本身,或者两者之间的网络。例如,攻击者可能通过电子邮件或聊天来针对用户作为某些形式攻击的入口点。这些攻击旨在诱使用户与恶意内容互动,以利用漏洞。这些攻击包括以下内容:

  • 反射性跨站脚本(XSS)

  • 社会工程(例如,网络钓鱼,短信欺诈)

  • 跨站请求伪造

  • 开放重定向攻击

或者,攻击者可能以系统本身为入口点进行攻击。这种形式的攻击通常旨在利用输入验证不足的系统。这些攻击的经典示例如下:

  • 结构化查询语言(SQL)注入

  • 远程代码执行

  • 主机头攻击

  • 拒绝服务

攻击者可能以用户和系统一起作为入口点,进行诸如持久性跨站脚本或点击劫持等攻击。最后,攻击者可能使用用户和系统之间的网络或网络设备作为入口点:

  • 中间人攻击

  • 重放攻击

本书教会你如何识别和抵御这些攻击,其中一些攻击有一个专门的章节(XSS 有可能有两个章节)。图 1.1 描绘了一个典型软件系统的攻击表面。四名攻击者同时向这个攻击表面施加压力,用虚线表示。尽量不要被细节淹没。这只是为你提供一个高层次概述。到本书结束时,你将了解每种攻击是如何工作的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.1 四名攻击者同时通过用户、系统和网络对攻击表面施加压力。

在每个安全系统的攻击表面下都有防御层;我们不只是保护周边。正如本章开头所述,这种分层的安全方法通常称为防御深度

1.2 防御深度

防御深度,一种源自国家安全局内部的哲学,认为系统应该通过多层安全来应对威胁。每一层安全都是双重目的:它抵御攻击,并在其他层失败时充当备份。我们从不把所有的鸡蛋放在一个篮子里;即使是优秀的程序员也会犯错误,而且定期会发现新的漏洞。

让我们首先通过比喻来探讨防御的深度。想象一座只有一层防御的城堡,即一支军队。这支军队经常保卫城堡免受攻击者的攻击。假设这支军队有 10%的失败几率。尽管军队很强大,国王对当前的风险水平感到不安。你或我能否接受一个无法抵御所有攻击的系统?我们的用户能否接受这一点?

国王有两个选项来降低风险。一个选择是加强军队。这是可能的,但不是经济有效的。消除最后 10%的风险将比消除前 10%的风险显然要昂贵得多。国王决定不是加强军队,而是通过在城堡周围挖掘一道护城河来增加另一层防御。

护城河能减少多少风险?只有军队和护城河都失败了,城堡才会被攻陷,因此国王用简单的乘法计算风险。如果像军队一样,护城河有 10% 的失败几率,那么每次攻击成功的几率就是 10% × 10%,或者 1%。想象一下,与建造一个有 1% 失败几率的军队相比,仅仅挖个坑并注水填满要花多少钱。

最后,国王在城堡周围修建了一堵墙。像军队和护城河一样,这堵墙有 10% 的失败几率。现在,每次攻击的成功几率是 10% × 10% × 10%,或者 0.1%。

防御的成本效益分析归结为算术和概率。增加另一层始终比试图完善单一层更具成本效益。深度防御认识到完美的徒劳;这是一种优势,而不是一种弱点。

随着时间的推移,一种防御层的实现比其他层更成功和流行;挖护城河的方式有限。一个常见问题的常见解决方案出现了。安全社区开始认识到一个模式,并且一种新技术从实验性发展到标准化。标准化机构评估该模式,讨论细节,定义规范,一个安全标准就诞生了。

1.2.1 安全标准

许多成功的安全标准是由国家标准与技术研究院(NIST)、互联网工程任务组(IETF)和万维网联盟(W3C)等组织建立的。通过本书,你将学习如何使用以下标准来保护系统:

  • 高级加密标准 (AES) — 一种对称加密算法。

  • 安全哈希算法 2 (SHA-2) — 一族密码哈希函数。

  • 传输层安全 (TLS) — 一种安全的网络协议。

  • OAuth 2.0 — 一种用于共享受保护资源的授权协议。

  • 跨源资源共享 (CORS) — 浏览器的资源共享协议。

  • 内容安全策略 (CSP) — 一种基于浏览器的攻击缓解标准。

为什么要标准化?安全标准为程序员提供了一个构建安全系统的共同语言。共同语言使来自不同组织的不同人员能够使用不同工具构建可互操作的安全软件。例如,一个 web 服务器向每种类型的浏览器提供相同的 TLS 证书;浏览器可以理解来自每种类型的 web 服务器的 TLS 证书。

此外,标准化促进了代码重用。例如,oauthlib 是 OAuth 标准的通用实现。这个库被 Django OAuth Toolkit 和 flask-oauthlib 包装,允许 Django 和 Flask 应用程序都使用它。

我会坦诚地告诉你:标准化并不能神奇地解决每个问题。有时候,一个漏洞是在大家都接受标准几十年后才被发现的。2017 年,一组研究人员宣布他们已经破解了 SHA-1 (shat tered.io/),一个之前享受了 20 多年行业应用的加密哈希函数。有时候,供应商不会在相同的时间范围内实施标准。每个主要浏览器支持某些 CSP 功能花了好几年的时间。尽管如此,标准化大部分时间确实是有效的,我们不能忽视它。

几个最佳实践已经发展出来以补充安全标准。防御深度本身就是一种最佳实践。像标准一样,安全系统遵循最佳实践;与标准不同,最佳实践没有具体规范。

1.2.2 最佳实践

最佳实践 不是由标准机构制定的产品;相反,它们是由模因、口口相传和像这本书一样的书定义的。这些是你必须做的事情,有时候你是独自一人。通过阅读本书,你将学会如何识别和追求这些最佳实践:

  • 在传输和静止状态下的加密

  • “不要自己造加密算法”

  • 最小权限原则

数据要么在传输中,要么在处理中,要么在静止中。当安全专家说“传输和静止状态下的加密”时,他们建议在数据在计算机之间移动时和写入存储时都进行加密。

当安全专家说“不要自己造加密算法”的时候,他们建议你重用经验丰富的专家的工作,而不是试图自己实现。依赖工具并不仅仅是为了满足紧迫的期限和写更少的代码。它变得流行是为了安全起见。不幸的是,许多程序员通过艰难的方式学到了这一点。通过阅读本书,你也将学会这一点。

最小权限原则PLP)保证用户或系统仅获得执行其职责所需的最小权限。在本书中,PLP 被应用于许多主题,如用户授权、OAuth 和 CORS。

图 1.2 描述了一个典型软件系统的安全标准和最佳实践的安排。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.2 防御深度应用于具有安全标准和最佳实践的典型系统

无一层防御是万能药。没有安全标准或最佳实践能够独立解决所有安全问题。因此,这本书的内容,就像大多数 Python 应用程序一样,包含了许多标准和最佳实践。把每一章都看作是一个额外防御层的蓝图。

安全标准和最佳实践可能看起来听起来不同,但在幕后,每一个都只是应用相同基本原理的不同方式。这些基本原理代表了系统安全的最原子单位。

1.2.3 安全基础

安全基础知识反复出现在安全系统设计和本书中。算术与代数或三角函数之间的关系类似于安全基础知识与安全标准或最佳实践之间的关系。通过阅读本书,您将学习如何通过结合这些基础知识来保护系统:

  • 数据完整性—数据是否改变了?

  • 认证—你是谁?

  • 数据认证—谁创作了这个数据?

  • 不可否认性—谁做了什么?

  • 授权—你可以做什么?

  • 机密性—谁可以访问这个?

数据完整性,有时也称为消息完整性,确保数据没有意外损坏(比特腐败)。它回答了“数据是否改变了?”的问题。数据完整性保证了数据被读取的方式与其被写入的方式相同。数据读者可以验证数据的完整性,无论谁创作了它。

认证回答了“你是谁?”的问题。我们每天都在进行这项活动;这是验证某人或某物身份的行为。当一个人能成功回应用户名和密码的挑战时,身份得到了验证。不过,认证不仅仅适用于人,机器也可以被认证。例如,一个持续集成服务器在从代码仓库拉取更改之前进行身份验证。

数据认证,通常称为消息认证,确保数据读者可以验证数据写入者的身份。它回答了“谁创作了这个数据?”的问题。与数据完整性一样,当数据读者和写入者不同时,以及当数据读者和写入者相同时,数据认证也适用。

不可否认性回答了“谁做了什么?”的问题。它保证了个人或组织没有否认其行为的方式。不可否认性可以应用于任何活动,但对于在线交易和法律协议至关重要。

授权,有时也称为访问控制,经常与认证混淆。这两个术语听起来相似,但代表着不同的概念。正如先前所述,认证回答了“你是谁?”的问题。而授权则回答了“你可以做什么?”的问题。阅读电子表格、发送电子邮件和取消订单都是用户可能被授权或未被授权做的操作。

机密性回答了“谁可以访问这个?”的问题。这个基础保证了两个或更多方可以私下交换数据。以保密方式传输的信息不能被未经授权的任何方以任何有意义的方式阅读或解释。

本书教会您如何使用这些基础知识构建解决方案。表 1.1 列出了每个基础知识和其对应的解决方案。

表 1.1 安全基础知识

建筑块解决方案
数据完整性安全网络协议版本控制包管理
认证用户认证系统认证
数据认证用户注册用户登录工作流密码重置工作流用户会话管理
不可否认性在线交易数字签名可信第三方
授权用户授权系统对系统授权文件系统访问授权
机密性加密算法安全网络协议

安全基础互相补充。单独使用每个基础并不是很有用,但是当它们结合在一起时就变得强大了。让我们考虑一些例子。假设一个电子邮件系统提供数据认证但不提供数据完整性。作为电子邮件接收者,你可以验证电子邮件发送者的身份(数据认证),但你无法确定电子邮件在传输过程中是否被修改。这并不是很有用,对吧?如果你无法验证实际数据,那么验证数据编写者的身份有什么意义呢?

想象一个新颖的网络协议,它保证了机密性但没有认证。窃听者无法访问你使用该协议发送的信息(机密性),但你无法确定你正在向谁发送数据。事实上,你可能正在向窃听者发送数据。上次你想与某人进行私人对话而不知道你在与谁交谈时是什么时候?通常,如果你想交换敏感信息,你也希望与你信任的人或事物进行交流。

最后,考虑一个支持授权但不支持认证的在线银行系统。这家银行始终确保你的资金由你管理;它只是不会要求你首先证明你的身份。一个系统如何在不知道用户是谁的情况下授权用户呢?显然,我们中没有人会把钱存入这家银行。

安全基础是安全系统设计的最基本构建模块。我们不能一遍又一遍地应用相同的基础。相反,我们必须混合搭配它们来构建防御层。对于每个防御层,我们希望将繁重的工作委托给工具。其中一些工具是 Python 的本机工具,其他工具则通过 Python 包提供。

1.3 工具

本书中的所有示例都是用 Python 编写的(具体来说是 3.8 版本)。为什么选择 Python?嗯,你不想读一本过时的书,我也不想写一本。Python 很受欢迎,而且越来越受欢迎。

编程语言流行度 (PYPL) 指数是基于谷歌趋势数据的编程语言流行度衡量标准。截至 2021 年中期,Python 在 PYPL 指数(pypl.github.io/PYPL.html)上排名第一,市场份额为 30%。在过去五年中,Python 的流行度增长超过了其他任何编程语言。

为什么 Python 如此受欢迎?对于这个问题有很多答案。大多数人似乎都同意有两个因素。首先,Python 是一种适合初学者的编程语言。它易于学习、阅读和编写。其次,Python 生态系统已经爆炸式增长。2017 年,Python 包索引(PyPI)达到了 100,000 个包。这个数字仅用了两年半的时间就翻了一番。

我不想写一本只涵盖 Python Web 安全的书。因此,一些章节介绍了诸如加密、密钥生成和操作系统等主题。我使用一些与安全相关的 Python 模块探讨这些主题:

有些工具有专门的章节,其他工具则遍布全书。还有一些工具仅有简短的介绍。你将学到关于以下工具的一些或许很多的知识:

Web 服务器占据了典型攻击面的很大一部分。因此,本书有许多章节专门讨论保护 Web 应用程序的问题。在这些章节中,我不得不问自己一个许多 Python 程序员熟悉的问题:Flask 还是 Django?这两个框架都值得尊重;它们之间的主要区别是极简主义与开箱即用功能之间的差异。相对于彼此,Flask 默认使用基本功能,而 Django 默认使用功能丰富的功能。

作为一个极简主义者,我喜欢 Flask。不幸的是,它将极简主义应用到了许多安全功能上。使用 Flask,你的大部分防御层都是委托给第三方库的。另一方面,Django 则较少依赖第三方支持,具有许多内置的保护功能,默认情况下启用。在这本书中,我使用 Django 来演示 Web 应用程序安全性。当然,Django 不是万能的;我还使用了以下第三方库:

图 1.3 展示了一个由这套工具组成的栈。在这个栈中,Gunicorn 通过 TLS 中继用户和服务器之间的流量。用户输入通过 Django 表单验证、模型验证和对象关系映射(ORM)进行验证;系统输出通过 HTML 转义进行清理。django-cors-headersdjango-csp 确保每个出站响应都使用适当的 CORS 和 CSP 标头进行锁定。hashlibhmac 模块执行哈希运算;cryptography 包执行加密操作。requests-oauthlib 与 OAuth 资源服务器进行接口交互。最后,Pipenv 防止包存储库中的漏洞。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1.3 一套常见 Python 组件的完整堆栈,在每个层级抵抗某种形式的攻击

这本书对框架和库没有偏见;它不偏袒任何一方。如果你钟爱的开源框架被另一种选择所取代,请不要把它当成个人攻击。这本书涵盖的每个工具都是通过问两个问题来选择的:

  • 这个工具成熟吗? 我们俩最后不应该把职业生涯押在一个刚出生的开源框架上。我故意不涉及尖端工具;这叫做“尖端”不是没有原因的。按照定义,处于这个开发阶段的工具不能被认为是安全的。因此,这本书中的所有工具都是成熟的;这里的一切都经过了实战考验。

  • 这个工具受欢迎吗? 这个问题与未来比过去更有关系,与过去无关。具体来说,读者在未来使用这个工具的可能性有多大?无论我使用哪种工具来演示一个概念,记住最重要的是概念本身。

1.3.1 保持务实

这是一本实用手册,而不是教科书;我更注重专业人士而不是学生。这并不是说安全的学术方面不重要。它非常重要。但安全和 Python 是广阔的主题。本材料的深度被限制在对目标受众最有用的内容上。

在这本书中,我涵盖了一些用于哈希和加密的功能。我不涉及这些功能背后的繁重数学。你将学习这些功能的行为;你不会学到这些功能是如何实现的。我会告诉你何时以及何时不应该使用它们。

阅读这本书会让你成为一个更好的程序员,但这并不能让你成为一个安全专家。没有一本书能做到这一点。不要相信那些承诺能做到这一点的书。阅读这本书,并写一个安全的 Python 应用程序!让现有系统更安全。自信地将你的代码推向生产环境。但不要将你的 LinkedIn 资料标题设置为密码学家

概要

  • 每次攻击都始于一个入口点,对于单个系统,这些入口点的总和被称为攻击面。

  • 攻击复杂性推动了深度防御的需求,这是一种以层为特征的架构方法。

  • 许多防御层坚持安全标准和最佳实践,以促进互操作性、代码重用和安全性。

  • 在底层,安全标准和最佳实践是应用相同基本概念的不同方式。

  • 你应该努力将繁重的工作委托给像框架或库这样的工具;许多程序员都是通过艰苦的方式学会这一点的。

  • 通过阅读这本书,你会成为一个更好的程序员,但这并不会让你成为一个密码学专家。

第一部分:密码学基础

我们每天都依赖哈希、加密和数字签名。在这三者中,加密通常是最受关注的。它在会议上、讲堂上以及主流媒体中更受关注。程序员们通常也更有兴趣学习它。

本书的第一部分反复展示了为什么哈希和数字签名与加密一样重要。此外,本书的后续部分展示了这三者的重要性。因此,第 2 至 6 章本身就很有用,但它们也有助于你理解许多后续章节。

第二章:哈希

本章涵盖

  • 定义哈希函数

  • 引入安全原型

  • 使用哈希验证数据完整性

  • 选择加密哈希函数

  • 使用hashlib模块进行加密哈希处理

在本章中,您将学习如何使用哈希函数来确保数据完整性,这是安全系统设计的基本构建块。您还将学习如何区分安全和不安全的哈希函数。在此过程中,我将向您介绍爱丽丝、鲍勃和其他几个原型角色。我使用这些角色贯穿整本书来说明安全概念。最后,您将学习如何使用hashlib模块对数据进行哈希处理。

2.1 什么是哈希函数?

每个哈希函数都有输入和输出。哈希函数的输入称为消息。消息可以是任何形式的数据。葛底斯堡演说、一张猫的图片和一个 Python 包都是潜在消息的例子。哈希函数的输出是一个非常大的数字。这个数字有许多名称:哈希值哈希哈希码摘要消息摘要

在这本书中,我使用术语哈希值。哈希值通常表示为字母数字字符串。哈希函数将一组消息映射到一组哈希值。图 2.1 说明了消息、哈希函数和哈希值之间的关系。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.1 哈希函数将一个称为消息的输入映射到一个称为哈希值的输出。

在这本书中,我将每个哈希函数描绘为一个漏斗。哈希函数和漏斗都接受可变大小的输入并产生固定大小的输出。我将每个哈希值描绘为一个指纹。哈希值和指纹分别唯一标识一条消息或一个人。

哈希函数彼此之间是不同的。这些差异通常归结为本节中定义的属性。为了说明前几个属性,我们将使用一个内置的 Python 函数,方便地命名为hash。Python 使用这个函数来管理字典和集合,而你和我将用它来进行教学目的。

内置的hash函数是介绍基础知识的好方法,因为它比本章后面讨论的哈希函数要简单得多。内置的hash函数接受一个参数,即消息,并返回一个哈希值:

$ python
>>> message = 'message'   # ❶
>>> hash(message)
2010551929503284934       # ❷
  • 1
  • 2
  • 3
  • 4

❶ 消息输入

❷ 哈希值输出

哈希函数具有三个基本属性:

  • 确定性行为

  • 固定长度的哈希值

  • 雪崩效应

确定性行为

每个哈希函数都是确定性的:对于给定的输入,哈希函数总是产生相同的输出。换句话说,哈希函数的行为是可重复的,而不是随机的。在 Python 进程中,内置的hash函数对于给定的消息值始终返回相同的哈希值。在交互式 Python shell 中运行以下两行代码。你的哈希值将匹配,但会与我的不同:

>>> hash('same message')
1116605938627321843        # ❶
>>> hash('same message')
1116605938627321843        # ❶
  • 1
  • 2
  • 3
  • 4

❶ 相同的哈希值

我在本章后面讨论的哈希函数是普遍确定性的。这些函数无论在何时何地调用,行为都是相同的。

固定长度的哈希值

消息具有任意长度;对于特定哈希函数,哈希值具有固定长度。如果一个函数不具备这个属性,那么它就不符合哈希函数的标准。消息的长度不会影响哈希值的长度。将不同的消息传递给内置的hash函数将给出不同的哈希值,但每个哈希值始终是一个整数。

雪崩效应

当消息之间的微小差异导致哈希值之间的巨大差异时,哈希函数被认为表现出雪崩效应。理想情况下,每个输出位都取决于每个输入位:如果两个消息只有一个位不同,那么平均只有一半的输出位应该匹配。哈希函数的评判标准是它与理想情况有多接近。

看一下以下代码。字符串和整数对象的哈希值都具有固定长度,但只有字符串对象的哈希值表现出雪崩效应:

>>> bin(hash('a'))
'0b100100110110010110110010001110011110011111011101010000111100010'
>>> bin(hash('b'))
'0b101111011111110110110010100110000001010000011110100010111001110'
>>> 
>>> bin(hash(0))
'0b0'
>>> bin(hash(1))
'0b1'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

内置的hash函数是一个很好的教学工具,但不能被视为加密哈希函数。接下来的部分将阐述这一点的三个原因。

2.1.1 加密哈希函数属性

加密哈希函数必须满足三个额外的标准:

  • 单向函数属性

  • 弱碰撞抗性

  • 强碰撞抗性

这些属性的学术术语分别是前像抗性第二前像抗性碰撞抗性。为了讨论方便,我避免使用学术术语,这并不是对学者们的有意不敬。

单向函数

用于加密目的的哈希函数,没有例外,必须是单向函数。如果一个函数易于调用但难以逆向工程,则称其为单向函数。换句话说,如果你有输出,那么很难确定输入。如果攻击者获得了一个哈希值,我们希望他们很难弄清楚消息是什么。

有多难?我们通常使用不可行这个词。这意味着非常困难—难到攻击者只有一个选择,如果他们想要逆向工程消息:暴力破解。

暴力破解是什么意思?每个攻击者,即使是一个不成熟的攻击者,也能够编写一个简单的程序来生成大量的消息,对每个消息进行哈希,并将每个计算出的哈希值与给定的哈希值进行比较。这是一个暴力破解攻击的例子。攻击者需要大量的时间和资源,而不是智力。

需要多少时间和资源?这是主观的。答案并非一成不变。例如,对本章后面讨论的一些哈希函数进行理论暴力攻击将需要数百万年和数十亿美元。一个理性的安全专业人士会认为这是不可行的。这并不意味着不可能。我们认识到没有完美的哈希函数,因为暴力攻击始终是攻击者的一个选择。

不可行性是一个不断变化的目标。几十年前被认为不可行的暴力攻击,今天或明天可能就变得实际。随着计算机硬件成本的持续下降,暴力攻击的成本也在降低。不幸的是,加密强度随着时间的推移而减弱。不要把这理解为每个系统最终都会变得脆弱。相反,要明白每个系统最终都必须使用更强大的哈希函数。本章将帮助您就使用哪些哈希函数做出明智的决定。

碰撞抗性

用于加密目的的哈希函数,没有例外,必须具有碰撞抗性。什么是碰撞?虽然不同消息的哈希值具有相同的长度,但它们几乎永远不会具有相同的值…几乎。当两个消息的哈希值相同时,称为碰撞。碰撞是不好的。哈希函数被设计来最小化碰撞。我们根据它们避免碰撞的能力来评判哈希函数;有些比其他的更好。

如果给定一个消息,一个哈希函数具有弱碰撞抗性,那么识别出一个第二个消息的哈希值与之相同是不可行的。换句话说,如果攻击者有一个输入,识别出另一个能够产生相同输出的输入是不可行的。

如果一个哈希函数具有强碰撞抗性,那么找到任何碰撞都是不可行的。弱碰撞抗性和强碰撞抗性之间的区别微妙。弱碰撞抗性限定于特定的给定消息;强碰撞抗性适用于任何一对消息。图 2.2 说明了这种差异。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.2 弱碰撞抗性与强碰撞抗性的比较

强碰撞抗性意味着弱碰撞抗性,反之则不然。任何具有强碰撞抗性的哈希函数也具有弱碰撞抗性;具有弱碰撞抗性的哈希函数不一定具有强碰撞抗性。因此,强碰撞抗性是一个更大的挑战;这通常是攻击者或研究人员破解加密哈希函数时首先丢失的属性。本章后面,我将向您展示一个现实世界中的例子。

关键词是不可行。尽管识别一个无碰撞的哈希函数会有多好,但我们永远也找不到,因为它根本不存在。想想看。消息可以有任意长度;哈希值只能有一个长度。因此,所有可能消息的集合总是大于所有可能哈希值的集合。这被称为鸽巢原理

在本节中,您了解了哈希函数是什么。现在是时候学习哈希如何确保数据完整性了。但首先,我将向您介绍一些原型角色。我在整本书中使用这些角色来说明安全概念,从本章开始讲述数据完整性。

2.2 原型角色

我在这本书中使用五个原型角色来说明安全概念(见图 2.3)。相信我,这些角色使阅读(和写作)这本书变得更容易。这本书中的解决方案围绕爱丽丝和鲍勃面临的问题展开。如果你读过其他安全书籍,你可能已经遇到过这两个角色。爱丽丝和鲍勃就像你一样——他们希望安全地创建和共享信息。偶尔,他们的朋友查理也会出现。这本书中每个示例的数据往往在爱丽丝、鲍勃和查理之间流动;记住 A、B 和 C。爱丽丝、鲍勃和查理是好角色。在阅读本书时,可以随意与这些角色产生共鸣。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.3 带光环的原型角色是好的;攻击者被指定为有角的。

伊芙和玛洛丽是坏角色。记住伊芙是邪恶的。记住玛洛丽是恶意的。这些角色通过试图窃取或修改他们的数据和身份来攻击爱丽丝和鲍勃。伊芙是被动攻击者;她是窃听者。她倾向于向攻击面的网络部分靠拢。玛洛丽是主动攻击者;她更加复杂。她倾向于使用系统或用户作为入口点。

记住这些角色;你会再次见到它们。爱丽丝、鲍勃和查理有光环;伊芙和玛洛丽有角。在下一节中,爱丽丝将使用哈希来确保数据完整性。

2.3 数据完整性

数据 完整性,有时被称为消息完整性,是确保数据没有意外修改的保证。它回答了这个问题,“数据是否改变了?”假设爱丽丝在一个文档管理系统上工作。目前,该系统存储每个文档的两份副本以确保数据完整性。为了验证文档的完整性,系统逐字节比较这两份副本。如果副本不匹配,文档被视为损坏。爱丽丝对系统消耗的存储空间感到不满。成本已经失控,随着系统容纳更多文档,问题变得更加严重。

爱丽丝意识到她有一个常见的问题,并决定用一个常见的解决方案来解决它,即一个加密散列函数。当每个文档被创建时,系统会计算并存储它的散列值。为了验证每个文档的完整性,系统首先重新计算其散列值。然后将新的散列值与存储中的旧散列值进行比较。如果散列值不匹配,则认为文档已损坏。

图 2.4 用四个步骤说明了这个过程。一个拼图图案描述了两个散列值的比较。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2.4 爱丽丝通过比较散列值而不是文档来确保数据完整性。

你能看出碰撞抵抗为什么很重要吗?假设爱丽丝使用的散列函数缺乏碰撞抵抗性。如果原始文件版本与损坏版本发生碰撞,系统就无法绝对地检测到数据损坏。

这一部分展示了散列的一个重要应用:数据完整性。在下一节中,你将学习如何选择一个适合做这件事的实际散列函数。

2.4 选择加密散列函数

Python 原生支持加密散列。无需第三方框架或库。Python 自带一个 hashlib 模块,提供了大多数程序员需要的加密散列的一切。algorithms_guaranteed 集合包含了保证在所有平台上可用的每个散列函数。这个集合中的散列函数代表了你的选择。很少有 Python 程序员会需要或者甚至看到这个集合之外的散列函数:

>>> import hashlib
>>> sorted(hashlib.algorithms_guaranteed)
['blake2b', 'blake2s', 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', 'sha512', 'shake_128',
 'shake_256']
  • 1
  • 2
  • 3
  • 4
  • 5

面对如此多的选择,感到不知所措是很自然的。在选择散列函数之前,我们必须将选项划分为安全和不安全的选项。

2.4.1 哪些散列函数是安全的?

algorithms_guaranteed 的安全和可靠的散列函数属于以下散列算法族:

  • SHA-2

  • SHA-3

  • BLAKE2

SHA-2

SHA-2 散列函数族于 2001 年由 NSA 发布。该族由 SHA-224、SHA-256、SHA-384 和 SHA-512 组成。SHA-256 和 SHA-512 是该族的核心。不必费心记住所有四个函数的名称;现在只需关注 SHA-256 即可。在本书中你会经常看到它。

你应该用 SHA-256 进行通用加密散列。这是一个很容易的决定,因为我们所使用的每个系统都已经在使用它。我们部署应用程序所依赖的操作系统和网络协议都依赖于 SHA-256,所以我们别无选择。你必须非常努力才能不使用 SHA-256。它安全、可靠、受到良好支持,并且被广泛使用。

SHA-2 家族中每个函数的名称方便地自述其哈希值长度。哈希函数通常根据其哈希值的长度进行分类、评判和命名。例如,SHA-256 是一个产生——你猜对了——长度为 256 位的哈希值的哈希函数。较长的哈希值更有可能是唯一的,更不容易发生碰撞。越长越好。

SHA-3

SHA-3哈希函数家族由 SHA3-224、SHA3-256、SHA3-384、SHA3-512、SHAKE128 和 SHAKE256 组成。SHA-3 是安全的、可靠的,并被许多人认为是 SHA-2 的自然继任者。不幸的是,在撰写本文时,SHA-3 的采用尚未获得动力。如果您在高安全环境中工作,应考虑使用 SHA3-256 等 SHA-3 函数。只需注意,您可能无法找到与 SHA-2 存在的相同支持水平。

BLAKE2

BLAKE2不像 SHA-2 或 SHA-3 那样受欢迎,但有一个很大的优势:BLAKE2 利用现代 CPU 架构以极快的速度进行哈希。因此,如果您需要对大量数据进行哈希,应考虑使用 BLAKE2。BLAKE2 有两种版本:BLAKE2b 和 BLAKE2s。BLAKE2b 针对 64 位平台进行了优化。BLAKE2s 针对 8 到 32 位平台进行了优化。

现在您已经学会了如何识别和选择安全的哈希函数,您准备好学习如何识别和避免不安全的哈希函数了。

2.4.2 哪些哈希函数是不安全的?

algorithms_guaranteed中的哈希函数是流行的跨平台的。这并不意味着它们每一个都是密码学安全的。Python 中保留了不安全的哈希函数,以保持向后兼容性。了解这些函数是值得的,因为您可能会在传统系统中遇到它们。algorithms_guaranteed的不安全哈希函数如下:

MD5

MD5是在上世纪 90 年代初开发的过时的 128 位哈希函数。这是有史以来最常用的哈希函数之一。不幸的是,尽管研究人员早在 2004 年就展示了 MD5 碰撞,但 MD5 仍在使用中。今天,密码分析师可以在不到一个小时的时间内在商品硬件上生成 MD5 碰撞。

SHA-1

SHA-1是由 NSA 在上世纪 90 年代中期开发的过时的 160 位哈希函数。像 MD5 一样,这个哈希函数曾经很受欢迎,但现在不再被认为是安全的。SHA-1 的第一个碰撞是在 2017 年由 Google 和荷兰研究机构 Centrum Wiskunde & Informatica 的合作努力宣布的。在理论上,这一努力剥夺了 SHA-1 的强碰撞抵抗力,而不是弱碰撞抵抗力。

许多程序员熟悉 SHA-1,因为它用于验证版本控制系统(如 Git 和 Mercurial)中的数据完整性。这两个工具都使用 SHA-1 哈希值来标识并确保每个提交的完整性。Git 的创建者 Linus Torvalds 在 2007 年的 Google Tech Talk 中说:“就 Git 而言,SHA-1 甚至不是一个安全功能。它纯粹是一种一致性检查。”

警告:在创建新系统时,不应将 MD5 或 SHA-1 用于安全目的。任何使用这两个函数用于安全目的的遗留系统都应重构为安全替代方案。这两个函数都很流行,但 SHA-256 是流行且安全的。它们都很快,但 BLAKE2 更快更安全。

下面是选择哈希函数时的 dos 和 don’ts 摘要:

  • 用于一般目的的加密哈希,请使用 SHA-256。

  • 在高安全环境中,请使用 SHA3-256,但预期的支持会比 SHA-256 较少。

  • 请使用 BLAKE2 对大消息进行哈希处理。

  • 永远不要将 MD5 或 SHA1 用于安全目的。

现在您已经学会如何选择安全的加密哈希函数了,让我们在 Python 中应用这个选择。

2.5 Python 中的加密哈希处理

hashlib 模块提供了每个哈希函数的命名构造函数,在 hashlib.algorithms_guaranteed 中。或者,每个哈希函数都可以通过通用构造函数 new 动态访问。该构造函数接受 algorithms_guaranteed 中的任何字符串。命名构造函数比通用构造函数更快,更受欢迎。下面的代码演示了如何使用这两种构造函数类型构造 SHA-256 的实例:

import hashlib

named = hashlib.sha256()           # ❶
generic = hashlib.new('sha256')    # ❷
  • 1
  • 2
  • 3
  • 4

❶ 命名构造函数

❷ 通用构造函数

可以使用或不使用消息初始化哈希函数实例。下面的代码初始化了一个带有消息的 SHA-256 函数。与内置的 hash 函数不同,hashlib 中的哈希函数要求消息的类型为字节:

>>> from hashlib import sha256
>>> 
>>> message = b'message'
>>> hash_function = sha256(message)
  • 1
  • 2
  • 3
  • 4

无论如何创建,每个哈希函数实例都有相同的 API。对于 SHA-256 实例的公共方法类似于对于 MD5 实例的公共方法。digesthexdigest 方法分别返回哈希值作为字节和十六进制文本:

>>> hash_function.digest()        # ❶
b'\xabS\n\x13\xe4Y\x14\x98+y\xf9\xb7\xe3\xfb\xa9\x94\xcf\xd1\xf3\xfb"\xf7\x
1c\xea\x1a\xfb\xf0+F\x0cm\x1d'
>>> 
>>> hash_function.hexdigest()     # ❷
'ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ 返回哈希值作为字节

❷ 返回哈希值字符串

以下代码使用 digest 方法演示了一个 MD5 碰撞。这两条消息只有少数不同的字符(加粗显示):

>>> from hashlib import md5
>>> 
>>> x = bytearray.fromhex(
...    
'd131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609
f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6da
cd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1e
c69821bcb6a8839396f9652b6ff72a70')
>>> 
>>> y = bytearray.fromhex(
...     
'd131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609
f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6da
cd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1e
c69821bcb6a8839396f965ab6ff72a70')
>>> 
>>> x == y                               # ❶
False                                    # ❶
>>> 
>>> md5(x).digest() == md5(y).digest()   # ❷
True                                     # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

❶ 不同的消息

❷ 相同的哈希值,碰撞

消息也可以使用 update 方法进行哈希处理,如下面的代码中所示。当需要单独创建和使用哈希函数时,这很有用。哈希值不受消息如何馈送到函数的影响:

>>> message = b'message'
>>> 
>>> hash_function = hashlib.sha256()                             # ❶
>>> hash_function.update(message)                                # ❷
>>> 
>>> hash_function.digest() == hashlib.sha256(message).digest()   # ❸
True                                                             # ❸
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 构造的哈希函数没有消息

❷ 使用 update 方法传递的消息

❸ 相同的哈希值

一条消息可以被分成多个块,并通过多次调用 update 方法进行迭代哈希处理,如下面代码中加粗显示的部分所示。每次调用 update 方法都会更新哈希值,而不会复制或存储消息字节的引用。因此,当无法一次性将大消息加载到内存中时,此功能非常有用。哈希值对消息处理方式不敏感。

>>> from hashlib import sha256
>>> 
>>> once = sha256()
>>> once.update(b'message')            # ❶
>>> 
>>> many = sha256()
>>> many.update(b'm')                  # ❷
>>> many.update(b'e') # ❷
>>> many.update(b's') # ❷
>>> many.update(b's') # ❷
>>> many.update(b'a') # ❷
>>> many.update(b'g')                  # ❷
>>> many.update(b'e')                  # ❷
>>> 
>>> once.digest() == many.digest()     # ❸
True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

❶ 使用消息初始化哈希函数

❷ 将消息分块给哈希函数

❸ 相同的哈希值

digest_size 属性以字节为单位公开哈希值的长度。回想一下,SHA-256,正如其名称所示,是一个 256 位的哈希函数:

>>> hash_function = hashlib.sha256(b'message')
>>> hash_function.digest_size
32
>>> len(hash_function.digest()) * 8
256
  • 1
  • 2
  • 3
  • 4
  • 5

加密哈希函数在定义上是普遍确定性的。它们天然跨平台。本章示例中的输入在任何计算机、任何编程语言和任何 API 上都会产生相同的输出。以下两个命令演示了这一保证,使用 Python 和 Ruby。如果同一加密哈希函数的两个实现产生不同的哈希值,那么至少其中一个是有问题的:

$ python -c 'import hashlib; print(hashlib.sha256(b"m").hexdigest())'
62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a

$ ruby -e 'require "digest"; puts Digest::SHA256.hexdigest "m"'
62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a
  • 1
  • 2
  • 3
  • 4
  • 5

另一方面,内置的 hash 函数默认情况下仅在特定的 Python 进程内是确定性的。以下两个命令演示了两个不同的 Python 进程对相同消息进行哈希处理得到不同的哈希值:

$ python -c 'print(hash("message"))'
8865927434942197212
$ python -c 'print(hash("message"))'    # ❶
3834503375419022338                     # ❷
  • 1
  • 2
  • 3
  • 4

❶ 相同的消息

❷ 不同的哈希值

警告:内置的 hash 函数绝对不应用于加密目的。这个函数非常快,但它没有足够的碰撞抵抗力,无法与 SHA-256 相提并论。

你可能会想到,“哈希值不就是校验和吗?” 答案是否定的。下一节将解释为什么不是。

2.6 校验和函数

哈希函数和校验和函数有一些共同点。哈希函数接受数据并生成哈希值;校验和函数接受数据并生成校验和。哈希值和校验和都是数字。这些数字用于检测不希望的数据修改,通常在数据静止或传输过程中。

Python 本身支持校验和函数,如循环冗余校验(CRC)和 Adler-32 在 zlib 模块中。以下代码演示了 CRC 的一个常见用例。该代码压缩和解压一个重复数据块。在此转换之前和之后计算数据的校验和(加粗显示)。最后,通过比较校验和来执行错误检测:

>>> import zlib
>>> 
>>> message = b'this is repetitious' * 42         # ❶
>>> checksum = zlib.crc32(message)                # ❶
>>> 
>>> compressed = zlib.compress(message)           # ❷
>>> decompressed = zlib.decompress(compressed)    # ❷
>>> 
>>> zlib.crc32(decompressed) == checksum          # ❸
True                                              # ❸
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

❶ 对消息进行校验和

❷ 压缩和解压消息

❸ 通过比较校验和未检测到任何错误

尽管它们相似,但哈希函数和校验函数不应混淆。哈希函数和校验函数之间的权衡是在于加密强度与速度之间的权衡。换句话说,加密哈希函数具有更强的碰撞抵抗力,而校验函数更快。例如,CRC 和 Adler-32 比 SHA-256 快得多,但都不具有足够的碰撞抵抗力。以下两行代码演示了无数 CRC 碰撞之一:

>>> zlib.crc32(b'gnu')
1774765869
>>> zlib.crc32(b'codding')
1774765869
  • 1
  • 2
  • 3
  • 4

如果您能够像这样使用 SHA-256 找到碰撞,那将在网络安全领域引发震动。将校验函数与数据完整性联系起来有点牵强。更准确地说,应该用错误检测来描述校验函数,而不是数据完整性。

警告:校验函数不应用于安全目的。可以使用加密哈希函数代替校验函数,但会付出相当大的性能代价。

在本节中,您学习了在加密哈希中使用 hashlib 模块,而不是 zlib 模块。下一章将继续介绍哈希。您将学习如何使用 hmac 模块进行键控哈希,这是一种常见的数据认证解决方案。

总结

  • 哈希函数将消息确定性地映射到固定长度的哈希值。

  • 您使用加密哈希函数来确保数据完整性。

  • 通常应使用 SHA-256 进行通用加密哈希。

  • 使用 MD5 或 SHA1 进行安全目的的代码存在漏洞。

  • 在 Python 中,您可以使用 hashlib 模块进行加密哈希。

  • 校验函数不适用于加密哈希。

  • 爱丽丝(Alice)、鲍勃(Bob)和查理(Charlie)是好人。

  • 伊夫(Eve)和玛洛丽(Mallory)是坏人。

第三章:密钥生成

本章涵盖

  • 生成安全密钥

  • 使用键控哈希验证数据身份验证

  • 使用hmac模块进行加密哈希

  • 防止时序攻击

在上一章中,您学习了如何使用哈希函数确保数据的完整性。 在本章中,您将学习如何使用键控哈希函数确保数据的身份验证。 我将向您展示如何安全地生成随机数和口令。 在此过程中,您将了解有关ossecretsrandomhmac模块的知识。 最后,您将学习如何通过比较长度恒定的时间中的哈希值来抵抗时序攻击。

数据身份验证

让我们重新审视上一章中爱丽丝的文件管理系统。 系统在存储每个新文档之前对其进行哈希处理。 要验证文档的完整性,系统会重新对其进行哈希处理,并将新的哈希值与旧的哈希值进行比较。 如果哈希值不匹配,则文档被视为损坏。 如果哈希值匹配,则文档被视为完整。

爱丽丝的系统有效地检测到了意外数据损坏,但并不完美。 网络攻击者玛洛瑞可能会利用爱丽丝。 假设玛洛瑞获得了对爱丽丝文件系统的写入访问权限。 在这个位置,她不仅可以更改文档,还可以将其哈希值替换为更改后的文档的哈希值。 通过替换哈希值,玛洛瑞阻止了爱丽丝检测到文档已被篡改。 因此,爱丽丝的解决方案只能检测意外消息损坏; 它无法检测到有意的消息修改。

如果爱丽丝想要抵抗玛洛瑞,她需要改变系统以验证每个文档的完整性和来源。 系统不能只回答“数据是否改变?”的问题。 系统还必须回答“谁创作了这个数据?” 换句话说,系统需要确保数据的完整性和数据的身份验证。

数据身份验证,有时也称为消息身份验证,确保数据读取者可以验证数据写入者的身份。 此功能需要两个东西:一个密钥和一个键控哈希函数。 在接下来的几节中,我将介绍密钥生成和键控哈希; 爱丽丝将这些工具结合起来以抵抗玛洛瑞。

键控哈希

如果要保持秘密,每个密钥都应该难以猜测。 在本节中,我比较和对比了两种类型的密钥:随机数和口令。 您将学习如何生成这两种密钥,以及何时使用其中一种。

随机数

在生成随机数时无需使用第三方库;Python 本身有很多方法可以实现这一点。然而,其中只有一些方法适用于安全目的。Python 程序员传统上使用os.urandom函数作为密码安全的随机数源。此函数接受一个整数size并返回size个随机字节。这些字节来自操作系统。在类 UNIX 系统上,这是/dev/urandom;在 Windows 系统上,这是CryptGenRandom

>>> import os
>>> 
>>> os.urandom(16)
b'\x07;`\xa3\xd1=wI\x95\xf2\x08\xde\x19\xd9\x94^'
  • 1
  • 2
  • 3
  • 4

Python 3.6 引入了一个专门用于生成密码安全随机数的显式高级 API,即secrets模块。os.urandom没有问题,但在本书中,我使用secrets模块来生成所有随机数。该模块具有三个方便的用于生成随机数的函数。所有三个函数都接受一个整数并返回一个随机数。随机数可以表示为字节数组、十六进制文本和 URL 安全文本。所有三个函数名称的前缀如下代码所示,为token_

>>> from secrets import token_bytes, token_hex, token_urlsafe
>>> 
>>> token_bytes(16)                                     # ❶
b'\x1d\x7f\x12\xadsu\x8a\x95[\xe6\x1b|\xc0\xaeM\x91'    # ❶
>>> 
>>> token_hex(16)                                       # ❷
'87983b1f3dcc18080f21dc0fd97a65b3'                      # ❷
>>> 
>>> token_urlsafe(16)                                   # ❸
'Z_HIRhlJBMPh0GYRcbICIg'                                # ❸
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

❶ 生成 16 个随机字节

❷ 生成 16 个十六进制文本的随机字节

❸ 生成 16 个 URL 安全文本的随机字节

在计算机上键入以下命令以生成 16 个随机字节。我愿意打赌你得到的数字与我不同:

$ python -c 'import secrets; print(secrets.token_hex(16))'
3d2486d1073fa1dcfde4b3df7989da55
  • 1
  • 2

第三种获取随机数的方法是使用random模块。该模块中的大多数函数不使用安全的随机数源。此模块的文档明确指出“不应用于安全目的”(docs .python.org/3/library/random.html)。secrets模块的文档断言“应该优先使用random模块中的默认伪随机数生成器”(docs.python.org/3/library/secrets.html)。

警告:永远不要将random模块用于安全或加密目的。该模块非常适用于统计学,但不适合安全或加密。

密码短语

密码短语是一系列随机单词,而不是一系列随机数字。列表 3.1 使用secrets模块从字典文件中随机选择的四个单词生成密码短语。

脚本首先将字典文件加载到内存中。该文件随标准类 UNIX 系统一起发货。其他操作系统的用户从网上下载类似的文件也不成问题(www.karamasoft.com/UltimateSpell/Dictionary.aspx)。脚本使用secrets .choice函数从字典中随机选择单词。此函数从给定序列返回一个随机项。

列表 3.1 生成一个四个单词的密码短语

from pathlib import Path
import secrets

words = Path('/usr/share/dict/words').read_text().splitlines()    # ❶

passphrase = ' '.join(secrets.choice(words) for i in range(4))    # ❷

print(passphrase)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

❶ 将字典文件加载到内存中

❷ 随机选择四个单词

像这样的字典文件是攻击者执行暴力攻击时使用的工具之一。因此,从相同来源构建秘密是非直观的。密码短语的力量在于大小。例如,密码短语whereat isostatic custom insupportableness的长度为 42 字节。根据www.useapassphrase.com的说法,这个密码短语的破解时间约为 163,274,072,817,384 世纪。对这么长的密钥进行暴力攻击是不可行的。密钥大小很重要。

一个随机数和一个密码短语自然满足秘密的最基本要求:两种密钥类型都难以猜测。随机数和密码短语之间的区别归结为长期人类记忆的局限性。

提示 随机数很难记住,而密码短语很容易记住。这种差异决定了每种密钥类型适用于哪些情景。

当一个人不需要或不应该记住一个秘密超过几分钟时,随机数是有用的。多因素认证(MFA)令牌和临时重置密码值都是随机数的良好应用场景。还记得secrets.token_bytessecrets.token_hexsecrets .token_urlsafe吗?这个前缀是对这些函数应该用于什么的提示。

当一个人需要长时间记住一个秘密时,密码短语是有用的。网站的登录凭据或安全外壳(SSH)会话都是密码短语的良好应用场景。不幸的是,大多数互联网用户并没有使用密码短语。大多数公共网站不鼓励使用密码短语。

重要的是要理解,随机数和密码短语不仅在正确应用时解决问题;当它们被错误应用时,它们会产生新问题。想象一下以下两种情况,一个人必须记住一个随机数。首先,随机数被遗忘了,它所保护的信息变得无法访问。其次,随机数被手写到系统管理员桌上的一张纸上,这样它就不太可能保密了。

想象一下以下情景,在这种情景中,密码短语用于短期秘密。假设您收到一个包含密码重置链接或密码重置代码的密码短语。如果一个恶意旁观者看到它在您的屏幕上,他们更有可能记住这个密钥吗?作为密码短语,这个密钥不太可能保密。

注意 为了简单起见,本书中的许多示例都是在 Python 源代码中显示的密钥。然而,在生产系统中,每个密钥都应该安全地存储在密钥管理服务中,而不是您的代码库中。亚马逊的 AWS 密钥管理服务(aws.amazon.com/kms/)和谷歌的云密钥管理服务(cloud.google.com/security-key-management)都是良好的密钥管理服务的示例。

你现在知道如何安全地生成一个密钥。你知道何时使用随机数,何时使用密码。这两种技能与本书的许多部分相关,从下一节开始。

3.1.2 带密钥的哈希

一些哈希函数接受一个可选的密钥。如图 3.1 所示,密钥是哈希函数的一个输入,就像消息一样。与普通哈希函数一样,带密钥哈希函数的输出是一个哈希值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.1 带密钥哈希函数除消息外还接受一个密钥。

哈希值对密钥值敏感。使用不同密钥的哈希函数会产生相同消息的不同哈希值。使用相同密钥的哈希函数会产生相同消息的匹配哈希值。下面的代码演示了带 BLAKE2 的带密钥哈希,BLAKE2 是一种可选密钥的哈希函数:

>>> from hashlib import blake2b
>>> 
>>> m = b'same message'
>>> x = b'key x'                                               # ❶
>>> y = b'key y'                                               # ❷
>>> 
>>> blake2b(m, key=x).digest() == blake2b(m, key=x).digest()   # ❸
True                                                           # ❸
>>> blake2b(m, key=x).digest() == blake2b(m, key=y).digest()   # ❹
False                                                          # ❹
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

❶ 第一个密钥

❷ 第二密钥

❸ 相同密钥,相同哈希值

❹ 不同密钥,不同哈希值

Alice 在她的文档管理系统上工作,可以通过带密钥的哈希添加一层对抗 Mallory 的防御。带密钥的哈希允许 Alice 使用只有她能产生的哈希值存储每个文档。Mallory 不能再擅自修改文档并重新计算哈希值了。没有密钥,Mallory 在验证修改后的文档时无法产生与 Alice 相同的哈希值。因此,Alice 的代码,如下所示,可以抵抗意外数据损坏和恶意数据修改。

列表 3.2 Alice 抵抗意外和恶意数据修改

import hashlib
from pathlib import Path

def store(path, data, key):
    data_path = Path(path)
    hash_path = data_path.with_suffix('.hash')

    hash_value = hashlib.blake2b(data, key=key).hexdigest()     # ❶

    with data_path.open(mode='x'), hash_path.open(mode='x'):    # ❷
        data_path.write_bytes(data)                             # ❷
        hash_path.write_text(hash_value)                        # ❷

def is_modified(path, key):
    data_path = Path(path)
    hash_path = data_path.with_suffix('.hash')

    data = data_path.read_bytes()                               # ❸
    original_hash_value = hash_path.read_text()                 # ❸

    hash_value = hashlib.blake2b(data, key=key).hexdigest()     # ❹

    return original_hash_value != hash_value                    # ❺
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

❶ 使用给定的密钥对文档进行哈希

❷ 将文档和哈希值写入单独的文件

❸ 从存储中读取文档和哈希值

❹ 使用给定的密钥重新计算新的哈希值

❺ 将重新计算的哈希值与从磁盘读取的哈希值进行比较

大多数哈希函数都不是带密钥的哈希函数。普通哈希函数,如 SHA-256,并不原生支持像 BLAKE2 那样的密钥。这启发了一群非常聪明的人来开发基于哈希的消息认证码(HMAC)函数。下一节将探讨 HMAC 函数。

3.2 HMAC 函数

HMAC 函数 是一种通用方法,可以像使用带密钥的哈希函数一样使用任何普通哈希函数。HMAC 函数接受三个输入:消息、密钥和一个普通的密码哈希函数(图 3.2)。没错,你没看错:HMAC 函数的第三个输入是另一个函数。HMAC 函数将所有繁重的工作都包装并委托给传递给它的函数。HMAC 函数的输出是——你猜对了——基于哈希的消息认证码(MAC)。MAC 实际上只是一种特殊类型的哈希值。为了简单起见,在本书中,我使用 哈希值 一词来代替 MAC

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.2 HMAC 函数接受三个输入:消息、密钥和哈希函数。

为自己着想,务必将 HMAC 函数牢记于心。HMAC 函数是本书后面提出的许多挑战的解决方案。当我讨论加密、会话管理、用户注册和密码重置流程时,这个主题将再次出现。

Python 对 HMAC 的回答是hmac模块。以下代码使用消息、密钥和 SHA-256 初始化了一个 HMAC 函数。通过将密钥和哈希函数构造函数引用传递给hmac.new函数来初始化 HMAC 函数。digestmod关键字参数指定了底层哈希函数。hashlib模块中对哈希函数构造函数的任何引用都是digestmod的可接受参数:

>>> import hashlib
>>> import hmac
>>> 
>>> hmac_sha256 = hmac.new(
...     b'key', msg=b'message', digestmod=hashlib.sha256)
  • 1
  • 2
  • 3
  • 4
  • 5

警告 digestmod kwarg 在 Python 3.8 发布时从可选变为必需。您应始终明确指定digestmod kwarg,以确保您的代码在不同版本的 Python 上顺利运行。

新的 HMAC 函数实例反映了它包装的哈希函数实例的行为。这里显示的digesthexdigest方法,以及digest_size属性,现在应该看起来很熟悉:

>>> hmac_sha256.digest()                                             # ❶
b"n\x9e\xf2\x9bu\xff\xfcz\xba\xe5'\xd5\x8f\xda\xdb/\xe4.r\x19\x01\x19v\x91
sC\x06_X\xedJ"
>>> hmac_sha256.hexdigest()                                          # ❷
'6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a'
>>> hmac_sha256.digest_size                                          # ❸
32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 以字节形式返回哈希值

❷ 以十六进制文本返回哈希值

❸ 返回哈希值大小

HMAC 函数的名称是基础哈希函数的衍生物。例如,您可以将包装 SHA-256 的 HMAC 函数称为 HMAC-SHA256:

>>> hmac_sha256.name
'hmac-sha256'
  • 1
  • 2

按设计,HMAC 函数通常用于消息认证。HMACMA字面上代表消息认证。有时,就像 Alice 的文档管理系统一样,消息的读者和消息的编写者是同一个实体。其他时候,读者和编写者是不同的实体。下一节将涵盖这种用例。

3.2.1 各方之间的数据认证

想象一下,Alice 的文档管理系统现在必须从 Bob 那里接收文档。Alice 必须确保每条消息在传输过程中没有被 Mallory 修改。Alice 和 Bob 就协议达成一致:

  1. Alice 和 Bob 共享一个秘密密钥。

  2. Bob 使用他的密钥副本和 HMAC 函数对文档进行哈希处理。

  3. Bob 将文档和哈希值发送给 Alice。

  4. Alice 使用她的密钥副本和 HMAC 函数对文档进行哈希处理。

  5. Alice 将她的哈希值与 Bob 的哈希值进行比较。

图 3.3 说明了这个协议。如果接收到的哈希值与重新计算的哈希值匹配,Alice 可以得出两个结论:

  • 消息是由具有相同密钥的人发送的,据推测是 Bob。

  • Mallory 无法在传输过程中修改消息。

![CH03_F03_Byrne

图 3.3 Alice 使用共享密钥和 HMAC 函数验证 Bob 的身份。

Bob 在发送给 Alice 之前使用 HMAC-SHA256 对他的消息进行哈希处理的协议的实现,如下列表所示。

列表 3.3 Bob 在发送消息之前使用 HMAC 函数

import hashlib
import hmac
import json

hmac_sha256 = hmac.new(b'shared_key', digestmod=hashlib.sha256)   # ❶
message = b'from Bob to Alice'                                    # ❶
hmac_sha256.update(message)                                       # ❶
hash_value = hmac_sha256.hexdigest()                              # ❶

authenticated_msg = {                                             # ❷
   'message': list(message),                                      # ❷
   'hash_value': hash_value, }                                    # ❷
outbound_msg_to_alice = json.dumps(authenticated_msg)             # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

❶ Bob 对文档进行哈希处理。

❷ 哈希值随文档一起传输

Alice 的协议实现,下图所示,使用 HMAC-SHA256 对接收到的文档进行哈希处理。如果两个 MAC 值相同,则消息被视为经过身份验证。

列表 3.4 Alice 在接收到 Bob 的消息后使用 HMAC 函数。

import hashlib
import hmac
import json

authenticated_msg = json.loads(inbound_msg_from_bob)
message = bytes(authenticated_msg['message'])

hmac_sha256 = hmac.new(b'shared_key', digestmod=hashlib.sha256)    # ❶
hmac_sha256.update(message)                                        # ❶
hash_value = hmac_sha256.hexdigest()                               # ❶

if hash_value == authenticated_msg['hash_value']:                  # ❷
    print('trust message')
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

❶ Alice 计算自己的哈希值。

❷ Alice 比较两个哈希值。

作为一个中间人,Mallory 无法欺骗 Alice 接受已经修改的消息。由于无法获取 Alice 和 Bob 共享的密钥,Mallory 无法为给定消息生成与他们相同的哈希值。如果 Mallory 在传输过程中修改了消息或哈希值,Alice 收到的哈希值将与 Alice 计算的哈希值不同。

看一下列表 3.4 中代码的最后几行。注意 Alice 使用 == 运算符来比较哈希值。这个运算符,信不信由你,使 Alice 在另一个全新的方式上容易受到 Mallory 的攻击。接下来的部分将解释攻击者如何像 Mallory 发动时间攻击。

3.3 时间攻击

数据完整性和数据验证都归结为哈希值比较。虽然比较两个字符串可能看起来很简单,但实际上有一种不安全的方法。== 运算符一旦发现两个操作数之间的第一个差异,就会求值为 False。平均而言,== 必须扫描并比较所有哈希值字符的一半。至少,它可能只需要比较每个哈希值的第一个字符。最多,当两个字符串匹配时,它可能需要比较两个哈希值的所有字符。更重要的是,如果两个哈希值共享一个公共前缀,== 将花费更长的时间来比较两个哈希值。你能发现这个漏洞吗?

Mallory 通过创建一个她希望 Alice 接受的文档来开始新的攻击,使其看起来像是来自 Bob。没有密钥,Mallory 不能立即确定 Alice 将对文档进行哈希的哈希值,但她知道哈希值将是 64 个字符长。她还知道哈希值是十六进制文本,因此每个字符有 16 个可能的值。

攻击的下一步是确定或破解 64 个哈希值字符中的第一个。对于该字符可以是的所有 16 个可能值,Mallory 制造一个以该值开头的哈希值。对于每个制造的哈希值,Mallory 将其与恶意文档一起发送给 Alice。她重复这个过程,测量并记录响应时间。经过大量响应后,Mallory 最终能够通过观察与每个十六进制值相关联的平均响应时间来确定 64 个哈希值字符的第一个。匹配的十六进制值的平均响应时间将略高于其他值。图 3.4 描述了 Mallory 如何破解第一个字符。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3.4 Mallory 在观察到 b 的略高平均响应时间后破解哈希值的第一个字符。

Mallory 通过重复这个过程来完成攻击,对剩下的 63 个字符中的 64 个字符进行操作,此时她就知道了整个哈希值。这是一个 时序攻击 的例子。这种攻击是通过从系统执行时间中获取未经授权的信息来执行的。攻击者通过测量系统执行操作所需的时间来获得关于私有信息的提示。在这个例子中,操作是字符串比较。

安全系统在比较哈希值时使用长度恒定的时间,故意牺牲了一小部分性能,以防止时序攻击漏洞。hmac 模块包含一个名为 compare_digest 的长度恒定时间比较函数。此函数具有与 == 操作符相同的功能结果,但时间复杂度不同。compare_digest 函数在检测到两个哈希值之间有差异时不会提前返回。它总是在返回之前比较所有字符。平均情况、最快情况和最慢情况都是相同的。这可以防止时序攻击,攻击者可以确定一个哈希值的值,如果他们可以控制另一个哈希值:

>>> from hmac import compare_digest
>>> 
>>> compare_digest('alice', 'mallory')    # ❶
False                                     # ❶
>>> compare_digest('alice', 'alice')      # ❷
True                                      # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ 不同的参数,相同的运行时间

❷ 相同的参数,相同的运行时间

始终使用 compare_digest 来比较哈希值。为了谨慎起见,即使你正在编写的代码只使用哈希值来验证数据完整性,也要使用 compare_digest。这个函数在本书的许多示例中都有使用,包括前一节的示例。compare_digest 的参数可以是字符串或字节。

时序攻击是一种特定类型的侧信道攻击。侧信道攻击 用于通过测量任何物理侧信道来推导出未经授权的信息。时间、声音、功耗、电磁辐射、无线电波和热量都是侧信道。认真对待这些攻击,因为它们不仅仅是理论上的。侧信道攻击已被用于破解加密密钥、伪造数字签名和获取未经授权的信息。

摘要

  • 通过密钥散列确保数据认证。

  • 如果一个人需要记住一个密钥,可以使用一个口令作为密钥。

  • 如果人类不需要记住一个密钥,可以使用一个随机数作为密钥。

  • HMAC 函数是你用于通用密钥散列的最佳选择。

  • Python 本身支持具有 hmac 模块的 HMAC 函数。

  • 通过在长度恒定的时间内比较哈希值来抵御时序攻击。

第四章:对称加密

本章涵盖内容

  • 使用加密确保机密性

  • 介绍 cryptography 包

  • 选择对称加密算法

  • 旋转加密密钥

在本章中,我将向你介绍cryptography包。你将学习如何使用这个包的加密 API 来确保机密性。前几章的键授权哈希和数据认证也会出现。在此过程中,你将学习有关密钥旋转的知识。最后,我将向你展示如何区分安全和不安全的对称分组密码。

4.1 什么是加密?

加密始于明文。明文是可以轻易理解的信息。《葛底斯堡演说》、一张猫的图片和一个 Python 包都是潜在的明文示例。加密是将明文混淆以隐藏信息不被未经授权的人看到。加密后的输出称为密文

加密的逆过程,将密文转换回明文,称为解密。用于加密和解密数据的算法称为密码。每个密码都需要一个密钥。密钥旨在成为授权访问加密信息的各方之间的秘密(图 4.1)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.1 明文是加密的人类可读输入和解密的输出;密文是加密的机器可读输出和解密的输入。

加密确保机密性。机密性是安全系统设计的一个原子构建块,就像前几章中的数据完整性和数据认证一样。与其他构建块不同,机密性没有复杂的定义;它是隐私的保证。在本书中,我将机密性分为两种隐私形式:

  • 个人隐私

  • 群体隐私

举个例子,假设爱丽丝想要写和读取敏感数据,并且没有打算让其他人阅读。爱丽丝可以通过加密她写的内容和解密她读的内容来保证个人隐私。这种隐私形式是对第一章讨论的静态和传输中的加密的补充。

或者,假设爱丽丝想要与鲍勃交换敏感数据。爱丽丝和鲍勃可以通过加密他们发送的内容和解密他们接收的内容来保证群体隐私。这种隐私形式是对静态和传输中的加密的补充。

在这一章中,你将学习如何使用 Python 和cryptography包实现静态加密。要安装这个包,我们必须首先安装一个安全的包管理器。

4.1.1 包管理

在本书中,我使用 Pipenv 进行包管理。我选择这个包管理器是因为它配备了许多安全功能。其中一些功能在第十三章中介绍。

注意 有许多 Python 包管理器,您不必使用与我相同的包管理器来运行本书中的示例。您可以自由选择使用 pipvenv 等工具跟随,但您将无法利用 Pipenv 提供的多个安全功能。

要安装 Pipenv,请根据您的操作系统选择以下命令之一。不建议使用 Homebrew(macOS)或 LinuxBrew(Linux)安装 Pipenv。

$ sudo apt install pipenv    # ❶
$ sudo dnf install pipenv    # ❷
$ pkg install py36-pipenv    # ❸
$ pip install --user pipenv  # ❹
  • 1
  • 2
  • 3
  • 4

❶ 在 Debian Buster+ 上

❷ 在 Fedora 上

❸ 在 FreeBSD 上

❹ 在所有其他操作系统上

接下来,运行以下命令。此命令在当前目录中创建两个文件,Pipfile 和 Pipfile.lock。Pipenv 使用这些文件来管理您的项目依赖项:

$ pipenv install
  • 1

除了 Pipfiles,上一个命令还创建了一个虚拟环境。这是一个针对 Python 项目的隔离、自包含的环境。每个虚拟环境都有自己的 Python 解释器、库和脚本。通过为每个 Python 项目提供自己的虚拟环境,可以防止它们相互干扰。运行以下命令以激活您的新虚拟环境:

$ pipenv shell
  • 1

警告 为自己做个好事,并在您的虚拟环境 shell 中运行本书中的每个命令。这确保您编写的代码能够找到正确的依赖关系。它还确保您安装的依赖关系不会与其他本地 Python 项目发生冲突。

与普通的 Python 项目一样,您应该在虚拟环境中运行本书中的命令。在下一节中,您将在此环境中安装许多依赖项的第一个,即 cryptography 包。作为 Python 程序员,这个包是您唯一需要的加密库。

4.2 cryptography

与其他一些编程语言不同,Python 没有原生的加密 API。少数开源框架占据了这一领域。最受欢迎的 Python 加密包是 cryptographypycryptodome。在本书中,我专门使用 cryptography 包。我更喜欢这个包,因为它有一个更安全的 API。在本节中,我将介绍这个 API 的最重要部分。

使用以下命令将 cryptography 包安装到您的虚拟环境中:

$ pipenv install cryptography
  • 1

cryptography 包的默认后端是 OpenSSL。这个开源库包含了网络安全协议和通用加密函数的实现。这个库主要用 C 语言编写。OpenSSL 被许多其他开源库所包装,比如主要编程语言中的 cryptography 包。

cryptography 包的作者将 API 分为两个级别:

  • 危险材料层,一个复杂的低级 API

  • 配方层,一个简单的高级 API

4.2.1 危险材料层

位于cryptography.hazmat之下的复杂低级 API 被称为危险材料层。在生产系统中使用这个 API 之前三思。危险材料层的文档(cryptography.io/en/latest/hazmat/primitives/)中写道:“只有当你百分之百确定自己知道在做什么时才应该使用它,因为这个模块充满了地雷、龙和带激光枪的恐龙。” 安全地使用这个 API 需要对加密学有深入的了解。一个微小的错误可能使系统变得脆弱。

危险材料层的有效使用案例寥寥无几。例如:

  • 你可能需要这个 API 来加密文件,文件太大无法放入内存。

  • 你可能被迫使用一种罕见的加密算法处理数据。

  • 你可能正在阅读一本使用这个 API 作为教学目的的书。

4.2.2 Recipes layer

简单的高级 API 被称为recipes layercryptography包的文档(cryptography.io/en/latest/)中写道:“我们建议尽可能使用 recipes layer,并仅在必要时回退到 hazmat layer。” 这个 API 将满足大多数 Python 程序员的加密需求。

recipes layer 是一个称为fernet的对称加密方法的实现。这个规范定义了一种旨在以可互操作的方式抵抗篡改的加密协议。这个协议由一个类Fernet封装,在cryptography.fernet之下。

Fernet类被设计为加密数据的通用工具。Fernet.generate_key方法生成 32 个随机字节。Fernet的 init 方法接受这个密钥,如下所示:

>>> from cryptography.fernet import Fernet     # ❶
>>> 
>>> key = Fernet.generate_key()
>>> fernet = Fernet(key)
  • 1
  • 2
  • 3
  • 4

❶ 在cryptography.fernet之下是简单的高级 API。

在内部,Fernet将密钥参数分成两个 128 位密钥。一个用于加密,另一个用于数据认证。(你在上一章学过数据认证。)

Fernet.encrypt方法不仅加密明文,还使用 HMAC-SHA256 对密文进行哈希。换句话说,密文变成了一条消息。密文和哈希值一起作为一个fernet token对象返回,如下所示:

>>> token = fernet.encrypt(b'plaintext')    # ❶
  • 1

❶ 加密明文,哈希密文

图 4.2 展示了如何使用密文和哈希值构建一个 fernet token。为简单起见,加密和有键哈希的密钥被省略。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.2 Fernet 不仅加密明文,还对密文进行了哈希。

Fernet.decrypt 方法是 Fernet.encrypt 的反方法。该方法从 Fernet 令牌中提取密文并使用 HMAC-SHA256 进行身份验证。如果新的哈希值与 Fernet 令牌中的旧哈希值不匹配,则会引发 InvalidToken 异常。如果哈希值匹配,则解密并返回密文:

>>> fernet.decrypt(token)     # ❶
b'plaintext'
  • 1
  • 2

❶ 身份验证和解密密文

图 4.3 描述了解密方法如何解构 Fernet 令牌。与上一图一样,解密和数据认证的密钥被省略了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.3 Fernet 不仅对密文进行解密,还对其进行身份验证。

你可能想知道为什么 Fernet 确保密文身份验证而不只是保密性。保密性的价值直到与数据认证结合才能完全实现。例如,假设 Alice 打算实现个人隐私。她分别加密和解密她写的和读的内容。通过隐藏她的密钥,Alice 知道她是唯一能解密密文的人,但这本身并不能保证她创建了密文。通过对密文进行身份验证,Alice 增加了一层防御,防止 Mallory 修改密文。

假设 Alice 和 Bob 想要实现群体隐私。双方分别加密和解密他们发送和接收的内容。通过隐藏密钥,Alice 和 Bob 知道 Eve 无法窃听他们的对话,但仅凭这一点不能保证 Alice 实际上收到了 Bob 发送的内容,反之亦然。只有数据认证才能为 Alice 和 Bob 提供这一保证。

Fernet 令牌是一项安全功能。每个 Fernet 令牌都是不透明的字节数组;没有正式的 FernetToken 类来存储密文和哈希值的属性。如果你真的想要,你可以提取这些值,但这会变得混乱。Fernet 令牌是这样设计的,以阻止你尝试做任何容易出错的事情,比如使用自定义代码解密或身份验证,或在身份验证之前进行解密。该 API 提倡“不要自己编写加密算法”,这是第一章介绍的最佳实践。Fernet 故意设计成易于安全使用而难于不安全使用。

一个 Fernet 对象可以解密由具有相同密钥的 Fernet 对象创建的任何 Fernet 令牌。你可以丢弃一个 Fernet 实例,但密钥必须被保存和保护。如果密钥丢失,明文将无法恢复。在下一节中,你将学习如何使用 Fernet 的配套工具 MultiFernet 轮换密钥。

4.2.3 密钥轮换

密钥轮换 用于将一个密钥替换为另一个密钥。为了废弃一个密钥,必须用它生成的所有密文进行解密,并使用下一个密钥重新加密。密钥可能因多种原因而需要进行轮换。一旦密钥受到损害,必须立即废弃。有时候当一个能够访问密钥的人员离开组织时,必须对密钥进行轮换。定期进行密钥轮换可以限制密钥受损的损害,但无法降低密钥受损的概率。

Fernet 结合 MultiFernet 类实现密钥轮换。假设要用新密钥替换旧密钥。使用这两个密钥实例化单独的 Fernet 实例。使用这两个 Fernet 实例实例化单个 MultiFernet 实例。MultiFernet 的 rotate 方法将使用旧密钥解密所有使用旧密钥加密的内容,并使用新密钥重新加密。一旦所有令牌都使用新密钥重新加密,就可以安全地废弃旧密钥。以下清单演示了使用 MultiFernet 进行密钥轮换。

清单 4.1 使用 MultiFernet 进行密钥轮换

from cryptography.fernet import Fernet, MultiFernet

old_key = read_key_from_somewhere_safe()
old_fernet = Fernet(old_key)

new_key = Fernet.generate_key()
new_fernet = Fernet(new_key)

multi_fernet = MultiFernet([new_fernet, old_fernet])        # ❶
old_tokens = read_tokens_from_somewhere_safe()              # ❶
new_tokens = [multi_fernet.rotate(t) for t in old_tokens]   # ❶

replace_old_tokens(new_tokens)                              # ❷
replace_old_key_with_new_key(new_key)                       # ❷
del old_key                                                 # ❷

for new_token in new_tokens:                                # ❸
    plaintext = new_fernet.decrypt(new_token)               # ❸
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

❶ 使用旧密钥解密,使用新密钥加密

❷ 弃用旧密钥,启用新密钥

❸ 需要新密钥才能解密新密文

密钥的角色决定了加密算法所属的类别。下一节将介绍 Fernet 所属的类别。

4.3 对称加密

如果一个加密算法使用相同的密钥进行加密和解密,就像 Fernet 封装的那种,我们称之为 对称。对称加密算法进一步分为两类:分组密码和流密码。

4.3.1 分组密码

分组密码 将明文加密为一系列固定长度的块。每个明文块被加密为一个密文块。块大小取决于加密算法。较大的块大小通常被认为更安全。图 4.4 演示了将三个明文块加密为三个密文块。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.4 一个分组密码接受 N 个明文分组,并产生 N 个密文分组。

有许多种对称加密算法。对于程序员来说,面对这些选择可能会感到不知所措。哪些算法是安全的?哪些算法是快速的?这些问题的答案实际上相当简单。当你阅读本节时,你将明白其中的道理。以下是几个常见的分组密码的例子:

  • 三重 DES

  • Blowfish

  • Twofish

  • 高级加密标准

三重 DES

三重 DES (3DES) 是数据加密标准 (DES) 的一种改进。顾名思义,这个算法在内部使用 DES 进行三次加密,因此被认为速度较慢。3DES 使用 64 位块大小和 56、112 或 168 位的密钥大小。

警告 3DES 已被 NIST 和 OpenSSL 废弃。不要使用 3DES(有关更多信息,请访问 mng.bz/pJoG)。

Blowfish

Blowfish 是由 Bruce Schneier 在 1990 年代初开发的。该算法使用 64 位块大小和 32 到 448 位的可变密钥大小。Blowfish 作为第一个主要没有专利的免费加密算法而获得了普及。

警告 Blowfish 在 2016 年失去了声望,因为它的分组大小使其容易受到一种名为 SWEET32 的攻击的影响。不要使用 Blowfish。即使 Blowfish 的创造者也建议使用 Twofish 替代。

Twofish

Twofish 在 1990 年代末作为 Blowfish 的后继者开发。该算法使用 128 位块大小和 128、192 或 256 位的密钥大小。Twofish 受到密码学家的尊重,但没有享受到其前身的流行。在 2000 年,Twofish 成为了一个为期三年的竞赛,被称为高级加密标准过程的决赛选手。你可以安全地使用 Twofish,但为什么不做每个人都做的事情,使用赢得这个比赛的算法呢?

高级加密标准

Rijndael 是一个由 NIST 在 2001 年标准化的加密算法,它在高级加密标准过程中击败了十多种其他密码。你可能从来没有听说过这个算法,尽管你经常使用它。这是因为 Rijndael 在被高级加密标准过程选中后采用了高级加密标准的名称。高级加密标准不仅仅是一个名称;它是一个竞赛称号。

高级加密标准 (AES) 是典型应用程序员需要了解的唯一对称加密算法。该算法使用 128 位块大小和 128、192 或 256 位的密钥大小。它是对称加密的典范。AES 的安全记录非常强大和广泛。AES 加密的应用包括网络协议如 HTTPS、压缩、文件系统、哈希和虚拟私人网络 (VPN)。还有哪些加密算法有自己的硬件指令?即使你试过,也无法建立一个不使用 AES 的系统。

如果你到现在还没有猜到,Fernet 在底层使用的是 AES。AES 应该是程序员在一般情况下选择的第一个通用加密方法。保持安全,不要试图聪明,忘记其他的分组密码。下一节将介绍流密码。

4.3.2 流密码

密码 不会按块处理明文。相反,明文被处理为一个个独立的字节流;一个字节进,一个字节出。顾名思义,流密码擅长加密连续或未知量的数据。这些密码通常被网络协议使用。

当明文非常小的时候,流密码比块密码有优势。例如,假设您正在使用块密码加密数据。您想加密 120 位的明文,但块密码将明文加密为 128 位块。块密码将使用填充方案来补偿 8 位的差异。通过使用 8 位填充,块密码可以操作,就好像明文位数是块大小的倍数一样。现在考虑当您需要加密仅 8 位的明文时会发生什么。块密码必须使用 120 位的填充。不幸的是,这意味着超过 90%的密文可以归因于填充。流密码避免了这个问题。它们不需要填充方案,因为它们不将明文处理为块。

RC4 和 ChaCha 都是流密码的示例。RC4 在网络协议中广泛使用,直到发现了半打漏洞。这种密码已被放弃,不应再使用。另一方面,ChaCha 被认为是安全的,而且无疑是快速的。您将在第六章中看到 ChaCha 的出现,那里我将介绍 TLS,一个安全的网络协议。

尽管流密码速度快且高效,但比块密码需求少。不幸的是,流密码的密文通常比块密码的密文更容易被篡改。在某些模式下,块密码也可以模拟流密码。下一节介绍了加密模式。

4.3.3 加密模式

对称加密算法在不同模式下运行。每种模式都有优点和缺点。当应用程序开发人员选择对称加密策略时,讨论通常不围绕块密码与流密码,或者使用哪种加密算法展开。相反,讨论围绕在哪种加密模式下运行 AES 展开。

电子密码本模式

电子密码本ECB模式是最简单的模式。以下代码演示了如何在 ECB 模式下使用 AES 加密数据。使用cryptography包的低级 API,此示例创建了一个具有 128 位密钥的加密密码。明文通过update方法输入到加密密码中。为了简单起见,明文是一个未填充的单个文本块:

>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives.ciphers import (
...     Cipher, algorithms, modes)
>>> 
>>> key = b'key must be 128, 196 or 256 bits'
>>> 
>>> cipher = Cipher(
...     algorithms.AES(key),                             # ❶
...     modes.ECB(),                                     # ❶
...     backend=default_backend())                       # ❷
>>> encryptor = cipher.encryptor()
>>> 
>>> plaintext = b'block size = 128'                      # ❸
>>> encryptor.update(plaintext) + encryptor.finalize()
b'G\xf2\xe2J]a;\x0e\xc5\xd6\x1057D\xa9\x88'              # ❹
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

❶ 使用 AES 在 ECB 模式下

❷ 使用 OpenSSL

❸ 一段纯文本块

❹ 一段密文块

ECB 模式异常脆弱。具有讽刺意味的是,ECB 模式的弱点使其成为教学的强大选择。ECB 模式不安全,因为它将相同的明文块加密为相同的密文块。这意味着 ECB 模式易于理解,但攻击者也很容易从密文中的模式推断出明文中的模式。

图 4.5 展示了这种弱点的一个经典示例。您在左侧看到一张普通的图像,右侧是它的实际加密版本。1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.5 在使用 ECB 模式加密时,明文中的模式会在密文中产生相应的模式。

ECB 模式不仅会揭示明文中的模式;它还会揭示明文之间的模式。例如,假设 Alice 需要加密一组明文。她错误地认为在 ECB 模式下加密它们是安全的,因为每个明文中都没有模式。然后 Mallory 未经授权地获得了密文。在分析密文时,Mallory 发现有些密文是相同的;然后她得出结论相应的明文也是相同的。为什么?Mallory,不像 Alice,知道 ECB 模式会将匹配的明文加密为匹配的密文。

警告:永远不要在生产系统中使用 ECB 模式加密数据。即使您使用像 AES 这样的安全加密算法,也不能安全使用 ECB 模式。

如果攻击者未经授权地获得您的密文,他们不应该能够推断出有关您的明文的任何信息。一个好的加密模式,如下面描述的那样,会混淆明文之间和明文内的模式。

密码块链接模式

密码块链接CBC模式通过确保每个块的更改会影响所有后续块的密文,克服了 ECB 模式的一些弱点。正如图 4.6 所示,输入模式不会导致输出模式。2

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4.6 在使用 CBC 模式加密时,明文中的模式不会在密文中产生相应的模式。

CBC 模式在使用相同密钥加密相同明文时也会产生不同的密文。CBC 模式通过使用初始化向量(IV)对明文进行个性化。与明文和密钥一样,IV 是加密密码的输入之一。AES 在 CBC 模式下要求每个 IV 都是不可重复的随机 128 位数字。

以下代码使用 CBC 模式的 AES 加密了两个相同的明文块。两个明文块都由两个相同的块组成,并与唯一的 IV 配对。请注意,两个密文都是唯一的,且都不包含模式:

>>> import secrets
>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives.ciphers import (
...     Cipher, algorithms, modes)
>>> 
>>> key = b'key must be 128, 196 or 256 bits'
>>> 
>>> def encrypt(data):
...     iv = secrets.token_bytes(16)      # ❶
...     cipher = Cipher(
...         algorithms.AES(key),          # ❷
...         modes.CBC(iv),                # ❷
...         backend=default_backend())
...     encryptor = cipher.encryptor()
...     return encryptor.update(data) + encryptor.finalize()
... 
>>> plaintext = b'the same message' * 2   # ❸
>>> x = encrypt(plaintext)                # ❹
>>> y = encrypt(plaintext)                # ❹
>>> 
>>> x[:16] == x[16:]                      # ❺
False                                     # ❺
>>> x == y                                # ❻
False                                     # ❻
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

❶ 生成 16 个随机字节

❷ 使用 AES 在 CBC 模式下。

❸ 两个相同的明文块

❹ 加密相同的明文

❺ 密文中没有模式

❻ 密文之间没有模式

IV 在加密和解密时是必需的。与密文和密钥一样,IV 是解密密码的输入之一,必须保存。如果明文丢失,则无法恢复。

Fernet 使用 CBC 模式的 AES 加密数据。通过使用Fernet,您不必担心生成或保存 IV。Fernet会为每个明文自动生成一个合适的 IV。IV 嵌入在 fernet 令牌中,紧邻密文和哈希值。Fernet还会在解密密文之前从令牌中提取 IV。

警告:一些程序员不幸地想要隐藏 IV,就像它是一个密钥一样。请记住,IV 必须保存但不是密钥。密钥用于加密一个或多个消息;IV 用于加密一个且仅一个消息。密钥是保密的;IV 通常与密文一起保存,没有混淆。如果攻击者未经授权地访问了密文,请假设他们拥有 IV。没有密钥,攻击者实际上仍然一无所获。

除了 ECB 和 CBC 外,AES 还以许多其他模式运行。其中一种模式,Galois/counter mode(GCM),允许像 AES 这样的块密码模拟流密码。你将在第六章再次见到 GCM。

摘要

  • 加密确保机密性。

  • Fernet 是对称加密和认证数据的安全简便方法。

  • MultiFernet 使密钥轮换变得不那么困难。

  • 对称加密算法使用相同的密钥进行加密和解密。

  • AES 是对称加密的首选,可能也是最后的选择。


  1. 左侧的图像来自 en.wikipedia.org/wiki/ile:Tux.jpg。该图像归功于 Larry Ewing,lewing@isc.tamu.edu,以及 GIMP。右侧的图像来自 en.wikipe dia.org/wiki/File:Tux_ecb.jpg

  2. 左侧的图像来自 en.wikipedia.org/wiki/File:Tux.jpg。该图像归功于 Larry Ewing,lewing@isc.tamu.edu,以及 GIMP。右侧的图像来自 en.wikipe dia.org/wiki/File:Tux_ecb.jpg

第五章:非对称加密

本章内容包括

  • 介绍密钥分发问题

  • 使用cryptography包演示非对称加密

  • 通过数字签名确保不可否认性

在上一章中,你学会了如何使用对称加密确保机密性。不幸的是,对称加密并非万灵药。单独来说,对称加密不适用于密钥分发,这是密码学中的一个经典问题。在本章中,你将学习如何使用非对称加密解决这个问题。在此过程中,你将更多地了解名为cryptography的 Python 包。最后,我将向你展示如何通过数字签名确保不可否认性。

5.1 密钥分发问题

当加密者和解密者是同一方时,对称加密效果很好,但它的扩展性不佳。假设 Alice 想要向 Bob 发送一条保密消息。她加密消息并将密文发送给 Bob。Bob 需要 Alice 的密钥来解密消息。现在 Alice 必须找到一种方法将密钥分发给 Bob,而不被 Eve,一个窃听者,拦截密钥。Alice 可以用第二个密钥加密她的密钥,但她如何安全地将第二个密钥发送给 Bob?Alice 可以用第三个密钥加密她的第二个密钥,但她如何…你明白了。密钥分发是一个递归问题。

如果 Alice 想向像 Bob 这样的 10 个人发送消息,问题就会变得更加严重。即使 Alice 将密钥物理分发给所有方,如果 Eve 从任何一个人那里获取了密钥,她将不得不重复这项工作。更换密钥的概率和成本将增加十倍。另外,Alice 可以为每个人管理不同的密钥——工作量增加一个数量级。这个密钥分发问题是非对称加密的灵感之一。

5.2 非对称加密

如果一个加密算法,如 AES,使用相同的密钥进行加密和解密,我们称之为对称。如果一个加密算法使用两个不同的密钥进行加密和解密,我们称之为非对称。密钥被称为密钥对

密钥对由一个私钥和一个公钥组成。私钥由所有者隐藏。公钥公开分发给任何人;它不是一个秘密。私钥可以解密公钥加密的内容,反之亦然。

非对称加密,如图 5.1 所示,是解决密钥分发问题的经典解决方案。假设 Alice 想要安全地向 Bob 发送一条保密消息,使用公钥加密。Bob 生成一个密钥对。私钥保密,公钥公开分发给 Alice。如果 Eve 看到 Bob 向 Alice 发送的公钥,没关系;那只是一个公钥。现在 Alice 使用 Bob 的公钥加密她的消息。她公开将密文发送给 Bob。Bob 接收到密文,并使用他的私钥解密它——唯一可以解密 Alice 消息的密钥。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.1 Alice 通过公钥加密机密地向 Bob 发送消息。

此解决方案解决了两个问题。首先,密钥分发问题已经解决。如果 Eve 设法获取到 Bob 的公钥和 Alice 的密文,她无法解密消息。只有 Bob 的私钥才能解密由 Bob 的公钥产生的密文。其次,此解决方案可扩展。如果 Alice 想把她的消息发送给 10 个人,每个人只需要生成自己的唯一密钥对。如果 Eve 成功地破坏了某个人的私钥,这不会影响其他参与者。

此部分演示了公钥加密的基本思想。下一节演示了如何使用史上最广泛使用的公钥密码系统在 Python 中执行此操作。

5.2.1 RSA 公钥加密

RSA 是一种经受住时间考验的经典非对称加密的例子。这个公钥密码系统是由 Ron Rivest、Adi Shamir 和 Leonard Adleman 在 1970 年代末期开发的。这个缩写代表了创建者的姓氏。

以下的 openssl 命令演示了如何使用 genpkey 子命令生成一个 3072 位的 RSA 私钥。在撰写本文时,RSA 密钥应至少为 2048 位:

$ openssl genpkey -algorithm RSA \      # ❶
    -out private_key.pem \              # ❷
    -pkeyopt rsa_keygen_bits:3072       # ❸
  • 1
  • 2
  • 3

❶ 生成 RSA 密钥

❷ 生成私钥文件到这个路径

❸ 使用 3072 位的密钥大小

注意 RSA 密钥和 AES 密钥之间的大小差异。为了达到可比较的强度,RSA 密钥需要比 AES 密钥大得多。例如,AES 密钥的最大大小是 256 位:这样大小的 RSA 密钥就是个笑话。这种对比反映了这些算法用于加密数据的基础数学模型。RSA 加密使用整数因子分解;AES 加密使用替换-置换网络。一般来说,用于非对称加密的密钥需要比用于对称加密的密钥更大。

以下 openssl 命令演示了如何使用 rsa 子命令从私钥文件中提取 RSA 公钥:

$ openssl rsa -pubout -in private_key.pem -out public_key.pem
  • 1

私钥和公钥有时存储在文件系统中。重要的是要管理这些文件的访问权限。私钥文件不应该对除所有者以外的任何人可读或可写。另一方面,公钥文件可以被任何人读取。以下命令演示了如何在类 Unix 系统上限制对这些文件的访问:

$ chmod 600 private_key.pem   # ❶
$ chmod 644 public_key.pem    # ❷
  • 1
  • 2

❶ 拥有者具有读取和写入权限。

❷ 任何人都可以读取这个文件。

注意:与对称密钥一样,非对称密钥在生产源代码或文件系统中没有用武之地。这样的密钥应该安全地存储在诸如亚马逊的 AWS 密钥管理服务(aws.amazon.com/kms/)和谷歌的 Cloud 密钥管理服务(cloud.google.com/security-key-management)之类的密钥管理服务中。

OpenSSL 将密钥串行化到磁盘上的格式称为增强隐私邮件PEM)。PEM 是编码密钥对的事实标准方式。如果您已经使用过 PEM 格式的文件,您可能会在每个文件中看到下面粗体显示的-----BEGIN头部:

-----BEGIN PRIVATE KEY-----
MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQDJ2Psz+Ub+VKg0
vnlZmm671s5qiZigu8SsqcERPlSk4KsnnjwbibMhcRlGJgSo5Vv13SMekaj+oCTl
...

-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAydj7M/lG/lSoNL55WZpu
u9bOaomYoLvErKnBET5UpOCrJ548G4mzIXEZRiYEqOVb9d0jHpGo/qAk5VCwfNPG
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

或者,可以使用cryptography包生成密钥。列表 5.1 演示了如何使用rsa模块生成私钥。generate_private_key的第一个参数是本书不讨论的 RSA 实现细节(有关更多信息,请访问www.imperialviolet.org/2012/03/16/rsae.html)。第二个参数是密钥大小。生成私钥后,从中提取公钥。

用 Python 生成 RSA 密钥对的列表 5.1

from cryptography.hazmat.backends import default_backend    # ❶
from cryptography.hazmat.primitives import serialization    # ❶
from cryptography.hazmat.primitives.asymmetric import rsa   # ❶

private_key = rsa.generate_private_key(                     # ❷
    public_exponent=65537,                                  # ❷
    key_size=3072,                                          # ❷
    backend=default_backend(), )                            # ❷

public_key = private_key.public_key()                       # ❸
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

❶ 复杂的低级 API

❷ 私钥生成

❸ 公钥提取

注意 生产密钥对的生成在 Python 中很少进行。通常,这是通过命令行工具如opensslssh-keygen完成的。

下面的列表演示了如何将内存中的两个密钥序列化为磁盘上的 PEM 格式。

用 Python 序列化 RSA 密钥对的列表 5.2

private_bytes = private_key.private_bytes(                    # ❶
    encoding=serialization.Encoding.PEM,                      # ❶
    format=serialization.PrivateFormat.PKCS8,                 # ❶
    encryption_algorithm=serialization.NoEncryption(), )      # ❶

with open('private_key.pem', 'xb') as private_file:           # ❶
    private_file.write(private_bytes)                         # ❶

public_bytes = public_key.public_bytes(                       # ❷
    encoding=serialization.Encoding.PEM,                      # ❷
    format=serialization.PublicFormat.SubjectPublicKeyInfo, ) # ❷

with open('public_key.pem', 'xb') as public_file:             # ❷
    public_file.write(public_bytes)                           # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

❶ 私钥序列化

❷ 公钥序列化

不管密钥对如何生成,都可以使用下面列表中显示的代码将其加载到内存中。

用 Python 反序列化 RSA 密钥对的列表 5.3

with open('private_key.pem', 'rb') as private_file:            # ❶
   loaded_private_key = serialization.load_pem_private_key(    # ❶
       private_file.read(),                                    # ❶
       password=None,                                          # ❶
       backend=default_backend()                               # ❶
   )                                                           # ❶

with open('public_key.pem', 'rb') as public_file:              # ❷
   loaded_public_key = serialization.load_pem_public_key(      # ❷
       public_file.read(),                                     # ❷
       backend=default_backend()                               # ❷
   )                                                           # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

❶ 私钥反序列化

❷ 公钥反序列化

下一个列表演示了如何使用公钥加密并用私钥解密。与对称块密码一样,RSA 使用填充方案加密数据。

注意 最佳非对称加密填充(OAEP)是 RSA 加密和解密的推荐填充方案。

用 Python 进行 RSA 公钥加密和解密的列表 5.4

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

padding_config = padding.OAEP(                           # ❶
   mgf=padding.MGF1(algorithm=hashes.SHA256()),          # ❶
   algorithm=hashes.SHA256(),                            # ❶
   label=None, )                                         # ❶

plaintext = b'message from Alice to Bob'

ciphertext = loaded_public_key.encrypt(                  # ❷
   plaintext=plaintext,                                  # ❷
   padding=padding_config, )                             # ❷

decrypted_by_private_key = loaded_private_key.decrypt(   # ❸
   ciphertext=ciphertext,                                # ❸
   padding=padding_config)                               # ❸

assert decrypted_by_private_key == plaintext
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

❶ 使用 OAEP 填充

❷ 用公钥加密

❸ 用私钥解密

非对称加密是双向的。你可以用公钥加密,用私钥解密;或者,你可以反向操作——用私钥加密,用公钥解密。这给我们提供了保密性和数据认证之间的权衡。用公钥加密的数据是保密的;只有私钥的所有者才能解密消息,但任何人都可能是其作者。用私钥加密的数据是认证的;接收者知道消息只能由私钥进行授权,但任何人都可以解密它。

本节演示了公钥加密如何确保保密性。下一节演示了私钥加密如何确保不可否认性。

5.3 不可否认性

在第三章,你学会了 Alice 和 Bob 如何通过密钥散列来确保消息认证。Bob 发送了一条消息以及一个哈希值给 Alice。Alice 也对消息进行了哈希。如果 Alice 的哈希值与 Bob 的哈希值匹配,她可以得出两个结论:消息具有完整性,并且 Bob 是消息的创建者。

现在从第三方 Charlie 的角度考虑这种情况。Charlie 知道谁创建了这条消息吗?不,因为 Alice 和 Bob 都共享一把密钥。Charlie 知道消息是由他们中的一个创建的,但他不知道是哪一个。没有任何东西能阻止 Alice 在声称消息是由 Bob 发送的同时创建消息。没有任何东西能阻止 Bob 在声称消息是由 Alice 创建的同时发送消息。Alice 和 Bob 都知道消息的作者是谁,但他们无法向任何其他人证明作者是谁。

当一个系统阻止参与者否认他们的行为时,我们称之为不可否认性。在这种情况下,Bob 将无法否认他的行为,即发送消息。在现实世界中,不可否认性通常在消息代表在线交易时使用。例如,销售点系统可能以不可否认性作为将商业伙伴法律约束以履行协议的一种方式。这些系统允许第三方,如法律机构,验证每笔交易。

如果 Alice、Bob 和 Charlie 想要不可否认性,Alice 和 Bob 将不得不停止共享密钥并开始使用数字签名。

5.3.1 数字签名

数字签名比数据验证和数据完整性更进一步,以确保不可否认性。数字签名允许任何人,而不仅仅是接收者,回答两个问题:谁发送了消息?消息在传输过程中是否被修改?数字签名与手写签名有许多相似之处:

  • 两种签名类型都是签名者独特的。

  • 两种签名类型都可以用来将签署者与合同法律约束起来。

  • 两种签名类型都难以伪造。

数字签名通常是通过将哈希函数与公钥加密相结合而创建的。要对消息进行数字签名,发送方首先对消息进行哈希处理。哈希值和发送者的私钥然后成为一个非对称加密算法的输入;此算法的输出是消息发送者的数字签名。换句话说,明文是哈希值,密文是数字签名。然后一起传输消息和数字签名。图 5.2 描述了 Bob 如何实现此协议。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.2 Bob 在发送给 Alice 之前使用私钥加密数字签名消息。

数字签名是与消息一起公开传输的;它不是一个秘密。有些程序员很难接受这一点。在一定程度上这是可以理解的:签名是密文,攻击者可以很容易地使用公钥解密它。请记住,尽管密文通常是隐藏的,但数字签名是一个例外。数字签名的目标是确保不可否认性,而不是保密性。如果攻击者解密了数字签名,他们不会获得私人信息。

5.3.2 RSA 数字签名

列表 5.5 展示了 Bob 对图 5.2 中所示想法的实现。此代码展示了如何使用 SHA-256、RSA 公钥加密以及一种名为概率签名方案(PSS)的填充方案对消息进行签名。RSAPrivateKey.sign 方法结合了这三个元素。

列表 5.5 Python 中的 RSA 数字签名

import json
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

message = b'from Bob to Alice'

padding_config = padding.PSS(                     # ❶
    mgf=padding.MGF1(hashes.SHA256()),            # ❶
    salt_length=padding.PSS.MAX_LENGTH)           # ❶

private_key = load_rsa_private_key()              # ❷
signature = private_key.sign(                     # ❸
    message,                                      # ❸
    padding_config,                               # ❸
    hashes.SHA256())                              # ❸

signed_msg = {                                    # ❹
    'message': list(message),                     # ❹
    'signature': list(signature),                 # ❹
}                                                 # ❹
outbound_msg_to_alice = json.dumps(signed_msg)    # ❹
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

❶ 使用 PSS 填充

❷ 使用列表 5.3 中所示的方法加载私钥

❸ 使用 SHA-256 进行签名

❹ 为 Alice 准备带有数字签名的消息

警告 RSA 数字签名和 RSA 公钥加密的填充方案不同。推荐使用 OAEP 填充进行 RSA 加密;推荐使用 PSS 填充进行 RSA 数字签名。这两种填充方案不能互换。

在接收到 Bob 的消息和签名后,但在信任消息之前,Alice 验证签名。

5.3.3 RSA 数字签名验证

在 Alice 接收到 Bob 的消息和数字签名后,她会执行三件事:

  1. 她对消息进行哈希。

  2. 她使用 Bob 的公钥解密签名。

  3. 她比较哈希值。

如果 Alice 的哈希值与解密的哈希值匹配,她就知道可以信任该消息。图 5.3 描绘了 Alice,接收方,如何实现协议的一部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5.3 Alice 接收 Bob 的消息并使用公钥解密验证他的签名。

列表 5.6 展示了 Alice 对图 5.3 中所示协议的实现。数字签名验证的所有三个步骤都委托给了 RSAPublicKey.verify。如果计算的哈希值与 Bob 解密的哈希值不匹配,verify 方法将抛出 InvalidSignature 异常。如果哈希值匹配,Alice 就知道消息没有被篡改,消息只能由拥有 Bob 的私钥的人发送,大概是 Bob。

列表 5.6 Python 中的 RSA 数字签名验证

import json
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature

def receive(inbound_msg_from_bob):
    signed_msg = json.loads(inbound_msg_from_bob)    # ❶
    message = bytes(signed_msg['message'])           # ❶
    signature = bytes(signed_msg['signature'])       # ❶

    padding_config = padding.PSS(                    # ❷
        mgf=padding.MGF1(hashes.SHA256()),           # ❷
        salt_length=padding.PSS.MAX_LENGTH)          # ❷

    private_key = load_rsa_private_key()             # ❸
    try:
        private_key.public_key().verify(             # ❹
            signature,                               # ❹
            message,                                 # ❹
            padding_config,                          # ❹
            hashes.SHA256())                         # ❹
        print('Trust message')
    except InvalidSignature:
        print('Do not trust message')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

❶ 接收消息和签名

❷ 使用 PSS 填充

❸ 使用列表 5.3 中所示的方法加载私钥

❹ 将签名验证委托给 verify 方法

Charlie,第三方,可以像 Alice 一样验证消息的来源。因此,Bob 的签名确保了不可否认性。他不能否认自己是消息的发送者,除非他还声称自己的私钥已被泄露。

Eve,一个中间人,如果她试图干预协议,将会失败。她可以尝试在传输到 Alice 的过程中修改消息、签名或公钥。在这三种情况下,签名都将无法通过验证。修改消息会影响 Alice 计算的哈希值。修改签名或公钥会影响 Alice 解密的哈希值。

本节深入探讨了数字签名作为非对称加密的应用。使用 RSA 密钥对进行这样的操作是安全、可靠且经过实战检验的。不幸的是,非对称加密并不是数字签名的最佳方式。下一节将介绍一个更好的替代方案。

5.3.4 椭圆曲线数字签名

与 RSA 一样,椭圆曲线密码系统围绕密钥对的概念展开。与 RSA 密钥对一样,椭圆曲线密钥对用于签署数据和验证签名;与 RSA 密钥对不同的是,椭圆曲线密钥对不对数据进行非对称加密。换句话说,RSA 私钥解密其公钥加密的内容,反之亦然。椭圆曲线密钥对不支持这种功能。

那么,为什么有人会选择椭圆曲线而不是 RSA?椭圆曲线密钥对可能无法对数据进行非对称加密,但在签署数据方面速度更快。因此,椭圆曲线密码系统已成为数字签名的现代方法,吸引人们摆脱 RSA,降低计算成本。

RSA 并不不安全,但椭圆曲线密钥对在签署数据和验证签名方面效率更高。例如,256 位椭圆曲线密钥的强度可与 3072 位 RSA 密钥相媲美。椭圆曲线和 RSA 之间的性能对比反映了这些算法使用的基础数学模型。椭圆曲线密码系统使用椭圆曲线;RSA 数字签名使用整数因子分解。

列表 5.7 演示了 Bob 如何生成一个椭圆曲线密钥对,并使用 SHA-256 对消息进行签名。与 RSA 相比,这种方法需要更少的 CPU 周期和更少的代码行数。私钥是使用 NIST 批准的椭圆曲线 SECP384R1 或 P-384 生成的。

列表 5.7 在 Python 中椭圆曲线数字签名

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec

message = b'from Bob to Alice'

private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())

signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))    # ❶
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

❶ 使用 SHA-256 进行签名

列表 5.8 继续上一列表 5.7,演示了 Alice 如何验证 Bob 的签名。与 RSA 一样,公钥从私钥中提取;如果签名未通过验证,verify 方法会抛出 InvalidSignature

列表 5.8 在 Python 中椭圆曲线数字签名验证

from cryptography.exceptions import InvalidSignature

public_key = private_key.public_key()   # ❶

try:
    public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
except InvalidSignature:                # ❷
    pass                                # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

❶ 提取公钥

❷ 处理验证失败

有时重新对消息进行哈希是不可取的。当处理大型消息或大量消息时,通常会出现这种情况。sign 方法,针对 RSA 密钥和椭圆曲线密钥,通过让调用者负责生成哈希值来适应这些情况。这使调用者可以选择高效地对消息进行哈希或重用先前计算的哈希值。下一个列表演示了如何使用 Prehashed 实用类对大型消息进行签名。

列表 5.9 在 Python 中高效签署大型消息

import hashlib
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, utils

large_msg = b'from Bob to Alice ...'              # ❶
sha256 = hashlib.sha256()                         # ❶
sha256.update(large_msg[:8])                      # ❶
sha256.update(large_msg[8:])                      # ❶
hash_value = sha256.digest()                      # ❶

private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())

signature = private_key.sign(                     # ❷
    hash_value,                                   # ❷
    ec.ECDSA(utils.Prehashed(hashes.SHA256())))   # ❷
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

❶ 调用者高效地对消息进行哈希

❷ 使用 Prehashed 实用类进行签名

到目前为止,您已经掌握了散列、加密和数字签名的工作知识。您学到了以下内容:

  • 散列确保数据的完整性和数据的认证。

  • 加密确保机密性。

  • 数字签名确保不可否认。

本章介绍了cryptography包中的许多低级示例,供教学目的使用。这些低级示例为您准备了下一章我将介绍的高级解决方案,即传输层安全性所需的一切。这种网络协议将您迄今所学的关于散列、加密和数字签名的一切内容汇集在一起。

总结

  • 非对称加密算法使用不同的密钥进行加密和解密。

  • 公钥加密是解决密钥分发问题的方案。

  • RSA 密钥对是一种经典且安全的非对称加密数据的方式。

  • 数字签名保证不可否认。

  • 椭圆曲线数字签名比 RSA 数字签名更有效。

第六章:传输层安全性

本章内容包括

  • 抵制中间人攻击

  • 理解传输层安全握手

  • 构建、配置和运行 Django Web 应用程序

  • 在使用 Gunicorn 安装公钥证书

  • 使用传输层安全保护 HTTP、电子邮件和数据库流量

在之前的章节中,我向你介绍了密码学。你学到了哈希、加密和数字签名等知识。在本章中,你将学习如何使用传输层安全 (TLS),这是一种普遍的安全网络协议。该协议是数据完整性、数据认证、机密性和不可否认性的应用。

阅读完本章后,你将了解 TLS 握手和公钥证书的工作原理。你还将学会如何生成和配置 Django Web 应用程序。最后,你将学会如何使用 TLS 保护电子邮件和数据库流量。

6.1 SSL? TLS? HTTPS?

在我们深入探讨这个主题之前,让我们先确定一些词汇术语。一些程序员将SSLTLSHTTPS这些术语用来互换使用,尽管它们的含义不同。

安全套接字层 (SSL) 协议是 TLS 的不安全前身。SSL 的最新版本已经超过 20 年了。随着时间的推移,这个协议发现了许多漏洞。2015 年,IETF 废弃了它 (tools.ietf.org/html/rfc7568)。TLS 以更好的安全性和性能取代了 SSL。

SSL 已经过时,但是术语SSL遗憾地仍然存在。它在方法签名、命令行参数和模块名中保留下来;本书包含了许多例子。API 为了向后兼容性而保留了这个术语。有时,程序员在实际上指的是TLS时会提到SSL

安全超文本传输协议 (HTTPS) 简单地说就是 SSL 或 TLS 上的超文本传输协议(HTTP)。HTTP 是一种用于在互联网上传输数据(如网页、图像、视频等)的点对点协议;这在短期内不会改变。

为什么你应该在 TLS 上运行 HTTP?HTTP 是在上世纪 80 年代定义的,当时互联网是一个更小、更安全的地方。从设计上来看,HTTP 不提供任何安全性;对话不是机密的,也没有任何一方经过身份验证。在下一节中,你将了解一类旨在利用 HTTP 限制的攻击。

6.2 中间人攻击

中间人攻击 (MITM) 是一种经典攻击。攻击者首先控制两个易受攻击的方之间的位置。这个位置可以是一个网络段或一个中间系统。攻击者可以利用他们的位置发起以下任一形式的中间人攻击:

  • 被动中间人攻击

  • 主动中间人攻击

假设伊夫,一个窃听者,在未经授权的情况下,获得了鲍勃的无线网络的访问权限后,发动了被动中间人攻击。鲍勃向 bank.alice.com 发送 HTTP 请求,bank.alice.com 向鲍勃发送 HTTP 响应。与此同时,伊夫,未经鲍勃和艾丽斯知情,被动拦截每个请求和响应。这使伊夫能够访问鲍勃的密码和个人信息。图 6.1 说明了被动中间人攻击。

TLS 无法保护鲍勃的无线网络。然而,它可以提供保密性——阻止伊夫以有意义的方式阅读对话。TLS 通过加密鲍勃和艾丽斯之间的对话来实现这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.1 伊夫通过 HTTP 进行被动中间人攻击。

现在假设伊夫在未经授权的情况下获得了位于鲍勃和 bank.alice.com 之间的中间网络设备的访问权限后,发动了主动中间人攻击。伊夫可以监听或甚至修改对话。利用这个位置,伊夫可以欺骗鲍勃和艾丽斯,使他们相信她是另一位参与者。通过欺骗鲍勃认为她是艾丽斯,以及欺骗艾丽斯认为她是鲍勃,伊夫现在可以在他们之间来回传递消息。在此过程中,伊夫修改了对话(图 6.2)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.2 伊夫通过 HTTP 进行主动中间人攻击。

TLS 无法保护位于鲍勃和艾丽斯之间的网络设备。然而,它可以防止伊夫冒充鲍勃或艾丽斯。TLS 通过认证对话来实现这一点,确保鲍勃正在直接与艾丽斯通信。如果艾丽斯和鲍勃想要安全地通信,他们需要开始使用 TLS 上的 HTTP。下一节将解释 HTTP 客户端和服务器如何建立 TLS 连接。

6.3 TLS 握手

TLS 是一种点对点的客户端/服务器协议。每个 TLS 连接都以客户端和服务器之间的握手开始。您可能已经听说过TLS 握手。实际上,并不存在一个 TLS 握手;有许多种。例如,TLS 的 1.1、1.2 和 1.3 版本都定义了不同的握手协议。即使在每个 TLS 版本中,握手也会受到客户端和服务器用于通信的算法的影响。此外,握手的许多部分,如服务器身份验证和客户端身份验证,都是可选的。

在本节中,我将介绍最常见的 TLS 握手类型:您的浏览器(客户端)与现代 Web 服务器执行的握手。此握手始终由客户端发起。客户端和服务器将使用 TLS 的 1.3 版本。版本 1.3 更快、更安全——而且,幸运的是,对您和我来说——比版本 1.2 更简单。这次握手的整个目的是执行三项任务:

  1. 密码套件协商

  2. 密钥交换

  3. 服务器身份验证

6.3.1 密码套件协商

TLS 是加密和哈希的应用。为了通信,客户端和服务器必须首先就一组称为密码套件的算法达成一致。每个密码套件定义了一个加密算法和一个哈希算法。TLS 1.3 规范定义了以下五个密码套件:

  • TLS_AES_128_CCM_8_SHA256

  • TLS_AES_128_CCM_SHA256

  • TLS_AES_128_GCM_SHA256

  • TLS_AES_256_GCM_SHA384

  • TLS_CHACHA20_POLY1305_SHA256

每个密码套件的名称由三个部分组成。第一个部分是一个常见前缀,TLS_。第二部分指定一个加密算法。最后一部分指定一个哈希算法。例如,假设客户端和服务器同意使用密码套件 TLS_AES_128_GCM_SHA256。这意味着双方同意使用 AES 以 128 位密钥在 GCM 模式下,并使用 SHA-256 进行通信。GCM 是一种以速度著称的块密码模式。除了机密性外,它还提供数据认证。图 6.3 解剖了这个密码套件的结构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.3 TLS 密码套件解剖

这五个密码套件可以简单总结为:加密使用 AES 或 ChaCha20;哈希使用 SHA-256 或 SHA-384。在前几章中,你已经了解了这四种工具。花点时间欣赏一下 TLS 1.3 相对于其前身有多简单。TLS 1.2 定义了 37 个密码套件!

请注意,这五个密码套件都使用对称加密,而不是非对称加密。AES 和 ChaCha20 受邀参加了派对;RSA 没有。TLS 通过对称加密确保机密性,因为它比非对称加密更高效,效率提高了三到四个数量级。在前一章中,你了解到对称加密的计算成本比非对称加密低。

客户端和服务器在加密对话时必须共享不仅仅是相同的密码套件,还必须共享一个密钥。

6.3.2 密钥交换

客户端和服务器必须交换一个密钥。这个密钥将与密码套件的加密算法结合使用,以确保机密性。该密钥仅限于当前对话。这样,如果密钥某种方式被泄露,损害仅限于单个对话。

TLS 密钥交换是密钥分发问题的一个例子。(你在前一章中学习过这个问题。)TLS 1.3 通过 Diffie-Hellman 方法解决了这个问题。

Diffie-Hellman 密钥交换

Diffie-HellmanDH密钥交换方法允许两个方安全地在不安全的通道上建立共享密钥。这种机制是密钥分发问题的有效解决方案。

在本节中,我使用爱丽丝、鲍勃和伊夫来引导您了解 DH 方法。代表客户端和服务器的爱丽丝和鲍勃将各自生成临时密钥对。爱丽丝和鲍勃将使用他们的密钥对作为最终共享秘密密钥的跳板。在阅读本文时,重要的是不要将中间密钥对与最终共享密钥混淆。以下是 DH 方法的简化版本:

  1. 爱丽丝和鲍勃公开同意两个参数。

  2. 爱丽丝和鲍勃各自生成一个私钥。

  3. 爱丽丝和鲍勃分别从参数和他们的私钥推导出一个公钥。

  4. 爱丽丝和鲍勃公开交换公钥。

  5. 爱丽丝和鲍勃独立计算一个共享的秘密密钥。

爱丽丝和鲍勃通过公开同意两个数字 p 和 g 开始此协议。这些数字是公开传输的。窃听者伊夫可以看到这两个数字。她不构成威胁。

爱丽丝和鲍勃分别生成私钥 a 和 b。这些数字是秘密的。爱丽丝将她的私钥隐藏起来,不让伊夫和鲍勃知道。鲍勃将他的私钥隐藏起来,不让伊夫和爱丽丝知道。

爱丽丝从 p、g 和她的私钥推导出她的公钥 A。同样,鲍勃从 p、g 和他的私钥推导出他的公钥 B。

爱丽丝和鲍勃交换他们的公钥。这些密钥是公开传输的;它们不是秘密。窃听者伊夫可以看到两个公钥。她仍然不构成威胁。

最后,爱丽丝和鲍勃使用彼此的公钥独立计算出相同的数字 K。爱丽丝和鲍勃丢弃他们的密钥对并保留 K。爱丽丝和鲍勃使用 K 加密他们余下的对话。图 6.4 说明了爱丽丝和鲍勃使用此协议达成共享密钥,即数字 14。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.4 爱丽丝和鲍勃使用 Diffie-Hellman 方法独立计算出一个共享密钥,即数字 14。

在现实世界中,p、私钥和 K 要比这大得多。更大的数字使得即使伊夫窃听了整个对话,也不可能逆向工程私钥或 K。尽管伊夫知道 p、g 和两个公钥,但她唯一的选择是暴力破解。

公钥加密

许多人对握手过程中缺少公钥加密感到惊讶;它甚至不是密码套件的一部分。SSL 和较早版本的 TLS 通常使用公钥加密进行密钥交换。最终,这种解决方案并不具有良好的可扩展性。

在此期间,硬件成本的下降使得暴力破解攻击变得更便宜。为了弥补这一点,人们开始使用更大的密钥对,以保持暴力破解攻击的成本高昂。

更大的密钥对却带来了一个不幸的副作用:Web 服务器花费了不可接受的时间执行非对称加密以进行密钥交换。TLS 1.3 通过明确要求 DH 方法来解决了这个问题。

DH 方法是比使用 RSA 等密码系统产生计算开销的公钥加密更有效的解决方案,它使用模算术而不是分发密钥。这种方法实际上并不是从一方向另一方分发密钥;密钥是由双方独立创建的。公钥加密并没有死;它仍然用于身份验证。

6.3.3 服务器身份验证

密码套件协商和密钥交换是保密性的前提条件。但是,如果不验证与您交谈的人的身份,私密对话有何用?TLS 除了提供隐私外,还是一种身份验证手段。身份验证是双向的,也是可选的。对于这个握手版本(即你的浏览器和 Web 服务器之间的握手),服务器将由客户端进行验证。

服务器通过向客户端发送公钥证书来验证自身,并完成 TLS 握手。证书包含并证明了服务器的公钥的所有权。证书必须由证书颁发机构CA)创建和颁发,这是一个致力于数字认证的组织。

公钥所有者通过向 CA 发送证书签名请求CSR)申请证书。CSR 包含有关公钥所有者和公钥本身的信息。图 6.5 说明了此过程。虚线箭头表示成功的 CSR,因为 CA 向公钥所有者发放了公钥证书。实线箭头说明了证书安装到服务器上,其中它被提供给浏览器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.5 一个公钥证书被颁发给一个所有者并安装在一个服务器上。

公钥证书

公钥证书在很多方面类似于您的驾驶执照。您通过驾驶执照来识别自己;服务器通过公钥证书来识别自己。您的驾驶执照由政府机构颁发给您;证书由证书颁发机构颁发给密钥所有者。警察在信任您之前会仔细检查您的驾驶执照;浏览器(或任何其他 TLS 客户端)在信任服务器之前会仔细检查证书。您的驾驶执照确认了驾驶技能;证书确认了公钥所有权。您的驾驶执照和证书都有过期日期。

让我们解剖一个您已经使用过的网站维基百科的公钥证书。下一个清单中的 Python 脚本使用ssl模块下载维基百科的生产公钥证书。下载的证书是该脚本的输出。

代码清单 6.1 get_server_certificate.py

import ssl

address = ('wikipedia.org', 443)
certificate = ssl.get_server_certificate(address)      # ❶
print(certificate)
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 下载维基百科的公钥证书

使用以下命令行运行此脚本。这将下载证书并将其写入名为 wikipedia.crt 的文件:

$ python get_server_certificate.py > wikipedia.crt
  • 1

公钥证书的结构由 RFC 5280 描述的安全标准 X.509 定义(tools.ietf.org/html/rfc5280)。TLS 参与者使用 X.509 以实现互操作性。服务器可以向任何客户端标识自己,客户端可以验证任何服务器的身份。

X.509 证书的解剖结构由一组常见字段组成。通过从浏览器的角度思考这些字段,您可以更加欣赏 TLS 认证。下面的 openssl 命令演示了如何以人类可读格式显示这些字段:

$ openssl x509 -in wikipedia.crt -text -noout | less
  • 1

在浏览器信任服务器之前,它将解析证书并逐个检查每个字段。让我们检查一些更重要的字段:

  • 主体

  • 颁发者

  • 主体的公钥

  • 证书有效期

  • 证书颁发机构签名

每个证书都像驾驶执照一样标识所有者。证书所有者由“主体”字段指定。 “主体”字段最重要的属性是通用名称,它标识了证书允许从中提供服务的域名。

如果浏览器无法将通用名称与请求的 URL 匹配,将拒绝该证书;服务器验证和 TLS 握手将失败。下面的列表以粗体显示了维基百科公钥证书的主体字段。CN 属性指定了通用名称。

列表 6.2 Wikipedia.org 的主体字段

...
        Subject: CN=*.wikipedia.org     # ❶
        Subject Public Key Info:
...
  • 1
  • 2
  • 3
  • 4

❶ 证书所有者通用名称

每个证书都标识了颁发者,就像驾驶执照一样。颁发维基百科证书的 CA 是 Let’s Encrypt。这家非营利 CA 专门提供免费的自动认证服务。下面的列表以粗体显示了维基百科公钥证书的颁发者字段。

列表 6.3 Wikipedia.org 的证书颁发者

...
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3    # ❶
        Validity
...
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 证书颁发者,Let’s Encrypt

每个公钥证书中都嵌入了证书所有者的公钥。下一个列表展示了维基百科的公钥;这是一个 256 位的椭圆曲线公钥。你在上一章已经介绍过椭圆曲线密钥对。

列表 6.4 Wikipedia.org 的公钥

...
Subject Public Key Info:
    Public Key Algorithm: id-ecPublicKey                         # ❶
 Public-Key: (256 bit)                                    # ❷
 pub: 
 04:6a:e9:9d:aa:68:8e:18:06:f4:b3:cf:21:89:f2: # ❸
 b3:82:7c:3d:f5:2e:22:e6:86:01:e2:f3:1a:1f:9a: # ❸
 ba:22:91:fd:94:42:82:04:53:33:cc:28:75:b4:33: # ❸
 84:a9:83:ed:81:35:11:77:33:06:b0:ec:c8:cb:fa: # ❸
                a3:51:9c:ad:dc # ❸
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

❶ 椭圆曲线公钥

❷ 指定了一个 256 位的密钥

❸ 实际的公钥,已编码

每个证书都有一个有效期,就像驾驶执照一样。如果当前时间不在此时间范围内,浏览器将不信任服务器。下面的列表显示了维基百科的证书具有三个月的有效期,以粗体显示。

列表 6.5 Wikipedia.org 的证书有效期

...
Validity
    Not Before: Jan 29 22:01:08 2020 GMT
    Not After : Apr 22 22:01:08 2020 GMT
...
  • 1
  • 2
  • 3
  • 4
  • 5

在每个证书的底部都有一个数字签名,由 Signature Algorithm 字段指定。(您在上一章学习了数字签名。)谁签署了什么?在这个例子中,证书颁发机构 Let’s Encrypt 签署了证书所有者的公钥——与证书中嵌入的相同的公钥。下一个清单表明 Let’s Encrypt 通过使用 SHA-256 对其进行哈希并用 RSA 私钥加密哈希值来签署了维基百科的公钥,加粗显示。(您在上一章学习了如何在 Python 中执行此操作。)

清单 6.6 维基百科.org 的证书颁发机构签名

...
Signature Algorithm: sha256WithRSAEncryption                  # ❶
    4c:a4:5c:e7:9d:fa:a0:6a:ee:8f:47:3e:e2:d7:94:86:9e:46:    # ❷
    95:21:8a:28:77:3c:19:c6:7a:25:81:ae:03:0c:54:6f:ea:52:    # ❷
    61:7d:94:c8:03:15:48:62:07:bd:e5:99:72:b1:13:2c:02:5e:    # ❷
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ Let’s Encrypt 使用 SHA-256 和 RSA 进行签名。

❷ 数字签名,编码

图 6.6 展示了这个公钥证书的最重要内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.6 维基百科.org Web 服务器向浏览器传输公钥证书。

浏览器将验证 Let’s Encrypt 的签名。如果签名未通过验证,浏览器将拒绝证书,TLS 握手将以失败结束。如果签名通过验证,浏览器将接受证书,握手将以成功结束。握手结束后,对话的其余部分将使用密码套件加密算法和共享密钥进行对称加密。

在本节中,您了解了 TLS 连接是如何建立的。一个典型的成功的 TLS 握手建立了三件事:

  1. 一个商定的密码套件

  2. 仅由客户端和服务器共享的密钥

  3. 服务器认证

在接下来的两节中,您将应用这些知识,构建、配置和运行一个 Django Web 应用程序服务器。您将通过生成和安装自己的公钥证书来保护此服务器的流量。

6.4 使用 Django 进行 HTTP

在本节中,您将学习如何构建、配置和运行一个 Django Web 应用程序。Django是一个您可能已经听说过的 Python Web 应用程序框架。我在本书的每个 Web 示例中都使用 Django。在您的虚拟环境中,运行以下命令安装 Django:

$ pipenv install django
  • 1

安装完 Django 后,django-admin 脚本将会在您的 shell 路径中。这个脚本是一个管理实用程序,将生成您的 Django 项目的框架。使用以下命令启动一个简单但功能齐全的 Django 项目,命名为alice

$ django-admin startproject alice
  • 1

startproject子命令将创建一个与您的项目同名的新目录。这个目录称为项目根目录。在项目根目录中有一个重要的文件名为 manage.py。这个脚本是一个特定于项目的管理实用程序。在本节的后面,您将使用它来启动您的 Django 应用程序。

在项目根目录旁边,就在 manage.py 旁边,有一个与项目根目录同名的目录。这个名称模糊的子目录称为Django 根目录。许多程序员会觉得这很令人困惑,可以理解。

在这一部分,你将使用 Django 根目录中的一个重要模块,即settings模块。这个模块是维护项目配置数值的中心位置。在本书中你会多次看到这个模块,因为我涵盖了与安全相关的许多 Django 设置。

Django 根目录还包含一个名为wsgi的模块。我稍后会介绍wsgi模块。你将使用它来在本章后面为你的 Django 应用程序提供 TLS 服务。图 6.7 展示了你项目的目录结构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.7 新 Django 项目的目录结构

注意 一些程序员对 Django 项目目录结构有着极强的意见。在本书中,所有 Django 示例都使用默认生成的项目结构。

使用以下命令运行你的 Django 服务器。从项目根目录中,使用runserver子命令运行 manage.py 脚本。命令行应该会挂起:

$ cd alice                      # ❶
$ python manage.py runserver    # ❷
...
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 从项目根目录开始

runserver子命令应该会挂起。

将浏览器指向 http:/./localhost:8000,以验证服务器是否正常运行。你将看到一个友好的欢迎页面,类似于图 6.8 中的页面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.8 Django 新项目的欢迎页面

欢迎页面上写着:“你看到这个页面是因为 DEBUG=True。”DEBUG设置是每个 Django 项目的重要配置参数。你可能已经猜到,DEBUG设置位于settings模块中。

6.4.1 DEBUG 设置

Django 生成带有DEBUG设置为True的 settings.py。当DEBUG设置为True时,Django 显示详细的错误页面。这些错误页面中的详细信息包括关于项目目录结构、配置设置和程序状态的信息。

警告 DEBUG对开发很有帮助,但对生产环境很糟糕。这个设置提供的信息帮助你在开发中调试系统,但也会暴露攻击者可以利用来破坏系统的信息。在生产环境中始终将DEBUG设置为False

提示 在更改settings模块之前,必须重新启动服务器才能生效。要重新启动 Django,在 shell 中按下 Ctrl-C 停止服务器,然后再次使用 manage.py 脚本启动服务器。

此时,你的应用程序可以通过 HTTP 提供网页服务。如你所知,HTTP 不支持机密性或服务器身份验证。当前状态下的应用程序容易受到中间人攻击。为解决这些问题,协议必须从 HTTP 升级到 HTTPS。

像 Django 这样的应用服务器实际上并不知道或处理 HTTPS。它不托管公钥证书,也不执行 TLS 握手。在下一节中,你将学习如何通过 Django 和浏览器之间的另一个进程来处理这些责任。

6.5 使用 Gunicorn 进行 HTTPS

在本节中,您将学习如何使用 Gunicorn 托管公钥证书,Gunicorn 是 Web 服务器网关接口(WSGI)协议的纯 Python 实现。该协议由 Python 增强提案(PEP)3333 (www.python.org/dev/peps/pep-3333/) 定义,旨在将 Web 应用程序框架与 Web 服务器实现分离。

您的 Gunicorn 进程将位于您的 Web 服务器和 Django 应用程序服务器之间。图 6.9 描绘了一个 Python 应用程序堆栈,使用 NGINX Web 服务器、Gunicorn WSGI 应用程序和 Django 应用程序服务器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.9 一个常见的 Python 应用程序堆栈,使用 NGINX、Gunicorn 和 Django

在你的虚拟环境中,使用以下命令安装 Gunicorn:

$ pipenv install gunicorn
  • 1

安装后,gunicorn命令将在您的 shell 路径中。此命令需要一个参数,即一个 WSGI 应用程序模块。django-admin 脚本已经为您生成了一个 WSGI 应用程序模块,位于 Django 根目录下。

在运行 Gunicorn 之前,请确保先停止正在运行的 Django 应用程序。在您的 shell 中按下 Ctrl-C 来执行此操作。接下来,从项目根目录运行以下命令,使用 Gunicorn 重新启动您的 Django 服务器。命令行应该会挂起:

$ gunicorn alice.wsgi       # ❶
[2020-08-16 11:42:20 -0700] [87321] [INFO] Starting gunicorn 20.0.4
...
  • 1
  • 2
  • 3

❶ alice.wsgi 模块位于 alice/alice/wsgi.py。

将您的浏览器指向 http:/./localhost:8000 并刷新欢迎页面。您的应用程序现在通过 Gunicorn 提供服务,但仍在使用 HTTP。要将应用程序升级为 HTTPS,您需要安装一个公钥证书。

6.5.1 自签名的公钥证书

如其名称所示,自签名的公钥证书是一个不由 CA 颁发或签名的公钥证书。您自己制作并签名。这是朝向正确证书的一条廉价便捷的过渡。这些证书提供机密性而无需认证;它们适用于开发和测试,但不适用于生产。创建一个自签名的公钥证书大约需要 60 秒,最多需要 5 分钟让您的浏览器或操作系统信任它。

使用以下openssl命令生成一个密钥对和自签名的公钥证书。此示例生成一个椭圆曲线密钥对和一个自签名的公钥证书。证书有效期为 10 年:

$ openssl req -x509 \                                  # ❶
    -nodes -days 3650 \                                # ❷
    -newkey ec:<(openssl ecparam -name prime256v1) \   # ❸
    -keyout private_key.pem \                          # ❹
    -out certificate.pem                               # ❺
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 生成一个 X.509 证书

❷ 使用 10 年的有效期

❸ 生成一个椭圆曲线密钥对

❹ 将私钥写入此位置

❺ 将公钥证书写入此位置

此命令的输出会提示您输入证书主题详细信息。您是主题。指定一个通用名称为localhost,以便在本地开发中使用此证书:

Country Name (2 letter code) []:US
State or Province Name (full name) []:AK
Locality Name (eg, city) []:Anchorage
Organization Name (eg, company) []:Alice Inc.
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:localhost    # ❶
Email Address []:alice@alice.com
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 用于本地开发

在提示符处按 Ctrl-C 停止运行的 Gunicorn 实例。要安装您的证书,请使用以下命令行重新启动 Gunicorn。keyfilecertfile 参数接受分别指向您的密钥文件和证书的路径。

$ gunicorn alice.wsgi \            # ❶
    --keyfile private_key.pem \    # ❷
    --certfile certificate.pem     # ❸
  • 1
  • 2
  • 3

❶ alice.wsgi 模块位于 alice/alice/wsgi.py。

❷ 您的私钥文件

❸ 您的公钥证书

Gunicorn 自动使用安装的证书来通过 HTTPS 而不是 HTTP 提供 Django 流量。将浏览器指向 https:/./localhost:8000 再次请求欢迎页面。这将验证您的证书安装并开始 TLS 握手。记得将 URL 方案从 http 更改为 https

当您的浏览器显示错误页面时不要感到惊讶。此错误页面将特定于您的浏览器,但根本问题相同:浏览器无法验证自签名证书的签名。您现在正在使用 HTTPS,但握手失败了。要继续,您需要让操作系统信任您的自签名证书。我无法覆盖解决此问题的每种方法,因为解决方案特定于您的操作系统。以下是在 macOS 上信任自签名证书的步骤:

  1. 打开密钥链访问,这是由 Apple 开发的密码管理实用程序。

  2. 将您的自签名证书拖到密钥链访问的证书部分。

  3. 在密钥链访问中双击证书。

  4. 展开信任部分。

  5. 在使用此证书下拉列表中,选择始终信任。

如果您使用不同的操作系统进行本地开发,我建议您搜索“如何在 <我的操作系统> 中信任自签名证书”。预计解决方案最多需要 5 分钟。与此同时,您的浏览器将继续防止中间人攻击。

浏览器会在操作系统之后信任您的自签名证书。重新启动浏览器以确保此过程快速完成。在 https:/./localhost:8000 上刷新页面以获取欢迎页面。您的应用程序现在正在使用 HTTPS,并且您的浏览器已成功完成握手!

将您的协议从 HTTP 升级到 HTTPS 是在安全方面的巨大进步。我用两件事情来结束这一节,您可以做两件事来使您的服务器更安全:

  • 禁止具有 Strict-Transport-Security 响应头的 HTTP 请求

  • 将入站 HTTP 请求重定向到 HTTPS

6.5.2 Strict-Transport-Security 响应头

服务器使用 HTTP Strict-Transport-Security(HSTS)响应头告诉浏览器只能通过 HTTPS 访问。例如,服务器将使用以下响应头指示浏览器在接下来的 3600 秒(1 小时)内只能通过 HTTPS 访问:

Strict-Transport-Security: max-age=3600
  • 1

冒号右侧的键值对,以粗体字显示,被称为指令。指令用于参数化 HTTP 头。在这种情况下,max-age指令表示浏览器应该仅在 HTTPS 上访问站点的时间,以秒为单位。

确保您的 Django 应用程序的每个响应都具有带有SECURE_HSTS_SECONDS设置的 HSTS 头。分配给此设置的值将转换为头文件的max-age指令。任何正整数都是有效值。

警告:如果您正在处理已经投入生产的系统,请非常小心处理SECURE_HSTS_SECONDS。此设置适用于整个站点,而不仅仅是请求的资源。如果您的更改导致任何问题,影响可能会持续与max-age指令值一样长。因此,向具有较大max-age指令的现有系统添加 HSTS 头是有风险的。逐步增加SECURE_HSTS_SECONDS从一个小数字开始是一个更安全的部署更改的方法。多小?问问自己如果出现问题,您可以承受多少停机时间。

服务器使用includeSubDomains指令发送 HSTS 响应头,告诉浏览器除了域名之外,所有子域都应该仅通过 HTTPS 访问。例如,alice.com 将使用以下响应头指示浏览器,alice.com 和 sub.alice.com 应该仅通过 HTTPS 访问:

Strict-Transport-Security: max-age=3600; includeSubDomains
  • 1

SECURE_HSTS_INCLUDE_SUBDOMAINS设置配置 Django 发送带有includeSubDomains指令的 HSTS 响应头。该设置默认为False,如果SECURE_HSTS_SECONDS不是正整数,则会被忽略。

警告:与SECURE_HSTS_SECONDS相关的每个风险都适用于SECURE_HSTS_INCLUDE_SUBDOMAINS。糟糕的部署可能会影响每个子域,持续时间为max-age指令值。如果您正在处理已经投入生产的系统,请从一个小值开始。

6.5.3 HTTPS 重定向

HSTS 头是一个很好的防御层,但作为响应头只能做到这么多;浏览器必须先发送请求,然后才能接收到 HSTS 头。因此,在初始请求结束时将浏览器重定向到 HTTPS 是很有用的。例如,对于 http:/./alice.com 的请求应该被重定向到 https:/./alice.com。

通过将SECURE_SSL_REDIRECT设置为True,确保您的 Django 应用程序将 HTTP 请求重定向到 HTTPS。将此设置分配为True会激活另外两个设置,SECURE_REDIRECT_EXEMPTSECURE_SSL_HOST,下面将介绍这两个设置。

警告:SECURE_SSL_REDIRECT默认为False。如果您的站点使用 HTTPS,则应将其设置为True

SECURE_REDIRECT_EXEMPT设置是用于暂停某些 URL 的 HTTPS 重定向的正则表达式列表。如果此列表中的正则表达式与 HTTP 请求的 URL 匹配,Django 将不会将其重定向到 HTTPS。此列表中的项目必须是字符串,而不是实际编译的正则表达式对象。默认值为空列表。

SECURE_SSL_HOST 设置用于覆盖 HTTPS 重定向的主机名。如果此值设置为 bob.com,Django 将永久重定向对 http:/./alice.com 的请求到 https:/./bob.com 而不是 https:/./alice.com。默认值为 None

到目前为止,你已经学到了很多关于浏览器和 Web 服务器如何通过 HTTPS 通信的知识;但浏览器并不是唯一的 HTTPS 客户端。在下一节中,你将看到如何在 Python 中以编程方式发送请求时使用 HTTPS。

6.6 TLS 和 requests 包

requests 包是 Python 中流行的 HTTP 库。许多 Python 应用程序使用此包在其他系统之间发送和接收数据。在本节中,我将介绍几个与 TLS 相关的功能。在你的虚拟环境中,使用以下命令安装 requests

$ pipenv install requests
  • 1

当 URL 方案为 HTTPS 时,requests 包会自动使用 TLS。下面代码中粗体显示的 verify 关键字参数禁用了服务器身份验证。此参数不会禁用 TLS;它放宽了 TLS。对话仍然是保密的,但服务器不再被验证:

>>> requests.get('https://www.python.org', verify=False)
connectionpool.py:997: InsecureRequestWarning: Unverified HTTPS request is
being made to host 'www.python.org'. Adding certificate verification is
strongly advised.
<Response [200]>
  • 1
  • 2
  • 3
  • 4
  • 5

显然,此功能在生产环境中是不合适的。它在集成测试环境中通常很有用,当系统需要与没有静态主机名的服务器通信时,或者与使用自签名证书的服务器通信时。

TLS 身份验证是双向的:除了服务器之外,客户端也可以被验证。TLS 客户端通过公钥证书和私钥进行自身验证,就像服务器一样。requests 包支持使用 cert 关键字参数进行客户端身份验证。下面代码中粗体显示的这个 kwarg 期望一个两部分元组。此元组表示证书和私钥文件的路径。verify kwarg 不影响客户端身份验证;cert kwarg 不影响服务器身份验证:

>>> url = 'https://www.python.org'
>>> cert = ('/path/to/certificate.pem', '/path/to/private_key.pem')
>>> requests.get(url, cert=cert)
<Response [200]>
  • 1
  • 2
  • 3
  • 4

或者,verifycert 关键字参数的功能可以通过 requestsSession 对象的属性来实现,如下所示:

>>> session = requests.Session()
>>> session.verify=False
>>> cert = ('/path/to/certificate.pem', '/path/to/private_key.pem')
>>> session.cert = cert
>>> session.get('https://www.python.org')
<Response [200]>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

TLS 不仅适用于 HTTP。数据库流量、电子邮件流量、Telnet、轻量级目录访问协议(LDAP)、文件传输协议(FTP)等都可以运行在 TLS 上。这些协议的 TLS 客户端具有比浏览器更多的“个性”。这些客户端在能力上差异很大,并且它们的配置更具供应商特定性。本章以超出 HTTP 范围的 TLS 两个用例结束:

  • 数据库连接

  • 电子邮件

6.7 TLS 和数据库连接

应用程序应确保数据库连接也使用 TLS 进行安全连接。TLS 确保你的应用程序连接到正确的数据库,并且从数据库写入和读取的数据不能被网络攻击者拦截。

Django 数据库连接由 DATABASES 设置管理。该字典中的每个条目代表不同的数据库连接。以下清单展示了默认的 Django DATABASES 设置。ENGINE 键指定了 SQLite,一个基于文件的数据库。NAME 键指定了存储数据的文件。

清单 6.7 默认的 Django DATABASES 设置

DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.sqlite3',
       'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),    # ❶
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ 在项目根目录的 db.sqlite3 中存储数据

默认情况下,SQLite 将数据存储为明文。很少有 Django 应用程序使用 SQLite 进入生产环境。大多数生产 Django 应用程序将通过网络连接到数据库。

数据库网络连接需要通用的自解释字段:NAMEHOSTPORTUSERPASSWORD。另一方面,TLS 配置对每个数据库都是特定的。供应商特定的设置由 OPTIONS 字段处理。此清单展示了如何配置 Django 以在 PostgreSQL 中使用 TLS。

清单 6.8 安全地使用 Django 与 PostgreSQL

DATABASES = {
   "default": {
       "ENGINE": "django.db.backends.postgresql",
       "NAME": "db_name",
       "HOST": db_hostname,
       "PORT": 5432,
       "USER": "db_user",
       "PASSWORD": db_password,
       "OPTIONS": {                     # ❶
           "sslmode": "verify-full",    # ❶
       },                               # ❶
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

❶ 供应商特定的配置设置位于 OPTIONS 下

不要假设每个 TLS 客户端都像浏览器一样执行服务器身份验证。如果未配置,TLS 客户端可能不会验证服务器的主机名。例如,PostgreSQL 客户端在连接时以两种模式验证证书的签名:verify-caverify-full。在 verify-ca 模式下,客户端不会根据证书的通用名称验证服务器主机名。这种检查只在 verify-full 模式下执行。

注意:加密数据库流量不能替代加密数据库本身;请始终同时进行。请查阅您的数据库供应商文档,了解更多关于数据库级加密的信息。

6.8 TLS 和电子邮件

Django 对电子邮件的回应是 django.core.mail 模块,这是 Python 的 smtplib 模块的包装 API。Django 应用程序使用简单邮件传输协议(SMTP)发送电子邮件。这种流行的电子邮件协议通常使用端口 25。与 HTTP 类似,SMTP 是上世纪 80 年代的产物。它不会尝试确保机密性或身份验证。

攻击者极有动机发送和接收未经授权的电子邮件。任何易受攻击的电子邮件服务器都可能成为垃圾邮件收入的潜在来源。攻击者可能希望未经授权地访问机密信息。许多网络钓鱼攻击都是从受攻击的电子邮件服务器发起的。

组织通过在传输中加密电子邮件来抵御这些攻击。为防止网络窃听者拦截 SMTP 流量,必须使用 SMTPS。这只是 TLS 上的 SMTP。SMTP 和 SMTPS 类似于 HTTP 和 HTTPS。您可以通过下面两节中介绍的设置将连接从 SMTP 升级到 SMTPS。

6.8.1 隐式 TLS

有两种启动到电子邮件服务器的 TLS 连接的方式。RFC 8314 将传统方法描述为“客户端建立明文应用程序会话……随后进行 TLS 握手,可以升级连接。” RFC 8314 推荐“一种在连接开始时立即进行 TLS 协商的替代机制,使用单独的端口。” 推荐的机制称为 隐式 TLS

EMAIL_USE_SSLEMAIL_USE_TLS 设置配置 Django 以通过 TLS 发送电子邮件。这两个设置默认为 False,只能有一个设置为 True,而且两者都不直观。合理的观察者会假设 EMAIL_USE_TLS 优于 EMAIL_USE_SSL。毕竟,TLS 在安全性和性能方面多年来取代了 SSL。不幸的是,隐式 TLS 是由 EMAIL_USE_SSL 而不是 EMAIL_USE_TLS 配置的。

使用 EMAIL_USE_TLS 比什么都不用要好,但是如果您的电子邮件服务器支持隐式 TLS,请使用 EMAIL_USE_SSL。我不知道为什么 EMAIL_USE_SSL 没有命名为 EMAIL_USE_IMPLICIT_TLS

6.8.2 电子邮件客户端身份验证

requests 包一样,Django 的电子邮件 API 支持 TLS 客户端身份验证。EMAIL_SSL_KEYFILEEMAIL_SSL_CERTFILE 设置代表私钥和客户端证书的路径。如果未启用 EMAIL_USE_TLSEMAIL_USE_SSL,这两个选项都不起作用,这是预期的。

不要假设每个 TLS 客户端都执行服务器身份验证。在撰写本文时,不幸的是 Django 在发送电子邮件时不执行服务器身份验证。

注意:与数据库流量一样,加密传输中的电子邮件不能替代加密静态电子邮件;一定要两者都做。大多数供应商会自动为您加密静态电子邮件。如果没有,请查阅您的电子邮件供应商文档,了解更多关于静态电子邮件加密的信息。

6.8.3 SMTP 身份验证凭据

EMAIL_USE_TLSEMAIL_USE_SSL 不同,EMAIL_HOST_USEREMAIL _HOST_PASSWORD 设置是直观的。这些设置代表 SMTP 认证凭据。SMTP 在传输过程中不会试图隐藏这些凭据;如果没有 TLS,它们很容易成为网络窃听者的目标。以下代码演示了在以编程方式发送电子邮件时如何覆盖这些设置。

清单 6.9 在 Django 中以编程方式发送电子邮件

from django.core.mail import send_mail

send_mail('subject',
         'message',
         'alice@python.org',                    # ❶
         ['bob@python.org'],                    # ❷
         auth_user='overridden_user_name',      # ❸
         auth_password='overridden_password')   # ❹
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

❶ 发件人电子邮件

❷ 收件人列表

❸ 覆盖 EMAIL_HOST_USER

❹ 覆盖 EMAIL_HOST_PASSWORD

在本章中,您学到了关于 TLS 的很多知识,这是传输中的加密行业标准。您知道此协议如何保护服务器和客户端。您知道如何将 TLS 应用于网站、数据库和电子邮件连接。在接下来的几章中,您将使用此协议安全地传输诸如 HTTP 会话 ID、用户身份验证凭据和 OAuth 令牌等敏感信息。您还将在本章中创建的 Django 应用程序的基础上构建几个安全的工作流程。

概要

  • SSL、TLS 和 HTTPS 不是同义词。

  • 中间人攻击有两种形式:被动和主动。

  • TLS 握手建立了一个密码套件、一个共享密钥和服务器身份验证。

  • Diffie-Hellman 方法是密钥分发问题的高效解决方案。

  • 公钥证书类似于您的驾驶执照。

  • Django 不负责 HTTPS;Gunicorn 负责。

  • TLS 身份验证适用于客户端和服务器。

  • TLS 除了保护 HTTP 外,还保护数据库和电子邮件流量。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/485451
推荐阅读
相关标签
  

闽ICP备14008679号