单向散列函数:如何保证信息完整性
什么是单向散列函数?
在数学上,单向函数和散列函数是两个不同类型的函数。所以,我们要想理解单向散列函数,我们就要先知道什么是单向函数,什么又是散列函数。
什么是单向函数?
单向函数是正向计算容易,逆向运算困难的函数。也就是说,给定你一个输入,你很容易计算出输出;但是给定你一个输出,你却很难计算出输入是什么。
《应用密码学》有一个很生动的例子来解释单向函数。把盘子打碎是一件很简单的事情,但是把这些碎片再拼接成一个完整的盘子,就是一件非常困难的事情。
也许,你会想,虽然把盘子碎片再拼接起来非常困难,但是仅仅就是非常困难而已,无论是手工还是计算机辅助,碎盘子还是可以拼接起来的。是的,这就是这个例子巧妙的地方。
单向函数就是这样的一个盘子。虽然我们强调,单向函数只能正向计算,不能逆向运算。但其实,这只是一个美好的愿望。
因为,我们能找到的、谈到的所谓的单向函数,都是正向计算容易,逆运算困难的函数。是的,我用的词语是“困难”,而不是“不能”,可能性只是很小,但不是没有。
为什么要强调“逆向运算困难”这件事?因为密码理论领域里很多棘手的问题,密码应用领域里的很多错误,都是来源于单向函数的这种不确定性。
比方说吧,每一个被破解的单向散列函数的密码学算法,在它被发明的时候,人们都没有找到逆向运算的办法,可是被破解的时候,人们就发现原来还是有办法去逆向运算的。
今天还是安全的算法,明天就可能被破解。
需要注意的是,我们要对这种不确定性保持足够的警惕,采取足够的防范措施。比如说,一个应用程序,至少要支持两种单向函数,当一种出现问题时,另外一种可以替补。
现在你知道了,单向函数是一个正向计算容易,逆向运算困难的函数。
那什么样的单向函数会更实用呢?
- 正向计算会更容易,容易程度就是这个函数的计算性能;
- 逆向运算会更困难,困难程度就是这个函数的破解强度
一个实用的单向函数,计算强度和破解强度要均衡考量,不可偏废
什么是散列函数?
散列函数是一个可以把任意大小的数据,转换成固定长度的数据的函数。比如说,无论输入数据是一个字节,或者一万个字节,输出数据都是 16 个字节。
我们把转换后的数据,叫做散列值。因为散列函数经常被人们直译为哈希函数,所以我们也可以称散列值为哈希值。
通常的,对于给定的输入数据和散列函数,散列值是确定不变的。可问题也来了,既然输入数据的大小没有限制,而输出结果的数据长度固定,那么你觉得,会不会存在散列值相同的两个或者多个数据呢?——是确定存在的。
通常,我们把这种情况称为散列值碰撞。对于散列函数,散列值碰撞可不是一件好事情。
那么,我们应该怎样避免散列值碰撞呢?其实,因为输入数据的大小没有限制,输出数据的长度固定,理论上,我们是无法避免散列值碰撞的。
我们只能在降低散列值碰撞的可能性上想办法。
最直观的办法,就是在输出数据的长度上想办法。虽然散列值长度固定,但是,我们可以让散列值长度变得更长,散列值越长,存在相同散列值的概率就越小,发生碰撞的可能性就越小。比如说,32 位固定长度的散列值就要比 16 位固定长度的散列值发生碰撞的可能性更小。是不是觉得我们可以解决问题了?问题是解决了,但从另一个角度来说,散列值越长,通常也就意味着计算越困难,计算性能越差。而且,你想一想,为什么当初我们要使用固定长度的散列值?不就是为了减少计算本身的性能损耗,从而获得性能优化吗?其实,散列值的长度选择,应该是权衡性能后的结果。
我们还要考虑散列值的质量。一个好的散列函数,它的散列值应该是均匀分布的。也就是说,每一个散列值出现的概率都是一样的。如果不这样的话,一部分散列值出现的概率就会较高,另一部分散列值出现的概率会较低,别人就更容易构造出两个或者多个数据,使得它们具有相同的散列值。这种行为,叫做碰撞攻击
什么是单向散列函数?
单向散列函数既是一个单向函数,也是一个散列函数。它不仅要满足单向函数的要求,还要满足散列函数的要求。
你还记得这两种函数的要求吗?其中,最要紧的就是:
-
逆向运算困难
-
构造碰撞困难
用现成的单向散列函数举例,比如 SHA-1 算法,它是一个常见的适用于密码学的单向散列函数。
分别加密 “Hello, world!” 和 “Hello, vorld!”,这两句话只有一位的差异,对比两个散列值,感受一位的数据差异,计算出的散列值能有多大的差异。
SHA-1("Hello, world!):
10010100 00111010 01110000 00101101 00000110 11110011 01000101 10011001 10101110 11100001 11111000 11011010 10001110 11111001 11110111 00101001 01100000 00110001 11010110 10011001
SHA-1("Hello, vorld!):
11001011 11111111 11111011 10010011 01010111 11000010 10001101 01011000 00100010 11000100 01010110 10000110 00101010 00110011 01010000 10111110 10000010 01111111 00100000 10101010
是不是差异还挺大的?这种现象,我们把它叫做雪崩效应。
雪崩效应是密码学算法一个常见的特点,指的是输入数据的微小变换,就会导致输出数据的巨大变化。严格雪崩效应是雪崩效应的一个形式化指标,我们也常用来衡量均匀分布。严格雪崩效应指的是,如果输入数据的一位反转,输出数据的每一位都有 50% 的概率会发生变化。
一个适用于密码学的单向散列函数,就要具有雪崩效应的特点,也就是说,如果一个单向散列函数具有雪崩效应,那么对于给定的数据,构造出一个新的、具有相同散列值的数据是困难的。
密码学的单向散列函数是用来解决数据完整性问题的。那么,单向散列函数是怎么解决数据完整性问题的呢?
怎么解决完整性问题?
想要解决完整性问题,我们就要知道完整性问题的背后逻辑是什么。
完整性意味着什么?如果数据有变动,能够被检测出来,我们就不采纳被篡改的数据。
刚才说了,在单向散列函数里,一段数据,无论它是少了一个字,多了一个字,或者修改了一个字,原始数据和修改后的数据的散列值都可能相差巨大。
而且,由于逆向运算困难,虽然存在具有相同散列值的两个或者多个数据,但是对于一个好的单向散列函数来说,刻意寻找这样的数据是困难的。如果困难程度足够大,我们就有足够信心认为,如果散列值没有变化,它对应的输入数据也没有变化。
所以,单向散列函数,就可以帮助我们解决完整性问题。
假如我们收到了一段数据,我们就可以重新计算这段数据的散列值。如果我们还可以获得数据发送者计算的散列值,我们就可以对比新计算的散列值和接收到的散列值。如果两个散列值是相同的,我们就可以认为这段数据是完整的;否则,这段数据就是被篡改过的。
可是,这里面依然有两个遗留问题,也是我们使用单向散列函数需要特别关注的两个问题。
第一个问题是,我们该选择什么样的散列函数,它的破解难度才能足够大?这样,我们才有足够的信心根据散列值判断数据的完整性。
第二个问题是,我们怎么能够安全地获得数据发送者计算的散列值?如果我们接收到的是被修改过的数据和修改过的散列值,我们是没有办法判断数据是不是完整的。