Android App 安全的HTTPS 通訊

漏洞描述

對於數字證書相關概念、Android 裏 https 通訊代碼就再也不復述了,直接講問題。缺乏相應的安全校驗很容易致使中間人攻擊,而漏洞的形式主要有如下3種:html

  • 自定義X509TrustManager
    在使用HttpsURLConnection發起 HTTPS 請求的時候,提供了一個自定義的X509TrustManager,
    未實現安全校驗邏輯,下面片斷就是常見的容易犯錯的代碼片斷。若是不提供自定義的X509TrustManager,
    代碼運行起來可能會報異常(緣由下文解釋),初學者就很容易在不明真相的狀況下提供了一個自定義的X509TrustManager,
    卻忘記正確地實現相應的方法。本文重點介紹這種場景的處理方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 自定義X509TrustManager,存在安全漏洞
* 跳過證書校驗
*/
public class UnSafeTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//do nothing,接受任意客戶端證書
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//do nothing,接受任意服務端證書
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
  • 自定義了HostnameVerifier
    在握手期間,若是 URL 的主機名和服務器的標識主機名不匹配,則驗證機制能夠回調此接口的實現程序來肯定是否應該容許此鏈接。
    若是回調內實現不恰當,默認接受全部域名,則有安全風險。代碼示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Created by chenfeiyue on 2018/6/1.
* Description :UnSafeHostnameVerifier
*/
public class UnSafeHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
// Always return true,接受任意域名服務器
return true;
}
}

HttpsURLConnection.setDefaultHostnameVerifier(new UnSafeHostnameVerifier());

修復方案

分而治之,針對不一樣的漏洞點分別描述,這裏就講的修復方案主要是針對非瀏覽器App,非瀏覽器 App 的服務端通訊對象比較固定,通常都是自家服務器,能夠作不少特定場景的定製化校驗。若是是瀏覽器 App,校驗策略就有更通用一些。java

  • 自定義X509TrustManager。前面說到,當發起 HTTPS 請求時,可能拋起一個異常,如下面這段代碼爲例(來自官方文檔):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

private void copyInputStreamToOutputStream(InputStream in, PrintStream out) throws IOException {
byte[] buffer = new byte[1024];
int c = 0;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
}

它會拋出一個SSLHandshakeException的異常。android

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:322)
at com.android.okhttp.Connection.upgradeToTls(Connection.java:201)
at com.android.okhttp.Connection.connect(Connection.java:155)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:276)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:211)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:382)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:332)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:199)
at com.android.okhttp.internal.http.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:210)
at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:25)
at me.longerian.abcandroid.datetimepicker.TestDateTimePickerActivity$1.run(TestDateTimePickerActivity.java:236)
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:318)
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:219)
at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:114)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:550)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:318)
... 10 more
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
... 16 more

Android 手機有一套共享證書的機制,若是目標 URL 服務器下發的證書不在已信任的證書列表裏,或者該證書是自簽名的,不是由權威機構頒發,那麼會出異常。對於咱們這種非瀏覽器 app 來講,若是提示用戶去下載安裝證書,可能會顯得比較詭異。幸虧還能夠經過自定義的驗證機制讓證書經過驗證。驗證的思路有兩種:瀏覽器

方案1

不管是權威機構頒發的證書仍是自簽名的,打包一份到 app 內部,好比存放在 asset 裏。經過這分內置的證書初始化一個KeyStore,而後用這個KeyStore去引導生成的TrustManager來提供驗證,具體代碼以下:安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// uwca.crt 打包在 asset 中,該證書能夠從https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下載
InputStream caInput = new BufferedInputStream(getAssets().open("uwca.crt"));
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
Log.i("Longer", "ca=" + ((X509Certificate) ca).getSubjectDN());
Log.i("Longer", "key=" + ((X509Certificate) ca).getPublicKey();
} finally {
caInput.close();
}

// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLSv1","AndroidOpenSSL");
context.init(null, tmf.getTrustManagers(), null);

URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
(HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
方案2

同方案1,打包一份到證書到 app 內部,但不經過KeyStore去引導生成的TrustManager,而是乾脆直接自定義一個TrustManager,本身實現校驗邏輯;校驗邏輯主要包括:服務器

  • 服務器證書是否過時
  • 證書籤名是否合法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import android.content.Context;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

/**
* Created by chenfeiyue on 2018/6/1.
* Description :自定義TrustManager 校驗服務端證書,有效期等
*/
public class SafeTrustManager implements X509TrustManager {

private Context mContext;

public SafeTrustManager(Context context) {
this.mContext = context;
}

/* 此處存放服務器證書密鑰 */
// private static final String PUB_KEY =
// "30820122300d06092a864886f70d01010105000382010f003082010a0282010100add086cfc3df3bcf54bffb4e044a911cc0eadbab61ead529a96525833a1a00f75df3d746e11666dbdf4ed8594c4f9194456a49a32a3dce999d9679d2cbc59cf9082935517e35a0706f1041ad053b727c9c92a47507d0313cf5b3788c609733255a89d40c6a8b8d1a90f0761e7dacf117e43fe1b5ae093e160f902a42433ebd57f91cf27b88cd46dcebb85aa0b33c6a48771ca445ace6f6668626d60156eecd1fc2feb282809f8f835b5f5c457890694f495fbf1620070b4a18094c44680beafac05c59ba062b2e889cc8e6a5feca13c3e473700858aceeac0e25f2ba0bfdf44b1040a9ecb15a3f7ea91a366baeeed02f0af78f982d5d0db854bf9476db5f15c10203010001";

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for (X509Certificate cert : chain) {

// Make sure that it hasn't expired.
cert.checkValidity();

// Verify the certificate's public key chain.
try {
X509Certificate x509Certificate = TLSSocketFactory.getX509Certificate(mContext);
cert.verify(x509Certificate.getPublicKey());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
}
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}

一樣上述代碼只能訪問 certs.cac.washington.edu 相關域名地址,若是訪問 https://www.taobao.com/ 或者 https://www.baidu.com/ ,則會在cert.verify(((X509Certificate) ca).getPublicKey());處拋異常,致使鏈接失敗。session

  • 自定義HostnameVerifier,簡單的話就是根據域名進行字符串匹配校驗;業務複雜的話,還能夠結合配置中心、白名單、黑名單、正則匹配等多級別動態校驗;整體來講邏輯仍是比較簡單的,反正只要正確地實現那個方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
//示例
if("yourhostname".equals(hostname)){
return true;
} else {
HostnameVerifier hv =
HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname, session);
}
}
};

參考

蘋果核 - Android App 安全的HTTPS 通訊
經過 HTTPS 和 SSL 確保安全app

相關文章
相關標籤/搜索