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

1、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 協議。html

 

  代入場景:假設如今 A 要與遠端的 B 創建安全的鏈接進行通訊。

  1. 直接使用對稱加密通訊,那麼密鑰沒法安全的送給 B 。
  2. 直接使用非對稱加密,B 使用 A 的公鑰加密,A 使用私鑰解密。可是由於B沒法確保拿到的公鑰就是A的公鑰,所以也不能防止中間人攻擊。

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

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

  中間證書:

    如今大多數CA不直接簽署服務器證書,而是簽署中間CA,而後用中間CA來簽署服務器證書。這樣根證書能夠離線存儲來確保安全,即便中間證書出了問題,能夠用根證書從新簽署中間證書。另外一個緣由是爲了支持一些很古老的瀏覽器,有些根證書自己,也會被另一個很古老的根證書籤名,這樣根據瀏覽器的版本,可能會看到三層或者是四層的證書鏈結構,若是能看到四層的證書鏈結構,則說明瀏覽器的版本很老,只能經過最先的根證書來識別算法

  校驗過程

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

 

2、HTTPS API :SSLSocketFactory SSLSocket

  Android 使用的是 Java 的 API。那麼 HTTPS 使用的 Socket 必然都是經過SSLSocketFactory 建立的 SSLSocket,固然本身實現了 TLS 協議除外。json

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

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多個根證書。安全

 

3、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 實現其餘的應用層協議,千萬別忘了作域名校驗以證實證書的身份。

 

4、關於證書

 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-
相關文章
相關標籤/搜索