Android程序正确安全的使用https协议

前言

    现在绝大多数Android程序开发中都会涉及到与服务端的通讯,在app与服务端程序通讯过程中绝大多数又会采用http/https作为通讯协议,因为它被广泛使用,并且在两端都有成熟可靠稳定易用的库来支持。在日常的业务迭代过程中,大多数开发者是不关心http/https背后的细节的,基本上用okhttp+retrofit直接封装一个http/https接口就可以直接访问服务器了,并且正确的拿到服务端返回数据。那么这样真的足够了么?尤其是https的情况,这样真的可以100%保证安全了么?下面逐步分析一下https背后的细节,以及Android程序开发中如何做到https接口访问如何尽可能的保证安全。

why https

    估计用过不知名小区宽带运营商的人可能都有过访问正规网站弹不正规小广告的经历。即使是大运营商有时候也会莫名其妙的在访问网页的时候弹出一些广告信息,或者运营商的一些活动信息。这就是运营商劫持了你访问网页的内容,强行加入了它自己的广告代码。而这成为可能的基础是用户发起网络请求的协议是http协议。http协议是明文协议,当用户访问网页时,请求和返回的网页html代码都是明文传输的,这对于运营商来说很容易修改返回的数据。这个大多数人应该都清楚。所以尽量使用https就可以有效避免这个问题。原因简单来说就是https传输是加密的密文,运营商无法解密,也就无法篡改数据。这几乎对于开发者来说已经是常识了,但细节呢?

https如何进行加密传数据的

    简单来说就是用非对称加密的方式协商传输密钥,然后用客户端和服务端双方协商好的密钥进行对称加密传输真正的数据。这个过程就是TLS/SSL。传输层安全性协议(Transport Layer Security,即TLS),SSL是之前的旧称(Secure Sockets Layer)。这个加密层来对http传输的数据进行加密,对于上层http协议来说,一切都没有什么区别。这个过程需要解决一个核心问题就是客户端要确定我与之通讯的服务端是我想要与之通讯的服务端,即对方是真的。这个问题可能由于dns劫持等情况,将客户端的域名解析到一个恶意网址,这样无论两端如何加密,客户端的数据都暴露给了恶意服务。所以保证传输安全的首要也是最重要的关键是如何认证服务端的真实身份。说清楚这些问题要进行一系列的概念解释:

非对称加密

    通常来讲是可以生成一对用于加解密的钥匙,一个称为公钥,一个称为私钥。数据用公钥加密,用私钥可以解密;反之亦然。常用的非对称加密算法是RSA,一种基于大数质因数分解极度困难的数学原理构建的算法,不赘述了。整个TLS的认证和密钥交换等重要环节都是基于非对称加密来实现的。

数字摘要(hash)

    程序员应该都知道,无论数据源长度如何,都可以用一个确定长度的数表示这个数据源,此过程不可逆。常用算法md5,sha-256等。

数字签名

    数字签名涉及到hash和私钥加密,公钥解密,具体的过程是:发送数据一方要防止数据被恶意篡改,所以用某一种hash算法(如SHA-256)算出要发送数据的摘要,并且用自己的私钥(非对称加密,如RSA算法生成的私钥)对这个摘要进行加密。最终发送方将要发送的数据和对数据的摘要(加密后的)一起发送给接收方。接收方接到数据后用相同的hash算法计算出数据源的hash值,并且用公钥(公钥如何分发到接收方,后续可知)解密之前的hash值密文,两个hash值如果相等说明数据未被篡改,反之数据被篡改。图:


Digital_Signature_diagram_zh-CN.svg.png
数字证书

    数字证书实际就是TLS过程第一步要传递数据,服务端会把自己的数字证书发送到客户端,让客户端验证自己是否是可信任的服务端。那么数字证书都包含什么数据,而客户端又如何选择是否信任它呢?图:

dstcert.png

