iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

我是Java工程師,關於密碼學我想懂的不多系列 第 32 篇

Day31 - 數位簽章 (Digital Signature):不可否認性與 Ed25519 / RSA-PSS 實戰

  • 分享至 

  • xImage
  •  

前言

在前面的章節中,我們主要聚焦於資料的「機密性(Confidentiality)」——如何透過 AES-GCM 或 ECIES 混合加密讓竊聽者無法讀取訊息內容。

然而,在零信任與開放網路架構中,單靠機密性是不夠的。我們還面臨三個關鍵問題:

  1. 身份驗證(Authentication):這封訊息真的是聲稱的傳送者發出的嗎?
  2. 資料完整性(Integrity):訊息在傳輸過程中是否被篡改過?
  3. 不可否認性(Non-repudiation):傳送者在發送訊息後,事後能否抵賴、否認該行為?

對稱加密與 MAC 雖然能提供完整性與驗證,但因為金鑰為雙方共享,無法區分是哪一方產生的訊息,因此無法提供「不可否認性」。

為解決這個難題,數位簽章(Digital Signature) 應運而生。本章將解析數位簽章的原理,剖析歷史上因簽章漏洞發生的資安浩劫(Sony PS3 事件),並透過 Java JCA 帶領讀者實戰現代兩大主流簽章標準——Ed25519 與 RSA-PSS。

一、 數位簽章的核心機制:先雜湊再簽署 (Hash-then-Sign)

非對稱加密與數位簽章在數學運算上互為逆過程,但其工程目的與使用的金鑰完全相反:

  【非對稱加密 (Encryption)】               【數位簽章 (Signature)】
   目標:確保機密性                         目標:不可否認性與身份驗證
   
   [ 傳送方 ] 使用【接收方公鑰】加密          [ 簽署方 ] 使用【自己私鑰】簽署
        │                                        │
        ▼                                        ▼
   [ 接收方 ] 使用【自己私鑰】解密          [ 驗證方 ] 使用【簽署方公鑰】驗驗

為什麼工程上必須採用「先雜湊再簽署 (Hash-then-Sign)」?

在實際系統中,我們絕不直接對原始大檔訊息進行私鑰簽署,而是先將訊息輸入密碼學雜湊函數(如 SHA-256),對其摘要(Digest)進行簽署:

[ 原始訊息 M ] ──> [ SHA-256 / SHA-512 ] ──> [ 訊息摘要 Digest (32/64 bytes) ]
                                                        │
                                                        ▼
[ 數位簽章 Signature ] <── [ 私鑰簽署運算 (Ed25519 / RSA-PSS) ]

  1. 效能優化:非對稱數學運算開銷極高,將任意長度的資料壓縮為固定長度(如 32 位元組)的摘要後再運算,能提升數千倍效能。
  2. 安全性防禦:避免攻擊者利用非對稱演算法的代數同態特性(如 RSA 的乘法同態),透過構造特定明文欺騙簽署引擎。

二、 RSA 簽章演進:從 PKCS#1 v1.5 到 RSA-PSS

RSA 簽章演算法經歷了顯著的安全演進:

1. 舊型 PKCS#1 v1.5 簽章的致命缺陷

傳統的 RSA PKCS#1 v1.5 簽章 是確定性的(Deterministic)。若實現不當(如未嚴格檢查 Padding 結構),容易遭受 Bleichenbacher 簽章偽造攻擊 或 Fault Attack。現代資安規範(如 NIST SP 800-56B)已強烈建議停止在新型系統中使用 PKCS#1 v1.5 簽章。

2. 現代黃金標準:RSA-PSS (Probabilistic Signature Scheme)

RSA-PSS(RFC 8017 / PKCS#1 v2.2) 引入了隨機鹽值(Salt)與 MGF1 遮蔽函數,將確定性簽章轉化為機率性簽章(Probabilistic Signature):

[ Message Hash ] ──> [ Padding & Salt 遮蔽 (MGF1) ] ──> [ EM ] ──> [ RSA 私鑰指數運算 (s = EMᵈ mod n) ]

  • 全域隨機化:即便對同一份檔案簽署 100 次,產出的簽章值也完全不同。
  • 可證明安全性(Provable Security):在隨機預言機模型(Random Oracle Model)下,RSA-PSS 被數學證明嚴格等價於大整數分解難題(IFP)。

三、 ECC 簽章演進:ECDSA 的 k-nonce 陷阱與 Ed25519 的崛起

1. 歷史級資安浩劫:Sony PS3 主機私鑰洩漏事件

傳統橢圓曲線簽章 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)失效的問題。

2. 新一代簽章王者:Ed25519 (EdDSA)

為了徹底解決 ECDSA 的複雜性與側信道威脅,密碼學家 Daniel J. Bernstein 等人提出了 Ed25519(Edwards-curve Digital Signature Algorithm,RFC 8032)。

Ed25519 建立在 Curve25519 / Edwards25519 扭曲愛德華曲線上,具備以下革命性優勢:

  1. 天然免疫 k-nonce 洩漏:Ed25519 內部自動使用私鑰與訊息的 Hash 衍生 nonce,不依賴外部隨機數發生器,天然具備 RFC 6979 特性。
  2. 常數時間執行(Constant-Time):所有點運算均不包含條件分支與動態記憶體查表,徹底免疫時序側信道攻擊(Timing Side-Channel Attacks)。
  3. 極致極速與小尺寸:
  • 私鑰尺寸:32 Bytes
  • 公鑰尺寸:32 Bytes
  • 簽章尺寸:64 Bytes
  • 簽驗簽速度比傳統 RSA-3072 快數十倍,且支援高併發批次驗簽(Batch Verification)。

四、 Java JCA 生產級實戰:Ed25519 與 RSA-PSS 簽驗簽

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);
    }
}


上一篇
Day30 - 金鑰交換協定:Diffie-Hellman (DH) 與 ECDH 金鑰協商實務
系列文
我是Java工程師,關於密碼學我想懂的不多 共 32 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言