构建自己的 SSLContext 时`无法找到有效的证书路径...` (Java)

`Unable to find valid certification path...` while building own SSLContext (Java)

我正在尝试在运行时通过 SSLContext 对象向 HttpClient 添加证书,这样我就可以将程序分发给任何拥有 JVM 的人,他们不必担心通过 keytool 效用。证书本身将作为资源与程序打包在一起。

虽然关于我将分享的错误消息的许多其他答案在历史上都是通过使用 keytool 来回答的,但我 really 不希望我的最终用户被它拖累。 Whosebug 上有一小部分问题超越了 keytool 并尝试提供替代解决方案,但 是我发现的最接近我实际困境的问题。

它完全符合我的场景,但随后导入了一个现有的信任库,而不是在运行时创建一个信任库并用证书加载它。

我的问题是:当我拥有运行时 KeyStore 对象时,我什至必须保存文字 KeyStore 吗?如果是这样,为什么我的代码不起作用?

这是我的代码的框架(请原谅任何违反规范的行为,我是 Java 中的编码新手):

private HttpClient buildClient() {
    return HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .followRedirects(HttpClient.Redirect.ALWAYS)
            .sslContext(getSSLContext())  // Where I set the SSLContext
            .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
            .connectTimeout(Duration.ofMinutes(2L))
            .build();
}

private SSLContext getSSLContext() {
    try {
        return createSSLContext();
    } catch (Exception e) {
        throw new RuntimeException("The necessary certificate could not be added to the HttpClient");
    }
}

private SSLContext createSSLContext() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, KeyManagementException, IOException {
    SSLContext context = SSLContext.getInstance("SSLv3");
    TrustManager[] trustManagers = createTrustManagers();
    context.init(null, trustManagers, null);
    return context;
}

private TrustManager[] createTrustManagers() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
        TrustManagerFactory factory = TrustManagerFactory.getInstance("PKIX");
    factory.init(getConfiguredKeyStore());
    return factory.getTrustManagers();
}

private KeyStore getConfiguredKeyStore() throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
        Certificate certificate = getCertificate();
    KeyStore keyStore = createNewKeystore();
    keyStore.setCertificateEntry("www.thewebsite.com", certificate);
    return keyStore;
}

private KeyStore createNewKeystore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    char[] password = "password".toCharArray();
    keyStore.load(null, password);
    return keyStore;
}

private Certificate getCertificate() throws CertificateException {
    InputStream certStream = getClass().getClassLoader().getResourceAsStream("certFile.crt");
    CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
    return certFactory.generateCertificate(certStream);
}

下面是它抛出的错误(这就好像根本没有将证书添加为上下文一样):

Exception in thread "main" javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:556)
    at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
    at CustomHttpClient.authenticate(CustomHttpClient.java:41)
    at Main.main(Main.java:61)
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:369)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:312)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:307)
    at java.base/sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:654)
    at java.base/sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:473)
    at java.base/sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:369)
    at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:396)
    at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:480)
    at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1267)
    at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1254)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:691)
    at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1199)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate.lambda$executeTasks(SSLFlowDelegate.java:1122)
    at java.net.http/jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.execute(HttpClientImpl.java:155)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate.executeTasks(SSLFlowDelegate.java:1117)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate.doHandshake(SSLFlowDelegate.java:1083)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:484)
    at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:268)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$TryEndDeferredCompleter.complete(SequentialScheduler.java:315)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
    at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439)
    at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
    at java.base/sun.security.validator.Validator.validate(Validator.java:264)
    at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:285)
    at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144)
    at java.base/sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:632)
    ... 23 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
    at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
    at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297)
    at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
    ... 28 more

我摸不着头脑。有人知道我能做什么吗?

该错误消息没有说明您的密钥库的质量 - 它只是说明无法匹配合适的证书来验证服务器提供的证书。

当我解决同样的问题时,我在编译时构建了一个密钥库文件,然后将其作为资源添加到应用程序 jar 中。应用程序通过从资源流加载来创建密钥库对象 - 从那里我们的代码看起来非常相似。

因此,我将更多地关注密钥库中的证书以及服务器提供的内容。至少在我的环境中我发现了很多彩蛋(比如提供给我的 DEV 机器的证书与其他用户或后来的生产环境不同)。