1. 概述

本文将深入讲解 数字签名(Digital Signature)的原理与实现,重点基于 Java Cryptography Architecture (JCA) 提供的核心 API 进行实战编码。你会接触到如下关键组件:

  • KeyPair:密钥对生成
  • MessageDigest:消息摘要计算
  • Cipher:加解密操作
  • KeyStore:密钥存储管理
  • Certificate:证书处理
  • Signature:签名与验证专用类

我们将从零开始理解数字签名的本质,生成密钥对,使用 CA 证书认证公钥身份,并分别通过 低层 API 手动实现高层 Signature 类简化实现 两种方式完成签名流程。

如果你在做接口安全、API 鉴权或文档防篡改系统,这篇内容值得集合,避免踩坑 ❗


2. 什么是数字签名?

数字签名不是加密,而是一种用于保障数据完整性和身份可信的技术手段。

2.1 数字签名的作用

完整性(Integrity):确保消息在传输过程中未被篡改
真实性(Authenticity):确认消息确实来自声称的发送方
不可否认性(Non-repudiation):发送方无法事后抵赖其行为

这三点是构建可信通信的基础。

2.2 带数字签名的消息发送流程

技术上讲,数字签名 = 使用私钥加密的消息摘要(hash)

具体步骤如下:

  1. 对原始消息计算摘要(如 SHA-256)
  2. 使用发送方的 私钥 加密该摘要 → 得到“数字签名”
  3. 将原始消息 + 数字签名 + 公钥(或证书)一并发送

⚠️ 注意:我们只加密了摘要,而不是整个消息本身。因此数字签名 不提供机密性,它只用来验证“谁发的”和“有没有改”。

2.3 接收方如何验证签名

接收方收到消息后执行以下操作:

  1. 用相同算法重新计算消息摘要
  2. 使用发送方的 公钥 解密数字签名,得到原始摘要
  3. 比较两个摘要是否一致

✅ 若一致 → 签名有效,说明:

  • 消息未被篡改
  • 发送者持有对应的私钥(身份可信)

3. 数字证书与公钥身份认证

核心问题:你怎么知道这个公钥真的是对方的?

设想一下,攻击者可以伪造一个公钥冒充你的好友。这时候即使签名能验,也是验了一个假身份。

解决方案就是:数字证书(Digital Certificate)

什么是数字证书?

📌 数字证书是一个由 可信第三方(CA)签发的电子文件,它的作用是将某个身份(比如域名、组织名)与一个公钥绑定在一起。

证书本身也包含签名:

  • 证书内容(含公钥、有效期、主体信息等)被 CA 用自己的私钥签名
  • 接收方可用 CA 的公钥验证该签名,从而信任证书中的公钥属于指定主体

这就形成了所谓的 证书链(Certificate Chain)

Root CA → Intermediate CA → Your Certificate

顶级 CA 是自签名的(self-signed),即用自己的私钥签自己的公钥,这是信任锚点。

常见格式

  • X.509:最主流的证书标准
  • 存储格式:
    • .der:二进制 DER 编码
    • .pem:Base64 文本格式(常以 -----BEGIN CERTIFICATE----- 开头)

Java 提供了 X509Certificate 类来解析和操作 X.509 证书,开箱即用 ✅


4. 密钥对管理

数字签名依赖非对称加密,所以需要一对密钥:私钥(签名用)、公钥(验证用)。

Java 中对应类型为 PrivateKeyPublicKey

4.1 生成密钥对

我们使用 JDK 自带的 keytool 工具生成 RSA 密钥对:

keytool -genkeypair -alias senderKeyPair -keyalg RSA -keysize 2048 \
  -dname "CN=Baeldung" -validity 365 -storetype JKS \
  -keystore sender_keystore.jks -storepass changeit

📌 解释参数:

  • -alias:密钥别名
  • -keyalg RSA -keysize 2048:使用 RSA 算法,2048 位强度
  • -dname "CN=Baeldung":主体名称(Common Name)
  • -keystore:输出到 JKS 格式的 KeyStore 文件
  • -storepass:KeyStore 的访问密码

执行后会生成 sender_keystore.jks,其中包含:

  • 私钥
  • 对应的公钥
  • 一个自签名的 X.509 证书(用于测试)

⚠️ 生产环境建议使用更安全的 PKCS12 格式(-storetype PKCS12),JKS 已逐步淘汰。

4.2 加载私钥用于签名

签名时需要获取 PrivateKey 实例:

KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("sender_keystore.jks"), "changeit".toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey("senderKeyPair", "changeit".toCharArray());

📌 注意:

  • getKey() 第二个参数是密钥的密码(可能不同于 keystore 密码)
  • Java 9+ 推荐使用 char[] 而非 String 存储密码,防止内存泄露

4.3 发布公钥(自签 vs CA 签)

✅ 方式一:自签名证书(适用于测试)

直接导出证书即可:

keytool -exportcert -alias senderKeyPair -storetype JKS \
  -keystore sender_keystore.jks -file sender_certificate.cer \
  -rfc -storepass changeit

-rfc 表示输出为 PEM 文本格式(Base64),便于查看和传输。

✅ 方式二:CA 签发证书(生产推荐)

需先生成 CSR(Certificate Signing Request):

keytool -certreq -alias senderKeyPair -storetype JKS \
  -keystore sender_keystore.jks -file sender_certificate.csr \
  -rfc -storepass changeit

