「憑證過期」或 SSLCertVerificationError 常被當成部署瑣事:換一張憑證、加上忽略驗證的旗標,請求能通就結案。這樣做只是把症狀推走,沒有回答真正的問題:此連線到底為什麼值得信任?

TLS 不是 HTTPS 底下的一個黑箱。它把兩端的身分、協商出的金鑰,以及後續每筆資料的防篡改綁成同一條安全通道。IETF 對 TLS 1.3 的定義也很直接:協定目標是防止竊聽、篡改與訊息偽造。RFC 8446 將這三件事分別落在 authentication、confidentiality 與 integrity。

本文受 Jaydeep 的 TLS/SSL 說明 啟發,但把重點移到可操作的責任邊界:憑證、金鑰協商、資料保護各自做什麼,以及錯誤發生時該驗證哪一層。

HTTPS 的三個保證,對應三種不同機制

目標TLS 提供的保證不該誤解成
身分驗證(Authentication)Client 確認連到的伺服器持有對應私鑰,且其憑證鏈與主機名稱可被信任憑證本身會加密所有資料
機密性(Confidentiality)建立通道後,只有兩端能讀取 application dataTLS 會隱藏所有流量中繼資料;長度仍可能被觀察
完整性(Integrity)攻擊者修改傳輸資料會被偵測對內容做一次裸 SHA-256 就能驗證發送者

這張表是排錯時最實用的切法。憑證鏈失敗是身分驗證問題;握手完成後被改過的資料是 record protection 問題;兩者不能靠同一個「關掉驗證」解決。

TLS 1.3:憑證驗身分,ECDHE 生金鑰,AEAD 保資料

現代 TLS 1.3 的流程可縮成四個責任明確的階段:

  1. ClientHello 帶著支援的版本、演算法與 key share 發起協商。
  2. ServerHello 選定參數;伺服器送出憑證與 CertificateVerify,證明它持有憑證公鑰配對的私鑰。
  3. client 驗證憑證鏈與主機名稱,雙方以 (EC)DHE shared secret 透過 HKDF 派生流量金鑰。
  4. 後續資料以 AEAD 保護;其驗證標記同時提供機密性與完整性。

RFC 8446 將握手定義為驗證通訊端、協商參數並建立 shared keying material 的協定;record protocol 才使用這些參數保護後續流量。握手與 record protocol 的分工 是理解 TLS 的核心。

這也修正一個常見說法:TLS 1.3 不是「client 產生 session key,再用伺服器公鑰加密送過去」。TLS 1.3 已移除靜態 RSA key exchange;雙方用 (EC)DHE 協商共享秘密並由 HKDF 派生金鑰。RFC 8446 的 key schedule 是精確的依據。

同樣地,單純把 SHA-256(data) 和資料一起傳送,不能證明來源:能替換資料的攻擊者也能重算 hash。TLS 1.3 使用 AEAD 與握手 transcript 的驗證,將完整性和持有秘密金鑰的端點綁在一起;裸 hash 只適合偵測非對抗性的意外改動。

憑證驗證至少包含 hostname、鏈與時間

瀏覽器或 client 不是只看「憑證還沒過期」。它需要確認:

  • 目標主機名稱是否符合憑證的 SAN;連 api.example.com 卻收到只簽給 example.com 的憑證,應該失敗。
  • leaf certificate 能否沿著 issuer chain 走到本機信任的 trust anchor。
  • 每張相關憑證是否仍在有效期間,並符合用途與基本約束。
  • 對端是否真的持有私鑰,亦即能通過握手中的簽名驗證。

X.509 的路徑驗證以 trust anchor、目標憑證與認證路徑為輸入;鏈如何取得則是應用與實作環境的責任。RFC 5280 §6.1 說明了這個邊界。主機名稱比對不是附加功能,而是阻擋「拿到另一個合法憑證就能冒充網站」的必要條件;可參考 RFC 6125。

一條命令先把問題拆開

當應用程式拋出 TLS 錯誤時,先在同一個執行環境跑下列命令,保留完整輸出到 incident 或部署紀錄:

openssl s_client \\
  -connect api.example.com:443 \\
  -servername api.example.com \\
  -verify_hostname api.example.com \\
  -verify_return_error \\
  -showcerts </dev/null

-servername 送出 SNI,確保虛擬主機回傳正確憑證;-verify_hostname 強迫把連線目標納入驗證;-verify_return_error 讓驗證錯誤不被默默略過。這些選項與 s_client 的連線與驗證行為可見於 OpenSSL 文件。

接著,將輸出的 leaf certificate 存成 certificate.pem,再只讀你要判斷的欄位:

openssl x509 -in certificate.pem -noout \\
  -subject -issuer -dates -ext subjectAltName
現象優先檢查正確修復方向
hostname mismatchSNI、SAN 與 client 實際連線 host為正確 hostname 簽發或修正 client endpoint
certificate has expiredleaf 與中繼憑證的 notAfter、系統時鐘續發與部署完整鏈;對時後重試
unable to get local issuer certificateserver 是否送出 intermediate、client trust store補齊鏈或更新受管信任根;不要略過 verify
只有內網服務失敗private CA 是否安裝在所有呼叫端發布並輪換內部 trust root,或採 mTLS 的受管身分流程

openssl x509 支援讀取 subject、issuer、dates 與 extensions;不要從肉眼判斷瀏覽器鎖頭圖示。OpenSSL x509 文件 是各選項的權威參考。

把 TLS 當成一項可驗證的依賴

我的立場是:TLS 不應只由平台或 ingress 團隊「處理掉」。API client 的 endpoint、SNI、trust store、憑證輪換與告警都是服務正確性的一部分。下一次 CI 或部署檢查,至少對每個公開 endpoint 執行一次上述 s_client 驗證,並在到期日前告警。這比事故當下把 verify=false 寫進程式碼少得多。

若你要再往底層看握手延遲、前向保密與 0-RTT 的取捨,可接著閱讀本站的 TLS 1.3 握手與 0-RTT 安全。