iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

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

Day35 - 現代網路安全基石:TLS 1.3 協定解析

  • 分享至 

  • xImage
  •  

前言

在前面的 33 天裡,我們分別深入研究了對稱加密(AES-GCM)、雜湊(SHA-3)、金鑰交換(ECDHE)、數位簽章(RSA/ECDSA)以及 PKI 憑證體系(X.509/KeyStore)。這些密碼學原語(Primitives)就像一塊塊樂高積木,而將這些積木完美組裝在一起,保護全球數十億網際網路連線的終極藝術品,就是 TLS(Transport Layer Security,傳輸層安全協定)。

在 2018 年發布的 TLS 1.3 (RFC 8446),是密碼學工程史上的一次重大重構。它大刀闊斧地砍掉了過去 20 年累積的歷史包袱與不安全設計,帶來了前所未有的效能與安全性。

本章將從密碼學的核心概念 密碼套件(Cipher Suite) 與 協商主導權 談起,詳細剖析 TLS 1.2 的運作機制,並與 TLS 1.3 進行深度對比。

一、 核心概念:什麼是密碼套件 (Cipher Suite)?

當 Client 與 Server 要建立 TLS 連線時,兩者必須先對「這次連線要用什麼密碼演算法」達成共識。這套演算法的組合,就稱為 密碼套件(Cipher Suite)。

一個密碼套件決定了握手與加密通訊過程中的 4 個關鍵要素:

  1. 金鑰交換演算法(Key Exchange):如何安全地協商出對稱金鑰。
  2. 身份驗證與簽章演算法(Authentication):如何驗證伺服器(或客戶端)的憑證真實性。
  3. 對稱加密演算法與工作模式(Symmetric Encryption):如何加密後續傳輸的應用層資料。
  4. 訊息鑑別碼 / 虛擬隨機函數(MAC / PRF):如何確保資料完整性與衍生金鑰。

1. 經典 TLS 1.2 密碼套件拆解

在 TLS 1.2 中,密碼套件的命名非常長且具備固定格式。例如:

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

  TLS _ ECDHE _ RSA _ WITH _ AES_256_GCM _ SHA384
  ───   ─────   ───        ───────────   ──────
   │      │      │              │          │
   │      │      │              │          └── 4. 訊息雜湊 / PRF 演算法 (SHA-384)
   │      │      │              └──────────── 3. 對稱加密與模式 (AES-256-GCM)
   │      │      └─────────────────────────── 2. 憑證簽章驗證 (RSA)
   │      └────────────────────────────────── 1. 金鑰交換機制 (ECDHE)
   └───────────────────────────────────────── 協定前綴

在 TLS 1.2 中,由於演算法組合高達數百種,導致協商邏輯極度複雜,甚至容易因允許使用過時演算法(如 RSA 金鑰交換或 CBC 模式)而引發降級攻擊(Downgrade Attacks)。

2. TLS 1.3 正名革命:解耦與精簡

TLS 1.3 將金鑰交換與身份驗證從密碼套件中獨立拆分出去,交由 Extensions(擴充欄位)單獨協商。因此,TLS 1.3 的密碼套件僅代表「對稱加密 + 訊息雜湊」,全世界只剩下 5 個標準套件:

  1. TLS_AES_128_GCM_SHA256 (預設廣泛支援)
  2. TLS_AES_256_GCM_SHA384 (高安全等級)
  3. TLS_CHACHA20_POLY1305_SHA256 (適用於缺乏 AES 硬體加速的行動裝置)
  4. TLS_AES_128_CCM_SHA256
  5. TLS_AES_128_CCM_8_SHA256

3. 關鍵機制:誰決定了 Cipher Suite? (TLS 1.2 vs. TLS 1.3 協商主導權)

不論是在 TLS 1.2 還是 TLS 1.3,最終決定選擇哪一個 Cipher Suite 的,永遠都是伺服器端(Server)。然而,兩者在「如何選擇」與「選哪一部分」的機制上有著顯著差異:

