Python 手写 SHA-1 算法实现常见错误解析与正确填充方案

3次阅读

Python 手写 SHA-1 算法实现常见错误解析与正确填充方案

本文详解 python 中手动实现 SHA-1 时因消息填充逻辑错误(特别是长度计算偏差)导致哈希值与 hashlib.sha1 不一致的根本原因,并提供修正后的完整、可验证的实现代码。

本文详解 python 中手动实现 sha-1 时因消息填充逻辑错误(特别是长度计算偏差)导致哈希值与 `hashlib.sha1` 不一致的根本原因,并提供修正后的完整、可验证的实现代码。

SHA-1 是一种广泛应用的密码学哈希算法,其标准流程包含预处理(填充)、分块处理和主循环计算三大部分。许多开发者在复现该算法时,常能正确实现轮函数、逻辑运算与旋转操作,却在消息填充(padding)阶段栽跟头——尤其容易忽略“追加 0x80 字节后长度已发生变化”这一关键事实,从而导致后续填充字节数和长度字段计算全部偏移。

核心问题在于填充规则:SHA-1 要求将原始消息扩展为长度 ≡ 448 (mod 512) 比特(即 ≡ 56 (mod 64) 字节),步骤如下:

  1. 追加单字节 0x80;
  2. 追加若干 0x00 字节,使当前总长度(含 0x80)满足 len % 64 == 56;
  3. 追加 8 字节的大端序无符号整数,表示原始消息的比特长度(非字节长度!)。

原代码中,msg_len 在追加 0x80 前获取,但后续计算零填充数量时未将其纳入考量:

msg_len = len(data)        # ← 此时是原始长度(未含 0x80) data += b"x80" # ❌ 错误:用原始长度计算,忽略了刚添加的 1 字节 data += b"x00" * ((56 - msg_len % 64) % 64)

正确做法是:以追加 0x80 后的新长度为基准计算所需填充字节数。由于 0x80 占 1 字节,等价于在原始长度上加 1:

立即学习Python免费学习笔记(深入)”;

msg_len = len(data)           # 原始字节长度 data += b"x80" # ✅ 正确:(msg_len + 1) 是追加 0x80 后的当前长度 pad_len = (56 - (msg_len + 1) % 64) % 64 data += b"x00" * pad_len # ✅ 正确:追加原始消息的 BIT 长度(不是字节长度!) data += (msg_len * 8).to_bytes(8, "big")  # 注意:msg_len 是字节数,*8 得比特数

此外,还需注意以下关键细节:

  • 位长字段必须为 64 位大端整数:即使原始消息极短(如 b””),也必须补足 8 字节,高位补零;
  • 所有中间算术需 32 位截断:每步加法、移位后应 & 0xffffffff,防止 Python 整数溢出影响逻辑;
  • 轮函数中的 c = rotl32(b, 30) 是标准写法(非 rotl32(c, 30)),原文代码此处正确;
  • 最终拼接顺序须严格按 h0 h1 h2 h3 h4 大端排列:to_bytes(20, “big”) 正确。

以下是修正后的完整可运行实现(已通过 b””, b”a”, b”abc”, b”message digest” 等多组测试用例验证,与 hashlib.sha1 输出完全一致):

from hashlib import sha1 as builtin_sha1  def rotl32(value: int, count: int) -> int:     return ((value << count) | (value >> (32 - count))) & 0xffffffff  def sha1(data: bytes) -> bytes:     # 初始化哈希值(RFC 3174)     h0 = 0x67452301     h1 = 0xefcdab89     h2 = 0x98badcfe     h3 = 0x10325476     h4 = 0xc3d2e1f0      msg_len = len(data)  # 原始字节长度      # 步骤1:追加字节 0x80     data = data + b"x80"      # 步骤2:追加 0x00,使 (len + 1) % 64 == 56 → 即 len(data) % 64 == 56     pad_len = (56 - (msg_len + 1) % 64) % 64     data = data + b"x00" * pad_len      # 步骤3:追加原始消息的比特长度(64位大端)     bit_length = msg_len * 8     data = data + bit_length.to_bytes(8, "big")      # 现在 data 长度必为 64 字节整数倍     assert len(data) % 64 == 0      # 主循环:每 64 字节为一块     for i in range(0, len(data), 64):         # 将 64 字节拆分为 16 个 32 位大端字         words = [             int.from_bytes(data[i + j:i + j + 4], "big")             for j in range(0, 64, 4)         ]          # 扩展至 80 个字(SHA-1 的消息调度)         for j in range(16, 80):             w = rotl32(                 words[j - 3] ^ words[j - 8] ^ words[j - 14] ^ words[j - 16],                 1             )             words.append(w & 0xffffffff)          # 初始化本块的链变量         a, b, c, d, e = h0, h1, h2, h3, h4          # 80 轮主循环         for j in range(80):             if 0 <= j <= 19:                 f = (b & c) | ((~b) & d)                 k = 0x5a827999             elif 20 <= j <= 39:                 f = b ^ c ^ d                 k = 0x6ed9eba1             elif 40 <= j <= 59:                 f = (b & c) | (b & d) | (c & d)                 k = 0x8f1bbcdc             else:  # 60 <= j <= 79                 f = b ^ c ^ d                 k = 0xca62c1d6              temp = (rotl32(a, 5) + f + e + k + words[j]) & 0xffffffff             e = d             d = c             c = rotl32(b, 30)             b = a             a = temp          # 更新链变量         h0 = (h0 + a) & 0xffffffff         h1 = (h1 + b) & 0xffffffff         h2 = (h2 + c) & 0xffffffff         h3 = (h3 + d) & 0xffffffff         h4 = (h4 + e) & 0xffffffff      # 输出:拼接 5 个 32 位哈希值为 20 字节大端序列     return (         (h0 << 128) | (h1 << 96) | (h2 << 64) | (h3 << 32) | h4     ).to_bytes(20, "big")  # ✅ 验证:与标准库完全一致 if __name__ == "__main__":     test_cases = [b"", b"a", b"abc", b"message digest", b"Hello, World!"]     for msg in test_cases:         assert sha1(msg) == builtin_sha1(msg).digest(), f"Failed on {msg!r}"     print("✅ All tests passed.")

总结与建议

  • SHA-1 填充逻辑是手工实现中最易出错的环节,务必严格对照 FIPS 180-4 或 RFC 3174 文档;
  • 推荐在开发过程中加入边界测试(空输入、单字节、55/56/57 字节输入),观察填充后长度是否符合 ≡ 56 (mod 64);
  • 对比调试时,可打印中间 data 的十六进制(如 data.hex())与标准库生成的填充后消息比对,快速定位偏差位置;
  • 实际项目中请始终使用 hashlib 等经充分审计的标准库,手写仅用于学习与理解算法本质。

text=ZqhQzanResources