聊聊 Android HTTPS 的使用姿勢(shì)

HTTPS 簡(jiǎn)介

HTTPS 全稱 HTTP over TLS油猫。TLS是在傳輸層上層的協(xié)議团南,應(yīng)用層的下層,作為一個(gè)安全層而存在昂芜,翻譯過(guò)來(lái)一般叫做傳輸層安全協(xié)議。

對(duì) HTTP 而言赔蒲,安全傳輸層是透明不可見(jiàn)的泌神,應(yīng)用層僅僅當(dāng)做使用普通的 Socket 一樣使用 SSLSocket 。

TLS是基于 X.509 認(rèn)證舞虱,他假定所有的數(shù)字證書都是由一個(gè)層次化的數(shù)字證書認(rèn)證機(jī)構(gòu)發(fā)出欢际,即 CA。另外值得一提的是 TLS 是獨(dú)立于 HTTP 的矾兜,任何應(yīng)用層的協(xié)議都可以基于 TLS 建立安全的傳輸通道损趋,如 SSH 協(xié)議。

代入場(chǎng)景

假設(shè)現(xiàn)在 A 要與遠(yuǎn)端的 B 建立安全的連接進(jìn)行通信椅寺。

  1. 直接使用對(duì)稱加密通信浑槽,那么密鑰無(wú)法安全的送給 B 。
  2. 直接使用非對(duì)稱加密返帕,B 使用 A 的公鑰加密桐玻,A 使用私鑰解密。但是因?yàn)锽無(wú)法確保拿到的公鑰就是A的公鑰荆萤,因此也不能防止中間人攻擊镊靴。

CA

為了解決上述問(wèn)題,引入了一個(gè)第三方,也就是上面所說(shuō)的 CA(Certificate Authority)偏竟。
CA 用自己的私鑰簽發(fā)數(shù)字證書煮落,數(shù)字證書中包含A的公鑰。然后 B 可以用 CA 的根證書中的公鑰來(lái)解密 CA 簽發(fā)的證書苫耸,從而拿到合法的公鑰州邢。那么又引入了一個(gè)問(wèn)題,如何保證 CA 的公鑰是合法的呢褪子。答案就是現(xiàn)代主流的瀏覽器會(huì)內(nèi)置 CA 的證書量淌。

中間證書

當(dāng)然,現(xiàn)在大多數(shù)CA不直接簽署服務(wù)器證書嫌褪,而是簽署中間CA呀枢,然后用中間CA來(lái)簽署服務(wù)器證書。這樣根證書可以離線存儲(chǔ)來(lái)確保安全笼痛,即使中間證書出了問(wèn)題裙秋,可以用根證書重新簽署中間證書。

校驗(yàn)過(guò)程

那么實(shí)際上缨伊,在 HTTPS 握手開始后摘刑,服務(wù)器會(huì)把整個(gè)證書鏈發(fā)送到客戶端,給客戶端做校驗(yàn)刻坊。校驗(yàn)的過(guò)程是要找到這樣一條證書鏈枷恕,鏈中每個(gè)相鄰節(jié)點(diǎn),上級(jí)的公鑰可以校驗(yàn)通過(guò)下級(jí)的證書谭胚,鏈的根節(jié)點(diǎn)是設(shè)備信任的錨點(diǎn)或者根節(jié)點(diǎn)可以被錨點(diǎn)校驗(yàn)徐块。那么錨點(diǎn)對(duì)于瀏覽器而言就是內(nèi)置的根證書啦。請(qǐng)注意上文的說(shuō)辭灾而,根節(jié)點(diǎn)并不一定是根證書胡控,下面會(huì)有說(shuō)明。

校驗(yàn)通過(guò)后旁趟,視情況校驗(yàn)客戶端昼激,以及確定加密套件和用非對(duì)稱密鑰來(lái)交換對(duì)稱密鑰。從而建立了一條安全的信道轻庆。

HTTPS API

SSLSocketFactory

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

一個(gè)典型的使用 HTTPS 方式如下:

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

此時(shí)使用的是默認(rèn)的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.
  }
}

默認(rèn)的 SSLSocketFactory 校驗(yàn)服務(wù)器的證書時(shí),會(huì)信任設(shè)備內(nèi)置的100多個(gè)根證書蛾方。

TrustManager

上文說(shuō)了像捶,SSL 握手開始后上陕,會(huì)校驗(yàn)服務(wù)器的證書,那么其實(shí)就是通過(guò) X509ExtendedTrustManager 做校驗(yàn)的拓春,更一般性的說(shuō)是 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();
}

那么最后校驗(yàn)服務(wù)器證書的過(guò)程會(huì)落到 checkServerTrusted 這個(gè)函數(shù)释簿,如果校驗(yàn)沒(méi)通過(guò)會(huì)拋出 CertificateException 。筆者不得不得吐槽一下硼莽,很多博客說(shuō)庶溶,配置 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();
    }
}

好的,如果你這么用的話懂鸵,隨便什么證書你都會(huì)信任偏螺,網(wǎng)絡(luò)毫無(wú)安全可言,可以隨意的被中間人攻擊匆光,所以千萬(wàn)不要這樣做套像。

SSL的配置

自定義信任策略

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

但是如果用系統(tǒng)默認(rèn)的 SSL,那么就是假設(shè)一切 CA 都是可信的周崭×可往往 CA 有時(shí)候也不可信,比如某家 CA 被黑客入侵什么的事屢見(jiàn)不鮮续镇。雖然 Android 系統(tǒng)自身可以更新信任的 CA 列表征绎,以防止一些 CA 的失效。那么為了更高的安全性磨取,我們希望指定信任的錨點(diǎn),可以類似采用如下的代碼:

