在前面的 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 進行深度對比。
當 Client 與 Server 要建立 TLS 連線時,兩者必須先對「這次連線要用什麼密碼演算法」達成共識。這套演算法的組合,就稱為 密碼套件(Cipher Suite)。
一個密碼套件決定了握手與加密通訊過程中的 4 個關鍵要素:
在 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)。
TLS 1.3 將金鑰交換與身份驗證從密碼套件中獨立拆分出去,交由 Extensions(擴充欄位)單獨協商。因此,TLS 1.3 的密碼套件僅代表「對稱加密 + 訊息雜湊」,全世界只剩下 5 個標準套件:
TLS_AES_128_GCM_SHA256 (預設廣泛支援)TLS_AES_256_GCM_SHA384 (高安全等級)TLS_CHACHA20_POLY1305_SHA256 (適用於缺乏 AES 硬體加速的行動裝置)TLS_AES_128_CCM_SHA256
TLS_AES_128_CCM_8_SHA256
不論是在 TLS 1.2 還是 TLS 1.3,最終決定選擇哪一個 Cipher Suite 的,永遠都是伺服器端(Server)。然而,兩者在「如何選擇」與「選哪一部分」的機制上有著顯著差異:
ClientHello 中發送一份自己支援的 Cipher Suite 列表(例如帶有 30 個套件,並依照 Client 自己的偏好排序)。ServerHello 中回傳唯一確定的一個 Cipher Suite。sslParameters.setUseCipherSuitesOrder(true) 或 Nginx 的 ssl_prefer_server_ciphers on),Server 會無視 Client 的排序,強制從交集項目中選出 Server 自己認為安全性最高(如強迫使用 ECDHE + AES-GCM)的套件,防範惡意 Client 故意選用弱加密法。ClientHello 中發送 TLS 1.3 Cipher Suite 列表(僅代表 AEAD+Hash),同時發送 supported_groups(如 X25519、secp256r1)以及預先產生的 key_share(金鑰交換公鑰)。key_share 猜測中(但包含在 Client 宣告支援的 supported_groups 列表中),Server 會發送 HelloRetryRequest,要求 Client 重新發送指定群組的 Key Share(這會增加 1 個 RTT,但決策權依然在 Server 手中)。為了理解 TLS 1.3 的偉大之處,我們必須先回顧 TLS 1.2 的握手流程。TLS 1.2 需要 2 個往返時間(2-RTT, Round-Trip Time) 才能完成連線建立與金鑰協商。
[ 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.3 在 2018 年被發布時,帶來了密碼學史上最劇烈的重構: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 | 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 要素(極度繁雜) | 僅保留 (對稱加密 + 雜湊),極致精簡 |
在實作 Java 程式碼前,我們必須釐清兩種 TLS 驗證架構:
KeyStore(裝私鑰與憑證),客戶端配置 TrustStore(裝信任的 Root CA)。KeyStore(證明自己是誰)與 TrustStore(決定信任誰)。接下來,我們將使用 Day 31 與 Day 32 製作的 PKCS12 檔案,建立一個強迫使用 TLS 1.3 的 Java Socket Server 與 Client。
Java Secure Socket Extension (JSSE) 提供以下核心類別:
KeyManagerFactory:負責讀取 KeyStore,在握手時把憑證丟給對方,並使用私鑰進行 CertificateVerify 簽章。TrustManagerFactory:負責讀取 TrustStore,在收到對方的憑證時,驗證其信任鏈(PKIX)與有效性。SSLContext:TLS 引擎的核心,負責將上述兩者結合,並產生 SSLSocket。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());
}
}
}
}
}
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());
}
}
}
}