
本文详解 Python 中手动实现 SHA-1 时因消息填充逻辑错误(特别是长度计算偏差)导致哈希值与 hashlib.sha1 不一致的根本原因,并提供修正后的完整、可验证的实现代码。
本文详解 python 中手动实现 sha-1 时因消息填充逻辑错误(特别是长度计算偏差)导致哈希值与 `hashlib.sha1` 不一致的根本原因,并提供修正后的完整、可验证的实现代码。
SHA-1 是一种广泛应用的密码学哈希算法,其标准流程包含预处理(填充)、分块处理和主循环计算三大部分。许多开发者在复现该算法时,常能正确实现轮函数、逻辑运算与旋转操作,却在消息填充(padding)阶段栽跟头——尤其容易忽略“追加 0x80 字节后长度已发生变化”这一关键事实,从而导致后续填充字节数和长度字段计算全部偏移。
核心问题在于填充规则:SHA-1 要求将原始消息扩展为长度 ≡ 448 (mod 512) 比特(即 ≡ 56 (mod 64) 字节),步骤如下:
- 追加单字节 0x80;
- 追加若干 0x00 字节,使当前总长度(含 0x80)满足 len % 64 == 56;
- 追加 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 等经充分审计的标准库,手写仅用于学习与理解算法本质。










