[英]why doesn't java send the client certificate during SSL handshake?
我正在尝试连接到安全的网络服务。
即使我的密钥库和信任库已正确设置,我也遇到了握手失败。
经过几天的挫折,无休止的谷歌搜索并询问周围的每个人,我发现唯一的问题是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(服务器名称指示),以下是解决方案。
在 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();
}
注 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;
问题是当使用由中间证书签名的客户端证书时,您需要将中间证书包含在您的信任库中,以便 java 可以找到它。 单独或与根发行 ca 捆绑在一起。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.