2013年1月28日 星期一

Java 與 Tomcat 搭配的加密連線:HTTPS 雙向認證

這篇嘗試的是雙向認證的 HTTPS,不過在認證方面仍然是採用自我認證,沒有使用公開的 CA。
其中根據書上所說,雙向認證跟單向認證的差別在於,雙向認證會讓客戶端和伺服器端互相認證對方,單向認證只有認證伺服器(確認伺服器是預期的那個)而已。

PS. 參考資料中,比較建議參考的對象是 [3,5],實際上最後我成功的方法就是綜合 [3,5] 得到的。


在進行 Java 之前,首先要先在 Tomcat 上面設定 HTTPS 以及 TLS 所需要的簽章。
這裡會引用一些 [5] 的說明。特別提到 [5] 是因為 [3] 的過程其實我都不知道那些動作在幹嘛,直到看了 [5] 以後才慢慢抓到那些動作大概的意義。

我的環境如下。
伺服器:ubuntu 12.04 server + Sun JDK 6 Update 38 + Tomcat 6.0.36
其中我的 JDK 的路徑在 /usr/local/jvm/jdk1.6.0_38/。

1、Tomcat 設定
1.1、為伺服器產生簽章庫
sudo /usr/local/jvm/jdk1.6.0_38/bin/keytool -genkey -v -alias tomcat -keyalg RSA -validity 10000 -keystore ./tomcat.keystore -dname "CN=localhost,OU=my,O=my,L=Taipei,ST=Taiwan,c=TW" -storepass changeit -keypass changeit
這個指令的結果,會在目前所在的目錄中產生一個 tomcat.keystore 的檔案,這個檔案就是要給 Tomcat 使用的簽章。

參數 -validity 表示這個簽章的有效期間,數字是以天為單位,因此指定 10000 表示有效期限為 10000 天。

參數 -dname 表示指定簽章中的相關資訊,如果不給這個參數的話,keytool 仍然會一個一個詢問。參數內容的意義轉載自 [4]:
CN:Common Name 名字與姓氏
OU:Organization Unit組織單位名稱
O:Organization組織名稱
L:Locality城市或區域名稱
ST:State州或省份名稱
C:Country國家名稱
其中要注意的是,如果只是測試用的話,可以像上面的指令一樣,CN 用 localhost 就好。但如果是真的要上線在公開網站上使用的話,CN 必須要輸入伺服器的永久網域名稱(IP 好像也可以,但 IP 也必須要是固定不變的),因為這會限制以後程式(也許也包含瀏覽器?)使用 HTTPS 連接時,必須要用 CN 指定的這個網域來連接才行。

PS. 如果使用的 CN 跟 Java 實際使用的網域不同時,Java 會吐以下的 Exception 而無法成功建立 HTTPS 連線。
java.security.cert.CertificateException: No subject alternative names present

1.2、為客戶端產生簽章庫
sudo /usr/local/jvm/jdk1.6.0_38/bin/keytool -genkey -v -alias client -keyalg RSA -storetype PKCS12 -validity 10000 -keystore ./client.p12 -dname "CN=localhost,OU=my,O=my,L=Taipei,ST=Taiwan,c=TW" -storepass changeit -keypass changeit
這個指令的結果會在目前所在的目錄產生一個 cosaclient.p12 的檔案,這個檔案是要給客戶端使用的簽章。
其中因為客戶端包含瀏覽器,為了要讓 IE 和 FireFox 可以匯入簽章,簽章必須要使用 PKCS12 格式。
另外客戶端使用的 CN 可以是任意值。

附註:如果想看簽章的內容,可以執行以下的 script,會顯示簽章所使用的演算法資訊。
openssl pkcs12 -in client.p12 -info -noout

1.3、讓 Tomcat 伺服器信任產生的客戶端簽章
在這個步驟中,首先因為沒辦法直接將 PKCS12 格式的簽章庫導入,因此要先把簽章庫匯出成 CER 的檔案。
sudo /usr/local/jvm/jdk1.6.0_38/bin/keytool -export -alias client -keystore ./client.p12 -storetype PKCS12 -storepass changeit -rfc -file ./client.cer
匯出後會產生一個 client.cer 的檔案,接著用這個匯出的 CER 檔案再導入到伺服器的簽章上,讓伺服器信任這個簽章。

sudo /usr/local/jvm/jdk1.6.0_38/bin/keytool -import -file ./client.cer -keystore ./tomcat.keystore -storepass changeit

