在分布式系统中,哈希算法是我们会进场听到的一个名词,比如我们在分布式数据库中,数据库的分片会使用哈希算法;在Redis的分片集群中,我们会使用哈希算法;甚至在安全领域,哈希算法的身影可以说到处都是。哈希(Hash)也称为散列,把任意长度的输入,通过散列算法,变换成固定长度的输出,这个输出值就是散列值,或者也叫哈希值。
哈希算法具备良好的确定性,也就是相同的输入总会产出相同的输出,虽然这看起来好像很稀松平常,但是如果你逆向思维一下,不同的输入基本上(回头我们在来说为啥是基本上)会产出不同的输出,我们就可以通过这个确定性来检测数据是否被恶意攻击者修改过。哈希算法的另外一个特性就是不可逆性,我们无法通过散列值来反推出原始数据,虽然说基于我们选在的散列值空间,一个哈希值可以有多个数据输入,但是我们无法知道具体是哪一个输入产生了哈希值。
由于哈希算法在整个安全领域的重要作用,以及哈希算法在分布式系统中的重要性,笔者接下来会通过两篇文章来详细介绍哈希算法,哈希算法的安全属性具体是什么,以及我们有哪些可选的哈希算法。首先我们从什么是哈希算法开始。
RocketMQ是阿里巴巴开源的分布式消息中间件,基于高可用分布式集群技术,提供低延时,高可靠的消息发布和订阅服务。读者如果在本地测试过和Rocket集成的代码,需要做的第一件事情就是下载最新版本的安装包,为了省事,很多同学应该会选择二进制安装包。当我们从http://rocketmq.apache.org/dowloading/releases/ 下载最新版本的时候,如下图所示:
我们可以点击对应的版本来现在二级制安装文件,但是大家应该注意上图高亮的部分有个【SHA512】的下载链接,当我们点击后,会下载挥着打开另外一个文件,如下所示:
4.9.1-rc1/rocketmq-all-4.9.1-bin-release.zip:
4E9BBF63 94B71993 45D24924 9F02F30A 1A3D7FBA EB01C267 24392850 AAF9B632 B05326DF
89F483D1 383496A1 4F187827 8A934D20 BFB79BFF 765A5F33 97582033
如果大家对这串数字没有任何感觉,或者从来都没有关注过,也可以在自己的机器上进行如下的操作:首先从apache的网站上下载对应版本rocketmq的二进制包,然后使用本地的openssl工具来对这个压缩包进行散列计算,然后可以对比一下散列值和网站上提供的散列值。不出意料的是,这两个值应该一致,这说明我们下载的安装包是完整的,未被篡改的,同时意味着是安全的。在笔者的机器上运行openssl dgst -sha512的输出如下:
➜ Downloads openssl dgst -sha512 rocketmq-all-4.9.1-bin-release.zip
SHA512(rocketmq-all-4.9.1-bin-release.zip)= 4e9bbf6394b7199345d249249f02f30a1a3d7fbaeb01c26724392850aaf9b632b05326df89f483d1383496a14f1878278a934d20bfb79bff765a5f3397582033
大家可以对比一下我们在本地计算出来的散列值和apache网站提供的散列值,这两个值是完全一样的。通过散列值,我们就可以验证从apache网站上下载的安装文件的完整性。
注:openssl输出的一串字符笔者有时候会称之为摘要(digest),有时候也会成为哈希值,这两个词在这篇文章中可以互换。读者应该也听说过散列(checksum)或者sum,虽然说在哈希算法的这个场景下,这四个词代表的相同的意思,但是笔者会在安全相关的文章中优先使用摘要和哈希值这两个术语。
我们可以使用opessl客户端命令行工具来计算下载文件或者任意信息的散列值,我们只需要把它们作为输入参数提供给OpenSSL,OpenSSL客户端工具会执行必要的步骤来计算摘要,并输出到Console。不知道读者是否考虑过,openssl背后具体做了哪些工作来确保我们下载文件的真实性和完整性?
哈希算法提供的这种真实性和完整性其实来自于哈希算法提供的叫“second pre-image resistance"安全特性。说人话就是我们很难在找到另外一个数据,产生哈希值。从具体的实践意义上看,rocketmq v4.9.1这个版本就和这个sha512散列值深度绑定在一起,恶意攻击者是无法通过篡改安装包,比如加入了特洛伊木马程序,来骗过用户,因为如果安装包被篡改过,通过openssl计算出来的哈希值肯定不一样,用户就能发现这个问题,进而丢弃安装包。
注:从second pre-image resistance这个名字你应该能猜测到应该还有一个叫pre-image resistance的特性。大白话来说,抗原像性指的是,我们无法通过哈希值,即便是知道哈希算法的情况下,也无法反推出原始值;而抗次原像性指的是,给定计算出的哈希值,我们很难找到另外一个消息,可以产生相同的哈希值。反过来看,抗原像性保证了消息的完整性被防篡改性。
注:笔者需要强调的是openssl输出的摘要字符串,本质上是二进制数据的base16编码表示,base16使用0-9和a-f的字符来标识二进制数据,当然我们也可以通过0和1来直接展示摘要信息,但是需要的存储空间会更大。而base16会将原始数据的1byte(8bits)编码为两个字符,减少了存储需要的空间之外,同时也提升了可读性(人)。对于二进制如何数据如何展示的问题,base64和base16是用的最多的两种编码,当然64位编码要比16位节省更多空间。
读到这里,眼尖手快的同学可能马上就跳起来了,你说的不对,通过这种方式下载的rocketmq还是可能被人替换成注入木马的版本。如果不太理解为什么,读者可以停下来思考一下!的确单凭摘要没有办法提供完整性,因为恶意攻击者完全有可能攻破apache的网站,然后用自己的修改过的安装文件替换apache提供的”真品“,同时顺便也把摘要信息也更新了。笔者要为有这种警觉的同学点个赞,这的确是个问题。
数据完整性没有办法通过摘要(哈希)来单独解决,我们必须首先信任apache的网站,确保这个网站提供的信息是我们信任的,另外我们必须信任整个传输信道,数据在传输的过程中不会被偷梁换柱。因此我们相信这个安装包的是一个组安全措施共同努力的结果,首先apache网站有证书,并且是浏览器信任的证书办法机构颁布的证书,其次我们信任apache这个组织,他们有足够的授权来杜绝安装包被注马;最后,我们信任数据传输信道,因为使用了https,即便是man in the middle的攻击者可以截获数据,但是因为是https加密的信道,看到的数据都是密文,别说替换,要破解成明文都几乎不可能。
在了解了哈希算法是什么之后,接下来我们来看看哈希算法的具体工作原理,如下图所示,我们可以把哈希算法想象成一个黑色的盒子,信息从一边输入,然后经过哈希计算之后,从另外一边输出哈希值:
如上图所示,输入的信息可以是任意长度的数据,也可以为空,而输出总是相同长度的数据,具有确定性:如果输入的数据相同,输出的数据一定相同。我们通常说哈希算法具备one-way的特性,其实就是我们无法通过输出的哈希值来反推出具体的输入数据,具备不可逆性。为了让读者对哈希算法有更加直观的体感,接下来我们使用OpenSSL的客户端命令行工具来验证一下上边描述的内容是否如实。
哈希算法在应用密码学中主要提供了三个安全特性,虽然说这个领域还在不断的发展,但是这三个特性是我们理解哈希算法是否符合安全要求,业务场景要求的前提和根基。
笔者在前文的注解部分一节介绍过其中的两个,这里我们来重新详细的解释一下哈希算法的安全属性。第一个安全特性叫pre-image resistence,大白话说就是没有人能够通过哈希值来返回出原始数据,也叫不可逆性。我们来更加形象的描述一下这个过程,日产生活中大家都喝过果汁,笔者就特别细化喝西柚和例子混合而成的果汁。而哈希算法的这种不可逆性就如同我们将多种类型的水果通过榨汁机进行了混合,我们无法通过混合果汁来分辨出到底有几个梨,多少西柚,如下图所示:
注:虽然说哈希算法具备不可逆性,但是聪明的读者可能会问,如果输入数据的空间非常小呢?我们来举个例子,笔者的小女儿已经4岁多了,现在说话用词已经非常丰富了。但是回想去1岁左右的时候,对外表达可能就几个字,比如说小朋友就只有5个字,那么我们其实不难破解哈希值的原始输入,因为5个字的空间,顶多试120次就能找到原始的输入。
因此我们必须注意pre-image resistence特性的局限性:我们无法通过加密或者散列来影藏容易被猜测的数据。哈希算法的第二个特性叫second pre-image resistance,大白话的意思是即便恶意攻击者持有哈希值和加密算法,无法找到另外一个输入通过相同的加密算法产生相同的哈希值。如下图所示:
接下来我们来看看第三个安全特性:collistion resistance(抗碰撞性)。抗碰撞性翻译成大白话后的意思是,哈希算法可以保证找不到两个不同的输入输入产生相同的哈希值,如下图所示:
很多初次接触哈希算法的同学对抗次原像性和抗碰撞性无法区分,认为是一回事,大家可以停下来仔细考虑一下,是否是一回事?这里的关键点是抗次原像性有一个输入是固定的,而抗碰撞两个输入都不固定,这是两种不同的安全场景。
好了,到这里为止,我们介绍完了哈希算法的三个安全特性,我们来总结一下这三个安全特性:
1,pre-image resistence,又叫抗原像性
2,second pre-image resistance,又叫抗次原像性
3,collision resistance,又叫抗碰撞性
如果把这三个安全特征单独来看,其实意义不大,大部分场景都需要组合这三个特性,或者反过来说,取决于我们如何在实际的业务场景使用哈希函数提供的这三个特性。在介绍具体的使用场景之前,我们先来看看哈希函数的局限性,俗话说知此知彼,才能用好啊。
哈希函数提供的安全特性有一个很重要的假设:合理使用。什么叫合理使用?我们举个例子,假设我们在网站上公开了一段哈希摘要,而这个摘要是对yes或者no这两个单词的哈希值。这种场景下,哈希值没有任何安全性可言,因为我们可以通过对yes和no进行散列,对比,很容易就找到输入信息。读者可能会说,这样不是就违反了抗原像性,笔者认为并不是哈希函数没有遵守抗原像性特性,而是使用不合理,输入数据的随机性不够。
说到这里聪明的读者可能会想到这个问题:既然哈希函数接受任意长度的输入值,那么对于特定长度的数据(比如说5个字符的数据输入),经过有限次的暴力尝试,我们还会通过哈希值找到原始的输入数据,这是不是又违反了抗次原像性特性呢?其实并不是,抗次原像性说的是很难找到这个输入数据,并不是不可能,只是从理论上说是可能的。要找到这个输入需要大量的尝试,需要平衡投入的资源和时间和产出的关系,如果你要花费10年来找到这个输入,可能找到的时候,也没有啥意义了。
其次,哈希函数生成哈希值的长度对于安全性来说也非常重要。当然这个问题并不只针对哈希函数,所有安全相关的加密算法都应该关注输出信息的长度。我们来聚个极端的例子,比如我们有个哈希函数funcyunpan(),接受任意长度的数据后产出两位(2 bit)的哈希值,那么产出的四个哈希值分别为:01,10,00,11,每个都是25%的几率。这个哈希算法就不具备放碰撞性,因为我们很容易就能找到两个输入产生相同的哈希值。
正是由于如上的原因,哈希函数为了保证安全性,输出的哈希值有最小长度限制。行业的最佳经验是最少为256bit(或者32bytes),这个长度就很难通过碰撞来找到两个不同的输入产生相同的哈希值,只存在通过大规模计算机来暴力破解的理论可行性。
那么这个行业最佳实践256位到底是怎么来的,不会是拍脑袋出来的吧!当然不是,真实的加密场景中,加密算法一般最少要求使用128bit的加密算法,这就意味着黑客需要尝试2^128次方来找到明文数据。对于哈希算法来说,为了满足三个安全特性,必须让样本空间足够大,因此也需要最少使用128bit的哈希值长度来抵御这三种特性可能受到的安全攻击。而这三个特性中,放碰撞性有个经典的攻击理论:birthday bound。
birthday bound这个理论来自于概率统计学,说的是在一个房间里,有多少人会导致两个人的生日相同的几率为50%,科学家们反复测试后,发现这个数字是23,也就是随便选择23个人,他们中有两个人的生日有50%的概率相同,这也就意味着产生了碰撞成功。而对于哈希算法带来的实践意义是,如果我们随机的在2^N这个空间内生成字符串,因此可能在2^N/2次后产生碰撞,因此如果要保障哈希的的三个安全特性,哈希值的长度应该最少是256bit。
如果我们的哈希函数生成256位的哈希值,那就意味着恶意攻击者尝试2^128次后,可能会产生碰撞,也就是找到了两个不同的输入产生相同的输出。而128位是加密的最小要求,因此我们的哈希值必须最少是256位,因为有birthday bound这个理论的存在。
当然理想很丰满,现在很骨感啊,在安全领域也是如此,开发人员总是有很多理由来裁剪摘要的尺寸(比如删除某些位),来减少长度,节省空间。虽然从实际落地角度可能合理,但是会造成安全风险,大家一定要记住下边两条准则:
1,256bit的哈希值程度可以提供抗碰撞性
2,128bit的哈希值能提供抗原像性和抗次原像性
这也就意味着,我们必须基于实际的业务场景和安全诉求,来平衡存储空间和安全性,千万不要在安全性要求比较高,比如需要抗碰撞性的时候节省存储空间,这种抓了芝麻漏了西瓜的做法,对于很多架构师来说,具有现实意义,大家务必消息。
好了,今天这篇文章就这么多了,下篇我们继续来讨论哈希函数(算法)具体如何使用,敬请期待!