iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

我是Java工程師,關於密碼學我想懂的不多系列 第 35 篇

Day34 - 密碼學的工具箱:OpenSSL 與 JDK keytool

  • 分享至 

  • xImage
  •  

前言

在過去的 30 幾天中,我們撰寫了大量 Java JCA 程式碼來生成金鑰、加密資料與驗證簽章。但在真實的企業開發與 DevOps 維運中,密碼學工程師有一半以上的時間並不是在寫 Code,而是在排查基礎設施的問題。

「為什麼憑證匯入失敗?」「為什麼 Nginx 啟動報錯說私鑰不匹配?」「為什麼 Java 發出 HTTPS 請求時噴出可怕的 PKIX path building failed?」

要解決這些「環境與格式」的疑難雜症,光靠 Java 程式碼是缺乏效率的。本章將為你裝備密碼學工程師必備的兩大工具——OpenSSL 與 JDK keytool,並徹底解析 Java TLS 連線中常常出現的 PKIX 錯誤與企業級憑證派送解法。

一、 OpenSSL:PKI 領域的最強瑞士刀

OpenSSL 幾乎內建於所有的 Linux 與 macOS 系統中,是處理 PEM/DER 格式、生成 CSR 與測試 TLS 連線的最強大 CLI 工具。

1. 憑證與私鑰的「照妖鏡」

當你拿到一堆 .pem、.crt 檔案,想知道它們到底有沒有過期、是誰簽發的,絕對不要用肉眼看 Base64:

# 1. 完整解析 X.509 憑證內容 (包含 Subject, Issuer, 效期與 SAN 擴充欄位)
openssl x509 -in server.crt -text -noout

# 2. 僅快速查看憑證的「有效期限」
openssl x509 -in server.crt -noout -dates

# 3. 解析私鑰內容 (以 RSA 為例,可查看模數與質數參數)
openssl rsa -in private.key -text -noout

# 4. 解析 CSR (憑證簽署請求) 內容,確認當初申請填寫的域名是否正確
openssl req -in request.csr -text -noout

2. 維運神技:私鑰與憑證是否匹配? (Modulus Check)

伺服器啟動失敗最常見的原因是「憑證跟私鑰根本不是同一對」。由於 RSA 憑證與私鑰共享同一個模數(Modulus),我們可以計算兩者的 Modulus MD5 雜湊值來比對:

openssl x509 -noout -modulus -in server.crt | openssl md5
openssl rsa -noout -modulus -in private.key | openssl md5
# 👉 如果輸出的 MD5 字串一模一樣,證明這把私鑰確實是這張憑證的主人!

3. 網路除錯神技:模擬 TLS 客戶端連線

當應用程式連不上遠端 HTTPS API 時,可使用 s_client 工具模擬握手,並強迫伺服器吐出它身上的所有信任鏈:

# 連線並印出伺服器回傳的所有憑證階層
openssl s_client -connect api.example.com:443 -showcerts

二、 JDK keytool:Java 金鑰庫 (KeyStore) 總管

keytool 隨附於 JDK 內,是專門管理 .p12 (PKCS12) 與 .jks 容器的工具。現代 JDK 預設金鑰庫格式已改為 PKCS12。

1. 檢視與產生 KeyStore

# 1. 檢視 PKCS12 檔案內所有的 Alias (別名) 與憑證詳細資訊
keytool -list -v -keystore server.p12 -storepass "你的密碼" -storetype PKCS12

# 2. 在 KeyStore 中直接生成一對全新的 RSA 3072 金鑰對 (自簽憑證)
keytool -genkeypair -alias my-server-key -keyalg RSA -keysize 3072 -validity 365 -keystore new-server.p12 -storetype PKCS12

2. 匯入與匯出憑證

# 1. 將外部的 Root CA 憑證匯入至自訂的 Java TrustStore 中
keytool -importcert -alias my-company-root -file rootCA.crt -keystore my-truststore.p12

# 2. 從 KeyStore 中把某個別名的憑證匯出為 PEM 格式 (.cer)
keytool -exportcert -alias my-server-key -keystore server.p12 -rfc -file exported.cer

三、 終極排障指南:擊潰 PKIX path building failed

呼叫 HTTPS API 時,最常讓新手崩潰的例外錯誤是:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

這代表 Java 的安全機制(TrustManager)不認識遠端伺服器憑證的簽發者,無法建立信任鏈。 網路上常教人實作「信任所有憑證」的程式碼,這在生產環境等同徹底拔除 TLS 防禦,絕對禁止!