然后将 sender_certificate.csr 提交给 CA(如 Let's Encrypt、DigiCert 等),CA 审核通过后返回签名后的证书(.cer.crt 文件)。

这样你的公钥就有了第三方背书,客户端更容易信任。

4.4 加载公钥用于验证

接收方需要将对方的公钥导入自己的 KeyStore。

先创建一个空的 KeyStore(可用任意密钥对生成后删除):

keytool -genkeypair -alias receiverKeyPair -keyalg RSA -keysize 2048 \
  -dname "CN=Receiver" -validity 365 -storetype JKS \
  -keystore receiver_keystore.jks -storepass changeit 

keytool -delete -alias receiverKeyPair -storepass changeit -keystore receiver_keystore.jks 

再导入对方的证书:

keytool -importcert -alias senderCert -storetype JKS \
  -keystore receiver_keystore.jks -file sender_certificate.cer \
  -storepass changeit

Java 代码加载公钥:

KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("receiver_keystore.jks"), "changeit".toCharArray());
Certificate certificate = keyStore.getCertificate("senderCert");
PublicKey publicKey = certificate.getPublicKey();

现在我们已有:

  • 发送方:PrivateKey
  • 接收方:PublicKey

可以开始签名和验证了 ✅


5. 使用 MessageDigest 和 Cipher 实现数字签名(底层实现)

这种方式虽然繁琐,但有助于理解底层原理,适合定制化场景。

5.1 生成消息摘要

假设我们要签名的是一段文件内容:

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));

使用 MessageDigest 计算 SHA-256 摘要:

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] messageHash = md.digest(messageBytes);

常见算法选择:

  • SHA-256:推荐,安全性高
  • SHA-384 / SHA-512:更长摘要,性能略低
  • MD5 / SHA-1:已被破解,禁止用于安全场景

接下来需要封装摘要信息,使用 Bouncy Castle 的 DigestInfo(需引入依赖):

DigestAlgorithmIdentifierFinder hashAlgorithmFinder = new DefaultDigestAlgorithmIdentifierFinder();
AlgorithmIdentifier hashingAlgorithmIdentifier = hashAlgorithmFinder.find("SHA-256");
DigestInfo digestInfo = new DigestInfo(hashingAlgorithmIdentifier, messageHash);
byte[] hashToEncrypt = digestInfo.getEncoded();

💡 DigestInfo 包含算法标识符和实际哈希值,符合 ASN.1 编码规范,是标准做法。

5.2 使用私钥加密摘要(即“签名”)

使用 Cipher 类进行 RSA 加密:

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] encryptedMessageHash = cipher.doFinal(hashToEncrypt);

此时 encryptedMessageHash 就是数字签名,应随消息一同发送。

5.3 验证签名(解密并比对)

接收方收到消息和签名后:

  1. 用公钥解密签名:
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] decryptedMessageHash = cipher.doFinal(encryptedMessageHash);
  1. 重新计算消息摘要:
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] newMessageHash = md.digest(messageBytes);
  1. 封装新的 DigestInfo
DigestAlgorithmIdentifierFinder hashAlgorithmFinder = new DefaultDigestAlgorithmIdentifierFinder();
AlgorithmIdentifier hashingAlgorithmIdentifier = hashAlgorithmFinder.find("SHA-256");
DigestInfo digestInfo = new DigestInfo(hashingAlgorithmIdentifier, newMessageHash);
byte[] expectedHash = digestInfo.getEncoded();
  1. 比较解密结果与本地计算结果:
boolean isCorrect = Arrays.equals(decryptedMessageHash, expectedHash);

✅ 相等 → 签名有效
❌ 不等 → 数据被篡改或签名非法

⚠️ 踩坑提示:很多人忘了 DigestInfo 封装,直接比较原始哈希,会导致验证失败!因为加密的是带算法头的结构化数据。


6. 使用 Signature 类实现数字签名(高层封装)

JCA 提供了更简洁的 Signature 类,推荐日常开发使用。

6.1 签名消息(简单粗暴三步走)

Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
signature.update(messageBytes);

byte[] digitalSignature = signature.sign();

📌 关键点:

  • "SHA256withRSA" 是标准算法名,表示先 SHA-256 再 RSA 加密
  • update() 可多次调用,适合流式处理大文件
  • sign() 返回签名字节数组,可 Base64 编码后传输

其他常用算法:

  • SHA1withRSA
  • SHA256withDSA
  • MD5withRSA(不推荐)

6.2 验证签名(同样简洁)

Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
signature.update(messageBytes);

boolean isCorrect = signature.verify(digitalSignature);

verify() 返回 true → 签名合法
false → 验证失败

整个过程无需手动处理摘要封装、加解密细节,JCA 内部自动完成,省心又安全 ✅


7. 总结

本文带你从零构建了一套完整的数字签名机制:

  • 理解了数字签名的三大作用:完整性、真实性、不可否认性
  • 掌握了通过 keytool 管理密钥对和证书
  • 实践了两种实现方式:
    • 底层实现:使用 MessageDigest + Cipher,灵活但易出错
    • 高层实现:使用 Signature 类,简洁高效,推荐生产使用
  • 强调了 DigestInfo 封装的重要性,避免常见验证失败坑点

🔗 示例代码已上传至 GitHub:https://github.com/yourname/java-digital-signature-demo

如果你正在做 API 签名、JWT 安全、电子合同等场景,这套机制就是底层基石。建议动手跑一遍,印象更深 💪


原始标题:Digital Signatures in Java | Baeldung