🏠 总目录📚 本教程 17 · 数字签名 ← →
📑 本页目录(点开跳转)

17 · 数字签名

⏱ 30 分钟 | ⭐ 唯一能提供"不可否认"的工具


🎯 一句话

MAC 能证明"消息没被改",但验证者自己也能伪造。 数字签名用私钥签、公钥验 —— 只有一个人能签,全世界都能验,而且他事后赖不掉。


🖋️ 一、定义与安全目标

信息关系

Gen()→(pk, sk)
Sign(sk, m)→σ
Verify(pk, m, σ)→0/1

安全定义:UF-CMA(选择消息攻击下的存在性不可伪造)

对照

攻击者拿到 pk,可以让你签【任意多条】他挑的消息,

仍然无法为【一条没签过的消息】造出合法签名 ⭐

⚠️ 一个更强的要求:SUF-CMA(强不可伪造) 连"为已签过的消息造出另一个不同的合法签名"都不行。 💡 为什么重要:如果签名可延展(malleable), 区块链上就能改变交易 ID —— 这就是比特币著名的 交易延展性漏洞, Mt. Gox 曾以此为破产理由之一。

MAC 数字签名
密钥 对称(双方共享) 非对称
谁能验证 只有持密钥的人 任何人 ⭐
不可否认 ❌(验证者也能伪造) ✅
速度 快(微秒) 慢(毫秒)
签名长度 16–32 字节 64–384 字节

🔑 二、RSA 签名

❌ 教科书版本

操作步骤

σ = M^d mod n,验证 σ^e ≟ M
💥 三个致命问题:
① 存在性伪造:随便选一个 σ,算 M = σ^e
(M, σ) 就是一个合法的"签名对"!攻击者无需私钥 ⭐
② 同态伪造:σ₁·σ₂ 是 M₁·M₂ 的合法签名
③ 只能签比 n 短的消息

✅ 必须先哈希再填充

关键信息

💥 PKCS#1 v1.5 签名的 Bleichenbacher'06 攻击: 如果验证实现没有严格检查填充结构(只是"找到 hash 就通过"), e=3 时攻击者可以伪造签名 —— 影响过 OpenSSL、Firefox、多个 JOSE/JWT 库。


📐 三、DSA / ECDSA 与那个致命的 k

操作步骤

  1. 签名(ECDSA):
  2. 选随机 k
  3. r = (kG).x mod n
  4. s = k⁻¹(H(m) + r·d) mod n ← d 是私钥
  5. 签名 = (r, s)

💀 k 的三条铁律

对照

⭐ ① k 重复 ⟹ 私钥直接泄露

两个签名用了同一个 k:

s₁ = k⁻¹(H₁ + rd)

s₂ = k⁻¹(H₂ + rd)

⟹ s₁ - s₂ = k⁻¹(H₁ - H₂)

⟹ k = (H₁ - H₂)/(s₁ - s₂) ← 解出 k

⟹ d = (s₁k - H₁)/r ← 解出私钥 💀

⭐ ② k 部分可预测 ⟹ 用格约减(LLL)也能恢复私钥

(只要泄露每个 k 的几个 bit,几十个签名就够)

⭐ ③ k 泄露 ⟹ 单个签名就能算出私钥

💥 真实事故: - PlayStation 3(2010):Sony 把 k 写成固定常数 → 私钥被算出 → 任何人都能签发"官方"固件,主机被彻底越狱 ⭐ - Android 比特币钱包(2013):SecureRandom 缺陷导致 k 重复 → 私钥被算出,钱被盗 - 多个硬件钱包 / IoT 设备:熵不足导致 k 可预测

✅ 解法:确定性签名

关键信息

RFC 6979:k = HMAC(私钥, H(消息))
【完全不需要随机数】,同一消息总是产生同一签名 ⭐
熵源坏掉也不会泄露私钥
Ed25519 内建这个设计 ⟹ 这也是推荐它的重要原因

🔑 这是一个漂亮的工程教训: 把"需要好随机数"这个脆弱的依赖,替换成"需要一个好哈希" —— 后者可靠得多。


🎩 四、Schnorr 签名与 Fiat–Shamir

Schnorr 签名结构极其简洁:

操作步骤

  1. 公钥 y = g^x
  2. 签名:
  3. 选随机 r,算承诺 t = g^r
  4. 挑战 c = H(m ‖ t) ⭐ 用哈希代替"验证者发挑战"
  5. 响应 s = r + c·x
  6. 签名 = (t, s)
  7. 验证:g^s ≟ t · y^c
