2009-11-03 76 views
170

我对HTTPS/SSL/TLS相当陌生,我对使用证书进行身份验证时客户端应该呈现的内容有些困惑。Java HTTPS客户端证书身份验证

我在写一个Java客户端,需要做一个简单的POST数据到一个特定的URL。这部分工作正常,唯一的问题是它应该通过HTTPS完成。 HTTPS部分相当容易处理(使用HTTPclient或使用Java的内置HTTPS支持),但我坚持使用客户端证书进行身份验证。我注意到在这里已经有一个非常类似的问题,我还没有用我的代码尝试过(将尽快完成)。我目前的问题是 - 无论我做什么 - Java客户端永远不会发送证书(我可以使用PCAP转储来检查此问题)。

我想知道当用证书进行身份验证时,客户端应该向服务器提供什么(专门用于Java--如果这很重要)?这是一个JKS文件,还是PKCS#12?他们应该有什么;只是客户端证书或密钥?如果是这样,哪个键?关于所有不同类型的文件,证书类型等都存在相当多的混淆。

正如我之前说过的,我是HTTPS/SSL/TLS的新手,所以我希望能够获得一些背景信息(不必是散文,我会为优秀文章提供链接)。

回答

186

终于成功地解决所有问题,所以我会回答我的问题。这些是我用来解决特定问题的设置/文件;

客户端的密钥库PKCS#12格式文件,其中包含

  1. 客户端的公共证书(在这种情况下通过自签名CA签名)
  2. 客户端的私人

要生成它我用OpenSSL的0例如,命令;

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever" 

提示:确保您获得最新的OpenSSL,版本0.9.8h,因为这似乎从它不允许你正确生成PKCS#12文件存在的问题的困扰。

当服务器明确请求客户端进行身份验证时,Java客户端将使用此PKCS#12文件将客户端证书呈现给服务器。请参阅Wikipedia article on TLS以了解客户端证书身份验证协议实际工作原理的概述(也解释了为什么我们需要此处的客户端私钥)。

客户的信任是包含中间CA证书直向前JKS格式文件。这些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握手。

上述文件仅限客户端使用。当你想要设置服务器时,服务器也需要自己的密钥和信任库文件。可以在this website上找到为Java客户端和服务器(使用Tomcat)设置完整工作示例的很好的演练。

问题/备注/提示

  1. 客户端证书认证只能由服务器来执行。
  2. 重要!)当服务器请求客户端证书(作为TLS握手的一部分)时,它还将提供可信CA的列表作为证书请求的一部分。当您希望提供身份验证的客户端证书是而不是由其中一个CA签名时,它将根本不会呈现(在我看来,这是奇怪的行为,但我确信这是有原因的) 。这是我的问题的主要原因,因为另一方没有正确配置他们的服务器来接受我的自签名客户端证书,并且我们认为问题在于我没有在请求中正确提供客户端证书。
  3. 获取Wireshark。它具有很好的SSL/HTTPS数据包分析功能,对于调试和发现问题将有很大的帮助。它与-Djavax.net.debug=ssl类似,但如果您对Java SSL调试输出不舒服,则更具结构化(可以说)更易于解释。
  4. 完全可以使用Apache httpclient库。如果您想使用httpclient,只需将目标URL替换为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
+3

“当您希望提供身份验证的客户端证书未由其中一个CA签名时,将根本不会显示”。因为客户端知道他们不会被服务器接受,所以不会显示证书。此外,您的证书可以由中间CA“ICA”进行签名,并且服务器可以为您的客户端显示根CA“RCA”,即使您的证书由ICA而非RCA签名,仍然可以让您选择证书。 – KyleM 2014-03-18 19:43:32

+2

作为上述评论的一个例子,考虑一种情况,您有一个根CA(RCA1)和两个中间CA(ICA1和ICA2)。在Apache Tomcat上,如果您将RCA1导入到信任库中,则您的Web浏览器将显示所有由ICA1和ICA2签名的证书,即使它们不在您的信任库中。这是因为它不是单个证书而是重要的链条。 – KyleM 2014-03-18 19:57:54

+2

“在我看来,这是奇怪的行为,但我相信这是有原因的”。原因是这就是它在RFC 2246中所说的。没有什么奇怪的。允许客户出示不被服务器接受的证书是奇怪的,而且会浪费时间和空间。 – EJP 2014-07-29 01:42:43

24

他们JKS文件只是一个证书和密钥对的容器。 在客户端验证的情况下,密钥的各个部分将在这里位于:

  • 客户的商店将包含客户端的私有和公共密钥对。它被称为密钥库
  • 服务器的存储将包含客户端的公共密钥。它被称为信任库

信任库和密钥库的分离不是必需的,但推荐使用。它们可以是相同的物理文件。

要设置两个店的文件系统位置,使用下面的系统属性:

-Djavax.net.ssl.keyStore=clientsidestore.jks 

,并在服务器上:

-Djavax.net.ssl.trustStore=serversidestore.jks 

要导出客户端的证书(公钥)到文件,这样你就可以将它复制到服务器上,使用

keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks 

导入客户端的公共区域C键到服务器的密钥存储,使用(如所提到的海报,这已经由服务器管理员完成)

keytool -import -file publicclientkey.cer -store serversidestore.jks 
+0

我应该提到我无法控制服务器。服务器已导入我们的公共证书。该系统的管理员告诉我,我需要明确提供证书,以便在握手期间发送证书(他们的服务器明确要求此证书)。 – tmbrggmn 2009-11-03 10:26:59

+0

您需要将公钥和私钥作为JKS文件用于您的公共证书(服务器已知)。 – sfussenegger 2009-11-03 10:38:30

+0

感谢您的示例代码。 在上面的代码中,什么是“mykey-public.cer”?这是客户公共证书(我们使用自签名证书)吗? – tmbrggmn 2009-11-04 08:21:06

8

的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; 
} 

} 
+0

如果您希望证书可用于所有使用特定JVM安装的应用程序,请按照[此答案](http: //stackoverflow.com/a/16397662/1134080)。 – ADTC 2015-05-06 10:37:55

+0

是用于设置客户端项目代理权限的'configureRequest()'方法吗? – shareef 2016-02-29 21:06:57

+0

是的,它是http客户端配置,在这种情况下它是代理配置 – wildloop 2016-03-02 00:10:50

26

其他答案显示如何全局配置客户端证书。 但是,如果你想以编程方式定义客户端密钥对一个特定连接,而不是在你的JVM上运行的所有应用程序全局定义它,那么你可以配置你自己的SSL连接,像这样:

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")); 
+0

我不得不使用'sslContext = SSLContexts.custom()。loadTrustMaterial(keyFile,PASSWORD).build();'。我无法使用'loadKeyMaterial(...)'处理它。 – 2016-11-18 11:00:24

+1

@ConorSvensson信任材料用于信任远程服务器证书的客户端,密钥材料用于信任客户端的服务器。 – Magnus 2016-11-18 13:54:12

+1

我真的很喜欢这个简洁明了的答案。如果有人感兴趣,我在这里提供了一个工作示例,并附有构建说明。 https://stackoverflow.com/a/46821977/154527 – 2017-10-19 15:43:42

0

我觉得修复这里是keystore类型,pkcs12(pfx)总是有私钥,而JKS类型可以不存在私钥存在。除非您在代码中指定或通过浏览器选择证书,否则服务器无法知道它代表另一端的客户端。

相关问题