为啥 java 在 SSL 握手期间不发送客户端证书?

Posted

技术标签:

【中文标题】为啥 java 在 SSL 握手期间不发送客户端证书?【英文标题】:why doesn't java send the client certificate during SSL handshake?为什么 java 在 SSL 握手期间不发送客户端证书? 【发布时间】:2012-03-07 03:13:06 【问题描述】:

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

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

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

具体来说:

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

我的问题:

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

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

【问题讨论】:

值得在@Bruno 的回答中添加,根本原因是有一个私钥但没有相应的证书(即具有相同的别名),所以当@ 询问时Java 无法发送一个987654321@ 消息。造成这种情况的其他原因可能包括没有由服务器指定的颁发者签署的证书,或者没有与服务器指定的密码匹配的证书。 我们解决了这个问题,只是匹配了服务器指定的密码。谢谢你们提供所有这些信息,它非常有用。 【参考方案1】:

您可能已将中间 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证书本身),以便客户端-certificate 在开头,它的颁发者证书就在下面。

这应该看起来像:

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

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

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

【讨论】:

感谢一百万布鲁诺 - 完成了这项工作。我认为这可能会有更多的记录。我花了几天时间试图弄清楚到底发生了什么。 Java 的提示几乎不存在。 我也需要将证书和私钥合并到导入中,但我从 *.pem 证书文件开始(以“-----BEGIN CERTIFICATE----- ") 和 *.key 私钥 ("-----BEGIN RSA PRIVATE KEY-----")。我尝试连接它们然后导入,但我得到: keytool error: java.lang.Exception: Input not an X.509 certificate 。有什么想法我哪里出错了吗?我在***.com/questions/5297867/… 看到可能需要转换为密钥类型 pkcs12。有任何想法吗?我被困住了!谢谢。 我的2cts:私钥必须与公钥的整个链一起分配。在我的这种情况下;我们的公钥需要与中间 CA 和根 CA 一起分配。此 PEM 文件必须加载到密钥库中:keytool -importcert -keystore store.jks -file bundle.pem -alias myalias ... .. 不受信任。还是安装回复? [否]:是的......这将更新(!)整个链的现有密钥。 您需要将您的证书和私钥导入 pkcs12 文件,如下所示:***.com/a/8224863,为我工作:D 如果用户想使用智能卡进行身份验证,则无法导出私钥......必须有一些其他方式..【参考方案2】:

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

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

【讨论】:

@JerylCook openssl s_client -showcerts -connect host.example.com:443 如何将这些添加到信任库 jks 文件中?【参考方案3】:

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

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

原因是缺少 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:设置标头“Host”以避免服务器错误“400 - Bad Request”。

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

【讨论】:

【参考方案4】:

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

【讨论】:

您需要将其包含在您的 keystore 中,并且该问题已在七年前得到正确回答。 密钥库仅用于客户端密钥,我们使用这些密钥对服务器进行身份验证。正如我所说,签名和 CA 证书链应该放在信任库中,所以请不要让人们感到困惑。学会区分PrivateKeyEntrytrustedCertEntry

以上是关于为啥 java 在 SSL 握手期间不发送客户端证书?的主要内容,如果未能解决你的问题,请参考以下文章

在 SSL/TLS 握手中的“server-hello”消息期间选择证书链的逻辑?

在TLS / SSL握手(证书等)中泄露了哪些信息? [关闭]

为啥我找不到 SSL 握手的信任库?

javax.net.ssl.SSLHandshakeException:握手期间远程主机关闭连接

SSL 握手后,客户端的会话密钥存储在哪里?

GRPC Java客户端和NodeJS服务器之间的ssl握手问题