在如何在JDK1.5中支援TLSv1.2這篇文章中,有講到如何對TLSv1.2的server進行HTTPS request,但這只是最簡單的實作例子,沒有考慮到憑證安全的檢查,如果被例如中間人攻擊的話,可能會連線請求中間被竄改了資料而不自知。
在前例中,可以看到在自訂的TrustManager 中,所有的method (getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())都沒有被實作內容,
如果發生了中間人攻擊,也就是在client端和server端的連線中間被某中間人攔截,
這時中間人可能會修改某些資料偽裝server端回傳資料給我們(client)端,
如果我們沒有對其憑證進行檢查的話,可能就無法察覺此攻擊了。
尤其是行動裝置的場合,例如手機在連到了駭客分享的wifi,程式如果不檢查HTTPS連線有無被劫持,就很容昜造成資安問題。
- 為了簡單化,我使用JDK1.8,跟JDK1.5不同,JDK1.8支持TLSv1.2,
所以不用跟上一篇文(如何在JDK1.5中支援TLSv1.2) 一樣使用BoncyCastle。
並且把Excpetion全部throws出去,不用try-catch來使程式碼較為清晰。
Note:
其實JDK1.8在對TLSv1.2連線時,是不用自已建立TrustManager的,並且自己就會檢查憑證,並在檢查憑證有問題時拋出錯誤。
但是要注意如果自己建立了TrustManager來用在連線中,卻不檢查憑證的話,在憑證有問題時是不會拋出錯誤的。
而JDK1.5因為要使用BoncyCastle並會需要自已建立TrustManager,所以就要特別使用檢查憑證的TrustManager。 - 我們可以使用封包監測工具,例如Fiddler,來模擬中間人攻擊,如果我們把Java的HTTPS request 掛上Proxy,讓連線中間經過Fiddler的話,相當於就是Fiddler當了中間人,這時對使用了沒有進行憑證檢查的TrustManager的Java程式,是不會發現有任何異狀的。
步驟:
- 這裡要連的實驗對像跟之前一樣是號稱只支持TLSv1.2的fancyssl網站,首先去此網站上下載憑證。
下載方式如下圖,按下F12打開開發者console後,選Security --> View Certificate --> 詳細資料 --> 複制到檔案。 - 使用java的工具 keytool 加入憑證到你要的憑證檔案中
keytool -import -alias {別名} -keystore {要被放入憑證的憑證檔} -file {從網站上下載的憑證}
例如:
keytool -import -alias fancyssl -keystore D:\test_cer -file D:\fancyssl_cer.cer
之後會要你打密碼,第一次產生憑證檔自己取。如果是本來就有的檔案通常密碼沒改就是預設 : changeit - 接下來我展示在Java1.8下的三種寫法:
- HttpsTest_notSafe.java
(自己建立的,沒有檢查憑證的TrustManager)import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; public class HttpsTest_notSafe { public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException { // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊 System.setProperty("http.proxyHost", "127.0.0.1"); System.setProperty("http.proxyPort", "8888"); System.setProperty("https.proxyHost", "127.0.0.1"); System.setProperty("https.proxyPort", "8888"); // // 自訂的TrustManager,沒有檢查憑證,不安全 TrustManager trustManager = new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // do nothing } public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // do nothing } }; SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); // 自訂的TrustManager,沒有檢查憑證,不安全 sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection(); httpsUrlConnection.connect(); // 印出Response printFromInputStream(httpsUrlConnection.getInputStream()); httpsUrlConnection.disconnect(); } static void printFromInputStream(InputStream in) throws IOException { BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in))); StringBuffer responseTextStringBuffer = new StringBuffer(); String tempString = null; while ((tempString = responseBufferedReader.readLine()) != null) { responseTextStringBuffer.append(tempString + "\n"); } String responseText = responseTextStringBuffer.toString(); System.out.println(responseText); } }
- HttpsTest_safe1.java
(JDK1.8 直接使用自帶TLSv1.2的https支持,不用建立TrustManager)import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; public class HttpsTest_safe1 { public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException { // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊 System.setProperty("http.proxyHost", "127.0.0.1"); System.setProperty("http.proxyPort", "8888"); System.setProperty("https.proxyHost", "127.0.0.1"); System.setProperty("https.proxyPort", "8888"); // //建立連線 HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection(); httpsUrlConnection.connect(); // 印出Response printFromInputStream(httpsUrlConnection.getInputStream()); httpsUrlConnection.disconnect(); } static void printFromInputStream(InputStream in) throws IOException { BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in))); StringBuffer responseTextStringBuffer = new StringBuffer(); String tempString = null; while ((tempString = responseBufferedReader.readLine()) != null) { responseTextStringBuffer.append(tempString + "\n"); } String responseText = responseTextStringBuffer.toString(); System.out.println(responseText); } }
- HttpsTest_safe2.java
(自已建立的,使用憑證檔產生出TrustManager)import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; public class HttpsTest_safe2 { public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException { // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊 System.setProperty("http.proxyHost", "127.0.0.1"); System.setProperty("http.proxyPort", "8888"); System.setProperty("https.proxyHost", "127.0.0.1"); System.setProperty("https.proxyPort", "8888"); // //從憑證產生TrustManager String password = "changeit"; //預設密碼為changeit //讀取憑證 File file = new File("D:/test_cer"); //通常會將憑證都加到%Java_Home%/lib/security/cacerts/中 //File file = new File(System.getProperty("java.home") + "/lib/security/cacerts"); InputStream in = new FileInputStream(file); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(in, password.toCharArray()); in.close(); //建立TrustManager TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509", "SunJSSE"); trustManagerFactory.init(keyStore); SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection(); httpsUrlConnection.connect(); // 印出Response printFromInputStream(httpsUrlConnection.getInputStream()); httpsUrlConnection.disconnect(); } static void printFromInputStream(InputStream in) throws IOException { BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in))); StringBuffer responseTextStringBuffer = new StringBuffer(); String tempString = null; while ((tempString = responseBufferedReader.readLine()) != null) { responseTextStringBuffer.append(tempString + "\n"); } String responseText = responseTextStringBuffer.toString(); System.out.println(responseText); } }
- HttpsTest_notSafe.java
並且在上述的三個Java程式中,對設定Proxy的程式碼註解或不註解來測試。
可以發現以下結果:
有拋出錯誤沒有拋出錯誤有拋出錯誤沒有拋出錯誤
有經過Fiddler | 沒有經過Fiddler | |
HttpsTest_notSafe.java | 沒有拋出錯誤 | 沒有拋出錯誤 |
HttpsTest_safe1.java | 有拋出錯誤 | 沒有拋出錯誤 |
HttpsTest_safe2.java | 有拋出錯誤 | 沒有拋出錯誤 |
可以看到,如果是JDK1.8如果使用了自帶TLSv1.2的支持寫法,即不自己建立TrustManager的話,程式是可以自行檢查出憑證錯誤的。
但如果使用了自已建立的TrustManager時,就要使用有檢查憑證的TrustManager才行,此篇文章使用的是由憑證檔產生TrustManager。
除了由憑證檔產生TrustManager以外,也可由自己撰寫檢查憑證相關的TrustManager method ((getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())) 。
原始碼下載:
HttpsTest.7z
但如果使用了自已建立的TrustManager時,就要使用有檢查憑證的TrustManager才行,此篇文章使用的是由憑證檔產生TrustManager。
除了由憑證檔產生TrustManager以外,也可由自己撰寫檢查憑證相關的TrustManager method ((getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())) 。
原始碼下載:
HttpsTest.7z
參考資料 :