This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
<audio id="audio" title="02单向散列函数如何保证信息完整性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/63/08/6393578ab5e0c7bf98899503b4eff308.mp3"></audio>
你好,我是范学雷。
从今天开始,我就要和你一起逐渐接触密码学的具体细节了。在这个过程中,我会讲到很多密码学相关的概念和诉求,这是我们打好基础的关键。不过,你也不用紧张,我会和你一起分析,一步步带你掌握这些知识点。
还记得上一讲,我们讨论的话题吗?我们通过牛郎织女约会送信的小例子,探讨了“密码学有什么用”这个问题,从而理解了信息安全的基本问题和基本需求。
问题出现了,我们也知道了该用密码学。现在就要来解决问题了,接下来的几讲,我们就先来解决“信息的完整性”这个问题。你还记得解决完整性的工具是什么吗?——单向散列函数。
可是,什么是单向散列函数?它是怎么解决完整性问题的?今天,我们就来讨论这两个问题。
## 什么是单向散列函数?
首先,我们从名字上看,一眼就能看出来单向散列函数有两个关键修饰词,“单向”和“散列”。
其实,在数学上,单向函数和散列函数是两个不同类型的函数。所以,我们要想理解单向散列函数,我们就要先知道什么是单向函数,什么又是散列函数。
### 什么是单向函数?
如果你没有了解过什么是单向函数,你可以先猜一下,为什么它叫单向函数?
**单向函数One-way Function<strong><strong>是正向计算容易,逆向运算困难的函数**</strong></strong>。也就是说,给定你一个输入,你很容易计算出输出;但是给定你一个输出,你却很难计算出输入是什么。
<img src="https://static001.geekbang.org/resource/image/3f/c0/3fae555f47d77acb0d375f832d0abec0.jpg" alt="">
还有这样的函数?是不是感觉有点神奇?
《应用密码学》有一个很生动的例子来解释单向函数。把盘子打碎是一件很简单的事情,但是把这些碎片再拼接成一个完整的盘子,就是一件非常困难的事情。
也许,你会想,虽然把盘子碎片再拼接起来非常困难,但是仅仅就是非常困难而已,无论是手工还是计算机辅助,碎盘子还是可以拼接起来的。是的,这就是这个例子巧妙的地方。
单向函数就是这样的一个盘子。虽然我们强调,单向函数只能正向计算,不能逆向运算。但其实,这只是一个美好的愿望。为什么我这么说?
因为,我们能找到的、谈到的所谓的单向函数,都是正向计算容易,逆运算困难的函数。是的,我用的词语是“困难”,而不是“不能”,可能性只是很小,但不是没有。
在我们的日常生活里,泼出去的水再也收不回,说过的话、做过的事也没地方买后悔药,单向似乎才是生活的常态。但在数学领域,有很多函数看起来像是严格的单向函数,我们既证明不了它是单向函数,也暂时找不到逆向运算的办法。到底有没有逆向运算的办法,我们现在还不知道。
为什么我要和你强调“逆向运算困难”这件事?因为密码理论领域里很多棘手的问题,密码应用领域里的很多错误,都是来源于单向函数的这种不确定性。
比方说吧,每一个被破解的单向散列函数的密码学算法,在它被发明的时候,人们都没有找到逆向运算的办法,可是被破解的时候,人们就发现原来还是有办法去逆向运算的。
今天还是安全的算法,明天就可能被破解。**这虽然使得密码学充满了挑战,但同时也使密码学充满了乐趣。**
不过,需要注意的是,我们要对这种不确定性保持足够的警惕,采取足够的防范措施。比如说,一个应用程序,至少要支持两种单向函数,当一种出现问题时,另外一种可以替补。
现在你知道了,单向函数是一个正向计算容易,逆向运算困难的函数。那我要是问你,对于我们来说,什么样的单向函数会更实用呢?我想,你应该可以回答出来:
- 一个更实用的单向函数,正向计算会更容易,容易程度就是这个函数的**计算性能;**
- 一个更实用的单向函数,逆向运算会更困难,困难程度就是这个函数的**破解强度**。
同样,我还是要强调一下,**一个实用的单向函数,计算强度和破解强度要均衡考量,不可偏废**。
以后我们谈到单向函数,指的都是正向计算容易,逆向运算困难的函数,除非特别声明。
### 什么是散列函数?
讲完了单向函数,我们再来看什么是散列函数。
**散列函数Hash Function是一个可以把任意大小的数据转行成固定长度的数据的函数**。比如说无论输入数据是一个字节或者一万个字节输出数据都是16个字节。
我们把转换后的数据,叫做**散列值**。因为散列函数经常被人们直译为哈希函数,所以我们也可以称散列值为哈希值。通常的,对于给定的输入数据和散列函数,散列值是确定不变的。
<img src="https://static001.geekbang.org/resource/image/91/98/91eae9e248828d8592fe0957fd633d98.jpg" alt="">
你可能会说,我懂了,不就是输入数据任意长度,输出数据固定长度吗?
是的,可问题也来了,既然输入数据的大小没有限制,而输出结果的数据长度固定,那么你觉得,会不会存在散列值相同的两个或者多个数据呢?——是确定存在的。
通常,我们把这种情况称为**散列值碰撞**。对于散列函数,散列值碰撞可不是一件好事情。
如果你学过Java语言或者数据结构和算法应该对哈希值这个概念不陌生。Java语言里的hashCode()方法,或者数据结构和算法里的哈希值,就是一个散列函数的运用。
如果hashCode()的实现出现散列值碰撞就会影响应用程序的性能比如HashMap的检索时间会显著加长。再比如说如果我们使用hashCode作为键值或者索引散列值碰撞会导致检索错误从而带来数据安全问题。
我在第一季[《代码精进之路》](https://time.geekbang.org/column/intro/100019601)专栏里,也讨论过散列值碰撞的性能基准测试。如果 10,000 个对象,只有 10 个不同的散列值,它的集合运算的性能是令人担忧的。因为这样和使用了没有散列值碰撞的实现相比,在性能方面,会有几百倍的差异。
现在,你应该意识到这个问题的重要性了,那么,我们应该怎样避免散列值碰撞呢?其实,因为输入数据的大小没有限制,输出数据的长度固定,理论上,我们是无法避免散列值碰撞的。
我们只能在降低散列值碰撞的可能性上想办法。也就是,我们要思考,如果我们不能避免散列值碰撞,我们会有什么办法可以降低散列值碰撞的风险呢?你可以先想一想。
最直观的办法,就是在**输出数据的长度**上想办法。虽然散列值长度固定,但是,我们可以让数据变得更长,**散列值越长,存在相同散列值的概率就越小,发生碰撞的可能性就越小**。
比如说32位固定长度的散列值就要比16位固定长度的散列值发生碰撞的可能性更小。
是不是觉得我们可以解决问题了?问题是解决了,但从另一个角度来说,散列值越长,通常也就意味着计算越困难,计算性能越差。而且,你想一想,为什么当初我们要使用固定长度的散列值?不就是为了减少计算本身的性能损耗,从而获得性能优化吗?
所以,散列值也不是越长越好。那么,我们到底该如何选择散列值的长度呢?
其实,散列值的长度选择,应该是**权衡性能**后的结果。比如Java语言里hashCode()的返回值是32位的整数也就意味着散列值的长度是32位。由于hashCode()的返回值主要是用来检索32位的整数已经足够大了所以这是一个合适的选择。
除了散列值长度之外,想要降低散列值碰撞的可能性,我们还要考虑散列值的质量。**一个好的散列函数,它的散列值应该是均匀分布的**。也就是说,每一个散列值出现的概率都是一样的。
如果不这样的话,一部分散列值出现的概率就会较高,另一部分散列值出现的概率会较低,别人就更容易构造出两个或者多个数据,使得它们具有相同的散列值。这种行为,叫做**碰撞攻击**。
如果你要实现在Java的hashCode()方法就需要考虑散列值的均匀分布问题。你可以看看《Effective Java》这本书里面有专门的文章介绍如何实现hashCode()方法,降低散列值碰撞的风险。
## 什么是单向散列函数?
我们说完了什么是单向函数和什么是散列函数,现在我们可以探讨什么是单向散列函数了。
**单向散列函数既是一个单向函数,也是一个散列函数**。它不仅要满足单向函数的要求,还要满足散列函数的要求。你还记得这两种函数的要求吗?其中,最要紧的就是:
- 逆向运算困难;
- 构造碰撞困难。
大部分的hashCode()方法的实现都满足不了逆向运算困难的要求所以它们是不能算作单向散列函数的。比如说按照Java的hashCode()方法的实现32位整数的哈希值是这个整数本身所以逆向运算一点难度都没有当然不能算作单向散列函数。
单向散列函数是一定要逆向运算困难的。
至于构造碰撞困难我用现成的单向散列函数给你举一个例子比如SHA-1算法它是一个常见的适用于密码学的单向散列函数。
现在你面前有两句话分别是“Hello, world!”和“Hello, vorld!”,这两句话只有一位的差异(w: 119/01110111, v: 118/01110110)我把它们的SHA-1算法计算出来的散列值列在了下面。
你可以对比两个散列值,感受一下一个位的输入数据差异,计算出的散列值能有多大的差异。
```
SHA-1(&quot;Hello, world!):
10010100 00111010 01110000 00101101 00000110 11110011 01000101 10011001 10101110 11100001 11111000 11011010 10001110 11111001 11110111 00101001 01100000 00110001 11010110 10011001
SHA-1(&quot;Hello, vorld!):
11001011 11111111 11111011 10010011 01010111 11000010 10001101 01011000 00100010 11000100 01010110 10000110 00101010 00110011 01010000 10111110 10000010 01111111 00100000 10101010
```
是不是差异还挺大的?这种现象,我们把它叫做雪崩效应。
**雪崩效应Avalanche Effect是密码学算法一个常见的特点指的是输入数据的微小变换就会导致输出数据的巨大变化**。严格雪崩效应是雪崩效应的一个形式化指标,我们也常用来衡量均匀分布。**严格雪崩效应指的是如果输入数据的一位反转输出数据的每一位都有50%的概率会发生变化。**
一个适用于密码学的单向散列函数,就要具有雪崩效应的特点,也就是说,如果一个单向散列函数具有雪崩效应,那么对于给定的数据,构造出一个新的、具有相同散列值的数据是困难的。
在这一讲的一开始,我们说过,密码学的单向散列函数是用来解决数据完整性问题的。那么,单向散列函数是怎么解决数据完整性问题的呢?
## 怎么解决完整性问题?
想要解决完整性问题,我们就要知道完整性问题的背后逻辑是什么。
完整性意味着什么?完整性的核心是**数据未经授权,不得更改**。对于“不得更改”这四个字,你最直观的感受是什么?是不是无论如何,数据都没有办法改动?这是一个很强的解读。一般情况下,也很难有满足的场景。
还有一种站在反面看的、曲线的解读,就是如果数据有变动,能够被检测出来,我们就不采纳被篡改的数据。使用单向散列函数,就可以通过检查数据是否有变动,来解决数据完整性问题。
我们刚才说了,在单向散列函数里,一段数据,无论它是少了一个字,多了一个字,或者修改了一个字,原始数据和修改后的数据的散列值都可能相差巨大。
而且,由于逆向运算困难,虽然存在具有相同散列值的两个或者多个数据,但是对于一个好的单向散列函数来说,刻意寻找这样的数据是困难的。如果困难程度足够大,我们就有足够信心认为,如果散列值没有变化,它对应的输入数据也没有变化。
所以,单向函数和散列函数的组合,单向散列函数,就可以帮助我们解决完整性问题。
假如我们收到了一段数据,我们就可以重新计算这段数据的散列值。如果我们还可以获得数据发送者计算的散列值,我们就可以对比新计算的散列值和接收到的散列值。如果两个散列值是相同的,我们就可以认为这段数据是完整的;否则,这段数据就是被篡改过的。
```
输入:
1、数据D
2、原始数据的散列值H
3、计算散列值使用的散列函数
输出:
数据D是不是完整的
运算:
1、使用散列函数计算数据D的散列值H';
2、对比数据的散列值H和计算获得的散列值如果两个散列值相同则数据D是完整的否则数据D是修改过的数据。
```
可是,这里面依然有两个遗留问题,也是我们使用单向散列函数需要特别关注的两个问题。
第一个问题是,我们该选择什么样的散列函数,它的破解难度才能足够大?这样,我们才有足够的信心根据散列值判断数据的完整性。
第二个问题是,我们怎么能够安全地获得数据发送者计算的散列值?如果我们接收到的是被修改过的数据和修改过的散列值,我们是没有办法判断数据是不是完整的。
第二个问题,我们放在稍后一点讨论。下一次,我们讨论第一个问题。
## Take Away今日收获
今天,我们讨论了单向函数、散列函数以及单向散列函数,还有怎么使用单向散列函数来解决数据和信息的完整性问题。
为什么我要先讲单向散列函数?因为,单向散列函数是密码学的基础。在一个应用系统里,如果单向散列函数选择失误,整个系统的安全性就无从谈起。之后,我们还会讨论单向散列函数是怎样和加密算法以及签名算法结合起来,构建宏大的信息安全基础架构的。
我们常说,铁打的营盘流水的兵。**在密码学里,最基础<strong><strong>的**</strong>概念**<strong>就**</strong>像是铁打的**<strong>营盘**</strong>,具有长久的生命力;而密码学算法就像是流水的兵,隔一阵儿就会换一茬</strong>
所以,每一次讨论,我总是会先交代清楚基本概念和基础诉求,然后再带你去看具体的算法。基本概念和基础诉求可以跟随你几十年,随着你对它们理解的加深,会逐渐加厚你的功力。理解了基本概念和基础诉求,你就可以得心应手地调度、安排生命只有十数年的密码学算法了。
这一讲,通过对单向散列函数的讨论,我们要:
<li>
**理解单向散列函数的以下三个特点:**
</li>
<li>
**单向散列函数正向计算容易,逆向运算困难;**
</li>
<li>
**单向散列函数运算结果均匀分布,构造碰撞困难;**
</li>
<li>
**对于相同的单向散列函数,** **给定数据的散列值是确定的,长度是固定的。**
</li>
<li>
**知道单向散列函数解决数据完整性问题的基本思路。**
</li>
## 思考题
我们回头看看上一次讨论过的牛郎织女的约会问题。牛郎要给织女发信息,七夕相约鹊桥会。
>
织女:
>
七月初七晚七点,鹊桥相会。不见不散。
>
牛郎
你能够帮助牛郎想想吗?该怎么使用单向散列函数,来防范约会信息被恶意修改?然后,你再想想,你建议的办法还有没有缺陷?欢迎在留言区留言,记录、讨论你的想法。
好的,今天就这样,我们下次再聊。

View File

@@ -0,0 +1,133 @@
<audio id="audio" title="03如何设置合适的安全强度" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/14/49/14aeff67bf3d3a740792f969a517d949.mp3"></audio>
你好,我是范学雷。
上一讲,我们讨论了单向散列函数,以及它是怎么解决数据完整性问题的。你还记得它解决问题的背后逻辑吗?就是因为单向散列函数有两个重要的特点:**逆向运算困难和构造碰撞困难**。
这两个特点使得我们仅仅修改数据中的一位,所得到的散列值和之前的相比,就会发生很大的变化。所以我们说,这两个困难也决定了一个单向散列函数的破解难度。
逆向运算越困难,破解难度越难;构造碰撞越困难,破解难度也越难。这点你应该懂了,但是,你有没有想过,困难程度要多大,才算困难?有什么指标可以衡量单向散列函数的破解难度?
一下出现这么多问题,是不是有点意外?其实,**密码学就是在和千奇百怪的问题<strong><strong>的**</strong>纠缠中获得进展的</strong>。这一次,我们来讨论困难有多难以及和破解难度相关的问题。
## 困难要有多难?
我们要探讨的第一个问题就是,一个单向散列函数的逆向运算和构造碰撞要困难到什么程度,它才能算是一个合格的单向散列函数呢?**如果凭感觉,在密码学的实践<strong><strong>中**</strong>**<strong>我们心中“完美”的**</strong>单向散列函数,应该困难到没有人可以逆向运算,也没有人可以构造碰撞。</strong>
可是,只要有人发现了有那么一对数据具有相同的散列值,不管这个人什么出身、什么来历,也不管这对数据有多么的千奇百怪,更不管破解方式是多么的不合常理,这个结果就意味着这个单向散列函数被破解了,不再安全了。
比如说下面的两段数据具有相同的MD5算法散列值MD5是一个单向散列函数
细心看的话你会注意到例子中的afbfa202和afbfa200以及6da0d1d5和6da0d155这两段数据是有差异的但是结果显示它们的散列值却是相同的。在这个例子里我们并不需要深入了解破解MD5的具体算法和实现我们只需要知道MD5被破解了MD5就不能够继续使用了。
```
M1:
4dc968ff 0ee35c20 9572d477 7b721587 d36fa7b2 1bdc56b7 4a3dc078 3e7b9518 afbfa202 a8284bf3 6e8e4b55 b35f4275 93d84967 6da0d1d5 5d8360fb 5f07fea2
M2:
4dc968ff 0ee35c20 9572d477 7b721587 d36fa7b2 1bdc56b7 4a3dc078 3e7b9518 afbfa200 a8284bf3 6e8e4b55 b35f4275 93d84967 6da0d155 5d8360fb 5f07fea2
Hash: MD5(M1) = MD5(M2)
008ee33a 9d58b51c feb425b0 959121c
```
我所了解的**现代单向散列函数<strong><strong>在**</strong>算法意义上的破解,都是通过宣布找到一对散列值碰撞的数据的形式发布的</strong>。还记得什么是散列值碰撞吧?就是指两份散列值的数据是相同的。
只有当你找到了这样的一对碰撞,你才能验证破解算法的有效性,算法的破解才能让人信服。
不过,话说回来,这固然是一个好的办法,可是对于还没有被破解的算法,有没有更直观的指标让我们感受它有多安全呢?对于已经破解的算法,有没有直观的指标让我们感受它有多脆弱呢?
在密码学这么讲究量化的领域当然不会缺少了这样的指标。其中最常用的指标就是安全强度Security Strength
## 什么是安全强度?
在密码学中安全强度通常使用“位”字节位来表述。比如说安全强度是32位。这里的“位”是什么意思**N位的安全强度表示破解一个算法需要2^N(2的N次方)次的运算**。
为什么要使用“位”来表示安全强度因为这样的话我们就可以很方便地比较不同算法的安全级别在同一个安全级别上组合不同的安全算法。比如说MD5的安全强度是不大于18位1024位的RSA密钥的安全强度是80位 SHA-256算法的安全强度是128位。
在这里给你出个小问题,如果我们把上面这几个算法安排成一个组合,这个组合的强度是怎样的?这个组合的强度并不高,因为**组合的强度,由最弱的算法和密钥决定**。所以,把它们安排成一个组合,不是一个好的想法。你可以先记下来,我们后面会再讨论算法组合的基本原则。
回到安全强度这个话题谈论单向散列函数算法之前让我们先来感受一下安全强度。比如MD5我们说了它的安全强度最多18位也就是说我们运算2^18=262144次就可以破解按现在的计算机一毫秒一次运算的速度计算需要262144毫秒折合4.34分钟。
MD5现在就是这么弱。其实在2006年就有研究者宣布研究成功即使是那时候的笔记本电脑在一分钟之内也可以找到一对散列值碰撞的数据了。
那128位的安全强度呢假设我们现在有一台速度快1000倍的计算机它能做到1纳秒运算一次。如果我们做类似上面的运算即使我们同时使用10亿台计算机破解它也需要一千万个十亿年。80位的安全强度同样的条件破解大概需要38年。
从上面的计算相信你可以感受到只是稍微增加几十位的安全强度破解难度就有巨大的提升。因为破解难度是安全强度位数的指数2^N。所以**在<strong><strong>实践**</strong>中,我们应该优先选择安全强度足够高的算法。</strong>
## 安全强度会变吗?
每一个密码算法诞生的时候,都有一个**理论上的设计安全强度**。注意理论上的意思就是有可能与实际情况不符。比如单向散列函数SHA-1在1993年发布的时候它的设计安全强度是80位。
12年后在2005年2月中国密码学家王小云教授带领的研究团队发现SHA-1的安全强度小于69位远远小于设计的80位。从此SHA-1的安全强度开始一路衰减。很快2005年8月王小云教授的团队又改进了破解算法发现SHA-1的安全强度只有63位了。
2015年10月密码学家马克·史蒂文斯Marc Stevens皮埃尔·卡普曼Pierre Karpman和托马斯·佩林Thomas Peyrin的研究团队发现SHA-1的安全强度只有57.5位。
更要紧的是他们估算如果使用云计算按照2015年亚马逊EC2云计算的定价和算力**57位的安全强度2015年的破解成本大致是10万美元**,你可以感受下密码强度和破解成本的数字。
2020年1月密码学家盖坦·勒伦GaëtanLeurent和托马斯·佩林Thomas Peyrin又发现 SHA-1的攻击复杂度是63.4位攻击成本大约为4.5万美元。
根据上面的数字,我们可以感受到,**一个64位安全强度的密码算法它现在的破解成本大概是5万美元左右**。不同类型的算法破解成本也许有很大偏差但是我们依然可以大致估算攻击成本。5万美元无论是对于一个有组织的研究机构还是犯罪集团都是一个很小的数目。
这可以说明什么如果一个系统的安全强度低于64位它的安全性几乎形同虚设。
通过SHA-1的例子我想强调的就是**一个算法的安全强度不是一成不变的。随着安全分析的进<strong><strong>步**</strong>,几乎所有密码学算法的安全强度都会衰减</strong>。今天看起来安全的算法,明天也许就有破解的办法。所以,**一个好的安全协议,应该考虑备份计划和应急计划**(参见极客时间[《代码精进之路》](https://time.geekbang.org/column/intro/100019601)专栏第41讲“预案代码的主动风险管理”里提到的双引擎和降落伞设计
## 使用多大的安全强度?
现在,我们已经知道了什么是安全强度,也感受了一下不同密码算法的安全强度,知道了安全强度是会变的。那么,我们今天要讨论的最后一个话题是,我们该使用多少位的安全强度?
多少位的安全强度算是安全的呢?其实,我们要是想找到一个确切的答案,我们不仅要看具体的使用场景,还要综合考虑性能和安全强度。是不是觉得会有点复杂和困难?
不过,**我<strong><strong>可以给你一个**</strong>建议,就是**<strong>参考、遵**</strong>循常用的推荐指标</strong>
业界内最新推荐的三个常用指标分别是:
- **美国的NIST国家标准技术研究所)**
- **德国的BSI联邦信息安全办公室**
- **欧洲的ECRYPT-CSA欧洲卓越密码网络**。
为了让你更直观地了解这三个指标,我还给你做了一个小结。
<img src="https://static001.geekbang.org/resource/image/22/9e/22828b7a07376a9afd082fdddfa1089e.jpg" alt="">
看到这个表,是不是感觉还是摸不到头脑?该怎么使用这个表呢?我们一起来看一个例子。
假设我们现在要设计一个新系统预期寿命十年也就是我们要从2020年开始运营运营到2030年结束。而且我们还要保证到2030年这个系统还是足够安全的。
首先我们按照NIST的建议2030年后112位的安全强度已经不能使用了所以如果我们遵守NIST的推荐指标这个系统就不建议选择112位安全强度的算法。
在BSI建议里2030年之前够用的话我们应该选择256位的安全强度。
我们再看ECRYPT-CSA的建议128位的安全强度只能用于2028年之前。到了2030年128位的安全强度就不能满足ECRYPT-CSA的建议了。所以如果我们遵循ECRYPT-CSA的建议这个系统就需要使用256位的安全强度。
你发现了吗ECRYPT-CSA的建议为什么这么保守其实这种保守的姿态背后隐含了对量子计算时代来临的担忧。在量子计算时代128位的安全强度稍显脆弱可是256位的安全强度还是足够的。**虽然量子时代还没有到来,但是我们现在就要开始考虑量子时代的挑战了。**
从上面的推荐,我们可以看到,**128位的安全强度目前来说是安全的**。不过,一个需要长期运营的系统,**如果性能瓶颈不是问题,现在就可以<strong><strong>开始**</strong>考虑使用256位强度的密码算法了</strong>
还记得我们上面提到的安全强度不足18位的MD5函数吗这么弱的安全强度几乎已经没有实用价值了。那么有哪些单向散列函数能达到128位甚至256位的安全强度这些问题我们下一次来讨论。
## Take Away今日收获
今天,通过讨论单向散列函数的“两个困难程度”,我们知道了困难有多难,还分析了破解强度的计量办法、安全强度的衰减、常见的安全强度推荐指标,以及一些可以直观感受的数字。
这些直观感受的数字可以帮助你建立对密码算法安全强度的印象。比如一个64位安全强度的密码算法它现在的破解成本大概是5万美元左右。再比如128位的安全强度按照现有的计算能力破解它需要一千万个十亿年。
这一讲,通过对安全强度的讨论,我们要:
- **知道<strong><strong>密码学**</strong>安全强度通常使用位来表示;</strong>
- **知道128位的安全强度<strong><strong>暂时**</strong>还是安全的;</strong>
- **知道长期的系统可以考虑开始使用256位安全强度的算法了。**
## 思考题
如果你能够使用你知道的所有的计算机包括你的个人计算机和公司的计算机系统比如亚马逊的云系统你能不能大概估算一下破解64位的安全强度、80位的安全强度、128位的安全强度分别都需要多长时间
这是一个能够帮助你建立对安全强度直观概念认知的办法。
欢迎在留言区留言,记录、讨论你的估算数据。
好的,今天就这样,我们下次再聊。

View File

@@ -0,0 +1,120 @@
<audio id="audio" title="04选择哈希算法应该考虑哪些因素" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/46/fb/464dc27f577a26817991a5036d0934fb.mp3"></audio>
你好,我是范学雷。
上一讲,通过讨论单向散列函数的两个困难程度,我们了解到了安全强度的计量办法,安全强度的衰减,以及常见的安全强度推荐指标,我们还对安全强度有了一个更直观的感受。
你还记得上一讲提到的现在的应用程序要使用128位或者更高安全强度的算法吗那么对于单向散列函数来说哪些算法能够满足这样的安全强度要求呢我们在选择这些算法的时候应该去考虑哪些因素呢这就是我们这次要解决的问题。
首先,让我们先来分析一下,还有哪些算法是可用的算法。
## 有哪些可用的算法?
为什么要先分析有哪些可用的算法呢?因为,在选择哈希算法的时候,我们的确需要综合考虑很多因素,但是如果这个算法是不可用的,其他因素也就无足轻重了。
所以,**判断一个现存的算法,还能不能继续使用是我们选择算法的第一步**。根据这个标准,我把常见的算法分为了以下三类:
- 退役的算法;
- 遗留的算法;
- 现行的算法。
**退役的算法,<strong><strong>就是那些**</strong>已经退出了历史舞台**<strong>的算法**</strong>**<strong>它们的**</strong>安全强度很弱,**<strong>你**</strong>一定不**<strong>能**</strong>再用了</strong>。如果你看到退役的算法还在使用,往往意味着这是一个过时的系统,或者是它的开发者缺少密码学常识(这怪不得它的开发者,毕竟密码学常识一直没有得到普及)。
如果是我们自己能够掌控的系统,一定要尽最大努力、尽快地升级算法。
**什么是遗留的算法?你只要记住,它们存在明显的安全问题,已经不足以支撑现在的安全强度需求了,你一定不要用在新系统中了**。因为,遗留的算法,已经走在退役的路上了。
那为什么有的人还在保留遗留的算法?因为,保留遗留算法,还是会让系统有更好的兼容性和互操作性,给现有系统升级到新算法留有一段时间。但是,新的代码和项目,就不要再使用遗留算法了。现在,还要继续运营的系统,也要想办法尽快升级算法。
到了最后,**只有现行的算法,<strong><strong>没有明显的安全问题,<strong><strong>是**</strong>我们</strong></strong>现在可以使用的算法</strong>。这是因为现行算法的安全性,是经过很多密码学专家分析验证的。一般到目前为止,还没有人能发现明显的安全缺陷。现行的算法,才是我们应该使用、可以放心的算法。
不过不同的推荐指标对于算法的选择也有不同的考量和倾向。我们这里使用上一讲我们提到过的较为保守的ECRYPT-CSA的2018年建议。
在下面的表格里我罗列了一些常见的算法以及一些相关的信息。其中计算性能参考的是ECRYPT在2020年7月和2019年10月对4096个字节数据的性能基准测试结果。
什么是计算性能它表示在数据运算时处理一个字节处理需要执行的微处理器的时钟周期数。它使用的度量单位是每字节周期数CPBCycles Per Byte
每字节周期数是一个常用的密码算法实际性能的参考指标。每字节花费的时钟周期数越小,表示这个算法运算得越快,性能越好。
<img src="https://static001.geekbang.org/resource/image/af/a9/af9f9f99535ea29f54b934cf2ce449a9.jpg" alt=""><img src="https://static001.geekbang.org/resource/image/e4/bb/e454af18046b37e5d51e575601e119bb.jpg" alt=""><img src="https://static001.geekbang.org/resource/image/79/77/79236952581b4a612749cbec11cc2577.jpg" alt="">
## 为什么有处理能力限制?
在上面的表格里你能看到有一列说的是数据处理能力。数据处理能力指的是对应的单向散列函数能够处理的最大的输入数据。比如SHA-256能够处理的最大数据是2^64位。
我们前面说过单向散列函数可以把任意大小的数据转行成固定长度的数据。那为什么有的单向散列函数还有处理能力限制呢上限不应该是无限大吗为什么有的单向散列函数比如SHAKE128又没有处理能力限制呢
问题虽然有点多,不过还是值得我们关注的。我们要想了解这个数据处理能力的限制是什么意思,就要知道它的由来。也就是说,我们需要了解单向散列函数是如何处理输入数据的。
一个典型的单向散列函数,由四个部分组成:数据分组、链接模式、单向压缩函数和终结函数。
<img src="https://static001.geekbang.org/resource/image/76/63/766c6b81c43b684a0f083c540ea94163.jpeg" alt="" title="单向散列函数处理过程">
我们来看数据分组,数据分组负责把输入数据分割成压缩函数能够处理的数据块。在上面的表里,有一项是“数据分块”,按照“位”来计量,指的就是压缩函数能够处理的数据块尺寸。
一般来说压缩函数能够接收的数据块大小是固定的。比如SHA-256的压缩函数只能处理512位的数据多一位不行少一位也不行。
可是实际单向散列函数的输入数据的大小不一定就是一个完整的、压缩函数可以接收的数据块。比如说我们可能使用SHA-256处理一个字节可能处理一千个字节也可能处理一百万个字节。输入数据可能是512位的整数倍也可能不是。
然后,整数倍的数据,可以送给压缩函数分批处理;不足整数倍的数据,就需要填充、补齐,变成压缩函数可以处理的数据块大小。
在下图SHA-1和SHA-2的数据补齐方案里输入数据长度是补充数据的一部分。其中SHA-1、SHA-224、SHA-256使用64位来表示输入数据长度SHA-384、SHA-512、 SHA-512/224和 SHA-512/256使用128位来表示输入数据长度。
但是,如果输入数据长度超过了数据补齐方案的限制,数据就没有办法分组了。这就是单向散列函数数据处理能力限制的来源。
<img src="https://static001.geekbang.org/resource/image/81/8f/810cda88e56e0yy5917220c3e1a3658f.jpg" alt="">
而我们说SHA-3的设计放弃了在数据补齐方案里使用固定位数表示输入数据长度的做法它也就不再有数据处理能力的限制。
幸运的是,到目前为止,这个数据处理能力限制是很高的,一般的应用程序很难超越它。不过,我们心里还是要有一根弦,需要考虑数据处理能力限制的时候,我们千万不能疏忽了它。
**选择什么样的数据补齐方案,是密码学里一个棘手的问题**。很多针对密码算法的攻击,都是从数据补齐方案下手的。我们关注这个问题,更多地是为了简单地了解单向散列函数的内部计算。
之后,我们会详细讨论单向散列函数的一个常见的算法错配问题:长度延展攻击。
## 算法的性能是怎么决定的?
我们再来看一个在选择哈希算法时应该重点考虑的因素,算法的性能问题。
从理论上讲,一个算法的性能主要是由算法的复杂度决定的。这里有一个假设,就是不考虑其他因素的影响。但是在实践中,其他因素有时候才是影响算法性能的主要因素,比如实现细节。
在一个算法的实现细节中,通常影响计算性能的因素有:
- 算法实现的内存使用影响;
- 算法实现有没有使用优化的步骤,比如并行计算或者预运算?
- 算法实现有没有使用硬件加速比如使用CPU关于算法的扩展指令
一个规规矩矩的算法实现它的性能一般落后于CPU扩展指令数十倍。遗憾的是并不是每一个算法都有CPU扩展指令或者每一个实现都支持CPU扩展指令。
另外计算机本身的指令集比如是使用32位还是64位的指令是否和算法匹配也是影响算法性能的一个重要因素。我们经常可以看到SHA-512的计算速度比SHA-256还要快。SHA-256使用32位的数据进行计算而SHA-512使用64位的数据进行计算。
现在的计算机一般都是64位的。所以运行在64位的计算机上基于32位的计算可能反而比基于64位的计算还要慢。
这对我们选用算法有什么启示呢?一个应用程序,**一般而言应该选用现行的、流行的算法。现行推荐的算法保证了算法的安全性。流行的算法成熟的实现会考虑实现优化包括CPU扩展指令的支持**。选用流行的算法,也是获得较好计算性能的一个实践办法。
对于单向散列函数,目前现行的、流行的算法有:
- SHA-256
- SHA-384
- SHA-512
使用现行的、流行的算法是不是就万无一失了?遗憾的是,我们依然需要小心谨慎,不要掉进已知的安全漏洞陷阱。下一次,我们讨论单向散列函数在应用中常见的问题,包括我们上面提到的“长度延展攻击”。
## Take Away今日收获
今天,通过罗列常见的单向散列函数算法,我们讨论了退役的、遗留的和现行的算法分类;知道了单向散列函数的处理能力限制,以及处理能力限制的来源;我们还简单讨论了影响算法性能的常见因素。这都是我们在选择哈希算法时需要考虑的。
这一讲,我们要:
- **了解三类单向散列函数算法:退役的算法、遗留的算法以及现行的算法;**
- **知道要<strong><strong>尽量选用现行的、流行的算法**</strong>。对于单向散列函数,它们是**<strong>SHA-256SHA-384**</strong>和SHA-512。</strong>
## 思考题
今天留给大家的是一个需要动手的思考题。我们罗列了常见的单向散列函数算法,知道了退役的、遗留的和现行的算法。知道了这样的概念,我们就要把它用起来。
在你正在开发的项目中,或者你关注的开放源代码项目中,试着搜索一下这些算法,看看哪些退役的算法还在使用,哪些遗留的算法还在使用。如果发现了退役算法和遗留算法的使用,你有没有什么建议?
这是一个能够帮助你理解算法生命阶段、解决现有项目历史遗留问题的好办法。欢迎在留言区留言,记录、讨论你的发现和建议。
好的,今天就这样,我们下次再聊。

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="05如何有效避免长度延展攻击" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ac/a8/ac38df93bdf51f582df9d4f04ca7d0a8.mp3"></audio>
你好,我是范学雷。
上一讲,我们列举了常见的单向散列函数,我们还知道了退役的、遗留的和现行的算法,通过对处理能力限制和算法的性能的讨论,我们对如何选择哈希算法有了更明确的认知。
还记得我们留了一个小尾巴吗?我们提到了“长度延展攻击”。“长度延展攻击”是怎么一回事?我们为什么要了解它?在单向散列函数的使用上,我们需要注意哪些安全问题?
这就是我们这一次要解决的事情。
## 什么是长度延展攻击?
我们先来看看什么是“长度延展”,这样会有利于你理解“长度延展攻击”。
现在假设我们有两段数据S和M以及一个单向散列函数h。如果我们要把这两段数据合并起来并且还要计算合并后的散列值这就叫做单向散列函数的长度延展。
不过问题来了是S放在前面h(S|M)还是M放在前面h(M|S))?既然,我们说,散列值是无法预测的,那么,数据编排的顺序有意义吗?
如果S和M都是公开的信息顺序是不重要的。可如果S是机密信息M是公开信息这两段数据的排列顺序就至关重要了。**如果机密信息放在了前面,就存在“长度延展攻击”的风险**。
弄清楚了长度延展长度延展攻击就很好理解了就是说我们可以利用已知数据的散列值计算原数据外加一段延展数据后的散列值。也就是说如果我们知道了h(S|M)我们就可以计算h(S|M|N)。其中数据N就是原数据追加的延展数据。
如果S和M都是公开的信息能够计算延展数据的散列值也没什么紧要的。但是如果S是机密数据它的用途一般就和机密有点关系。比如说因为没有人知道我拥有的机密数据S所以当我给定一段公开信息M后只有我自己才能计算S和M的散列值。
通过验证S和M的散列值我就知道一个给定散列值是我计算、派发出去的还是别人伪造的。
比如下面的这段数据:
```
key_id=44fefa051fc1c61f5e76f27e620f51d5&amp;perms=read&amp;hash_sig=38d39516d896f879d403bd327a932d9e
```
其中key_id表示机密数据的编号perms表示操作权限hash_sig是使用机密数据key对perms的签名。签名的计算就是使用单向散列函数
```
sig = h(key|perms)
```
由于使用了机密数据key按照设想这段数据只能由机密数据的持有者生成然后分发出去供授权的人使用。机密数据的持有者接收到这样的数据后重新计算数据签名然后对比请求数据里的签名。如果两个签名相同就表示这是一个自己生成的、合法的授权就可以授予请求数据所要求的权利。
不过这个设计就存在“长度延展攻击”的风险。攻击者并不需要知道机密数据就可以通过一个已知的URL构造出一个新的合法的URL从而获得不同的授权。
伪造的数据看起来像下面的样子:
```
key_id=44fefa051fc1c61f5e76f27e620f51d5&amp;perms=read\0x80\0x00...\0x02&amp;delete&amp;hash_sig=a8e6b9704f1da6ae779ad481c4c165a3
```
在这段伪造的数据中0x80到0x02之间的数据是数据块补齐数据而且新添加了删除的权限并且重新计算、替换了数据签名。
其中,数据签名需要使用机密数据,而攻击者并不知道机密数据,那么攻击者怎样伪造数据签名呢?要解决这个疑问,我们需要先看看单向散列函数的构造。
我们在上一讲简单地提到过,一起来重新回顾一下。一个典型的单向散列函数,应该由四个部分组成:数据分组、链接模式、压缩函数和终结函数。
我们之前着重说了数据分组,我们现在来看看其他的部分:
<img src="https://static001.geekbang.org/resource/image/76/63/766c6b81c43b684a0f083c540ea94163.jpeg" alt="" title="单向散列函数处理过程">
- 压缩函数是单向函数,负责着算法的单向性要求;
- 终结函数不是单向函数,负责着整理压缩函数的输出,形成散列值的任务;
- 链接模式,负责把下一个数据分组和上一个压缩函数的输出结果结合起来,确保算法的雪崩效应能够延续。
值得一提的是在MD5SHA-1SHA-256和SHA-512的算法设计中终结函数就是把压缩函数的输出向量排列成一个字节串。知道了字节串我们也就知道了压缩函数的输出向量。
压缩函数接收一个数据分组和上一个压缩函数的运算结果。如果知道了上一个压缩函数的运算结果,我们就能够计算下一个分组数据的压缩函数运算结果。**这里,就是出现安全漏洞的地方**。
<img src="https://static001.geekbang.org/resource/image/e6/38/e6339667aff222cbdefa25dc79549638.jpeg" alt="" title="单向散列函数压缩函数示意图">
我们把原来的散列值作为压缩函数的一个输入,我们再按照数据补齐规范,去补齐原来数据到数据分组的整数倍,然后加入新的数据,我们就可以计算原数据和扩展数据的散列值了。
<img src="https://static001.geekbang.org/resource/image/31/a5/31e2ea275d7e21d49f34294319e31ea5.jpeg" alt="" title="单向散列函数长度延展攻击示意图">
新的散列值的计算,不需要知道预先设想的机密数据。但是整个散列值的计算,又的确使用了机密数据。只不过,这个计算过程需要两个部分,第一部分由机密数据的持有者计算,第二部分是攻击者使用第一部分的结果,伪造了一个使用了机密数据的散列值。
但是如果我们把数据编排顺序换一下把公开信息M放在前面机密信息S放在后面长度延展攻击就不起作用了。这就是数据编排顺序对数据安全性的影响。
## 怎么有效避免长度延展攻击?
一个单向散列函数只要使用了类似上述的压缩函数和链接模式都是“长度延展攻击”的可疑对象。我们上一次提到的MD2、MD5、SHA-0、SHA-1、SHA-2都有长度延展攻击的风险。其中对于下列算法长度延展攻击是完全有效的
- MD2
- MD5
- SHA-0
- SHA-1
- SHA-256
- SHA-512
对于下列算法,长度延展攻击虽然不是完全有效,但是算法的安全级别显著降低了:
- SHA-224
- SHA-384
对于下列算法长度延展攻击没有效果包括所有的SHA-3算法
- SHA-512/224
- SHA-512/256
- SHA-3
上面这么长的列表,你是不是觉得好多,有点烦?其实,我们讨论长度延展攻击,目的不是让你记住上述的列表。
我们要从中学会、理解一个实用的经验:**不要单纯使用单向散列函数来处理既包含机密信息、又包含公开信息的数据**。即使我们把机密信息放在最后处理,这种使用方式也不省心。
**如果<strong><strong>我们**</strong>需要使用机密数据产生数据的签名我们应该使用设计好的、经过验证的算法比如我们后面会讨论的消息验证码Message Authentication Code和基于单向散列函数的消息验证码Hash-based Message Authentication Code</strong>
另外,如果需要设计算法,我们还要理解另外一个实用的原则:**算法要皮实、耐用,不能有意无意地用错了就有安全漏洞**。你看SHA-1和SHA-2已经很简单、皮实了用错了场景还是有严重的问题。相比之下SHA-3同样简单但是更皮实。
这和我们在[《代码精进之路》](https://time.geekbang.org/column/intro/100019601)的专栏里反复讨论的API要简单、直观、皮实是一个道理。
既然我们不能单纯地使用单向散列函数处理混合了机密信息和公开信息的数据。那我们能不能单纯地使用机密信息,或者单纯地使用公开信息?回答这个问题,还要看具体的使用场景。
## 有哪些典型的适用场景?
我们已经知道了,单向散列函数是密码学的核心。下面是一些典型的使用单向散列函数的场景:
- 校验数据完整性;
- 数字签名,和非对称密钥及其算法结合使用;
- 消息验证码,和对称密钥及其算法结合使用;
- 生成伪随机数;
- 生成对称密钥。
还记得我们在之前,讨论过了怎么使用单向散列函数校验数据完整性。
```
输入:
1、数据D
2、原始数据的散列值H
3、计算散列值使用的散列函数
输出:
数据D是不是完整的
运算:
1、使用散列函数计算数据D的散列值H';
2、对比数据的散列值H和计算获得的散列值如果两个散列值相同则数据D是完整的否则数据D是修改过的数据。
```
如果我们单纯地使用单向散列函数校验数据完整性,是要对比数据的散列值的。既然是对比,也就意味着有两个散列值。这时候,我们需要考虑的主要问题就是:给定的散列值有没有被更改?
散列值的计算是公开的,给定一段数据,谁都可以计算它的散列值。如果数据可以被修改,而且给定的散列值也是修改后的数据的散列值,这个数据完整性校验是没有意义的。
所以,单纯使用单向散列函数去校验数据的完整性,我们需要确保给定的散列值是不能被修改的,这就是这个使用场景的限制。
其余的单向散列函数的使用场景,我们后面还会接着讨论。
## Take Away今日收获
今天,我们讨论了单向散列函数的长度延展攻击,以及使用单向散列函数需要注意的事项,还列举了典型的单向散列函数使用场景。
通过今天的讨论,我们要:
- 知道单向散列函数存在长度延展攻击;
- 了解避免长度延展攻击的办法;
- 尽量不要单纯使用单向散列函数来处理包含机密信息的数据。
另外,今天也是单向散列函数这一模块的最后一讲了。我们也来小结一下这一模块要注意的知识点,拉个清单。
在这一模块里,我们要掌握下面的基本概念和最佳实践:
1. **知道单向散列函数的三个特点:正向计算容易,逆向计算困难,散列值长度固定。**
1. **如果散列值不能被恶意修改,单向散列函数可以用来解决数据完整性问题。**
1. **知道有退役的算法、遗留的算法和现行的算法,并且不要使用退役的算法,尽快升级遗留的算法。**
1. **了解密码学算法常用的三个推荐系统,美国的 NIST**、**德国的 BSI和欧洲的 ECRYPT-CSA要养成定期查看推荐指标的习惯跟得上密码学的进展。**
1. **知道安全强度以及现在要使用128位的安全强度的密码学算法长期系统要考虑使用256位的密码学算法。**
1. **知道要尽量选用现行的、流行的算法。对于单向散列函数来说它们是SHA-256SHA-384和 SHA-512。**
1. **尽量不要单纯使用单向散列函数来处理包含机密信息的数据,如果不得已,要尽量避免长度延展攻击。**
## 思考题
好的,又到了留思考题的时间了。
今天的思考题是一个拓展题,你要自己去发现单向散列函数的更多适用场景。
我们一直强调,使用单向散列函数校验数据完整性,需要保证原始的散列值不能被更改。你能不能找到一些场景,可以让我们不用担心原始的散列值被更改,单纯使用单向散列函数就可以校验数据完整性?
除了我们上面列出来的一些场景,你能不能找出更多的单向散列函数使用场景?比如说,利用散列值长度固定的特点,利用碰撞困难的特点?
欢迎在留言区留言,记录、讨论你发现的新使用场景。
好的,今天就这样,我们下次再聊。