1. 程式人生 > >在Android應用中使用自定義證書的HTTPS連線(下)

在Android應用中使用自定義證書的HTTPS連線(下)

因為這部分才是本文的重點,要說得詳細一點,所以單獨做成一篇來說。

安全地使用自定義證書的HTTPS連線方式

終極解決方案是:把證書編譯到應用中去,由應用自己來驗證證書。

生成KeyStore

要驗證自定義證書,首先要把證書編譯到應用中去,這需要JSSE提供的keytool工具來生成KeyStore檔案。參考《Java 安全套接字程式設計以及 keytool 使用最佳實踐》,我試過了用JKS格式,但是結果連線失敗,報錯:Wrong version of key store。後來看了SO的這個帖才知道必須使用BKS的1.46版。更詳細的內容參考這篇《Using a Custom Certificate Trust Store on Android

》。

這裡所謂的證書,實際上就是公鑰,你可以從web伺服器配置的.crt檔案或.pem檔案裡獲得。比如12306就直接提供了公鑰證書下載,真是“服務周到”啊。

還 有一個比較簡單的辦法就是直接從瀏覽器裡獲得。比如用 FireFox 開啟 https 連結,在位址列頂部的小鎖上點一下,然後點“更多資訊……”-“檢視證書”-“詳細內容”-“匯出”,即可將網站的X.509證書匯出為一個文字檔案。不 過需要注意的是,這種方法只對某些HTTPS伺服器有效——通常是使用自簽名證書或是使用類似StarCom免費證書伺服器,但是對 12306 或 google 這種的就無效了,具體原因不明。

另外,不論是瀏覽器匯出,還是伺服器端獲得,都是公鑰證書,有兩種格式:純文字的.crt格式或是二進位制的.cer格式。兩種都可以用。

然後,你需要一個特定版本的JCE Provider,就是上面說過的那個SO帖裡給的:http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar 。注意,bouncycastle官網上目前釋出的1.50版我試了一下不可用,不知道是不是我開啟的方式不對,總之用這個1.46版的是沒錯的。

把這兩個檔案放在一起,然後在這個目錄下執行以下命令:

keytool -importcert -v -trustcacerts -alias cert12306 -file srca.cer \
-keystore cert12306.bks -storetype BKS \
-providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath ./bcprov-jdk15on-146.jar -storepass pw12306

執行後將顯示證書內容並提示你是否確認,按Y回車確認即可。

其中cert12306是個隨便取的別名,供keytool管理時方便而已。srca.cer就是從12306網站下載的證書檔案。cert12306.bks是生成的keyStore檔案,注意,這個檔案必須以JAVA變數名的方式命名,比如不能直接叫12306.bks,否則在載入資源時會因為名字不是合格的JAVA變數名而出錯。 ./bcprov-jdk15on-146.jar 就是剛才下載的那個JCE Provider。最後pw12306是一個密碼,用於確保KeyStore檔案本身的安全。

使用自定義keyStore實現連線

以下就是這個方案的實現,基本上和TrustAll差不多,也是需要一個自定義的SSLSocketFactory,不過因為還是需要驗證證書的,所以就不需要再定義TrustManager了,用系統內建的即可。不過為了讀取KeyStore資源,需要增加一個Context引數。

另外,前面生成的那個cert12306.bks檔案要放到 res/raw/ 目錄下。

public class SSLCustomSocketFactory extends SSLSocketFactory {
    private static final String TAG = "SSLCustomSocketFactory";

    private static final String KEY_PASS = "pw12306";

    public SSLCustomSocketFactory(KeyStore trustStore) throws Throwable {
        super(trustStore);
    }

    public static SSLSocketFactory getSocketFactory(Context context) {
        try {
            InputStream ins = context.getResources().openRawResource(R.raw.cert12306);

            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            try {
                trustStore.load(ins, KEY_PASS.toCharArray());
            }
            finally {
                ins.close();
            }
            SSLSocketFactory factory = new SSLCustomSocketFactory(trustStore);
            return factory;
        } catch (Throwable e) {
            Log.d(TAG, e.getMessage());
            e.printStackTrace();
        }
        return null;
    }
}

其中cert12306就是bks資原始檔名,pw12306就是前面設定的密碼。

同樣的,使用這個Factory註冊到scheme:

schReg.register(new Scheme("https", SSLCustomSocketFactory.getSocketFactory(context), 443));

現在,也可以成功地連線12306了,而且如此實現,基本上就能達到與用商業證書一樣的安全性了。

不過因為現在只使用了12306的證書,沒有使用系統證書,所以只能連線12306,連GOOGLE都連線不了了。但這個問題好解決,只需要在連線不同網站時使用不同的HttpClient即可。或者自己實現一個混合驗證的SSLSocketFactory。

更加安全的雙向認證的HTTPS連線方式

當然,還有一種更安全的HTTPS通訊方式叫做雙向認證。相對的,上面所說的全都是指服務端的單向認證:即只有伺服器配置了證書,客戶端只是使用伺服器證書的公鑰。而雙向認證則是客戶端也有一個由伺服器簽發的證書,這樣伺服器可以確認連線過來的客戶端的身份,網路銀行就使用了這種方式。

雙向認證的實現方式與本篇的實現方式差不多,只是在trustStore基礎之上再增加一個keyStore,其內容為客戶端證書,當然這個證書就是同時包含公鑰和私鑰的。因為搭建一個雙向認證的伺服器比較麻煩,一般應用也沒必要,所以這裡不再詳細說明。