在Java和默认的信任库中使用自定义信任库


75

我正在用Java编写一个应用程序,该应用程序通过HTTPS连接到两个Web服务器。一个通过默认的信任链获得了受信任的证书,另一个使用自签名证书。当然,连接到第一个服务器是开箱即用的,而使用自签名证书连接到服务器则无法工作,直到我使用该服务器上的证书创建了一个trustStore。但是,与默认受信任服务器的连接不再起作用,因为显然,一旦创建了自己的信任库,默认的trustStore就会被忽略。

我发现的一种解决方案是将默认trustStore中的证书添加到我自己的证书中。但是,我不喜欢这种解决方案,因为它要求我继续管理该trustStore。(我不能假设这些证书在可预见的将来保持不变,对吗?)

除此之外,我发现了两个存在5年的类似问题的线程:

在JVM中注册多个密钥库

如何为Java服务器拥有多个SSL证书

他们俩都深入Java SSL基础架构。我希望到目前为止,有一个更方便的解决方案,可以在我的代码的安全性检查中轻松解释。


Answers:


91

您可以使用与上一个答案中提到的模式类似的模式(针对另一个问题)。

本质上,掌握默认的信任管理器,创建另一个使用您自己的信任库的信任管理器。将它们都包装在一个自定义的信任管理器实现中,该实现将调用都委派给两者(当一个失败时回退到另一个)。

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参考指南对此有一个“重要说明”,现在在同一指南的第8版中已降级为“重要说明” :

JDK在java-home / lib / security / cacerts文件中附带了数量有限的受信任的根证书。如keytool参考页中所述,如果您将此文件用作信任库,则有责任维护(即添加和删除)此文件中包含的证书。

根据您联系的服务器的证书配置,您可能需要添加其他根证书。从适当的供应商处获取所需的特定根证书。


感谢您提供的出色解决方案。这困扰了我大约一年!
杰米

该解决方案对我来说非常有效。与前面的评论一样,我花了很多时间来尝试解决这个问题,直到尝试解决方案。谢谢!
djenning90 '19

这个似乎在Java spring容器环境中不起作用。
丹尼斯·王

@DenisWang取决于该容器中使用的内容(不确定是Spring还是Spring Boot),它有可能不使用默认的SSLContext,而是拥有自己的SSLContext。您需要查看正在使用的Spring部分所使用的内容。
布鲁诺

6

您可以通过调用TrustManagerFactory.init((KeyStore)null)并获取默认信任库来获取默认信任库X509Certificate。将此与您自己的证书结合在一起。您可以从加载自签名证书.jks.p12文件,KeyStore.load也可以加载一个.crt(或.cer通过)文件CertificateFactory

这是一些演示代码,阐明了这一点。如果使用浏览器从stackoverflow.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(
                "stackoverflow",
                // To test, download the certificate from stackoverflow.com with your browser
                loadCertificate(new File("stackoverflow.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://stackoverflow.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初始化了吗?
PedroRomão20年

你忘了打电话吗load(null, null)?尽管这看起来很奇怪,这是至关重要的,因为它好,它初始化密钥库🤓
约翰内斯Brodwall

我也一样。这很奇怪,因为密钥库对象已经加载。
PedroRomão20年

@PedroRomão我没有尝试理解问题,而是将代码示例替换为可以轻松进行测试的示例。请让我知道这对你有没有用。
约翰内斯·布罗德沃尔

在添加所有证书的循环中,有一个崩溃。我发现在setCertificateEntry崩溃时添加了我自己的一个错误-密钥库是否已初始化?
PedroRomão20年

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();
    }

}

我不太确定是否应该在此处发布此内容,因为它也可以被视为促进“我的库”的一种方式,但我认为这可能对面临相同挑战的开发人员有所帮助。


3

这是布鲁诺答案的更干净的版本

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);
}

-2

如我所知,您还可以使用SSLContextBuilderApache HttpComponents库中的类将自定义密钥库添加到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不是用于信任库,而是用于不同目的的密钥库。是不是
ramtech '18

不,我认为loadTrustMaterial()从给定参数加载TrustManager,并将其内部添加到列表中。因此,您可以多次调用它,并且TrustManager会聚合。
OliLay

1
我研究一下该怎么loadTrustMaterial做。至少在Java11实现中,该列表将替换为给定的列表。如果您进行反编译sun.security.provider.JavaKeyStore#engineLoad,则会看到this.entries.clear();那里。
米·洛菲扎德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.