總覽與定位
TLS 不改動 TCP 的任何一個位元,卻讓同一條位元組串流從「裸奔」變成「加密、而且能證明對方是誰」。
TLS(Transport Layer Security)是一個插在 TCP 與應用協定之間的獨立層:
對下,它就是一個普通的 TCP 應用,照樣 connect()、照樣讀寫位元組;
對上,它提供一條介面幾乎一模一樣、但內容已被加密與驗證的通道。
正因為這個「夾層」設計,HTTP、WebSocket、MQTT 幾乎不必為了加密而改寫自己——把 socket 換成 TLS socket 就好。
前一篇的 TCP/UDP 篇停在一個明確的缺口:TCP 從頭到尾只保證「不掉、不亂」,沒保證「沒人看得懂」——
你寫進 socket 的每一個位元組,在路徑上的每一台路由器、每一個中間設備眼裡都是明文。
本篇補上這缺失的一層,而且完全不動 TCP:三向交握照做、序號與重傳照跑,TLS 只是在建連完成之後、應用資料開始之前,插入一段自己的交握。
另一條回扣線是 dns-deep-dive:DNS 把名稱換成 IP,但它從來沒有保證那個 IP 真的屬於那個名稱——
快取污染、惡意 Resolver、被劫持的回應都可能把你導向別處。TLS 的憑證驗證正是補這一刀:連上之後,對方必須拿出一張「這個名稱歸我」的憑證。
名稱同時是 DNS 查詢的輸入,也是整條信任鏈的起點。
三者缺一不可,而最常被跳過的是第三個。工程現場那句「反正先把憑證驗證關掉,之後再開」——
curl -k、rejectUnauthorized: false、verify=False——
等於把 TLS 從「安全通道」降級成「加密的猜謎遊戲」:你仍然有加密,只是不知道在跟誰加密。
把視角拉高一點會發現:HTTPS、WSS、MQTTS 這些名字裡的「S」,其實全是同一個 TLS。
| 名稱 | 實際組成 | 預設 port | 差異只在哪裡 |
|---|---|---|---|
| HTTPS | HTTP over TLS over TCP | 443 | TLS 交握中以 ALPN 協商 h2 或 http/1.1 |
| WSS | WebSocket over TLS | 443 | 先跑 HTTPS,再在同一條 TLS 通道上做 Upgrade |
| MQTTS | MQTT over TLS | 8883 | 換 port、常搭配 mTLS 做裝置身分 |
| gRPC | HTTP/2 over TLS | 443 | ALPN 必須協商到 h2 |
| DoT / DoH | DNS over TLS / HTTPS | 853 / 443 | 被保護的內容換成 DNS 查詢 |
這張表就是本篇在系列裡的角色:下面五列共用同一套交握、同一套憑證驗證、同一套 session resumption。HTTP、QUIC、WebSocket、MQTT 四篇談到「加密怎麼做」時,都會直接回頭引用本篇的結論。
TLS 1.3 交握:一次來回換一條加密通道
1-RTT 不是最佳化技巧,而是「把密鑰交換材料塞進第一則訊息」的結構性結果。
TLS 1.3(RFC 8446)把交握壓縮到一個 RTT——RTT 是封包往返一趟的時間,TCP/UDP 篇談三向交握時已經定義過。 它做得到的關鍵,在於一個果斷的決定:把所有非 (EC)DHE 的密鑰交換方式全部刪掉。 既然一定是橢圓曲線 Diffie-Hellman,客戶端就可以在第一則訊息裡直接猜一個曲線群組、把自己的臨時公鑰一起送出, 不必先問一輪「你支援什麼」再送。TLS 1.2 那種「先協商、再交換」的兩趟往返,就此少掉一趟。
client_random、supported_versions(宣告 1.3)、key_share(臨時 ECDHE 公鑰)、
cipher_suites、signature_algorithms、server_name(SNI)、alpn。整則明文。server_random、伺服器自己的 key_share、選定的密碼套件。仍是明文——但它是最後一則明文。Finished,並且不必等伺服器回應,同一趟就能送出第一個請求——這就是 1-RTT 的意思。交握完成後伺服器才會補送 NewSessionTicket(第 6 節談 Session Resumption 時的主角)。留意訊息的「加密起點」:ServerHello 之後的每一則交握訊息都已加密,包含憑證本身。
雙方算出同一把密鑰的方式,是 Diffie-Hellman 的老把戲:
因為兩把私鑰都是這次連線才生成、用完即丟的臨時值,即使伺服器的憑證私鑰日後外洩, 攻擊者也解不開先前錄下的流量——這個性質叫前向保密(Forward Secrecy)。 在 TLS 1.2 只有選用 ECDHE 密碼套件時才有;在 TLS 1.3 是強制的,因為 RSA 密鑰傳輸已經被移除。
順帶釐清一個高頻誤解:密碼套件(Cipher Suite)在 1.3 已經瘦身。
1.2 的 ECDHE-RSA-AES128-GCM-SHA256 把密鑰交換、簽章、加密、雜湊四件事綁成一串;
1.3 只留下 TLS_AES_128_GCM_SHA256 這種「AEAD(Authenticated Encryption with Associated Data,把加密與完整性驗證一次做完的模式)+ 雜湊」的組合,
密鑰交換群組與簽章演算法改由各自的 extension 獨立協商。所以在 1.3 談「這個 cipher suite 有沒有前向保密」是問錯問題——它一定有。
| 面向 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 完整交握 RTT | 2-RTT | 1-RTT(resumption 可到 0-RTT) |
| 密鑰交換 | RSA 傳輸 / DHE / ECDHE 都可 | 只剩 (EC)DHE |
| 前向保密 | 看你選哪個套件 | 強制具備 |
| 憑證是否加密 | 明文送出(誰都看得到你連的是哪個服務) | 已加密 |
| 對稱加密模式 | CBC、RC4、AEAD 混雜 | 只剩 AEAD |
| 其他移除項 | 壓縮、重新協商、自訂 DH 群組 | 全部刪除(各自對應過真實漏洞) |
TLS 1.3 的設計哲學是「減法」:CRIME 之於壓縮、BEAST/Lucky13 之於 CBC、DROWN 之於舊版相容——刪掉選項比修好每個選項可靠。本篇只以此表對比,不展開 1.2 的細節。
客戶端在 ClientHello 裡是猜一個曲線群組送出 key_share(實務上多半猜 X25519)。
如果伺服器只接受另一個群組,它會回 HelloRetryRequest 要求客戶端換群組重送一次 ClientHello,交握就退化成 2-RTT。
這也是「換了憑證/調了 TLS 設定之後,延遲莫名多了一個 RTT」的典型成因——用 openssl s_client 或抓包看有沒有 HRR 就知道。
憑證鏈驗證:名稱是信任的起點
加密解決「別人看不到」,憑證解決「對面是不是本人」——後者才是 TLS 真正難的部分。
伺服器在交握中送出的是一張 X.509 憑證,核心內容只有一句話:
「這個公鑰屬於這些名稱」,並由某個 CA(Certificate Authority,憑證頒發機構)用它的私鑰簽章背書。
而 CertificateVerify 這則訊息才是把「持有憑證」與「持有私鑰」綁在一起的關鍵——
它用憑證對應的私鑰對整段交握紀錄簽章。光是把別人的憑證抄下來重播是沒用的,因為簽不出這一步。
單一憑證不夠,實務上是一條憑證鏈(Certificate Chain):
客戶端拿到鏈之後要逐項檢查,任何一項失敗就中止連線:
- 鏈可達信任錨:每張憑證都能被上一層的公鑰驗簽,最終接到本機信任庫裡的某個 Root。
- 名稱相符:你要連的主機名必須出現在 leaf 的 SAN(Subject Alternative Name)裡。
- 有效期:當下時間落在
notBefore/notAfter之間——所以機器時鐘不準會導致奇怪的 TLS 失敗。 - 用途正確:Key Usage / Extended Key Usage 含
serverAuth;中繼憑證必須帶CA:TRUE。 - 未被吊銷:CRL、OCSP 或 OCSP stapling(下詳)。
- (瀏覽器)Certificate Transparency:憑證必須附帶 SCT,證明它已被記入公開日誌,讓誤發能被發現。
Common Name(CN)是 X.509 的歷史遺跡。RFC 6125 早已建議改用 SAN,Chrome 自 58 版起完全忽略 CN,只認 SAN 的 dNSName。
自簽憑證時只填 CN、沒填 SAN,是「curl 說 OK 但瀏覽器紅字」的頭號原因。
另外兩個名稱比對的細節:萬用憑證 *.example.com 只匹配一層(a.example.com 可以,a.b.example.com 不行),
而且不涵蓋 apex(example.com 本身要另外列進 SAN)——這與 dns-deep-dive 談 CNAME 時的 apex 特殊性是同一種坑。
dns-deep-dive 說過 DNS 原生沒有真實性保證,答案可能被污染。但注意:攻擊者就算把 api.example.com 解析到自己的 IP,也拿不出一張 SAN 含該名稱、且能鏈到公開 Root 的憑證——他簽不出來。名稱驗證因此成為 DNS 不可信時的最後一道防線,這也是為什麼「連到錯的 IP」在 HTTPS 下通常只是連不上,而不是被靜默竊聽。
有趣的是信任的來源繞了回去:CA 怎麼知道你擁有這個網域?靠 DNS。ACME 協定的 DNS-01 挑戰要你在 _acme-challenge.example.com 放一筆 TXT,HTTP-01 則要你在該名稱指向的主機上放一個檔案。所以 DV 憑證的信任根源,本質上是「網域控制權」。而 CAA 這種 DNS 紀錄,就是網域擁有者用來宣告「只有這幾家 CA 可以幫我簽憑證」的護欄——回到 DNS 篇那句話:名稱是一切的起點。
吊銷(revocation)是憑證體系最尷尬的一環:憑證一旦簽出去就在外面,私鑰外洩時必須有辦法作廢它。
| 機制 | 作法 | 問題 |
|---|---|---|
| CRL | CA 發佈吊銷清單,客戶端下載 | 清單越長越大,更新不即時 |
| OCSP | 客戶端即時向 CA 查詢單張憑證狀態 | 多一次外部往返(延遲)、洩漏使用者在連哪個站、CA 掛掉時只能「軟失敗」放行 |
| OCSP Stapling | 伺服器自己定期取得已簽章的狀態,交握時一併附上 | 解掉延遲與隱私問題;但伺服器要正確設定與更新,過期的 staple 反而會壞事 |
| 短效憑證 | 縮短有效期(數天~數十天),到期自然失效 | 目前業界主流方向;前提是自動化簽發與部署必須夠可靠 |
實務結論:吊銷檢查在公開網路上一直不可靠,所以主流答案是「把憑證有效期縮到短得讓吊銷不重要」——這也是 mTLS 場景(見第 5 節)普遍發短效憑證的原因。
拿真機看一次最有感。openssl s_client 會把整條鏈與驗證結果攤開:
$ openssl s_client -connect api.example.com:443 -servername api.example.com \
-alpn h2,http/1.1 </dev/null
Certificate chain
0 s:CN=api.example.com # leaf:SAN 必須含你連的名稱
i:C=US, O=Let's Encrypt, CN=R11 # 由這張中繼簽發
1 s:C=US, O=Let's Encrypt, CN=R11 # 中繼:伺服器必須自己送出
i:C=US, O=ISRG, CN=ISRG Root X1 # 根:在本機信任庫,不會被傳送
---
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
ALPN protocol: h2 # ALPN 協商結果(第 4 節)
Verify return code: 0 (ok) # 非 0 就是鏈/名稱/時間/吊銷出問題
除錯順序建議固定:先看 Verify return code,再看鏈裡有沒有中繼(只有 0 s: 一列通常就是漏送),最後才懷疑名稱與時間。特別注意瀏覽器可能靠 AIA 補抓中繼而顯示正常,但 curl/Java/Go 會直接失敗——「瀏覽器可以、服務對服務不行」十之八九是這個。
SNI 與 ALPN:交握裡的兩個路由旋鈕
一個決定「伺服器該拿哪張憑證出來」,一個決定「這條通道上要說哪種協定」——都在第一則訊息就定案。
這兩個 extension 都源自同一個時序問題:TLS 交握發生在應用協定開口之前。
伺服器要選憑證時,HTTP 的 Host 標頭還沒送出來(它在加密通道裡);
客戶端想知道對方支不支援 HTTP/2 時,也還沒有任何應用層訊息可問。答案只能塞進 ClientHello。
SNI(Server Name Indication)解的是「一個 IP 上住了幾千個網域」的問題:
客戶端在 ClientHello 明白寫出 server_name = api.example.com,
伺服器據此挑出對應的憑證與虛擬主機設定。沒有 SNI,共用 IP 的多租戶託管與 CDN 就不可能存在——
每個 HTTPS 網站都得配一個獨立 IP。
整條決策鏈都發生在加密開始之前:伺服器還沒送出憑證,就得先靠 ClientHello 裡這個明文欄位決定要拿哪一張出來——這就是 SNI 非明文不可的結構性原因,也是下一段 ECH 要補的那個洞。
代價是:SNI 是明文的。TLS 1.3 把憑證藏起來了,卻藏不住 SNI——它必須在密鑰協商完成前就被讀到。
於是路徑上的任何設備都能知道你要連哪個網域(雖然不知道你在上面做什麼)。這既被用於企業/營運商的流量分類,也被用於封鎖。
補救方案是 ECH(Encrypted Client Hello):把真正的 ClientHello 用伺服器公開在 DNS HTTPS 紀錄裡的公鑰加密、包在一個無害的外層 ClientHello 中——
又一次,起點還是 DNS。ECH 目前仍在逐步部署,本篇僅點到為止。
ALPN(Application-Layer Protocol Negotiation)則是在同一趟交握裡把「上層協定」也談完:
客戶端在 ClientHello 附上偏好清單(例如 ["h2", "http/1.1"]),伺服器在已加密的 EncryptedExtensions 裡回覆選中的那一個。
零額外往返——這正是它取代早期 NPN 與「先連 HTTP/1.1 再 Upgrade」做法的原因。
| 面向 | SNI | ALPN |
|---|---|---|
| 回答什麼問題 | 你要連哪個名稱? | 通道上要跑哪個協定? |
| 誰提出 / 誰決定 | 客戶端提出,伺服器據此選憑證 | 客戶端列偏好,伺服器決定 |
| 出現位置 | ClientHello(明文) | 提議在 ClientHello(明文),結果在 EncryptedExtensions(已加密) |
| 典型值 | api.example.com | h2、http/1.1、h3、mqtt |
| 缺席時 | 伺服器只能回預設憑證 → 多半名稱不符而失敗 | 退回預設協定(通常 HTTP/1.1) |
實務上兩者常一起出現在同一個地方:L4 負載均衡器可以只讀明文 SNI 就決定把連線轉給哪個後端叢集,完全不必解密(TLS passthrough);而要依 ALPN 分流就得先終止 TLS——這個取捨會在第 8 節談面試折衷時再出現一次。
瀏覽器連 HTTPS 網站時不會先用 HTTP/1.1 再升級——它在 ClientHello 就送出 h2, http/1.1,
伺服器沒回 h2 就一路走 HTTP/1.1。所以「我的服務為什麼沒跑在 HTTP/2」這題,第一個要看的不是應用設定,
而是邊緣那台終止 TLS 的元件有沒有開 ALPN h2。下一篇的 HTTP 篇談 HTTP/2 多路復用時,這個開關就是它的入口。
mTLS:把單向的信任變成雙向
同一套機制反過來用一次:這次換伺服器問「你又是誰」。
一般 HTTPS 是單向驗證:客戶端驗伺服器,伺服器不知道你是誰(身分留給應用層的 cookie 或 token)。
mTLS(Mutual TLS,雙向 TLS)只是在交握中多插兩則訊息:
伺服器送 CertificateRequest,客戶端回自己的 Certificate 與 CertificateVerify。
驗證邏輯完全相同——鏈、名稱、有效期、用途(這次要 clientAuth)。
{EncryptedExtensions} 與伺服器自己的 {Certificate} 之間,內容是「我接受哪些 CA、哪些簽章演算法」。這一則就是 mTLS 與一般 HTTPS 的唯一分歧點。mTLS 不是另一套協定,只是在同一次交握裡多插這三則訊息(對照第 2 節的完整交握圖);驗證邏輯與伺服器端完全相同,只是用途欄位從 serverAuth 換成 clientAuth。
為什麼公開網站幾乎不用它?因為憑證派送問題無解:你沒辦法要求每個使用者安裝一張憑證、也管不了他換手機。 但把場景換成服務之間,這個缺點消失了——服務是你自己部署的,憑證可以由 CI/service mesh 自動注入。 於是 mTLS 成為零信任(Zero Trust)架構的地基:不再假設「內網 = 可信」,每一次呼叫都要出示身分。
| 面向 | mTLS | Bearer Token(JWT/API Key) |
|---|---|---|
| 身分綁在哪 | 連線本身(傳輸層) | 每一個請求(應用層) |
| 被竊取後 | 私鑰不離開機器就無法冒用(不可重放) | token 被複製即可冒用,直到過期 |
| 誰驗證 | TLS 終止點(sidecar/gateway/伺服器本身) | 應用程式或 API gateway |
| 粒度 | 工作負載 / 主機層級 | 使用者 / 請求層級(可帶 scope) |
| 營運成本 | 要自建 CA、派發與輪替憑證 | 要管簽章金鑰與失效機制 |
兩者不是二選一而是分層:常見組合是用 mTLS 證明「哪個服務在呼叫」,用 token 攜帶「代表哪個使用者」。
實務上 mTLS 幾乎不手動維運。service mesh(Istio、Linkerd)的做法是:每個工作負載啟動時由 mesh 的私有 CA 簽發一張
有效期以小時計的憑證,身分寫在 SAN 的 URI 欄位(SPIFFE ID,例如 spiffe://cluster/ns/payments/sa/checkout),
由 sidecar 自動輪替。這正好呼應第 3 節談憑證吊銷時的結論:與其解決吊銷,不如讓憑證短命到不需要吊銷。
mTLS 通過只代表「對端持有一張我信任的 CA 簽發的憑證」。如果那張 CA 什麼都簽,任何拿到憑證的服務都能連上任何服務。 真正的存取控制必須額外做:比對 SPIFFE ID 或 SAN 白名單、在 mesh 的授權策略裡寫明「誰可以呼叫誰」。 另一個常見誤區是在 TLS 已於邊緣終止(見第 8 節)之後,仍以為後端拿得到客戶端憑證——沒有的話,身分必須由邊緣以標頭轉遞,而那個標頭一定要在信任邊界上被強制覆寫,否則就是偽造身分的後門。
Session Resumption、0-RTT 與重放風險
省下的不只是一個 RTT,還有整條憑證鏈與一次非對稱簽章——但最後那一步省得有代價。
完整交握的成本有兩塊:時間(1 個 RTT)與 CPU(伺服器要做一次非對稱簽章、客戶端要驗一整條鏈,還得傳幾 KB 的憑證)。 對一個用戶反覆連線、或裝置頻繁斷線重連的場景,每次都重來一遍很浪費。 Session Resumption(會話恢復)就是把「我們上次已經互相驗證過」這件事帶到下一條連線。
TLS 1.3 把舊版的 session ID 與 session ticket 兩套機制統一成一個概念:PSK(Pre-Shared Key,預共享密鑰)。
完整交握結束後,伺服器送出一則或多則 NewSessionTicket;客戶端把 ticket 存起來,
下次連線時在 ClientHello 用 pre_shared_key extension 出示它。伺服器解開 ticket 還原出 PSK,
雙方就能直接導出流量金鑰——不必再送憑證、不必再做簽章驗證。
| 模式 | 應用資料何時能送 | 省下什麼 | 風險 |
|---|---|---|---|
| 完整交握 | TLS 1-RTT 後 | — | — |
| Resumption(不帶 early data) | TLS 1-RTT 後(不變) | 憑證傳輸 + 非對稱簽章 + 鏈驗證(純 CPU 與頻寬) | 低 |
| Resumption + 0-RTT | 立刻,與 ClientHello 同行 | 再省掉那 1 個 RTT | 可被重放、且該段無前向保密 |
最容易被答錯的一列是中間那列:單純的 resumption 並不會變成 0-RTT,它仍是 1-RTT,省的是計算與頻寬。只有明確啟用 early data 才有 0-RTT。
0-RTT(early data)的作法是:客戶端在送出 ClientHello 的同一趟,就用 PSK 導出的金鑰加密第一個應用請求一併送出。
伺服器若接受,就能在交握還沒結束前處理它。延遲收益很誘人(等於完全消除 TLS 的交握延遲),但它打破了兩個原本成立的性質:
1. 沒有前向保密。early data 是用「上一次交握留下的 PSK」加密的,沒有這次新鮮的 ECDHE 參與。
只要 PSK 或 ticket 金鑰外洩,先前錄下的 early data 就能被解開。(交握完成後的流量仍有前向保密,因為 psk_dhe_ke 模式照樣做 ECDHE。)
2. 可以被重放(Replay)。攻擊者不必解密,只要原封不動把那段封包再送一次。伺服器在交握完成前沒有任何「新鮮性」證據可以判斷這是不是舊的複本——
完整交握靠 ServerHello 裡的隨機值保證每次都不同,而 early data 送出時那個隨機值還不存在。
因此 0-RTT 的防線是分層的,而且最後一道一定在應用層:
- 單次使用票券:伺服器記住已用過的 ticket 並拒絕第二次——但這在多台機器的叢集裡需要共享狀態,成本不低。
- 時效窗 + 重放偵測快取:只接受 ticket 年齡落在數秒內的 early data,並在這段時效窗內以本機快取擋掉重複;超出時效窗一律降級成正常 1-RTT。
- 只放冪等請求:冪等性(Idempotency)指同一個請求執行一次與執行多次的結果相同。 early data 只該放這種請求(讀取類),下一篇的 HTTP 篇談方法語義時會把這個性質講完整。
- 伺服器可以拒絕:不接受 early data 時,客戶端必須在交握完成後重送。HTTP 世界對應的訊號是
425 Too Early—— Nginx 的ssl_early_data on若沒搭配對$ssl_early_data的判斷,等於把重放風險直接開放給後端。
還有一個常被忽略的營運面:ticket 金鑰(STEK)本身就是一把長效密鑰。 它用來加密所有票券,若長期不輪替,等於替整個叢集的 resumption 流量開了一扇後門,前向保密也被削弱; 但若各台機器的 STEK 不同步,客戶端拿著 A 機的票券連到 B 機就會 resumption 失敗、退回完整交握。 「跨機共享 + 定期輪替」這兩個需求互相拉扯,是 TLS 終止層最典型的維運題。
$ openssl s_client -connect api.example.com:443 -sess_out sess.pem </dev/null
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256 # 完整交握:送憑證鏈 + 做簽章
Post-Handshake New Session Ticket arrived # 交握「之後」才發 PSK ticket
$ openssl s_client -connect api.example.com:443 -sess_in sess.pem </dev/null
Reused, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256 # 用 PSK 恢復:不再送憑證、不再驗鏈
Verify return code: 0 (ok)
New 與 Reused 這一字之差,就是「有沒有省下憑證鏈與簽章」。在負載均衡器後方反覆執行第二條指令,若一直顯示 New,多半就是各節點的 ticket 金鑰沒同步。
互動 · 交握成本計算機
把前面六節的每一個機制推到兩端,看它們各自在「第一個應用位元組上路之前」的帳單上加了多少。
前面六節談的東西,最後都折算成同一個量:在第一個應用位元組能送出去之前,你要等掉幾個 RTT、燒掉多少 CPU、傳掉多少位元組。
下面的計算機把三種模式——完整交握、resumption、resumption + 0-RTT——畫成三條成本曲線:
橫軸是網路 RTT,縱軸是首個應用位元組可送出前的等待時間(沿用第 8 節那條
TCP 1-RTT + TLS 交握 的算法,不含伺服器回應的那一趟)。
拖動 RTT 滑桿會移動曲線上的游標,其餘旋鈕則改變曲線本身的形狀。
曲線之外還有三筆帳:交握位元組、非對稱運算次數,以及單核每秒能收下幾條新交握。 這三筆是 resumption 真正省下的東西——它省的是計算與頻寬,不是那個 RTT。
四個值得停下來比對的畫面(以預設的 ECDSA P-256、鏈長 2、OCSP stapling、RTT 60 ms 為起點,此時三條曲線分別是 121 / 120 / 60 ms):(1) 把吊銷切成「即時 OCSP」,只有完整交握那條曲線整條抬高(60 ms 處由 121 變 193 ms), 另外兩條紋風不動——因為 resumption 根本不送憑證,也就沒有吊銷可查。 (2) 勾選 HelloRetryRequest,0-RTT 曲線由 60 ms 跳到 180 ms 並與 resumption 曲線重合:early data 被拒、必須在交握後重送,0-RTT 的收益整個消失。 (3) 把演算法切成 RSA 2048,等待時間幾乎不動,但「單核每秒可收下的新交握」從 1,370 條掉到 617 條—— RSA 簽章慢、驗章快,這個反轉在延遲上看不出來,在伺服器容量上很致命。 (4) 把 early data 拉到 16 KB,所有時間讀數都不變,只有「可被重放的請求數」由 3 個變成 21 個——0-RTT 的代價是曝險的量,不是延遲。
面試折衷與實務選型
TLS 的面試題很少是「加密怎麼做」,而是「你把 TLS 終止在哪、憑證誰管、0-RTT 敢不敢開」。
| 決策點 | 選項 | Trade-off |
|---|---|---|
| TLS 終止位置 | 邊緣終止 vs 端到端加密 vs passthrough | 邊緣終止能做 L7 路由、快取與集中憑證管理,但內網段變明文;端到端最安全卻要每個服務都管憑證;passthrough 只靠明文 SNI 分流,後端自己終止 |
| 憑證演算法 | RSA 2048 vs ECDSA P-256 | ECDSA 簽章快、憑證小(交握 CPU 明顯較省);RSA 相容性最好。折衷做法是雙憑證,依客戶端能力擇一送出 |
| 憑證有效期 | 長效手動 vs 短效自動化 | 短效讓吊銷失效問題自然消解,但把可用性壓在自動化流程上——續期壞掉就是全站中斷 |
| 0-RTT | 開 vs 關 | 行動端與高延遲鏈路收益最大;但必須確保 early data 只承載冪等請求,且叢集有重放偵測。預設關掉是安全的答案 |
| Resumption 狀態 | session ticket vs 伺服器端快取 | ticket 無狀態、天生跨機(前提是 STEK 同步且輪替);伺服器快取好控管但要共享儲存,且對長連線叢集是額外依賴 |
| 服務間身分 | mTLS vs token | mTLS 不可重放、與應用碼解耦,但要營運私有 CA;token 部署簡單,卻只要洩漏就能被冒用 |
| 版本下限 | 只開 1.3 vs 保留 1.2 | 只開 1.3 最乾淨,但老舊客戶端、部分 IoT 裝置與中間設備會直接連不上——IoT 場景尤其要先盤點 |
共同結構:每一列都在「安全強度」「延遲」「維運複雜度」三者中挑兩個。答題時把犧牲的那一個講出來,比背機制名稱有價值得多。
Q:TLS 為什麼要有憑證,只加密不行嗎?
答:只加密擋不住中間人——他跟你建一條加密通道、再跟真伺服器建另一條,全程可讀。憑證把「公鑰」綁定到「名稱」,讓你確認加密的對象就是你要連的那個名稱。
Q:TLS 1.3 為什麼比 1.2 快?
答:不是密碼演算法變快,而是刪掉了非 (EC)DHE 的密鑰交換,客戶端得以在 ClientHello 就附上 key_share,把 2-RTT 壓成 1-RTT;resumption 另外省掉憑證與簽章。
Q:一個 IP 怎麼服務上千個 HTTPS 網域?
答:靠 SNI——客戶端在明文的 ClientHello 指名要連哪個網域,伺服器據此選憑證;代價是網域名稱洩漏,ECH 才是解方。
Q:0-RTT 有什麼風險?
答:無前向保密、且可被重放——因為 early data 在伺服器隨機值出現之前就送出了。所以只能放冪等請求,並靠單次票券/時效窗/應用層 425 防守。
Q:憑證過期造成全站中斷,怎麼避免?
答:自動化續期 + 到期前多層告警 + 監控實際端點的剩餘天數(不是只看簽發紀錄);並確認中繼憑證也在監控範圍內。
Q:mTLS 能取代 API 授權嗎?
答:不能。它只回答「你是誰」,「你能做什麼」要另外用白名單或授權策略處理。
把本篇收攏成一句:TLS 沒有改變 TCP 的任何行為,它只是在應用資料開始之前插入一段交握, 用臨時密鑰換到機密性與完整性、用憑證鏈把公鑰綁回你在 DNS 查的那個名稱。 之後的 HTTPS、WSS、MQTTS,全都是在這條已經談好的通道上換不同的內容。
但別忘了帳單。在第一個位元組真正上路之前,你已經付了:
0-RTT 只是把其中一段的成本轉嫁成重放風險,並沒有真的消滅它。 真正的解法是更釜底抽薪的一步:如果傳輸層的交握與加密的交握本來就是同一件事,為什麼要分兩次做? 這個念頭會逼出一個把 TCP 換掉的協定——系列後面的 QUIC 篇會處理它。
而在那之前,眼前這條安全通道已經打通了,卻還沒人說話。 通道上要用什麼語法提出請求、怎麼表達結果、怎麼重複利用同一條連線—— 下一篇進入 HTTP/1.1 與 HTTP/2:這一層的語義、連線模型,以及多路復用為什麼還是會卡住。