在 java 中使用自定义信任库以及默认信任库
Posted
技术标签:
【中文标题】在 java 中使用自定义信任库以及默认信任库【英文标题】:Using a custom truststore in java as well as the default one 【发布时间】:2014-08-24 16:57:27 【问题描述】:我正在用 Java 编写一个应用程序,它通过 HTTPS 连接到两个 Web 服务器。一个通过默认信任链获得受信任的证书,另一个使用自签名证书。当然,连接到第一台服务器是开箱即用的,而使用自签名证书连接到服务器是行不通的,直到我使用来自该服务器的证书创建了一个 trustStore。但是,与默认受信任服务器的连接不再起作用,因为显然,一旦我创建了自己的信任库,默认信任库就会被忽略。
我找到的一个解决方案是将默认 trustStore 中的证书添加到我自己的证书中。但是,我不喜欢这个解决方案,因为它需要我继续管理那个 trustStore。 (我不能假设这些证书在可预见的未来保持不变,对吧?)
除此之外,我发现两个 5 年前的线程有类似的问题:
Registering multiple keystores in JVM
How can I have multiple SSL certificates for a Java server
他们都深入研究了 Java SSL 基础架构。我希望现在有一个更方便的解决方案,我可以在对我的代码进行安全审查时轻松解释它。
【问题讨论】:
Registering multiple keystores in JVM的可能重复 【参考方案1】:您可以使用与我在 previous answer 中提到的类似的模式(针对不同的问题)。
基本上,获取默认信任管理器,创建第二个使用您自己的信任存储的信任管理器。将它们都包装在自定义信任管理器实现中,该实现将调用委托给两者(当一个失败时回退到另一个)。
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here initialises the TMF with the default trust store.
tmf.init((KeyStore) null);
// Get hold of the default trust manager
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers())
if (tm instanceof X509TrustManager)
defaultTm = (X509TrustManager) tm;
break;
FileInputStream myKeys = new FileInputStream("truststore.jks");
// Do the same with your trust store this time
// Adapt how you load the keystore to your needs
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, "password".toCharArray());
myKeys.close();
tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(myTrustStore);
// Get hold of the default trust manager
X509TrustManager myTm = null;
for (TrustManager tm : tmf.getTrustManagers())
if (tm instanceof X509TrustManager)
myTm = (X509TrustManager) tm;
break;
// Wrap it in your own class.
final X509TrustManager finalDefaultTm = defaultTm;
final X509TrustManager finalMyTm = myTm;
X509TrustManager customTm = new X509TrustManager()
@Override
public X509Certificate[] getAcceptedIssuers()
// If you're planning to use client-cert auth,
// merge results from "defaultTm" and "myTm".
return finalDefaultTm.getAcceptedIssuers();
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException
try
finalMyTm.checkServerTrusted(chain, authType);
catch (CertificateException e)
// This will throw another CertificateException if this fails too.
finalDefaultTm.checkServerTrusted(chain, authType);
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException
// If you're planning to use client-cert auth,
// do the same as checking the server.
finalDefaultTm.checkClientTrusted(chain, authType);
;
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] customTm , null);
// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);
您不必将该上下文设置为默认上下文。你如何使用它取决于你使用的客户端库(以及它从哪里获取它的套接字工厂)。
话虽如此,原则上,无论如何您总是必须根据需要更新信任库。 Java 7 JSSE 参考指南对此有一个“重要说明”,现在降级为"note" in version 8 of the same guide:
JDK 附带了数量有限的受信任根证书 java-home/lib/security/cacerts 文件。如 keytool 中所述 参考页,您有责任维护(即添加 并删除)如果您使用此文件中包含的证书 文件作为信任库。
取决于您使用的服务器的证书配置 联系,您可能需要添加额外的根证书。获得 需要来自相应供应商的特定根证书。
【讨论】:
感谢这个伟大的解决方案。这已经困扰了我大约一年了! 这个解决方案非常适合我。与之前的评论一样,在尝试您的解决方案之前,我花了很多时间试图弄清楚如何做到这一点但没有成功。谢谢! 这个似乎在 Java spring 容器环境中不起作用。 @DenisWang 这取决于该容器中使用的内容(不确定是 Spring 还是 Spring Boot),它有可能不使用默认的 SSLContext 但有自己的。您需要查看您正在使用的 Spring 部分使用了什么。【参考方案2】:据我所知,您还可以使用 Apache HttpComponents 库中的 SSLContextBuilder
类将您的自定义密钥库添加到 SSLContext
:
SSLContextBuilder builder = new SSLContextBuilder();
try
keyStore.load(null, null);
builder.loadTrustMaterial(keyStore, null);
builder.loadKeyMaterial(keyStore, null);
catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException
| UnrecoverableKeyException e)
log.error("Can not load keys from keystore ''", keyStore.getProvider(), e);
return builder.build();
【讨论】:
问题不就是将所有信任材料添加到一个信任存储中吗?SSLContextBuilder#loadTrustMaterial
只能用于加载一个信任库。另一种方法SSLContextBuilder#loadKeyMaterial
不是用于信任库,而是用于不同目的的密钥库。不是吗?
不,我认为 loadTrustMaterial() 从给定参数加载 TrustManager 并将它们内部添加到列表中。因此,您可以多次调用它,然后 TrustManager 会被聚合。
我看看loadTrustMaterial
会做什么。至少在 Java11 实现中,列表将被给定列表替换。如果你反编译sun.security.provider.JavaKeyStore#engineLoad
,你会在那里看到this.entries.clear();
。【参考方案3】:
您可以通过调用TrustManagerFactory.init((KeyStore)null)
检索默认信任存储并获取其X509Certificate
s。将此与您自己的证书结合起来。您可以使用KeyStore.load
从.jks
或.p12
文件加载自签名证书,也可以通过CertificateFactory
加载.crt
(或.cer
)文件。
这里有一些演示代码来说明这一点。如果您使用浏览器从 ***.com 下载证书,则可以运行代码。如果您同时注释掉添加加载的证书和默认值,代码将获得SSLHandshakeException
,但如果您保留其中任何一个,它将返回状态代码 200。
import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;
public class HttpsWithCustomCertificateDemo
public static void main(String[] args) throws Exception
// Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
// Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
trustStore.load(null, null);
// If you comment out the following, the request will fail
trustStore.setCertificateEntry(
"***",
// To test, download the certificate from ***.com with your browser
loadCertificate(new File("***.crt"))
);
// Uncomment to following to add the installed certificates to the keystore as well
//addDefaultRootCaCertificates(trustStore);
SSLSocketFactory sslSocketFactory = createSslSocketFactory(trustStore);
URL url = new URL("https://***.com/");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
// Alternatively, to use the sslSocketFactory for all Http requests, uncomment
//HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
conn.setSSLSocketFactory(sslSocketFactory);
System.out.println(conn.getResponseCode());
private static SSLSocketFactory createSslSocketFactory(KeyStore trustStore) throws GeneralSecurityException
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManagers, null);
return sslContext.getSocketFactory();
private static X509Certificate loadCertificate(File certificateFile) throws IOException, CertificateException
try (FileInputStream inputStream = new FileInputStream(certificateFile))
return (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(inputStream);
private static void addDefaultRootCaCertificates(KeyStore trustStore) throws GeneralSecurityException
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Loads default Root CA certificates (generally, from JAVA_HOME/lib/cacerts)
trustManagerFactory.init((KeyStore)null);
for (TrustManager trustManager : trustManagerFactory.getTrustManagers())
if (trustManager instanceof X509TrustManager)
for (X509Certificate acceptedIssuer : ((X509TrustManager) trustManager).getAcceptedIssuers())
trustStore.setCertificateEntry(acceptedIssuer.getSubjectDN().getName(), acceptedIssuer);
【讨论】:
这条线不工作。 trustStore.setCertificateEntry(acceptedIssuer.getSubjectDN().getName(), acceptedIssuer);它因 KeyStoreException 崩溃:无法插入证书; KeyStore 初始化了吗? 您是否忘记致电load(null, null)
。尽管它看起来很奇怪,但它是必不可少的,因为它初始化了密钥库?
我也一样。这很奇怪,因为 keystore 对象已经加载了。
@PedroRomão 我没有尝试理解问题,而是将代码示例替换为您可以轻松测试的代码示例。让我知道它是否适合您。
在添加所有证书的循环中,有一个会崩溃。我发现在 setCertificateEntry 添加一个我自己的错误时崩溃 - 密钥库是否已初始化?【参考方案4】:
这里是Bruno's answer的简洁版
public void configureTrustStore() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException,
CertificateException, IOException
X509TrustManager jreTrustManager = getJreTrustManager();
X509TrustManager myTrustManager = getMyTrustManager();
X509TrustManager mergedTrustManager = createMergedTrustManager(jreTrustManager, myTrustManager);
setSystemTrustManager(mergedTrustManager);
private X509TrustManager getJreTrustManager() throws NoSuchAlgorithmException, KeyStoreException
return findDefaultTrustManager(null);
private X509TrustManager getMyTrustManager() throws FileNotFoundException, KeyStoreException, IOException,
NoSuchAlgorithmException, CertificateException
// Adapt to load your keystore
try (FileInputStream myKeys = new FileInputStream("truststore.jks"))
KeyStore myTrustStore = KeyStore.getInstance("jks");
myTrustStore.load(myKeys, "password".toCharArray());
return findDefaultTrustManager(myTrustStore);
private X509TrustManager findDefaultTrustManager(KeyStore keyStore)
throws NoSuchAlgorithmException, KeyStoreException
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore); // If keyStore is null, tmf will be initialized with the default trust store
for (TrustManager tm : tmf.getTrustManagers())
if (tm instanceof X509TrustManager)
return (X509TrustManager) tm;
return null;
private X509TrustManager createMergedTrustManager(X509TrustManager jreTrustManager,
X509TrustManager customTrustManager)
return new X509TrustManager()
@Override
public X509Certificate[] getAcceptedIssuers()
// If you're planning to use client-cert auth,
// merge results from "defaultTm" and "myTm".
return jreTrustManager.getAcceptedIssuers();
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
try
customTrustManager.checkServerTrusted(chain, authType);
catch (CertificateException e)
// This will throw another CertificateException if this fails too.
jreTrustManager.checkServerTrusted(chain, authType);
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
// If you're planning to use client-cert auth,
// do the same as checking the server.
jreTrustManager.checkClientTrusted(chain, authType);
;
private void setSystemTrustManager(X509TrustManager mergedTrustManager)
throws NoSuchAlgorithmException, KeyManagementException
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] mergedTrustManager , null);
// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);
【讨论】:
这帮助我修复了 javax.net.ssl.SSLHandshakeException:sun.security.validator.ValidatorException:PKIX 路径构建失败:sun.security.provider.certpath.SunCertPathBuilderException:无法找到有效的证书路径到请求的目标。谢谢! @KulanSachinthana 太棒了,我很高兴听到这有帮助!【参考方案5】:也许我回答这个问题已经晚了 6 年,但它也可能对其他开发人员有所帮助。我也遇到了加载默认信任库和我自己的自定义信任库的相同挑战。在为多个项目使用相同的自定义解决方案后,我认为创建一个库并将其公开以回馈社区会很方便。请看这里:Github - SSLContext-Kickstart
用法:
import nl.altindag.sslcontext.SSLFactory;
import javax.net.ssl.SSLContext;
import java.security.cert.X509Certificate;
import java.util.List;
public class App
public static void main(String[] args)
String trustStorePath = ...;
char[] password = "password".toCharArray();
SSLFactory sslFactory = SSLFactory.builder()
.withDefaultTrustMaterial()
.withTrustMaterial(trustStorePath, password)
.build();
SSLContext sslContext = sslFactory.getSslContext();
List<X509Certificate> trustedCertificates = sslFactory.getTrustedCertificates();
我不太确定是否应该在此处发布此内容,因为它也可以被视为宣传“我的库”的一种方式,但我认为这对面临同样挑战的开发人员会有所帮助。
您可以使用以下sn-p添加依赖项:
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart</artifactId>
<version>7.0.0</version>
</dependency>
【讨论】:
以上是关于在 java 中使用自定义信任库以及默认信任库的主要内容,如果未能解决你的问题,请参考以下文章
为 Android 显示自定义信息窗口 Android 地图实用程序库
CMake:从“make install [all]”中排除自定义安装目标
java.security.InvalidAlgorithmParameterException:Linux上的trustAnchors参数必须为非空,或者为啥默认的信任库为空[重复]