天天看點

Android HTTPS 自制證書實作雙向認證(OkHttp + Retrofit + Rxjava)

由于最近要做一個安全性比較高的項目,是以需要用到HTTPS進行雙向認證。由于設計項目架構的時候,用戶端是采用MVVM架構,基于DataBinding + Retrofit + Rxjava來實作Android端。

查閱很多資料,基于原生HttpClient實作雙向認證的例子很多,但對于Retrofit的資料網上還是比較少,官方文檔也是一句帶過,沒有具體的介紹。

看了

《Android中https請求的單向認證和雙向認證》 ,給了我很大的啟發,于是嘗試着部落客的方式制作證書,再次嘗試的時候果然成功了。

科普一下,什麼是HTTPS?

簡單來說,HTTPS就是“安全版”的HTTP, HTTPS = HTTP + SSL。HTTPS相當于在應用層和TCP層之間加入了一個SSL(或TLS),SSL層對從應用層收到的資料進行加密。TLS/SSL中使用了RSA非對稱加密,對稱加密以及HASH算法。

RSA算法基于一個十分簡單的數論事實:将兩個大素數相乘十分容易,但那時想要對其乘積進行因式分解卻極其困難,是以可以将乘積公開作為加密密鑰。

SSL:(Secure Socket Layer,安全套接字層),為Netscape所研發,用以保障在Internet上資料傳輸之安全,利用資料加密(Encryption)技術,可確定資料在網絡上之傳輸過程中不會被截取。它已被廣泛地用于Web浏覽器與伺服器之間的身份認證和加密資料傳輸。SSL協定位于TCP/IP協定與各種應用層協定之間,為資料通訊提供安全支援。

SSL協定可分為兩層:

SSL記錄協定(SSL Record Protocol):它建立在可靠的傳輸協定(如TCP)之上,為高層協定提供資料封裝、壓縮、加密等基本功能的支援。

SSL握手協定(SSL Handshake Protocol):它建立在SSL記錄協定之上,用于在實際的資料傳輸開始前,通訊雙方進行身份認證、協商加密算法、交換加密密鑰等。

TLS:(Transport Layer Security,傳輸層安全協定),用于兩個應用程式之間提供保密性和資料完整性。TLS 1.0是IETF(Internet Engineering Task Force,Internet工程任務組)制定的一種新的協定,它建立在SSL 3.0協定規範之上,是SSL 3.0的後續版本,可以了解為SSL 3.1,它是寫入了 RFC的。

該協定由兩層組成: TLS 記錄協定(TLS Record)和 TLS 握手協定(TLS Handshake)。

進入正文

基于Retrofit實作HTTPS思路

由于Retrofit是基于OkHttp實作的,是以想通過Retrofit實作HTTPS需要給Retrofit設定一個OkHttp代理對象用于處理HTTPS的握手過程。代理代碼如下:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//為OkHttp對象設定SocketFactory用于雙向認證
    .hostnameVerifier(new UnSafeHostnameVerifier())
    .build();
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://10.2.8.56:8443")
    .addConverterFactory(GsonConverterFactory.create())//添加 json 轉換器
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 擴充卡
    .client(okHttpClient)//添加OkHttp代理對象
    .build();
           

證書制作思路:

首先對于雙向證書驗證,也就是說,

用戶端持有服務端的公鑰證書,并持有自己的私鑰,服務端持有客戶的公鑰證書,并持有自己私鑰,

建立連接配接的時候,用戶端利用服務端的公鑰證書來驗證伺服器是否上是目标伺服器;服務端利用用戶端的公鑰來驗證用戶端是否是目标用戶端。(請參考RSA非對稱加密以及HASH校驗算法)

服務端給用戶端發送資料時,需要将服務端的證書發給用戶端驗證,驗證通過才運作發送資料,同樣,用戶端請求伺服器資料時,也需要将自己的證書發給服務端驗證,通過才允許執行請求。

下面我畫了一個圖,來幫助大家來了解雙向認證的過程,證書生成流程,以及各個檔案的作用,大家可以對照具體步驟來看

相關格式說明

JKS:數字證書庫。JKS裡有KeyEntry和CertEntry,在庫裡的每個Entry都是靠别名(alias)來識别的。

P12:是PKCS12的縮寫。同樣是一個存儲私鑰的證書庫,由.jks檔案導出的,使用者在PC平台安裝,用于标示使用者的身份。

CER:俗稱數字證書,目的就是用于存儲公鑰證書,任何人都可以擷取這個檔案 。

BKS:由于Android平台不識别.keystore和.jks格式的證書庫檔案,是以Android平台引入一種的證書庫格式,BKS。

有些人可能有疑問,為什麼Tomcat隻有一個server.keystore檔案,而用戶端需要兩個庫檔案?

因為有時用戶端可能需要通路過個服務,而伺服器的證書都不相同,是以用戶端需要制作一個truststore來存儲受信任的伺服器的證書清單。是以為了規範建立一個truststore.jks用于存儲受信任的伺服器證書,建立一個client.jks來存儲用戶端自己的私鑰。對于隻涉及與一個服務端進行雙向認證的應用,将server.cer導入到client.jks中也可。

具體步驟如下:

1.生成用戶端keystore
keytool -genkeypair -alias client -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore client.jks
           

