我正在尝试连接到安全的网络服务。
即使我的密钥库和信任库已正确设置,我也会收到握手失败。
经过几天的挫折,无休止的谷歌搜索并问周围的人我发现唯一的问题是java选择不在握手期间将客户端证书发送到服务器。
具体做法是:
我的问题:
我确实为此设置了一个肮脏的解决方法,但我对此并不高兴,所以如果有人能为我澄清这个,我会很高兴。
答案 0 :(得分:89)
您可能已将中间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
答案 1 :(得分:5)
作为此处的添加,您可以使用%> openssl s_client -connect host.example.com:443 并查看转储并检查所有主证书是否对客户端有效。您正在输出的底部寻找此信息。 验证返回码:0(确定)
如果您添加 -showcerts ,它将转储与主机证书一起发送的钥匙串的所有信息,这是您加载到钥匙串中的信息。
答案 2 :(得分:0)
我见过的大多数解决方案都是围绕使用 keytool 进行的,但都没有一个适合我的情况。
这里是一个非常简短的描述:我有一个PKCS12(.p12),它在禁用了证书验证的邮递员中可以正常工作,但是我总是以编程方式最终遇到服务器错误“ 400 Bad Request” /“不需要SSL证书”已发送”。
原因是缺少TLS扩展SNI(服务器名称指示),下面是解决方法。
在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();
}
注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:设置标头“主机”以避免服务器错误“ 400-错误的请求”。
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;
答案 3 :(得分:-2)
问题是,当使用由中间人签名的客户端证书时,您需要在信任关系中包括该中间人,以便Java可以找到它。可以是单独发行,也可以与发行根的ca捆绑在一起。