B - 哈希函数

哈希函数

我的博客

原书:《Understanding Cryptography: A Text book for Students and Practitioners》

哈希函数可以计算消息的摘要。消息的摘要(哈希值),可以视作消息的指纹。不像本书中介绍的其他的加密算法,哈希函数不需要密钥。哈希函数在计算数字签名与消息认证码中十分重要。哈希函数在其他密码学算法中也有十分广泛的应用。

11.1 动机:对长消息签名

在前面介绍的所有签名算法中,原文的长度都是有限制的,比如在 RSA 算法中消息长度不能长过模数,在实际应用中为 1024/3072 比特位长,既 128 字节 - 384 字节长度,大部分邮件的长度都会超过这个长度。在实际应用中,原文的长度通常都会超过这个长度。那么就有了问题:我们如何高效计算长消息的签名?一个直观的方式是将消息 \(x\) 按块划分,签名每一个块。但是这会又如下问题:

问题1:高计算代价

数字签名的计算是基于非对称算法的,涉及大数值的指数模运算。即便是单个操作都需要相当长的时间,而如果是像上面这样的对长数据的签名,无疑需要相当大的计算量。而且,除了签名者需要进行这些签名操作,校验一方也需要为了校验签名消耗大量的计算资源。

问题2:消息负载增加

显然,上面提到的这种方式,消息负载除了消息,还需要发送这些消息的签名,而上面提到的方案中,签名又与消息的长度一致,那么 1MB 的文件长度将需要发送 2MB 的数据。

问题3:安全限制

像上面提到的这个方案,攻击者 O 可以移除消息与其对应的签名,或者打乱消息-签名的顺序等。

基于上面的种种考量,我们需要对任意长度的消息做一个短签名。解决这个问题的方法就是哈希函数,如果我们具有一个哈希函数,可以计算消息的指纹,我们可以对所有的消息做哈希,计算得到一整条消息的指纹,之后只需要对这个指纹进行签名即可。

