繁体   English   中英

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

[英]why doesn't java send the client certificate during SSL handshake?

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

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

经过几天的挫折,无休止的谷歌搜索并询问周围的每个人,我发现唯一的问题是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 只是“按原样”执行并失败。

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

您可能已将中间 CA 证书导入密钥库,而未将其与您拥有客户端证书及其私钥的条目相关联。 您应该能够使用keytool -v -list -keystore store.jks看到这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

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

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

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

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

原因是缺少 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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM