Android : 關於HTTPS、TLS/SSL認證以及客戶端證書導入方法


一、HTTPS 簡介

  HTTPS 全稱 HTTP over TLS/SSL(TLS就是SSL的新版本3.1)。TLS/SSL是在傳輸層上層的協議,應用層的下層,作為一個安全層而存在,翻譯過來一般叫做傳輸層安全協議。對 HTTP 而言,安全傳輸層是透明不可見的,應用層僅僅當做使用普通的 Socket 一樣使用 SSLSocket 。TLS是基於 X.509 認證,他假定所有的數字證書都是由一個層次化的數字證書認證機構發出,即 CA。另外值得一提的是 TLS 是獨立於 HTTP 的,使用了RSA非對稱加密,對稱加密以及HASH算法,任何應用層的協議都可以基於 TLS 建立安全的傳輸通道,如 SSH 協議。

 

  代入場景:假設現在 A 要與遠端的 B 建立安全的連接進行通信。

  1. 直接使用對稱加密通信,那么密鑰無法安全的送給 B 。
  2. 直接使用非對稱加密,B 使用 A 的公鑰加密,A 使用私鑰解密。但是因為B無法確保拿到的公鑰就是A的公鑰,因此也不能防止中間人攻擊。

     為了解決上述問題,引入了一個第三方,也就是上面所說的 CA(Certificate Authority):  

    CA 用自己的私鑰簽發數字證書,數字證書中包含A的公鑰。然后 B 可以用 CA 的根證書中的公鑰來解密 CA 簽發的證書,從而拿到A的公鑰。那么又引入了一個問題,如何保證 CA 的公鑰是合法的呢?答案就是現代主流的瀏覽器會內置 CA 的證書。

  中間證書:

    現在大多數CA不直接簽署服務器證書,而是簽署中間CA,然后用中間CA來簽署服務器證書。這樣根證書可以離線存儲來確保安全,即使中間證書出了問題,可以用根證書重新簽署中間證書。另一個原因是為了支持一些很古老的瀏覽器,有些根證書本身,也會被另外一個很古老的根證書簽名,這樣根據瀏覽器的版本,可能會看到三層或者是四層的證書鏈結構,如果能看到四層的證書鏈結構,則說明瀏覽器的版本很老,只能通過最早的根證書來識別

  校驗過程

    那么實際上,在 HTTPS 握手開始后,服務器會把整個證書鏈發送到客戶端,給客戶端做校驗。校驗的過程是要找到這樣一條證書鏈,鏈中每個相鄰節點,上級的公鑰可以校驗通過下級的證書,鏈的根節點是設備信任的錨點或者根節點可以被錨點校驗。那么錨點對於瀏覽器而言就是內置的根證書啦(注:根節點並不一定是根證書)。校驗通過后,視情況校驗客戶端,以及確定加密套件和用非對稱密鑰來交換對稱密鑰。從而建立了一條安全的信道。

 

二、HTTPS API :SSLSocketFactory SSLSocket

  Android 使用的是 Java 的 API。那么 HTTPS 使用的 Socket 必然都是通過SSLSocketFactory 創建的 SSLSocket,當然自己實現了 TLS 協議除外。

一個典型的使用 HTTPS 方式如下: (ps:網絡連接方式有HttpClient(5.0開始廢棄)、HttpURLConnection、OKHttp 和 Volley)

URL url = new URL("https://google.com");
HttpsURLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();

此時使用的是默認的SSLSocketFactory(沒有加載自己的證書),與下段代碼使用的SSLContext是一致的:

private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
  try {
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, null, null);
    return defaultSslSocketFactory = sslContext.getSocketFactory();
  } catch (GeneralSecurityException e) {
    throw new AssertionError(); // The system has no TLS. Just give up.
  }
}

默認的 SSLSocketFactory 校驗服務器的證書時,會信任設備內置的100多個根證書。

 

三、SSL的配置

 自定義信任策略

  如果不加載自己的證書,系統會為你配置好一個安全的 SSL,但系統默認的 SSL認為一切 CA 都是可信的,可往往 CA 有時候也不可信,比如某家 CA 被黑客入侵什么的事屢見不鮮。雖然 Android 系統自身可以更新信任的 CA 列表,以防止一些 CA 的失效,如果為了更高的安全性,可以希望指定信任的錨點,類似采用如下的代碼:

// 取到證書的輸入流
InputStream caInput = context.getResources().openRawResource(R.raw.ca_cert);
Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput);

// 創建 Keystore 包含我們的證書
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// 創建一個 TrustManager 僅把 Keystore 中的證書 作為信任的錨點
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);

// 用 TrustManager 初始化一個 SSLContext
ssl_ctx = SSLContext.getInstance("TLS");  //定義:public static SSLContext ssl_ctx = null;
ssl_ctx.init(null, tmf.getTrustManagers(), new SecureRandom());

然后可以通過SSLSocketFactory 與服務器進行交互:

