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


72

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

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

经过几天的挫败,无休止的谷歌搜索并询问周围的所有人,我发现唯一的问题是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只是“靠书”而失败。

我确实设法解决了这个问题,但是对此我不是很高兴,因此如果有人可以为我澄清这个问题,我将感到非常高兴。


2
值得添加到@Bruno的答案中,其根本原因是存在私钥但没有相应的证书(即具有相同的别名),因此Java在CertificateRequest消息询问时无法发送一个。造成此问题的其他原因可能包括没有指定服务器的发行者签名的证书,或者没有与指定服务器的密码匹配的证书。
user207421 2014年

我们仅通过匹配服务器指定的密码就解决了这个问题。谢谢你们提供的所有这些信息,它非常有用。
阿尔弗雷多·卡里略

Answers:


102

您可能已将中间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),准备bundle.pem包含该客户端证书和中间CA证书(如果需要,还可以包括根CA证书本身)的文件(叫它),以使客户端证书在其发布者的开头和证书就在下面。

看起来应该像这样:

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

现在,将此捆绑包重新导入到您的私钥为的别名中:

keytool -importcert -keystore store.jks -alias myalias -file bundle.pem

5
感谢一百万布鲁诺-做到了。我认为这可能会有更多记录。我花了几天的时间来弄清楚到底是怎么回事。Java的提示实际上是不存在的。
JakubHlavatý2012年

2
我也需要将证书和私钥结合到导入中,但是我从一个* .pem证书文件(以“ ----- BEGIN CERTIFICATE -----”开始)和一个* .key私有文件开始密钥(“ ----- BEGIN RSA PRIVATE KEY -----”)。我尝试将它们串联,然后导入,但得到:keytool错误:java.lang.Exception:输入的不是X.509证书。有什么想法我要去哪里吗?我在stackoverflow.com/questions/5297867 / ...上阅读,可能需要转换为密钥类型pkcs12。有任何想法吗?我被卡住了!谢谢。
马修·康奈尔

我的2cts:必须为私钥分配公钥的整个链。我们的公钥需要分配给中间CA和根CA。该PEM文件必须加载到密钥库中:keytool -importcert -keystore store.jks -file bundle.pem -alias myalias ... ..不可信。是否安装回复?[no]:是的...这将用整个链更新(!)现有密钥。
Marc

您需要将证书和私钥导入到pkcs12文件中,如下所示:stackoverflow.com/a/8224863,对我
有用

如果用户想通过智能卡进行身份验证,则不能导出私钥...必须采用其他方法
。–

7

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

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


@JerylCookopenssl s_client -showcerts -connect host.example.com:443
天空

如何将这些添加到truststore jks文件?
格子衬衫

0

我见过的大多数解决方案都是围绕使用keytool进行的,但都不符合我的情况。

这是一个非常简短的描述:我有一个PKCS12(.p12),它在禁用了证书验证的Postman中可以正常工作,但是通过编程,我总是最终收到服务器错误“ 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/cn/java/javase/11/security/java-secure-socket-extension-jsse-reference-guide.html


-1

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


1
您需要将其包含在keystore中,并且该问题已在七年前正确回答。
user207421'2

密钥库仅用于客户端密钥,我们用于对服务器进行身份验证的密钥。如我所说,签名和CA证书链应该放在信任存储中,因此请不要混淆人们。学会区分PrivateKeyEntrytrustedCertEntry
IgorC '19
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.