抓包原理
抓包的基本原理就是中间人攻击 HTTPS 的握手过程。Mac 上可使用 Charles 进行抓包。本质上就是两段 HTTPS 连接,Client <--> Man-In-The-Middle 和 Man-In-The-Middle <--> Server。使用 Charles 进行抓包,需要 Client 端提前将 Charles 的根证书添加在 Client 的信任列表中。
Android 端防止抓包 —— Certificate Pinning
回顾之前的 HTTPS 的握手过程,可以知道 SSL 的核心过程就是客户端验证证书链合法性——客户端检查证书链中是否有一个证书或者公钥存在于客户端的可信任列表中。
手机系统中内置了上百份不同的根证书。Certificate Pinning 的原理其实就是 app 中内置需要被信任的特定证书,app 在验证服务器传过来的证书链时,使用这些特定证书来验证的。
- 叶子证书。如果 app 选择 pinning 叶子证书,那么就可以 100% 保证 ssl 证书链的合法性。但是叶子证书有效期短,服务器换证书(因为私钥泄露、证书到期等原因)的话就客户端 app 就需要用新的叶子证书验证。
- 中间证书。如果 app 选择 pinning 中间证书,那么客户端 app 也就选择了相信签发中间证书的机构所签发的其他证书。这样的话,只要服务端使用同一个中间机构签发的叶子证书,客户端 app 就不需要做任何改变。同时,中间证书有效期也非常长。
- 根证书。从证书链的验证过程来看,它的效果与中间证书相同,但是信任了更多的中间机构及其签发的证书。根证书的有效期比中间证书更长。
客户端 app 可以同时 pinning 多个证书,以灵活地适应各种证书验证策略。
pinning 证书还是公钥?
证书的主要作用是公钥的载体,但在实践中我们更多是去 pinning 公钥,SubjectPublicKeyInfo(SPKI)。这是因为很多服务器会去定期旋转证书,但是证书旋转后,证书中的公钥还是相同的公钥。
私钥泄露怎么办?
如果私钥泄露了,那么服务器端就不得不使用新的私钥做出新的证书。客户端为了预防这种情况,可以提前 pinning 这些新的证书。这样,当服务器替换新的证书时,客户端 app 就可以不做任何改动。
如何存放证书或者公钥?
- 内置。客户端 app 可以将证书或公钥 hardcode 到代码中,或者作为资源文件放进 asset 中。这样做的坏处就是,老版本的 app 难以替换其中的证书或公钥。
- 第一次使用时保存。客户端 app 第一次访问服务器进行 ssl 证书链验证时,将其中的公钥保存下来。这种方法适用于客户端 app 不能提前知道它将要访问的服务器地址的时候。
- 网络下发。最灵活的方法是,服务端提供一个证书服务器专门用于向客户端 app 下发需要 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>
节点接受 SubjectPublicKeyInfo 的 hash 值。
OkHttp
OkHttp 从 2.1 开始直接支持 Certificate Pinning。
HttpsURLConnection
1. Add Certificate To TrustManager
我在项目实践中发现有的服务器并不会在 ssl 握手阶段将完整的证书链传输过来——只会传证书链中的根证书和叶子证书。如果安卓系统中使用 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)
但是浏览器对于这种缺失中间证书的服务器却能验证通过,主要原因是浏览器访问有完整证书链的网站时,如果发现证书链中有浏览器没有内置的中间证书,那么浏览器会将该证书缓存下来,这样浏览器访问其他没有该中间证书的服务器时,就可以使用这个缓存的中间证书来验证证书链。
解决安卓上出现这个问题的方法是将这个中间证书通过 app 添加到信任证书列表中。我们需要将该中间证书加入到 App 运行时所用的 TrustManager
中。
- 将需要添加的 CA 证书加载到
InputStream
中 - 使用这个
InputStream
创建一个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()
允许开发者在系统对证书链验证通过后,再次使用自己的方法验证证书链。
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);