HTTPS 與 TLS 的實際運作原理:握手與憑證
你新增了 SSL 憑證、將網域指向伺服器,然後鎖頭圖示出現了。大多數開發者到此為止。然後某天,一個 API 用戶端失敗並顯示 UNABLE_TO_VERIFY_LEAF_SIGNATURE,某個行動應用程式在較舊的 Android 上拒絕你的憑證,或者負載平衡器終止了 TLS,而你的應用程式看到的一切都是純 HTTP。突然間,你需要了解在 TCP 連線與第一個 HTTP 請求之間究竟發生了什麼事。
本文將帶你了解 HTTPS 與 TLS 的真正運作方式:握手做了什麼、憑證如何驗證、TLS 1.3 有哪些改變,以及如何診斷你實際會遇到的錯誤。
HTTPS 究竟增加了什麼
HTTPS 是透過 TLS 連線傳送的 HTTP。TLS 提供三項保證:
- 機密性 — 流量使用握手期間協商的對稱金鑰加密。
- 完整性 — 每個記錄都帶有驗證標籤,因此可偵測竄改。
- 身分驗證 — 伺服器證明它控制與你請求的主機名稱相符之憑證的私鑰。
請注意清單上沒有什麼:用戶端預設不會被驗證。TLS 驗證伺服器;你的應用程式則驗證使用者。
TLS 1.2 握手,逐步解析
TLS 1.2 在較舊的基礎設施和許多企業代理中仍很常見,因此值得了解。完整的握手如下所示:
- ClientHello — 用戶端傳送支援的 TLS 版本、一個隨機值、一份加密套件清單,以及(如適用)SNI 主機名稱。
- ServerHello — 伺服器選擇版本與加密套件,傳回自己的隨機值,並傳送其憑證鏈。
- 金鑰交換 — 使用 ECDHE 時,伺服器傳送臨時公開金鑰;用戶端傳送自己的。雙方推導出相同的共享密鑰。伺服器使用其憑證的私鑰簽署握手記錄。
- Finished — 雙方傳送握手記錄的 MAC,確認沒有任何東西被竄改。
- 應用程式資料 — HTTP 請求和回應以加密形式流動。
關鍵細節是臨時金鑰交換。使用 ECDHE 時,每個連線都會產生新的金鑰對。即使攻擊者記錄了多年的流量,之後竊取了伺服器的私鑰,他們也無法解密過去的會話。這種特性稱為前向保密,也是靜態 RSA 金鑰交換被棄用的主要原因。
TLS 1.3 如何改變遊戲規則
TLS 1.3 將握手簡化為單次往返,並完全移除了舊式演算法。
| 面向 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 往返次數(完整握手) | 2 | 1 |
| 金鑰交換 | RSA 或 ECDHE | 僅 ECDHE(前向保密) |
| 加密套件 | 分開的加密、MAC、金鑰交換 | 僅 AEAD 套件,例如 TLS_AES_128_GCM_SHA256 |
| 靜態 RSA、CBC、RC4 | 允許 | 已移除 |
| 會話恢復 | 會話 ID / 票證 | PSK 搭配 0-RTT 選項 |
在 TLS 1.3 中,用戶端會猜測金鑰交換群組並在第一個訊息中傳送其份額,因此伺服器可以立即完成金鑰協商。憑證仍會傳送,但現在會被加密,這意味著被動觀察者無法再看到正在提供哪個主機名稱——除非使用了加密用戶端 Hello,否則只有 ClientHello 中的 SNI 會洩露它。
0-RTT 恢復允許回訪的用戶端在第一個航班中傳送應用程式資料。它很快,但 0-RTT 資料可能被重放,因此切勿用於非冪等請求,例如付款。
憑證與鏈驗證
憑證將公開金鑰綁定到主機名稱。驗證是一個鏈式遍歷:
- 伺服器傳送其葉憑證加上中間憑證。
- 用戶端在其儲存區中尋找受信任的根憑證,然後驗證鏈上每個簽章。
- 它會根據主體別名(SAN)欄位檢查主機名稱——現代用戶端會忽略通用名稱。
- 它會檢查有效日期和撤銷狀態(CRL 或 OCSP,通常會裝訂)。
unable to verify 錯誤最常見的原因是缺少中間憑證。瀏覽器有時會透過快取中間憑證來掩蓋這個問題,但命令列工具和行動應用程式會直接失敗。始終提供完整的鏈,葉憑證在前,然後是中間憑證——絕不要包含根憑證。
使用 OpenSSL 進行實用除錯
當出現問題時,先檢查伺服器實際傳送了什麼,再責怪用戶端。
# 顯示伺服器呈現的憑證鏈
openssl s_client -connect example.com:443 -servername example.com -showcerts
# 僅協商 TLS 1.3 並列出所選的加密套件
openssl s_client -connect example.com:443 -tls1_3
# 一行檢查憑證到期日
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltName如果 s_client 回報 verify error:num=21:unable to verify the first certificate,則鏈不完整。如果回報主機名稱不符,請檢查 SAN 清單——萬用字元憑證僅涵蓋一個標籤,因此 *.example.com 符合 api.example.com,但不符合 a.b.example.com。
常見錯誤配置
- 混合內容 — 一個 HTTPS 頁面載入
http://指令碼或圖片。瀏覽器會封鎖主動混合內容,並可能自動升級被動內容。 - TLS 在負載平衡器終止 — 後端看到的是純 HTTP。轉發
X-Forwarded-Proto並僅信任來自已知代理的此標頭,否則你的應用程式可能會產生錯誤的重新導向 URL。 - 啟用了弱協定版本 — 應停用 TLS 1.0 和 1.1。目標是僅使用 TLS 1.2 和 1.3。
- 沒有 HSTS — 沒有
Strict-Transport-Security標頭,第一個請求仍可能被降級。從較短的max-age開始,然後在確認後提高它。 - 生產環境中的自簽憑證 — 用於內部測試沒問題,但它們會訓練使用者和用戶端忽略警告。
合理的伺服器配置
對於 Nginx,現代基線配置如下所示:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;讓伺服器在 TLS 1.3 中選擇加密套件——用戶端偏好在那裡沒問題,因為套件都很強。除非你輪換票證金鑰,否則保持會話票證關閉,因為洩露的票證金鑰可能會危及恢復。
常見問題
HTTPS 會加密主機名稱嗎?
預設不會。ClientHello 中的 SNI 擴充功能以明文傳送,以便伺服器知道要呈現哪個憑證。加密用戶端 Hello(ECH)會隱藏它,但採用率仍然有限。路徑和查詢字串始終會被加密。
為什麼我的憑證在瀏覽器中有效,但在 curl 或行動應用程式中失敗?
幾乎總是缺少中間憑證。瀏覽器通常會自行擷取或快取中間憑證,而較嚴格的用戶端要求伺服器傳送完整的鏈。使用 openssl s_client -showcerts 驗證。
TLS 1.3 總是比 TLS 1.2 快嗎?
對於新連線,是的——一次往返而不是兩次。對於恢復的連線,差距會縮小,而 0-RTT 可以讓 TLS 1.3 更快,代價是非冪等請求的重放風險。
如果您在 TLS 變更後稽核網頁伺服器,日誌分析通常會揭示實際影響:損壞的重新導向、意外的 4xx 尖峰,或仍在使用 HTTP 的用戶端。您可以將存取日誌放入 Nginx 日誌分析器 中,以查看狀態碼分佈並找出從未完成升級到 HTTPS 的用戶端。