76

我正在尝试连接到安全的网络服务。

即使我的密钥库和信任库已正确设置,我也遇到了握手失败。

经过几天的挫折,无休止的谷歌搜索和询问周围的每个人,我发现唯一的问题是java选择在握手期间不将客户端证书发送到服务器。

具体来说:

  1. 服务器请求客户端证书 (CN=RootCA) - 即“给我一个由根 CA 签名的证书”
  2. Java查看了密钥库,只找到了由“SubCA”签名的我的客户端证书,而该证书又由“RootCA”颁发。它没有费心去查看信任库……嗯,好吧,我猜
  3. 可悲的是,当我尝试将“SubCA”证书添加到密钥库时,这根本没有帮助。我确实检查了证书是否已加载到密钥库中。他们会这样做,但 KeyManager 会忽略除客户端之外的所有证书。
  4. 以上所有导致java认为它没有任何满足服务器请求的证书并且什么都不发送的事实......tadaaa握手失败:-(

我的问题:

  1. 是否有可能我以“破坏证书链”或其他方式将“SubCA”证书添加到密钥库,以便 KeyManager 仅加载客户端证书并忽略其余证书?(Chrome 和 openssl 设法弄清楚了,为什么 java 不能? - 请注意,“SubCA”证书始终作为受信任的权威单独提供,因此 Chrome 在握手期间显然正确地将其与客户端证书一起打包)
  2. 这是服务器端的正式“配置问题”吗?服务器是第三方。我希望服务器请求由“SubCA”机构签署的证书,因为这是他们为我们提供的。我怀疑这在 Chrome 和 openssl 中有效的事实是因为它们“限制较少”,而 java 只是“按书本”而失败。

我确实设法为此制定了一个肮脏的解决方法,但我对此并不满意,所以如果有人能为我澄清这个问题,我会很高兴。

4

4 回答 4

112

您可能已将中间 CA 证书导入密钥库,但未将其与您拥有客户端证书及其私钥的条目相关联。您应该能够使用keytool -v -list -keystore store.jks. 如果每个别名条目仅获得一个证书,则它们不会在一起。

您需要将您的证书及其链一起导入到具有您的私钥的密钥库别名中。

要找出哪个密钥库别名具有私钥,请使用keytool -list -keystore store.jks(我在此处假设 JKS 存储类型)。这会告诉你这样的事情:

Your keystore contains 1 entry

myalias, Feb 15, 2012, PrivateKeyEntry, 
Certificate fingerprint (MD5): xxxxxxxx

在这里,别名是myalias。如果您-v除此之外使用,您应该看到Alias Name: myalias.

如果您还没有单独拥有它,请从密钥库中导出您的客户端证书:

keytool -exportcert -rfc -file clientcert.pem -keystore store.jks -alias myalias

这应该会给你一个 PEM 文件。

使用文本编辑器(或cat),使用该客户端证书和中间 CA 证书(如果需要,可能还有根 CA 证书本身)准备文件(我们称之为bundle.pem),以便客户端证书位于开头及其颁发者证书就在下面。

这应该看起来像:

-----BEGIN CERTIFICATE-----
MIICajCCAdOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADA7MQswCQYDVQQGEwJVSzEa
....
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICkjCCAfugAwIBAgIJAKm5bDEMxZd7MA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
....
-----END CERTIFICATE-----

现在,将此捆绑包一起导入到您的私钥所在的别名中:

keytool -importcert -keystore store.jks -alias myalias -file bundle.pem
于 2012-02-15T20:21:31.253 回答
7

作为此处的补充,您可以使用%> openssl s_client -connect host.example.com:443并查看转储并检查所有主证书是否对客户端有效。您正在输出的底部寻找这个。 验证返回码:0(ok)

如果您添加-showcerts,它将转储与主机证书一起发送的钥匙串的所有信息,这是您加载到钥匙串中的内容。

于 2014-07-23T00:02:50.743 回答
0

我见过的大多数解决方案都是围绕使用keytool进行的,但没有一个与我的情况相符。

这是一个非常简短的描述:我有一个 PKCS12 (.p12),它在禁用证书验证的 Postman 中工作正常,但是以编程方式我总是最终收到服务器错误“400 Bad Request”/“No required SSL certificate was sent” .

原因是缺少 TLS 扩展 SNI(服务器名称指示),以下是解决方案。


向 SSL 上下文添加扩展/参数

在 SSLContext init 之后,添加以下内容:

SSLSocketFactory factory = sslContext.getSocketFactory();
    try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
        SSLParameters sslParameters = socket.getSSLParameters();
        sslParameters.setServerNames(Collections.singletonList(new SNIHostName(hostName)));
        socket.setSSLParameters(sslParameters);
        socket.startHandshake();
    }

