Java HTTPS客户端证书authentication
我对HTTPS / SSL / TLS相当陌生,而且我对使用证书进行身份validation时客户端的performance有些困惑。
我正在写一个Java客户端,需要做一个简单的POST数据到一个特定的URL。 这部分工作正常,唯一的问题是它应该通过HTTPS完成。 HTTPS部分相当容易处理(使用HTTPclient或使用Java的内置HTTPS支持),但我坚持使用客户端证书进行身份validation。 我注意到在这里已经有一个非常类似的问题,我还没有用我的代码尝试过(将尽快这样做)。 我目前的问题是 – 无论我做什么 – Java客户端永远不会发送证书(我可以检查这与PCAP转储)。
我想知道当用证书进行身份validation时,客户端应该向服务器提供什么(专门用于Java–如果这很重要的话)? 这是一个JKS文件,或PKCS#12? 他们应该是什么 只是客户端证书,或一个关键? 如果是这样,哪个键? 关于所有不同types的文件,证书types等都有相当多的混淆。
正如我之前所说,我是HTTPS / SSL / TLS的新手,所以我也希望能够获得一些背景信息(不一定是散文,我会解决链接到好文章)。
最后设法解决所有的问题,所以我会回答我自己的问题。 这些是我用来解决特定问题的设置/文件;
客户端的密钥库是一个包含PKCS#12格式的文件
- 客户的公共证书(在这种情况下由自签名的CA签名)
- 客户的私钥
为了生成它,我使用了OpenSSL的pkcs12
命令。
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"
提示:确保你得到最新的OpenSSL, 而不是版本0.9.8h,因为这似乎是一个错误,不允许你正确生成PKCS#12文件。
这个PKCS#12文件将被Java客户端使用,当服务器明确地请求客户端authentication时,把客户端证书提交给服务器。 请参阅TLS上的维基百科文章,了解客户端证书身份validation协议实际如何工作的概述(也解释了为什么我们需要客户端的私钥)。
客户端的信任库是一个简单的JKS格式文件,包含根 证书或中间CA证书 。 这些CA证书将确定您将允许哪些端点进行通信,在这种情况下,它将允许您的客户端连接到任何一个服务器提供由信任库的一个CA签名的证书。
要生成它,你可以使用标准的Java keytool,例如;
keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca
使用此信任库,您的客户端将尝试与提供由myca.crt
标识的CA签名的证书的所有服务器进行完整的SSL握手。
以上文件仅限客户端使用。 当你想build立一个服务器时,服务器也需要自己的密钥和信任库文件。 在这个网站上可以find为Java客户端和服务器(使用Tomcat)build立一个完整工作示例的很好的演示。
问题/备注/提示
- 客户端证书authentication只能由服务器执行。
- ( 重要! )当服务器请求客户端证书(作为TLS握手的一部分)时,它也将提供一个可信的CA列表作为证书请求的一部分。 当您希望提供身份validation的客户端证书没有被这些CA之一签名时,它将不会被呈现(在我看来,这是一个奇怪的行为,但是我确信这是有原因的)。 这是我的问题的主要原因,因为对方没有正确configuration他们的服务器接受我的自签名客户端证书,我们认为问题是在我的最后没有正确提供客户端证书的请求。
- 获取Wireshark。 它具有很好的SSL / HTTPS数据包分析function,对于debugging和发现问题将起到巨大的帮助。 它类似于
-Djavax.net.debug=ssl
但是如果您对Java SSLdebugging输出感到不舒服,它会更加结构化和(可以说)更容易解释。 -
完全可以使用Apache httpclient库。 如果您想要使用httpclient,只需将目标URLreplace为HTTPS等价物并添加以下JVM参数(对于任何其他客户端来说都是相同的,无论您想要使用哪个库通过HTTP / HTTPS发送/接收数据) :
-Djavax.net.debug=ssl -Djavax.net.ssl.keyStoreType=pkcs12 -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStorePassword=whatever -Djavax.net.ssl.trustStoreType=jks -Djavax.net.ssl.trustStore=client-truststore.jks -Djavax.net.ssl.trustStorePassword=whatever
他们JKS文件只是一个证书和密钥对的容器。 在客户端身份validationscheme中,密钥的各个部分将位于此处:
- 客户的商店将包含客户的私钥和公钥对。 它被称为密钥库 。
- 服务器的存储将包含客户端的公钥。 它被称为信任库 。
信任库和密钥库的分离不是强制性的,但推荐使用。 它们可以是相同的物理文件。
要设置两个存储的文件系统位置,请使用以下系统属性:
-Djavax.net.ssl.keyStore=clientsidestore.jks
并在服务器上:
-Djavax.net.ssl.trustStore=serversidestore.jks
要将客户端的证书(公钥)导出到一个文件中,所以可以将它复制到服务器上,使用
keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks
要将客户端的公钥导入服务器的密钥库,使用(如上所述,这已经由服务器pipe理员完成)
keytool -import -file publicclientkey.cer -store serversidestore.jks
其他答案显示如何全局configuration客户端证书。 但是,如果您想以编程方式为特定连接定义客户端密钥,而不是跨JVM上运行的每个应用程序全局定义它,则可以像这样configuration自己的SSLContext:
String keyPassphrase = ""; KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray()); SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(keyStore, null) .build(); HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build(); HttpResponse response = httpClient.execute(new HttpGet("https://example.com"));
Maven pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>some.examples</groupId> <artifactId>sslcliauth</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>sslcliauth</name> <dependencies> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.4</version> </dependency> </dependencies> </project>
Java代码:
package some.examples; import java.io.FileInputStream; import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLContext; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.ssl.SSLContexts; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.apache.http.entity.InputStreamEntity; public class SSLCliAuthExample { private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName()); private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS"; private static final String CA_KEYSTORE_PATH = "./cacert.jks"; private static final String CA_KEYSTORE_PASS = "changeit"; private static final String CLIENT_KEYSTORE_TYPE = "PKCS12"; private static final String CLIENT_KEYSTORE_PATH = "./client.p12"; private static final String CLIENT_KEYSTORE_PASS = "changeit"; public static void main(String[] args) throws Exception { requestTimestamp(); } public final static void requestTimestamp() throws Exception { SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory( createSslCustomContext(), new String[]{"TLSv1"}, // Allow TLSv1 protocol only null, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) { HttpPost req = new HttpPost("https://changeit.com/changeit"); req.setConfig(configureRequest()); HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin")); req.setEntity(ent); try (CloseableHttpResponse response = httpclient.execute(req)) { HttpEntity entity = response.getEntity(); LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine()); EntityUtils.consume(entity); LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString()); } } } public static RequestConfig configureRequest() { HttpHost proxy = new HttpHost("changeit.local", 8080, "http"); RequestConfig config = RequestConfig.custom() .setProxy(proxy) .build(); return config; } public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException { // Trusted CA keystore KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE); tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray()); // Client keystore KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE); cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray()); SSLContext sslcontext = SSLContexts.custom() //.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize .loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate .build(); return sslcontext; } }
对于那些只想设置双向authentication(服务器和客户端证书)的用户来说,这两个链接的组合可以帮助你:
双向身份validation设置:
https://linuxconfig.org/apache-web-server-ssl-authentication
你不需要使用他们提到的opensslconfiguration文件; 只是使用
-
$ openssl genrsa -des3 -out ca.key 4096
-
$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt
生成您自己的CA证书,然后通过以下方式生成并签署服务器和客户端密钥:
-
$ openssl genrsa -des3 -out server.key 4096
-
$ openssl req -new -key client.key -out server.csr
-
$ openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt
和
-
$ openssl genrsa -des3 -out client.key 4096
-
$ openssl req -new -key client.key -out client.csr
-
$ openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt
其余的请按照链接中的步骤操作。 Chrome的证书pipe理工作与上面提到的firefox示例相同。
接着,通过以下方式设置服务器
请注意,您已经创build了服务器.crt和.key,因此您不必再执行该步骤。
我认为这里的修复是keystoretypes,pkcs12(pfx)总是有私钥,JKStypes可以不存在私钥存在。 除非您在代码中指定或通过浏览器select证书,否则服务器无法知道它代表另一端的客户端。