在 Java 中使用自訂 TrustStore
一、簡介
在本教程中,我們將了解如何在 Java 中使用自訂 TrustStore。我們將首先覆寫預設的 TrustStore,然後探索組合來自多個 TrustStore 的憑證的方法。我們還將了解已知的問題和挑戰是什麼以及我們如何克服它們。
2. 重寫自訂信任庫
因此,首先,讓我們覆蓋預設的 TrustStore。對於 JDK 9 及更高版本,它很可能是位於lib/security/cacerts
中的cacerts
檔案。對於版本 9 以下的 JDK, cacerts
位於jre/lib/security/cacerts
下。要覆寫它,我們需要傳遞一個 VM 參數-Djavax.net.ssl.trustStore
,其中包含要用作值的 TrustStore 的絕對路徑。例如,如果我們像這樣啟動 JVM:
java -Djavax.net.ssl.trustStore=/path/to/another/truststore.p12 app.jar
然後,Java 將使用/path/to/another/truststore.p12
作為 TrustStore,而不是cacerts,
。
然而,這種方法有個小問題。當我們覆蓋 TrustStore 的位置時,將不再考慮預設的cacerts
TrustStore 。這表示預先安裝 JDK 的所有受信任 CA 憑證現在將不再可用。
3. 組合多個信任庫
因此,為了解決上面列出的問題,我們可以做以下兩件事:
- 將所有預設
cacerts
憑證包含到我們要使用的新 TrustStore 中 - 嘗試以程式設計方式要求 Java 在實體信任解析期間查看兩個 TrustStore
我們將在下面回顧這兩種方法,因為它們各有優缺點。
4. 合併信任庫
第一種方法是解決問題的相對簡單的方法。在這種情況下,我們可以從預設的 TrustStore 中建立一個新的 TrustStore。透過這樣做,我們確保新的 TrustStore 將包含所有初始 CA 憑證:
keytool -importkeystore -srckeystore cacerts -destkeystore new_trustStore.p12 -srcstoretype PKCS12 -deststoretype PKCS12
然後,我們將需要的憑證匯入到新建立的TrustStore中:
keytool -import -alias SomeSelfSignedCertificate -keystore new_trustStore.p12 -file /path/to/certificate/to/add
我們可以修改初始的 TrustStore(即cacerts
本身),這可能是個可行的選擇。依賴此確切 JDK 安裝的其他應用程式是唯一需要考慮的事情。他們也會將這些新新增的憑證接收到預設cacerts
。這可以也可以不可以,這取決於要求。
5. 以程式設計方式考慮兩個信任庫
這種方法比我們所描述的方法稍微複雜一些。挑戰在於,在 JDK 中,沒有內建方法可以要求TrustManager
(決定信任某人的那個)考慮多個 TrustStore。所以我們必須自己實現它。
要做的第一件事是取得TrustManagerFactory
的實例。當我們擁有它時,我們將能夠獲得我們需要的TrustManager
:
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
因此,在這裡,我們獲得預設的TrustManagerFactory
,然後使用null
作為參數對其進行初始化。 init
方法使用給定的 TrustStore 初始化TrustManagerFactory
。附帶說明一下,Java KeyStore 和 TrustStore 都是由KeyStore
Java 類別表示。因此,當我們傳遞null
作為參數時, TrustManagerFactory
會使用預設的 TrustStore ( cacerts
) 初始化自身。
一旦我們有了它,我們應該從TrustManagerFactory
取得實際的TrustManager
。更具體地說,我們需要X509TrustManager
。此TrustManager
負責確定給定的 x509 憑證是否值得信任:
X509TrustManager defaultX509CertificateTrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager x509TrustManager) {
defaultX509CertificateTrustManager = x509TrustManager;
break;
}
}
因此,我們有預設的 JDK 的X509TrustManager
,它只知道預設的cacerts.
現在,我們需要載入我們自己的TrustStore
並使用我們的新 TrustStore 初始化新的TrustManagerFactory
:
try (FileInputStream myKeys = new FileInputStream("new_TrustStore.p12")) {
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, "new_TrustStore_pwd".toCharArray());
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(myTrustStore);
X509TrustManager myTrustManager = null;
for (TrustManager tm : trustManagerFactory.getTrustManagers()) {
if (tm instanceof X509TrustManager x509TrustManager) {
myTrustManager = x509TrustManager;
break;
}
}
}
正如我們所看到的,我們已經使用給定的密碼將 TrustStore 載入到新的KeyStore
物件中。然後我們得到另一個預設的TrustManagerFactory
實例( getInstance()
方法總是傳回一個新物件)並使用我們的 TrustStore 對其進行初始化。然後,以與上面相同的方式,我們找到X509TrustManager
,它現在考慮我們的 TrustStore。現在,剩下的唯一一件事就是配置SSLContext
以使用X509TrustManager
實現——預設的和我們的。
6. 重新配置SSLContext
現在,我們需要教導SSLContext
使用我們的 2 X509TrustManagers
。問題是我們無法將它們單獨傳遞到SSLContext
中。這是因為,令人驚訝的是, SSLContext
將只使用它找到的第一個X509TrustManager
,而忽略其餘的.
為了克服這個問題,我們需要建立一個最終的X509TrustManager
,它是兩個X509TrustManagers:
X509TrustManager finalDefaultTm = defaultX509CertificateTrustManager;
X509TrustManager finalMyTm = myTrustManager;
X509TrustManager wrapper = new X509TrustManager() {
private X509Certificate[] mergeCertificates() {
ArrayList<X509Certificate> resultingCerts = new ArrayList<>();
resultingCerts.addAll(Arrays.asList(finalDefaultTm.getAcceptedIssuers()));
resultingCerts.addAll(Arrays.asList(finalMyTm.getAcceptedIssuers()));
return resultingCerts.toArray(new X509Certificate[resultingCerts.size()]);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return mergeCertificates();
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
finalMyTm.checkServerTrusted(chain, authType);
} catch (CertificateException e) {
finalDefaultTm.checkServerTrusted(chain, authType);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
finalDefaultTm.checkClientTrusted(mergeCertificates(), authType);
}
};
然後使用我們的包裝器初始化TLS
SSLContext
:
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] { wrapper }, null);
SSLContext.setDefault(context);
我們也將此SSLContext
設定為預設值。這是為了以防萬一,因為大多數想要建立安全連線的用戶端都會使用TLS
SSLContext
。不過,這可以作為備份選項。我們終於完成了。
七、結論
在本文中,我們探討如何在一個 Java 應用程式中使用來自不同 TrustStore 的憑證。
不幸的是,在 Java 中,如果我們從命令列指定 TrustStore 位置,這將指示 Java 僅使用指定的 TrustStore。因此,我們的選擇是修改預設的cacerts
TrustStore 檔案或建立一個包含所有必要的 CA 憑證條目的全新 TrustStore 檔案。更複雜的方法是強制SSLContext
以程式方式考慮兩個 TrustStore。
儘管如此,所有這些選項都可以工作,我們應該使用符合我們要求的選項。
與往常一樣,本文的完整原始碼可在 GitHub 上取得。