題目與適用情境
api.example.com 輪替 TLS 憑證後,瀏覽器和一般的 curl https://api.example.com 請求都成功。一個使用自訂 truststore 的 Java 21 Worker 回報:SunCertPathBuilderException: unable to find valid certification path。 同時,curl https://203.0.113.10 能連到同一個負載平衡器,卻無法通過身分驗證。
請解釋 TLS 用戶端如何建構並驗證憑證路徑、如何核對目標服務身分、為何這些用戶端可能得到 不同結果,以及如何在不關閉憑證驗證或主機名稱驗證的前提下定位和修復故障。
明確採用以下假設:api.example.com 和 203.0.113.10 都是虛構範例;端點使用一般的 公開信任伺服器憑證;Java Worker 的自訂信任庫與瀏覽器的信任設定彼此獨立;負載平衡器可以在 同一個 IP 上承載多個 TLS 網域。本題核心是憑證認證與線上疑難排解,不展開密碼套件協商、TLS 1.3 金鑰派生與 0-RTT。
這道題適用於通用軟體工程、網路、SRE、平台、安全、後端和用戶端職位。合格答案不能只背 「葉端憑證—中繼憑證—根憑證」,還要把 PKI 規則落到可觀察的用戶端行為上。
面試官考察什麼
第一,候選人能否把經常混在一起的四個判斷拆開:
- 路徑建構:從葉端憑證出發,經過零張或多張中繼憑證,找到一條通往本地信任錨的候選
路徑。
- 路徑驗證:驗證候選路徑的簽章、時間、CA 約束、金鑰用途、名稱約束、關鍵擴充、演算法、
政策,以及它能否用於 TLS 伺服器認證。
- 服務身分比對:將設定中的目標網域或 IP 與
subjectAltName中相同類型的識別碼比對。 - 交握證明:驗證
CertificateVerify,確認對端持有葉端憑證對應的私鑰,並把身分綁定到
本次交握。
第二,候選人能否在沒有證據時克制地下結論。瀏覽器、系統工具、容器、JVM、行動應用程式和 企業代理可能使用不同的信任錨、中繼憑證快取或探索機制、時鐘、演算法政策、撤銷政策和參考身分。
第三,候選人能否設計可控的重現實驗。高品質回答會帶著正確的 SNI 擷取伺服器實際提供的 憑證鏈,用故障用戶端的真實信任材料重現,保持網域不變而固定目標 IP,比較成功與失敗路徑, 並檢查每一個負載平衡執行個體。
最後,候選人能否安全修復。匯入來源不明的葉端憑證、信任所有憑證、跳過主機名稱檢查或使用 curl -k 只會掩蓋已經失效的安全控制。修復方案必須恢復預期的憑證鏈或信任政策;如果有暫時 信任變更,還要提供復原和到期計畫。
回答前要釐清的問題
- 每個用戶端實際使用什麼 URL 和參考身分?直接存取 IP 字面值是在驗證 IP 身分;保留
https://api.example.com 這個 URL,只把流量導向同一 IP,驗證的仍是 api.example.com 這個 DNS 身分。
- 執行時到底載入哪個信任庫?要核對 JVM 參數、容器映像、執行使用者、程序環境和掛載檔案,
不能拿開發機的預設信任庫代替線上事實。
- 失敗請求實際收到哪條憑證鏈?SNI、監聽器、區域、代理、IPv4/IPv6 和部署偏差都可能
改變端點回傳的憑證。
- 所有用戶端是否同時失敗?檢查本地時間、憑證有效期、CA 套件版本、演算法政策,以及是否
只有某個後端或邊緣執行個體提供新鏈。
- 是否存在 TLS 中間代理?企業代理可能把公開葉端憑證替換成企業根簽發的憑證;瀏覽器信任
企業根,自訂 JVM 信任庫卻不一定信任。
- 錯誤具體落在哪一層?路徑建構失敗、憑證過期、主機名稱不符、演算法不支援、撤銷檢查
失敗、TLS 協商失敗是不同分支。必須保留完整例外和驗證軌跡。
30 秒回答框架
「我會把驗證拆成路徑建構、路徑驗證、服務身分比對和私鑰持有證明。伺服器通常提供葉端憑證 和所需中繼憑證;用戶端用它們建構一條通往本地已信任錨點的路徑,再驗證簽章、時間、CA 與 金鑰約束、關鍵擴充、演算法和伺服器用途。接著,用戶端把設定的網域或 IP 與同類型 SAN 比對。 SNI 只協助伺服器選擇憑證,不會建立信任,也不等於主機名稱驗證。
瀏覽器成功不能證明 Java 的路徑有效,因為自訂 JVM 信任庫、中繼憑證探索、代理根、時鐘和 政策都可能不同。我會用正確 SNI 擷取實際憑證鏈,用 Worker 的真實信任庫重播驗證,比較建構 出的路徑,再用 curl --resolve 固定 IP 但保留 DNS 身分。最後修復伺服器中繼憑證或受控信任 錨,絕不關閉驗證,並在所有邊緣節點和真實 Worker 上重新測試。」
逐步深入分析
1. 先確定三類獨立輸入。驗證器需要目標葉端憑證、一組可協助建構路徑的「不受信任」 中繼憑證,以及一個或多個本地信任錨。這裡的「不受信任」不代表惡意,只表示中繼憑證是路徑 建構材料;伺服器把它送來,並不會讓它自動成為信任錨。
HTTPS 伺服器通常提供葉端憑證和用戶端所需的中繼憑證,一般不需要提供根憑證。驗證器最終 落到由本地政策設定的根或其他錨點。伺服器即使送來根憑證,也無法讓用戶端信任一個原本未知 的根。
2. 先建構候選路徑,再驗證其中一條。葉端憑證給出簽發者,中繼憑證還可指向更上級簽發者; 交叉簽署可能形成多條可選路徑。用戶端可從交握、快取或實作特有的探索機制取得中繼憑證。 RFC 5280 定義路徑驗證,卻明確把「如何取得候選憑證序列」留在規範範圍之外。因此,兩個符合 標準的用戶端仍可能擁有不同建構輸入並選擇不同路徑。
SunCertPathBuilderException 表示 Java 路徑建構器沒有依據現有憑證和政策找到一條可接受 路徑。可能原因包括缺少中繼憑證、自訂信任庫沒有目標信任錨、替代路徑不可用,或某項政策約束 不滿足。單憑這條例外不能確定是哪一種。
3. 驗證選中的路徑。用戶端用簽發者公鑰逐級檢查相關憑證簽章,同時處理約束。關鍵檢查有:
- 目前時間必須落在憑證有效期內;
- 每個簽發憑證都要通過
basicConstraints獲准充當 CA,並滿足路徑長度限制; - CA 的金鑰用途允許簽發憑證,葉端憑證則要符合實際用戶端對 TLS 伺服器用途和延伸金鑰用途的
政策;
- 正確處理名稱約束、憑證政策和已辨識的關鍵擴充;
- 演算法和金鑰強度滿足用戶端目前安全政策;
- 路徑最終落到由本地政策選擇的信任錨。
撤銷是另一個政策維度。不同用戶端取得和處理 CRL、OCSP 回應、OCSP Stapling、網路故障以及 軟失敗/硬失敗的方式不完全相同。不能把「RFC 5280 路徑驗證通過」說成「所有用戶端都執行了 相同線上撤銷檢查」。
4. 單獨比對服務身分。參考身分來自設定的 HTTPS URL 等可信輸入,不能來自反向 DNS、 伺服器憑證本身,或攻擊者提供的任意名稱。依照 RFC 9525,現代服務身分放在 subjectAltName 中;不能為此退回 subject Common Name。
DNS 名稱和 IP 位址對應不同 SAN 類型。https://api.example.com 需要比對 DNS-ID; https://203.0.113.10 則要求 iPAddress SAN 精確包含該位址。只含 api.example.com 的 DNS SAN 不能滿足 IP 請求。支援萬用字元時,* 必須占據最左側完整標籤, 而且只比對一層:*.example.com 可以比對 api.example.com,不能比對 v2.api.example.com 或 example.com。
SNI 與身分驗證職責不同。SNI 告訴多租戶伺服器應該回傳哪張憑證;參考身分告訴用戶端那張 憑證必須涵蓋哪個名稱。請求可能傳送正確 SNI,之後仍然主機名稱驗證失敗;也可能因未傳送 SNI 而收到預設憑證,再以另一種原因失敗。
5. 驗證葉端私鑰的持有證明。有效路徑和相符 SAN 把身分綁定到一把公鑰。在使用憑證認證的 TLS 1.3 交握中,CertificateVerify 會用對應私鑰對交握摘要簽章。用戶端驗證它,才能確認 對端在這次協商中控制著那把私鑰。這個判斷與建構路徑、比對主機名稱彼此獨立。
6. 解釋為何三個現象可以同時成立。這些觀察結果並不矛盾:
| 現象 | 能證明什麼 | 不能證明什麼 |
|---|---|---|
| 瀏覽器成功 | 該瀏覽器在自身環境下找到可接受路徑和身分 | 自訂 JVM 擁有相同根、中繼憑證、代理、時鐘或政策 |
curl https://api.example.com 成功 | curl 目前 TLS 後端和 CA 來源接受該端點 | curl 與 Java 使用完全相同的驗證輸入 |
curl https://203.0.113.10 身分失敗 | 憑證缺少相符 IP-ID,或伺服器選了另一張憑證 | 透過 DNS 名稱存取也應失敗 |
| Java 路徑建構失敗 | Worker 的輸入和政策未能建構可接受路徑 | 葉端憑證在所有用戶端都無效,或流程已執行到主機名稱比對 |
7. 分別重現端點回傳和路徑驗證。先使用準確 DNS 名稱和 SNI。以下參數以 OpenSSL 3.6 為準;應先執行 openssl version,因為 macOS 系統內建的 LibreSSL 和較舊套件提供的選項 不同。-showcerts 顯示伺服器實際提供的內容;-verifyreturnerror 遇到驗證錯誤即回傳; -verify_hostname 驗證目標 DNS 身分。
openssl s_client \
-connect api.example.com:443 \
-servername api.example.com \
-showcerts \
-verify_return_error \
-verify_hostname api.example.com \
</dev/null把葉端憑證和中繼憑證分別儲存成 PEM,透過受控匯出取得故障環境真實信任根,再明確重播路徑 驗證:
openssl verify \
-CAfile worker-roots.pem \
-untrusted served-intermediates.pem \
-purpose sslserver \
-verify_hostname api.example.com \
leaf.pem這個實驗明確區分信任錨和中繼憑證,但無法完整模擬每一種 JVM 演算法、撤銷或 Provider 政策。 最終重現仍要在相同 Java 映像中,使用相同 truststore 和執行參數開啟信任管理器除錯。對外分享 日誌前要清理內部名稱和憑證材料。
如果要固定某個負載平衡位址,又不想改變 HTTPS 參考身分,應保留 URL,只覆寫解析結果:
curl --resolve api.example.com:443:203.0.113.10 \
https://api.example.com/health對每個公布的 IPv4、IPv6、區域和邊緣執行個體重複測試。直接請求 https://203.0.113.10 是另一項身分測試,不能用它證明 api.example.com 的憑證錯誤。
8. 修復失敗層,並驗證復原邊界。如果伺服器未提供必要中繼憑證,應在所有 TLS 終止點部署 正確的葉端憑證加中繼憑證套件,並再次確認實際提供的憑證鏈。如果組織明確使用私有 CA 或代理 CA, 應透過受控信任庫流程散布核准的根,並記錄擁有者、範圍、指紋、到期日和復原方式。如果 SAN 簽錯,就重新簽發憑證;如果時鐘、舊執行個體或演算法政策不一致,就直接修正對應條件。
不要把端點葉端憑證永久當成根匯入,不要從瀏覽器快取複製來源不明的憑證,不要關閉 Endpoint Identification,不要安裝寬鬆 TrustManager,也不要發佈 curl -k。修復後要驗證真實 Worker、 瀏覽器、curl、所有邊緣位址、舊憑證清理和到期前監控,還要確認錯誤主機名稱與不受信任測試鏈 仍會被拒絕。
高品質參考回答
「我不會因為瀏覽器能用就判斷 Java 有問題。每個驗證器都依據自己的目標憑證、中繼憑證集合、 信任錨、參考身分、時間和政策作出決定。
我會拆成四項檢查。第一,路徑建構從葉端憑證經中繼憑證找到本地信任錨;伺服器提供的憑證只是 建構輸入,不會創造信任。第二,路徑驗證檢查簽章、有效期、CA 和金鑰約束、關鍵擴充、演算法、 政策及伺服器認證用途。第三,端點身分驗證把設定名稱與 SAN 比對:DNS URL 需要 DNS-ID,IP 字面值 URL 需要 IP-ID;SNI 只負責讓虛擬主機選擇憑證。第四,TLS 驗證 CertificateVerify,確認對端持有葉端私鑰並把它綁定到本次交握。
瀏覽器可能使用不同根庫、已快取或可探索的中繼憑證,或者企業代理根。Java 21 Worker 的自訂 信任庫可能缺少所需錨點或建構材料,而例外本身只說明沒有建構出可接受路徑。IP 形式的 curl 失敗也符合預期:憑證可能涵蓋 DNS 名稱,卻沒有 IP SAN。
我會先用 openssl sclient、正確 SNI、-verifyreturn_error 和 -verify_hostname 擷取實際鏈,核對 SAN、簽發者、有效期、基本約束、金鑰用途、EKU、演算法 和指紋。接著用 Worker 根憑證的受控匯出驗證已儲存的葉端和中繼憑證,並在真實 Java 容器中 使用實際參數重現。curl --resolve 可以逐個檢查負載平衡 IP,同時把 URL 身分和 SNI 保持為 api.example.com。
缺中繼憑證就修復所有 TLS 終止點的鏈套件;缺少核准的私有根或代理根,就走受控信任庫散布, 不信任單張葉端憑證;SAN、時鐘或政策錯誤就修復對應層。我絕不會用 trust-all、關閉主機名稱 驗證或 -k 交差。只有真實 Worker 在每個邊緣節點成功,同時錯誤主機名稱和不受信任鏈仍被 拒絕,才能關閉故障。」
常見錯誤
- 把路徑建構和路徑驗證說成同一步 →用戶端可能先探索到不同候選路徑 → **分別說明目標
憑證、中繼憑證集合、信任錨和政策。**
- 因為伺服器送來根憑證就信任它 →信任錨來自本地政策 → **把伺服器憑證視為不受信任的
建構輸入。**
- 只驗證簽章,不看 CA 約束 →簽章正確不代表簽發者有權簽憑證 → **檢查基本約束、路徑長度、
金鑰用途、關鍵擴充和用途。**
- 用憑證裡的名稱決定預期身分 →這等於讓伺服器自己選擇要證明什麼 → **從設定 URL 推導
參考身分。**
- 認為 SNI 就是主機名稱驗證 →SNI 只選擇虛擬主機 → 單獨執行 SAN 比對。
- 期待 DNS SAN 驗證 IP URL →DNS-ID 與 IP-ID 是不同類型 → **固定路由但保留 DNS 身分時
使用 --resolve。**
- 看到一條例外就斷定缺中繼憑證 →信任庫、政策、時間、代理和部署偏差都可能產生類似現象
→ 擷取實際鏈,用真實執行輸入重現。
- 用
-k或 trust-all 修復 →這會把認證通道降成未認證通道 → **修復憑證鏈、身分、受控根、
時鐘或政策。**
追問與回答
追問 1:伺服器應該傳送根憑證嗎?
通常不需要。伺服器應傳送葉端憑證,以及抵達用戶端已信任根所需的中繼憑證。已經信任該根的 用戶端不需要伺服器重複傳送;不信任該根的用戶端也不會因收到它而建立信任。傳送根還會增加 交握位元組數。
追問 2:為什麼瀏覽器能補救缺少的中繼憑證,其他用戶端卻失敗?
不同實作可能擁有不同的中繼憑證快取或探索行為。某個瀏覽器可能已經快取中繼憑證,或透過 實作特有機制取得它;隔離的 Worker 可能只有交握內容和自訂信任庫。因此伺服器應主動提供必要 中繼憑證,不能依賴用戶端補救。疑難排解時要確認實際路徑,不能假設所有瀏覽器行為一致。
追問 3:自簽憑證只要主機名稱相符就安全嗎?
不安全。身分比對和路徑信任是兩個獨立判斷。憑證可以包含正確 DNS SAN,卻無法建構到本地 信任錨。私有環境可以透過受控設定信任自簽根,但名稱相符本身不會建立這種信任。
追問 4:面試中應如何回答憑證撤銷檢查?
要說清楚政策邊界。CRL、OCSP、Stapling、狀態快取、網路存取以及軟失敗/硬失敗規則都可能因 用戶端而異。應確認實際驗證器檢查什麼、狀態不可用時如何處理。不能聲稱所有用戶端都做相同 線上檢查,也不能為了消除故障而默默把必須硬失敗的政策改弱。
追問 5:哪些證據足以關閉這次故障?
記錄每個邊緣節點提供的葉端和中繼憑證指紋、Worker 依據真實信任根建構出的路徑、DNS 名稱與 私鑰證明通過結果,以及真實 Worker 請求成功證據。補充錯誤 DNS 名稱、沒有 IP-ID 的 IP 字面值、 不受信任憑證鏈三類負向測試。確認沒有繞過邏輯、所有負載平衡執行個體都提供預期鏈、監控涵蓋 到期與輪替,並為任何暫時信任項或診斷產物設定擁有者和清理日期。