此案例的完整 HTTP 客户端类(不适用于生产)

注 1: SSLContextException 和 KeyStoreFactoryException 只是扩展了 RuntimeException。

注意 2:证书验证被禁用,此示例仅供开发人员使用。

注 3:在我的情况下不需要禁用主机名验证,但我将其作为注释行包含在内

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Objects;

public class SecureClientBuilder {

    private String host;
    private int port;
    private boolean keyStoreProvided;
    private String keyStorePath;
    private String keyStorePassword;

    public SecureClientBuilder withSocket(String host, int port) {
        this.host = host;
        this.port = port;
        return this;
    }

    public SecureClientBuilder withKeystore(String keyStorePath, String keyStorePassword) {
        this.keyStoreProvided = true;
        this.keyStorePath = keyStorePath;
        this.keyStorePassword = keyStorePassword;
        return this;
    }

    public CloseableHttpClient build() {
        SSLContext sslContext = keyStoreProvided
                ? getContextWithCertificate()
                : SSLContexts.createDefault();

        SSLConnectionSocketFactory sslSocketFactory =
                new SSLConnectionSocketFactory(sslContext);

        return HttpClients.custom()
                .setSSLSocketFactory(sslSocketFactory)
                //.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                .build();
    }

    private SSLContext getContextWithCertificate() {
        try {
            // Generate TLS context with specified KeyStore and
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(getKeyManagerFactory().getKeyManagers(), new TrustManager[]{getTrustManager()}, new SecureRandom());

            SSLSocketFactory factory = sslContext.getSocketFactory();
            try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
                SSLParameters sslParameters = socket.getSSLParameters();
                sslParameters.setServerNames(Collections.singletonList(new SNIHostName(host)));
                socket.setSSLParameters(sslParameters);
                socket.startHandshake();
            }

            return sslContext;
        } catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
            throw new SSLContextException("Could not create an SSL context with specified keystore.\nError: " + e.getMessage());
        }
    }

    private KeyManagerFactory getKeyManagerFactory() {
        try (FileInputStream fileInputStream = getResourceFile(keyStorePath)) {
            // Read specified keystore
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(fileInputStream, keyStorePassword.toCharArray());

            // Init keystore manager
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
            keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
            return keyManagerFactory;
        } catch (NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | IOException | KeyStoreException e) {
            throw new KeyStoreFactoryException("Could not read the specified keystore.\nError: " + e.getMessage());
        }
    }

    // Bypasses error: "unable to find valid certification path to requested target"
    private TrustManager getTrustManager() {
        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
            }

            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    }

    private FileInputStream getResourceFile(String keyStorePath) throws FileNotFoundException {
        URL resourcePath = getClass().getClassLoader().getResource(keyStorePath);
        return new FileInputStream(resourcePath.getFile());
    }

}

使用上面的客户端生成器

注 1:在“资源”文件夹中查找密钥库 (.p12)。

注 2:设置标头“主机”以避免服务器错误“400 - 错误请求”。

String hostname = "myHost";
int port = 443;
String keyStoreFile = "keystore.p12";
String keyStorePass = "somepassword";
String endpoint = String.format("https://%s:%d/endpoint", host, port);

CloseableHttpClient apacheClient = new SecureClientBuilder()
        .withSocket(hostname, port)
        .withKeystore(keyStoreFile, keyStorePass)
        .build();

HttpGet get = new HttpGet(endpoint);
get.setHeader("Host", hostname + ":" + port);
CloseableHttpResponse httpResponse = apacheClient.execute(get);

assert httpResponse.getStatusLine().getStatusCode() == 200;

参考文档

https://docs.oracle.com/en/java/javase/11/security/java-secure-socket-extension-jsse-reference-guide.html

于 2020-08-10T09:05:36.183 回答
-2

问题是当使用由中间证书签名的客户端证书时,您需要在您的信任中包含中间证书,以便 java 可以找到它。单独或与根发行 ca 捆绑在一起。

于 2019-02-06T06:59:25.003 回答