恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

RSA数字签名实战:从原理到Python代码实现与验签排查

  • 首页
  • 资讯中心
  • /
  • RSA数字签名实战:从原理到Python代码实现与验签排查

相关资讯

Java HttpServletRequest Body读一次就没了?原理与稳定提取方案 2026/10/18 0:56:03
低代码之王是谁?五大技术流派深度对比与选型指南 2026/10/18 0:56:03
加密性能优化实战:从AES到RSA的选型与压测排错指南 2026/10/18 0:56:03

最新资讯

DSP芯片与OpenClaw:端侧AI智能体硬件底座的持续推理实践
ComfyUI-LTXVideo 完整攻略:LTX-2 视频生成从 0 到出片一次到位
操作系统进程调度模拟:五种算法实现与对比详解
Oracle 11g 升级 19C 实战:RMAN 副本恢复与 DBUA 静默升级避坑指南
YOLO26算法工业车间环氧树脂目标检测+训练好的模型+7525张数据集+pyqt可视化界面
国网DLT698-45协议主站调试工具(升级版)安装配置与排错指南

今日推荐

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

RSA数字签名实战:从原理到Python代码实现与验签排查

发布时间:2026/10/18 0:56:03
RSA数字签名实战:从原理到Python代码实现与验签排查 1. 从一次接口联调说起为什么RSA签名值得单独写一篇前段时间帮一个做支付网关的朋友排查线上问题对方系统用RSA做请求签名结果联调阶段一直报验签失败。抓包看数据签名值长度对不上再一看代码对方把私钥签名和公钥验签的参数顺序搞反了还顺手把Base64编码做了两次。这类问题在RSA数字签名的落地过程中太常见了几乎每个第一次接触非对称加密签名的开发者都会踩一遍。RSA数字签名这件事核心就两个动作产生签名和验证签名。私钥持有者用私钥对消息摘要做运算生成一段只有自己才能产出的密文这就是签名任何人拿到公钥对同一消息做同样的摘要运算再用公钥去验证签名是否匹配就能确认消息确实来自私钥持有者且中途没有被篡改。听起来简单但真正写起来摘要算法选哪个、填充模式怎么定、编码格式怎么统一、密钥怎么管理每一步都有坑。这篇内容适合三类人看一是正在做API签名、JWT、License授权、文件完整性校验的后端开发者二是想搞明白RSA签名底层到底在算什么的安全方向学习者三是已经会调库但总在联调时翻车的工程师。我会把签名产生和验证的完整链路拆开从数学原理到Python代码实现再到实际项目里的参数约定和排查技巧全部讲透。读完你至少能做到自己手写一套可用的RSA签名验签流程并且知道每一步为什么这么设计。2. RSA签名的整体设计与核心思路拆解2.1 签名和加密到底有什么区别很多人第一次接触RSA会把签名和加密混为一谈觉得都是用RSA无非一个用公钥一个用私钥。这个理解只对了一半方向确实相反但目的完全不同。加密的目的是保密发送方用接收方的公钥加密只有接收方的私钥能解开保证内容不被第三方看到。签名的目的是认证和防篡改发送方用自己的私钥签名任何人用发送方的公钥都能验证保证内容确实来自发送方且没被改过。用生活里的例子类比加密像是把信锁进一个只有收信人有钥匙的保险箱签名像是你在信末盖了一个只有你能刻出来的印章任何人拿你的印章样本一比就知道这信是不是你发的、有没有被涂改。这个区别直接决定了实现上的差异。加密是对整个消息做运算或者对对称密钥做运算而签名不是对消息本身直接做RSA运算而是先对消息做哈希摘要再对摘要做RSA运算。为什么因为RSA运算慢而且RSA能处理的数据长度受模数长度限制直接对长消息做RSA既不现实也不安全。2.2 为什么签名要先哈希再运算这里涉及一个关键设计先摘要后签名。流程是消息 - 哈希函数 - 固定长度摘要 - 私钥运算 - 签名值。哈希函数比如SHA-256把任意长度的消息压缩成固定长度的摘要比如256位。这个摘要有两个特性一是单向性无法从摘要反推原文二是抗碰撞性几乎不可能找到两条不同消息产生相同摘要。签名时对摘要做RSA运算验证时对收到的消息重新算摘要再用公钥解签名得到原始摘要两者一比就知道消息有没有被动过。如果跳过哈希直接对消息做RSA会带来几个问题第一RSA运算有长度上限长消息根本签不了第二RSA的数学结构存在一些可乘性直接签名可能被构造性攻击第三性能极差。所以哈希RSA是行业标准做法不是可选项。2.3 填充模式PKCS#1 v1.5 还是 PSSRSA签名在运算前还要做一步填充。裸的RSA运算教科书式RSA是不安全的必须加上填充方案。目前主流有两种PKCS#1 v1.5老牌方案兼容性极好几乎所有语言和库都支持。填充结构固定实现简单。PSSProbabilistic Signature Scheme较新的方案填充中引入随机盐值安全性证明更完备被认为是更现代的选择。选哪个我的经验是如果是新项目且双方都能控制优先PSS如果是和老系统对接、或者对接方是Java默认的SHA256withRSA那基本就是PKCS#1 v1.5。这个选择必须在联调前和对方确认清楚因为两种填充产生的签名值完全不同验签必然失败。2.4 摘要算法的选择摘要算法决定了签名的安全强度和输出长度。常见的有SHA-1、SHA-256、SHA-512。SHA-1已经被证明存在碰撞攻击新项目绝对不要用。SHA-256是目前最通用的选择安全性和性能平衡得好。SHA-512更安全但摘要更长RSA运算稍慢一般用于对安全要求极高的场景。这里有个容易忽略的点摘要算法必须和签名算法标识一致。比如你用SHA-256算摘要验签方也必须用SHA-256不能一个用SHA-256一个用SHA-1。在代码里这个信息通常体现在签名算法的命名上比如SHA256withRSA、RS256JWT里的叫法。3. 核心细节解析与实操要点3.1 密钥对的生成与格式约定RSA签名的第一步是有一对密钥。私钥签名公钥验签。密钥生成有两个关键参数模数长度和公钥指数。模数长度就是常说的2048位RSA、4096位RSA。2024年的今天1024位已经不安全2048位是底线4096位用于长期安全需求。模数越长运算越慢签名值也越长。2048位签名的输出是256字节4096位是512字节这个长度在联调时经常被用来快速判断对方用的多少位密钥。公钥指数通常用655370x10001。这个值不是随便选的太小比如3会有安全隐患太大又影响运算效率65537是一个兼顾安全和性能的经典选择。密钥的存储格式也有讲究。常见的有格式特点适用场景PEMBase64编码带-----BEGIN头尾配置文件、跨语言交换DER二进制格式程序内部、紧凑存储PKCS#1RSA专用格式头是BEGIN RSA PRIVATE KEY老系统PKCS#8通用私钥格式头是BEGIN PRIVATE KEY新系统推荐联调时最常见的坑就是格式不匹配。对方给你一个PKCS#8的私钥你用只认PKCS#1的解析器去读直接报错。Python的cryptography库对两种格式都支持但pycryptodome的某些接口只认特定格式这个后面代码部分会细说。3.2 消息摘要的计算细节摘要计算看起来简单但有几个细节决定成败。第一消息的编码。签名前要把消息转成字节串。如果消息是字符串用什么编码UTF-8是默认选择但如果对方用GBK摘要就完全不同。这个必须在接口文档里写死。第二是否包含额外字段。实际项目里签名往往不是对消息体本身签名而是对消息体时间戳随机数appId拼接后的字符串签名。拼接顺序、分隔符、是否排序这些都要约定清楚。我见过因为参数拼接顺序不一致导致验签失败的案例排查了大半天。第三换行和空格。如果消息是JSON序列化时是否带空格、是否排序key都会影响摘要。稳妥的做法是双方约定一个规范的序列化方式比如按key字典序排序、无多余空格。3.3 签名值的编码与传输RSA运算出来的签名是二进制字节串直接放进JSON或URL里会有问题所以通常要做编码。最常见的是Base64也有用Hex的。Base64编码有几个变种需要警惕标准Base64用和/URL安全的Base64用-和_是否带填充也有区别。如果签名要放在URL参数里必须用URL安全变体否则会被解析成空格验签必挂。还有一个高频坑Base64编解码的次数。有些库在签名接口内部已经做了Base64返回的已经是编码后的字符串如果你再手动编码一次就变成双重编码。验签方解一次得到的是Base64字符串而不是原始签名字节自然验不过。这个问题的排查方法很简单看签名值的长度标准Base64编码后长度是原始字节的4/3左右如果明显偏长多半是编码多了。3.4 公钥的分发与信任签名验证的前提是验签方持有正确的公钥。公钥的分发方式直接影响安全性。最朴素的方式是直接把公钥文件给对方。但这里有个信任问题怎么确认拿到的公钥真的是对方的而不是中间人替换的这就涉及到证书体系PKI公钥被包装在证书里由受信任的CA签发验签方通过验证证书链来确认公钥的真实性。在内部系统之间通常简化处理公钥通过配置中心或密钥管理服务分发配合网络隔离和访问控制来保证安全。但如果是开放平台对外提供接口就必须考虑证书体系否则签名机制形同虚设。提示公钥可以公开但必须保证来源可信。私钥绝对不能出现在任何日志、代码仓库、客户端代码里。我见过把私钥硬编码在前端JS里的等于没有签名。4. 实操过程与核心环节实现4.1 环境准备与依赖选择Python做RSA签名主流有两个库cryptography和pycryptodome。cryptography是现在的事实标准API设计现代文档完善底层用OpenSSL性能和安全性都有保障。pycryptodome是老的pycrypto的继任者API偏底层但有些老项目还在用。我的建议是新项目一律用cryptography。安装很简单pip install cryptography如果需要处理PEM文件的读写cryptography自带serialization模块够用。如果要做更复杂的密钥格式转换可以配合openssl命令行工具。4.2 生成RSA密钥对先用代码生成一对2048位的RSA密钥并保存成PEM格式from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization # 生成私钥 private_key rsa.generate_private_key( public_exponent65537, key_size2048, ) # 序列化私钥为PEMPKCS#8格式无加密 private_pem private_key.private_bytes( encodingserialization.Encoding.PEM, formatserialization.PrivateFormat.PKCS8, encryption_algorithmserialization.NoEncryption(), ) # 从私钥导出公钥 public_key private_key.public_key() public_pem public_key.public_bytes( encodingserialization.Encoding.PEM, formatserialization.PublicFormat.SubjectPublicKeyInfo, ) with open(private_key.pem, wb) as f: f.write(private_pem) with open(public_key.pem, wb) as f: f.write(public_pem)这段代码有几个点值得说明。public_exponent65537是标准选择。key_size2048是当前的安全底线。私钥用PKCS#8格式序列化兼容性最好。公钥用SubjectPublicKeyInfo格式这是X.509标准里的公钥格式跨语言通用。如果你拿到的是PKCS#1格式的私钥头是BEGIN RSA PRIVATE KEYcryptography也能读它会自动识别。但如果你要生成PKCS#1格式需要用TraditionalOpenSSL格式formatserialization.PrivateFormat.TraditionalOpenSSL4.3 产生签名完整代码与参数解析下面是对消息产生签名的完整实现用PKCS#1 v1.5填充加SHA-256摘要import base64 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding def sign_message(private_key_path: str, message: str) - str: # 读取私钥 with open(private_key_path, rb) as f: private_key serialization.load_pem_private_key( f.read(), passwordNone, ) # 消息编码为字节 message_bytes message.encode(utf-8) # 执行签名 signature private_key.sign( message_bytes, padding.PKCS1v15(), hashes.SHA256(), ) # Base64编码便于传输 return base64.b64encode(signature).decode(utf-8)逐行拆解一下。load_pem_private_key读取PEM私钥如果有密码就传password参数。message.encode(utf-8)把字符串转字节编码方式必须和验签方一致。private_key.sign是核心三个参数分别是消息字节、填充方案、摘要算法。最后Base64编码成字符串。如果要用PSS填充把padding.PKCS1v15()换成padding.PSS( mgfpadding.MGF1(hashes.SHA256()), salt_lengthpadding.PSS.MAX_LENGTH, )PSS的盐长度建议用MAX_LENGTH这是最安全的配置。注意PSS的签名是随机的同样的消息每次签名结果都不同但都能验签通过这是正常现象不要以为是bug。4.4 验证签名完整代码与参数解析验签是签名的逆过程用公钥验证def verify_signature(public_key_path: str, message: str, signature_b64: str) - bool: # 读取公钥 with open(public_key_path, rb) as f: public_key serialization.load_pem_public_key(f.read()) # 解码签名 signature base64.b64decode(signature_b64) # 消息编码 message_bytes message.encode(utf-8) try: public_key.verify( signature, message_bytes, padding.PKCS1v15(), hashes.SHA256(), ) return True except Exception: return Falsepublic_key.verify如果验签失败会抛异常所以用try包裹。注意参数顺序签名在前消息在后这和签名接口的参数顺序是反的很容易搞混。填充方案和摘要算法必须和签名时完全一致否则必然失败。4.5 一个完整的可运行示例把上面的代码串起来做一个端到端的演示if __name__ __main__: # 假设密钥已生成 msg orderId12345amount99.00timestamp1700000000 sig sign_message(private_key.pem, msg) print(签名值:, sig) ok verify_signature(public_key.pem, msg, sig) print(验签结果:, ok) # 篡改消息测试 tampered msg extra1 print(篡改后验签:, verify_signature(public_key.pem, tampered, sig))运行结果应该是第一次验签返回True篡改后返回False。这个测试很重要能验证你的签名机制确实起到了防篡改作用。如果篡改后还能验过说明哪里出问题了。4.6 参数选择背后的计算逻辑为什么2048位密钥的签名是256字节因为RSA的模数n是2048位签名值s是模n下的一个数所以s的范围是0到n-1用字节表示就是2048/8256字节。Base64编码后长度是256的4/3向上取整约344个字符。如果你看到签名值Base64后是344字符左右基本可以确认是2048位密钥。为什么摘要用SHA-256而不是SHA-512SHA-256输出32字节SHA-512输出64字节。对于2048位RSAPKCS#1 v1.5填充需要预留至少11字节填充头所以能容纳的摘要最大是256-11245字节两种都放得下。但SHA-256性能更好且256位的安全强度已经足够匹配2048位RSA两者安全强度大致相当。用SHA-512配2048位RSA有点浪费配4096位RSA更合理。5. 常见问题与排查技巧实录5.1 验签失败的排查思路验签失败是最高频的问题原因五花八门。我整理了一个排查顺序按这个顺序走基本能定位到问题排查项检查内容常见错误密钥匹配公钥是否由对应私钥导出用错密钥对摘要算法双方是否一致一个SHA-256一个SHA-1填充模式双方是否一致一个PKCS#1一个PSS消息编码字符串转字节的编码UTF-8 vs GBK消息内容拼接顺序、字段、空格参数顺序不一致签名编码Base64变体、编码次数双重编码、URL安全变体签名值完整性传输中是否被截断URL里变空格排查时最有效的手段是打印中间值。把消息的字节串、摘要的十六进制、签名解码后的字节长度都打出来和对方对比。哪一步对不上问题就在哪。5.2 那些年踩过的编码坑Base64的坑我踩过至少三次值得单独说。第一次是URL传输问题。签名放在URL参数里Base64里的被服务端解析成空格验签失败。解决办法是用URL安全的Base64把换成-/换成_。Python里base64.urlsafe_b64encode(signature).decode()第二次是双重编码。对方库的签名接口返回的已经是Base64字符串我这边又编码了一次导致验签方解码后拿到的是Base64文本而不是原始签名。排查方法是看长度双重编码后长度会明显偏长。第三次是填充符问题。有些Base64实现带填充有些不带。如果一方带一方不带解码时可能出错。稳妥做法是约定统一或者解码时用能容忍缺失填充的实现。5.3 密钥格式转换的实操联调时经常需要转换密钥格式。比如对方给的是PKCS#1你的库要PKCS#8。用openssl命令行可以快速转换# PKCS#1 转 PKCS#8 openssl pkcs8 -topk8 -inform PEM -in pkcs1_key.pem -outform PEM -nocrypt -out pkcs8_key.pem # PKCS#8 转 PKCS#1 openssl rsa -in pkcs8_key.pem -traditional -out pkcs1_key.pem # 从私钥导出公钥 openssl rsa -in private_key.pem -pubout -out public_key.pemPython里也可以用cryptography做转换读取后重新序列化成目标格式即可。转换时注意私钥的加密状态带密码的私钥需要提供密码。5.4 性能与并发的注意事项RSA签名是CPU密集型操作2048位一次签名大概几毫秒。单次调用感觉不到但高并发场景下会成为瓶颈。优化思路有几个。第一缓存密钥对象。不要每次签名都从文件读取和解析私钥把private_key对象缓存在内存里只读一次。第二控制签名频率。如果是对每个请求签名考虑批量签名或者用更轻量的HMAC做请求级签名RSA只用于关键操作。第三考虑密钥长度。2048位够用就不要上4096位性能差好几倍。验签方也有类似问题公钥对象同样应该缓存。另外验签失败会抛异常异常处理本身有开销如果遭遇大量伪造请求要做好限流和防护。5.5 安全性方面的几个红线最后说几个安全红线都是实际项目中容易忽视的。私钥绝不能硬编码在代码里也不能提交到代码仓库。应该放在密钥管理服务或者受保护的配置文件中配合访问权限控制。私钥文件权限设为600只有属主可读。签名机制必须配合时间戳和随机数防止重放攻击。光有签名攻击者可以把截获的请求原样重发签名照样能验过。加上时间戳比如5分钟有效期和一次性随机数服务端记录已用过的nonce才能防重放。摘要算法和密钥长度要跟上时代。SHA-1和1024位RSA都应该淘汰。定期评估密钥强度必要时做密钥轮换。注意签名验证失败时不要给客户端返回具体的失败原因比如摘要不匹配还是公钥错误统一返回验签失败即可。详细的失败原因只记录在服务端日志里避免给攻击者提供信息。6. 写在最后的一点个人体会RSA签名的代码本身不长难的是把每个环节的约定对齐。我现在的习惯是任何涉及签名的接口联调先和对方确认一张签名参数清单密钥长度、摘要算法、填充模式、消息编码、拼接规则、签名编码方式六项全部书面确认后再动手写代码。这张清单能省掉至少一半的联调时间。另外自己写一个最小可运行的签名验签demo比读十篇文档都管用。把密钥生成、签名、验签、篡改测试跑通一遍再往项目里集成心里就有底了。上面那些代码你直接复制就能跑建议先跑通再改。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号