很多文章對(duì)客戶端https的使用都是很模糊的,不但如此,有些開(kāi)發(fā)者直接從網(wǎng)上拷貝一些使用https的“漏洞”代碼,無(wú)形之中讓客戶端處在一種高風(fēng)險(xiǎn)的情況下。
今天我們就對(duì)有關(guān)https使用的問(wèn)題進(jìn)行深入的探討,希望能解決以往的困惑。對(duì)于https,需要了解其工作原理的可以參考https是如何工作的?,更多關(guān)于https的問(wèn)題我會(huì)站在客戶端的角度在后面陸陸續(xù)續(xù)的寫(xiě)出來(lái)。
首先來(lái)說(shuō)說(shuō)什么是證書(shū)鎖定。
證書(shū)鎖定是用來(lái)限制哪些證書(shū)和證書(shū)頒發(fā)機(jī)構(gòu)是可信任的。需要我們直接在代碼中固定寫(xiě)死使用某個(gè)服務(wù)器的證書(shū),然后用自定義的信任存儲(chǔ)去代替系統(tǒng)系統(tǒng)自帶的,再去連接我們的服務(wù)器,我們將這種做法稱之為證書(shū)鎖定。換言之,證書(shū)鎖定就是在代碼中驗(yàn)證當(dāng)前服務(wù)器是否持有某張指定的證書(shū),如果不是則強(qiáng)行斷開(kāi)鏈接。
有同學(xué)問(wèn)證書(shū)鎖定有什么好處么?最大的好處使用證書(shū)鎖定提高安全性,降低了成本。為什么這么說(shuō)呢?如果你想破解該通信,需要首先拿到客戶端,然后對(duì)其反編譯,修改后再重新打包簽名,相比原先的做法,這無(wú)疑是增加了破解難度。除了之外,由于證書(shū)鎖定可以使用自簽名的證書(shū),那就意味著我們不需要再向Android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)購(gòu)買證書(shū)了,這樣就可以剩下每年1000多塊錢(qián)的證書(shū)費(fèi)用,能省一點(diǎn)就省一點(diǎn)嘛。
現(xiàn)在,我們來(lái)看看如何在retrofit中進(jìn)行證書(shū)鎖定。
OkHttpClient client =newOkHttpClient.Builder()
.certificatePinner(newCertificatePinner.Builder()
.add("sbbic.com","sha1/C8xoaOSEzPC6BgGmxAt/EAcsajw=")
.add("closedevice.com","sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
.build())
通過(guò)上面的代碼不難看出,retrofit中的證書(shū)鎖定同樣是借助OkHttpClient實(shí)現(xiàn)的:通過(guò)為OkHttpClient添加CertificatePinner即可。CertificatePinner對(duì)象以構(gòu)建器的方式創(chuàng)建,可以通過(guò)其add()方法來(lái)鎖定多個(gè)證書(shū)。
現(xiàn)在我們深入下證書(shū)鎖定的原理。我們知道,無(wú)論http還是https,都是服務(wù)端被動(dòng),客戶端主動(dòng)。那么問(wèn)題來(lái)了,客戶端第一次發(fā)出請(qǐng)求之后,無(wú)法確定服務(wù)端是不是合法的。那么很可能就會(huì)出現(xiàn)以下情景:
正常情況下是這樣,我們想要根據(jù)文章aid查看某篇文章內(nèi)容,其流程如下:
此時(shí),如果黑客惡意攔截這個(gè)通信過(guò)程,會(huì)是怎么樣?
此時(shí)惡意服務(wù)端完全可以發(fā)起雙向攻擊:對(duì)上可以欺騙服務(wù)端,對(duì)下可以欺騙客戶端,更嚴(yán)重的是客戶端段和服務(wù)端完全感知不到已經(jīng)被攻擊了。這就是所謂的中間人攻擊。
中間人攻擊(MITM攻擊)是指,黑客攔截并篡改網(wǎng)絡(luò)中的通信數(shù)據(jù)。又分為被動(dòng)MITM和主動(dòng)MITM,被動(dòng)MITM只竊取通信數(shù)據(jù)而不修改,而主動(dòng)MITM不當(dāng)能竊取數(shù)據(jù),還會(huì)篡改通信數(shù)據(jù)。最常見(jiàn)的中間人攻擊常常發(fā)生了公共wifi或者公共路由上,有興趣的私下可以問(wèn)我,這里不做演示了。
現(xiàn)在可以看看證書(shū)鎖定是怎么樣提高安全性,避免中間人攻擊的,用一張簡(jiǎn)單的流程圖來(lái)說(shuō)明:
不難看出,通過(guò)證書(shū)鎖定能有有效的避免中間人攻擊。
證書(shū)鎖定盡管帶了較高的安全性,但是這種安全性的提高卻犧牲了靈活性。一旦當(dāng)證書(shū)發(fā)生變化時(shí),我們的客戶端也必須隨之升級(jí),除此之外,我們的服務(wù)端不得不為了兼容以前的客戶端而做出一些妥協(xié)或者說(shuō)直接停用以前的客戶端,這對(duì)開(kāi)發(fā)者和用戶來(lái)說(shuō)并不是那么的友好。
但實(shí)際上,極少情況下我們才會(huì)變動(dòng)證書(shū)。因此,如果產(chǎn)品安全性要求比較高還是啟動(dòng)證書(shū)鎖定吧。
使用android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)頒發(fā)的證書(shū)
有些同學(xué)可能好奇自己公司中使用https,但是在客戶端代碼中并沒(méi)有書(shū)寫(xiě)綁定證書(shū)的代碼?以訪問(wèn)github的代碼為例:
publicvoidloadData() {
Retrofit retrofit =newRetrofit.Builder().baseUrl("https://api.github.com/").build();
GitHubApi api = retrofit.create(GitHubApi.class);
Call call = api.contributorsBySimpleGetCall(mUserName, mRepo);
call.enqueue(newCallback() {@OverridepublicvoidonResponse(Call call, Response response) {// handle response}@OverridepublicvoidonFailure(Call call, Throwable t) {// handle failure}
});
}
在https是如何工作的一文中我們說(shuō)過(guò)android已經(jīng)幫我們預(yù)置了150多個(gè)證書(shū),這些證書(shū)你可以在設(shè)置->安全->信任的憑據(jù)中看到(在windows中,你可以在命令行中打開(kāi)certmgr.msc來(lái)打開(kāi)證書(shū)管理器,這里你可以看看windows預(yù)置的證書(shū))?,F(xiàn)在可以明白了,之所以沒(méi)有內(nèi)置證書(shū)的原因在于:我們服務(wù)端用的證書(shū)是從android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)購(gòu)買的證書(shū),在android中已經(jīng)內(nèi)置了這些證書(shū),而默認(rèn)情況下,retrofit 2.0 所依賴的okhttp 3.0 是信任它們,因此可以直接訪問(wèn)而無(wú)需在客戶端設(shè)置什么。
使用非android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)頒發(fā)的證書(shū)或自簽名證書(shū)
購(gòu)買證書(shū)畢竟是花錢(qián)的,現(xiàn)在免費(fèi)的證書(shū)有少之又少,因此使用自簽名證書(shū)就是另外一種常見(jiàn)的方式了。什么是自簽名證書(shū)呢?所謂的自簽名證書(shū)就是沒(méi)有通過(guò)受信任的證書(shū)頒發(fā)機(jī)構(gòu),自己給自己頒發(fā)的證書(shū)(下文中,我將非android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)頒發(fā)的證書(shū)也歸為自簽名證書(shū))。最典型的就是12306火車購(gòu)票,使用的證書(shū)就不是受信任的證書(shū)頒發(fā)機(jī)構(gòu)頒發(fā)的,而是旗下SRCA(中鐵數(shù)字證書(shū)認(rèn)證中心,簡(jiǎn)稱中鐵CA,它是鐵道自己搞的機(jī)構(gòu),因此相當(dāng)于自己給自己頒發(fā)證書(shū))頒發(fā)的證書(shū),如下圖:
SSL證書(shū)分為三類:
1. 由android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)或者該結(jié)構(gòu)下屬的機(jī)構(gòu)頒發(fā)的證書(shū),比如Symantec,GoDaddy等機(jī)構(gòu),約150多個(gè)。更多的自行在手機(jī)“設(shè)置->安全->信任的憑據(jù)”中查看
2.沒(méi)有被android所認(rèn)可的證書(shū)所頒發(fā)的證書(shū)
3. 自己頒發(fā)的證書(shū)
這三類證書(shū)中,只有第一種在使用中不會(huì)出現(xiàn)安全提示,不會(huì)拋出異常。
由于我們使用的是自簽名的證書(shū),因此客戶端不信任服務(wù)器,會(huì)拋出異常:javax.NET.ssl.SSLHandshakeException:.為此,我們需要自定義信任處理器(TrustManager)來(lái)替代系統(tǒng)默認(rèn)的信任處理器,這樣我們才能正常的使用自定義的正說(shuō)或者非android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)頒發(fā)的證書(shū)。
針對(duì)使用場(chǎng)景,又分為以下兩種情況:一種是安全性要求不高的情況下,客戶端無(wú)需內(nèi)置證書(shū);另外一種則是客戶端內(nèi)置證書(shū)。
下面我會(huì)針對(duì)這兩種情況說(shuō)明其中一些問(wèn)題點(diǎn)。
我們知道由于我們使用的是自簽名的證書(shū),所以需要自定義TrustManager,那么很多開(kāi)發(fā)者的處理策略非常簡(jiǎn)單粗暴:讓客戶端不對(duì)服務(wù)器證書(shū)做任何驗(yàn)證,其實(shí)現(xiàn)代碼如下:
publicstaticSSLSocketFactorygetSSLSocketFactory()throwsException {//創(chuàng)建一個(gè)不驗(yàn)證證書(shū)鏈的證書(shū)信任管理器。finalTrustManager[] trustAllCerts =newTrustManager[]{newX509TrustManager() {@OverridepublicvoidcheckClientTrusted(
java.security.cert.X509Certificate[] chain,
String authType)throwsCertificateException {
}@OverridepublicvoidcheckServerTrusted(
java.security.cert.X509Certificate[] chain,
String authType)throwsCertificateException {
}@Overridepublicjava.security.cert.X509Certificate[]getAcceptedIssuers() {returnnewjava.security.cert.X509Certificate[0];
}
}};// Install the all-trusting trust managerfinalSSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts,newjava.security.SecureRandom());// Create an ssl socket factory with our all-trusting managerreturnsslContext
.getSocketFactory();
}//使用自定義SSLSocketFactoryprivatevoidonHttps(OkHttpClient.Builder builder) {try{
builder.sslSocketFactory(getSSLSocketFactory()).hostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
}catch(Exception e) {
e.printStackTrace();
}
}
上面的代碼不要輕易的應(yīng)用在實(shí)際工程中,除非你能容忍他的危害。為什么這么說(shuō)呢?繼續(xù)往下看。
在上面的代碼中,我們自行實(shí)現(xiàn)X509TrustManager時(shí)并沒(méi)有對(duì)其中三個(gè)核心的方法進(jìn)行 具體實(shí)現(xiàn)(主要是沒(méi)有在checkServerTrusted()驗(yàn)證證書(shū)),這樣做相當(dāng)于直接忽略了檢驗(yàn)服務(wù)端證書(shū)。因此無(wú)論服務(wù)器的證書(shū)如何,都能建立起https鏈接。
看起來(lái)好像解決了我們的問(wèn)題,實(shí)則帶來(lái)更大的危害。此時(shí),雖然能建立HTTPS連接,但是無(wú)形之中間人攻擊打開(kāi)了一道門(mén)。此時(shí),黑客完全可以攔截到我們的HTTPS請(qǐng)求,然后用偽造的證書(shū)冒充真正服務(wù)端的數(shù)字證書(shū),由于客戶端不對(duì)證書(shū)做驗(yàn)證(也就沒(méi)法判斷服務(wù)端到底是正常的還是偽造的),這樣客戶端就會(huì)和黑客的服務(wù)器建立連接。這就相當(dāng)于你以為你對(duì)的對(duì)面是個(gè)美女,卻沒(méi)有想到已經(jīng)被掉包了,想想“貍貓換太子”就明白了。(對(duì)這點(diǎn)不明白的同學(xué),可以參見(jiàn)證書(shū)鎖定中的示例。)
那么怎么避免呢?我們需要在自定義TrustManager時(shí)重寫(xiě)checkServerTrusted()方法,并在該方法中校驗(yàn)證書(shū),完善后的代碼如下:
publicstaticSSLSocketFactorygetSSLSocketFactory()throwsException {// Create a trust manager that does not validate certificate chainsfinalTrustManager[] trustAllCerts =newTrustManager[]{newX509TrustManager() {//證書(shū)中的公鑰publicstaticfinalString PUB_KEY ="3082010a0282010100d52ff5dd432b3a05113ec1a7065fa5a80308810e4e181cf14f7598c8d553cccb7d5111fdcdb55f6ee84fc92cd594adc1245a9c4cd41cbe407a919c5b4d4a37a012f8834df8cfe947c490464602fc05c18960374198336ba1c2e56d2e984bdfb8683610520e417a1a9a5053a10457355cf45878612f04bb134e3d670cf96c6e598fd0c693308fe3d084a0a91692bbd9722f05852f507d910b782db4ab13a92a7df814ee4304dccdad1b766bb671b6f8de578b7f27e76a2000d8d9e6b429d4fef8ffaa4e8037e167a2ce48752f1435f08923ed7e2dafef52ff30fef9ab66fdb556a82b257443ba30a93fda7a0af20418aa0b45403a2f829ea6e4b8ddbb9987f1bf0203010001";@OverridepublicvoidcheckClientTrusted(
java.security.cert.X509Certificate[] chain,
String authType)throwsCertificateException {
}//客戶端并為對(duì)ssl證書(shū)的有效性進(jìn)行校驗(yàn)@OverridepublicvoidcheckServerTrusted(
java.security.cert.X509Certificate[] chain,
String authType)throwsCertificateException {if(chain ==null) {thrownewIllegalArgumentException("checkServerTrusted:x509Certificate array isnull");
}if(!(chain.length >0)) {thrownewIllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}if(!(null!= authType && authType.equalsIgnoreCase("RSA"))) {thrownewCertificateException("checkServerTrusted: AuthType is not RSA");
}// Perform customary SSL/TLS checkstry{
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore)null);for(TrustManager trustManager : tmf.getTrustManagers()) {
((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
}
}catch(Exception e) {thrownewCertificateException(e);
}// Hack ahead: BigInteger and toString(). We know a DER encoded Public Key begins// with 0×30 (ASN.1 SEQUENCE and CONSTRUCTED), so there is no leading 0×00 to drop.RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();
String encoded =newBigInteger(1/* positive */, pubkey.getEncoded()).toString(16);// Pin it!finalbooleanexpected = PUB_KEY.equalsIgnoreCase(encoded);if(!expected) {thrownewCertificateException("checkServerTrusted: Expected public key: "+ PUB_KEY +", got public key:"+ encoded);
}
}@Overridepublicjava.security.cert.X509Certificate[]getAcceptedIssuers() {returnnewjava.security.cert.X509Certificate[0];
}
}};// Install the all-trusting trust managerfinalSSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts,newjava.security.SecureRandom());// Create an ssl socket factory with our all-trusting managerreturnsslContext
.getSocketFactory();
}
其中PUB_KEY是我們證書(shū)中的公鑰,你可以自行從自己的證書(shū)中提取。我們看到,在checkServerTrusted()方法中,我們通過(guò)證書(shū)的公鑰信息來(lái)確認(rèn)證書(shū)的真?zhèn)?,如果?yàn)證失敗,則中斷請(qǐng)求。當(dāng)然,此處加入證書(shū)的有效期會(huì)更加的完善,實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,這里就不做說(shuō)明了。
除了上面那種在checkServerTrusted()實(shí)現(xiàn)證書(shū)驗(yàn)證的方式之外,我們也可以利用retrofit中CertificatePinner來(lái)實(shí)現(xiàn)證書(shū)鎖定,同樣也能達(dá)到我們的目的。
如果我們使用的是自簽名證書(shū),那么客戶端中的retrofit該如何進(jìn)行設(shè)置呢?關(guān)鍵還是我們上文提到的TrustManager。在retrofit中使用自簽名證書(shū)大致要經(jīng)過(guò)以下幾步:
將證書(shū)添加到工程中
自定義信任管理器TrustManager
用自定義TrustManager代替系統(tǒng)默認(rèn)的信任管理器
我們按步驟進(jìn)行說(shuō)明。
比如現(xiàn)在我們有個(gè)證書(shū)media.bks,首先需要將其放在res/raw目錄下,當(dāng)然你可以可以放在assets目錄下。
我們知道Java本身支持的證書(shū)格式j(luò)ks,但是遺憾的是在android當(dāng)中并不支持jks格式正式,而是需要bks格式的證書(shū)。因此我們需要將jks證書(shū)轉(zhuǎn)換成bks格式證書(shū),關(guān)于jks轉(zhuǎn)bks不再本文做重點(diǎn)說(shuō)明
和上面不同的是,這里需要實(shí)現(xiàn)本地證書(shū)的加載,具體見(jiàn)代碼:
protectedstaticSSLSocketFactorygetSSLSocketFactory(Context context,int[] certificates) {if(context ==null) {thrownewNullPointerException("context == null");
}//CertificateFactory用來(lái)證書(shū)生成CertificateFactory certificateFactory;try{
certificateFactory = CertificateFactory.getInstance("X.509");//Create a KeyStore containing our trusted CAsKeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null,null);for(inti =0; i < certificates.length; i++) {//讀取本地證書(shū)InputStreamis= context.getResources().openRawResource(certificates[i]);
keyStore.setCertificateEntry(String.valueOf(i), certificateFactory.generateCertificate(is));if(is!=null) {is.close();
}
}//Create a TrustManager that trusts the CAs in our keyStoreTrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);//Create an SSLContext that uses our TrustManagerSSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(),newSecureRandom());returnsslContext.getSocketFactory();
}catch(Exception e) {
}returnnull;
}
用自定義TrustManager代替系統(tǒng)默認(rèn)的信任管理器
privatevoidonHttpCertficates(OkHttpClient.Builder builder) {int[] certficates =newint[]{R.raw.media};
builder.socketFactory(getSSLSocketFactory(AppContext.context(), certficates));
}
這樣我們就可以的客戶端就可以使用自簽名的證書(shū)了。其實(shí)不難發(fā)現(xiàn),使用非android認(rèn)證證書(shū)頒發(fā)機(jī)構(gòu)頒發(fā)的證書(shū)的關(guān)鍵在于:修改android中SSLContext自帶的TrustManager以便能讓我們的簽名通過(guò)驗(yàn)證。