数字签名
想象一个场景:你收到一份电子合同,用鼠标轻轻一点,合同就「签」好了。但你怎么确定这份合同真的是对方签署的,而不是被中间人篡改过的?
这就是数字签名要解决的问题。数字签名不是手写签名的电子化,而是一种数学方案,能够证明消息确实来自声称的发送者,且在传输过程中未被篡改。它是现代互联网信任体系的基石——从代码签名到 HTTPS 证书,从区块链交易到电子合同,处处都有数字签名的身影。
数字签名的基本原理
数字签名的核心思想可以用一句话概括:用私钥「标记」消息,任何人都可以用公钥验证。
签名的数学基础
签名过程涉及三个算法:
- 密钥生成:生成签名密钥对(私钥
sk、公钥 pk)
- 签名:使用私钥
sk 对消息 m 生成签名 σ = Sign(sk, m)
- 验证:使用公钥
pk 验证签名 Verify(pk, m, σ) 返回 true/false
签名的数学原理依赖于陷门函数(Trapdoor Function)的设计:正向计算容易,逆向计算困难,且只有拥有私钥的人才能进行特定方向的计算。
flowchart LR
A["消息 m"] --> B["签名算法"]
C["私钥 sk"] --> B
B --> D["签名 σ"]
D --> E["验证算法"]
F["公钥 pk"] --> E
E --> G{"验证结果"}
C -.->|"陷门"| G
签名必须满足的三个属性
不可伪造(Unforgeability):攻击者在知道公钥和任意消息的情况下,无法计算出有效签名。这基于计算复杂度假设(如大数分解的困难性)。
不可否认(Non-repudiation):签名者无法否认自己签署过的消息。因为只有私钥持有者才能产生有效签名,而私钥是唯一的。
可验证性(Verifiability):任何拥有公钥的人都可以验证签名的有效性,无需私钥参与。
RSA 签名方案
RSA 既可以用于加密,也可以用于签名。签名过程本质上是「用私钥加密哈希值」。
PKCS#1 v1.5 签名
这是最经典也是曾经最广泛使用的 RSA 签名方案。
RSA
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.KeyPair;
import java.util.Base64;
public class RSASignatureDemo {
public static void main(String[] args) throws Exception {
// 生成 RSA 密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
String message = "这是一份重要的电子合同,金额 100 万元。";
// 使用 SHA-256 with RSA 进行签名
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(keyPair.getPrivate());
signer.update(message.getBytes());
byte[] signature = signer.sign();
System.out.println("RSA 签名长度: " + signature.length + " 字节");
System.out.println("签名值: " + Base64.getEncoder().encodeToString(signature));
// 验证签名
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message.getBytes());
boolean isValid = verifier.verify(signature);
System.out.println("签名验证结果: " + isValid);
}
}
RSA-PSS 签名
PKCS#1 v1.5 签名存在一些理论上的攻击面(如 Bleichenbacher 的百万消息攻击),PSS 签名通过更严谨的填充机制提供了更强的安全保障。
RSA-PSS
import java.security.Signature;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
public class RSAPSSDemo {
public static void main(String[] args) throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
String message = "需要更高安全性的签名场景";
// SHA-256 with RSA-PSS
Signature signer = Signature.getInstance("SHA256withRSA/PSS");
signer.initSign(keyPair.getPrivate());
signer.setParameter(new java.security.spec.MGF1ParameterSpec("SHA-256"));
signer.update(message.getBytes());
byte[] signature = signer.sign();
// 验证
Signature verifier = Signature.getInstance("SHA256withRSA/PSS");
verifier.initVerify(keyPair.getPublic());
verifier.setParameter(new java.security.spec.MGF1ParameterSpec("SHA-256"));
verifier.update(message.getBytes());
boolean isValid = verifier.verify(signature);
System.out.println("RSA-PSS 签名验证: " + isValid);
}
}
RSA 签名最佳实践
- 优先使用 RSA-PSS 而非 PKCS#1 v1.5
- 密钥长度至少 2048 位,敏感场景使用 3072 或 4096 位
- 使用 SHA-256 或更强的哈希算法,不要使用 MD5 或 SHA-1
:::
ECDSA 签名
椭圆曲线数字签名算法(ECDSA)是 RSA 的高效替代品。相同安全强度下,ECDSA 的密钥长度远小于 RSA。
ECDSA 的数学原理
ECDSA 基于椭圆曲线离散对数问题(ECDLP):给定椭圆曲线上一点 G 和公钥 Q = dG,很难求出私钥 d。
ECDSA
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.KeyPair;
import java.security.spec.NamedParameterSpec;
import java.util.Base64;
public class ECDSASignatureDemo {
public static void main(String[] args) throws Exception {
// 使用 P-256 曲线
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
keyGen.initialize(new NamedParameterSpec("P-256"));
KeyPair keyPair = keyGen.generateKeyPair();
System.out.println("ECDSA P-256 私钥长度: " +
keyPair.getPrivate().getEncoded().length + " 字节");
System.out.println("ECDSA P-256 公钥长度: " +
keyPair.getPublic().getEncoded().length + " 字节");
String message = "区块链交易内容";
// 签名
Signature signer = Signature.getInstance("SHA256withECDSA");
signer.initSign(keyPair.getPrivate());
signer.update(message.getBytes());
byte[] signature = signer.sign();
System.out.println("ECDSA 签名长度: " + signature.length + " 字节");
System.out.println("签名值: " + Base64.getEncoder().encodeToString(signature));
// 验证
Signature verifier = Signature.getInstance("SHA256withECDSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message.getBytes());
boolean isValid = verifier.verify(signature);
System.out.println("验证结果: " + isValid);
}
}
ECDSA
public class KeySizeComparison {
public static void main(String[] args) {
System.out.println("===== 相同安全强度下的密钥长度对比 =====");
System.out.println();
System.out.println("| 安全强度 | RSA 密钥长度 | ECDSA 曲线 |");
System.out.println("|----------|---------------|-------------|");
System.out.println("| 80 位 | 1024 位 | P-160 |");
System.out.println("| 112 位 | 2048 位 | P-224 |");
System.out.println("| 128 位 | 3072 位 | P-256 |");
System.out.println("| 192 位 | 7680 位 | P-384 |");
System.out.println("| 256 位 | 15360 位 | P-521 |");
System.out.println();
System.out.println("P-256 (128 位安全) 的 ECDSA 签名速度约是 RSA-2048 的 4 倍");
}
}
EdDSA 签名
EdDSA(Edwards-curve Digital Signature Algorithm)是 2011 年提出的新型签名算法,基于 Curve25519 曲线设计,被认为是目前最优雅和高效的签名方案。
EdDSA 的设计目标:
- 签名速度快:比 ECDSA 和 RSA 快一个数量级
- 签名长度短:64 字节,比 ECDSA 的 70-72 字节更紧凑
- 侧信道安全:天然抗时序攻击和缓存攻击
- 安全性可证明:基于离散对数的困难性,有严格的数学证明
EdDSA
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.KeyPair;
import java.security.spec.NamedParameterSpec;
import java.util.Base64;
public class EdDSADemo {
public static void main(String[] args) throws Exception {
// 使用 Ed25519 曲线
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EdDSA");
keyGen.initialize(new NamedParameterSpec("Ed25519"));
KeyPair keyPair = keyGen.generateKeyPair();
System.out.println("Ed25519 私钥长度: " +
keyPair.getPrivate().getEncoded().length + " 字节");
System.out.println("Ed25519 公钥长度: " +
keyPair.getPublic().getEncoded().length + " 字节");
String message = "高性能签名场景,如区块链";
// 签名
Signature signer = Signature.getInstance("EdDSA");
signer.initSign(keyPair.getPrivate());
signer.update(message.getBytes());
byte[] signature = signer.sign();
System.out.println("EdDSA 签名长度: " + signature.length + " 字节");
// 验证
Signature verifier = Signature.getInstance("EdDSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message.getBytes());
System.out.println("验证结果: " + verifier.verify(signature));
}
}
签名的应用场景
代码签名
软件发布时使用代码签名,让用户能够验证软件的来源和完整性。
代码签名验证(演示概念)
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.PublicKey;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class CodeSigningVerification {
public static boolean verifySignedJar(Path jarPath, InputStream certStream) throws Exception {
// 从证书创建 PublicKey
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(certStream);
PublicKey publicKey = cert.getPublicKey();
// 验证 JAR 签名(实际使用 jarsigner 工具)
// 这里演示签名验证的概念
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
// 读取 JAR 内容
byte[] jarContent = Files.readAllBytes(jarPath);
sig.update(jarContent);
// 验证
return sig.verify(readEmbeddedSignature(jarPath));
}
private static byte[] readEmbeddedSignature(Path jarPath) {
// 实际实现:从 JAR 的 META-INF 目录读取签名块
throw new UnsupportedOperationException("演示用");
}
}
文档签名
PDF
public class DocumentSigningService {
public byte[] signDocument(byte[] pdfContent, PrivateKey privateKey) throws Exception {
// 计算文档哈希
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(pdfContent);
// 使用私钥签名哈希
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(hash);
byte[] signature = signer.sign();
// 将签名嵌入 PDF(实际使用 PDF 库如 iText)
return embedSignatureInPDF(pdfContent, signature);
}
public boolean verifyDocument(byte[] signedPdf) throws Exception {
// 提取签名和文档内容
byte[] signature = extractSignature(signedPdf);
byte[] originalContent = extractOriginalContent(signedPdf);
// 从证书获取公钥
PublicKey publicKey = extractPublicKeyFromPDF(signedPdf);
// 验证
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(originalContent);
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(hash);
return verifier.verify(signature);
}
}
签名与加密的组合使用
签名和加密是两个独立的过程,它们可以组合使用来同时保证机密性和完整性。
签名然后加密
签名+加密组合方案
public class SignAndEncrypt {
public byte[] signThenEncrypt(byte[] message, KeyPair signingKey, PublicKey recipientKey)
throws Exception {
// 1. 签名:使用发送者的私钥
Signature signer = Signature.getInstance("SHA256withECDSA");
signer.initSign(signingKey.getPrivate());
signer.update(message);
byte[] signature = signer.sign();
// 2. 组合消息和签名
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.writeInt(message.length);
baos.write(message);
baos.write(signature);
byte[] combined = baos.toByteArray();
// 3. 加密:使用接收者的公钥
Cipher cipher = Cipher.getInstance("ECIES");
cipher.init(Cipher.ENCRYPT_MODE, recipientKey);
return cipher.doFinal(combined);
}
}
加密然后签名
加密+签名组合方案
public class EncryptAndSign {
public EnvelopedMessage encryptThenSign(byte[] message, KeyPair recipientKey,
KeyPair signingKey, String recipientId, String senderId)
throws Exception {
// 1. 加密消息
Cipher cipher = Cipher.getInstance("ECIES");
cipher.init(Cipher.ENCRYPT_MODE, recipientKey.getPublic());
byte[] encrypted = cipher.doFinal(message);
// 2. 对加密内容签名
Signature signer = Signature.getInstance("SHA256withECDSA");
signer.initSign(signingKey.getPrivate());
signer.update(encrypted);
byte[] signature = signer.sign();
// 3. 返回信封消息
return new EnvelopedMessage(encrypted, signature, senderId);
}
}
:::warning 签名与加密的区别
- 签名:证明「消息确实来自我」,但不加密内容
- 加密:保证「只有你能看到内容」,但不证明来源
同时需要机密性和完整性时,必须同时使用签名和加密。
证书与签名的关系
数字证书是签名方案的重要应用。证书将公钥与身份绑定,由证书颁发机构(CA)使用 CA 的私钥签名。
X.509
import java.security.cert.X509Certificate;
import java.security.PublicKey;
public class CertificateSignatureVerification {
public boolean verifyCertificate(X509Certificate cert, X509Certificate issuerCert)
throws Exception {
// 获取颁发者的公钥
PublicKey issuerPublicKey = issuerCert.getPublicKey();
// 验证证书签名
cert.checkValidity(); // 检查证书有效期
cert.verify(issuerPublicKey); // 验证签名
// 验证证书用途
boolean[] keyUsage = cert.getKeyUsage();
if (keyUsage == null || !keyUsage[0]) { // digitalSignature 位
throw new SecurityException("证书不能用于数字签名");
}
return true;
}
}
思考题
问题 1:为什么说「用私钥加密数据」和「用公钥解密」不是 RSA 签名的准确描述?
参考答案
这个描述虽然直观但不准确,原因如下:
-
数学操作相同,但语义不同:RSA 签名确实涉及「用私钥计算」的操作,但这个操作的目的是签名,而不是解密数据。签名是对消息哈希值进行数学运算,而解密是将密文还原为明文。
-
签名不提供机密性:加密的目的是让只有私钥持有者能解密,保护机密性。签名的目的是证明来源,不隐藏任何内容。
-
签名的验证不等于解密:任何人都可以用公钥「验证」签名,但不能「解密」出有意义的内容。签名验证返回的是布尔值,而不是原始消息。
正确的理解是:签名是使用私钥对消息哈希进行运算;验证是使用公钥检查哈希值是否匹配。加密和解密涉及的是完整消息的对称性操作,而签名只涉及哈希值。
NIST 的标准文档明确使用「Sign」和「Verify」而非「Encrypt with Private Key」和「Decrypt with Public Key」,正是为了避免这种混淆。
问题 2:如果私钥泄露了,攻击者可以伪造签名。但签名本身具有不可否认性,这意味着签名者无法否认。矛盾吗?
参考答案
这里涉及签名者的不可否认性与私钥保护义务之间的权衡。
不可否认性的前提:私钥确实是签名者独自持有的。如果私钥泄露,攻击者用泄露的私钥生成的签名,技术上仍是「该私钥持有者的签名」。
法律层面的处理:在法律实践中,「不可否认性」通常意味着:
- 在正常情况下,签名者无法否认自己签署的内容
- 但如果能证明私钥被窃取或泄露,可以作为抗辩理由
- 责任通常在签名者:签名者有义务保护私钥安全
技术层面的缓解措施:
- 硬件令牌:私钥存储在智能卡或 USB Key 中,无法被导出
- 多因素签名:签名时需要密码 + 硬件令牌,攻击者即使拿到令牌也需要密码
- 时间戳:签名时使用可信时间戳服务,证明签名发生的时间
- 密钥轮换:定期更换签名密钥,限制泄露密钥的影响窗口
- 撤销机制:私钥泄露后,立即向 CA 申请证书撤销
因此,技术上的不可否认性 + 完善的安全措施 = 法律上的可执行性。