1. Introduction
1.导言
In this tutorial, we’re going to take a look at how to use custom TrustStore in Java. We’re going first to override the default TrustStore and then explore the ways to combine certificates from multiple TrustStores. We’ll also see what the known problems and challenges are and how we can surpass them.
在本教程中,我们将了解如何在 Java 中使用自定义TrustStore。我们将首先覆盖默认的 TrustStore,然后探索组合多个 TrustStore 中的证书的方法。我们还将了解已知的问题和挑战是什么,以及如何克服这些问题和挑战。
2. Overriding Custom TrustStore
2.覆盖自定义 TrustStore
So, first, let’s override the default TrustStore. Most likely, it’s going to be the cacerts file located in lib/security/cacerts for JDK 9 and above. For JDKs below version 9, cacerts is located under jre/lib/security/cacerts. To override it, we need to pass a VM argument -Djavax.net.ssl.trustStore with the absolute path to the TrustStore to be used as the value. For instance, if we would launch JVM like this:
因此,首先让我们覆盖默认的 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
Then, instead of cacerts, Java would use /path/to/another/truststore.p12 as a TrustStore.
然后,Java 将使用 /path/to/another/truststore.p12 作为 TrustStore,而不是 cacerts 。
However, this approach has a small problem. When we override the location of the TrustStore, the default cacerts TrustStore won’t be taken into account anymore. That means, that all of the trusted CA’s certificates that come with JDK preinstalled will now no longer be available.
不过,这种方法有一个小问题。当我们覆盖 TrustStore 的位置时,默认的 cacerts TrustStore 将不再被考虑在内。这意味着,JDK 预装的所有受信任 CA 证书现在都不再可用。
3. Combining Multiple TrustStores
3.组合多个信托存储库
So, to solve the problem listed above, we can do either of two things:
因此,要解决上述问题,我们可以从以下两个方面入手:
- include all of the default cacerts certificates into the new TrustStore that we want to use
- try to programmatically ask Java to look into both TrustStores during the resolution of the trust of the entity
We’ll review both approaches below as they have their pros and cons.
下面我们将回顾这两种方法,因为它们各有利弊。
4. Merging TrustStores
4.合并信托存储库
The first approach is a relatively simple solution to the problem. In this case, we can create a new TrustStore from the default one. By doing so, we make sure that the new TrustStore will include all of the initial CA certificates:
第一种方法是相对简单的解决方案。在这种情况下,我们可以从默认的 TrustStore 创建一个新的 TrustStore。这样,我们就能确保新的 TrustStore 包含所有初始 CA 证书:
keytool -importkeystore -srckeystore cacerts -destkeystore new_trustStore.p12 -srcstoretype PKCS12 -deststoretype PKCS12
Then, we import the certificates we need into the newly created TrustStore:
然后,我们将需要的证书导入新创建的 TrustStore:
keytool -import -alias SomeSelfSignedCertificate -keystore new_trustStore.p12 -file /path/to/certificate/to/add
We can modify the initial TrustStore (meaning the cacerts itself), which might be a viable option. Other applications that rely on this exact JDK installation are the only thing to consider. They will receive these newly added certificates into default cacerts as well. That could or could not be OK, depending on the requirements.
我们可以修改初始 TrustStore(指cacerts本身),这可能是一个可行的选择。唯一需要考虑的是依赖于该 JDK 安装的其他应用程序。它们也将在默认 cacerts 中接收这些新添加的证书。这可能没问题,也可能没问题,这取决于具体要求。
5. Programmatically Consider Both TrustStores
5.以编程方式同时考虑两个信托库
This approach is a bit more complicated than the one we described. The challenge is that in JDK, there are no built-in ways to ask TrustManager (the one that decides to trust somebody) to consider multiple TrustStores. So we would have to implement it ourselves.
这种方法比我们描述的方法要复杂一些。难点在于 JDK 中没有内置的方法来要求 TrustManager(决定信任某人的管理器)考虑多个 TrustStores。因此,我们必须自己实现它。
The first thing to do is to get the instance of the TrustManagerFactory. When we have it, we’ll be able to get the TrustManager that we need:
首先要做的是获取 TrustManagerFactory 的实例。有了它,我们就能获得所需的 TrustManager :
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
So here, we get the default TrustManagerFactory, and then we initialize it with null as an argument. The init method initializes the TrustManagerFactory with the given TrustStore. As a side note, Java KeyStore and TrustStore are both represented by the KeyStore Java class. So when we pass null as an argument, TrustManagerFactory would initialize itself with default TrustStore (cacerts).
因此,我们在这里获取默认的 TrustManagerFactory,然后以 null 作为参数对其进行初始化。init 方法使用给定的 TrustStore 初始化 TrustManagerFactory 。顺便提一下,Java KeyStore 和 TrustStore 都由 KeyStore Java 类表示。因此,当我们传递 null 作为参数时,TrustManagerFactory 将使用默认 TrustStore(cacerts)初始化自身。
Once we have that, we should get the actual TrustManager from TrustManagerFactory. More specifically, we need the X509TrustManager. This TrustManager is the one that is responsible for determining whether the given x509 Certificate should be trusted or not:
有了这些,我们就应该从 TrustManagerFactory 中获取实际的 TrustManager 。更具体地说,我们需要 X509TrustManager 。该 TrustManager 负责确定给定的 x509 证书是否应被信任:
X509TrustManager defaultX509CertificateTrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager x509TrustManager) {
defaultX509CertificateTrustManager = x509TrustManager;
break;
}
}
So, we have the default JDK’s X509TrustManager, which knows only about default cacerts. Now, we need to load our own TrustStore and initialize the new TrustManagerFactory with this new TrustStore of ours:
因此,我们有了 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;
}
}
}
As we can see, we have loaded our TrustStore into a new KeyStore object using the given password. Then we get yet another default TrustManagerFactory instance (getInstance() method always returns a new object) and initialize it with our TrustStore. Then, in the same way as above, we find the X509TrustManager, which considers our TrustStore now. Now, the only thing left is configuring SSLContext to use both X509TrustManager implementations – the default one and ours.
我们可以看到,我们使用给定的密码将 TrustStore 加载到了一个新的 KeyStore 对象中。然后,我们再获取一个默认的 TrustManagerFactory 实例(getInstance() 方法总是返回一个新对象),并用我们的 TrustStore 对其进行初始化。然后,按照上述同样的方法,我们找到 X509TrustManager,它现在考虑了我们的 TrustStore。现在,唯一剩下的事情就是配置 SSLContext 以同时使用 X509TrustManager 实现(默认实现和我们的实现)。
6. Reconfiguring SSLContext
6.重新配置 SSLContext
Now, we need to teach the SSLContext to use our 2 X509TrustManagers. The problem is that we cannot pass them separately into SSLContext. That is because SSLContext, surprisingly, will use only the first X509TrustManager it finds and will ignore the rest. To overcome this, we need to create a single finalX509TrustManager that is a wrapper over our two X509TrustManagers:
现在,我们需要教 SSLContext 使用我们的 2 个 X509TrustManagers 。问题是,我们无法将它们分别传入 SSLContext。这是因为SSLContext竟然只会使用它找到的第一个X509TrustManager,而忽略其余的。为了解决这个问题,我们需要创建一个单一的最终X509TrustManager,作为两个X509TrustManager 的封装:</em
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);
}
};
And then initialize the TLS SSLContext with our wrapper:
然后使用我们的封装器初始化 TLS SSLContext :
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] { wrapper }, null);
SSLContext.setDefault(context);
We’re also setting this SSLContext as a default one. This is just in case since most of the clients that want to establish a secure connection would use TLS SSLContext. This serves as a backup option, though. We’re finally done.
我们还将此 SSLContext 设置为默认值。这只是以防万一,因为大多数希望建立安全连接的客户端都会使用 TLS SSLContext 。不过,这是一个备用选项。我们终于完成了。
7. Conclusion
7.结论
In this article, we explored the ways how to use certificates from different TrustStores in one Java application.
在本文中,我们探讨了如何在一个 Java 应用程序中使用来自不同 TrustStores 的证书。
Unfortunately, in Java, if we specify the TrustStore location from the command line, this would instruct Java to use only the specified TrustStore. So our options are either to modify the default cacerts TrustStore file or create a brand new TrustStore file that would contain all required CA certificate entries. A more complex approach is to force SSLContext to consider both TrustStores programmatically.
遗憾的是,在 Java 中,如果我们在命令行中指定 TrustStore 的位置,这将指示 Java 只使用指定的 TrustStore。因此,我们的选择要么是修改默认的 cacertsTrustStore 文件,要么是创建一个包含所有必要 CA 证书条目的全新 TrustStore 文件。更复杂的方法是强制 SSLContext 以编程方式考虑两个 TrustStore。
Still, all of these options would work, and we should use the one that fits our requirements.
不过,所有这些方案都可以使用,我们应该使用符合我们要求的方案。
As always, the entire source code for this article is available over on GitHub.
一如既往,本文的全部源代码均可在 GitHub 上获取。