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

我正在连接一个安全的网络服务。

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

经过几天的沮丧,无休止的搜索和询问周围的每个人,我发现唯一的问题是 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 只是“照章办事”,结果失败了。

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.

91944 次浏览

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

在这里添加时,可以使用 % > openssl _ client-connect host.example.com : 443查看转储,并检查所有主要证书对客户机是否有效。您将在输出的底部找到这个。 验证返回代码: 0(ok)

如果您添加 舞会,它将转储所有的密钥链信息,随主机证书一起发送,这是您加载到您的密钥链。

问题是,当使用由中间证书签名的客户端证书时,你需要将中间证书包含在你的信托中,这样 java 就可以找到它。可以单独使用,也可以作为一个包使用 root 发行者 ca。

我看到的大多数解决方案都是围绕着使用 键盘工具而展开的,没有一个与我的案例相匹配。

这里有一个非常简短的描述: 我有一个 PKCS12(。P12)在 Postman 中工作得很好,但是在程序上我总是得到服务器错误“400个坏请求”/“没有发送所需的 SSL 证书”。

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


向 SSL Context 添加扩展名/参数

在 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 客户端类(NOTFORPRODUATION)

注意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: keystore (. p12)在“ resources”文件夹中查找。

注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