📐 验证为什么成立(想看再点)

$$g^s = g^{r + cx} = g^r \cdot g^{cx} = t \cdot (g^x)^c = t\cdot y^c \quad\checkmark$$

⭐ Fiat–Shamir 转换:一个通用的魔法

信息关系

原本:一个【交互式】身份认证协议
(证明者发承诺→验证者发随机挑战→证明者响应)
转换:把"验证者发的随机挑战"替换成 c = H(消息 ‖ 承诺)
变成【非交互式】的签名 ⭐
💡 为什么安全:哈希的输出不可预测,
证明者无法"先知道挑战再准备承诺"

🔑 这个转换是密码学里最有用的工具之一: 任何交互式的零知识证明,都能用它变成一个签名或非交互式证明。 🔗 第 21 章会看到它是 zk-SNARK 的基础。

⚠️ Fiat–Shamir 的经典实现坑: 哈希必须把承诺 t 和消息 m 都包含进去。 只哈希消息(漏掉 t)会导致可伪造 —— 这被称为 "weak Fiat–Shamir", 2023 年还在多个 zk 库里被发现。

Schnorr 的优势

优势 说明
可证明安全 在 ROM 下从离散对数归约,比 ECDSA 干净
线性结构 ⭐ 支持多签聚合(MuSig)、门限签名
签名短 64 字节

💡 比特币 2021 年的 Taproot 升级引入了 Schnorr 签名, 主要动机就是多签可以聚合成一个签名 —— 省空间,还提高隐私。


🔍 五、实践中的选型

算法 签名长度 特点
Ed25519 ⭐ 64 B 首选。快、确定性、无 k 值风险
ECDSA P-256 64 B 兼容性好,但 k 值风险高
RSA-PSS 3072 384 B 遗留系统兼容
BLS 48 B ⭐ 可聚合:一千个签名合成一个(第 16 章配对)
ML-DSA ~2.4 KB 抗量子(第 23 章)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

sk = Ed25519PrivateKey.generate()
sig = sk.sign(b"message")                    # ⭐ 64 字节,确定性
sk.public_key().verify(sig, b"message")      # 失败抛 InvalidSignature

⚠️ 六、签名之外的三个协议层问题

流程图

签名验证通过 ≠ 这条消息可以被接受
① 重放:老签名仍然合法→需要序号/时间戳(第 11 章)
② 上下文混淆:A 场景的签名被拿到 B 场景用
✅ 解法:【域分隔】—— 签名前加前缀 "myapp:v1:transfer:" ⭐
③ 密钥复用:同一密钥既签名又加密→可能互相打穿
✅ 解法:不同用途用不同密钥

💥 上下文混淆的真实例子:某些钱包用同一个密钥签"登录挑战"和"转账交易", 攻击者把一个伪装成登录请求的转账交易发给用户签名 → 钱直接没了。 域分隔能完全堵住这类攻击。


🔗 和站内其他章的关系

相关的地方 和这一章的关系
第 11 章 MAC 签名是它的非对称版,多了不可否认
第 14 章 RSA 同态性 教科书签名可伪造的原因
第 9 章 抗碰撞 碰撞 ⟹ 能把签名"搬到"另一条消息上 ⭐
第 4 章 随机数 k 值事故的根因
第 21 章 ZKP Fiat–Shamir 是共用的桥梁
第 18 章 PKI 证书就是 CA 的签名
模型上线之后 · 合规审计与模型卡 那边列的「审计要能回答的五个问题」(决策怎么做的 / 用了哪些数据 / 有没有对某群体不利 / 怎么发现和纠正 / 谁负责)全都默认记录本身是可信的——而「记录有没有被事后改过」它没有回答,数据库时间戳也答不了,只有签名 + 哈希链答得了 ⭐
Claude 资料库 · AI 原生 SDLC 安全 供应链信任的地基:签名的镜像、签名的权重、签名的依赖;⚠️ 但签名只保证「这是我发的」,不保证「这是好的」

