
电子签名怎么签:3个源码级细节决定安全,附最佳实践
面试被问“电子签名怎么签”,90%的人只会说“用非对称加密”,追问原理就卡壳。别慌,今天直接拆代码,用 最佳实践 告诉你,从密钥生成到验签,每一步该怎么落地。
入口定位:签名不是“加密”
很多初学者把“电子签名”和“数据加密”混为一谈。这是第一个坑。
电子签名的核心目标是:防抵赖、防篡改、验身份。
它不是为了隐藏数据内容。如果你把明文数据直接签名,任何人都能看到内容;如果你把密文签名,别人能看密文,但不知道内容。
真正的电子签名流程是:
哈希:对原文计算摘要(如 SHA-256)。
私钥签名:用发送者的私钥对摘要进行加密(实际上是数学运算,生成签名值)。
发送:把【原文 + 签名值】一起发给接收者。
验签:接收者用发送者的公钥解密签名值,得到摘要A;同时对收到的原文计算哈希,得到摘要B。
比对:如果 A == B,则签名有效。
为什么不用公钥签名?
因为公钥是公开的,任何人都能用公钥“签名”,那这个签名就毫无意义。只有私钥是唯一的,才能证明“只有我能生成这个签名”。
核心片段:Python cryptography 库实战
我们来看一段基于 cryptography 库的真实代码。这是目前 Python 生态中处理密码学操作最推荐的库,底层是 C 实现,性能极高,且符合现代密码学标准。
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.exceptions import InvalidSignature
import os
# 1. 生成 RSA 密钥对(2048位,符合当前安全标准)
private_key = rsa.generate_private_key(
public_exponent=65537, # 标准公钥指数
key_size=2048, # 密钥长度,越大越安全但越慢
backend=None # 默认后端
)
public_key = private_key.public_key()
# 2. 准备原始数据
message = bHello, this is a secure message for signing.
# 3. 签名过程:使用 PSS 填充方案(推荐)
# PSS (Probabilistic Signature Scheme) 比 PKCS#1 v1.5 更安全
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), # 掩码生成函数
salt_length=padding.PSS.MAX_LENGTH # 盐值长度,最大
),
hashes.SHA256() # 哈希算法
)
print(f原始消息: {message.decode()})
print(f签名值 (Hex): {signature.hex()[:32]}...)
# 4. 验签过程
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print(验签成功:签名有效,数据未被篡改。)
except InvalidSignature:
print(验签失败:签名无效或数据被篡改。)
# 5. 篡改测试
tampered_message = bHello, this is a TAMPERED message for signing.
try:
public_key.verify(
signature,
tampered_message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print(错误:不应该验签成功!)
except InvalidSignature:
print(验签失败:检测到数据被篡改(符合预期)。)
逐行解析关键设计:
rsa.generate_private_key: 生成密钥对时,key_size=2048 是目前的 最佳实践。1024位已被认为不安全,4096位性能开销大,2048位是安全与性能的平衡点。
padding.PSS: 这里用了 PSS 填充,而不是更常见的 PKCS1v15。PSS 是概率性签名,理论上安全性更高,能抵抗更多类型的攻击。在金融、政务等高安全场景,PSS 是首选。
hashes.SHA256: 哈希算法选择 SHA-256。MD5 和 SHA-1 已被破解,不要用。SHA-384/512 也可以,但 SHA-256 在兼容性和性能上更优。
public_key.verify: 验签时,必须使用与签名时完全一致的填充方案和哈希算法。哪怕盐值长度不同,也可能导致验签失败。
设计思想:为什么是“哈希+私钥”?
很多人问:为什么不直接对原文用私钥加密?
原因有三:
性能问题:RSA 加密很慢。如果原文是 1MB 的合同,直接用 RSA 加密需要几百毫秒。而先哈希成 32 字节摘要,再 RSA 签名,只需几毫秒。
安全性问题:RSA 的明文长度有限制。如果原文超过密钥长度减填充长度,RSA 会直接报错。哈希后固定长度,避免了这个问题。
标准统一:所有数字签名标准(如 PKCS#1、RFC 4051)都规定:签名 = Sign(Hash(原文))。这是行业共识,方便互操作。
GitHub 开源仓库参考:
你可以查看 cryptography/cryptography 这个 GitHub 仓库。它是 Python 密码学的事实标准,背后是 OpenSSL 引擎。在 src/rust 目录下,你可以看到 Rust 实现的底层绑定,这保证了即使在 Python 层调用,底层运算也是高性能的。
手写简化版:理解底层逻辑
为了让你真正理解“签名”是什么,我们用伪代码模拟一下 RSA 签名的数学过程(仅用于学习,勿用于生产):
# 简化版 RSA 签名(非生产可用)
# 假设 p, q 是两个大素数,n = p*q, phi(n) = (p-1)*(q-1)
# 选 e 使得 gcd(e, phi(n)) = 1,通常 e=65537
# 求 d 使得 e*d ≡ 1 mod phi(n)
# 公钥: (e, n)
# 私钥: (d, n)
# 签名: s = m^d mod n
# 验签: m = s^e mod n
# 注意:实际中 m 不能直接是原文,必须是哈希值 h
# 签名: s = h^d mod n
# 验签: h' = s^e mod n
# 比对: h == h'
# 为什么 d 是私钥?
# 因为只有知道 p 和 q,才能算出 phi(n),进而算出 d
# 攻击者不知道 p, q,就无法算出 d,就无法伪造签名
关键点:
签名是私钥运算:s = h^d mod n。只有拥有 d(私钥)的人才能计算。
验签是公钥运算:h' = s^e mod n。任何人拥有 e(公钥)都能验证。
哈希的作用:保证原文的唯一性。如果原文改一个字符,哈希值完全改变,签名就无效。
应用场景与避坑指南
1. 文件完整性校验
合同、日志、配置文件的完整性校验。签名后,任何篡改都会被发现。
2. API 请求签名
在微服务间通信或第三方 API 调用中,对请求参数签名,防止中间人篡改。
最佳实践避坑:
密钥管理:私钥绝对不能硬编码在代码里。使用 KMS(密钥管理服务)或硬件安全模块(HSM)存储。
算法选择:优先使用 Ed25519(椭圆曲线签名)。比 RSA 2048 更快、签名更短(64字节 vs 256字节),且安全性更高。Python cryptography 库支持 Ed25519。
时间戳:签名应包含时间戳,防止重放攻击。
证书链:公钥如何证明是真实的?需要 CA 颁发的证书。在 Web 场景中,HTTPS 的 TLS 握手就包含证书验证。
Ed25519 示例(更现代的选择):
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
# 生成 Ed25519 密钥对
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
message = bEd25519 is faster and more secure.
signature = private_key.sign(message)
# 验签
public_key.verify(signature, message) # 无异常即成功
Ed25519 签名只需一次哈希,运算极快,适合高并发场景。
你在项目里踩过这个坑吗?评论区聊聊
电子签名看似简单,实则细节决定成败。从算法选择、填充方案到密钥管理,每一步都有陷阱。
你在项目中用电子签名时,遇到过验签失败、性能瓶颈,还是密钥管理难题?是用了 RSA 还是 Ed25519?有没有因为填充方案不一致导致跨语言兼容性问题?
评论区聊聊你的实战经验,一起避坑。