// SSLSocketFactory 或 SSLSocket 都行
//1.創建監聽指定服務器地址以及指定服務器監聽的端口號
SSLSocketFactory socketFactory = (SSLSocketFactory)ssl_ctx.getSocketFactory();
ssl_socket = (SSLSocket) socketFactory.createSocket(serverUrl, Integer.parseInt(serverPort)); //定義:private final String serverUrl = "42.98.106.44";
                                                       
//   private final String serverPort = "8086"; //2.拿到客戶端的socket對象的輸出/輸入流,通過read/write方法和服務器交互數據 ssl_input = new BufferedInputStream(ssl_socket.getInputStream()); ssl_output = new BufferedOutputStream(ssl_socket.getOutputStream());

  以上做法只有我們的 ca_cert.crt 才會作為信任的錨點,只有 ca_cert.crt 以及他簽發的證書才會被信任。

  說起來有個很有趣的玩法,考慮到證書會過期、升級,我們既不想只信任我們服務器的證書,又不想信任 Android 所有的 CA 證書。有個不錯的的信任方式是把簽發我們服務器的證書的根證書導出打包到 APK 中,然后用上述的方式做信任處理。仔細思考一下,這未嘗不是一種好的方式。只要日后換證書還用這家 CA 簽發,既不用擔心失效,安全性又有了一定的提高。因為比起信任100多個根證書,只信任一個風險會小很多。正如最開始所說,信任錨點未必需要根證書。因此同樣上面的代碼也可以用於自簽名證書的信任,相信看官們能舉一反三,就不再多述。

  證書固定

  上文自定義信任錨點的時候說了一個很有意思的方式,只信任一個根CA,其實更加一般化和靈活的做法就是用證書固定。

  其實 HTTPS 是支持證書固定技術的(CertificatePinning),通俗的說就是對證書公鑰做校驗,看是不是符合期望。HttpsUrlConnection 並沒有對外暴露相關的API,而在 Android 大放光彩的 OkHttp 是支持證書固定的,雖然在 Android 中,OkHttp 默認的 SSL 的實現也是調用了 Conscrypt,但是重新用 TrustManager 對下發的證書構建了證書鏈,並允許用戶做證書固定。具體 API 的用法可見 CertificatePinner 這個類,這里不再贅述。

  域名校驗

  Android 內置的 SSL 的實現是引入了Conscrypt 項目,而 HTTP(S)層則是使用的OkHttp。而 SSL 層只負責校驗證書的真假,對於所有基於SSL 的應用層協議,需要自己來校驗證書實體的身份,因此 Android 默認的域名校驗則由 OkHostnameVerifier 實現的,從 HttpsUrlConnection 的代碼可見一斑:

static {
    try {
        defaultHostnameVerifier = (HostnameVerifier)
                Class.forName("com.android.okhttp.internal.tls.OkHostnameVerifier")
                .getField("INSTANCE").get(null);
    } catch (Exception e) {
        throw new AssertionError("Failed to obtain okhttp HostnameVerifier", e);
    }
}

如果校驗規則比較特殊,可以傳入自定義的校驗規則給 HttpsUrlConnection。同樣,如果要基於 SSL 實現其他的應用層協議,千萬別忘了做域名校驗以證明證書的身份。

 

四、關於證書

 1.證書概念:證書是對現實生活中 某個人或者某件物品的價值體現 比如古董頒發見證書 ,人頒發獻血證等 通常證書會包含以下內容:

          證書擁有者名稱(CN),組織單位(OU)組織(O),城市(L) 區(ST) 國家/地區( C )

               證書的過期時間 證書的頒發機構 證書頒發機構對證書的簽名,簽名算法,對象的公鑰等

               數字證書的格式遵循X.509標准。X.509是由國際電信聯盟(ITU-T)制定的數字證書標准。

  

 2. 證書類型:

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中即可。
 
導入BKS使用代碼示例:(上面“ SSL的配置”部分已展示過導入證書的方式)
KeyStore keyStore = KeyStore.getInstance("BKS"); // 訪問keytool創建的Java密鑰庫
InputStream keyStream = context.getResources().openRawResource(R.raw.alitrust);

char keyStorePass[]="123456".toCharArray();  //證書密碼
keyStore.load(keyStream,keyStorePass);

TrustManagerFactory trustManagerFactory =   TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);//保存服務端的授權證書

ssl_ctx = SSLContext.getInstance("SSL");
ssl_ctx.init(null, trustManagerFactory.getTrustManagers(), null);

 

 3.制作證書:

  方式一:利用keytool生成證書

  ①.生成客戶端keystore:

keytool -genkeypair -alias client -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore client.jks

  ②.生成服務端keystore:

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

  ③.導出客戶端證書:

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

  ④.導出服務端證書:

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

  ⑤.證書交換:

將客戶端證書導入服務端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

  ⑥.生成Android識別的BKS庫文件:

//將client.jks和truststore.jks分別轉換成client.bks和truststore.bks,然后放到android客戶端的assert目錄下,
//然后再通過 Context.getAssets().open("xxx.bks") 獲得文件輸入流;
keytool -importcert -trustcacerts -keystore key.bks -file client.jks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
keytool -importcert -trustcacerts -keystore key.bks -file truststore.jks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

  ⑦.配置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:受信任密鑰庫密碼

  ⑧.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;
}

  ⑨Android App通過OkHttpClient進行網絡訪問:

