关于Android的https通讯安全
原文链接:http://pingguohe.net/2016/02/26/Android-App-secure-ssl.html
起因
前段时间,同事拿着一个代码安全扫描出来的 bug 过来咨询,我一看原来是个 https
通信时数字证书校验的漏洞,一想就明白了大概;其实这种问题早两年就有大规模的暴露,各大厂商App
也纷纷中招,想不到过了这么久天猫客户端里还留有这种坑;然后仔细研究了漏洞所在的代码片段,原来所属的是新浪微博分享 sdk 内部的,因为这个 sdk
是源码引用的,一直没有更新,年久失修,所以也就被扫描出来了。因此给出的解决方案是:
先获取最新的 sdk,看其内部是否已解决,已解决的话升级 sdk 版本即可;
第1步行不通,那就自己写校验逻辑,猫客全局通信基本已经使用 https 通信,参考着再写一遍校验逻辑也不是问题;
后来查了一下网上信息,早在2014年10月份,乌云
平台里就已经暴露过天猫这个漏洞,想必当时一定是忙于双十一忽略了这个问题。
虽然这个问题通过升级 sdk
解决了,但是这个问题纯粹是由于开发者本身疏忽造成的;特别是对于初级开发人员来说,可能为了解决异常,屏蔽了校验逻辑;所以我还是抽空再 review
了一下这个漏洞,整理相关信息。
漏洞描述
对于数字证书相关概念、Android 里 https 通信代码就不再复述了,直接讲问题。缺少相应的安全校验很容易导致中间人攻击,而漏洞的形式主要有以下3种:
自定义X509TrustManager
在使用HttpsURLConnection发起 HTTPS 请求的时候,提供了一个自定义的X509TrustManager,未实现安全校验逻辑,下面片段就是当时新浪微博 sdk 内部的代码片段。如果不提供自定义的X509TrustManager,代码运行起来可能会报异常(原因下文解释),初学者就很容易在不明真相的情况下提供了一个自定义的X509TrustManager,却忘记正确地实现相应的方法。本文重点介绍这种场景的处理方式。
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
//do nothing,接受任意客户端证书
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
//do nothing,接受任意服务端证书
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
自定义了HostnameVerifier
在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。如果回调内实现不恰当,默认接受所有域名,则有安全风险。代码示例。
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// Always return true,接受任意域名服务器
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);
信任所有主机名
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
修复方案
分而治之,针对不同的漏洞点分别描述,这里就讲的修复方案主要是针对非浏览器App,非浏览器 App 的服务端通信对象比较固定,一般都是自家服务器,可以做很多特定场景的定制化校验。如果是浏览器 App,校验策略就有更通用一些。
自定义X509TrustManager。前面说到,当发起 HTTPS 请求时,可能抛起一个异常,以下面这段代码为例(来自官方文档):
try {
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
private void copyInputStreamToOutputStream(InputStream in, PrintStream out) throws IOException {
byte[] buffer = new byte[1024];
int c = 0;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
}
它会抛出一个SSLHandshakeException的异常。
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:322)
at com.android.okhttp.Connection.upgradeToTls(Connection.java:201)
at com.android.okhttp.Connection.connect(Connection.java:155)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:276)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:211)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:382)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:332)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:199)
at com.android.okhttp.internal.http.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:210)
at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:25)
at me.longerian.abcandroid.datetimepicker.TestDateTimePickerActivity$1.run(TestDateTimePickerActivity.java:236)
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:318)
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:219)
at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:114)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:550)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:318)
... 10 more
Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
... 16 more
Android 手机有一套共享证书的机制,如果目标 URL 服务器下发的证书不在已信任的证书列表里,或者该证书是自签名的,不是由权威机构颁发,那么会出异常。对于我们这种非浏览器 app 来说,如果提示用户去下载安装证书,可能会显得比较诡异。幸好还可以通过自定义的验证机制让证书通过验证。验证的思路有两种:
方案1
不论是权威机构颁发的证书还是自签名的,打包一份到 app 内部,比如存放在 asset 里。通过这份内置的证书初始化一个KeyStore,然后用这个KeyStore去引导生成的TrustManager来提供验证,具体代码如下:
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// uwca.crt 打包在 asset 中,该证书可以从https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下载
InputStream caInput = new BufferedInputStream(getAssets().open("uwca.crt"));
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
Log.i("Longer", "ca=" + ((X509Certificate) ca).getSubjectDN());
Log.i("Longer", "key=" + ((X509Certificate) ca).getPublicKey();
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLSv1","AndroidOpenSSL");
context.init(null, tmf.getTrustManagers(), null);
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
(HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
这样就可以得到正确的输出内容:
<html>
<head>
<title>UW Services CA Test Page</title>
</head>
<body>
<h2>
UW Services CA test page
</h2>
<p>
<b>QUESTION</b>:
Did you arrive here without any security alerts or warnings?</p>
<ul>
<p>
<li>
<b>YES</b> - This test page uses a certificate issued by the
UW Services Certificate Authority. If you reached this page
without any alerts or warnings from your browser, you
have successfully installed the UW Services CA Certificate
into your browser.
<p>
<li>
<b>NO</b> - If your browser warned you about the validity of this
test page's security certificate, or the certificate
authority is unrecognized, you may not have successfully
installed the UW Services CA Certificate.
<p>
</ul>
<form action="https://www.washington.edu/computing/ca/" method=get>
<input type=submit value="Return to the Install Page">
</form>
</body>
</html>
如果你用上述同样的代码访问 https://www.taobao.com/ 或者 https://www.baidu.com/ ,则会抛出那个SSLHandshakeException异常,也就是说对于特定证书生成的TrustManager,只能验证与特定服务器建立安全链接,这样就提高了安全性。如之前提到的,对于非浏览器 app 来说,这是可以接受的。
方案2
同方案1,打包一份到证书到 app 内部,但不通过KeyStore去引导生成的TrustManager,而是干脆直接自定义一个TrustManager,自己实现校验逻辑;校验逻辑主要包括:
•服务器证书是否过期
•证书签名是否合法
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// uwca.crt 打包在 asset 中,该证书可以从https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下载
InputStream caInput = new BufferedInputStream(getAssets().open("uwca.crt"));
final Certificate ca;
try {
ca = cf.generateCertificate(caInput);
Log.i("Longer", "ca=" + ((X509Certificate) ca).getSubjectDN());
Log.i("Longer", "key=" + ((X509Certificate) ca).getPublicKey());
} finally {
caInput.close();
}
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLSv1","AndroidOpenSSL");
context.init(null, new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
for (X509Certificate cert : chain) {
// Make sure that it hasn't expired.
cert.checkValidity();
// Verify the certificate's public key chain.
try {
cert.verify(((X509Certificate) ca).getPublicKey());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}, null);
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
(HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
同样上述代码只能访问 certs.cac.washington.edu 相关域名地址,如果访问 https://www.taobao.com/ 或者 https://www.baidu.com/ ,则会在cert.verify(((X509Certificate) ca).getPublicKey());处抛异常,导致连接失败。
•自定义HostnameVerifier,简单的话就是根据域名进行字符串匹配校验;业务复杂的话,还可以结合配置中心、白名单、黑名单、正则匹配等多级别动态校验;总体来说逻辑还是比较简单的,反正只要正确地实现那个方法。
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
//示例
if("yourhostname".equals(hostname)){
return true;
} else {
HostnameVerifier hv =
HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname, session);
}
}
};
•主机名验证策略改成严格模式
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
原文链接:http://www.jianshu.com/p/7c1bc2daef8d
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
关于Android的https通讯安全的更多相关文章
- fiddler Android下https抓包全攻略
fiddler Android下https抓包全攻略 fiddler的http.https的抓包功能非常强大,可非常便捷得对包进行断点跟踪和回放,但是普通的配置对于像招商银行.支付宝.陌陌这样的APP ...
- Android进程间通讯
最近研究了一下Android进程间通讯,原来只是会用,但是只是会用是不行滴,就来研究一下. 刚开始看的时候,我的头是这么大,看了一夜的时候,头就变成这样了,,吓得宝宝赶紧上床休息了,. 先喝喝茶讲个故 ...
- 网络--三种网络通讯方式及Android的网络通讯机制
Android平台有三种网络接口可以使用,他们分别是:java.net.*(标准Java接口).Org.apache接口和Android.net.*(Android网络接口).下面分别介绍这些接口的功 ...
- Android组件的通讯——Intent
转载:Android组件的通讯-Intent 1.概述 一个应用程序的三个核心组件——activities.services.broadcast receivers,都是通过叫做intents的消息激 ...
- 深入理解HTTPS通讯原理
一.HTTPS简介 HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer),简单来讲就是加了安全的HTTP,即HTTP+SSL:我们知 ...
- Android进阶笔记04:Android进程间通讯(IPC)之Messenger
一. Android进程间通讯之Messenger 的引入 (1)引言: 平时一说进程间通讯,大家都会想到AIDL,其实messenger和AIDL作用一样,都可以进行进程间通讯.它是基于消 ...
- android中跨进程通讯的4种方式
转自:http://blog.csdn.net/lyf_007217/article/details/8542359 帖子写的很好.看来一遍,试了一遍,感觉太有意义.必须转过来! android中跨进 ...
- https通讯原理
https通讯原理 HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息.TLS/SSL协议不仅仅是一套加密传输的协议,更是一件经 ...
- Android蓝牙串口通讯【转】
本文转载自:http://blog.sina.com.cn/s/blog_631e3f2601012ixi.html Android蓝牙串口通讯 闲着无聊玩起了Android蓝牙模块与单片机蓝牙模块的 ...
随机推荐
- [OpenCV]在显示窗口中截图
[OpenCV]在显示窗口中截图 简介 介绍使用OpenCV实现简单的截图功能.首先阐述实现此功能的基本步骤,然后给出实现代码,最后贴出实验结果以及遇到的问题. 基本步骤 我们需要知道OpenCV使用 ...
- React Native自定义导航条
Navigator和NavigatorIOS 在开发中,需要实现多个界面的切换,这时候就需要一个导航控制器来进行各种效果的切换.在React Native中RN为我们提供了两个组件:Navigator ...
- Swift中switch强大的模式匹配
不少人觉得Swift中switch语句和C或C++,乃至ObjC中的差不多,此言大谬! 让本猫带领大家看一下Swift中switch语句模式匹配的威力. 所谓模式匹配就是利用一定模式(比如couple ...
- React Native之ListView实现九宫格效果
概述 在安卓原生开发中,ListView是很常用的一个列表控件,那么React Native(RN)如何实现该功能呢?我们来看一下ListView的源码 ListView是基于ScrollView扩展 ...
- 20 ViewPager demo5,6:FragmentAdapter 导航数据
Demo5 文件结构: MainActivity.java package com.qf.day20_viewpager_demo5; import java.util.ArrayList; impo ...
- Latex居中
居中文本 环境:\begin{center} 第一行\\第二行\\...第n行 \end{center}.可以用\\[长度]来插入可以省略的额外行间距.在一个环境内部,可以用命令\centering来 ...
- 实现memcpy函数
已知memcpy的函数为: void* memcpy(void* dest , const void* src , size_t count)其中dest是目的指针,src是源指针.不调用c++/c的 ...
- 【java虚拟机序列】java中的垃圾回收与内存分配策略
在[java虚拟机系列]java虚拟机系列之JVM总述中我们已经详细讲解过java中的内存模型,了解了关于JVM中内存管理的基本知识,接下来本博客将带领大家了解java中的垃圾回收与内存分配策略. 垃 ...
- Android简易实战教程--第十七话《自定义彩色环形进度条》
转载请注明出处:http://blog.csdn.net/qq_32059827/article/details/52203533 点击打开链接 在Android初级教程里面,介绍了shape用法 ...
- 编译GDAL支持ArcObjects
编译GDAL支持ArcObjects. 首先修改nmake.opt文件中对应的ArcObjects,修改后的如下所示: #uncomment to use ArcObjects ARCOBJECTS_ ...