一條連線、兩次交握:在不更動端點的情況下升級 TLS 加密
我的上一篇文章以一個論點作結:路徑上的一個元件可以對一端說後量子、對另一端說傳統密碼,在兩端皆不變更的情況下升級連線。這聽起來不該成立—TLS 是端到端加密且可偵測竄改的。訣竅在於:根本沒有單一的連線。而是兩條,縫合在一起。
你無法在傳輸途中修改 TLS 交握
TLS 刻意把自己綁在一起。每個交握訊息都會餵入一個持續累積的轉錄雜湊 (transcript hash),而最後的 Finished 訊息是對該轉錄的 MAC—只要更動金鑰交換的一個位元組,MAC 就會失敗,交握隨之中止。接著每一個應用資料記錄,都以源自該交握的金鑰、用 AEAD 標籤封印。因此你無法坐在中間、在一條活躍的連線上「把 X25519 改寫成 X25519MLKEM768」。數學不允許。要改變加密,你必須終止這條連線,再開另一條。
兩個端點,一個代理程式
TLS 接合 (splice) 正是如此:兩條完整、獨立的 TLS 工作階段,代理程式位於中間,在兩側都扮演完整的端點。
client ⇄ [ agent ] ⇄ server
session A session B
classical X25519 X25519MLKEM768
對客戶端而言,代理程式就是伺服器。它執行完整的伺服器端交握,協商客戶端所支援的任何方式—例如 TLS 1.2 上的傳統 X25519。對伺服器而言,代理程式就是客戶端。它執行另一個獨立的客戶端交握,協商伺服器所支援的任何方式—例如 TLS 1.3 上的 X25519MLKEM768。
兩次交握、兩套金鑰排程、兩組流量金鑰。應用資料從工作階段 A 解密出來,再重新加密進工作階段 B,逐一記錄地進行。客戶端以為它直接連到了伺服器;伺服器以為是客戶端連來的。兩端都沒改一行程式碼。
憑證才是關鍵所在
有一個決定成敗的關鍵:要對客戶端扮演「伺服器」,代理程式必須提出一張該伺服器名稱、且客戶端會接受的憑證—而 TLS 存在的目的,正是要阻止中間人這麼做。因此接合只在操作者明確建立信任的情況下才成立:
出站 (Outbound)(升級你的客戶端發起的連線):代理程式即時為請求的名稱簽發一張葉憑證,由你的客戶端已信任的 CA 簽署。由操作者佈建—而非偽造的公開憑證。
入站 (Inbound)(你經營該伺服器):你將真實的伺服器憑證與金鑰交給代理程式。不需簽發;它呈現真正的憑證。
無論哪種方式,信任都是刻意授予的。沒有它,客戶端會正確地拒絕—這正是系統依設計運作的表現。
為什麼這不只是「代理 (proxy)」
TCP 代理盲目地搬移位元組。TLS 接合則是兩次完整的 TLS 端點:它解析並重建每一個交握訊息、執行金鑰排程、處理 AEAD,並調和兩組可能在版本、加密套件、金鑰交換群組與擴充上都不同的協商參數。有趣的工程在於這個不一致—一端是提供 TLS 1.2 傳統密碼的客戶端,另一端是想要 TLS 1.3 加 ML-KEM 的伺服器。代理程式必須遞給每一側一個它認為完全合理的交握,同時橋接兩者之間的落差。
而且必須精準。若重建的上游 ClientHello 所提供的群組與你產生的金鑰不符,或轉錄雜湊錯了一個位元組,Finished MAC 就會失敗,交握隨之崩潰。TLS 交握裡沒有「大致正確」這回事。
一次一條連線
這就是「在不更動端點的情況下遷移」背後的機制。無論它運行於核心資料路徑的內聯模式,或是使用者空間的代理模式,形態都相同:終止、協商兩側各自能做到的最佳方式、在兩者之間轉譯。一個混合的環境—現代的客戶端、老舊的來源端、你無法升級的設備—就變成你可以逐一連線、依自己進度遷移的東西,而不是一次全部、或乾脆不做。
這就是後量子遷移作為一場長達數年的「打掉重練」,與作為一個你能在封包層解決的工程問題之間的差別。若你想知道在自己的流量上這會是什麼樣子,那正是我的起點。
← 所有文章