如1所述,在https客户端将服务器证书下载完成后,需要对服务器证书进行信任检查。Android系统(Windows系统也是如此)预先在系统里面安装了一些具有公信力的CA证书,后期用户也可以自行安装一些信得过的CA证书。这些CA证书就构成了受信CA集合。任何https服务器证书必须得到至少一个受信CA认证,这个服务器证书才会被信任。
具体到APP开发,我们若要对一个自签名的https服务器进行请求,很显然我们需要首先让自创CA受信。让自创CA受信有两种方法:
a、让用户安装我们自创的CA根证书,这样做在安卓系统里面显然很麻烦。
b、在APP里面内置CA证书。
大多数自签名APP用的是b策略,相关做法在安卓官方文档里面也有提及
https://developer.android.com/training/articles/security-ssl.html#SelfSigned
https://developer.android.com/training/articles/security-config.html#CustomTrust
new Thread(new Runnable() {
@Override
public void run() {
Log.i("yuyong", "post start");
OkHttpClient.Builder client_builder = new OkHttpClient.Builder();
client_builder.connectTimeout(10, TimeUnit.SECONDS);
client_builder.writeTimeout(10, TimeUnit.SECONDS);
client_builder.readTimeout(20, TimeUnit.SECONDS);
try {
client_builder.sslSocketFactory(SSLSocketFactoryBuilder.get(MainActivity.this, "ca.crt"));
client_builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
Log.i("yuyong", "check host-->" + s);
return true;
}
});
} catch (Exception e) {
e.printStackTrace();
}
Log.i("yuyong", "ssl ready");
OkHttpClient client = client_builder.build();
Request.Builder req_builder = new Request.Builder();
req_builder.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0");
req_builder.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
req_builder.addHeader("Accept-Language", "en-US,en;q=0.5");
req_builder.addHeader("Accept-Encoding", "gzip, deflate");
req_builder.addHeader("Connection", "keep-alive");
req_builder.addHeader("Content-Type", "application/json;chatset=UTF-8");
req_builder.addHeader("Pragma", "no-cache");
req_builder.addHeader("Cache-Control", "no-cache");
FormBody.Builder form_builder = new FormBody.Builder();
form_builder.add("key", "fuck you");
req_builder.post(form_builder.build());
req_builder.url("https://192.168.0.13:4444/test_params/do_post_test");
Call call = client.newCall(req_builder.build());
try {
Response response = call.execute();
Log.i("yuyong", response.body().string());
} catch (Exception e) {
e.printStackTrace();
return;
}
}
}).start();
SSLSocketFactoryBuilder.java
package com.thinking.loginsdkdemo;
import android.content.Context;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Created by Yu Yong on 2018/3/30.
*/
public class SSLSocketFactoryBuilder {
private static Certificate CA = null;
public static SSLSocketFactory get(Context ctx, String file_name) throws Exception {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = new BufferedInputStream(ctx.getAssets().open(file_name));
try {
CA = cf.generateCertificate(caInput);
Log.i("yuyong", "CA Pub key = " + CA.getPublicKey().toString());
} finally {
caInput.close();
}
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{mTrustManager}, null);
return context.getSocketFactory();
}
private static TrustManager mTrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
for (X509Certificate x509Certificate : x509Certificates) {
Log.i("yuyong", "Server Pub key = " + x509Certificate.getPublicKey().toString());
//检查站点证书的有效性
x509Certificate.checkValidity();
try {
//用CA证实站点证书
x509Certificate.verify(CA.getPublicKey());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
}
}
};
}
以上有两点需要说明:
1、关于CA证书检验服务器证书
如1所述,当客户端试图进行https请求的时候,客户端会先下载服务端证书到本地。对应到程序里面,下载完成之后的比对发生在public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException函数回掉里面。在这个回掉里面我们就用我们信任的CA对服务器证书(变量x509Certificate)进行检验。
检验分两部分,检验有效性x509Certificate.checkValidity();(证书是否过期等),检验可信性x509Certificate.verify(CA.getPublicKey());(是否由当前CA签发)。
2、关于请求地址检验
网站证书往往会指定域名,如果当前请求的域名与网站证书声明的域名不一致就会导致网络路径出错(类似上文《HTTPS(一)自签名https》提到的第9点“2、CA证明站点可信”)。
这个问题可以通过自定义HostnameVerifier解决。
04-02 03:30:54.040 4765-5375/com.thinking.loginsdkdemo I/yuyong: post start
04-02 03:30:54.043 4765-5375/com.thinking.loginsdkdemo I/yuyong: CA Pub key = OpenSSLRSAPublicKey{modulus=c185ab3ccc8db813322840f590492e1d4c7ee640e09ee4dd96be6421f982beea16dd55280ddacba03b2289c887e2363efe204ab7fc2225e5b146e218b9713d9d408408b112478e33cd6026da652a6769c6d335adedefa233ceb5e8f659e677dec1a3390c6bd3b7c83ab37f14a2f9eae1bd90957a5f3bce246810f987fcd0238436fc5a8ce433684b9ff252d93386db31beb3bbe79aa8cc1dc45cea38d3b02d8026f09ed536887670cdf9c87636ad81bc734d48708fc0a858ec4c2632d836ed0fdabc2ce5abbf58e4827b8a871397a8f350064825acbb181b94ddf0e1ce93d68a08ce9ab48efb5b437e0ee0cb4097d38fcc7cb9cc1e4b2a8e88c69ea59ac2dbc9,publicExponent=10001}
04-02 03:30:54.046 4765-5375/com.thinking.loginsdkdemo I/yuyong: ssl ready
04-02 03:30:54.101 4765-5375/com.thinking.loginsdkdemo I/yuyong: Server Pub key = OpenSSLRSAPublicKey{modulus=bc9cb769e597507bad28f629751892a2a38a5ff80c85a02d738053eb9d34222920b39cd4e50eaeb98b8e0b0085d47f95df9b6fb94b7ad5e7e6a1ccbc42cbba85e62f3dc3f86c6e7c5445ff3e34b58ab5b7b4c0a6d4cc2f76c0573cac0f11122adcbe84824c9a129eacf3011ab9127bd5cc44bf791a18586802a6a52ef95ec2d26435554fec1584cdb9ef7f89ccece0025e571b4c1d5df78684e7b287a8ec589221ace372bfefe93829b450b9791b5722b08eae2a94be32f05da764bde99bbe36843ffb3b698a00082932e3e33127374a860a51e0acc28d04de155c7a4a61e30af6704e622ea011f21cd507a8a2f65d8cc90f8f561917f9424207edd70b076381,publicExponent=10001}
04-02 03:30:54.105 4765-5375/com.thinking.loginsdkdemo I/yuyong: check host-->192.168.0.13
04-02 03:30:54.164 4765-5375/com.thinking.loginsdkdemo I/yuyong: hello this is get_post-->fuck you
结果显示可以征程运行,可以看到打印出来的“Server Pub key”值就是配置在nginx.conf
ssl_certificate /home/thinking/Desktop/test-proj/https/com.thinking.test.crt;
文件的值
设置好手机代理之后,用Fiddler尝试抓包
可以看到有两次http/https过程
第一次是证书下载,是普通的http,第二次是我们代码里所做的post过程,是https的。
而且,通过查看第二次请求的来回数据,发现消息详情也是可见的
但是,我们通过日志发现服务器证书被篡改了,而且public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException函数打印了异常
04-02 03:40:22.352 5671-5749/com.thinking.loginsdkdemo I/yuyong: post start
04-02 03:40:22.356 5671-5749/com.thinking.loginsdkdemo I/yuyong: CA Pub key = OpenSSLRSAPublicKey{modulus=c185ab3ccc8db813322840f590492e1d4c7ee640e09ee4dd96be6421f982beea16dd55280ddacba03b2289c887e2363efe204ab7fc2225e5b146e218b9713d9d408408b112478e33cd6026da652a6769c6d335adedefa233ceb5e8f659e677dec1a3390c6bd3b7c83ab37f14a2f9eae1bd90957a5f3bce246810f987fcd0238436fc5a8ce433684b9ff252d93386db31beb3bbe79aa8cc1dc45cea38d3b02d8026f09ed536887670cdf9c87636ad81bc734d48708fc0a858ec4c2632d836ed0fdabc2ce5abbf58e4827b8a871397a8f350064825acbb181b94ddf0e1ce93d68a08ce9ab48efb5b437e0ee0cb4097d38fcc7cb9cc1e4b2a8e88c69ea59ac2dbc9,publicExponent=10001}
04-02 03:40:22.358 5671-5749/com.thinking.loginsdkdemo I/yuyong: ssl ready
04-02 03:40:22.479 5671-5749/com.thinking.loginsdkdemo I/yuyong: Server Pub key = OpenSSLRSAPublicKey{modulus=d82dad8feabd5f491f0bfae3e045900c3d0961363b99139f4cdb24d2b14b312783ba963511c16960d709f678bc5c95134bfa1e3d2a142b07aac0179681213cab81f2608fdd0eaa2449cd20a5ede48e5724372637b8115cb55a185921cef0768cc454ac6270423d1f4139c8e0c82b08ff1141c8ba4c0ab403b38276cac4b826aae1426ca4b6b72b22456fc2f0806b6913d98cdbcaafd9c49a4c486d7dd092c684b301d29e92a0fe5ee25f50de165000eb42f8e15055f6982d946fd962adf14179ede4863c60340531ce31805fff09c00308ba9ba582d5c2f93be93890f827c2f7cbcebd7adefdc99e995e161c96ba22ce66534dc58b85cb44d02dd99b389589cd,publicExponent=10001}
04-02 03:40:22.480 5671-5749/com.thinking.loginsdkdemo D/OpenSSLLib: OpensslErr:Module:4(105:); file:external/boringssl/src/crypto/rsa/padding.c ;Line:114;Function:RSA_padding_check_PKCS1_type_1
04-02 03:40:22.480 5671-5749/com.thinking.loginsdkdemo D/OpenSSLLib: OpensslErr:Module:4(131:); file:external/boringssl/src/crypto/rsa/rsa_impl.c ;Line:507;Function:rsa_default_verify_raw
04-02 03:40:22.480 5671-5749/com.thinking.loginsdkdemo D/OpenSSLLib: OpensslErr:Module:11(6:); file:external/boringssl/src/crypto/x509/a_verify.c ;Line:122;Function:ASN1_item_verify
04-02 03:40:22.481 5671-5749/com.thinking.loginsdkdemo W/System.err: java.security.SignatureException
04-02 03:40:22.481 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.android.org.conscrypt.OpenSSLX509Certificate.verifyOpenSSL(OpenSSLX509Certificate.java:358)
04-02 03:40:22.481 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.android.org.conscrypt.OpenSSLX509Certificate.verify(OpenSSLX509Certificate.java:384)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.thinking.loginsdkdemo.SSLSocketFactoryBuilder$1.checkServerTrusted(SSLSocketFactoryBuilder.java:64)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:182)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:611)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:362)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.RealConnection.connectTls(RealConnection.java:299)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.java:268)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:160)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:256)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:134)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:113)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:42)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
04-02 03:40:22.482 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:125)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:200)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at okhttp3.RealCall.execute(RealCall.java:77)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at com.thinking.loginsdkdemo.MainActivity$2.run(MainActivity.java:104)
04-02 03:40:22.483 5671-5749/com.thinking.loginsdkdemo W/System.err: at java.lang.Thread.run(Thread.java:761)
04-02 03:40:22.493 5671-5749/com.thinking.loginsdkdemo I/yuyong: check host-->192.168.0.13
04-02 03:40:22.552 5671-5749/com.thinking.loginsdkdemo I/yuyong: hello this is get_post-->fuck you
这充分说明了Fiddler是通过中间人攻击窃取了我们的数据。防止这种情况也很简单,就在这个异常捕获的地方做处理就行了,即
catch (SignatureException e) {
e.printStackTrace();
}