iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

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

Day30 - 金鑰交換協定:Diffie-Hellman (DH) 與 ECDH 金鑰協商實務

  • 分享至 

  • xImage
  •  

前言

在對稱加密(如 AES-GCM)中,通訊雙方必須持有相同的金鑰才能進行加解密。然而,在零信任的網際網路中,我們面臨著一個世紀難題——「如何讓兩台從未見過面的機器,在滿是駭客監聽的開放通道上,安全地產生出一把只有他們兩人知道的對稱金鑰?」

若直接傳送金鑰,竊聽者即可輕鬆攔截;若採用 RSA 加密傳送,則會有金鑰過長、效率低下以及缺乏前向安全性(Forward Secrecy)等問題。

1976 年,Whitfield Diffie 與 Martin Hellman 提出了劃時代的 Diffie-Hellman (DH) 金鑰交換協定,允許雙方各自產生私鑰,透過交換公鑰,在不傳遞任何秘密的前提下,共同計算出相同的「共享秘密(Shared Secret)」。本章將帶領讀者從經典 DH 演進至現代 TLS 1.3 的核心——ECDH / ECDHE,並以 Java JCA 打造生產級的金鑰協商與 ECIES 混合加密系統。

一、 Diffie-Hellman (DH) 協定原理與局限

1. 經典 DH 的數學證明(有限域乘法群)

在數學上,DH 利用了 Day 28 介紹的離散對數難題(DLP):

  1. 公開參數:雙方約定一個大質數 p 與一個生成元 g。
  2. 生成金鑰:
  • Alice 產生私鑰 a,計算公鑰:A ≡ g^a (mod p)。
  • Bob 產生私鑰 b,計算公鑰:B ≡ g^b (mod p)。
  1. 公開交換:Alice 將 A 傳給 Bob,Bob 將 B 傳給 Alice。
  2. 計算共享秘密:
  • Alice 計算:S_{Alice} ≡ B^a ≡ (g^b)^a ≡ g^{ba} (mod )p
  • Bob 計算:S_{Bob} ≡ A^b ≡ (g^a)^b ≡ g^{ab} (mod )p

因為在同餘代數中 (g^b)^a ≡ (g^a)^b ≡ g^{ab} (mod p,雙方算出的共享秘密 S 完全一致)!

2. 經典 DH 的局限

經典 DH 的安全性建立在大整數離散對數難題(DLP)上。由於數域篩法(GNFS)的發展,要達到現代 128-bit 安全強度,經典 DH 的模數 p 必須高達 3072 bits。這導致:

  • 計算負擔重:3072-bit 大數模次方運算極度耗費 CPU 時間。
  • 網路開銷大:每次握手需要傳輸數百位元組的模數與公鑰。

二、 橢圓曲線 Diffie-Hellman (ECDH)

為了解決經典 DH 的效能瓶頸,現代密碼學將運算舞台從「整數模運算」轉移至 Day 29 學到的「橢圓曲線點群」,升級為 ECDH(Elliptic Curve Diffie-Hellman)。

1. 數學結構映射

ECDH 完全繼承了 DH 的邏輯,只是將「模次方運算」替換為「橢圓曲線純量乘法」:

經典 DH: g^a mod p => ECDH: k_A * G

2. ECDH 數學推導與證明

  1. 公開參數:選定標準橢圓曲線 E 及基準點 G。
  2. 金鑰對生成:
  • Alice 產生私密純量 k_A,計算公鑰點:Q_A = k_A * G。
  • Bob 產生私密純量 k_B,計算公鑰點:Q_B = k_B * G。
  1. 公開交換:Alice 傳送座標點 Q_A 給 Bob,Bob 傳送座標點 Q_B 給 Alice。
  2. 計算共享點(Shared Point):
  • Alice 計算:S_{Alice} = k_A * Q_B = k_A (k_B * G) = k_A k_B G
  • Bob 計算:S_{Bob} = k_B * Q_A = k_B (k_A * G) = k_B k_A G

