聊聊 Android HTTPS 的使用姿勢

HTTPS 簡介

HTTPS 全稱 HTTP over TLS。TLS是在傳輸層上層的協(xié)議,應用層的下層,作為一個安全層而存在,翻譯過來一般叫做傳輸層安全協(xié)議。

對 HTTP 而言,安全傳輸層是透明不可見的,應用層僅僅當做使用普通的 Socket 一樣使用 SSLSocket 。

TLS是基于 X.509 認證,他假定所有的數(shù)字證書都是由一個層次化的數(shù)字證書認證機構發(fā)出,即 CA。另外值得一提的是 TLS 是獨立于 HTTP 的,任何應用層的協(xié)議都可以基于 TLS 建立安全的傳輸通道,如 SSH 協(xié)議。

代入場景

假設現(xiàn)在 A 要與遠端的 B 建立安全的連接進行通信。

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

CA

為了解決上述問題,引入了一個第三方,也就是上面所說的 CA(Certificate Authority)。
CA 用自己的私鑰簽發(fā)數(shù)字證書,數(shù)字證書中包含A的公鑰。然后 B 可以用 CA 的根證書中的公鑰來解密 CA 簽發(fā)的證書,從而拿到合法的公鑰。那么又引入了一個問題,如何保證 CA 的公鑰是合法的呢。答案就是現(xiàn)代主流的瀏覽器會內置 CA 的證書。

中間證書

當然,現(xiàn)在大多數(shù)CA不直接簽署服務器證書,而是簽署中間CA,然后用中間CA來簽署服務器證書。這樣根證書可以離線存儲來確保安全,即使中間證書出了問題,可以用根證書重新簽署中間證書。

校驗過程

那么實際上,在 HTTPS 握手開始后,服務器會把整個證書鏈發(fā)送到客戶端,給客戶端做校驗。校驗的過程是要找到這樣一條證書鏈,鏈中每個相鄰節(jié)點,上級的公鑰可以校驗通過下級的證書,鏈的根節(jié)點是設備信任的錨點或者根節(jié)點可以被錨點校驗。那么錨點對于瀏覽器而言就是內置的根證書啦。請注意上文的說辭,根節(jié)點并不一定是根證書,下面會有說明。

校驗通過后,視情況校驗客戶端,以及確定加密套件和用非對稱密鑰來交換對稱密鑰。從而建立了一條安全的信道。

HTTPS API

SSLSocketFactory

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

一個典型的使用 HTTPS 方式如下:

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

TrustManager

上文說了,SSL 握手開始后,會校驗服務器的證書,那么其實就是通過 X509ExtendedTrustManager 做校驗的,更一般性的說是 X509TrustManager :

/**
 * The trust manager for X509 certificates to be used to perform authentication
 * for secure sockets.
 */
public interface X509TrustManager extends TrustManager {

    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException;

    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException;

    public X509Certificate[] getAcceptedIssuers();
}

那么最后校驗服務器證書的過程會落到 checkServerTrusted 這個函數(shù),如果校驗沒通過會拋出 CertificateException 。筆者不得不得吐槽一下,很多博客說,配置 SSL 差不多是這樣的:

private static synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
    try {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[]{
                new X509TrustManager() {
                    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

                    }

                    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    }

                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        }, null);
        return sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
        throw new AssertionError();
    }
}

好的,如果你這么用的話,隨便什么證書你都會信任,網(wǎng)絡毫無安全可言,可以隨意的被中間人攻擊,所以千萬不要這樣做。

SSL的配置

自定義信任策略

如果不清楚怎么配置 SSL ,最好的辦法就是不配置他,系統(tǒng)會為你配置好一個安全的 SSL 。

但是如果用系統(tǒng)默認的 SSL,那么就是假設一切 CA 都是可信的??赏?CA 有時候也不可信,比如某家 CA 被黑客入侵什么的事屢見不鮮。雖然 Android 系統(tǒng)自身可以更新信任的 CA 列表,以防止一些 CA 的失效。那么為了更高的安全性,我們希望指定信任的錨點,可以類似采用如下的代碼:

// 取到證書的輸入流
InputStream is = new FileInputStream("anchor.crt");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca = cf.generateCertificate(is);

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

// 創(chuàng)建一個 TrustManager 僅把 Keystore 中的證書 作為信任的錨點
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

// 用 TrustManager 初始化一個 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
return sslContext.getSocketFactory();

那么只有我們的 anchor.crt 才會作為信任的錨點,只有 anchor.crt 以及他簽發(fā)的證書才會被信任。

說起來有個很有趣的玩法,考慮到證書會過期、升級,我們既不想只信任我們服務器的證書,又不想信任 Android 所有的 CA 證書。有個不錯的的信任方式是把簽發(fā)我們服務器的證書的根證書導出打包到 APK 中,然后用上述的方式做信任處理。

仔細思考一下,這未嘗不是一種好的方式。只要日后換證書還用這家 CA 簽發(fā),既不用擔心失效,安全性又有了一定的提高。因為比起信任100多個根證書,只信任一個風險會小很多。

正如最開始所說,信任錨點未必需要根證書。因此同樣上面的代碼也可以用于自簽名證書的信任,相信看官們能舉一反三,就不再多述。

注意點

服務器下發(fā)證書不全

上文提到現(xiàn)在大多數(shù)的場景是根證書離線存儲,使用二級證書簽發(fā)服務器證書。而系統(tǒng)默認是只信任根證書的,因此就產(chǎn)生了一個小小的信任的縫隙。

如果服務器下發(fā)證書的時候沒有發(fā)送一條證書鏈,而是只發(fā)了自己的證書,那么信任鏈就因為缺一環(huán)而導致校驗會失敗。

一般發(fā)現(xiàn)這種情況筆者只建議去聯(lián)系運維的同學去配置服務器而不會在應用端做任何更改。

域名校驗

Android 內置的 SSL 的實現(xiàn)是引入了Conscrypt 項目,而 HTTP(S)層則是使用的2.x的 OkHttp。

而 SSL 層只負責校驗證書的真假,對于所有基于SSL 的應用層協(xié)議,需要自己來校驗證書實體的身份,因此 Android 默認的域名校驗則由 OkHostnameVerifier 實現(xiàn)的,從 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);
    }
}

如果校驗規(guī)則比較特殊,可以傳入自定義的校驗規(guī)則給 HttpsUrlConnection。

同樣,如果要基于 SSL 實現(xiàn)其他的應用層協(xié)議,千萬別忘了做域名校驗以證明證書的身份。

證書固定

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

其實 HTTPS 是支持證書固定技術的(CertificatePinning),通俗的說就是對證書公鑰做校驗,看是不是符合期望。

HttpsUrlConnection 并沒有對外暴露相關的API,而在 Android 大放光彩的 OkHttp 是支持證書固定的,雖然在 Android 中,OkHttp 默認的 SSL 的實現(xiàn)也是調用了 Conscrypt,但是重新用 TrustManager 對下發(fā)的證書構建了證書鏈,并允許用戶做證書固定。具體 API 的用法可見 CertificatePinner 這個類,這里不再贅述。

小結

安全無小事,尤其是網(wǎng)絡通信方面。希望本文能給諸位讀者一些小小的啟發(fā)。最后斷更了那么久,實在是抱歉。堅持寫博客確實不易,新的一年筆者會努力的。

感謝大家的支持!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容