Coding Story

在黑暗中寫故事 👻

0%

關於 Android 憑證過期

前言故事

最近網路新聞在流傳著一篇「Android 使用者注意!7.1.1 前舊機將不能正常瀏覽網頁」,小唯的公司也對此在做一些可能的準備動作。之所以會對這議題這麼敏感,正是因為不久前,小唯公司才因為 AddTrust External Root CA 過期,換了自己網站的簽署 root CA 憑證,造成在 Android 5 以下的手機,使用 WebView 時發生錯誤…

不建議的解決辦法

小唯對這件事仍是記憶猶新,當時的解決辦法是,把新憑證一起打包進 App 中,使用 Android 更替 X509TrustManager 的方式,將需要允許連線的憑證包進去。

過程中,有個錯誤做法值得一提,因為網路上會搜尋到這種做法:

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
// "信任全部的" X509TrustManager
final TrustManager[] tm =
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[] {};
}
}
};

final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tm, new java.security.SecureRandom());
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

// 準備建立 OkHttpClient
OkHttpClient.Builder builder = ...
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) tm[0]);
OkHttpClient = builder.build();

這是網路常見的做法之一,不建議的原因是還是安全性考量,默認相信所有憑證會讓惡意網頁有機可乘。

更好的做法

小唯團隊使用的做法是,建立一個全新的鏈狀 X509TrustManager 如下,它會先檢查 Android 系統憑證,再檢查公司的新憑證,除此之外的一率不放行:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
public class CustomX509TrustManager implements X509TrustManager {

// 建立憑證鏈,會依序檢查是否符合憑證
private final List<X509TrustManager> trustManagers;

public static TrustManager[] getTrustManagers(KeyStore keyStore) {
return new TrustManager[] {new CustomX509TrustManager(keyStore)};
}

public CustomX509TrustManager(KeyStore keystore) {
this.trustManagers = new ArrayList<>();
// 加入原本的 TrustManager
this.trustManagers.add(getDefaultTrustManager());
// 加入指定 keystore TrustManager
this.trustManagers.add(getTrustManager(keystore));
try {
this.trustManagers.add(trustManagerForCertificates(trustedCertificatesInputStream()));
} catch (Exception e) {
// 無法添加我們自己的 cert,使用系統預設的
}
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
for (X509TrustManager trustManager : trustManagers) {
try {
trustManager.checkClientTrusted(chain, authType);
return; // 有其中一個 cert 通過,就當作通過
} catch (CertificateException e) {
// 這個不通過,換下一個
}
}
throw new CertificateException("None of the TrustManagers trust this certificate chain");
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
for (X509TrustManager trustManager : trustManagers) {
try {
trustManager.checkServerTrusted(chain, authType);
return; // 有其中一個 cert 通過,就當作通過
} catch (CertificateException e) {
// 這個不通過,換下一個
}
}
throw new CertificateException("None of the TrustManagers trust this certificate chain");
}

@Override
public X509Certificate[] getAcceptedIssuers() {
List<X509Certificate> certificates = new ArrayList<>();
for (X509TrustManager trustManager : trustManagers) {
certificates.addAll(Arrays.asList(trustManager.getAcceptedIssuers()));
}
return certificates.toArray(new X509Certificate[0]);
}

private X509TrustManager getDefaultTrustManager() {
return getTrustManager(null);
}

private X509TrustManager getTrustManager(KeyStore keystore) {
return getTrustManager(TrustManagerFactory.getDefaultAlgorithm(), keystore);
}

/**
* 只取出 keystore 中 X509 TrustManager
*/
private X509TrustManager getTrustManager(String algorithm, KeyStore keystore) {
TrustManagerFactory factory;
try {
factory = TrustManagerFactory.getInstance(algorithm);
factory.init(keystore);
List<TrustManager> trustManagerList = Arrays.asList(factory.getTrustManagers());
List<X509TrustManager> x509TrustManagerList = new ArrayList<>();
if (trustManagerList != null) {
for (TrustManager tm : trustManagerList) {
if (tm instanceof X509TrustManager) {
x509TrustManagerList.add((X509TrustManager) tm);
}
}
}
if (x509TrustManagerList.size() > 0) {
return x509TrustManagerList.get(0);
}
} catch (NoSuchAlgorithmException | KeyStoreException e) {
e.printStackTrace();
}
return null;
}

/**
* 將指定 InputStream 的的憑證建成 TrustManager
*/
private X509TrustManager trustManagerForCertificates(InputStream in)
throws GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("Unexpected empty InputStream");
}

// 建立 keystore
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}

// 使用 keystore 建立 TrustManager
KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
List<TrustManager> trustManagerList = Arrays.asList(trustManagerFactory.getTrustManagers());
List<X509TrustManager> x509TrustManagerList = new ArrayList<>();
if (trustManagerList != null) {
for (TrustManager tm : trustManagerList) {
if (tm instanceof X509TrustManager) {
x509TrustManagerList.add((X509TrustManager) tm);
}
}
}
if (x509TrustManagerList.size() > 0) {
return x509TrustManagerList.get(0);
}
return null;
}

private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = null; // By convention, 'null' creates an empty key store.
keyStore.load(in, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}

private static InputStream trustedCertificatesInputStream() {
// 憑證也能放在 resource 內,demo 放這容易看
String cert =
""
+ "-----BEGIN CERTIFICATE-----\n"
// 憑證內容放這
+ "-----END CERTIFICATE-----\n";
return new Buffer().writeUtf8(cert).inputStream();
}
}

以上從 https://square.github.io/okhttp/https/#customizing-trusted-certificates-kt-java 變化而來

其他做法

小唯之後又發現,okhttp 官方 github 也有類似的 sample,之後也許也能試試

okhttp 官方 github CustomTrust.java

關於 Android 7.1.1

至於 Android 7.1.1 版本之前的裝置呢?因為許多網站簽署的憑證即將到期,勢必會換成新的憑證,造成舊機 WebView 或瀏覽器可能不支援新的憑證,解決方法還是建議更新系統(如果可以的話),要不就是 App 要準備處理過期憑證的錯誤,適時提醒使用者憑證問題,以及建議的解決方式,避免一開啟網頁就遇到一片空白,甚至 App 強制關閉 😖

另一個小唯在思索的事情是,非自己產品的相關的網頁,是否該用 WebView 開啟?嚴格來說其實不應該,因為不清楚其他人會在相關網頁內,做什麼事情,尤其時當開啟 JavaScript 後,這個問題更值得所有用 App WebView 去開啟第三方網頁的團隊去思索~


相關連結: CompositeTrustManager, okhttp customizing trusted certificates, okhttp customizing trusted certificates

------------- 本文结束 App minSdkVersion 到底什麼時候才能提升啊 -------------