【TLS 1.2 的決定機制:Proposal + Server Select】

  1. Client 發起提案:客戶端在 ClientHello 中發送一份自己支援的 Cipher Suite 列表(例如帶有 30 個套件,並依照 Client 自己的偏好排序)。
  2. Server 進行裁決:伺服器收到列表後,比對自身支援的套件,在 ServerHello 中回傳唯一確定的一個 Cipher Suite。
  3. 主導權與偏好設定(Server Order vs. Client Order):
  • 預設情況(Client Preference):伺服器通常會從 Client 給的列表中,挑選 Client 排在最前面的那一個。
  • 安規最佳實務(Server Preference / Honor Cipher Order):若伺服器開啟了「伺服器優先」設定(例如 Java 的 sslParameters.setUseCipherSuitesOrder(true) 或 Nginx 的 ssl_prefer_server_ciphers on),Server 會無視 Client 的排序,強制從交集項目中選出 Server 自己認為安全性最高(如強迫使用 ECDHE + AES-GCM)的套件,防範惡意 Client 故意選用弱加密法。

【TLS 1.3 的決定機制:二段式選定與 Key Share】

  1. 解耦的決策過程:Client 在 ClientHello 中發送 TLS 1.3 Cipher Suite 列表(僅代表 AEAD+Hash),同時發送 supported_groups(如 X25519、secp256r1)以及預先產生的 key_share(金鑰交換公鑰)。
  2. Server 做雙重決定:Server 必須選出「一組 Cipher Suite」 + 「一組 Key Share 橢圓曲線」。
  3. HelloRetryRequest (HRR) 退回機制:如果 Server 想要使用的金鑰交換群組(例如 secp256r1)不在 Client 預先發送的 key_share 猜測中(但包含在 Client 宣告支援的 supported_groups 列表中),Server 會發送 HelloRetryRequest,要求 Client 重新發送指定群組的 Key Share(這會增加 1 個 RTT,但決策權依然在 Server 手中)。

二、 經典 TLS 1.2 運作流程解析 (2-RTT)

為了理解 TLS 1.3 的偉大之處,我們必須先回顧 TLS 1.2 的握手流程。TLS 1.2 需要 2 個往返時間(2-RTT, Round-Trip Time) 才能完成連線建立與金鑰協商。

TLS 1.2 握手時序圖 (以 ECDHE 金鑰交換為例)

[ Client ]                                                    [ Server ]
    │                                                             │
    │ 1. ClientHello (支援的 TLS1.2, Cipher Suites, ClientRandom)  │
    ├────────────────────────────────────────────────────────────>│
    │                                                             │
    │ 2. ServerHello (選定 Cipher Suite, ServerRandom)            │
    │ 3. Certificate (傳送伺服器 X.509 憑證)                       │
    │ 4. ServerKeyExchange (傳送 ECDH 公鑰 Qs + 伺服器私鑰簽章)    │
    │ 5. ServerHelloDone                                          │
    |<────────────────────────────────────────────────────────────┤  <-- RTT 1 完成
    │                                                             │
    │ (驗證 Certificate & 簽章)                                   │
    │ (生成 ECDH 客戶端金鑰對 Qc)                                 │
    │ (計算 Pre-Master Secret -> Master Secret -> AES 對稱金鑰)   │
    │                                                             │
    │ 6. ClientKeyExchange (傳送 ECDH 客戶端公鑰 Qc)              │
    │ 7. ChangeCipherSpec (通知後續切換為加密傳輸)                │
    │ 8. Finished (傳送前述握手訊息的 Hash 簽章供驗證)            │
    ├────────────────────────────────────────────────────────────>│
    │                                                             │
    │                               (計算 Master Secret & AES 金鑰)│
    │ 9. ChangeCipherSpec                                         │
    │ 10. Finished                                                │
    |<────────────────────────────────────────────────────────────┤  <-- RTT 2 完成
    │                                                             │
    │<========== (握手完成!開始傳送 HTTP/Application Data) =======>│

TLS 1.2 的兩大核心痛點

  1. 握手延遲過高 (2-RTT):在應用層資料(如 HTTP Request)傳送前,網路必須先來回跑兩趟。在跨國網路或高延遲環境下,會產生明顯的卡頓感。
  2. RSA 金鑰交換缺乏前向機密性 (PFS):TLS 1.2 允許舊式 RSA 金鑰交換(Client 用 Server 公鑰直接加密 Pre-Master Secret 傳給 Server)。如果攻擊者錄下現有側錄封包,並在 5 年後偷到 Server 私鑰,就能解密過去歷史上記錄到的所有通訊內容!

三、 TLS 1.3 革命:握手流程與 TLS 1.2 深度對比

TLS 1.3 在 2018 年被發布時,帶來了密碼學史上最劇烈的重構:1-RTT 握手 與 強制前向機密性。