推導結論:S_{Alice} = S_{Bob} = S = (x_S, y_S)。雙方成功在不傳遞私鑰的情況下,推導出曲線上的同一個點 S!

3. DH 與 ECDH 完整規格對照表

比較維度 經典 Diffie-Hellman (DH) 橢圓曲線 Diffie-Hellman (ECDH)
底層數學難題 離散對數難題 (DLP) 橢圓曲線離散對數難題 (ECDLP)
私鑰形式 隨機大整數 a 隨機純量 k_A
公鑰形式 大整數 A ≡ g^a (mod p 曲線座標點 Q_A = k_A * G )
共享秘密計算 S ≡ B^a (mod p 共享曲線點 S = k_A * Q_B )
128-bit 強度金鑰長度 3072 bits 256 bits (如 secp256r1 / X25519)
傳輸封包與計算效能 封包大、CPU 負擔重 金鑰極小、計算速度快數十倍

三、現代密碼學運作——混合加密 (Hybrid Cryptography)

理解了 DH/ECDH 的運算後,我們必須建立現代資安架構的最核心認知:現代系統「幾乎不直接使用非對稱加密」來加密實際的明文訊息。

網際網路傳輸普遍採用 「非對稱金鑰協商 + 對稱資料加密」 的混合加密架構(Hybrid Cryptography):

  【非對稱金鑰協商階段 (Asymmetric Phase)】
  Alice                                                 Bob
    │                                                    │
    ├───── 1. 透過 DH/ECDH 交換公鑰 ──────────────────────┤
    │                                                    │
    ▼                                                    ▼
  算得相同 Shared Secret                              算得相同 Shared Secret
    │                                                    │
    └───── 2. 透過 HKDF 鍛造出對稱金鑰 (如 AES Key) ───────┘
                               │
                               ▼
  【對稱資料加密階段 (Symmetric Phase)】
  Alice                                                 Bob
    │                                                    │
    ├───── 3. 使用 AES-GCM 加密大規模資料 (Ciphertext) ───>│
    │                                                    │
    │                                                    ▼
    │                                              使用 AES-GCM 解密

為什麼一定要用「混合加密」?

  1. 效能差異懸殊:非對稱加密(RSA / ECC)涉及高昂的模次方與橢圓曲線點乘運算,對稱加密(AES / ChaCha20)則擁有硬體指令集加速(AES-NI)。對稱加密的速度是非對稱加密的 1000 倍以上。
  2. 資料長度限制:非對稱加密(如 RSA-OAEP)單次能加密的長度受限於金鑰位元數,且 ECC 的公鑰點本身根本無法直接對任意長度文字進行算術加密。
  3. 分工明確的黃金組合:
  • DH / ECDH(非對稱):專門解決「不安全通道上的對稱金鑰分配問題」。
  • AES-GCM(對稱):專門解決「大容量資料高速傳送與防篡改問題」。

四、 中間人攻擊 (MITM) 與前向安全性 (PFS / ECDHE)

儘管 ECDH 在數學上非常優雅,但在實際網路工程中,它面臨兩個致命威脅:

1. 未認證金鑰交換與中間人攻擊(MITM Attack)

ECDH 協定本身是未經身分驗證(Unauthenticated)的。若駭客 Eve 潛伏在網路節點中:

[ Alice ] ──> (傳送 Q_A) ──> [ Eve 截獲 ] ──> (傳送 Q_Eve) ──> [ Bob ]
[ Alice ] <── (傳送 Q_Eve) <── [ Eve 攔截 ] <── (傳送 Q_B) <─── [ Bob ]

  • Alice 誤以為自己與 Bob 建立共享秘密,實則是與 Eve 建立 S_{Alice-Eve}。
  • Bob 誤以為自己與 Alice 建立共享秘密,實則是與 Eve 建立 S_{Bob-Eve}。
  • 防禦解法:ECDH 必須結合身分驗證機制(如使用 ECDSA / Ed25519 簽章或數位憑證 X.509 簽署公鑰),這也是 TLS 握手時伺服器必須出示憑證的原因。