✅ 检查点

  1. 数字签名和 MAC 的三个关键区别?
  2. UF-CMA 和 SUF-CMA 的区别?后者为什么重要?
  3. 教科书 RSA 签名的三个问题?
  4. FDH 怎么解决其中两个?
  5. ECDSA 中 k 重复会怎样?写出推导。
  6. RFC 6979 的做法是什么?它体现了什么工程思想?
  7. Fiat–Shamir 转换做了什么?它的经典实现坑是什么?
  8. Schnorr 签名相比 ECDSA 的三个优势?比特币为什么引入它?
  9. 什么是域分隔?它防的是什么攻击?
👀 答案
  1. ①MAC 对称、签名非对称 ②MAC 只有持密钥者能验,签名任何人都能验 ③MAC 不提供不可否认(验证者也能伪造),签名提供。
  2. UF-CMA:无法为没签过的消息造合法签名。SUF-CMA:连"为已签过的消息造出另一个不同的合法签名"都不行。重要是因为签名可延展会改变区块链上的交易 ID——比特币的交易延展性漏洞。
  3. ①存在性伪造:随便选 σ 算 M=σ^e 就得到合法签名对 ②同态伪造:σ₁·σ₂ 是 M₁·M₂ 的签名 ③只能签比 n 短的消息。
  4. σ = H(M)^d:哈希破坏同态性(H(M₁M₂)≠H(M₁)H(M₂)),且反算的 σ^e 几乎不可能恰好是某消息的哈希;顺带解决了长度限制。
  5. s₁ = k⁻¹(H₁+rd),s₂ = k⁻¹(H₂+rd) ⟹ s₁−s₂ = k⁻¹(H₁−H₂) ⟹ k = (H₁−H₂)/(s₁−s₂) ⟹ d = (s₁k−H₁)/r,私钥直接泄露。
  6. k = HMAC(私钥, H(消息))——完全不需要随机数,同一消息总产生同一签名。思想:把"需要好随机数"这个脆弱依赖,替换成"需要一个好哈希"。
  7. 把交互式协议里验证者发的随机挑战替换成 c = H(消息‖承诺),变成非交互式签名。坑:哈希必须同时包含承诺 t 和消息 m,漏掉 t 就是 "weak Fiat–Shamir",可伪造——2023 年仍在多个 zk 库中被发现。
  8. ①可证明安全(ROM 下从 DLP 干净归约)②线性结构支持多签聚合和门限签名 ③签名短。比特币 Taproot 引入它主要为了多签聚合成一个签名——省空间且提高隐私。
  9. 签名前加上用途前缀(如 "myapp:v1:transfer:")。防上下文混淆——A 场景的签名被拿到 B 场景用。真实例子:钱包用同一密钥签"登录挑战"和"转账交易",攻击者把转账伪装成登录请求让用户签。

🛑 可以停在这里

⚡ 走神救援

签名是私钥签、公钥验,⭐ 唯一提供不可否认的工具——MAC 的验证者也持密钥,所以他能伪造。

除了「造不出新消息的签名」,还有更强的一档:连给已经签过的消息再造一个不同的签名都不行。⭐ 它重要是因为签名可延展会改变交易 ID——历史上真出过事。

教科书 RSA 签名三宗罪里最刺眼的是 ⭐ 存在性伪造:随便挑一个数反算回去,就得到一对合法的「消息+签名」,全程不需要私钥。 ✅ 所以必须先哈希——它同时破坏了同态性,并让反算出来的东西几乎不可能正好是某条消息的哈希。

💀⭐ ECDSA 那个一次性随机数的三条铁律:重复使用会直接解出私钥;部分可预测时用格约减、几十个签名就能恢复;泄露则单个签名即破。💥 历史上游戏机被越狱、钱包被盗都栽在这里。 ✅ ⭐ 处方是确定性签名:随机数由私钥和消息哈希算出来,完全不需要随机源——把「需要好随机数」这个脆弱依赖换成了「需要好哈希」。

⭐ Schnorr 那条线通向别处:把验证者的随机挑战换成哈希,任何交互式零知识证明都能变成签名。⚠️ 但哈希必须同时包含承诺和消息——漏掉承诺是一个真实存在、且近年仍在库里被发现的漏洞。它的另外两个好处是可证明安全和⭐ 线性结构支持多签聚合。

⚠️ 协议层三个问题里最容易忽略的是 ⭐ 上下文混淆——真实事故是钱包用同一把密钥既签登录又签转账。防御是域分隔:签之前先加一个写明用途的前缀。

下一节 👉 18-PKI与证书.md

打卡记录保存在你的浏览器里,首页能看到总进度