TLS 1.3 握手時序圖 (1-RTT)

TLS 1.3 採用「預測型」握手。Client 在發送 ClientHello 時,就主動帶上預先產生的 ECDH 公鑰(Key Share),將金鑰協商壓進 1 個 RTT 內:

[ Client ]                                                    [ Server ]
    │                                                             │
    │ 1. ClientHello (支援 TLS1.3, 簡化版 Cipher Suites)           │
    │    + KeyShare (預先生成的 ECDH 公鑰 Qc)                      │
    ├────────────────────────────────────────────────────────────>│
    │                                                             │
    │                               (生成 ECDH 伺服器公鑰 Qs)     │
    │                               (計算 Shared Secret -> AES金鑰)│
    │                                                             │
    │ 2. ServerHello + KeyShare (傳送 ECDH 公鑰 Qs)               │
    │ 3. EncryptedExtensions (⚠️ 從此處開始,後續通訊全部已加密!) │
    │ 4. Certificate (伺服器 X.509 憑證)                          │
    │ 5. CertificateVerify (用伺服器私鑰對握手資料簽章)            │
    │ 6. Finished (握手確認)                                      │
    |<────────────────────────────────────────────────────────────┤  <-- RTT 1 完成
    │                                                             │
    │ (驗證憑證與 CertificateVerify 簽章)                         │
    │ (計算 Shared Secret -> AES金鑰)                             │
    │ 7. Finished                                                 │
    ├────────────────────────────────────────────────────────────>│
    │                                                             │
    │<========== (握手完成!開始傳送 Application Data) ===========>│

💡 隱私大躍進:在 TLS 1.3 中,從 EncryptedExtensions 開始,連 Server 的憑證傳輸(第 4 步)都是加密的!這阻止了中間監聽者透過側錄憑證來偷看用戶到底在訪問哪個網站。

TLS 1.2 vs TLS 1.3 綜合對比表

比較維度 TLS 1.2 TLS 1.3
握手延遲 (RTT) 2-RTT(需來回 2 次才能傳資料) 1-RTT(重連甚至支援 0-RTT Early Data)
Cipher Suite 決定權 Server 決定(可設定是否遵從 Client 偏好順序) Server 決定(且可透過 HRR 強制指定 Key Share 群組)
金鑰交換演算法 支援 RSA, DH, DHE, ECDHE 僅支援 ECDHE / DHE(強制具備前向機密性 PFS)
靜態 RSA 金鑰交換 支援(存在私鑰洩漏後歷史資料被解密風險) 徹底廢除
對稱加密模式 支援 Stream (RC4)、Block (CBC) 與 AEAD 僅保留 AEAD 認證加密 (GCM, ChaCha20-Poly1305)
不安全雜湊 支援 MD5, SHA-1 徹底廢除,僅保留 SHA-256 / SHA-384
憑證傳輸狀態 明文傳輸(可被第三方監聽獲取 Subject) 加密傳輸(保護用戶隱私)
密碼套件結構 包含 4 要素(極度繁雜) 僅保留 (對稱加密 + 雜湊),極致精簡

四、 單向認證 (1-Way TLS) vs 雙向認證 (mTLS)

在實作 Java 程式碼前,我們必須釐清兩種 TLS 驗證架構:

  1. 單向認證 (1-Way TLS):最常見的情境(如瀏覽器訪問 Google)。只有客戶端會去驗證伺服器的憑證。伺服器必須配置 KeyStore(裝私鑰與憑證),客戶端配置 TrustStore(裝信任的 Root CA)。
  2. 雙向認證 (Mutual TLS, mTLS):零信任架構(Zero Trust)、微服務網格(Service Mesh)與高安規 B2B 連線的標配。伺服器與客戶端互相驗證對方的憑證。雙方都必須同時具備 KeyStore(證明自己是誰)與 TrustStore(決定信任誰)。

五、 Java JCA 實戰:建構 TLS 1.3 Client / Server

接下來,我們將使用 Day 31 與 Day 32 製作的 PKCS12 檔案,建立一個強迫使用 TLS 1.3 的 Java Socket Server 與 Client。

1. 核心類別解析 (JSSE API)