2. 靜態 ECDH vs 暫態 ECDHE(Ephemeral ECDH)

  • Static ECDH(靜態):伺服器的 ECDH 金鑰對長期固定。若伺服器私鑰在數年後被洩露,攻擊者可拿出過去在網路上側錄的所有歷史封包,全部一次性破解!
  • ECDHE(Ephemeral,暫態):每次 TCP/TLS 連線時,雙方臨時生成一次性的動態金鑰對,握手完畢並導出對稱金鑰後,立即將記憶體中的暫態私鑰抹除。
  • 完美前向安全性(Perfect Forward Secrecy, PFS):即便伺服器的長效身份私鑰在未來洩漏,攻擊者也絕對無法解密過去已側錄的歷史流量。現代 TLS 1.3 已強制要求廢除傳統 RSA 與 Static DH,全面轉向 ECDHE。

五、 Java JCA 生產級實戰——ECDH + HKDF + AES-GCM (ECIES 機制)

致命資安禁忌:
ECDH 算出的共享點 S = (x_S, y_S),其 x 座標點的分佈在密碼學上並不具備完全均勻的隨機性。絕不能直接把 x 座標位元組拿來當作 AES 金鑰!
正確做法(ECIES 範式):必須將 ECDH 產生的原始共享秘密(Raw Shared Secret)輸入至 HKDF(HMAC-based Key Derivation Function) 衍生出指定長度且高熵的 AES 金鑰。

以下演示 Alice 與 Bob 透過 Java JCA KeyAgreement 完成 ECDH 金鑰協商、HKDF 金鑰衍生與 AES-GCM 混合加密的完整流程:

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import java.util.Base64;

public class EcdhEciesProductionExample {

    private static final String EC_CURVE = "secp256r1";
    private static final int GCM_TAG_LENGTH_BITS = 128;
    private static final int GCM_IV_LENGTH_BYTES = 12;

    public static void main(String[] args) throws Exception {
        System.out.println("=== 模擬 Alice 與 Bob 的 ECDH 金鑰協商與 ECIES 加密過程 ===");

        // Step 1: Alice 與 Bob 分別生成各自的 ECDH 暫態金鑰對 (ECDHE)
        KeyPair aliceKeyPair = generateEcKeyPair();
        KeyPair bobKeyPair = generateEcKeyPair();

        // Step 2: 雙方在不安全通道上公開交換公鑰 (Alice 傳 Q_A 給 Bob,Bob 傳 Q_B 給 Alice)
        PublicKey alicePublicKey = aliceKeyPair.getPublic();
        PublicKey bobPublicKey = bobKeyPair.getPublic();

        // Step 3: Alice 使用自己的私鑰 + Bob 的公鑰,計算 Shared Secret
        byte[] aliceSharedSecret = computeEcdhSharedSecret(aliceKeyPair.getPrivate(), bobPublicKey);

        // Step 4: Bob 使用自己的私鑰 + Alice 的公鑰,計算 Shared Secret
        byte[] bobSharedSecret = computeEcdhSharedSecret(bobKeyPair.getPrivate(), alicePublicKey);

        // 驗證雙方算出的 Shared Secret 是否完全一致
        boolean isMatch = Arrays.equals(aliceSharedSecret, bobSharedSecret);
        System.out.println("Alice 與 Bob 算出的 ECDH Raw Shared Secret 一致: " + isMatch);

        // Step 5: 透過 HKDF-SHA256 將 Raw Shared Secret 衍生為強化的 AES-256 對稱金鑰
        byte[] hkdfSalt = "ECDH-Day29-Salt".getBytes(StandardCharsets.UTF_8);
        byte[] hkdfInfo = "ECIES-AES-256-GCM-Key".getBytes(StandardCharsets.UTF_8);

        SecretKey aliceAesKey = deriveAesKeyViaHkdf(aliceSharedSecret, hkdfSalt, hkdfInfo);
        SecretKey bobAesKey = deriveAesKeyViaHkdf(bobSharedSecret, hkdfSalt, hkdfInfo);

        // Step 6: Alice 使用導出的 AES 金鑰加密訊息 (ECIES 加密)
        String plaintext = "Hello Bob! This is an ECIES protected message from Day 29.";
        byte[] iv = new byte[GCM_IV_LENGTH_BYTES];
        new SecureRandom().nextBytes(iv); // 生成隨機 IV

        byte[] ciphertext = encryptAesGcm(plaintext.getBytes(StandardCharsets.UTF_8), aliceAesKey, iv);
        System.out.println("\nAlice 傳送的 AES-GCM 密文 (Base64): " + Base64.getEncoder().encodeToString(ciphertext));

        // Step 7: Bob 使用導出的 AES 金鑰解密訊息
        byte[] decryptedBytes = decryptAesGcm(ciphertext, bobAesKey, iv);
        String decryptedMessage = new String(decryptedBytes, StandardCharsets.UTF_8);
        System.out.println("Bob 成功解密訊息: " + decryptedMessage);
    }

