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

发布于 2025-01-06 02:44:31 字数 872 浏览 1 评论 0原文

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

尽管我的密钥库和信任库已正确设置,但握手失败。

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

具体:

  1. 服务器请求客户端证书 (CN=RootCA) - 即“给我一个由根 CA 签名的证书”
  2. Java 查看密钥库,只找到由“SubCA 签名”的客户端证书”,由“RootCA”颁发。它没有费心去查看信任库...好吧,我想可悲
  3. 的是,当我尝试将“SubCA”证书添加到密钥库时,这根本没有帮助。我检查了证书是否已加载到密钥库中。它们确实如此,但 KeyManager 会忽略除客户端证书之外的所有证书。
  4. 上述所有情况都会导致 Java 决定它没有任何满足服务器请求的证书并且不发送任何内容...ta-da 握手失败 :-(

我的问题:

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

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

I'm trying to connect to a secure web service.

I was getting a handshake failure even though my keystore and truststore had been set correctly.

After several days of frustration, endless googling, and asking everyone around I found out that the only problem was that Java chose not to send the client certificate to the server during the handshake.

Specifically:

  1. Server requested a client certificate (CN=RootCA) - i.e. "give me a cert that is signed by the root CA"
  2. Java looked into the Keystore and only found my client certificate signed by the "SubCA", which is issued by the "RootCA". It didn't bother to look into the truststore...duh OK I guess
  3. Sadly when I tried to add the "SubCA" certificate to the keystore, that didn't help at all. I checked if the certificates were loaded into the keystore. They do, but the KeyManager ignores all certificates except the client one.
  4. All of the above leads to the fact that Java decides it doesn't have any certificates that satisfy the server's request and sends nothing...ta-da handshake failure :-(

My questions:

  1. Is it possible that I added the "SubCA" certificate to the Keystore in a manner that "broke the certificate chain" or something so that the KeyManager only loads the client certificate and ignores the rest? (Chrome and openssl manage to figure that out so why can't Java? - note that the "SubCA" cert is always presented separately as the trusted authority so Chrome correctly packs it along with the client cert during handshake)
  2. Is this a formal "configuration issue" on the server side? The server is a third party. I would expect the server to request a certificate signed by the "SubCA" authority since that's what they provided us with. I suspect that the fact that this works in Chrome and openssl is because they are "less restrictive" and Java just goes "by the book" and fails.

I did manage to put together a dirty workaround for this, but I'm not very happy about it so I'll be glad if anyone can clarify this one for me.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

静水深流 2025-01-13 02:44:31

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

It's possible that you may have imported the intermediate CA certificate into the keystore without associating it with the entry where you have your client certificate and its private key. You should be able to see this using keytool -v -list -keystore store.jks. If you only get one certificate per alias entry, they're not together.

You would need to import your certificate and its chain together into the keystore alias that has your private key.

To find out which keystore alias has the private key, use keytool -list -keystore store.jks (I'm assuming JKS store type here). This will tell you something like this:

Your keystore contains 1 entry

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

Here, the alias is myalias. If you use -v in addition to this, you should see Alias Name: myalias.

If you don't have it separately already, export your client certificate from the keystore:

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

This should give you a PEM file.

Using a text editor (or cat), prepare file (let's call it bundle.pem) with that client certificate and the intermediate CA certificate (and possibly the root CA certificate itself if you want), so that the client-certificate is at the beginning and its issuer cert is just under.

This should look like:

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

Now, import this bundle back together into the alias where your private key is:

keytool -importcert -keystore store.jks -alias myalias -file bundle.pem
っ〆星空下的拥抱 2025-01-13 02:44:31

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

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

As an add here, you can use %> openssl s_client -connect host.example.com:443 and see the dump and check that all the main cert is valid against the client. You are looking for this at the bottom of the output.
Verify return code: 0 (ok)

If you add -showcerts it will dump all the info of the keychain that was sent along with the host certificate, which is what you loaded into your keychain.

离旧人 2025-01-13 02:44:31

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

这是一个非常简短的描述:我有一个 PKCS12 (.p12),它在禁用证书验证的 Postman 中工作正常,但是以编程方式我总是最终收到服务器错误“400 错误请求”/“未发送所需的 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:设置标头“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

Most of the solutions I've seen revolve around using the keytool and none of them matched my case.

Here is a very brief description: I've got a PKCS12 (.p12) which works fine in Postman with disabled certificate verification, however programmatically I always ended up getting server error "400 Bad Request" / "No required SSL certificate was sent".

The reason was a missing TLS extension SNI (Server Name Indication) and following is the solution.


Adding an extension/parameter to SSL Context

After SSLContext init, add the following:

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();
    }

Full HTTP Client class for this case (NOT FOR PRODUCTION)

Note 1: SSLContextException and KeyStoreFactoryException simply extend RuntimeException.

Note 2: Certificate validations are disabled, this example was intended for dev use only.

Note 3: Disabling hostname verification was not required in my case, but I included it as a commented line

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());
    }

}

Using the client builder above

Note 1: keystore (.p12) is looked for in "resources" folder.

Note 2: Header "Host" is set to avoid server error "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;

Reference docs

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

恬淡成诗 2025-01-13 02:44:31

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

The thing is when using client cert signed by an intermediate one you need to include the intermediate one in your trustore so java can find it. Either solo or as a bundle with the root issuing ca.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文