在前面的章節中,我們主要聚焦於資料的「機密性(Confidentiality)」——如何透過 AES-GCM 或 ECIES 混合加密讓竊聽者無法讀取訊息內容。
然而,在零信任與開放網路架構中,單靠機密性是不夠的。我們還面臨三個關鍵問題:
對稱加密與 MAC 雖然能提供完整性與驗證,但因為金鑰為雙方共享,無法區分是哪一方產生的訊息,因此無法提供「不可否認性」。
為解決這個難題,數位簽章(Digital Signature) 應運而生。本章將解析數位簽章的原理,剖析歷史上因簽章漏洞發生的資安浩劫(Sony PS3 事件),並透過 Java JCA 帶領讀者實戰現代兩大主流簽章標準——Ed25519 與 RSA-PSS。
非對稱加密與數位簽章在數學運算上互為逆過程,但其工程目的與使用的金鑰完全相反:
【非對稱加密 (Encryption)】 【數位簽章 (Signature)】
目標:確保機密性 目標:不可否認性與身份驗證
[ 傳送方 ] 使用【接收方公鑰】加密 [ 簽署方 ] 使用【自己私鑰】簽署
│ │
▼ ▼
[ 接收方 ] 使用【自己私鑰】解密 [ 驗證方 ] 使用【簽署方公鑰】驗驗
在實際系統中,我們絕不直接對原始大檔訊息進行私鑰簽署,而是先將訊息輸入密碼學雜湊函數(如 SHA-256),對其摘要(Digest)進行簽署:
[ 原始訊息 M ] ──> [ SHA-256 / SHA-512 ] ──> [ 訊息摘要 Digest (32/64 bytes) ]
│
▼
[ 數位簽章 Signature ] <── [ 私鑰簽署運算 (Ed25519 / RSA-PSS) ]
RSA 簽章演算法經歷了顯著的安全演進:
傳統的 RSA PKCS#1 v1.5 簽章 是確定性的(Deterministic)。若實現不當(如未嚴格檢查 Padding 結構),容易遭受 Bleichenbacher 簽章偽造攻擊 或 Fault Attack。現代資安規範(如 NIST SP 800-56B)已強烈建議停止在新型系統中使用 PKCS#1 v1.5 簽章。
RSA-PSS(RFC 8017 / PKCS#1 v2.2) 引入了隨機鹽值(Salt)與 MGF1 遮蔽函數,將確定性簽章轉化為機率性簽章(Probabilistic Signature):
[ Message Hash ] ──> [ Padding & Salt 遮蔽 (MGF1) ] ──> [ EM ] ──> [ RSA 私鑰指數運算 (s = EMᵈ mod n) ]
傳統橢圓曲線簽章 ECDSA 在數學上非常安全,但它隱藏著一個極度危險的工程陷阱——簽章隨機數(k-nonce)的重用。
在 ECDSA 演算法中,每次簽章都必須透過 SecureRandom 產生一個一次性的純量 k:
r = (k * G)_x (mod n)
s ≡ k^{-1} (z + r * d) (mod n) \quad (d \text{ 為私鑰}, z \text{ 為訊息雜湊})
2010 年,Sony 在 PS3 遊戲主機的韌體簽章系統中犯下了低級錯誤:硬編碼了固定的 k 值。
攻擊者只要拿到同一把私鑰簽署的兩份不同訊息 (m_1, r, s_1) 與 (m_2, r, s_2),透過基礎代數即可直接還原出私鑰 d:
s_1 - s_2 ≡ k^{-1}(z_1 - z_2) (mod n) => k ≡ \frac{z_1 - z_2}{s_1 - s_2} (mod n)
求得 k 後代回公式:
d ≡ \frac{s_1 * k - z_1}{r} (mod n)
這導致 PS3 的根私鑰完全洩漏,防禦機制徹底崩潰。
💡 補救方案:RFC 6979 提出了「確定性 ECDSA」,改用
HMAC-SHA256(私鑰, 訊息)動態衍生 k,從根本上解決了隨機數發生器(RNG)失效的問題。
為了徹底解決 ECDSA 的複雜性與側信道威脅,密碼學家 Daniel J. Bernstein 等人提出了 Ed25519(Edwards-curve Digital Signature Algorithm,RFC 8032)。
Ed25519 建立在 Curve25519 / Edwards25519 扭曲愛德華曲線上,具備以下革命性優勢:
Java 15+ 原生引入了對 Ed25519 的標準 JCA 支援(Ed25519),同時 Java 也完善支援了 RSASSA-PSS。以下範例演示如何在 Java 中實作這兩種現代簽章標準:
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.Base64;
public class DigitalSignatureProductionExample {
public static void main(String[] args) throws Exception {
System.out.println("=== Day 31 - 現代數位簽章 (Ed25519 與 RSA-PSS) 實戰 ===");
String document = "Action: Transfer 100,000 to Account-8899. Non-repudiable audit log.";
byte[] dataBytes = document.getBytes(StandardCharsets.UTF_8);
// ==========================================
// 實戰 1: Ed25519 簽章與驗簽 (現代推薦首選)
// ==========================================
System.out.println("\n--- [ 模式一:Ed25519 (EdDSA) 數位簽章 ] ---");
// 1. 生成 Ed25519 KeyPair (Java 15+)
KeyPairGenerator ed25519KeyGen = KeyPairGenerator.getInstance("Ed25519");
KeyPair edKeyPair = ed25519KeyGen.generateKeyPair();
// 2. 簽署階段 (Sign)
byte[] edSignatureBytes = signEd25519(dataBytes, edKeyPair.getPrivate());
System.out.println("Ed25519 公鑰長度: " + edKeyPair.getPublic().getEncoded().length + " bytes");
System.out.println("Ed25519 簽章長度: " + edSignatureBytes.length + " bytes (固定 64 bytes)");
System.out.println("簽章 Base64: " + Base64.getEncoder().encodeToString(edSignatureBytes));
// 3. 驗簽階段 (Verify)
boolean isEdValid = verifyEd25519(dataBytes, edSignatureBytes, edKeyPair.getPublic());
System.out.println("✅ Ed25519 簽章驗證結果: " + isEdValid);
// ==========================================
// 實戰 2: RSA-PSS 簽章與驗簽 (傳統相容首選)
// ==========================================
System.out.println("\n--- [ 模式二:RSA-PSS (RSASSA-PSS) 數位簽章 ] ---");
// 1. 生成 3072-bit RSA KeyPair
KeyPairGenerator rsaKeyGen = KeyPairGenerator.getInstance("RSA");
rsaKeyGen.initialize(3072);
KeyPair rsaKeyPair = rsaKeyGen.generateKeyPair();
// 2. 簽署階段 (Sign)
byte[] pssSignatureBytes = signRsaPss(dataBytes, rsaKeyPair.getPrivate());
System.out.println("RSA-3072 PSS 簽章長度: " + pssSignatureBytes.length + " bytes");
// 3. 驗驗階段 (Verify)
boolean isPssValid = verifyRsaPss(dataBytes, pssSignatureBytes, rsaKeyPair.getPublic());
System.out.println("✅ RSA-PSS 簽章驗證結果: " + isPssValid);
}
/**
* Ed25519 私鑰簽署
*/
public static byte[] signEd25519(byte[] data, PrivateKey privateKey) throws Exception {
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(privateKey);
signer.update(data);
return signer.sign();
}
/**
* Ed25519 公鑰驗簽
*/
public static boolean verifyEd25519(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(publicKey);
verifier.update(data);
return verifier.verify(signature);
}
/**
* RSA-PSS 私鑰簽署 (指定 SHA-256 與 MGF1-SHA256)
*/
public static byte[] signRsaPss(byte[] data, PrivateKey privateKey) throws Exception {
Signature signer = Signature.getInstance("RSASSA-PSS");
// 配置 PSS 參數:Primary Hash = SHA-256, MGF1 Hash = SHA-256, Salt Length = 32 bytes
PSSParameterSpec pssSpec = new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1
);
signer.setParameter(pssSpec);
signer.initSign(privateKey);
signer.update(data);
return signer.sign();
}
/**
* RSA-PSS 公鑰驗簽
*/
public static boolean verifyRsaPss(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
Signature verifier = Signature.getInstance("RSASSA-PSS");
PSSParameterSpec pssSpec = new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1
);
verifier.setParameter(pssSpec);
verifier.initVerify(publicKey);
verifier.update(data);
return verifier.verify(signature);
}
}