以下是常見根本原因與企業級正規解法:

原因 1:企業 IT 派送根憑證,但 Java 預設不讀取 OS 憑證庫

  • 情境:公司網管透過 Windows GPO 派送了公司的 Root CA 憑證到伺服器的「受信任的根憑證授權單位」,其他程式(如瀏覽器、C#、Node.js)都正常連線,唯獨 Java 報錯。因為 **Java 預設只讀取自家的 $JAVA_HOME/lib/security/cacerts**,完全無視作業系統的憑證。
  • 正規解法 (零代碼 OS 橋接):透過 JVM 參數強制 Java 讀取作業系統的 TrustStore,無需修改程式碼。
  • Windows 環境 (使用 CryptoAPI 橋接):
java -Djavax.net.ssl.trustStoreType=Windows-ROOT -Djavax.net.ssl.trustStore=NONE -jar myapp.jar
  • macOS 環境 (讀取 Keychain):
java -Djavax.net.ssl.trustStoreType=KeychainStore -Djavax.net.ssl.trustStore=NONE -jar myapp.jar
  • Linux 環境 (直接指向 OS 憑證檔):
# CentOS / RHEL
java -Djavax.net.ssl.trustStore=/etc/pki/java/cacerts -jar myapp.jar
# Ubuntu / Debian
java -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -jar myapp.jar

現代架構最佳實務:Docker / K8s 的動態憑證注入

如果你的系統已走向容器化(Docker/K8s),IT 派送憑證到虛擬機 OS 層的策略會失效。最佳實務是在容器啟動腳本 (entrypoint.sh) 中,動態將掛載進來的公司根憑證匯入 Java 暫存的 TrustStore 中,保持映像檔純淨:

#!/bin/bash
# 1. 複製一份 JVM 預設 cacerts 到暫存區,避免污染原始唯讀檔
cp $JAVA_HOME/lib/security/cacerts /tmp/app-truststore.p12

# 2. 動態將 K8s Secret 或 Volume 掛載的公司憑證匯入
keytool -importcert -noprompt -alias company-root -file /opt/certs/company-root.crt \
        -keystore /tmp/app-truststore.p12 -storepass changeit

# 3. 啟動 Java 並掛載客製化 TrustStore
java -Djavax.net.ssl.trustStore=/tmp/app-truststore.p12 \
     -Djavax.net.ssl.trustStorePassword=changeit \
     -jar /app/myapp.jar

原因 2:遠端伺服器配置錯誤,少傳送了「中繼憑證」

  • 情境:伺服器確實買了合法的商業憑證,但 Nginx / Apache 沒設定好,握手時只傳了終端憑證,沒傳中繼憑證(Intermediate CA),導致 Java 找不到橋樑連回 Root CA。
  • 診斷方式:用 openssl s_client -connect api:443 -showcerts 看對方吐出幾張憑證。如果是單獨一張,通常就是設定錯了。
  • 解法:請對方維運補齊 Certificate Chain(如 Nginx 的 fullchain.pem)。

原因 3:SNI (Server Name Indication) 缺失

  • 情境:一台 IP 背後掛了多個域名的虛擬主機,如果 Java 用戶端在握手時沒有帶上 SNI,伺服器不知道你要連哪一個,會丟一張預設的錯誤憑證給你。
  • 解法:確保使用現代的 HTTP Client(如 Java 11 HttpClient),預設皆會正確發送 SNI。

四、 啟動 JVM TLS 底層日誌 (The Ultimate Debug Flag)

如果排查後還是找不到 PKIX 報錯原因,請在啟動時加入以下 JVM 參數,強迫底層印出最詳細的 TLS 握手與憑證信任鏈比對日誌:

# JDK 11+ 現代參數 (精準控制日誌層級)
java -Djavax.net.debug=ssl:handshake:trustmanager -jar myapp.jar

在 Console 日誌中,你可以清晰看到:

  1. ClientHello 協商了什麼演算法。
  2. 伺服器到底傳了哪些 chain 憑證過來。
  3. TrustManager 正在拿哪一個本機根憑證進行比對,以及在哪一步發生斷裂。

上一篇
Day33 - 金鑰與憑證容器標準
下一篇
Day35 - 現代網路安全基石:TLS 1.3 協定解析
系列文
我是Java工程師,關於密碼學我想懂的不多 共 37 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言