前言
https利用PKI体系对web通信进行认证和加密,认证和加密的重要性不言而喻。在实现https过程中,证书校验这一环节起到至关重要的作用,但经常会出现各种问题。本文梳理了Android应用中校验https证书过程会出现的各种情况
首先简单说下证书校验逻辑:
如下图,网站使用的https证书存在某个证书路径下,最顶层的是全球公认的根证书(Root CA),接着是根证书CA签发的二级证书,然后是网站证书,即证书链结构。
(目前市场上全球信任的支持浏览器的 SSL证书主要有 3 种: EV SSL证书、OV SSL证书和 DV SSL证书)
其中根证书有很多个(也有不可靠、被攻击的根证书),证书层级也可以不止三层。
网站证书是否可信,必须要证明其在某个可信根证书链中。
Android app的https通信中,存在各种方式对待https证书
1、app忽略https证书错误
app在开发测试过程中使用测试版服务器资源,其https证书可能使用openssl自行生成,不存在于任何可信根证书链中,app加载https资源时会提升证书错误,于是研发便在代码层忽略证书错误
》忽略webview中的https证书错误
@Override
public void onReceivedSslError(WebView view,
SslErrorHandler handler, SslError error) {
handler.proceed();//忽略错误证书, 危险
//handler.cancel();
}
》忽略URLConnection中的https通信错误
//httpClient or UrlConnection使用自定义的X509TrustManager, 且getAcceptedIssuers返回为空,
会信任所有https, 危险
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
//TrustManager自定义
public MySSLSocketFactory(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
@Override
public Socket createSocket(Socket socket, String host, int port,
boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port,
autoClose);
}
public static SSLSocketFactory getSocketFactory() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory factory = new MySSLSocketFactory(trustStore);
return factory;
} catch (Exception e) {
e.getMessage();
return null;
}
}
}
//schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
//schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
HttpsURLConnection.setDefaultSSLSocketFactory(MySSLSocketFactory.getSocketFactory());
//HttpsURLConnection.setDefaultSSLSocketFactory(SSLSocketFactory.getSocketFactory());//安全的写法
代码中忽略了证书错误,导致“网络中间人”使用任何证书都可以进行https通信嗅探,https形同http
2、代码中实现SSL Pinning(证书绑定)
顶级根证书存在太多,而且有不可靠、被攻击的根证书。这样即使证书校验通过,也可能是“中间人”攻击者签发的证书,仍然可以嗅探通信
SSL Pinning:使用指定的N个证书链进行https证书校验过程,或指定证书hash值与网站证书对比
在TrustManager的checkServerTrusted函数中对比证书hash
这里提供另外一种使用本地证书库的方式
public class MySSLSocketFactory1 extends SSLSocketFactory {
public MySSLSocketFactory1(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);
}
public static SSLSocketFactory getSocketFactory(Context context) {
InputStream input = null;
try {
// //cer,der证书
// input = context.getResources().openRawResource(R.raw.fiddler);//
// CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
// Certificate cer = cerFactory.generateCertificate(input);
// KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
// keyStore.load(null, null);
// keyStore.setCertificateEntry("ca", cer);
//密钥库
KeyStore keyStore = KeyStore.getInstance("BKS");
InputStream in = context.getResources().openRawResource(R.raw.my);
try {
keyStore.load(in, "123123".toCharArray());
} finally {
in.close();
}
SSLSocketFactory factory = new MySSLSocketFactory1(keyStore);
return factory;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
input = null;
}
}
}
}
Android各个框架如何实现SSL pinning,参考https://medium.com/@appmattus/android-security-ssl-pinning-1db8acb6621e
(另外dns提供了更直接的解决方案,浏览器也有相应的HPKP解决方案
DNS CAA:通过DNS配置,对外声明哪个CA可以签发自己域名的SSL证书。防止中间ca作恶
https://blog.qualys.com/ssllabs/2017/03/13/caa-mandated-by-cabrowser-forum
)
3、配置文件中增加ssl pinning
代码中实现较麻烦,官方支持在Android 7/N(API24)版本直接在配置文件中控制证书校验
AndroidManifest.xml
res/xml/network_security_config.xml
还可以配置可信CA,强制https通信等
官方参考https://developer.android.com/training/articles/security-config
4、忽略用户/管理员安装证书
SSL Pinning在https证书过期阶段处理比较麻烦,一般app仅使用https通信,并不绑定证书
没有绑定证书,可以通过在手机上安装代理(fiddler)的证书,然后对app的抓包分析
但是Android 7.0(minSdkVersion>24)之后,Android系统默认不接受用户安装的证书,如下
想要分析app网络数据,需要root之后写入证书,或使用低版本手机,或者重打包app
(安装证书到system store:先安装到user store /data/misc/keystore,然后将文件复制到/etc/security/cacerts
https://nelenkov.blogspot.com/2011/12/ics-trust-store-implementation.html)
结语
使用https是必然趋势,且使用SSL pinning也是必然趋势,为了更安全