2018年3月31日 星期六

Java - HTTPS 檢查證書的安全連線


如何在JDK1.5中支援TLSv1.2這篇文章中,有講到如何對TLSv1.2的server進行HTTPS request,但這只是最簡單的實作例子,沒有考慮到憑證安全的檢查,如果被例如中間人攻擊的話,可能會連線請求中間被竄改了資料而不自知。

在前例中,可以看到在自訂的TrustManager 中,所有的method (getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())都沒有被實作內容,
如果發生了中間人攻擊,也就是在client端和server端的連線中間被某中間人攔截,
這時中間人可能會修改某些資料偽裝server端回傳資料給我們(client)端,
如果我們沒有對其憑證進行檢查的話,可能就無法察覺此攻擊了。

尤其是行動裝置的場合,例如手機在連到了駭客分享的wifi,程式如果不檢查HTTPS連線有無被劫持,就很容昜造成資安問題。

經過了查詢資料及自行實作後,我在這邊提供一個如何對 HTTPS 的 憑證進行驗證的方法。


  1. 為了簡單化,我使用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。
  2. 我們可以使用封包監測工具,例如Fiddler,來模擬中間人攻擊,如果我們把Java的HTTPS request 掛上Proxy,讓連線中間經過Fiddler的話,相當於就是Fiddler當了中間人,這時對使用了沒有進行憑證檢查的TrustManager的Java程式,是不會發現有任何異狀的。

步驟:

  1. 這裡要連的實驗對像跟之前一樣是號稱只支持TLSv1.2的fancyssl網站,首先去此網站上下載憑證。
    下載方式如下圖,按下F12打開開發者console後,選Security --> View Certificate --> 詳細資料 --> 複制到檔案。
  2. 使用java的工具 keytool 加入憑證到你要的憑證檔案中
    keytool -import -alias {別名} -keystore {要被放入憑證的憑證檔} -file {從網站上下載的憑證}
    例如:
    keytool -import -alias fancyssl -keystore D:\test_cer -file D:\fancyssl_cer.cer
    之後會要你打密碼,第一次產生憑證檔自己取。如果是本來就有的檔案通常密碼沒改就是預設 : changeit
  3. 接下來我展示在Java1.8下的三種寫法:
    1. 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);
       }
      }
    2. 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);
       }
      }
    3. 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);
       }
      }
我們可以打開 Fiddler,並在 Tools --> Options --> HTTPS,勾選 "Decrypt HTTPS traffic 設定攔截 HTTPS request 模擬中間人,
並且在上述的三個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

參考資料
  1. Java 使用自签证书访问https站点
  2. 苹果核 - Android App 安全的HTTPS 通信
  3. Sun Java System Application Server Enterprise Edition 8.2 管理指南
  4. Java Keytool的使用及申請憑證(以Microsoft Active Directory Certificate Services為例)
  5. java中 SSL认证和keystore使用

沒有留言 :

張貼留言