为什么java握手期间java不发送客户端证书?

时间:2012-02-15 18:30:30

标签: java ssl https certificate ca

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

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

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

具体做法是:

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

    1. 是否有可能以“破坏证书链”的方式将“SubCA”证书添加到密钥库中,以便KeyManager仅加载客户端证书而忽略其余证书? (Chrome和openssl设法解决这个问题,为什么不能用java? - 请注意,“SubCA”证书总是作为可信任的权限单独提供,因此Chrome显然在握手期间正确地将其与客户端证书一起打包)
    2. 这是服务器端的正式“配置问题”吗?服务器是第三方。我希望服务器能够请求由“SubCA”机构签署的证书,因为这是他们提供给我们的。我怀疑这个在Chrome和openssl中工作的事实是因为它们“限制性较小”而且java只是“按书”而失败。
    3. 我确实为此设置了一个肮脏的解决方法,但我对此并不高兴,所以如果有人能为我澄清这个,我会很高兴。

4 个答案:

答案 0 :(得分:89)

您可能已将中间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

答案 1 :(得分:5)

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

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

答案 2 :(得分:0)

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

这里是一个非常简短的描述:我有一个PKCS12(.p12),它在禁用了证书验证的邮递员中可以正常工作,但是我总是以编程方式最终遇到服务器错误“ 400 Bad Request” /“不需要SSL证书”已发送”。

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


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

在SSLContext初始化之后,添加以下内容:

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

答案 3 :(得分:-2)

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