假设我们具有这样的哈希函数,B 计算整个消息 \(x\) 的哈希值 \(z\),并使用私钥 \(k_{pr,B}\) 计算 \(z\) 的签名 \(s\)。在接收侧,A 计算接收到消息的哈希值 \(z'\),使用 B 的公钥 \(k_{pub,B}\) 校验签名 \(s\)。在签名侧与校验侧的操作都是针对消息的哈希值 \(z\),而不是消息本身。哈希值通常称作消息摘要或消息指纹。

在我们讨论哈希函数的安全属性之前,我们可以先简单思索一下哈希函数的输入输出行为:我们希望能够对任意长度的消息 \(x\) 做哈希处理;我们希望这个哈希函数 \(h\) 必须很高效,即便我们对上百兆的数据做哈希操作,都可以快速计算;哈希函数的输出是一个固定长度的数值,实际应用中的哈希函数的输出长度在 128-512 比特位;最后,我们期望计算的消息摘要需要对所有的输入比特敏感,这表示,即便是对输入 \(x\) 的一丁点修改,得到的消息摘要都会又显著不同,这个行为与块运算的特性类似。

下面是一个具象化的表示:

消息1:Alice was beginning to get very tired of sitting by her sister on the bank, and having nothing to do.

摘要1:DFDC349A

消息2:I am not a crook.

摘要2:FB93E283

消息3:I am not a cook.

摘要3:A3F4439B

11.2 哈希函数的安全性需求

前面介绍过哈希函数没有密钥。哈希函数的安全性需要考虑下面几个方面:

  1. 预像阻力(preimage resistance),或单向性,既给定哈希值,很难找到对应的原始数据
  2. 第二预像阻力(second preimage resistance),或弱碰撞阻力,给定一个输入数据,很难找到另一个输入数据,二者哈希值相同
  3. 碰撞阻力,或强碰撞阻力,很难找到两个不同的输入数据,二者的哈希值相同

11.2.1 预像阻力或单向性

哈希函数需要具有单向性,给定哈希值 \(z\) 必须很难计算得到输入消息 \(x\)。我们下面展示为什么预像阻力是十分重要的。B 加密了消息但没有加密签名,既他发送了下面的消息对:

\[(e_k(x),sig_{k_{pr,B}}(z)) \]

在这里 \(e_k()\) 是一个对称运算,比如 AES。假设 B 使用 RSA 数字签名,签名计算为:

\[s = sig_{k_{pr,B}}(z) \equiv z^d \ mod \ n \]

攻击者 O 可以使用 B 的公钥做计算:

\[s^e \equiv z \ mod \ n \]

如果哈希函数不具有单向性,那么 O 可以通过 \(h^{-1}(z) = x\) 计算得到消息 \(x\)。这样就暴露了原文。

11.2.2 第二预像阻力或弱碰撞阻力

两个不同的消息不会哈希得到相同的值,这意味着,\(z_1 = h(x_1) = h(x_2) = z_2\) 的情况是不可能的。我们可以将这样的碰撞分成两个类型。第一种情况,\(x_1\) 已经给定,我们尝试找到 \(x_2\),称作第二预像阻力或弱碰撞阻力;第二种情况是攻击者可以自由选择 \(x_1\)\(x_2\),称作强碰撞阻力,下一小节会介绍。

假设 B 哈希消息 \(x_1\) 对其签名,如果攻击者 O 能够找到第二条消息 \(x_2\),满足 \(h(x_1) = h(x_2)\),那么他可以使用 \(x_2\) 做消息替换。那么 A 将会收到 \((x_2,s)\) 消息对,并校验为真。

因此我们需要避免弱碰撞的发生,但是基于抽屉原理,这是不可能的。如果有 100 个小球,希望放入 99 个抽屉中,那么至少有一个抽屉需要放入 2 个小球。因为每一个哈希函数的输出都是固定位长,\(n\) 比特,只有 \(2^n\) 种输出值,而输入到哈希函数的数量则是无限的,因而必然有多个输入会计算得到相同的输出值。

既然弱碰撞在理论中必然存在,我们只需要在实际中尽量避免就好了,从而使给定 \(x_1\)\(h(x_1)\),不能构造出 \(x_2\) 使得 \(h(x_1) = h(x_2)\)。不过攻击者依然能够随便选择 \(x_2\) 计算它的哈希值,进行匹配,这类似于穷举法。为防止这样的攻击,对于现今的计算机计算能力,哈希函数的输出位长必须要在 80 位长以上。

11.2.3 碰撞阻力与生日攻击

如果能够找到两个不同的输入 \(x_1 \neq x_2\)\(h(x_1) = h(x_2)\)。这相较于弱碰撞更难以实现。下面展示攻击者 O 如何将这样的碰撞应用在攻击中,他以两条消息开始,比如:

消息 \(x_1\):Transfer $10 into Oscar's account

消息 \(x_2\):Transfer $10000 into Oscar's account

他可以对消息进行修饰,比如填充 tab 添加空格或在消息的末尾添加回车键等。这样,消息所表示的意义没有改变,但是哈希值却可以做出很大的改变。O 执行修饰操作,直到得到 \(h(x_1) = h(x_2)\),如果消息具有 64 个位置可以修改,可以得到相同意义消息的 \(2^{64}\) 版本的不同哈希值,在得到相同哈希的结果后,他就有能力进行替换攻击了。

我们前面提到过抽屉原理,这样的碰撞总是存在的。问题是找到这样的碰撞是否困难。如果哈希函数的输出 80 位长,则必须检查 \(2^{80}\) 条消息。不过实际情况是攻击者只需要进行 \(2^{40}\) 次检验。这是基于生日悖论。

生日悖论 在一个派对中要有多少人才会有相同生日的人存在?一年按照 365 天计算,我们的直觉至少需要约 183 人(一年天数的一半)。然而实际情况却大跌眼镜。我们先计算两个人不在同一天生日的概率:

\[P(两个人生日不同) = (1 - 1/365) \]

如果第三个人加入,那么他们三个都不在同一天生日的概率则是:

\[P(三个人生日不同) = (1 - 1/365)\cdot(1 - 2/365) \]

这样,就有 \(t\) 个人生日不同的概率为:

\[P(t个人生日不同) = (1 - 1/365)\cdot(1 - 2/365)...(1 - (t-1)/365) \]

如果选择 \(t = 366\),那么必然有生日相同的人。现在我们回到问题,如果有 50% 的概率有两个人生日相同,那么排队仅需要有 23 个人。如果排队人数提升到 40 人,那么有生日相同的人的概率将会提升到 90%,这就是经典的生日悖论。

对哈希函数的碰撞搜索也有生日悖论类似的问题存在。对于哈希函数,天数可以换为 \(2^n\) 元素,\(n\) 是哈希函数的输出位长。实际上,\(n\) 是哈希函数安全性的关键。问题变为,需要多少的消息 \((x_1,x_2,...,x_t)\) O 需要进行哈希直到他能够得到 \(h(x_i) = h(x_j)\)\(t\) 个消息没有发生碰撞的可能是:

\[P(无碰撞) = (1 - 1/2^n)(1 - 2/2^n)...(1 - (t-1)/2^n) \\ = \prod_{i = 1}^{t - 1}(1 - i/2^n) \]

使用微积分知识,有如下约数:

\[e^{-x} \approx 1 - x,i/2^n << 1 \]

我们可以计算约数:

\[P(无碰撞) \approx \prod_{i = 1}^{t - 1}e^{-i/2^n} \\ \approx e^{-(1+2+3+...+t-1)/2^n} \]

数列计算有:

\[1 + 2 + ... + t-1 = t(t-1)/2 \]

带入有:

\[P(无碰撞) \approx e^{-t(t-1)/2\cdot2^n} \]

如果我们计算有碰撞发生的概率 \(\lambda = 1 - P(无碰撞)\),那么有:

\[\lambda \approx 1 - e^{-t(t-1)/2^{n+1}} \\ ln(1 - \lambda) \approx -t(t-1)/2^{n+1} \\ t(t-1) \approx 2^{n+1}ln(1/1-\lambda) \]

因为 \(t >> 1\),有 \(t^2 \approx t(t-1)\)

\[t \approx \sqrt{(2^{n+1}ln(1/1-\lambda))} \\ t \approx 2^{(n+1)/2} \sqrt{(ln(1/1-\lambda))} \]

假设我们希望计算 80 位长 50% 概率发生碰撞,有:

\[t \approx 2^{81/2}\sqrt{ln(1/(1-0.5))} \approx 2^{40.2} \]

不同哈希输出位长下,两个概率碰撞需要的消息条数:

\(\lambda\) 128 160 256 384 512
0.5 \(2^{65}\) \(2^{81}\) \(2^{129}\) \(2^{193}\) \(2^{257}\)
0.9 \(2^{67}\) \(2^{82}\) \(2^{130}\) \(2^{194}\) \(2^{258}\)

哈希函数特性:

  1. 任意长度的输入消息
  2. 固定长度的输出哈希
  3. 可以高效计算
  4. 预像阻力
  5. 第二预像阻力
  6. 碰撞阻力

11.3 哈希算法总览

到此为止,我们只介绍了哈希函数的需求,现在我们介绍如何构造哈希函数。有两种类型的哈希函数:

  1. 专用哈希函数 这样的算法专门构造用来做哈希运算
  2. 基于块运算的哈希函数 可以使用块运算如 AES 构造哈希函数

前面我们提到哈希函数可以处理任意长度的输入消息,并生成固定长度的输出。实际中,可以通过将输入分割成一系列等长的块实现。这些块可以使用哈希函数顺序处理,哈希函数的核心是压缩函数。这样的迭代设计称作 Merkle-Damgard 构造。最后一次迭代压缩运算的输出就是最终的哈希值。

11.3.1 专用哈希函数:MD4 家族

专用哈希函数是专门设计的算法。在过去的二十年,出现了很多这样的算法。实际上,最流行的是 MD4 家族,MD5、SHA 家族以及 RIPEMD 都是基于 MD4。MD4 是由 Ronald Rivest 开发的消息摘要算法,它可以使用软件高效实现。它使用 32 位的变量,所有的操作都是布尔函数,既逻辑与或非异或。MD4 家族的所有哈希函数都有类似的软件友好特性。

MD4 的强化版本是 MD5,由 Rivest 于 1991 年提出。这两个哈希函数都能够计算 128 比特位的输出,它们的碰撞阻力约为 \(2^{64}\),MD5 应用十分广泛,在网络安全协议,计算文件的校验和或存储密码的哈希值,但是它具有一定的缺陷。美国 NIST 于 1993 年发布了新的消息摘要标准 SHA: Secure Hash Algorithm,这是 SHA 家族的第一个成员,现在都将它称作 SHA-0,在 1995 年基于 SHA-0 调整为 SHA-1。SHA-0 与 SHA-1 算法使用压缩函数来提升它的安全性,这两个算法都会输出 160 比特位输出。

排除分析攻击,SHA-0 与 SHA-1 的最大碰撞阻力是 \(2^{80}\),并不能完美契合诸如 AES 这样的比特位长为 128 - 256 比特位的算法。相似的,大部分公钥策略可以提供更高的安全等级,比如在椭圆曲线中的 256 比特位的曲线的安全等级是 128 位。因此在 2001 年 NIST 引入了 SHA-1 的三个变体:SHA-256、SHA-384、SHA-512,它们的消息摘要分别是 256、384 与 512 比特位。2004 年引入了 SHA-224 以契合 3DES,这四个哈希函数通常被称作 SHA-2。

在 2004 年,王小云提出了针对 MD5 与 SHA-0 的碰撞发现攻击。一年之后,攻击扩展到了 SHA-1,碰撞搜索为 \(2^{63}\) 步,相较于基于生日攻击的 \(2^{80}\) 步,显著减少。

算法 输出位 输入位 攻击轮 碰撞搜索
MD5 128 512 64
SHA-1 160 512 80 尚未
SHA-224 224 512 64
SHA-256 256 512 64
SHA-384 384 1024 80
SHA-512 512 1024 80

需要注意的是,能够搜索到碰撞并不意味着算法在任何场景下都是不安全的。有一些对哈希函数的应用,密钥派生或密码存储只需要预像阻力与第二预像阻力要高,在这样的应用场景中,MD5 依然是充分安全的。

11.3.2 块运算哈希函数

哈希函数也可以使用块运算链构造实现。像是哈希函数 SHA-1,我们将消息 \(x\) 分割成固定长度的 \(x_i\),消息块 \(x_i\) 使用块长度 \(b\) 的块运算 \(e\) 加密,输入 \(m\) 位的密钥给到块运算,我们对前一个输出 \(H_{i-1}\) 通过映射 \(g\) 对其映射,这是一个 \(b\) 位转 \(m\) 位的映射。在加密完消息块 \(x_i\) 后,我们将结果与最初的消息块做异或,最后一个输出就是整个消息的哈希值 \(H_n = h(x)\)。函数可以表示为:

\[H_i = e_{g(H_{i-1})}(x_i) \oplus x_i \]

这个构造以它的构造这名命,称作 Matyas-Meyer-Oseas 哈希函数。当然存在其他的块运算构造哈希函数,比如:

\[H_i = H_{i-1} \oplus e_{x_i}(H_{i-1}) \\ H_i = H_{i-1} \oplus x_i \oplus e_{g(H_{i-1})}(x_i) \]

上面的三种哈希函数需要初始值 \(H_0\),这可以是公用值,全零向量。所有策略的输出比特位长都是块运算的宽度。在只需要预像阻力与第二预像阻力的情境下,可以使用 AES 这样的 128 位块运算。对于需要碰撞阻力的应用,128 位长的块运算是不够的,生日攻击可以将其安全性降低到 64 位以下。

另一个获取长消息摘要的方式是将块运算并联,构造得到双倍块运算宽度。

11.4 SHA-1

SHA-1 对最长 \(2^{64}\) 位的消息生成 160 位的输出。进行哈希计算之前,算法需要预处理消息。在进行真正的计算时,压缩函数以 512 位的块对消息进行处理。压缩函数一共 80 轮,以每阶段 20 轮分成 4 个阶段。

11.4.1 预处理

在进行真正的哈希运算之前,消息 \(x\) 需要先进行填充成为 512 位的倍数,填充后的消息之后被分割成块,初始值 \(H_0\) 设置为预先定义的常量。

填充

假设我们的消息长度为 l 比特,为了获得总长为 512 字节的消息,我们填充 1 并在之后跟 k 个 0,以及 64 位 l 的二进制。那么需要填充的 0 的个数为:

\[k \equiv 512 - 64 - 1 - l = 448 - (l+1) \ mod \ 512 \]

例 11.1 给定消息 "abc",由 ASCII 码字符组成,总长度为 24 比特:

a:01100001,b:01100010,c:01100011

那么填充后为:

01100001 01100010 01100011 1 0(423) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00011000

分割填充的消息

在使用压缩函数之前,我们需要将消息分割成为 512 位一组 \(x_1,x_2,...,x_n\),每一个 512 位的块又可以分割成 16 个 32 位组成的字。

初始化 \(H_0\)

一个 160 位的 buffer 用来存储初始化哈希值,用作第一次迭代。五个 32 位的字是固定的,并可以用十六进制表示为:

\(A = H_0^{(0)} = 67452301\)

\(B = H_0^{(0)} = EFCDAB89\)

\(C = H_0^{(0)} = 98BADCFE\)

\(D = H_0^{(0)} = 10325476\)

\(E = H_0^{(0)} = C3D2E1F0\)

11.4.2 计算哈希

略.

11.4.3 实现

SHA-1 的设计方便了软件实现。每一轮都只需要 32 位寄存器的布尔运算。在现代 64 位处理器上可以实现 1 Gbit/s 的吞吐量。SHA-1 以及其他的 MD4 家族的算法的一个缺点是难以并行运算。而在硬件上,基于 FPGA 可以实现几 Gbit/s,并不比软件实现高太多。

posted @ 2023-04-10 21:59  ArvinDu  阅读(32)  评论(0编辑  收藏  举报