MAC 能证明消息没被篡改,也能证明它来自某个持有密钥的人——但它无法证明那个人是谁。Alice 和 Bob 共享密钥 K,Alice 用 K 签了一条消息发给 Bob,Bob 验证通过。但 Bob 也能用 K 伪造一条”来自 Alice”的消息。在法庭上,Alice 完全可以说”这条消息是 Bob 自己生成的”。**数字签名(Digital Signature)**用非对称密钥解决了这个问题:私钥签名、公钥验证,只有私钥持有者能签名,任何人都能验证。
一、数字签名基础
1.1 为什么 MAC 不够?
MAC 能保证消息没有被篡改,且确实来自某个持有密钥的人。但问题在于——对称密钥意味着双方都能生成有效的 MAC。假设 Alice 和 Bob 共享密钥 K:
- Alice 用 K 生成 MAC 发给 Bob → Bob 可以验证
- 但 Bob 也能用 K 伪造一条”来自 Alice”的消息
- 在法庭上,Alice 可以说”这条消息可能是 Bob 自己生成的”
| 场景 | MAC 够用吗? | 数字签名 |
|---|---|---|
| API 请求认证 | 双方互信即可 | 过度设计 |
| JWT 内部服务 | 微服务间共享密钥 | 可选 |
| 电子合同签署 | 需要不可抵赖 | 必须 |
| 代码发布签名 | 需要公开验证 | 必须 |
| 证书签发 | CA 必须不可抵赖 | 必须 |
| 区块链交易 | 需要公开验证 | 必须 |
简单原则:如果只有两方参与且互信,用 MAC;如果需要第三方验证或不可抵赖,用数字签名。
1.2 签名与验证
数字签名使用私钥签名、公钥验证,提供认证 + 完整性 + 不可抵赖:
1.3 签名的三种用途
| 用途 | 含义 | 实际场景 |
|---|---|---|
| 认证(Authentication) | 确认签名者身份 | SSH 登录、代码提交 |
| 完整性(Integrity) | 消息未被篡改 | 软件分发、固件升级 |
| 不可抵赖(Non-repudiation) | 签名者无法否认 | 电子合同、金融交易 |
| 属性 | MAC | 数字签名 |
|---|---|---|
| 认证 | ||
| 完整性 | ||
| 不可抵赖 | (对称密钥) | (非对称密钥) |
| 验证者 | 仅密钥持有者 | 任何人(公钥公开) |
二、RSA-PSS 签名
2.1 RSA 签名的数学原理
RSA 签名本质上是”用私钥加密”——更准确地说,是对消息摘要做模幂运算:
签名:s = H(m)^d mod n验证:H(m) = s^e mod n
其中:- d 是私钥指数- e 是公钥指数(通常为 65537)- n 是模数(两个大素数的乘积)注意:直接对原始消息做 RSA 运算是不安全的,必须先哈希再填充再签名。这就是填充方案的作用。
2.2 为什么不用 PKCS#1 v1.5?
PKCS#1 v1.5 填充结构过于简单,存在多种攻击:
| 填充方案 | 安全性 | 攻击 | 可证明安全 |
|---|---|---|---|
| PKCS#1 v1.5 | Bleichenbacher、存在性伪造 | 否 | |
| RSA-PSS | 无已知实际攻击 | 是(在 RSA 假设下) |
PKCS#1 v1.5 的填充格式:0x00 | 0x01 | FF FF ... FF | 0x00 | DigestInfo。问题在于验证时只检查前缀和后缀,中间的 FF 字节数量没有被严格校验——这为伪造留下了空间。
2.3 PKCS#1 v1.5 vs PSS 详细对比
| 维度 | PKCS#1 v1.5 | RSA-PSS |
|---|---|---|
| 填充结构 | 固定格式 | 随机化(含盐值) |
| 安全证明 | 无 | 有(随机预言模型) |
| 签名确定性 | 确定(同一消息同一签名) | 随机(同一消息不同签名) |
| 存在性伪造 | 可能(低指数时) | 不可行 |
| Bleichenbacher | 受影响 | 不受影响 |
| 兼容性 | 广泛(旧系统) | 较新(TLS 1.3 推荐) |
为什么 PKCS#1 v1.5 还在用?答案是兼容性。大量遗留系统(旧版 Java、嵌入式设备、智能卡)只支持 v1.5。迁移需要时间,但新系统必须使用 PSS。TLS 1.3 已经明确要求 RSA-PSS。
2.4 Bleichenbacher 攻击
1998 年 Daniel Bleichenbacher 发现了针对 PKCS#1 v1.5 的适应性选择密文攻击:
- 攻击者构造伪造密文,发送给服务器
- 服务器返回”填充是否有效”的错误信息
- 利用这个预言机(oracle),逐步缩小明文空间
- 经过约 100 万次查询,恢复任意密文对应的明文
2018 年以 ROBOT(Return Of Bleichenbacher’s Oracle Threat)卷土重来,影响了 Facebook、PayPal 等众多网站。
# Python: RSA-PSS 签名(推荐方式)from cryptography.hazmat.primitives.asymmetric import rsa, paddingfrom cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)message = b"Important document"
# 签名signature = private_key.sign( message, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256())
# 验证public_key = private_key.public_key()public_key.verify(signature, message, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())// Go: RSA-PSS 签名privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)hashed := sha256.Sum256([]byte("Important document"))sig, _ := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, hashed[:], nil)err := rsa.VerifyPSS(&privateKey.PublicKey, crypto.SHA256, hashed[:], sig, nil)三、ECDSA 签名
3.1 ECDSA 签名算法详解
ECDSA 基于 非对称加密 中介绍的椭圆曲线。
签名算法:
输入:私钥 d,消息 m1. 计算哈希 e = H(m)2. 生成随机数 k(1 ≤ k ≤ n-1)3. 计算曲线点 (x₁, y₁) = k · G4. 计算 r = x₁ mod n(若 r = 0 则重新选 k)5. 计算 s = k⁻¹(e + r·d) mod n(若 s = 0 则重新选 k)6. 输出签名 (r, s)验证算法:
输入:公钥 Q,消息 m,签名 (r, s)1. 验证 r, s ∈ [1, n-1]2. 计算哈希 e = H(m)3. 计算 w = s⁻¹ mod n4. 计算 u₁ = e·w mod n,u₂ = r·w mod n5. 计算曲线点 (x₁, y₁) = u₁·G + u₂·Q6. 验证 r ≡ x₁ mod n验证时通过 u₁·G + u₂·Q 重建了签名时的 k·G,因为 u₁·G + u₂·Q = w·(e + r·d)·G = k·G。
3.2 为什么 k 值如此关键
ECDSA 签名中的随机数 k 是整个安全性的命门。如果 k 出了问题,私钥就会泄露:
场景一:k 值重复
签名1:(r, s₁),其中 s₁ = k⁻¹(e₁ + r·d) mod n签名2:(r, s₂),其中 s₂ = k⁻¹(e₂ + r·d) mod n
k 相同 → r 相同,所以:k = (e₁ - e₂) / (s₁ - s₂) mod nd = (s₁·k - e₁) / r mod n ← 私钥恢复!PS3 被黑事件(2010)
Sony 的 PS3 使用 ECDSA 签名验证游戏是否为正版,但所有签名使用相同的 k 值(硬编码在代码中)。黑客 Hector Martin 发现后,用上述方法恢复了 Sony 的私钥,从而可以签名任意 PS3 软件。
# 演示:k 值重复导致私钥泄露def recover_private_key(e1, e2, s1, s2, r, n): """从两个使用相同 k 的签名中恢复私钥""" k = ((e1 - e2) * pow(s1 - s2, -1, n)) % n d = ((s1 * k - e1) * pow(r, -1, n)) % n return d场景二:k 值可预测
即使 k 不完全相同,只要部分比特可预测,也能用格攻击(lattice attack)恢复私钥。2019 年有研究者发现某些比特币钱包的 k 值只有约 128 位随机性,用 LLL 格基规约算法就能恢复私钥。
ECDSA 的 k 值安全是整个签名系统的阿喀琉斯之踵。如果 RNG 在签名时出问题(比如虚拟机快照导致熵不足),私钥就会泄露。这就是为什么 EdDSA 的确定性设计如此重要。
3.3 确定性 ECDSA(RFC 6979)
为了解决 k 值随机性的问题,RFC 6979 提出确定性 k 值生成:
k = HMAC_DRBG(private_key, message_hash)
同一消息 + 同一私钥 → 同一 k → 同一签名这消除了对高质量 RNG 的依赖,同时保持了 ECDSA 的兼容性。
# Python: 确定性 ECDSA 签名(RFC 6979)from cryptography.hazmat.primitives.asymmetric.ec import ( generate_private_key, ECDSA, SECP256R1)from cryptography.hazmat.primitives import hashes
private_key = generate_private_key(SECP256R1())message = b"Important document"# cryptography 库默认使用确定性 k(RFC 6979)signature = private_key.sign(message, ECDSA(hashes.SHA256()))public_key = private_key.public_key()public_key.verify(signature, message, ECDSA(hashes.SHA256()))// Java: ECDSA 签名KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");kpg.initialize(256); // secp256r1KeyPair kp = kpg.generateKeyPair();
Signature sig = Signature.getInstance("SHA256withECDSA");sig.initSign(kp.getPrivate());sig.update("Important document".getBytes());byte[] signature = sig.sign();
sig.initVerify(kp.getPublic());sig.update("Important document".getBytes());boolean valid = sig.verify(signature);四、EdDSA 签名(Ed25519)
4.1 Ed25519 的设计哲学
Ed25519 由 Daniel J. Bernstein 等人设计,目标非常明确——消除 ECDSA 的所有坑:
| ECDSA 的痛点 | Ed25519 的解决方案 |
|---|---|
| k 值需要随机数 | 确定性签名,不需要 RNG |
| 不同的曲线参数 | 固定曲线(Curve25519),无参数协商 |
| 签名格式不统一 | 固定 64 字节签名 |
| 验证实现复杂 | 验证公式简单,不易出错 |
| 侧信道攻击 | 设计上抗侧信道 |
4.2 为什么 Ed25519 不需要随机数
Ed25519 的签名过程完全确定性:
1. 私钥扩展:hash(key) → (prefix, public_key_scalar)2. 确定性 nonce:r = H(prefix || message) ← 不需要随机数!3. 签名计算: R = r · B(B 是基点) S = (r + H(R || A || message) · a) mod l4. 输出签名 (R, S),共 64 字节关键在第 2 步——nonce r 由私钥前缀和消息的哈希决定,而非随机生成。同一消息 + 同一私钥 → 同一签名;不同消息 → 不同 nonce。不依赖系统 RNG,即使在熵不足的环境中也是安全的。
4.3 Ed25519 vs ECDSA 详细对比
| 维度 | ECDSA (secp256r1) | Ed25519 |
|---|---|---|
| 曲线 | Weierstrass 形式 | Twisted Edwards 形式 |
| 密钥大小 | 32 字节(+参数) | 32 字节(自包含) |
| 签名大小 | 64 字节(DER 编码可变) | 64 字节(固定) |
| 签名速度 | 中 | 快(~2x ECDSA) |
| 验证速度 | 中 | 快 |
| 随机数 | 需要(除非 RFC 6979) | 不需要(确定性) |
| 批量验证 | 不支持 | 支持(~3x 加速) |
| 安全等级 | 128 位 | 128 位 |
| 侧信道 | 需要小心实现 | 设计上抗侧信道 |
| 标准化 | NIST/FIPS | RFC 8032 |
4.4 Ed25519 代码示例
# Python: Ed25519 签名(推荐)from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = Ed25519PrivateKey.generate()message = b"Important document"signature = private_key.sign(message)public_key = private_key.public_key()public_key.verify(signature, message)// Go: Ed25519 签名publicKey, privateKey, _ := ed25519.GenerateKey(nil)message := []byte("Important document")signature := ed25519.Sign(privateKey, message)valid := ed25519.Verify(publicKey, message, signature)// Java: Ed25519 签名(需要 Java 15+)KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");KeyPair kp = kpg.generateKeyPair();Signature sig = Signature.getInstance("Ed25519");sig.initSign(kp.getPrivate());sig.update("Important document".getBytes());byte[] signature = sig.sign();4.5 Ed25519 在 SSH 与 Git 中的应用
# 生成 Ed25519 SSH 密钥ssh-keygen -t ed25519 -C "your@email.com"
# Git 使用 Ed25519 签名 commitgit config user.signingkey ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...git commit -S -m "signed commit"| 工具 | 旧方案 | 推荐方案 |
|---|---|---|
| SSH 密钥 | RSA 2048/4096 | Ed25519 |
| Git 签名 | GPG (RSA) | SSH (Ed25519) |
| SSH 证书 | RSA | Ed25519 |
如果你还在用 RSA SSH 密钥,建议迁移到 Ed25519。密钥更短、速度更快、安全性更高。一条命令即可:ssh-keygen -t ed25519。
五、证书链与信任
5.1 为什么需要证书链?
数字签名解决了”消息确实由私钥持有者签署”的问题,但没有解决”这个公钥属于谁”的问题。假设你收到一个 Ed25519 签名的消息,验证通过了——但你怎么知道这个公钥真的是 Alice 的?攻击者 Eve 完全可以生成自己的密钥对,然后声称”这是 Alice 的公钥”。
5.2 证书链的结构
证书链验证过程:
- 从 end-entity 证书开始
- 找到签发者(issuer)的中间 CA 证书
- 用中间 CA 的公钥验证 end-entity 证书的签名
- 继续向上,直到到达 Root CA
- 检查 Root CA 是否在系统信任存储中
| 证书类型 | 签名者 | 用途 | 有效期 |
|---|---|---|---|
| Root CA | 自己(自签名) | 信任锚点 | 20-25 年 |
| Intermediate CA | Root CA 或上级中间 CA | 签发终端证书 | 5-10 年 |
| End-Entity | Intermediate CA | 实际使用(TLS、代码签名) | 90 天-1 年 |
5.3 为什么需要中间 CA?
- 安全隔离:Root CA 的私钥存储在离线 HSM 中,极少使用。如果 Root CA 密钥泄露,整个信任体系崩塌
- 分工管理:不同中间 CA 可以负责不同业务(TLS 证书、代码签名、S/MIME)
- 吊销效率:中间 CA 被泄露只需吊销该中间 CA,影响范围有限
证书与 PKI 的完整内容将在 证书与 PKI 中深入展开,包括 OCSP、CRL、Let’s Encrypt 等。
六、签名流程与最佳实践
6.1 完整签名工作流
6.2 签名还是加密?先签名还是先加密?
| 方案 | 做法 | 安全性 | 问题 |
|---|---|---|---|
| 先签名后加密 | Sign → Encrypt | 推荐 | 无 |
| 先加密后签名 | Encrypt → Sign | 有风险 | 可能被替换签名 |
| 签名并加密 | Encrypt && Sign | 最安全 | 实现复杂 |
推荐”先签名后加密”:先对明文签名,然后对(明文 + 签名)加密。这样接收者解密后可以验证签名来源。
| 需求 | 方案 |
|---|---|
| 只需完整性 + 认证 | 签名(或 MAC) |
| 只需机密性 | 加密 |
| 机密性 + 不可抵赖 | 先签名后加密 |
| 机密性 + 认证 | AEAD(加密 + MAC) |
6.3 多签名场景
顺序签名(Counter-signing):文档 → A 签名 → (文档, sig_A) → B 签名 → (文档, sig_A, sig_B)。验证时按序验证。
并行签名(Independent signing):文档 → A 签名 → sig_A,文档 → B 签名 → sig_B。最终 (文档, sig_A, sig_B),独立验证。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 顺序签名 | 有先后顺序,后签者认可前签 | 验证必须按序 | 合同审批流 |
| 并行签名 | 独立验证,效率高 | 无先后关系 | 多方见证 |
6.4 实践:签名 JWT
# Python: 使用 Ed25519 签名 JWTimport json, base64from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = Ed25519PrivateKey.generate()
def base64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = base64url(json.dumps({"alg": "EdDSA", "typ": "JWT"}).encode())payload = base64url(json.dumps({"sub": "user123", "exp": 1735689600}).encode())signing_input = f"{header}.{payload}".encode()signature = private_key.sign(signing_input)jwt_token = f"{header}.{payload}.{base64url(signature)}"七、签名攻击
7.1 k 值重用攻击(ECDSA)
前面已经讲过 PS3 事件。这里给出更完整的攻击代码:
# ECDSA k 值重用攻击演示def ecdsa_k_reuse_attack(e1, e2, s1, s2, r, n): """当两个 ECDSA 签名使用相同的 k 值时,恢复私钥""" k = ((e1 - e2) * pow(s1 - s2, -1, n)) % n d = ((s1 * k - e1) * pow(r, -1, n)) % n return d, k| 事件 | 年份 | 原因 | 后果 |
|---|---|---|---|
| PS3 被黑 | 2010 | Sony 硬编码 k 值 | 私钥泄露,可签名任意软件 |
| 比特币钱包 | 2013-2019 | RNG 缺陷导致 k 部分可预测 | 多个地址私钥被恢复 |
| Android Bitcoin | 2013 | Android SecureRandom 漏洞 | 比特币被盗 |
7.2 存在性伪造攻击
存在性伪造指攻击者可以在不知道私钥的情况下伪造一个有效签名,虽然无法控制消息内容。
PKCS#1 v1.5 的存在性伪造:如果验证实现不严格检查 DigestInfo 中的算法标识符,攻击者可以构造一个”看起来合法”但实际对应不同哈希算法的签名。
低指数 RSA 的存在性伪造:当 e = 3 时,攻击者可以计算 s³ 使其格式恰好符合 PKCS#1 v1.5 填充,从而伪造签名。
| 攻击 | 条件 | 防御 |
|---|---|---|
| PKCS#1 v1.5 伪造 | 验证不严格 | 使用 RSA-PSS |
| 低指数 RSA 伪造 | e = 3 + PKCS#1 v1.5 | e ≥ 65537 + PSS |
| ECDSA 伪造 | 弱曲线 | 使用标准曲线 |
7.3 时序攻击
签名验证的时间可能泄露私钥信息。攻击者通过精确测量验证时间,逐步推断私钥的比特。
# 不安全的比较(时序泄露)def verify_unsafe(computed, received): return computed == received # 逐字节比较,不等时立即返回
# 安全的比较(常数时间)import hmacdef verify_safe(computed, received): return hmac.compare_digest(computed, received) # 总是比较全部字节2011 年 Brumley 和 Hakioja 展示了对 OpenSSL ECDSA 实现的时序攻击,通过网络延迟测量恢复了 TLS 服务器的私钥。
7.4 故障注入攻击
故障注入通过物理手段(电压毛刺、激光、电磁脉冲)干扰签名计算,使设备输出一个错误的签名。
RSA-CRT 故障攻击:RSA 签名通常使用中国剩余定理(CRT)加速。一次故障签名就能分解 n:
正常:s = s_p · q · q_inv + s_q · p · p_inv mod n故障:s' = s_p' · q · q_inv + s_q · p · p_inv mod ngcd(s - s', n) = p ← 因子分解!| 攻击方式 | 目标 | 难度 | 防御 |
|---|---|---|---|
| 电压毛刺 | 智能卡 | 中 | 签名前验证结果 |
| 激光注入 | HSM | 高 | 故障检测电路 |
| 电磁脉冲 | 嵌入式设备 | 中 | 冗余计算 |
7.5 重复签名密钥选择攻击(DSKS)
DSKS 攻击者生成一个不同的密钥对,使得同一个签名在两个密钥下都验证通过。这破坏了不可抵赖性——Alice 签名的文档,攻击者可以声称”这是用我的密钥签的”。
防御方法:在签名中包含公钥的哈希,或使用证书绑定公钥身份。
7.6 攻击防御总览
| 攻击 | 目标 | 防御 | 代码示例 |
|---|---|---|---|
| k 值重用 | ECDSA 私钥恢复 | EdDSA 或确定性 k(RFC 6979) | 见 3.3 节 |
| 存在性伪造 | PKCS#1 v1.5 无消息伪造 | 使用 RSA-PSS | 见 2.3 节 |
| 时序攻击 | 通过签名时间推断私钥 | 常数时间实现 | hmac.compare_digest |
| 故障注入 | 畸改签名计算恢复私钥 | 签名后验证 + 冗余计算 | 签名后 self-verify |
| DSKS | 破坏不可抵赖性 | 证书绑定 + 公钥哈希 | 签名包含公钥指纹 |
| Bleichenbacher | RSA PKCS#1 v1.5 填充预言机 | 使用 RSA-PSS | 见 2.4 节 |
七·附、实践:数字签名与验证
数字签名的概念并非凭空出现。1976 年,Diffie 和 Hellman 在《New Directions in Cryptography》中首次提出”数字签名”的概念——用私钥生成、公钥验证的数学构造,实现手写签名的三大法律属性:真实性(签名者身份可验证)、完整性(签名与文档绑定)、不可抵赖性(签名者无法否认)。1977 年 RSA 诞生,成为第一个实用的数字签名方案;1991 年 NIST 发布 DSA 标准;2000 年代椭圆曲线签名(ECDSA)因更短的密钥和签名被广泛采用;2017 年 Ed25519 作为确定性签名的代表,成为新项目的首选。
从 RSA 到 EdDSA 的演进,核心驱动力是安全性与效率的平衡:RSA 签名需要 2048+ 位密钥和 256 字节签名,而 Ed25519 只需 32 字节密钥和 64 字节签名,提供同等 128 位安全等级。更重要的是,Ed25519 的确定性签名消除了 ECDSA 的随机数 k 重用风险——2010 年 Sony PlayStation 3 就因 ECDSA 的 k 值重用导致私钥泄露。
附.1 前置知识
- Python 3.8+ + cryptography 库(
pip install cryptography) - 理解本章前七节的理论内容(RSA-PSS、ECDSA、EdDSA、证书链)
附.2 RSA-PSS 签名与验证
RSA-PSS 是 RSA 签名的推荐方案,使用概率性签名方案(Probabilistic Signature Scheme)替代不安全的 PKCS#1 v1.5:
from cryptography.hazmat.primitives.asymmetric import rsa, paddingfrom cryptography.hazmat.primitives import hashes
# 生成 RSA-2048 密钥对private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)public_key = private_key.public_key()
message = b"Important document to sign"
# RSA-PSS 签名signature = private_key.sign( message, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256())print(f"签名长度: {len(signature)} bytes")print(f"签名(hex): {signature.hex()[:40]}...")
# 验证签名try: public_key.verify(signature, message, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) print("验证: 通过 ")except Exception as e: print(f"验证: 失败 — {e}")
# 篡改消息后验证tampered = b"Tampered document to sign"try: public_key.verify(signature, tampered, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())except Exception: print("篡改后验证: 失败 — 检测到篡改")签名长度: 256 bytes签名(hex): 50bf2f3a939a87d32759e76256ab4f6d610e8ecd...验证: 通过篡改后验证: 失败 — 检测到篡改注:RSA-PSS 的 salt_length=MAX_LENGTH 表示使用最大允许的 salt 长度,提供最强的安全性。PSS 的概率性意味着同一消息多次签名结果不同。
附.3 ECDSA 签名与验证
ECDSA 使用椭圆曲线提供与 RSA 同等安全性,但密钥和签名更短:
from cryptography.hazmat.primitives.asymmetric import ecfrom cryptography.hazmat.primitives import hashes
ecc_private = ec.generate_private_key(ec.SECP256R1())ecc_public = ecc_private.public_key()
ecdsa_sig = ecc_private.sign(message, ec.ECDSA(hashes.SHA256()))print(f"ECDSA 签名长度: {len(ecdsa_sig)} bytes")print(f"签名(hex): {ecdsa_sig.hex()[:40]}...")
# 验证try: ecc_public.verify(ecdsa_sig, message, ec.ECDSA(hashes.SHA256())) print("验证: 通过 ")except Exception as e: print(f"验证: 失败 — {e}")
# 篡改检测try: ecc_public.verify(ecdsa_sig, tampered, ec.ECDSA(hashes.SHA256()))except Exception: print("篡改后验证: 失败 — 检测到篡改")ECDSA 签名长度: 72 bytes签名(hex): 3046022100ae7eb83486c33e9e3208627785b37d...验证: 通过篡改后验证: 失败 — 检测到篡改注意:ECDSA 每次签名使用不同的随机数 k,因此同一消息多次签名结果不同。如果 k 值重用或可预测,攻击者可以恢复私钥——这就是 Sony PS3 被破解的原因。
附.4 Ed25519 签名与验证
Ed25519 是现代签名算法的首选:确定性签名、常数时间实现、无随机数依赖:
from cryptography.hazmat.primitives.asymmetric import ed25519
ed_private = ed25519.Ed25519PrivateKey.generate()ed_public = ed_private.public_key()
ed_sig = ed_private.sign(message)print(f"Ed25519 签名长度: {len(ed_sig)} bytes (固定 64 bytes)")print(f"签名(hex): {ed_sig.hex()[:40]}...")
# 验证try: ed_public.verify(ed_sig, message) print("验证: 通过 ")except Exception as e: print(f"验证: 失败 — {e}")
# 确定性签名:同一消息两次签名结果相同ed_sig2 = ed_private.sign(message)print(f"两次签名相同: {ed_sig == ed_sig2} (Ed25519 是确定性签名)")Ed25519 签名长度: 64 bytes (固定 64 bytes)签名(hex): d130e75f30fe1ca1cd4c601426042bcf5d175148...验证: 通过两次签名相同: True (Ed25519 是确定性签名)对比 ECDSA 的非确定性签名:
ecdsa_sig2 = ecc_private.sign(message, ec.ECDSA(hashes.SHA256()))print(f"ECDSA 两次签名相同: {ecdsa_sig == ecdsa_sig2}")ECDSA 两次签名相同: False注:Ed25519 的确定性签名消除了 ECDSA 的随机数 k 重用风险,是安全性的根本提升。RFC 6979 为 ECDSA 定义了确定性 k 的生成方法,但 Ed25519 从设计层面就保证了这一点。
附.5 签名算法性能对比
import time
# 性能基准测试(100 次取平均)for name, sign_func, verify_func in [ ("RSA-PSS", rsa_sign, rsa_verify), ("ECDSA P-256", ecdsa_sign, ecdsa_verify), ("Ed25519", ed_sign, ed_verify),]: start = time.perf_counter() for _ in range(100): sig = sign_func() sign_ms = (time.perf_counter() - start) * 10
start = time.perf_counter() for _ in range(100): verify_func(sig) verify_ms = (time.perf_counter() - start) * 10
print(f"{name:15s}: 签名 {sign_ms:.2f} ms, 验证 {verify_ms:.2f} ms")RSA-PSS : 签名 0.80 ms, 验证 0.04 ms, 签名长度 256 bytesECDSA P-256 : 签名 0.03 ms, 验证 0.10 ms, 签名长度 71 bytesEd25519 : 签名 0.04 ms, 验证 0.11 ms, 签名长度 64 bytes附.6 实践小结
| 算法 | 密钥大小 | 签名大小 | 签名速度 | 验证速度 | 确定性 | 安全性 | 推荐场景 |
|---|---|---|---|---|---|---|---|
| RSA-PSS 2048 | 256 B | 256 B | 慢 | 快 | 否 | 112 位 | 兼容旧系统 |
| ECDSA P-256 | 32 B | 64 B | 快 | 中 | 否(需 RFC 6979) | 128 位 | TLS 证书 |
| Ed25519 | 32 B | 64 B | 快 | 中 | 是 | 128 位 | 新项目首选 |
关键教训:
- 新项目首选 Ed25519:确定性签名消除随机数风险,64 字节签名节省带宽
- RSA-PSS 仅用于兼容:256 字节签名和密钥在现代系统中是不必要的开销
- 永远不要使用 PKCS#1 v1.5:存在性伪造攻击(Bleichenbacher)使其不安全
- 签名后必须验证篡改:每个签名方案都应展示”签名→验证→篡改检测”的完整流程
八、算法选择与总结
8.1 算法选择决策表
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 新项目通用签名 | Ed25519 | 确定性、快速、安全 |
| 需要兼容旧系统 | RSA-PSS 2048+ | 广泛支持 |
| TLS 证书 | RSA-PSS 或 ECDSA | CA 决定 |
| JWT 签名 | EdDSA (Ed25519) | 短签名、快速验证 |
| 代码签名 | RSA-PSS 4096 或 ECDSA P-384 | 高安全等级 |
| 区块链 | Ed25519 或 secp256k1 | 生态决定 |
| 智能卡/HSM | ECDSA P-256 | 硬件支持好 |
8.2 密钥长度与安全等级
| 算法 | 密钥大小 | 签名大小 | 安全等级 | NIST 推荐 |
|---|---|---|---|---|
| RSA-2048 | 256 字节 | 256 字节 | 112 位 | 2030 年前 |
| RSA-3072 | 384 字节 | 384 字节 | 128 位 | 长期 |
| RSA-4096 | 512 字节 | 512 字节 | 150 位 | 长期 |
| ECDSA P-256 | 32 字节 | 64 字节 | 128 位 | 长期 |
| ECDSA P-384 | 48 字节 | 96 字节 | 192 位 | 长期 |
| Ed25519 | 32 字节 | 64 字节 | 128 位 | 长期 |
8.3 关键要点
| 维度 | 关键要点 |
|---|---|
| RSA-PSS | RSA 签名推荐方案,可证明安全,替代 PKCS#1 v1.5 |
| ECDSA | 椭圆曲线签名,需好的随机数(或 RFC 6979) |
| EdDSA | 确定性签名,最安全最简单,新项目首选 |
| 证书链 | Root CA → Intermediate → End-Entity,信任传递 |
| 签名流程 | 先哈希后签名,验证证书链,保护私钥 |
| 攻击防御 | 确定性签名、PSS 填充、常数时间、故障检测 |
后量子密码(如 Dilithium、Falcon)正在标准化中。NIST 已在 2024 年正式发布后量子签名标准,但大规模部署仍需时间。详见 后量子密码。
上一章探讨了哈希函数与消息认证码。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






