抓包原理
抓包的基本原理就是中間人攻擊 HTTPS 的握手過程胚宦。Mac 上可使用 Charles 進行抓包。本質上就是兩段 HTTPS 連接燕垃,Client <--> Man-In-The-Middle 和 Man-In-The-Middle <--> Server枢劝。使用 Charles 進行抓包,需要 Client 端提前將 Charles 的根證書添加在 Client 的信任列表中卜壕。
Android 端防止抓包 —— Certificate Pinning
回顧之前的 HTTPS 的握手過程您旁,可以知道 SSL 的核心過程就是客戶端驗證證書鏈合法性——客戶端檢查證書鏈中是否有一個證書或者公鑰存在于客戶端的可信任列表中。
手機系統(tǒng)中內置了上百份不同的根證書轴捎。Certificate Pinning 的原理其實就是 app 中內置需要被信任的特定證書鹤盒,app 在驗證服務器傳過來的證書鏈時,使用這些特定證書來驗證的侦副。
- 葉子證書侦锯。如果 app 選擇 pinning 葉子證書,那么就可以 100% 保證 ssl 證書鏈的合法性跃洛。但是葉子證書有效期短率触,服務器換證書(因為私鑰泄露、證書到期等原因)的話就客戶端 app 就需要用新的葉子證書驗證汇竭。
- 中間證書葱蝗。如果 app 選擇 pinning 中間證書,那么客戶端 app 也就選擇了相信簽發(fā)中間證書的機構所簽發(fā)的其他證書细燎。這樣的話两曼,只要服務端使用同一個中間機構簽發(fā)的葉子證書,客戶端 app 就不需要做任何改變玻驻。同時悼凑,中間證書有效期也非常長。
- 根證書璧瞬。從證書鏈的驗證過程來看户辫,它的效果與中間證書相同,但是信任了更多的中間機構及其簽發(fā)的證書嗤锉。根證書的有效期比中間證書更長渔欢。
客戶端 app 可以同時 pinning 多個證書,以靈活地適應各種證書驗證策略瘟忱。
pinning 證書還是公鑰奥额?
證書的主要作用是公鑰的載體苫幢,但在實踐中我們更多是去 pinning 公鑰,SubjectPublicKeyInfo(SPKI)垫挨。這是因為很多服務器會去定期旋轉證書韩肝,但是證書旋轉后,證書中的公鑰還是相同的公鑰九榔。
私鑰泄露怎么辦哀峻?
如果私鑰泄露了,那么服務器端就不得不使用新的私鑰做出新的證書帚屉∶战耄客戶端為了預防這種情況漾峡,可以提前 pinning 這些新的證書攻旦。這樣,當服務器替換新的證書時生逸,客戶端 app 就可以不做任何改動牢屋。
如何存放證書或者公鑰?
- 內置槽袄。客戶端 app 可以將證書或公鑰 hardcode 到代碼中烙无,或者作為資源文件放進 asset 中。這樣做的壞處就是遍尺,老版本的 app 難以替換其中的證書或公鑰截酷。
- 第一次使用時保存。客戶端 app 第一次訪問服務器進行 ssl 證書鏈驗證時乾戏,將其中的公鑰保存下來迂苛。這種方法適用于客戶端 app 不能提前知道它將要訪問的服務器地址的時候。
- 網絡下發(fā)鼓择。最靈活的方法是三幻,服務端提供一個證書服務器專門用于向客戶端 app 下發(fā)需要 pinning 的證書或公鑰,客戶端 app 只需要 pinning 這個證書服務器即可呐能。
Pinning Certificate
Android N
從 SDK 24 開始念搬,Android 支持通過 xml 來配置 certificate pinning,見 Network Security Configuration摆出。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
<!-- backup pin -->
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
</pin-set>
</domain-config>
</network-security-config>
其中 <pin>
節(jié)點接受 SubjectPublicKeyInfo 的 hash 值朗徊。
OkHttp
OkHttp 從 2.1 開始直接支持 Certificate Pinning。
HttpsURLConnection
1. Add Certificate To TrustManager
我在項目實踐中發(fā)現有的服務器并不會在 ssl 握手階段將完整的證書鏈傳輸過來——只會傳證書鏈中的根證書和葉子證書偎漫。如果安卓系統(tǒng)中使用 HttpUrlConnection
訪問服務器爷恳,拋出如下類似異常:
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
但是瀏覽器對于這種缺失中間證書的服務器卻能驗證通過,主要原因是瀏覽器訪問有完整證書鏈的網站時骑丸,如果發(fā)現證書鏈中有瀏覽器沒有內置的中間證書舌仍,那么瀏覽器會將該證書緩存下來妒貌,這樣瀏覽器訪問其他沒有該中間證書的服務器時,就可以使用這個緩存的中間證書來驗證證書鏈铸豁。
解決安卓上出現這個問題的方法是將這個中間證書通過 app 添加到信任證書列表中灌曙。我們需要將該中間證書加入到 App 運行時所用的 TrustManager
中。
- 將需要添加的 CA 證書加載到
InputStream
中 - 使用這個
InputStream
創(chuàng)建一個KeyStore
- 使用這個
KeyStore
初始化一個TrustManager
- 使用這個
TrustManager
去初始化一個SSLContext
- 由
SSLContext
提供一個SSLSocketFactor
- 使用這個
SSLSocketFactory
覆蓋HttpUrlConnection
的SSLSocketFactory
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
val cf: CertificateFactory = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
val ca: X509Certificate = caInput.use {
ca.generateCertificate(it) as X509Certificate
}
System.out.println("ca=" + ca.subjectDN)
// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
// Create a TrustManager that trusts the CAs inputStream our KeyStore
val tmfAlgorithm: String = TrustManagerFoctory.getDefaultAlgorithm()
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
// Create an SSLContext that uses our TrustManager
val context: SSLContext = SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
// Tell the URLConnection to use a SocketFactory from our SSLContext
val url = URL("https://certs.cac.washington.edu/CAtest/")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.sslSocketFactory = context.socketFactory
val inputStream: InputStream = urlConnection.inputStream
copyInputStreamToOutputStream(inputStream, System.out)
2. Certificate Pinning With HttpsUrlConnection
使用 X509TrustManagerExtensions 可以將證書 pinning 到 app 中节芥。X509TrustManagerExtensions.checkServerTrusted()
允許開發(fā)者在系統(tǒng)對證書鏈驗證通過后在刺,再次使用自己的方法驗證證書鏈。
private void validatePinning(X509TrustManagerExtensions trustManagerExt, HttpsURLConnection conn, Set<String> validPins) throws SSLException {
String certChainMsg = "";
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
List<X509Certificate> trustedChain = trustedChain(trustManagerExt, conn);
for (X509Certificate cert : trustedChain) {
byte[] publicKey = cert.getPublicKey().getEncoded();
md.update(publicKey, 0, publicKey.length);
String pin = Base64.encodeToString(md.digest(), Base64.NO_WRAP);
certChainMsg = " sha256/" + pin + " : " + cert.getSubjectDN().toString() + "\n";
if (validPins.contains(pin)) {
return;
}
}
} catch(NoSuchAlgorithmException e) {
thrown new SSLException(e);
}
throw new SSLPeerUnverifiedException("Certificate pinning failure\n" + "Peer certificate chain:\n" + certChainMsg);
}
private List<X509Certificate> trustedChain(X509TrustManagerExtensions trustManagerExt, HttpsURLConnection conn) throws SSLException {
Certificate[] serverCerts = conn.getServerCertificates();
X509Certificate[] untrustedCerts = Arrays.copyOf(serverCerts, serverCerts.length, X509Certificate[].class);
String host = conn.getURL().getHost();
try {
return trustManagerExt.checkServerTrusted(untrustedCerts, "RSA", host);
} catch(CertificateException e) {
throw new SSLException(e);
}
}
使用方法如下:
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
// Find first X509TrustManagerFactory in the TrustManagerFactory
X509TrustManager x509TrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
x509TrustManager = (X509TrustManager) trustManager;
break;
}
}
X509TrustManagerExtensions trustManagerExt = new X509TrustManagerExtensions(x509TrustManager);
...
URL url = new URL("https://www.xxx.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
Set<String> validPins = Collections.singleton("4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=");
validatePinning(trustManagerExt, urlConnection, validPins);