2009-11-03 75 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類型可以不存在私鑰存在。除非您在代碼中指定或通過瀏覽器選擇證書,否則服務器無法知道它代表另一端的客戶端。

相關問題