上图是维基百科的数字证书,它包含了一些标识自己身份的信息,重要的信息是维基百科服务端所用的非对称加密所用的公钥,这个公钥就是客户端后续要用来加密对称加密密钥的关键。另外更重要的信息是对证书中的所有信息的一个数字签名。签名讲过要对证书进行验证,需要用一把公钥解密数字签名,才能得到它代表的hash值,这样才能验证是否数据被篡改。那么公钥在哪里?公钥藏在其他数字证书里,确切的说,公钥在签发这张数字证书的数字证书里。这就要涉及到证书链了,在解释证书链之前,先看一下数字证书是如何形成的(签发)。更多关于数字证书的解释参见:https://zh.wikipedia.org/wiki/%E5%85%AC%E9%96%8B%E9%87%91%E9%91%B0%E8%AA%8D%E8%AD%89

CA及数字证书的签发

    数字证书认证机构(英语:Certificate Authority,缩写为CA),简单的说有这些称为CA的机构(很多是商业公司)具有向外发放数字证书的权威(大家都信,并且还为之埋单)。签发证书除了标识你的身份,你要用证实可靠的域名信息等等外(刚才介绍的数字证书的信息),最大的作用, 我的理解,就是为你生成公钥,而它(CA)保留了对应的私钥,并且用私钥生成给你签发证书的数字签名。数字证书的来源最终都会追溯到CA(被大家接受的可信的数字证书,自签发不算)。而处于管理方便,CA还会签发一些中介证书,这些中介证书可以签发中介证书,也可以签发最终用户需要的证书。而这样一层层的签发,以及验证回溯,就是证书链。

证书链验证

    我们回到TLS第一个步骤,服务端下发给客户端一个证书信息,这时候,大多数情况下,这个证书信息不止包含一个证书,而是一串证书。这些证书都是父子关系,最下层的证书由上层证书签发,而上层证书又是由更上层的证书签发。证书签发的尽头在哪里?答案是根证书。根证书只是一个概念,稍后详述。现在解释一下这一串证书如何发生关系以及如何验证合法性的。
    客户端收到一串证书信息后,会从最底层的证书开始向上验证证书合法性。程序首先找到底层证书的签发者,通过签发者信息定位到签发者证书(都已经下发到客户端了),定位到签发者证书后,在定位签发者的签发者证书,如此向上递归,最终到达根证书,根证书的签发者是自己,所以递归到此结束,程序取得根证书中携带的公开密钥,然后掉头回去验证下一级证书的数字签名,往复这个过程,最终到底层证书位置,如果数字签名都能对的上,一次证书链验证就顺利结束了,客户端信任了服务端下发的底层证书,并且取得它的公开密钥,将用此密钥加密双方协商的对称加密密钥,以后的事情就容易的多了。图:


cachain.png
根证书

    上面步骤的基石就是所谓的根证书,它的概念跟普通数字证书没有区别,唯一的区别就是它的签发者就是它本身。至于为什么CA要搞出一堆中间的中介证书,这个不是十分明确。但不管怎样,最终都会回溯到各CA签发的根证书。所有根证书都是默认被大众认可的,人为是安全可靠的。根证书在上述过程中并不传输给客户端,它被操作系统厂商、浏览器厂商等内置到系统或者程序中,作为信任基石存在。也就是说作为客户端,并不需要关心根证书在哪里,它可不可信,这些都交给操作系统就好了。正是有了最终这个保证,前面的所有环境都有了验证的标准,使TLS得以被广泛的应用,从而构建起了我们现在世界上的安全数据通讯。图:


rootcat.png

    总结下,正是基于上述的技术,客户端基于预装于程序中(浏览器)或者操作系统中的默认被信任的根证书完成链式的认证,最终信任由服务端下发给客户端的数字证书,基于此证书包含的公钥进行非对称加密完成对称加密所需的密钥的秘密传输。