//自定義方法,獲取OkHttpClient實例:
public
static OkHttpClient getOkHttpClient(SSLSocketFactory sslSocketFactory) {   OkHttpClient.Builder builder = new OkHttpClient.Builder();   builder.connectTimeout(15L, TimeUnit.SECONDS);   builder.sslSocketFactory(sslSocketFactory ); //添加sslSocketFactory   builder.hostnameVerifier(new HostnameVerifier() {    @Override    public boolean verify(String hostname, SSLSession session) {    return true; //自定義判斷邏輯:true-安全,false-不安全   }   });   return builder.build(); } ......
//activity端傳入之前創建的 sslSocketFactory 拿到 OkHttpClient 實例后便可進行post和get請求: OkHttpClient okHttpClient = getOkHttpClient(sslSocketFactory);
// 發送格式定義 MediaType JSON
= MediaType.parse("application/json; charset=utf-8"); MediaType STRING = MediaType.parse("text/x-markdown; charset=utf-8"); // post請求(以json格式發送)===================================== JSONObject jsonObject = new JSONObject(); jsonObject.put("Model", "KK309"); jsonObject.put("Vid", "0x1234"); jsonObject.put("Pid", "0x5678"); jsonObject.put("Version", 99); String requestBody = jsonObject.toString(1); final Request postReq = new Request.Builder() .url(url) //填入自己服務器的URL地址 .post(RequestBody.create(JSON, requestBody)) .build(); Call postCall = okHttpClient.newCall(postReq); postCall.enqueue(new Callback() { //發送post請求 @Override public void onFailure(Call call, IOException e) { Log.d("SSL", "Post ---> onFailure: "+ e); } @Override public void onResponse(Call call, Response response) throws IOException { Log.d("SSL", "Post ---> onResponse: " + response.body().string()); } }); // get請求=================================================== final Request getReq = new Request.Builder() .url(url) //填入自己服務器的URL地址 .get() //默認就是GET請求,可以不寫 .build(); Call getCall = okHttpClient.newCall(getReq); getCall.enqueue(new Callback() { //發送get請求 @Override public void onFailure(Call call, IOException e) { Log.d("SSL", "Get ---> onFailure: "+ e); } @Override public void onResponse(Call call, Response response) throws IOException { Log.d("SSL", "Get ---> onResponse: " + response.body().string()); } });

 

  方式二:利用openssl生成證書(keytool沒辦法簽發證書,而openssl能夠進行簽發和證書鏈的管理

    ①創建CA私鑰,創建目錄ca

      openssl genrsa -des3 -out ca/ca-key.pem 1024              //-des:表示生成的key是有密碼保護的
       (注:如果是將生成的key與server的證書一起使用,最好不需要密碼,就是不要這個參數,不然客戶端每次使用都需要輸入密碼)
      openssl rsa -in ca-key.pem -out ca-key.notneedpassword.pem  //也可以用此命令讓其不需要輸密碼

 

    ②創建證書請求

      openssl req -new -out ca/ca-req.csr -key ca/ca-key.pem  

 以下為終端輸出信息:

Enter pass phrase for ca/ca-key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:ZheJiang
Locality Name (eg, city) []:hz
Organization Name (eg, company) [Internet Widgits Pty Ltd]:happylife
Organizational Unit Name (eg, section) []:test
Common Name (e.g. server FQDN or YOUR name) []:test1
Email Address []:test2

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123456
An optional company name []:nanosic

 

    ③自簽署證書

       openssl x509 -req -in ca/ca-req.csr -out ca/ca-cert.pem -signkey ca/ca-key.pem -days 3650

 

    ④導出ca證書

     ------>生成瀏覽器支持的.p12格式

      openssl pkcs12 -export -clcerts -in ca/ca-cert.pem -inkey ca/ca-key.pem -out ca/ca.p12

      只導出ca證書,不導出ca的秘鑰:

      openssl pkcs12 -export -nokeys -cacerts -in ca/ca-cert.pem -inkey ca/ca-key.pem -out ca/ca1.p12

 

     ------>轉成Android支持的.BKS格式

      keytool -importcert -trustcacerts -keystore key.bks -file ca-cert.pem -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

 

 補充:關於使用keytool生成bks格式證書

    JKS和JCEKS是Java密鑰庫(KeyStore)的兩種比較常見類型,JKS的Provider是SUN,在每個版本的JDK中都有;
    BKS來自BouncyCastleProvider,它使用的也是TripleDES來保護密鑰庫中的Key,它能夠防止證書庫被不小心修改(Keystore的keyentry改掉1個bit都會產生錯誤),BKS能夠跟JKS互操作;
    而jdk的keytool只能生成jks的證書庫,如果生成bks的則需要下載BouncyCastle庫,參考如下配置環境:
    ①. 到官網 https://www.bouncycastle.org/latest_releases.html 下載.jar工具包:

    ②.放到本機JDK的安裝目錄\jre\lib\ext 下面,然后便可通過前面的方法使用keytool生成BSK證書。



-end-

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM