Java Grizzly 服务器和 Jersey 客户端收到致命警报:certificate_unknown

Java Grizzly server and Jersey client Received fatal alert: certificate_unknown

我有一个带有嵌入式 SSL 服务器和客户端的 Java 应用程序。

我的应用程序使用客户端身份验证来确定客户端的身份,因此服务器配置了 wantClientAuth=true 和 needClientAuth=true。服务器还配置有服务器标识(cert/key 对)。服务器证书 SubjectDN 在可分辨名称的 CN 部分不包含服务器的主机名。服务器证书也不包含 x.509 备用名称扩展中的服务器 IP 地址。

我的客户端配置了客户端身份。它被配置为不执行主机名验证。它还配置了一个 trust-all 信任管理器(临时),以通常的方式定义。在客户端,收到的错误是:

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

到目前为止,我所做的所有修复尝试都只是成功地使它更频繁地失败。

我在另一个 Whosebug 问题中找到了这个命令行并尝试连接:

openssl s_client -connect 10.200.84.48:9298 -cert cert.pem -key key.pem -state -debug

这有效!我可以使用 openssl 客户端和客户端的私钥和证书建立连接,但是当我尝试使用我的 Java 客户端时,它失败并出现上述错误。

我确定我在两端使用了正确的密钥和证书。

出于调试目的,我将打印语句添加到“trust-all”client-side 信任库中,我注意到这三种方法中的 none 会被调用以验证服务器的证书(无论证书的内容如何,​​它都应该这样做)。

我在动态管理的 server-side 信任库中做了同样的事情,因为客户端身份来来去去。我知道每当修改信任库内容时都必须构建一个新的信任管理器,因为信任管理器复制信任库内容而不是持有对提供的 KeyStore object 的引用,所以我的代码就是这样做的。当客户端尝试连接时,服务器会调用 checkClientTrusted 和 getAcceptedIssuers 并且显示的证书内容是正确的。

这是真正奇怪的部分 - 它间歇性地工作。有时我获得成功的连接和数据交换,有时它失败并出现标题错误(在服务器的 JSSE 调试输出中看到)和相关的 client-side 上面提到的关于 PKIX 路径构建的错误。

还有一个事实:服务器正在使用从 SSLEngineConfigurator 创建的 grizzly 嵌入式服务器,客户端是一个纯 Jersey 客户端,配置了 SSLContext。

我完全不知所措。有没有人见过这样的事情?我能否提供更多信息以帮助您更好地理解上下文?

更新:

这是来自 server-side JSSE 调试日志的片段:

javax.net.ssl|FINE|25|grizzly-nio-kernel(7) SelectorRunner|2022-05-24 03:06:01.221 UTC|Alert.java:238|Received alert message (
"Alert": {
  "level"      : "fatal",
  "description": "certificate_unknown"
}
)
javax.net.ssl|SEVERE|25|grizzly-nio-kernel(7) SelectorRunner|2022-05-24 03:06:01.221 UTC|TransportContext.java:316|Fatal (CERTIFICATE_UNKNOWN): Received fatal alert: certificate_unknown (
"throwable" : {
  javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
          at sun.security.ssl.Alert.createSSLException(Alert.java:131)
          at sun.security.ssl.Alert.createSSLException(Alert.java:117)
          at sun.security.ssl.TransportContext.fatal(TransportContext.java:311)

服务器“收到致命警报:certificate_unknown”这一事实告诉我,客户端是生成警报并导致问题的原因。似乎客户端不喜欢服务器的证书,尽管我使用的是 trust-all 定义如下的信任管理器事件:

    RestClientImpl(@Nonnull Endpoint endpoint, @Nonnull Credentials clientCreds,
            @Nullable KeyStore trustStore, @Nonnull Configuration cfg, @Nonnull ExecutorService es) {
        this.endpoint = endpoint;
        ClientBuilder builder = ClientBuilder.newBuilder();
        setupClientSecurity(builder, clientCreds, trustStore);
        this.client = builder
                .executorService(es)
                .register(JsonProcessingFeature.class)
                .register(LoggingFeature.class)
                .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME_CLIENT, log.getName())
                .connectTimeout(cfg.getLong(CFG_REST_CLIENT_TMOUT_CONNECT_MILLIS), TimeUnit.MILLISECONDS)
                .readTimeout(cfg.getLong(CFG_REST_CLIENT_TMOUT_READ_MILLIS), TimeUnit.MILLISECONDS)
                .build();
        this.baseUri = "https://" + endpoint.getAddress() + ':' + endpoint.getPort() + '/' + BASE_PATH;
        log.debug("client created for endpoint={}, identity={}: client-side truststore {}active; "
                        + "hostname verification {}active", endpoint, osvIdentity,
                clientSideTrustStoreActive ? "" : "NOT ", hostnameVerifierActive ? "" : "NOT ");
    }

    private void setupClientSecurity(ClientBuilder builder, @Nonnull Credentials clientCreds,
            @Nullable KeyStore trustStore) {
        try {
            SSLContext sslContext = makeSslContext(clientCreds, trustStore);
            builder.sslContext(sslContext);
            if (trustStore != null) {
                hostnameVerifierActive = true;
            } else {
                builder.hostnameVerifier((hostname, session) -> true);
            }
        } catch (IOException | GeneralSecurityException e) {
            log.error("Failed to create SSL context with specified client credentials and "
                    + "server certificate for endpoint={}, osv identity={}", endpoint, osvIdentity);
            throw new IllegalArgumentException("Failed to create SSL context for connection to endpoint="
                    + endpoint + ", osv identity=" + osvIdentity, e);
        }
    }

    private SSLContext makeSslContext(@Nonnull Credentials clientCreds, @Nullable KeyStore trustStore)
            throws IOException, GeneralSecurityException {
        SSLContext context = SSLContext.getInstance(SSL_PROTOCOL);  // TLSv1.2
        X509Certificate clientCert = clientCreds.getCertificate();
        PrivateKey privateKey = clientCreds.getPrivateKey();

        // initialize key store with client private key and certificate
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null);
        keyStore.setCertificateEntry(CLIENT_CERT_ALIAS, clientCert);
        keyStore.setKeyEntry(CLIENT_KEY_ALIAS, privateKey, KEYSTORE_PASSWORD, new Certificate[] {clientCert});
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, KEYSTORE_PASSWORD);
        KeyManager[] km = kmf.getKeyManagers();

        // initialize trust store with server cert or with no-verify trust manager if no server cert provided
        TrustManager[] tm;
        if (trustStore != null) {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(trustStore);
            tm = tmf.getTrustManagers();
            clientSideTrustStoreActive = true;
        } else {
            tm = new TrustManager[] {
                    new X509TrustManager() {

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            log.debug("client-side trust manager: getAcceptedIssuers (returning empty cert list)");
                            return new X509Certificate[0];
                        }
                        @Override
                        public void checkClientTrusted(X509Certificate[] certs, String authType) {
                            log.debug("client-side trust manager: checkClientTrusted authType={}, certs={}",
                                    authType, certs);
                        }
                        @Override
                        public void checkServerTrusted(X509Certificate[] certs, String authType) {
                            log.debug("client-side trust manager: checkServerTrusted authType={}, certs={}",
                                    authType, certs);
                        }
                    }
            };
        }
        context.init(km, tm, null);
        return context;
    }

碰巧,这个问题的答案与客户端的使用方式有关,而不是它的配置方式。客户端非常主流,主要使用默认设置构建。唯一独特(且相关)的配置方面是它使用自定义 SSLContext。

此 JDK 1.8.0 错误自 2016 年以来一直公开,指出了问题的根本原因。 https://bugs.openjdk.java.net/browse/JDK-8160347

该错误是针对 1.8 提交的。0_92-b14。我正在 1.8.0_312-b07 上测试我的代码。看来这个错误在 6 年后仍然存在于 JSSE 中!

值得庆幸的是,提交错误的用户还提交了一个解决方法:只需调用 HttpsURLConnection.getDefaultSSLSocketFactory() 一次,然后允许多个线程同时访问您的客户端。我试过了,现在我的客户可以完美地工作了。希望这对某人有所帮助。