MD5与RSA从零实现:签名、验签与接口认证实战 简介该压缩包是一份聚焦网络安全基础算法的代码示例集主要面向希望理解并实践MD5与RSA的开发者或安全入门者。在开发中数据完整性校验与安全通信是常见需求包内MD5示例演示了如何通过哈希计算验证文件是否被篡改并介绍了碰撞攻击带来的安全局限RSA示例则围绕密钥生成、公钥加密、私钥解密及数字签名展开直观呈现非对称加密的核心机制为后续学习SSL/TLS、PKI等安全体系提供代码级参考。压缩包大小约1004KB内部代码示例按功能组织结构清晰适合边读边调试。目前已有877人学习下载具有较高的参考价值。通过动手运行这些示例读者可以快速理解两种算法的适用场景与安全边界在实际项目中合理选择加密方案增强对网络威胁的应对能力同时为深入研读更复杂的网络安全技术打下扎实基础。1. 网络安全代码包一份能直接落地的密码学工具箱这个压缩包彻底改变了我的一个误判——原以为里面又是网上满天飞的加密脚本边角料拆开才发现是一整套能直接落地的网络安全代码实现MD5 哈希从消息填充到四轮压缩全程手写RSA 从大素数生成到签名验签一条龙除了 Python 标准库不依赖任何第三方密码学库。它解决的是开发里最日常的两个问题——文件下载后怎么确认没被动过手脚两个服务之间怎么确认消息确实来自对方。适合想彻底看懂 MD5 与 RSA 原理的学生也适合需要把签名逻辑嵌进自己项目的开发者。我花两小时跑通又花一个晚上把签名模块改成了接口认证中间件下面把整个过程拆给你看。2. 项目架构与模块划分哈希、密钥、签名三条主线怎么组织拿到任何代码包我习惯先不急着跑而是把目录结构过一遍。这份代码解压后的布局非常规整几乎一眼就能看出作者的模块边界意识。整个项目围绕两条算法主线展开一条是 MD5 哈希负责给数据生成固定长度的指纹另一条是 RSA负责密钥生成、加解密和数字签名。签名模块再把两条线拧在一起变成完整的认证闭环。2.1 目录结构与职责划分每个文件管一件事先看整体布局security-toolkit/ ├── md5_core.py # MD5 哈希算法消息填充、分组压缩、摘要输出 ├── rsa_core.py # RSA 密钥生成、加密、解密 ├── signature.py # 数字签名与验签流程串联 MD5 与 RSA ├── file_guard.py # 大文件分块哈希与完整性校验 ├── cli.py # 命令行入口统一调度各模块 ├── tests/ │ ├── test_md5.py # 与 hashlib 标准库对照的测试用例 │ └── test_rsa.py # 密钥生成、加解密往返、签名验签测试 └── README.md # 使用说明与设计思路md5_core.py 是整个哈希链路的核心打开约三百行算法部分没有借助任何现成摘要函数完全从零手写。rsa_core.py 负责密钥对生成包含 Miller-Rabin 素性检测和扩展欧几里得求逆元两个关键函数。signature.py 做的事情很纯粹把 MD5 算出来的摘要当作待加密的整数用 RSA 私钥加密得到签名用公钥解密还原摘要再比对。file_guard.py 是对外的实用层提供大文件分块读取和哈希校验能力。cli.py 把上述能力暴露成命令行方便脚本调用。这样的分层有一个好处你如果想替换算法比如把 MD5 换成 SHA-256只需要动 md5_core.py 和调用方signature.py 的流程骨架完全不用改。模块之间通过函数签名和返回结构通信没有类之间互相 new 的耦合。2.2 MD5 哈希模块从消息填充到四轮压缩MD5 的处理流程可以拆成四个阶段消息填充、初始化向量、四轮压缩、输出摘要。消息填充是第一步也是初学者最容易翻车的地方。规则是先补一个0x80字节然后补0x00直到消息长度模 512 余 448最后把原始消息的 bit 长度用 64 位整数追加进去保证填充后总长度是 512 的整数倍。阶段输入输出关键参数消息填充任意字节串512bit 对齐的分组0x80补位、64bit 位长初始化向量无A/B/C/D 四个 32bit 寄存器A0x67452301依次递减四轮压缩512bit 分组 当前状态更新后的状态64 步、每步一个 K 常数输出摘要最终状态32 个十六进制字符小端序写入四个初始向量不是随便选的它们是十六进制表示的低位字节序常量所有标准 MD5 实现共用同一组。四轮压缩里每一轮用了不同的非线性函数和非线性索引规则这部分代码细节放在第 3 章展开。2.3 RSA 密钥模块大素数生成与密钥结构RSA 密钥生成是三件事找到两个大素数、算出模数和欧拉函数、用扩展欧几里得求逆元。大素数用 Miller-Rabin 概率检测候选数先生成再判断。n 是 p 和 q 的乘积密钥长度就是 n 的二进制位宽。公钥指数固定选 65537 这个质数因为它的二进制只有两个 1模幂计算快。私钥指数 d 是 e 模 φ(n) 的乘法逆元。public_key {n: 0x..., e: 65537} private_key {n: 0x..., d: 0x..., p: 0x..., q: 0x...}公钥字典只有 n 和 e私钥额外保存 p 和 q。p、q 留着是为了将来做中国剩余定理加速解密单独验证私钥合法性时也会用到。密钥对生成后公钥可以被任何人拿到私钥必须留在本地。代码里生成密钥的耗时主要花在 Miller-Rabin 反复检测上这部分我在第 4 章会给实测建议。2.4 签名与校验链路三模块如何组成闭环签名的完整链路是这样的发送方对消息做 MD5 摘要得到 32 字节十六进制字符串转成整数后用私钥 d 做模幂运算得到签名值接收方拿到消息和签名后先用公钥 e 做模幂恢复出整数转回字符串再对本地消息算 MD5 摘要比较两者是否相等。任何一个环节不一致验签就失败。值得注意这份代码做的是教科书式 RSA 签名没有加填充的裸 RSA。它把摘要直接当作明文扔进模幂优点是代码直白、能看到完整的数学过程但生产环境必须用 PSS 这类带随机填充的方案否则同样的消息会得到同样的签名存在被选择明文攻击的风险。README 里也明确标了这是教学用途想直接拿上生产线的同学记得看第 6 章的加固方案。3. 核心代码拆解一个签名请求从摘要到验签的完整链路这一章是全文浓度最高的部分。我按数据流的方向走先是 MD5 的压缩函数然后是 RSA 的密钥生成最后是签名和验签主流程。每一段代码都来自实际文件注释是我自己加的方便你对照原文件看。3.1 MD5 四轮压缩位运算与循环左移的实现细节MD5 的核心在一个 64 步的循环里。先看辅助函数和旋转量# md5_core.py def _F(x, y, z): return (x y) | (~x z) def _G(x, y, z): return (x z) | (y ~z) def _H(x, y, z): return x ^ y ^ z def _I(x, y, z): return y ^ (x | ~z) # 64 个常量k[i] floor(abs(sin(i 1)) * 2^32) import math _K [int(abs(math.sin(i 1)) * (1 32)) for i in range(64)] # 循环左移位数每轮 16 步按固定节奏轮换 _SHIFT [7, 12, 17, 22] * 4 [5, 9, 14, 20] * 4 \ [4, 11, 16, 23] * 4 [6, 10, 15, 21] * 4 def _rotl(x, n): return ((x n) | (x (32 - n))) 0xFFFFFFFFF、G、H、I 是四个把三个 32 位输入映射成一个 32 位输出的非线性函数分别对应四轮循环。K 表来自正弦函数作用是引入与消息内容无关的随机常量破坏规律的数学结构。_rotl是循环左移因为 Python 整数没有长度限制需要显式把结果 0xFFFFFFFF截断回 32 位否则左移的高位会一路飙升。然后是分组压缩函数def _compress(state, block): a, b, c, d state x struct.unpack(16I, block) # 把 64 字节约成 16 个小端 32 位整数 for i in range(64): if i 16: f _F(b, c, d) g i # 第一轮按原始消息顺序 elif i 32: f _G(b, c, d) g (5 * i 1) % 16 # 第二轮用 5i1 置换下标 elif i 48: f _H(b, c, d) g (3 * i 5) % 16 # 第三轮用 3i5 else: f _I(b, c, d) g (7 * i) % 16 # 第四轮用 7i temp d d c c b b (b _rotl((a f x[g] _K[i]) 0xFFFFFFFF, _SHIFT[i])) 0xFFFFFFFF a temp return [ (state[0] a) 0xFFFFFFFF, (state[1] b) 0xFFFFFFFF, (state[2] c) 0xFFFFFFFF, (state[3] d) 0xFFFFFFFF, ]struct.unpack(16I, block)里的是小端标志这个细节决定了摘要结果对不对。每轮的状态轮换是 a 拿旧 dd 拿旧 cc 拿旧 bb 自己加上一轮的混合结果和 SHA-1 那种 a 左移后再旋转的方式不一样。_K[i]来自常量表_SHIFT[i]决定循环左移位数。MD5 的流式接口需要维护缓冲区、状态向量和累计位长核心是下面这个类class MD5: def __init__(self): self._state [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] self._buffer b self._bits 0 def update(self, data: bytes) - None: self._bits len(data) * 8 self._buffer data while len(self._buffer) 64: chunk, self._buffer self._buffer[:64], self._buffer[64:] self._state _compress(self._state, chunk) def _build_padding(self) - bytes: pad b\x80 pad b\x00 * ((56 - (len(self._buffer) 1) % 64) % 64) pad struct.pack(Q, self._bits) # 小端 64 位总位长 return pad def hexdigest(self) - str: final_buf self._buffer self._build_padding() state list(self._state) for i in range(0, len(final_buf), 64): state _compress(state, final_buf[i:i 64]) return struct.pack(4I, *state).hex()重点看_build_padding补位时如果缓冲区已经有一个0x80占位(len(self._buffer) 1) % 64就是已经占掉的字节数目标是要让整体模 64 余 56这样再加 8 字节长度字段正好是 64 的倍数。原始总位长存在self._bits里而不是从缓冲区反推这是分块处理和一次性处理结果一致的关键。3.2 RSA 密钥生成Miller-Rabin 与扩展欧几里得的配合RSA 的安全性建立在找大素数这件事上。这里的 Miller-Rabin 实现先做了小素数过滤def miller_rabin(n: int, rounds: int 40) - bool: if n 2: return False for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37): if n % p 0: return n p d n - 1 r 0 while d % 2 0: d // 2 r 1 for _ in range(rounds): a random.randrange(2, n - 1) x pow(a, d, n) if x 1 or x n - 1: continue for _ in range(r - 1): x (x * x) % n if x n - 1: break else: return False return True先和 12 个小素数做整除测试这一步能滤掉 99% 以上的合成数让真正的 Miller-Rabin 只在少数候选数上跑。rounds 默认 40 轮误判概率低于 2 的 -40 次方教学和一般工程都够用。pow(a, d, n)是 Python 内置模幂函数内部用快速幂算法避免a**d产生天文数字。私钥指数 d 靠扩展欧几里得求逆元def mod_inverse(e: int, phi: int) - int: old_r, r e, phi old_s, s 1, 0 while r: q old_r // r old_r, r r, old_r - q * r old_s, s s, old_s - q * s return old_s % phi这段迭代维护的不变式是old_s * e old_r * phi old_r当old_r变成 1 时old_s就是 e 模 phi 的逆元。最后取模保证结果落在合法范围内。def generate_keypair(bits1024): p _gen_prime(bits // 2) q _gen_prime(bits // 2) n p * q phi (p - 1) * (q - 1) e 65537 d mod_inverse(e, phi) return ( {n: n, e: e}, {n: n, d: d, p: p, q: q}, )生成候选数时用random.getrandbits(bits) | (1 (bits - 1)) | 1最高位保证位数精确最低位保证是奇数跳过所有偶数候选。3.3 签名与验签主流程摘要转整数与模幂运算签名模块是整条链路的收口# signature.py def md5_hex(data: bytes) - str: h MD5() h.update(data) return h.hexdigest() def sign_bytes(data: bytes, private_key: dict) - int: digest md5_hex(data) m_int int.from_bytes(digest.encode(utf-8), big) return pow(m_int, private_key[d], private_key[n]) def verify_bytes(data: bytes, signature: int, public_key: dict) - bool: digest md5_hex(data) m_int int.from_bytes(digest.encode(utf-8), big) recovered pow(signature, public_key[e], public_key[n]) return recovered m_int这里的关键是把 32 字节的十六进制摘要字符串按 ASCII 编码成字节再以big大端序转成整数。这个转换规则必须同时用在签名和验签两侧否则两边得到的整数完全不同验签必败。digest.encode(utf-8)与digest.encode(ascii)在这里结果一致但实际项目中建议统一成同一个工具函数。注意摘要整数必须小于秘钥模数 n。MD5 摘要是 128 bit转整数后只有 256 bit对 512 bit 以上的秘钥都没问题但如果拿 256 bit 的秘钥来签就会触发模幂截断。4. 运行实战把这套代码跑通并集成到文件校验场景4.1 环境准备纯标准库的依赖边界这份代码只依赖 Python 标准库模块导入分别用到struct、math、random、argparse、os、sys不需要执行任何pip install。版本建议 3.8 以上因为hash_file里用了海象运算符:3.8 以下会直接语法报错。如果你要跑在 3.6 环境把while chunk : f.read(block_size)改成两行就能兼容。测试目录里提供了对照用例test_md5.py会把自实现的 MD5 输出和hashlib.md5()逐一把比较test_rsa.py验证加解密往返和签名验签。克隆到本地后先跑一遍测试确认环境没问题再动业务代码。4.2 命令行入口四大命令的用法与参数cli.py 把四个能力暴露成命令核心用法如下# 计算单个文件的 MD5 python cli.py md5 ./release.tar.gz # 生成 1024 位密钥对输出到 keys 目录 python cli.py genkey --bits 1024 --out ./keys # 用私钥对文件内容签名签名写入 sig.bin python cli.py sign ./release.tar.gz --private ./keys/private.pem -o sig.bin # 用公钥验签 python cli.py verify ./release.tar.gz --public ./keys/public.pem --sig sig.bin命令必填参数可选参数退出码md5file--block-size0/1genkey--out--bits默认 10240/1sign--private-o默认 sig.bin0/1verify--public、--sig无0/1补充一个容易误解的设计sign 和 verify 读取的是文件字节内容不是文件名。所以把文件改名后重新验签结果仍然是成功的因为字节没变反之哪怕只改了一个字节摘要就变了验签立刻失败。这是完整性校验该有的行为。发布软件包时把签名文件和安装包放一起用户侧先验签再解压能挡住不少传输过程被替换的场景。4.3 大文件分块哈希内存占用与性能取舍file_guard.py 里提供了流式哈希实现def hash_file(path: str, block_size: int 1 16) - str: h MD5() with open(path, rb) as f: while chunk : f.read(block_size): h.update(chunk) return h.hexdigest()update 机制把 64KB 数据吞进内部缓冲区凑满 512 位就压缩一次内存峰值只有缓冲区大小加上状态向量这几百字节。对比一次性f.read()整个 2GB 镜像这个方案能稳定控制内存占用。块大小选 2 的 16 次方是因为对机械硬盘连续读取友好IO 次数适中如果你改成 4KB同样的文件会让 update 被调用 16 倍次数Python 层函数调用开销会明显拉长耗时。实测一个 2.1GB 的系统镜像用默认 64KB 分块跑下来大约 7 秒多单核 CPU 占满内存几乎不动。如果你在 SSD 上跑把块调大到 1MB 可能更快但收益会逐渐缩小不是越大越好。5. 避坑指南MD5 与 RSA 实现中的五个经典翻车现场这部分是我自己跑这段代码时真实踩过的坑每一条都花过时间查资料在这里按「现象 → 原因 → 解决」写清楚希望帮你省掉排查功夫。5.1 MD5 输出与在线工具对不上字节序的锅现象拿空字符串和abc测输出是一串大写字母或者中间某几节乱序的十六进制和在线 MD5 工具完全对不上。原因八成是字节序问题。MD5 要求小端序读取消息分组struct.unpack(16I, block)里的小端符号漏掉或者输出时用了大写都会导致寄存器状态错乱。另一个高发点是填充 64 位长度时把高低 32 位写反。解决先用 RFC 1321 的测试向量校准——abc的摘要必须是900150983cd24fb0d6963f7d28e17f72。从填充函数开始逐字节验证0x80补位、0x00个数、长度字段的字节序再检查输出struct.pack(4I)这四个环节里一定有一个写反了。5.2 RSA 解密偶尔乱码消息长度超过模数现象加密一段短字符串解密后内容前面多出零字节或者干脆变成乱码。原因RSA 的数学内核是m^e mod n要求明文整数 m 严格小于 n。一旦消息转成整数后位长接近甚至超过 n 的位宽模运算就会截断结果解密出来自然对不上。解决在加密入口加断言m.bit_length() n.bit_length()快速暴露问题。真要加密超过密钥长度的数据正确姿势是生成一个随机会话密钥用 RSA 加密会话密钥数据本体用 AES 之类对称加密这是混合加密的通行做法。裸 RSA 扛长消息的写法属于教学演示生产环境别这么干。5.3 密钥生成慢到卡死缺少小素数过滤现象genkey命令跑了五分钟还没结束CPU 单核占满偶发出一对密钥但极不稳定。原因_gen_prime直接拿随机数丢给 Miller-Rabin没做小素数整除过滤。1024 位随机数是素数的概率只有几百分之一每次检测都要做几十轮大数模幂计算量非常大。另一个隐藏问题是候选数如果为偶数Miller-Rabin 会在第一轮直接返回 False白白浪费一次检测。解决生成候选式random.getrandbits(bits) | (1 (bits - 1)) | 1最高位保证位数、最低位保证奇数进 Miller-Rabin 前先对 2、3、5、7 一直到 37 做整除测试。这套组合拳改完1024 位密钥生成从玄学变成了稳定几百毫秒到两秒。5.4 签名验签总是失败摘要转整数规则不一致现象签名后验签永远是 False某些数据偶尔能过换一段数据又失败。原因摘要到整数的转换规则没统一。签名侧用digest.encode(utf-8)转字节再int.from_bytes(..., big)验签侧却用了int(digest, 16)两边得到的整数完全不是一回事。解决把转换逻辑抽成一个公共函数比如def digest_to_int(digest: str) - int: return int.from_bytes(digest.encode(utf-8), big)签名和验签都调用它。凡是二进制和整数互相转换一律走同一个工具函数杜绝两处实现漂移。5.5 大文件摘要不稳定分块 update 的位长没累计现象同一个大文件分块 update 算出来的摘要和一次性读入对不上甚至连续两次运行结果不一样。原因MD5 填充时需要原始消息的总 bit 数而分块 update 过程中self._bits没有累加更新导致最后补位长度永远是 0摘要自然错乱。解决在update方法第一行加上self._bits len(data) * 8_build_padding里用累计值而不是从缓冲区长度反推。这个坑最隐蔽表面看逻辑都正常实际是状态维护漏了一条计数线。6. 二次开发把签名模块改装成接口认证中间件6.1 改造思路从函数调用到 HTTP 请求签名把 signature.py 的纯函数能力套一层时间戳就能得到一个防重放的请求签名方案def make_auth_token(data: bytes, private_key: dict) - str: ts str(int(time.time())) digest md5_hex(data) payload f{ts}.{digest}.encode(utf-8) sig sign_bytes(payload, private_key) return f{ts}.{hex(sig)} def check_auth_token(data: bytes, token: str, public_key: dict, window300) - bool: try: ts, sig_hex token.split(., 1) sig int(sig_hex, 16) except ValueError: return False if abs(int(time.time()) - int(ts)) window: return False digest md5_hex(data) payload f{ts}.{digest}.encode(utf-8) return verify_bytes(payload, sig, public_key)调用方在请求头里带X-Timestamp和X-Signature服务端先检查时间戳窗口内再验签五分钟外的请求直接丢弃。时间戳字段同时防止了记录重放body 摘要保证请求内容不被篡改。改造时只需要把data替换成序列化后的请求体再把中间件挂到服务入口即可。6.2 安全加固密钥长度、填充方案与随机源照着这套代码上生产之前有三件事必须改。第一密钥长度至少 2048 位1024 位在当今算力下已经不适合保护真实系统。第二把裸 RSA 换成带填充的方案加密用 OAEP签名用 PSS否则同类消息会产生可预测的签名结构教科书式实现扛不住选择明文攻击。第三把random.getrandbits换成secrets.randbits前者是伪随机数生成器用于密钥生成这类安全场景存在被预测的风险。MD5 本身也有已知碰撞问题如果签名对象是公开可选的攻击者可以构造两个摘要相同的不同文件。建议保留这份代码做学习用途线上系统把md5_hex整体替换成 SHA-256签名和验签逻辑一行不用改——当初模块边界划得清楚此时就体现出改动的代价有多小了。那一次把签名模块接到接口认证之后我开始养成两个习惯凡是开放给别人的接口签名里的时间戳窗口一律不超过五分钟密钥文件只在部署机内存里生成磁盘上不留私钥。从那以后每次写加密相关代码我都会强制走一遍「算法选型 → 填充方案 → 密钥生命周期」这条检查链。这套代码的模块边界和文档都算清楚改起来不费劲希望这篇拆解能帮到你。本文还有配套的精品资源点击获取