// 取到證書的輸入流
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)建一個(gè) TrustManager 僅把 Keystore 中的證書 作為信任的錨點(diǎn)
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

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

那么只有我們的 anchor.crt 才會(huì)作為信任的錨點(diǎn)柴墩,只有 anchor.crt 以及他簽發(fā)的證書才會(huì)被信任忙厌。

說(shuō)起來(lái)有個(gè)很有趣的玩法,考慮到證書會(huì)過(guò)期江咳、升級(jí)逢净,我們既不想只信任我們服務(wù)器的證書,又不想信任 Android 所有的 CA 證書歼指。有個(gè)不錯(cuò)的的信任方式是把簽發(fā)我們服務(wù)器的證書的根證書導(dǎo)出打包到 APK 中爹土,然后用上述的方式做信任處理。

仔細(xì)思考一下踩身,這未嘗不是一種好的方式胀茵。只要日后換證書還用這家 CA 簽發(fā),既不用擔(dān)心失效挟阻,安全性又有了一定的提高琼娘。因?yàn)楸绕鹦湃?00多個(gè)根證書峭弟,只信任一個(gè)風(fēng)險(xiǎn)會(huì)小很多。

正如最開始所說(shuō)脱拼,信任錨點(diǎn)未必需要根證書瞒瘸。因此同樣上面的代碼也可以用于自簽名證書的信任,相信看官們能舉一反三熄浓,就不再多述情臭。

注意點(diǎn)

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

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

如果服務(wù)器下發(fā)證書的時(shí)候沒(méi)有發(fā)送一條證書鏈惯雳,而是只發(fā)了自己的證書朝巫,那么信任鏈就因?yàn)槿币画h(huán)而導(dǎo)致校驗(yàn)會(huì)失敗。

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

域名校驗(yàn)

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

而 SSL 層只負(fù)責(zé)校驗(yàn)證書的真假潮孽,對(duì)于所有基于SSL 的應(yīng)用層協(xié)議揪荣,需要自己來(lái)校驗(yàn)證書實(shí)體的身份,因此 Android 默認(rèn)的域名校驗(yàn)則由 OkHostnameVerifier 實(shí)現(xiàn)的往史,從 HttpsUrlConnection 的代碼可見(jiàn)一斑:

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);
    }
}

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

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

證書固定

上文自定義信任錨點(diǎn)的時(shí)候說(shuō)了一個(gè)很有意思的方式订歪,只信任一個(gè)根CA脖祈,其實(shí)更加一般化和靈活的做法就是用證書固定。

其實(shí) HTTPS 是支持證書固定技術(shù)的(CertificatePinning)刷晋,通俗的說(shuō)就是對(duì)證書公鑰做校驗(yàn)盖高,看是不是符合期望。

HttpsUrlConnection 并沒(méi)有對(duì)外暴露相關(guān)的API眼虱,而在 Android 大放光彩的 OkHttp 是支持證書固定的喻奥,雖然在 Android 中,OkHttp 默認(rèn)的 SSL 的實(shí)現(xiàn)也是調(diào)用了 Conscrypt捏悬,但是重新用 TrustManager 對(duì)下發(fā)的證書構(gòu)建了證書鏈撞蚕,并允許用戶做證書固定。具體 API 的用法可見(jiàn) CertificatePinner 這個(gè)類过牙,這里不再贅述诈豌。

小結(jié)

安全無(wú)小事仆救,尤其是網(wǎng)絡(luò)通信方面。希望本文能給諸位讀者一些小小的啟發(fā)矫渔。最后斷更了那么久彤蔽,實(shí)在是抱歉。堅(jiān)持寫博客確實(shí)不易庙洼,新的一年筆者會(huì)努力的顿痪。

感謝大家的支持!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末油够,一起剝皮案震驚了整個(gè)濱河市蚁袭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌石咬,老刑警劉巖揩悄,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鬼悠,居然都是意外死亡删性,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門焕窝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蹬挺,“玉大人,你說(shuō)我怎么就攤上這事它掂“桶铮” “怎么了?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵虐秋,是天一觀的道長(zhǎng)榕茧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)客给,這世上最難降的妖魔是什么雪猪? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮起愈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘译仗。我一直安慰自己抬虽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布纵菌。 她就那樣靜靜地躺著阐污,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咱圆。 梳的紋絲不亂的頭發(fā)上笛辟,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天功氨,我揣著相機(jī)與錄音,去河邊找鬼手幢。 笑死捷凄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的围来。 我是一名探鬼主播跺涤,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼监透!你這毒婦竟也來(lái)了桶错?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤胀蛮,失蹤者是張志新(化名)和其女友劉穎院刁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粪狼,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡退腥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸳玩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阅虫。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖不跟,靈堂內(nèi)的尸體忽然破棺而出颓帝,到底是詐尸還是另有隱情,我是刑警寧澤窝革,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布购城,位于F島的核電站,受9級(jí)特大地震影響虐译,放射性物質(zhì)發(fā)生泄漏瘪板。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一漆诽、第九天 我趴在偏房一處隱蔽的房頂上張望侮攀。 院中可真熱鬧,春花似錦厢拭、人聲如沸兰英。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)畦贸。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間薄坏,已是汗流浹背趋厉。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胶坠,地道東北人君账。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像涵但,于是被迫代替她去往敵國(guó)和親杈绸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容