Java Secure Socket Extension (JSSE) 提供以下核心類別:

  • KeyManagerFactory:負責讀取 KeyStore,在握手時把憑證丟給對方,並使用私鑰進行 CertificateVerify 簽章。
  • TrustManagerFactory:負責讀取 TrustStore,在收到對方的憑證時,驗證其信任鏈(PKIX)與有效性。
  • SSLContext:TLS 引擎的核心,負責將上述兩者結合,並產生 SSLSocket。

2. TLS 1.3 Server 實作

import javax.net.ssl.*;
import java.io.*;
import java.security.KeyStore;

public class Tls13Server {
    public static void main(String[] args) throws Exception {
        System.out.println("=== 啟動 TLS 1.3 Server ===");

        // 1. 載入伺服器的 KeyStore (包含 Server 私鑰與憑證)
        char[] password = "StorePassword2026!".toCharArray();
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream("server-keystore.p12")) {
            keyStore.load(fis, password);
        }

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, password);

        // 2. 初始化 SSLContext (指定 TLS 1.3)
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        // 單向認證:Server 只需要 KeyManager,不需要 TrustManager
        sslContext.init(kmf.getKeyManagers(), null, null);

        // 3. 建立 SSLServerSocket 並綁定 Port
        SSLServerSocketFactory ssf = sslContext.getServerSocketFactory();
        try (SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(8443)) {
            // 強制僅啟用 TLS 1.3
            serverSocket.setEnabledProtocols(new String[]{"TLSv1.3"});

            // (可選) 強制開啟伺服器偏好排序 (Server Preference)
            SSLParameters params = serverSocket.getSSLParameters();
            params.setUseCipherSuitesOrder(true);
            serverSocket.setSSLParameters(params);

            System.out.println("伺服器正在 Port 8443 監聽 (僅限 TLS 1.3)...");

            while (true) {
                try (SSLSocket clientSocket = (SSLSocket) serverSocket.accept();
                     BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                     PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {

                    System.out.println("客戶端已連線,協定版本: " + clientSocket.getSession().getProtocol());
                    System.out.println("使用的密碼套件: " + clientSocket.getSession().getCipherSuite());

                    String msg = in.readLine();
                    System.out.println("收到訊息: " + msg);
                    out.println("伺服器已安全收到您的訊息: " + msg);
                } catch (Exception e) {
                    System.err.println("客戶端連線錯誤: " + e.getMessage());
                }
            }
        }
    }
}

3. TLS 1.3 Client 實作

import javax.net.ssl.*;
import java.io.*;
import java.security.KeyStore;

public class Tls13Client {
    public static void main(String[] args) throws Exception {
        System.out.println("=== 啟動 TLS 1.3 Client ===");

        // 1. 載入客戶端的 TrustStore (包含信任的 Root/中繼憑證)
        // 如果是自簽憑證,這裡必須載入包含該憑證的 TrustStore,否則會觸發 PKIX 錯誤
        char[] password = "changeit".toCharArray();
        KeyStore trustStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream("client-truststore.p12")) {
            trustStore.load(fis, password);
        }

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        // 2. 初始化 SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        // 單向認證:Client 只需要 TrustManager 驗證別人,不需要 KeyManager 證明自己
        sslContext.init(null, tmf.getTrustManagers(), null);

        // 3. 建立 SSLSocket 並連線至 Server
        SSLSocketFactory sf = sslContext.getSocketFactory();
        try (SSLSocket socket = (SSLSocket) sf.createSocket("localhost", 8443)) {
            // 強制僅啟用 TLS 1.3
            socket.setEnabledProtocols(new String[]{"TLSv1.3"});
      
            // 啟用端點識別 (Endpoint Identification) - 強制檢查 SAN 域名是否匹配
            SSLParameters sslParams = socket.getSSLParameters();
            sslParams.setEndpointIdentificationAlgorithm("HTTPS");
            socket.setSSLParameters(sslParams);

            socket.startHandshake();
            System.out.println("TLS 握手成功!協定: " + socket.getSession().getProtocol());

            // 4. 傳送與接收資料 (資料此時已被 AES-GCM 加密)
            try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
           
                out.println("Hello TLS 1.3 Server! 這是來自 Java 的極密訊息。");
                System.out.println("伺服器回應: " + in.readLine());
            }
        }
    }
}

上一篇
Day34 - 密碼學的工具箱:OpenSSL 與 JDK keytool
下一篇
Day36 - 硬體金鑰保護:HSM、智慧卡與 USB Token
系列文
我是Java工程師,關於密碼學我想懂的不多 共 37 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言