2.生成服務端keystore

keytool -genkeypair -alias server -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore server.keystore
//注意:CN必須與IP位址比對,否則需要修改host
           

3.導出用戶端證書

keytool -export -alias client -file client.cer -keystore client.jks -storepass 123456 
           

4.導出服務端證書

keytool -export -alias server -file server.cer -keystore server.keystore -storepass 123456 
           

5.重點:證書交換

将用戶端證書導入服務端keystore中,再将服務端證書導入用戶端keystore中, 一個keystore可以導入多個證書,生成證書清單。
生成用戶端信任證書庫(由服務端證書生成的證書庫):
    keytool -import -v -alias server -file server.cer -keystore truststore.jks -storepass 123456 
将用戶端證書導入到伺服器證書庫(使得伺服器信任用戶端證書):
    keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456 
           

6.生成Android識别的BKS庫檔案

用Portecle工具轉成bks格式,最新版本是1.10。
下載下傳連結:https://sourceforge.net/projects/portecle/
運作protecle.jar将client.jks和truststore.jks分别轉換成client.bks和truststore.bks,然後放到android用戶端的assert目錄下

>File -> open Keystore File -> 選擇證書庫檔案 -> 輸入密碼 -> Tools -> change keystore type -> BKS -> save keystore as -> 儲存即可

這個操作很簡單,如果不懂可自行百度。

我在Windows下生成BKS的時候會報錯失敗,後來我換到CentOS用OpenJDK1.7立馬成功了,如果在這步失敗的同學可以換到Linux或Mac下操作,
将生成的BKS拷貝回Windows即可。
           

7.配置Tomcat伺服器

修改server.xml檔案,配置8443端口
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
           clientAuth="true" sslProtocol="TLS"
           keystoreFile="${catalina.base}/key/server.keystore" keystorePass="123456"
           truststoreFile="${catalina.base}/key/server.keystore" truststorePass="123456"/>

備注: - keystoreFile:指定伺服器密鑰庫,可以配置成絕對路徑,本例中是在Tomcat目錄中建立了一個名為key的檔案夾,僅供參考。 
      - keystorePass:密鑰庫生成時的密碼 
      - truststoreFile:受信任密鑰庫,和密鑰庫相同即可 
      - truststorePass:受信任密鑰庫密碼
           

8.Android App編寫BKS讀取建立證書自定義的SSLSocketFactory

private final static String CLIENT_PRI_KEY = "client.bks";
private final static String TRUSTSTORE_PUB_KEY = "truststore.bks";
private final static String CLIENT_BKS_PASSWORD = "123456";
private final static String TRUSTSTORE_BKS_PASSWORD = "123456";
private final static String KEYSTORE_TYPE = "BKS";
private final static String PROTOCOL_TYPE = "TLS";
private final static String CERTIFICATE_FORMAT = "X509";

public static SSLSocketFactory getSSLCertifcation(Context context) {
  SSLSocketFactory sslSocketFactory = null;
  try {
    // 伺服器端需要驗證的用戶端證書,其實就是用戶端的keystore
    KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 用戶端信任的伺服器端證書
    KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);//讀驗證書
    InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
    InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);//加載證書
    keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
    trustStore.load(tsIn, TRUSTSTORE_BKS_PASSWORD.toCharArray());
    ksIn.close();
    tsIn.close();
    //初始化SSLContext
    SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_FORMAT);
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_FORMAT);
    trustManagerFactory.init(trustStore);
    keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
    sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); 

    sslSocketFactory = sslContext.getSocketFactory();

  } catch (KeyStoreException e) {...}//省略各種異常處理,請自行添加
  return sslSocketFactory;
}
           

9.Android App擷取SSLFactory執行個體進行網絡通路

private void fetchData() {
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
      .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//擷取SSLSocketFactory
      .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName驗證器
      .build();

  Retrofit retrofit = new Retrofit.Builder()
       .baseUrl("https://10.2.8.56:8443")//填寫自己伺服器IP
       .addConverterFactory(GsonConverterFactory.create())//添加 json 轉換器
       .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 擴充卡
       .client(okHttpClient)
       .build();

  IUser userIntf = retrofit.create(IUser.class);

  userIntf.getUser(user.getPhone())
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread()) 
        .subscribe(new Subscriber<UserBean>() {
                //省略onCompleted、onError、onNext
        }
  });
}
private class UnSafeHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String hostname, SSLSession session) {
      return true;//自行添加判斷邏輯,true->Safe,false->unsafe
  }
}
           

結束語

由于雙向認證涉及的原理知識太多,有些地方我也是一筆帶過,本文想着重介紹證書的制作以及應用。在此奉勸各位,如果不了解RSA非對稱加密,對稱加密以及HASH校驗算法 的同學,最好還是先看書學習一下。

了解原理對于進步來說是十分有幫助的,網上的資料魚龍混雜,不了解原理的話你根本無從分辨網上文章的正誤。

(不過我這篇文章絕對是正确的雙向認證,大家可以放心)

源碼位址:

GITHUB源碼下載下傳

找不到包,或者版本不相容的朋友可以參考一下demo。

作者:ChongmingLiu

連結:

https://www.jianshu.com/p/64172ccfb73b

繼續閱讀