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)行通信椅寺。
- 直接使用對(duì)稱加密通信浑槽,那么密鑰無(wú)法安全的送給 B 。
- 直接使用非對(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ì)努力的顿痪。
感謝大家的支持!