很多文章對(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)。
證書(shū)鎖定
簡(jiǎn)介
首先來(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多塊錢的證書(shū)費(fèi)用师幕,能省一點(diǎn)就省一點(diǎn)嘛。
retrofit中使用證書(shū)鎖定
現(xiàn)在诬滩,我們來(lái)看看如何在retrofit中進(jìn)行證書(shū)鎖定霹粥。
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.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ū)。
證書(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ū)鎖定的缺點(diǎn)
證書(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的代碼為例:
public void loadData() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
GitHubApi api = retrofit.create(GitHubApi.class);
Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
// handle response
}
@Override
public void onFailure(Call<ResponseBody> 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ū))『蓿現(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ū)畢竟是花錢的宽档,現(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ū)分為三類:
- 由android認(rèn)可的證書(shū)頒發(fā)機(jī)構(gòu)或者該結(jié)構(gòu)下屬的機(jī)構(gòu)頒發(fā)的證書(shū)璧诵,比如Symantec,Go Daddy等機(jī)構(gòu),約150多個(gè)仇冯。更多的自行在手機(jī)“設(shè)置->安全->信任的憑據(jù)”中查看
2.沒(méi)有被android所認(rèn)可的證書(shū)所頒發(fā)的證書(shū)
- 自己頒發(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)。
客戶端不內(nèi)置證書(shū)
我們知道由于我們使用的是自簽名的證書(shū)危喉,所以需要自定義TrustManager,那么很多開(kāi)發(fā)者的處理策略非常簡(jiǎn)單粗暴:讓客戶端不對(duì)服務(wù)器證書(shū)做任何驗(yàn)證宋渔,其實(shí)現(xiàn)代碼如下:
public static SSLSocketFactory getSSLSocketFactory() throws Exception {
//創(chuàng)建一個(gè)不驗(yàn)證證書(shū)鏈的證書(shū)信任管理器。
final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts,
new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
return sslContext
.getSocketFactory();
}
//使用自定義SSLSocketFactory
private void onHttps(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ū)棚贾,完善后的代碼如下:
public static SSLSocketFactory getSSLSocketFactory() throws Exception {
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
//證書(shū)中的公鑰
public static final String PUB_KEY = "3082010a0282010100d52ff5dd432b3a05113ec1a7065fa5a80308810e4e181cf14f7598c8d553cccb7d5111fdcdb55f6ee84fc92cd594adc1245a9c4cd41cbe407a919c5b4d4a37a012f8834df8cfe947c490464602fc05c18960374198336ba1c2e56d2e984bdfb8683610520e417a1a9a5053a10457355cf45878612f04bb134e3d670cf96c6e598fd0c693308fe3d084a0a91692bbd9722f05852f507d910b782db4ab13a92a7df814ee4304dccdad1b766bb671b6f8de578b7f27e76a2000d8d9e6b429d4fef8ffaa4e8037e167a2ce48752f1435f08923ed7e2dafef52ff30fef9ab66fdb556a82b257443ba30a93fda7a0af20418aa0b45403a2f829ea6e4b8ddbb9987f1bf0203010001";
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] chain,
String authType) throws CertificateException {
}
//客戶端并為對(duì)ssl證書(shū)的有效性進(jìn)行校驗(yàn)
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] chain,
String authType) throws CertificateException {
if (chain == null) {
throw new IllegalArgumentException("checkServerTrusted:x509Certificate array isnull");
}
if (!(chain.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
if (!(null != authType && authType.equalsIgnoreCase("RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not RSA");
}
// Perform customary SSL/TLS checks
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore) null);
for (TrustManager trustManager : tmf.getTrustManagers()) {
((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
}
} catch (Exception e) {
throw new CertificateException(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 = new BigInteger(1 /* positive */, pubkey.getEncoded()).toString(16);
// Pin it!
final boolean expected = PUB_KEY.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: Expected public key: "
+ PUB_KEY + ", got public key:" + encoded);
}
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts,
new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
return sslContext
.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á)到我們的目的迹炼。
客戶端內(nèi)置證書(shū)
如果我們使用的是自簽名證書(shū)砸彬,那么客戶端中的retrofit該如何進(jìn)行設(shè)置呢?關(guān)鍵還是我們上文提到的TrustManager斯入。在retrofit中使用自簽名證書(shū)大致要經(jīng)過(guò)以下幾步:
- 將證書(shū)添加到工程中
- 自定義信任管理器TrustManager
- 用自定義TrustManager代替系統(tǒng)默認(rèn)的信任管理器
我們按步驟進(jìn)行說(shuō)明拿霉。
添加證書(shū)到工程
比如現(xiàn)在我們有個(gè)證書(shū)media.crt,首先需要將其放在res/raw目錄下咱扣,當(dāng)然你可以可以放在assets目錄下绽淘。
自定義TrustManager
和上面不同的是,這里需要實(shí)現(xiàn)本地證書(shū)的加載闹伪,具體見(jiàn)代碼:
protected static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {
if (context == null) {
throw new NullPointerException("context == null");
}
//CertificateFactory用來(lái)證書(shū)生成
CertificateFactory certificateFactory;
try {
certificateFactory = CertificateFactory.getInstance("X.509");
//Create a KeyStore containing our trusted CAs
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
for (int i = 0; i < certificates.length; i++) {
//讀取本地證書(shū)
InputStream is = 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 keyStore
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
//Create an SSLContext that uses our TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
}
return null;
}
用自定義TrustManager代替系統(tǒng)默認(rèn)的信任管理器
private void onHttpCertficates(OkHttpClient.Builder builder) {
int[] certficates = new int[]{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)證。
暫告一段落
本文中簡(jiǎn)單首先介紹了證書(shū)鎖定的使用杀怠、原理及優(yōu)缺點(diǎn)椰憋,接著對(duì)客戶端使用自定義證書(shū)中的一些點(diǎn)做了介紹,希望能幫助各位打造安全的安卓客戶端赔退。
另外橙依,大多數(shù)情況下,我建議使用證書(shū)鎖定來(lái)提高安全性硕旗。關(guān)于雙向證書(shū)驗(yàn)證窗骑,后續(xù)有時(shí)間再補(bǔ)充。