SHA-1 作为经典的密码学哈希算法,其规范中定义的消息填充规则看似简单,但在 Python 中手动实现时却容易因为比特与字节的混淆、长度编码方式错误而得到错误摘要。理解填充的本质,是写出正确 SHA-1 函数的前提。

SHA-1 填充的底层规则
SHA-1 处理的数据单位是 512 比特的分组。在哈希计算前,原始消息必须先填充,使得总长度满足特定条件。填充过程分为三步:首先在消息末尾添加一个比特 1;接着补入若干个比特 0,直到消息总长度(含填充)对 512 取模等于 448;最后在末尾附加一个 64 比特的无符号整数,表示原始消息的比特长度。
这里最容易被忽略的是,所有长度计算都以比特为单位,而不是字节。例如一个 3 字节的消息,原始比特长度是 24,填充时要先加 1 比特变成 25 比特,再补 0 到 448 比特模 512,也就是补 423 个 0 比特,最后加 64 比特长度字段,总共 512 比特刚好一个分组。如果只在字节层面思考,往往会少算那个单独的 1 比特,或者把 0 字节数量算错。
常见错误写法与问题定位
下面这段代码是初学者常写的填充逻辑,它直接在字节串后补零字节,并错误地把字节长度写入末尾:
def bad_padding(msg: bytes):
# 错误示例:以字节为单位乱补
ml = len(msg)
msg += b'x80' # 只加了 0x80 一个字节,相当于比特1加后面七个0
while len(msg) % 64 != 56: # 64字节=512比特,56字节=448比特
msg += b'x00'
# 错误:把字节长度当比特长度,且用小端打包
msg += ml.to_bytes(8, 'little')
return msg
print(bad_padding(b'abc'))
这段代码的第一个问题是,b'x80' 虽然表示一个字节,但在比特视角下它是 10000000,已经包含了要求的那个 1 比特和七个 0 比特,这本身可行;但循环条件以 64 字节为模、目标 56 字节,看似对应 448 比特,却没有考虑如果原始消息长度模 64 已经大于 56,应当再补一个分组。第二个严重错误是末尾用 to_bytes(8, 'little') 写入了字节长度且为小端,而 SHA-1 要求写入的是原始比特长度,并且必须是大端序。
当用上面错误函数处理 b'abc' 后送入后续分组处理,得到的哈希会和标准 SHA-1 对不上。很多调试者会怀疑主循环里的逻辑运算写错,其实根因就在填充。这种错误在长消息和短消息下表现不一致,短消息可能偶然正确,长消息必然错误,非常隐蔽。
正确的填充方案实现
正确的做法应当显式以比特长度计算,并使用 struct 模块以大端方式写入 64 位比特长度。以下函数可稳妥处理任意长度输入:
import struct
def correct_padding(msg: bytes):
# 原始比特长度
bit_len = len(msg) * 8
# 先加一个字节 0x80,即比特1后跟7个0
padded = msg + b'x80'
# 当前字节长度,目标为模64余56
while len(padded) % 64 != 56:
padded += b'x00'
# 附加64位大端比特长度
padded += struct.pack('>Q', bit_len)
return padded
# 测试
data = b'abc'
p = correct_padding(data)
print(len(p)) # 应为64字节
print(p.hex())
在这个实现里,bit_len 明确使用比特数,避免了字节与比特的错位。struct.pack('>Q', bit_len) 中的 >Q 表示大端无符号长整型,正好对应 SHA-1 规范里的长度字段。循环补零保证分组前长度模 512 比特为 448,也就是模 64 字节为 56,逻辑清晰且对所有长度安全。
需要注意,如果原始消息字节长度模 64 已经大于或等于 56,例如是 60 字节,那么加完 0x80 后变成 61,循环会继续补到 56+64=120 字节,也就是跨了一个额外分组,这符合标准。手动实现时千万不要为了简化而截断填充,否则长消息必然算错。
填充后如何接入主循环
完成填充的函数输出必然是 64 字节的整数倍,可以直接按每 64 字节切片,转成 16 个 32 位大端整数作为分组送入 SHA-1 压缩函数。下面给出衔接示例:
def to_blocks(padded: bytes):
blocks = []
for i in range(0, len(padded), 64):
chunk = padded[i:i+64]
words = list(struct.unpack('>16I', chunk))
blocks.append(words)
return blocks
blocks = to_blocks(correct_padding(b'abc'))
print(len(blocks), len(blocks[0]))
通过 struct.unpack('>16I', chunk) 把每个分组变成 16 个 unsigned int,就满足了 SHA-1 对消息调度的输入要求。此后只需实现标准的 80 步逻辑函数和五常量初始值,就能完整跑通手写 SHA-1。
从工程角度看,手写哈希算法主要价值在于理解协议细节而非生产使用。但填充作为第一道关口,一旦写错,后面无论运算多严谨都得不到正确结果。把上述填充函数作为独立模块测试,用已知向量如 b'abc' 验证输出长度与十六进制,是排查问题的最快路径。
小结与避坑清单
手写 SHA-1 填充时请牢记以下要点:长度字段必须是原始消息的比特数而非字节数;长度字段用大端 64 位表示;填充字节序列以 0x80 起头,之后全零;当现有长度模 64 余 56 不满足时要跨分组补零。只要填充正确,后续主循环调试会轻松许多。
建议把填充函数单独抽出来,配合标准库的 hashlib.sha1 做对照测试。若两者在多种长度输入下填充后分组数一致,再继续实现压缩逻辑,可显著降低整体调试成本。