    /**
     * 生成 secp256r1 (NIST P-256) 金鑰對
     */
    private static KeyPair generateEcKeyPair() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC");
        keyPairGen.initialize(new ECGenParameterSpec(EC_CURVE));
        return keyPairGen.generateKeyPair();
    }

    /**
     * 使用 JCA KeyAgreement 計算 ECDH 共享秘密
     */
    private static byte[] computeEcdhSharedSecret(PrivateKey privateKey, PublicKey partnerPublicKey) throws Exception {
        KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
        keyAgreement.init(privateKey);
        keyAgreement.doPhase(partnerPublicKey, true);
        return keyAgreement.generateSecret();
    }

    /**
     * 輕量級 HKDF-SHA256 金鑰衍生 (HKDF-Extract & Expand)
     */
    private static SecretKey deriveAesKeyViaHkdf(byte[] ikm, byte[] salt, byte[] info) throws Exception {
        // 1. HKDF-Extract: PRK = HMAC-SHA256(Salt, IKM)
        Mac hmacExtract = Mac.getInstance("HmacSHA256");
        hmacExtract.init(new SecretKeySpec(salt, "HmacSHA256"));
        byte[] prk = hmacExtract.doFinal(ikm);

        // 2. HKDF-Expand: OKM = HMAC-SHA256(PRK, info || 0x01)
        Mac hmacExpand = Mac.getInstance("HmacSHA256");
        hmacExpand.init(new SecretKeySpec(prk, "HmacSHA256"));
        hmacExpand.update(info);
        hmacExpand.update((byte) 0x01); // Counter = 1
        byte[] okm = hmacExpand.doFinal();

        // 截取前 32 bytes (256 bits) 作為 AES-256 金鑰
        byte[] aesKeyBytes = Arrays.copyOf(okm, 32);
        return new SecretKeySpec(aesKeyBytes, "AES");
    }

    /**
     * AES-GCM 加密
     */
    private static byte[] encryptAesGcm(byte[] plaintext, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
        return cipher.doFinal(plaintext);
    }

    /**
     * AES-GCM 解密
     */
    private static byte[] decryptAesGcm(byte[] ciphertext, SecretKey key, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
        return cipher.doFinal(ciphertext);
    }
}


上一篇
Day29 - 橢圓曲線密碼學 (ECC) 數學基礎:橢圓曲線幾何與點運算
下一篇
Day31 - 數位簽章 (Digital Signature):不可否認性與 Ed25519 / RSA-PSS 實戰
系列文
我是Java工程師,關於密碼學我想懂的不多 共 32 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言