在 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) 检索默认信任存储并获取其X509Certificates。将此与您自己的证书结合起来。您可以使用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]”中排除自定义安装目标

Maven中的自定义工作

如何支持自定义安装的高版本openssl库

java.security.InvalidAlgorithmParameterException:Linux上的trustAnchors参数必须为非空,或者为啥默认的信任库为空[重复]