SHA-1 哈希算法的常量与初始化向量设计
我将详细讲解 SHA-1 哈希算法的常量和初始化向量(IV)的设计。这是一个基础但至关重要的主题,因为它构成了 SHA-1 算法进行安全散列的初始状态。
题目描述
SHA-1 是一个产生 160 位(20 字节)哈希值的密码散列函数。在开始处理任何输入消息之前,算法需要一组初始的内部状态(即初始化向量 IV)和一组在整个计算过程中用到的常量。理解这些常数是如何被设计、选择以及使用的,是理解 SHA-1 算法工作原理和安全基础的关键第一步。
解题过程(知识讲解)
第一步:SHA-1 算法结构回顾
SHA-1 是一种基于 Merkle-Damgård 结构的迭代散列函数。它包含两个主要阶段:
- 预处理:对输入消息进行填充,使其长度满足特定条件,然后分割成若干个 512 位的消息块。
- 压缩处理:一个 160 位的内部状态(由 5 个 32 位寄存器 A, B, C, D, E 组成)会与每一个 512 位的消息块进行运算,不断更新,直到所有消息块处理完毕。最终,这 5 个寄存器的连接值就是输出的哈希值。
而我们的核心问题就是:在第一个消息块被处理之前,这 5 个寄存器的初始值是什么?以及在压缩过程中用到哪些固定值?
第二步:初始化向量(Initial Hash Values, H0)
在算法开始时,必须设置初始的哈希值。这 5 个 32 位的初始值(H0 到 H4)构成了算法的初始内部状态。
具体的十六进制值如下:
- H0 = 0x67452301
- H1 = 0xEFCDAB89
- H2 = 0x98BADCFE
- H3 = 0x10325476
- H4 = 0xC3D2E1F0
为什么是这些看起来“随机”的数字?
- 非零性:它们都不是零,这确保了初始状态是有“信息”的,避免了平凡起点。
- 高汉明重量:它们的二进制表示中 0 和 1 的分布看起来比较均匀(没有明显的规律),这有助于在计算开始时就将非线性特性引入。
- 与 MD 家族一脉相承:SHA-1 的前身是 SHA-0,而 SHA-0 的 IV 与 MD4/MD5 的设计思路相似。这些值通常是“没有什么特别含义”的常数,是设计者选定的。你可以把它们理解为“魔数”。它们的选择标准是为了让算法在没有任何输入时,就从一个无偏的、复杂的起点开始,从而有助于抵抗某些特定的密码学攻击(如固定点攻击)。
如何记忆? 观察 H0 到 H3,你会发现它们像是 0x01234567 到 0x76543210 的一种特定交错和倒置,而 H4 则是另一个互补的序列。这更像是一种精心设计的、具有对称美感的模式,而非真正的随机。
第三步:SHA-1 的常量 Kt
在 SHA-1 的压缩函数中,每一轮(共 80 步)都会使用一个 32 位的常量 Kt。这 80 步被划分为 4 个阶段,每个阶段 20 步,并使用一个不同的常量。
四个阶段及其常量如下:
| 轮次 (t) | 常量 Kt (十六进制) | 常量 Kt (十进制) | 逻辑函数阶段 |
|---|---|---|---|
| 0 ≤ t ≤ 19 | 0x5A827999 | 1,518,225,049 | (B ∧ C) ∨ (¬B ∧ D) |
| 20 ≤ t ≤ 39 | 0x6ED9EBA1 | 1,859,720,609 | B ⊕ C ⊕ D |
| 40 ≤ t ≤ 59 | 0x8F1BBCDC | 2,409,948,476 | (B ∧ C) ∨ (B ∧ D) ∨ (C ∧ D) |
| 60 ≤ t ≤ 79 | 0xCA62C1D6 | 3,399,837,398 | B ⊕ C ⊕ D |
这些常量从何而来?
这些常量的选择同样不是随机的,它们有明确的数学来源,目的是提供额外的非线性并打破可能存在的规律性。
设计依据:
-
平方根函数:这些常数实际上是某些整数平方根的二进制小数部分的前 32 位。
- K0 = 0x5A827999 = floor(√2 * 2^30) 的十六进制表示。(√2 ≈ 1.414213562…)
- K1 = 0x6ED9EBA1 = floor(√3 * 2^30) 的十六进制表示。(√3 ≈ 1.732050807…)
- K2 = 0x8F1BBCDC = floor(√5 * 2^30) 的十六进制表示。(√5 ≈ 2.236067977…)
- K3 = 0xCA62C1D6 = floor(√10 * 2^30) 的十六进制表示。(√10 ≈ 3.162277660…)
-
为什么选择平方根?
- 无偏性:已知数学常数(如√2, √3)的二进制展开被认为是无偏的,看起来像随机位流,没有明显的周期性或相关性。
- 可验证性:使用公开的数学常数可以证明设计者没有在算法中隐藏“后门”。任何人都可以独立计算并验证这些值。
- 标准化:它们是与输入消息完全无关的固定值,为压缩函数提供了确定性的、非线性的“佐料”。
第四步:常量与初始向量在算法中的使用流程
让我们串联起它们在 SHA-1 算法中的完整生命周期:
-
初始化:
- 将五个 32 位变量
a, b, c, d, e分别设置为初始向量 H0, H1, H2, H3, H4。 - 准备好常量数组 K[0..79],根据当前步数 t 选择对应的 Kt。
- 将五个 32 位变量
-
处理每个512位消息块:
- 对于当前块,进行 80 步运算。
- 在第 t 步(t 从 0 到 79)中,会执行一个核心公式来更新
a, b, c, d, e:
TEMP = ROTL^5(a) + f_t(b,c,d) + e + K_t + W_t
然后e = d; d = c; c = ROTL^30(b); b = a; a = TEMP; - 在这里,
K_t就是对应的轮常数,它与扩展后的消息字W_t以及当前的寄存器值相加,共同驱动哈希状态的变化。
-
状态更新与最终输出:
- 一个消息块处理完后,将计算得到的
a, b, c, d, e与处理前的 H0..H4 分别模 2^32 相加,结果作为下一个消息块的初始 H0..H4。 - 所有消息块处理完毕后,最终的 H0 || H1 || H2 || H3 || H4 就是 160 位的 SHA-1 哈希值。
- 一个消息块处理完后,将计算得到的
总结
- 初始化向量 (H0-H4):它们是算法的“初始种子”,是设计者选定的一组具有良好比特扩散特性的固定“魔数”,决定了哈希计算的起点。
- 轮常数 (Kt):它们来源于简单无理数(如√2, √3)的二进制表示的一部分,为算法80轮运算的每一轮提供了公开、可验证、无偏的非线性源。
正是这些精心设计的固定值与输入消息的动态扩展部分相互作用,使得 SHA-1 能够对任意长度的输入产生一个看似随机的、固定长度的“指纹”。虽然 SHA-1 如今因密码学攻击已被认为不安全,不再推荐用于需要抗碰撞的场景,但其核心设计中常量和初始化向量的选择思路,仍然是理解经典哈希函数设计的重要范例。