中间人攻击

    一切几乎无懈可击,但是还存在一点瑕疵,这就是存在中间人攻击的可能。对于了解中间人攻击的人来说,数字证书就是为了防止中间人攻击的有效手段,那怎么还有可能存在中间人攻击呢?我们这里只讨论android app的情况。我们这里所说针对https的中间人攻击实际上应该加上一个引号,类似于中间人攻击。那么要实现这种攻击需要具备什么条件呢?一个最重要的条件就是要将非CA根证书预装到手机中,从而在app访问一个恶意https地址时,能够自建一个证书链验证,欺骗客户端这是一个有效服务器,从而完成后续的恶意操作。这个条件太过苛刻,几乎不存在大规模爆发的情况,而且预装证书到手机这个动作极难完成,所以这里说的风险实际上指的是防止app的业务数据逻辑和特征被其它开发者窥探,说白一点就是不想让别人调试分析你的接口访问。这个对于某一些极度敏感的app非常重要,比如涉及到金钱交易的app。
    上面所说的在android开发中常常会用到,就是利用charles、fiddler这样的抓包工具去抓取https请求。而抓取https请求的操作步骤就是安装上述软件自签发的根证书到手机中。

app如何应对

    上面的情况对于app如何做对应的防范呢?对于android开发来说,可以采取app内置完整的数字证书链,绕过系统的数字证书验证,只信任给自己服务端签发的数字证书,这样就能有效的防止手机中安装了非CA根证书的验证风险。

    private static SSLSocketFactory getSSLSocketFactory() {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            final int[] ids = new int[]{R.raw.cert1, R.raw.cert2, R.raw.cert3, R.raw.cert4};
            for (int i = 0; i < ids.length; i++) {
                keyStore.setCertificateEntry("cert" + i, loadCertificate(ids[i]));
            }
            TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            factory.init(keyStore);
            SSLContext ssl = SSLContext.getInstance("TLS");
            ssl.init(null, factory.getTrustManagers(), null);
            return ssl.getSocketFactory();
        } catch (Exception e) {
            return null;
        }
    }

    private static X509Certificate loadCertificate(int id) {
        InputStream in = null;
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            in = BaseApplication.getContext().getResources().openRawResource(id);
            X509Certificate cert = (X509Certificate) cf.generateCertificate(in);
            return cert;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }
    }

上面代码是读取raw资源中的证书链,将他们设置进入keystore中,然后用keysotre初始化TrustManagerFactory,最终得到SSLSocketFactory实例。这个实例可以备用做初始化okhttp,也可以用作初始化httpurlconnection库。经过这样的处理,你的app就只能认证你自己服务器下发的证书了,任何其他网站的证书都不会通过你的okhttp或者httpurlconnection库发起请求的验证。这样其他开发这拿到你开发的app,想要通过安装一个证书抓取app的https请求就被拒绝了,对于安全性要求极高的app来说,这一点防御也尤为重要。
    很多需求可能需要app内嵌webview来展示H5页面,那这个时候也需要注意https证书验证相关的一个安全问题,即当我们自定义webviewClient时(webView.setWebViewClient(webviewClient)),需要继承WebViewClient并重写其中的一些方法,重写onReceivedSslError方法时一定不要直接调用handler.proceed(),这样会让证书验证错误被忽略,使webview继续访问证书有问题的站点,这就存在极大的安全隐患。正确的做法是检测到证书验证失败,弹出对话框询问用户下一步的处理,根据用户选择决定是否忽略掉不合法的证书验证。

@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
    final AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage("证书验证失败,继续访问可能存在安全风险,是否继续?");
    builder.setPositiveButton("continue", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            handler.proceed();
        }
    });
    builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            handler.cancel();
        }
    });
    final AlertDialog dialog = builder.create();
    dialog.show();
}

结束

    在android7.0后(包括7.0),系统认证证书链的逻辑发生了变化,默认不再相信用户自己安装的根证书,如果要信任用户安装的根证书需要在app中添加一些权限声明代码。这样的改动无疑提升了安全性,使上述的抓包分析行为在7.0以上系统变的更加困难,提升了app的安全性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容