数字签名¶
本文你会学到:
- 为什么加密能保护机密性,但无法证明"这条消息确实是你发的"
- 数字签名如何像"手写签名"一样同时提供身份认证和不可否认性
- 签名安全强度的"木桶短板"原则——摘要算法和公钥算法如何互相制约
- Java
Signature类的标准使用模式(初始化→更新→签名/验签) - Hash-and-Sign 范式为什么必须先哈希再签名,以及 FDH 方案的理论意义
- DSA、ECDSA、EdDSA、RSA 签名、SM2 国密签名各自的原理和 Java API 用法
- 在实际项目中如何选择合适的签名算法
🤔 为什么需要数字签名?¶
你已经知道,对称加密解决了**机密性**问题,HMAC 解决了**完整性 + 认证**问题。但 HMAC 有一个根本限制——通信双方必须共享同一个密钥。如果你收到一条消息,HMAC 能告诉你"这条消息确实来自持有密钥的人",但它无法帮你向第三方证明"这条消息来自 Alice 而不是 Bob"——因为 Alice 和 Bob 共享同一个密钥,谁都能生成有效的 HMAC。
更关键的问题是**不可否认性(non-repudiation)**:Alice 签了一份电子合同后声称"我没签过",你怎么证明?HMAC 做不到——因为密钥是共享的,你无法排除 Bob 伪造的可能性。
💡 想象现实中的手写签名:你的笔迹别人难以模仿,所以签名可以作为身份证明。数字签名在密码学世界里扮演同样的角色——只有持有**私钥**的人才能生成签名,而任何人都可以用**公钥**验证签名的真实性。
数字签名的安全模型:EUF-CMA¶
与 MAC 类似,数字签名的安全性也用 EUF-CMA(Existential Unforgeability under Chosen Message Attack)来定义。区别在于:MAC 使用共享密钥,数字签名使用公私钥对。
攻击游戏:
- 挑战者生成密钥对 \((pk, sk)\),将 \(pk\) 发送给攻击者
- 攻击者可以请求任意消息的签名(chosen message attack)
- 攻击者输出一个新消息和对应的签名——如果用 \(pk\) 验证通过则攻击者获胜
数字签名方案被定义为三元组 \(\mathcal{S} = (\text{G}, \text{S}, \text{V})\):
- 密钥生成 G:输出 \((pk, sk)\)
- 签名 S:用私钥 \(sk\) 对消息签名
- 验证 V:用公钥 \(pk\) 验证签名
💡 Java Signature 类的 initSign()/update()/sign() 对应 \(\text{S}\),initVerify()/verify() 对应 \(\text{V}\),而密钥对由 KeyPairGenerator 生成(对应 \(\text{G}\))。
数字签名提供三个核心保证:
- 身份认证(authentication):只有私钥持有者能生成有效签名
- 完整性(integrity):消息被篡改后签名验证会失败
- 不可否认性(non-repudiation):签名者无法否认自己签署过这条消息
sequenceDiagram
participant Alice as Alice(签名者)
participant Bob as Bob(验证者)
participant Eve as Eve(攻击者)
Note over Alice: 持有私钥,生成签名
Alice->>Bob: 消息 + 数字签名
Note over Bob: 持有公钥,验证签名
Bob->>Bob: 验证签名通过 ✅
Note over Eve: 截获消息和签名
Eve->>Bob: 篡改消息 + 原始签名
Bob->>Bob: 验证签名失败 ❌
Eve->>Bob: 原始消息 + 伪造签名
Bob->>Bob: 验证签名失败 ❌(没有私钥)
数字签名分为两大类:
- 确定性签名(deterministic):同一私钥对同一消息,每次产生的签名完全相同(如 RSA PKCS#1 v1.5)
- 非确定性签名(non-deterministic):每次签名都会引入随机数,同一消息的签名每次不同(如 DSA、ECDSA)
🛡️ 签名安全强度¶
当你需要选择签名算法的密钥长度和摘要算法时,你会发现一个容易被忽视的问题:签名的安全强度由多个组件共同决定,其中最弱的那个组件决定了整体安全性。
💡 把签名想象成一个链条——摘要算法、公钥算法、密钥长度是链条上的每个环节。链条的强度取决于最薄弱的环节,而不是最强的那个。
强不可伪造性¶
EUF-CMA 只要求攻击者不能为**新消息**伪造签名。但更严格的安全定义还要求:即使在**已签名的消息**上,攻击者也不能生成新的有效签名。
两种额外攻击需要防御:
- Message Confusion:攻击者找到一个新消息 \(m'\) 使得已有的签名 \(\sigma\) 也是 \(m'\) 的合法签名
- Signer Confusion:攻击者找到另一对公私钥 \((pk', sk')\) 使得 \(\sigma\) 是 \(pk'\) 下对 \(m\) 的合法签名
💡 SHA-256withRSA 的 "with" 语法确保哈希算法是固定的,防止消息混淆攻击——验证者知道应该用 SHA-256 计算摘要,攻击者无法利用不同哈希算法的结果来构造合法签名。
NIST SP 800-57 定义了不同算法的安全强度:
摘要算法安全强度¶
| 摘要算法 | 安全强度(位) |
|---|---|
| SHA-1 | ≤ 80 |
| SHA-224, SHA-512/224, SHA3-224 | 112 |
| SHA-256, SHA-512/256, SHA3-256 | 128 |
| SHA-384, SHA3-384 | 192 |
| SHA-512, SHA3-512 | 256 |
公钥算法安全强度¶
| 算法 | 密钥长度 | 安全强度(位) |
|---|---|---|
| DSA / RSA | 1024 | ≤ 80 |
| DSA / RSA | 2048 | 112 |
| DSA / RSA | 3072 | 128 |
| ECDSA | 160-223 | ≤ 80 |
| ECDSA | 224-255 | 112 |
| ECDSA | 256-383 | 128 |
| ECDSA | 384-511 | 192 |
| ECDSA | 512+ | 256 |
木桶短板原则¶
选择签名组件时,遵循一条通用规则:摘要算法的安全强度应不低于公钥算法。同样,如果签名方案涉及掩码生成函数(MGF),MGF 中使用的摘要强度也不应低于签名摘要的强度。
| 推荐组合 | 公钥算法 | 密钥长度 | 摘要算法 | 安全强度 |
|---|---|---|---|---|
| 入门级 | RSA | 2048 | SHA-256 | 112 |
| 推荐级 | RSA | 3072 | SHA-256 | 128 |
| 推荐级 | ECDSA | P-256 | SHA-256 | 128 |
| 高安全 | ECDSA | P-384 | SHA-384 | 192 |
| 极高安全 | RSA | 15360 | SHA-512 | 256 |
⚠️ 不要用 SHA-1 生成新签名。虽然验证旧的 SHA-1 签名在大多数场景下仍然可行,但生成新的 SHA-1 签名已被 FIPS 禁止,存在安全隐患。
☕ Signature 类¶
JCA 中,签名操作统一由 java.security.Signature 类提供。和 JCA 的其他引擎类一样,Signature 对象通过 getInstance() 工厂方法创建,而不是直接构造。
算法命名规范¶
签名算法的标准命名格式为:
例如:
| 算法名称 | 含义 |
|---|---|
SHA256withDSA |
DSA 签名 + SHA-256 摘要 |
SHA256withECDSA |
ECDSA 签名 + SHA-256 摘要 |
SHA256withRSA |
RSA PKCS#1 v1.5 签名 + SHA-256 摘要 |
SHA256withRSAandMGF1 |
RSA-PSS 签名 + SHA-256 摘要 |
EdDSA |
EdDSA 签名(摘要算法由密钥类型决定) |
SM3withSM2 |
SM2 签名 + SM3 摘要 |
标准使用模式¶
无论哪种签名算法,Signature 的使用都遵循三个步骤:
graph LR
A[1. 初始化<br/>initSign / initVerify] --> B[2. 传入数据<br/>update]
B --> C["3. 签名 / 验证<br/>sign / verify"]
签名生成(私钥操作):
| Signature 类签名流程 | |
|---|---|
签名验证(公钥操作):
| Signature 类验证流程 | |
|---|---|
部分签名算法还需要调用 setParameter() 设置额外参数(如 RSA-PSS 的盐值长度、SM2 的用户 ID),这些将在对应章节中介绍。
🔐 DSA(Digital Signature Algorithm)¶
DSA(Digital Signature Algorithm,数字签名算法)诞生于 1991 年,是 NIST 发布的第一个数字签名标准(FIPS PUB 186),至今仍是许多安全标准的基石。
离散对数问题¶
当你需要对一段数据"签名"时,你面临一个核心挑战:如何在公开公钥的前提下,让签名只与私钥相关?DSA 的答案来自**离散对数问题(Discrete Logarithm Problem,DLP)**。
💡 想象你有一个大质数 P,以及一个生成元 G。已知 Y = G^X mod P 和公钥 Y,要推导出私钥 X 在计算上是不可行的——这就是离散对数问题。DSA 的安全性正是建立在这个难题之上。
DSA 参数:P、Q、G¶
DSA 密钥基于一组公开参数 (P, Q, G):
- P:大质数,位长从 {1024, 2048, 3072} 中选择
- Q:P - 1 的质因数,位长从 {160, 224, 256} 中选择(取决于 P 的位长)
- G:GF(P) 乘法群中阶为 Q 的生成元,满足 1 < G < P
在 Java 中,这些参数通过 java.security.spec.DSAParameterSpec 携带。参数可以通过 AlgorithmParameterGenerator 生成,也可以在密钥生成时自动创建。
密钥生成¶
DSA 密钥对包含一个私钥 X 和公钥 Y:
- X:随机生成,满足 0 < X < Q
- Y = G^X mod P
| 生成 2048 位 DSA 密钥对 | |
|---|---|
注意:生成 DSA 参数(P/Q/G)是一个较慢的操作。如果性能敏感,可以预先生成参数并复用。
签名流程¶
DSA 签名由两个整数 R 和 S 组成,计算步骤如下:
- 生成随机数 K,满足 0 < K < Q
- 计算 R = (G^K mod P) mod Q
- 计算 S = K^(-1) * (H(m) + X*R) mod Q
其中 H(m) 是消息 m 的哈希值。
graph TD
A["生成随机数 K<br/>0 < K < Q"] --> B["计算 R = (G^K mod P) mod Q"]
B --> C["计算 S = K⁻¹ × (H(m) + X×R) mod Q"]
C --> D["签名 = (R, S)"]
style A fill:transparent,stroke:#f57c00,color:#adbac7
style B fill:transparent,stroke:#0288d1,color:#adbac7
style C fill:transparent,stroke:#0288d1,color:#adbac7
style D fill:transparent,stroke:#388e3c,color:#adbac7
⚠️ 随机数 K 的安全性至关重要:如果 K 被泄露或不够随机,攻击者可以直接推导出私钥 X。历史上著名的索尼 PS3 破解事件就是因为 K 值没有随机化。如果你无法确保良好的随机数生成器,应该使用确定性 DSA(RFC 6979)。
Java 代码示例¶
signature.sign() 返回的字节数组是 R 和 S 两个整数的 ASN.1 DER 编码(SEQUENCE 包含两个 INTEGER)。由于 ASN.1 INTEGER 是有符号的,签名长度可能有 1 字节的差异。
确定性 DSA(RFC 6979)¶
当 DSA 的随机数 K 不够安全或不可用时,RFC 6979 提供了一种确定性方案——从私钥和消息哈希中计算出 K,而非依赖随机数生成器。这样同一私钥对同一消息总是产生相同的签名,同时 K 的安全性等同于私钥。
在 Bouncy Castle 中,确定性 DSA 的算法名称为 DDSA(普通 DSA)和 ECDDSA(椭圆曲线 DSA)。注意:确定性签名可以由标准 DSA/ECDSA 验证器验证,无需特殊处理。
📈 ECDSA(Elliptic Curve DSA)¶
当你需要与 DSA 相同安全强度但更短的密钥时,你会发现在椭圆曲线上实现 DSA——即 ECDSA(Elliptic Curve DSA,椭圆曲线数字签名算法)——是一个更好的选择。
为什么用椭圆曲线?¶
传统 DSA 使用大整数域,2048 位密钥提供 112 位安全强度。ECDSA 使用椭圆曲线数学,256 位密钥就能达到 128 位安全强度——密钥更短,计算更快,签名更小。
💡 把传统 DSA 想象成在数论世界里做乘方运算(大数域),把 ECDSA 想象成在几何曲线上做"加法"运算(椭圆曲线域)。两者的安全基础都是"离散对数问题",但椭圆曲线的离散对数问题更难解,因此可以用更短的密钥达到同等安全强度。
曲线命名¶
ECDSA 定义在两种有限域上:
- GF(p):基于大质数的域,FIPS 曲线以
P-开头(如 P-256、P-384) - GF(2^m):基于不可约多项式的域,以
B-或K-(Koblitz 曲线)开头
常用的曲线及其别名:
| FIPS 名称 | SEC 名称 | 密钥长度 | 安全强度 |
|---|---|---|---|
| P-256 | secp256r1 / prime256v1 | 256 位 | 128 位 |
| P-384 | secp384r1 | 384 位 | 192 位 |
| P-521 | secp521r1 | 521 位 | 256 位 |
密钥生成与签名¶
ECDSA 的密钥生成和签名流程与传统 DSA 类似,只是数学运算从大整数域换到了椭圆曲线域:
- 私钥 d:随机生成,满足 0 < d < N(N 是曲线基点 G 的阶)
- 公钥 Q = d × G(椭圆曲线点乘)
签名步骤:
- 生成随机数 K,满足 0 < K < N
- 计算 (x, y) = K × G
- R = x mod N(如果 R == 0,重新生成 K)
- S = K^(-1) × (H(m) + d × R) mod N
DSA vs ECDSA 对比¶
| 特性 | DSA | ECDSA |
|---|---|---|
| 数学基础 | 大整数离散对数问题 | 椭圆曲线离散对数问题 |
| 128 位安全所需密钥长度 | 3072 位 | 256 位 |
| 签名大小 | ~56 字节(DER 编码) | ~72 字节(DER 编码) |
| 签名确定性 | 非确定性(随机 K) | 非确定性(随机 K) |
| 确定性变体 | DDSA(RFC 6979) | ECDDSA(RFC 6979) |
| 参数生成速度 | 慢(大质数搜索) | 快(使用预定义曲线) |
| 算法名称 | SHA256withDSA |
SHA256withECDSA |
⚡ EdDSA(Edwards Curve DSA)¶
当你需要一种**天然确定性**且**抗侧信道攻击**的签名方案时,传统 DSA/ECDSA 的随机数依赖就成了一个负担。EdDSA(Edwards Curve Digital Signature Algorithm)通过巧妙的数学设计彻底消除了这个问题。
什么是 Edwards 曲线?¶
Edwards 曲线是椭圆曲线的一种特殊形式,具有数学上更"整齐"的运算性质。EdDSA 使用的具体曲线有两种:
| 曲线 | 安全强度 | 内置摘要 | 签名长度 |
|---|---|---|---|
| Ed25519 | ~128 位 | SHA-512 | 64 字节(R: 32 + S: 32) |
| Ed448 | ~224 位 | SHAKE-256 | 114 字节(R: 57 + S: 57) |
EdDSA 的三大优势¶
- 确定性签名:不依赖随机数生成器,同一私钥对同一消息总是产生相同签名。K 值从私钥和消息中确定性推导
- 抗侧信道攻击:Edwards 曲线的运算设计天然抵抗时序攻击和功耗分析攻击
- 签名恒定长度:Ed25519 签名固定 64 字节,Ed448 固定 114 字节,不会像 DSA 那样因 ASN.1 编码出现 1 字节的差异
Java 代码示例¶
EdDSA 的使用非常简洁——算法名称统一为 EdDSA,具体使用哪条曲线由密钥类型决定:
如果需要锁定到特定曲线,也可以直接使用 Ed25519 或 Ed448 作为算法名称。
⚠️ Ed25519 有多种变体(纯 Ed25519、带 context 的 Ed25519ctx、带预哈希的 Ed25519ph),不同变体的签名不能交叉验证。在 Java 中使用 Bouncy Castle 时,
EdDSA算法名称默认使用纯 Ed25519。
Ed25519 vs ECDSA:现代签名的事实标准¶
你已经知道 ECDSA 和 Ed25519 都是椭圆曲线签名方案,但选哪个?答案几乎总是:新项目用 Ed25519。理由藏在它们核心设计的最大差异里。
随机 nonce:ECDSA 的阿喀琉斯之踵
ECDSA 签名需要一个每次都不同的随机数 k(也叫 nonce)。如果 k 被重复使用或可预测,攻击者可以直接推导出私钥——只需两个共享 k 的签名就足够了。
💡 这不是理论威胁。2010 年,索尼 PlayStation 3 的固件签名系统因为将 k 硬编码为常量,导致攻击者提取了私钥,PS3 随后被完全破解。ECDSA 是一把"如果 k 安全则无懈可击,如果 k 出问题则彻底崩溃"的算法。
graph TD
A["ECDSA 签名<br/>需要随机 k"] --> B{k 是否安全?}
B -- "k 重复或可预测" --> C["攻击者可推导私钥 ❌"]
B -- "k 真随机且唯一" --> D["签名安全 ✅"]
style A fill:transparent,stroke:#0288d1,color:#adbac7
style B fill:transparent,stroke:#f57c00,color:#adbac7
style C fill:transparent,stroke:#d32f2f,color:#adbac7
style D fill:transparent,stroke:#388e3c,color:#adbac7
Ed25519 的确定性设计
Ed25519 从根本上消除了这个问题:nonce 不再是随机数,而是从私钥和消息共同哈希**确定性推导**得出。即使在随机数生成器质量不佳的嵌入式环境中,Ed25519 依然安全。
| 维度 | ECDSA(P-256) |
Ed25519 |
|---|---|---|
| nonce 来源 | 随机数生成器(每次不同) | 确定性推导(私钥 + 消息) |
| nonce 重用风险 | 私钥泄露(致命) | 无风险(nonce 无法重用) |
| 签名大小 | ~72 字节(DER 编码) | 64 字节(固定) |
| 侧信道抗性 | 一般(需额外防护) | 优秀(曲线运算天然常量时间) |
| 安全基础 | 无正式安全证明 | 基于 Schnorr(有安全归约) |
| 兼容性 | 极广泛(TLS、JWT、代码签名) | 越来越广泛(SSH、TLS 1.3、JWT) |
📌
Ed25519本质上是Schnorr签名在 Edwards25519 曲线上的实例化,而ECDSA是为绕过 Schnorr 专利(2008 年到期)而设计的"扭曲变体"。专利到期后,EdDSA终于可以直接继承 Schnorr 的所有优良性质。
🔧 Hash-and-Sign 范式与 FDH 签名方案¶
为什么必须先哈希再签名?¶
考虑一个"裸签名"方案:签名 = \(m^d \bmod n\)。这种方案有一个致命缺陷:任何人都可以伪造签名,不需要知道私钥。攻击者只需选择一个随机签名 \(\sigma\),然后计算 \(m = \sigma^e \bmod n\)——\(m\) 就是用公钥 \(e\) 验证通过的"消息"。
这就是**Hash-and-Sign 范式**被发明的原因:
先用哈希函数将任意长消息压缩为固定长度的摘要,再对摘要签名。安全性依赖碰撞抗性:如果 \(H\) 是碰撞安全的且 \(\text{S}\) 是安全的,则 \(\text{S}'\) 也是安全的。
FDH:Full Domain Hash¶
FDH(Full Domain Hash) 是 Hash-and-Sign 范式的理论实例化:签名 = \(H(m)^{-1} \bmod n\)(在陷门置换下求逆)。\(H\) 被视为随机预言(random oracle),将消息映射到陷门置换的定义域内。
FDH 在随机预言模型下的安全归约为:
其中 \(Q_\text{ro}\) 是哈希查询次数,\(\text{OWadv}\) 是底层的单向函数安全优势。RSA-FDH 利用 RSA 的随机自归约性获得了更紧的界:\(\text{SIGadv} \leq 2.72 \cdot (Q_s + 1) \cdot \text{RSAadv}\)。
⚠️ 无哈希的 RSA 签名还面临**盲签名攻击**:攻击者选择随机数 \(r\),计算 \(m' = m \cdot r^e \bmod n\) 让签名者签名,得到 \(\sigma' = (m')^d \bmod n = \sigma \cdot r \bmod n\),再除以 \(r\) 就得到 \(m\) 的合法签名——签名者完全不知道自己签了什么消息。这就是为什么 RSA 签名必须使用填充方案(PKCS#1 v1.5 或 PSS),而不是直接对消息求逆。
💡 SHA256withRSA 就是 Hash-and-Sign 的实例化。RSA-PSS 比 PKCS#1 v1.5 更接近 FDH 的理论安全保证,这也是 NIST 推荐 RSA-PSS 的原因之一。
Schnorr 签名与聚合签名(MuSig)¶
在「EdDSA(Edwards Curve DSA)」中你已经遇到了 Schnorr 的身影——EdDSA 正是基于它构建的。Schnorr 本身有一个 ECDSA 无法做到的独特性质:线性性(linearity),这使得多方签名聚合成为可能。
Schnorr 的线性性质
Schnorr 签名在数学上满足"加法可交换":多个签名者各自生成的签名分量可以直接相加,合并成一个与单个签名**大小完全相同**的聚合签名。验证时,聚合公钥也可以同样相加。这个性质被称为**签名聚合(Signature Aggregation)**。
graph LR
A["签名者 Alice<br/>(R_A, s_A)"] --> D
B["签名者 Bob<br/>(R_B, s_B)"] --> D
C["签名者 Carol<br/>(R_C, s_C)"] --> D
D["聚合签名<br/>(R_A+R_B+R_C, s_A+s_B+s_C)"] --> E["验证者<br/>一次验证 ✅"]
style A fill:transparent,stroke:#0288d1,color:#adbac7
style B fill:transparent,stroke:#0288d1,color:#adbac7
style C fill:transparent,stroke:#0288d1,color:#adbac7
style D fill:transparent,stroke:#388e3c,color:#adbac7
style E fill:transparent,stroke:#7b1fa2,color:#adbac7
MuSig:安全的多方聚合协议
直接将 Schnorr 签名相加存在安全问题:恶意签名者可以声称自己的公钥为 pk_evil = pk_evil_raw - pk_alice,从而"抵消"其他人的公钥,独自控制聚合密钥——这被称为 rogue-key attack(流氓密钥攻击)。
MuSig 协议(2018)通过**承诺-挑战-响应**的多轮交互解决了这个问题:
- 每位签名者先承诺(commit)自己的 nonce 值
- 汇总所有承诺后,各自公开 nonce
- 基于所有人的 nonce 和公钥计算挑战,产生可聚合的签名分量
MuSig2(2021)将交互轮次从 3 轮降至 2 轮,更适合实际部署。
📌 比特币 Taproot(2021 年激活)采用了
Schnorr签名(基于secp256k1曲线),核心原因正是聚合能力:多签名合约在链上的占用空间与单签名相同,既节省手续费,又增强了隐私性。ECDSA不具备线性性质,无法直接聚合——每个签名者的签名必须单独验证。
零知识证明入口:签名怎样证明而不泄露¶
理解数字签名为什么是安全的,需要回答一个更根本的问题:你怎么证明你知道某个秘密,同时又不把秘密本身说出来?
这正是**零知识证明(Zero-Knowledge Proof,ZKP)** 解决的问题,而数字签名恰好是 ZKP 的一种特殊实例。
Σ 协议:交互式零知识证明
想象 Peggy 要向 Victor 证明她知道私钥 x(满足 Y = g^x),但不想透露 x 本身——这就是 Schnorr 识别协议(Schnorr Identification Protocol):
- 承诺(Commitment):Peggy 生成随机数 k,计算 R = g^k,将 R 发给 Victor
- 挑战(Challenge):Victor 发送随机挑战值 c
- 响应(Response):Peggy 计算 s = k + c·x,发给 Victor
Victor 验证 g^s = g^(k+cx) = R · Y^c 是否成立。这个三步模式(承诺→挑战→响应)被称为 Σ 协议(Sigma Protocol)——Victor 学不到任何关于 x 的信息,但能确信 Peggy 知道 x。
Fiat-Shamir 变换:从交互式到签名
交互式协议有个实际问题:Victor 必须在线。1986 年,Fiat 和 Shamir 提出了关键技巧:让 Peggy 自己扮演 Victor,用哈希函数生成挑战值:
c = H(R || message)
挑战值不再来自真实验证者,而来自哈希函数。只要哈希输出不可预测,安全性就与交互版本等价。这正是 Schnorr 签名的数学本质——签名本质上是**非交互式零知识证明(NIZK)**。
graph TD
A["Σ 协议<br/>(交互式,需 Victor 在线)"] -- "Fiat-Shamir 变换<br/>c = H(R || msg)" --> B["Schnorr 签名<br/>(非交互式 NIZK)"]
B -- "Edwards25519 + 确定性 nonce" --> C["Ed25519"]
B -- "secp256k1 + Taproot" --> D["比特币聚合签名"]
style A fill:transparent,stroke:#0288d1,color:#adbac7
style B fill:transparent,stroke:#388e3c,color:#adbac7
style C fill:transparent,stroke:#7b1fa2,color:#adbac7
style D fill:transparent,stroke:#f57c00,color:#adbac7
📌 ZKP 的应用远不止于签名:现代区块链(如 Zcash、zkSync)使用更通用的 ZK-SNARK / ZK-STARK 实现隐私保护的计算验证。这些进阶话题详见「下一代密码学」(
../next-generation/)。参考「散列函数与完整性保护」(../hashing-and-integrity/) 了解哈希函数在 Fiat-Shamir 变换中承担的角色。
🔢 RSA 签名¶
当你需要一种被几乎所有系统广泛支持的签名算法时,RSA 几乎是默认选择。自 1977 年发表以来,RSA 一直是公钥密码学的"主力军"。
RSA 签名原理¶
RSA 密钥对由模数 N(两个大质数 P 和 Q 的乘积)、公钥指数 E 和私钥指数 D 组成,满足 E × D ≡ 1 mod ((P-1)(Q-1))。
RSA 签名的基本运算非常简洁:
- 签名:S = PAD(H(m))^D mod N
- 验证:PAD(H(m)) == S^E mod N
其中 PAD() 是填充函数,H(m) 是消息摘要。签名和验证互为逆运算——签名用私钥做指数运算,验证用公钥做指数运算。
💡 这个"签名 = 私钥加密"的直觉虽然简化了,但在调试时很有用:用公钥"解密"签名值可以恢复填充后的哈希,帮你排查签名失败的原因。
PKCS#1 v1.5 签名¶
PKCS#1 v1.5 是最经典的 RSA 签名方案(RFC 8017),使用 type 1 填充:
其中 DigestInfo 包含摘要算法的 OID 和摘要值的 DER 编码。由于填充中不包含随机部分,PKCS#1 v1.5 是**确定性**的——同一密钥对同一消息总是产生相同的签名。
RSA 签名长度等于密钥长度(字节数):2048 位密钥产生 256 字节签名,4096 位密钥产生 512 字节签名。
为什么 RSA 签名有「教科书」漏洞¶
当你第一次学习 RSA 时,可能听说过这样一个简洁描述:签名 = 消息^私钥 mod N,验证 = 签名^公钥 mod N。这个"教科书版 RSA 签名"看起来优雅,但它隐藏了一个致命的**可伪造性漏洞**。
无消息伪造:不需要私钥的"签名"
想象你是攻击者,以下步骤可以在完全不接触私钥的情况下伪造合法签名:
- 随机挑选任意数 σ(即"伪造签名")
- 用公钥计算 m = σ^e mod N
- 宣称"消息 m 的签名就是 σ"
验证者用公钥验算 σ^e mod N = m,与声称的消息完全匹配——验证通过。攻击者根本没有碰私钥。
💡 这就像反着走棋局:你先随机摆好"终局",再声称这是某局对弈的结果——只要规则允许这种反向推导,伪造就成立。无哈希、无填充的裸 RSA 恰好允许这种"倒推"。
PKCS#1 v1.5 的实现级漏洞
为对抗无消息伪造,PKCS#1 v1.5 引入了严格的填充格式(DigestInfo + 0xFF 序列),使倒推不再可行。但 1998 年 Bleichenbacher 发现了更深的问题:如果实现代码对填充校验不够严格,攻击者可以在不知道私钥的情况下伪造签名。
2006 年 Bleichenbacher 给出了具体的签名伪造攻击。更严重的是,2019 年一项研究(Chau et al.)发现大量知名开源库存在这一缺陷——这不是罕见的边界情况,而是系统性的实现错误。
⚠️
RSA PKCS#1 v1.5签名**不是算法本身不安全**,而是"正确实现极其困难"。现有系统为向后兼容不得不继续使用时,请严格校验填充格式,并考虑迁移到RSA-PSS(见下节)。
RSA-PSS 签名¶
虽然 PKCS#1 v1.5 至今未被实际攻破,但密码学界更推荐使用 RSA-PSS(Probabilistic Signature Scheme,概率签名方案)。PSS 通过引入随机盐值(salt)使签名具有随机性,并且在理论上被证明是安全的。
PSS 的填充过程比 PKCS#1 v1.5 复杂得多,核心思路是使用掩码生成函数 MGF(Mask Generation Function)对填充数据进行随机化处理。
⚠️ RSA-PSS 验证的两个常见陷阱: 1. 盐值长度不匹配:盐值长度不编码在签名中,验证时必须指定与签名时相同的盐值长度 2. MGF 摘要不一致:MGF 使用的摘要算法应与消息摘要相同,确保安全强度一致
PKCS#1 v1.5 vs PSS 对比¶
| 特性 | PKCS#1 v1.5 | RSA-PSS |
|---|---|---|
| 确定性 | 是(同一签名) | 否(随机盐值,除非盐值长度为 0) |
| 安全性证明 | 无理论证明 | 有可证明安全性 |
| 兼容性 | 极广泛(几乎所有系统) | 较新系统支持 |
| 算法名称 | SHA256withRSA |
SHA256withRSAandMGF1 |
| 参数配置 | 无需额外参数 | 需要 PSSParameterSpec |
| 推荐程度 | 兼容场景可用 | 新项目优先选择 |
签名延展性攻击¶
你以为一个合法签名在数学上只有唯一的形式?对于大多数签名方案来说,并非如此。
ECDSA 的天然可延展性
ECDSA 签名由两个整数 (r, s) 组成。由于椭圆曲线的对称性,如果 (r, s) 是消息 m 的合法签名,那么 (r, -s mod n) 也是合法签名——n 是曲线阶。攻击者不需要私钥,就能从任何已有签名生成另一个同样有效的签名。这种性质叫做**签名延展性(Signature Malleability)**。
比特币 MtGox 事件(2014)
2014 年 2 月,曾是最大比特币交易所的 MtGox 宣告破产,损失约 85 万枚比特币。调查显示,攻击者利用签名延展性修改了提款交易的签名:
- 用户发起提款,广播包含
ECDSA签名的交易 - 攻击者截获,用 -s mod n 替换 s,重新广播篡改版本
- 两笔交易签名都合法,但**交易 ID(txid)不同**(txid 是含签名的整个交易的哈希)
- MtGox 系统误认为原始交易失败,触发重复发款逻辑
sequenceDiagram
participant User as 用户
participant Attacker as 攻击者
participant Network as 比特币网络
User->>Network: 广播提款交易 sig=(r, s)
Attacker->>Network: 广播篡改交易 sig=(r, -s mod n)
Note over Network: 两者签名均合法,txid 不同
Network->>Attacker: 篡改交易先被确认
User->>Network: 原始交易显示未确认
Note over User: 向 MtGox 投诉"提款失败"
Note over Network: MtGox 误判重复发款 ❌
缓解措施
- Bitcoin SegWit(隔离见证):将签名数据移出 txid 计算范围,使 txid 不再受签名内容影响,从根本上解决了比特币场景的延展性问题
Ed25519的 RFC 8032 规范:要求验证时严格检查 s < n/2,拒绝"等价但不同"的签名,减少延展性攻击面- BLS 签名:基于双线性配对,具有天然的签名唯一性,同时支持聚合;但计算成本更高,适合对签名大小和聚合有严格要求的场景
⚠️ 签名延展性并不意味着签名方案被"攻破"——攻击者仍然无法伪造针对自己选定消息的签名(EUF-CMA 安全性不受影响)。但在**依赖"签名唯一性"的协议**(如区块链 txid 去重)中,延展性可能成为逻辑漏洞的入口。设计依赖签名的协议时,不要假设同一消息只会有一种合法签名。
🔏 SM2 国密签名¶
当你的项目需要满足中国政府的合规要求时(如电子签章、政务系统、金融领域),SM2 是必须了解的签名算法。SM2 由中国国家密码管理局发布,基于 256 位椭圆曲线 sm2p256v1,搭配 SM3 摘要算法。
SM2 的独特之处¶
SM2 与其他 EC 签名算法最大的区别在于**消息预处理**:在签名之前,SM2 会先计算一个"用户身份前缀" ZA,并将其与原始消息拼接。ZA 的计算涉及:
- 用户 ID(默认为
"1234567812345678"的 ASCII 编码) - 曲线参数 A、B
- 基点 G 的坐标
- 签名者公钥 Q 的坐标
💡 这个预处理步骤相当于给每个签名者打上"身份烙印",使得不同用户的签名即使使用相同曲线也无法混淆。
SM2ParameterSpec¶
Bouncy Castle 通过 SM2ParameterSpec 允许你显式指定用户 ID。如果不设置,则使用默认 ID "1234567812345678"。签名和验证双方必须使用相同的 ID,否则验证会失败。
Java 代码示例¶
SM2 签名由 (r, s) 两个 32 字节整数组成,DER 编码后通常为 70-72 字节。
💡 算法选择建议¶
当你面对一个需要数字签名的项目时,如何选择算法?以下对比表格覆盖了主要的决策维度:
算法全面对比¶
| 维度 | DSA | ECDSA | EdDSA | RSA PKCS#1 v1.5 | RSA-PSS | SM2 |
|---|---|---|---|---|---|---|
| 数学基础 | 离散对数 | 椭圆曲线离散对数 | Edwards 曲线 | 大整数分解 | 大整数分解 | 椭圆曲线离散对数 |
| 128 位安全密钥长度 | 3072 位 | 256 位 | 255 位 | 3072 位 | 3072 位 | 256 位 |
| 签名大小 | ~56 字节 | ~72 字节 | 64 字节 | 256 字节 | 256 字节 | ~72 字节 |
| 确定性 | 否(可用 DDSA) | 否(可用 ECDDSA) | 是 | 是 | 否 | 否 |
| 侧信道抗性 | 一般 | 一般 | 优秀 | 一般 | 一般 | 一般 |
| 兼容性 | 一般 | 广泛 | 较新 | 极广泛 | 较广泛 | 中国标准 |
| 算法名称 | SHA256withDSA |
SHA256withECDSA |
EdDSA |
SHA256withRSA |
SHA256withRSAandMGF1 |
SM3withSM2 |
| 额外参数 | 无 | 无 | 无 | 无 | PSSParameterSpec |
SM2ParameterSpec |
选择建议¶
- 通用互联网服务(TLS、JWT、代码签名):优先 ECDSA P-256(性能好、签名小、兼容性强)或 Ed25519(确定性、性能优秀)
- 需要广泛兼容性(PDF 签名、XML 签名、旧系统集成):RSA PKCS#1 v1.5(2048 位以上)
- 追求理论安全性:RSA-PSS(有可证明安全性的证明)
- 中国合规场景(政务、金融、电子签章):SM2(国密标准强制要求)
- 嵌入式/物联网设备:Ed25519(小密钥、小签名、确定性、无随机数依赖)
- 避免使用:DSA 3072(密钥太大、性能不如 ECDSA)、SHA-1 签名
💡 如果没有特殊限制,Ed25519 是当前最推荐的新项目选择——确定性签名消除了随机数安全隐患,签名紧凑(64 字节),性能优秀,且被越来越多的协议和框架支持。
📚 参考来源(本笔记增强部分)¶
- David Wong, Real-World Cryptography (Manning, 2021), Chapter 7: Signatures and zero-knowledge proofs
- 章节文本:会话工作区
files/rwc-chapters/ch07.txt