1.4、讓客戶端信任伺服器的簽章
類似的步驟,反過來要讓客戶端把伺服器加入信任的簽章。
同樣的,也是要先把簽章庫匯出成為簽章,然後再用簽章產生一個 TrustStore 的檔案。這裡比較不同的是,使用匯入的指令時,我們是產生另一個檔案,而不是像步驟 1.3 那樣把簽章加入已經產生的 KeyStore。
sudo /usr/local/jvm/jdk1.6.0_38/bin/keytool -export -alias tomcat -keystore ./tomcat.keystore -storepass changeit -rfc -file ./tomcat.cer
sudo /usr/local/jvm/jdk1.6.0_38/bin/keytool -import -alias tomcat -file ./tomcat.cer -keystore ./client.truststore -storepass changeit
兩個指令完成後,會產生一個 client.truststore 的檔案。

1.5、設定 Tomcat
簽章都產生完以後,就要設定 Tomcat 了。
首先在 Tomcat 的 server.xml 裡找到以下的片段,將它們的註解刪掉。
<Connector port="443" protocol="HTTP/1.1" SSLEnabled="true" maxThreads="150" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" />
然後在最後面補上 KeyStore 和 TrustStore 的資訊,兩個都指向步驟 1.1 產生的簽章庫的檔案。
<Connector port="443" protocol="HTTP/1.1" SSLEnabled="true" maxThreads="150" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS"
keystoreFile="/usr/local/TOMCAT/key/tomcat.keystore" keystorePass="changeit"
truststoreFile="/usr/local/TOMCAT/key/tomcat.keystore" truststorePass="changeit" />
設定完以後重開 Tomcat。

Java Client
在 Java 端的變動,其實只有在建立連線之前先設定 TLS,然後建立連線時改 cast 成 HttpsURLConnection。
FileInputStream keyStoreStream = null;
FileInputStream trustStoreStream = null;
try {
  keyStoreStream = new FileInputStream(new File("D:/client.p12"));
  KeyStore clientStore = KeyStore.getInstance("PKCS12");
  clientStore.load(keyStoreStream, "changeit".toCharArray());

  KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
  kmf.init(clientStore, "changeit".toCharArray());
  KeyManager[] kms = kmf.getKeyManagers();

  KeyStore trustStore = KeyStore.getInstance("JKS");
  trustStoreStream = new FileInputStream(new File("D:/client.truststore"));
  trustStore.load(trustStoreStream, "changeit".toCharArray());

  TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  tmf.init(trustStore);
  TrustManager[] tms = tmf.getTrustManagers();

  SSLContext sslContext = SSLContext.getInstance("TLS");
  sslContext.init(kms, tms, new SecureRandom());

  HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
} catch (KeyStoreException e) {
  e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
  e.printStackTrace();
} catch (CertificateException e) {
  e.printStackTrace();
} catch (UnrecoverableKeyException e) {
  e.printStackTrace();
} catch (KeyManagementException e) {
  e.printStackTrace();
} finally {
  if (keyStoreStream != null) keyStoreStream.close();
  if (trustStoreStream != null) trustStoreStream.close();
}

// Construct the connection.
HttpsURLConnection httpConnection =  (HttpsURLConnection) url.openConnection();

這裡有用到兩個檔案,分別是前面步驟 1.2 和 1.4 產生的 client.p12client.truststore
這兩個檔案必須先從伺服器那邊取得,讓 Java 能夠在建立連線時使用這兩個伺服器產生的簽證檔案。

另外可以注意到最後一行,原本開啟連線時,是把 URLConnection cast 成 HttpURLConnection
但要用 HTTPS 時就要改成 cast 到 HttpsURLConnection。
連線開啟後,後面的使用方法都跟一般 HttpURLConnection 完全一樣了。

2013-03-25 補充:
在查了一些資料後,發現不管是單向認證還是雙向認證,都必須要讓客戶端先具有證書檔
但是現在被要求要像瀏覽器那樣,不需要證書就可以接受伺服器的自我認證。
這時就只能使用 [7] 回應中比較不建議使用的方法了。

在上面 Java Client 部分的程式碼中,4~21 行的部份置換成以下的程式碼,就可以跳過證書認證(即一律信任伺服器的認證)。
sslContext = SSLContext.getInstance("TLS");

// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] {
  new javax.net.ssl.X509TrustManager() {
    public X509Certificate[] getAcceptedIssuers() {
      return null;
    }
    
    public void checkClientTrusted(X509Certificate[] certs, String authType) {}
    
    public void checkServerTrusted(X509Certificate[] certs, String authType) {}
  }
};
        
sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); 

參考資料:
1、Tomcat配备SSL单向认证
2、Java SSL: unable to find valid certification path to requested target
3、Java Tomcat SSL 服务端/客户端双向认证(一)
4、JAVA数字证书及TOMCAT SSL支持配置说明
5、Tomcat 6中配置SSL双向认证
6、Java 2-way TLS/SSL (Client Certificates) and PKCS12 vs JKS KeyStores
7、telling java to accept self-signed ssl certificate

沒有留言: