Android安全开发之安全使用HTTPS

HTTPS通信原理

HTTPS是HTTP over SSL/TLS,HTTP是应用层协议,TCP是传输层协议,在应用层和传输层之间,增加了一个安全套接层SSL(Secure Sockets Layer,安全套接层)/TLS(Transport Layer Security,传输层安全),TLS与SSL在传输层对网络连接进行加密:
这里写图片描述
SSL/TLS层负责客户端和服务器之间的加解密算法协商、密钥交换、通信连接的建立。安全连接的建立过程如下所示:
这里写图片描述

数字证书、CA与HTTPS

信息安全的基础依赖密码学,密码学涉及算法和密钥,算法一般是公开的,而密钥需要得到妥善的保护,密钥如何产生、分配、使用和回收,这涉及公钥基础设施
公钥基础设施(PKI,Public Key Infrastructure)是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。公钥存储在数字证书中,标准的数字证书一般由可信数字证书认证机构(CA,Certificate Authority,根证书颁发机构)签发,此证书将用户的身份跟公钥链接在一起。CA必须保证其签发的每个证书的用户身份是唯一的。
链接关系(证书链)通过注册和发布过程创建,取决于担保级别,链接关系可能由CA的各种软件或在人为监督下完成。PKI的确定链接关系的这一角色称为注册管理中心(RA,Register Authority,也称中级证书颁发机构或者中间机构)。RA确保公钥和个人身份链接,可以防抵赖。如果没有RA,CA的Root 证书遭到破坏或者泄露,由此CA颁发的其他证书就全部失去了安全性,所以现在主流的商业数字证书机构CA一般都是提供三级证书,Root 证书签发中级RA证书,由RA证书签发用户使用的证书。
CA与RA补充解释:CA(Certification Authority,认证中心)中心,又称为数字证书认证中心,作为电子商务交易中受信任的第三方,专门解决公钥体系中公钥的合法性问题。CA中心为每个使用公开密钥的用户发放一个数字证书,数字证书的作用是证明证书中列出的用户名称与证书中列出的公开密钥相对应。CA中心的数字签名使得攻击者不能伪造和篡改数字证书。在数字证书认证的过程中,证书认证中心(CA)作为权威的、公正的、可信赖的第三方,其作用是至关重要的。认证中心就是一个负责发放和管理数字证书的权威机构。同样CA允许管理员撤销发放的数字证书,在证书废止列表(CRL)中添加新项并周期性地发布这一数字签名的CRL。RA(Registration Authority,注册审批机构),数字证书注册审批机构。RA系统是CA的证书发放、管理的延伸。它负责证书申请者的信息录入、审核以及证书发放等工作;同时,对发放的证书完成相应的管理功能。发放的数字证书可以存放于IC卡、硬盘或软盘等介质中。RA系统是整个CA中心得以正常运营不可缺少的一部分。RA作为CA认证体系中的一部分,能够直接从CA提供者那里继承CA认证的合法性。能够使客户以自己的名义发放证书,便于客户开展工作。
X509证书链,左边的是CA根证书,中间的是RA中间机构,右边的是用户:
这里写图片描述
www.google.com.hk 网站的证书链如下,CA证书机构是 GeoTrust Global CA,RA机构是 Google Internet Authority G2,网站的证书为 *.google.com.hk:
这里写图片描述
HTTPS通信所用到的证书由CA提供,需要在服务器中进行相应的设置才能生效。另外在我们的客户端设备中,只要访问的HTTPS的网站所用的证书是可信CA根证书签发的,如果这些CA又在浏览器或者操作系统的根信任列表中,就可以直接访问,而如12306.cn网站,它的证书是非可信CA提供的,是自己签发的,所以在用谷歌浏览器打开时,会提示“您的连接不是私密连接”,证书是非可信CA颁发的:
这里写图片描述
所以在12306.cn的网站首页会提示为了我们的购票顺利,请下载安装它的根证书,操作系统安装后,就不会再有上图的提示了。

自有数字证书的生成

HTTPS网站所用的证书可向可信CA机构申请,不过这一类基本上都是商业机构,申请证书需要缴费,一般是按年缴费,费用因为CA机构的不同而不同。如果只是APP与后台服务器进行HTTPS通信,可以使用openssl工具生成自签发的数字证书,可以节约费用,不过得妥善保护好证书私钥,不能泄露或者丢失。HTTPS通信所用的数字证书格式为X.509。

自签发数字证书步骤如下:

Step1:生成自己的CA根证书
生成CA私钥文件ca.key:

openssl genrsa -out ca_private.key 1024

生成X.509证书签名请求文件ca.csr:

openssl req -new -key ca_private.key -out ca.csr

在生成ca.csr的过程中,会让输入一些组织信息等。
生成X.509格式的CA根证书ca_public.crt(公钥证书):

openssl x509 -req -in ca.csr -signkey ca_private.key -out ca_public.crt

Step2:生成服务端证书
先生成服务器私钥文件server_private.key:

openssl genrsa -out server_private.key 1024

根据服务器私钥生成服务器公钥文件server_public.pem:

openssl rsa -in server_private.key -pubout -out server_public.pem

服务器端需要向CA机构申请签名证书,在申请签名证书之前依然是创建自己的证书签名请求文件server.csr:

openssl req -new -key server_prviate.key -out server.csr

这里写图片描述
对于用于HTTPS的CSR,Common Name必须和网站域名一致,以便之后进行Host Name校验。
服务器端用server.csr文件向CA申请证书,签名过程需要CA的公钥证书和私钥参与,最终颁发一个带有CA签名的服务器端证书server.crt:

openssl x509 -req -CA ca_public.crt -CAkey ca_private.key -CAcreateserial -in server.csr -out server.crt

如果服务器端还想校验客户端的证书,可以按生成服务器端证书的形式来生成客户端证书。
使用openssl查看证书信息:

openssl x509 -in server.crt -text -noout

用web.py搭建一个简单的服务器测试生成的server.crt,文件webpytest.py为:

import web
import web.wsgiserver 
import CherryPyWSGIServer
#服务器端证书设置
CherryPyWSGIServer.ssl_certificate = "/Users/xxx/tmp/https/cert/server.crt"
CherryPyWSGIServer.ssl_private_key = "/Users/xxx/tmp/https/cert/server_private.key"

urls = ("/.*","hello")
app = web.application(urls,globals())

class hello:
    def GET(self):
        return 'Hello, world!'
if __name__ = "__main__":
    app.run()

在本地运行web服务器程序:
python webpytest.py 1234
在safari浏览器中输入 https://0.0.0.0:1234 ,提示此证书无效(主机名不相符),因为在生成服务器端证书签名请求文件server.csr时,在Common Name中输入的是localhost,与0.0.0.0不符:
这里写图片描述
在safari浏览器中输入 https://localhost:1234 ,不再提示主机名不相符了,而是提示此证书是由未知颁发机构签名的,因为是私有CA签发的证书,私有CA不在浏览器或者操作系统的的根信任列表中:
这里写图片描述
还可用以下命令查看网站证书信息:

openssl s_client -connect localhost:1234

服务器端搭建成功,接下来讲Android客户端怎么和服务端进行HTTPS通信。

使用HttpsURLConnection进行HTTPS通信

Android官网给出了使用HttpsURLConnection API访问HTTPS的网站示例:

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

此方法的特点:
- 由Android系统校验服务端数字证书的合法性,用可信CA签发的数字证书的网站才可以正常访问,私有CA签发的数字证书的网站无法访问。
- 不能抵御在用户设备上安装证书(将中间人服务器的证书放到设备的信任列表中)进行中间人攻击,做此类攻击的一般是为了分析应用和服务器的交互协议,找应用和服务器的其他漏洞。
- 如果网站没有启用SSL site wide(use HTTPS only)或HSTS(HTTP Strict Transport Security)则无法抵御SSL Strip(HTTPS降级为HTTP)攻击,局域网攻击,如针对免费WiFi。
如果要使用私有CA签发的证书,必须重写校验证书链TrustManager中的方法,否则的话会出现:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found。

但是在重写TrustManger中的checkServerTrusted()很多开发者什么也没有做,会导致证书弱校验(没有真正校验证书)。
如下是错误的写法:

public static String getUnSafeFromServer(){
    String response = "";
    final String https_url = "https://certs.cac.washington.edu/CAtest/";
    new AsyncTask<String, Void, Boolean>(){
        @Override
        protected Boolean doInBackground(String... Params){
            try{
                TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManger(){
                        @Override
                        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException{
                        }
                        @Override
                        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException{
                            //未真正实现校验服务器端证书
                        }
                        @Override
                        public X509Certificate[] getAcceptedIssuers(){
                            return new X509Certificate[0];
                        }
                    }
                };
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, trustAllCerts, null);

                URL url = new URL(https_url);
                HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url.openConnection();
                httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
                //默认信任所有主机
                //httpsURLConnection.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
                final HostnameVerifier hostnameVerifier = new HostnameVerifier(){
                    @Override
                    public boolean verify(String s, SSLSession sslSession){
                        //未校验服务器证书的域名是否相符
                        return true;
                    }
                };
                httpsURLConnection.setHostnameVerifier(hostnameVerifier);
                InputStream in = httpsURLConnection.getInputStream();
                copyInputStreamToOutputStream(in, System.out);
                return true;
            }catch(Exception ex){
                ex.printStackTrace();
                return false;
            }
        }
        @Override
        protected void onPostExecute(Boolean result){
            if(!result){
                Log.d("tag","ssl error");
            }
        }
    }.execute(https_url);
}

正确的写法是真正实现TrustManger的checkServerTrusted(),对服务器证书域名进行强校验或者真正实现HostnameVerifier的verify()方法。
真正实现TrustManger的checkServerTrusted()代码如下:

@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException{
    if(x509Certificates == null){
        throw new IllegalArgumentException("Check Server x509Certificates is null");
    }
    if(x509Certificates.length<0){
        throw new IllegalArgumentException("Check Server x509Certificates is empty");
    }
    for(X509Certificate cert:x509Certificates){
        cert.checkValidity();
        //检查服务器证书签名是否有问题
        try{
            //和APP预埋的证书做对比
            cert.verify(serverCert.getPublicKey());
        }catch(NoSuchAlgorithmException e){
            e.printStackTrace();
        }catch(InvalidKeyException e){
            e.printStackTrace();
        }catch(NoSuchProviderException e){
            e.printStackTrace();
        }catch(SignatureException e){
            e.printStackTrace();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

其中serverCert是APP中预埋的服务器端公钥证书,如果是以文件形式,其获取为如下形式:

String certName = "uwca.crt";
InputStream certInput = new BufferedInputStream(getAssets().open(certName));
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate serverCert = (X509Certificate)certificateFactory.generateCertificate(certStream);

对服务器证书域名进行强校验:

httpsURLConnection.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

真正实现HostnameVerifier的verify()方法:

final HostnameVerifier hostnameVerifier = new HostnameVerifier(){
    @Override
    public boolean verify(String s, SSLSession sslSession){
        HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
        Boolean result = hv.verify("*.washington.edu",sslSession);
        return result;
    }
};
httpsURLConnection.setHostnameVerifier(hostnameVerifier);

另外一种写法证书锁定,直接用预埋的证书来生成TrustManger:

public static String getSafeFromServer(final InputStream certStream){
    //certStream为服务器端证书输入流
    String response = "";
    String httpsUrl3 = "https://certs.cac.washington.edu/CAtest/";
    final String httpsUrl = httpsUrl3;
    new AsyncTask<String,Void, Boolean>(){
        @Override
        protected Boolean doInBackground(String... Params){
            try{
                //以X.509格式获取证书
                CertificateFactory certificateFactory = CertificatedFactory.getInstance("X.509");
                Certificate cert = certificateFactory.generateCertificate(certStream);
                Log.d("tag","cert key:"+((X509Certificate)cert).getPublicKey().toString());

                //生成一个包含服务器端证书的KeyStore
                String keyStoreType = KeyStore.getDefaultType();
                Log.d("tag","keystore type:"+keyStoreType);
                KeyStore keyStore = KeyStore.getInstance(keyStoreType);
                keyStore.load(null, null);
                keyStore.setCertificateEntry("cert", cert);

                //用包含服务器端证书的KeyStore生成一个TrustManager
                String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
                Log.d("tag","tmfAlgorithm:"+tmfAlgorithm);
                TrustManagerFactory trustManagerFactory = TrustMangerFactory.getInstance(tmfAlgorithm);
                trustManagerFactory.init(keyStore);

                //生成一个使用我们的TrustManager的SSLContext
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, trustManagerFactory.getTrustManger(), null);

                URL url = new URL(httpsUrl);
                HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url.openConnection();
                httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
                InputStream in = httpsURLConnection.getInputStream();
                copyInputStreamToOutputStream(in, System.out);
            }catch(CertificateException e){
                e.printStackTrace();
            }catch(KeyStoreException e){
                e.printStackTrace();
            }catch(NoSuchAlgorithmException e){
                e.printStackTrace();
            }catch(IOException e){
                e.printStackTrace();
            }catch(KeyManagementException e){
                e.printStackTrace();
            }
            return true;
        }
    }.execute(httpsUrl);
    return response;
}

参数certStream是证书文件的InputSteam流:

try{
    String certName = "uwca.crt";
    InputStream certInput = new BufferedInputStream(getAssets().open(certName));
    MyHttpsURLConnectionClient.getSafeFromServer(certInput);
}catch(IOExcetion e){
    e.printStackTrace();
}

另外可以用以下命令查看服务器证书的公钥:

keytool -printcert -rfc -file uwca.crt

直接复制粘贴可以将公钥信息硬编码在代码中:

public static String UWCA_CERT = "-----BEGIN PUBLIC KEY-----\n" +
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYEzaN/OfuedBH1mmN8tj0tVMP=\n" +
Nisum2IQHKOGImFJhfKb5Q2fweX9Ouvi6s+NJE256wE/9cYWw4T8DmHvf8MZ498v\n" +
zes2Ok/ugy+g5Eb4QvugaL+XjjR0uJxU2rdWeVe1n+AoF68ZL5S7pntb0B5K/Qmp\n" +
5AnzqpVkptFLK1MlnQIDAQAB\n" +
-----END PUBLIC KEY-----";

可以用以下形式获取此公钥对应的X.509证书:

InputStream certStream = new Buffer().writeUtf8(MyCertString.UWCA_CERT).inputStream();
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate serverCert = (X509Certificate)certificateFactory.generateCertificate(certStream);

使用OKHttp3.0进行HTTPS通信

除了使用Android系统提供的HttpsURLconnection进行https通信,还有其他的第三方库可以使用,以OKhttp3.0为例,先看未校验服务器端证书链、未校验服务端证书域名的错误写法:

public static void getUnsafeOkHttpClient(){
    try{
        //不校验证书链
        final X509TrustManager[] trustAllCerts = new X509TrustManager[]{
            new X509TrustManger(){
                @Override
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException{
                    //不校验客户端证书
                }
                @Override
                public void checkServerTrusted(X509Certificate[] x509Certificates, String s)throws CertificateException{
                    //不校验服务器端证书
                }
            }
        };
        final SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, null);

        final HostnameVerifier hostnameVerifier = new HostnameVerifier(){
            @Override
            public boolean verify(String s, SSLSession sslSession){
                //未真正校验服务器端证书域名
                return true;
            }
        };
        OkHttpClient okHttpClient = new OkHttpClient.Builder().hostnameVerifier(hostnameVerifier).sslSocketFactory(sslContext.getSocketFactory(),trustAllCerts[0].build());
        String url = "https://certs.cac.washington.edu/CAtest/";

        Request request = new Request.Builder().url(url).build();
        Call call  okHttpClient.newCall(request);
        call.enqueue(new Callback()){
            @Override
            public void onFailure(Call call, IOException e){
                Log.d("tag","onFailure:"+e.getMessage());
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException{
                final String res = response.body().string();
                Log.d("tag", "onResponse:"+res);
            }
        });
    }catch(Exception ex){
        ex.printStackTrace();
        throw new RuntimeException(ex);
    }
}

这些错误的发生其实和HttpsURLConnection的其实相同,都涉及SSLContext和HostnameVerifier,聚安全应用扫描器都能扫出来这些潜在风险点,解决办法也和2.3 节相同使用HttpsURLConnection都是真正实现TrustManager和HostnameVerifier中的方法。

Webview的HTTPS安全

目前很多应用都用webview加载H5页面,如果服务端采用的是可信CA颁发的证书,在 webView.setWebViewClient(webviewClient) 时重载 WebViewClient的onReceivedSslError() ,如果出现证书错误,直接调用handler.proceed()会忽略错误继续加载证书有问题的页面,如果调用handler.cancel()可以终止加载证书有问题的页面,证书出现问题了,可以提示用户风险,让用户选择加载与否,如果是需要安全级别比较高,可以直接终止页面加载,提示用户网络环境有风险:

@Override
public void onReceivedSslError(WebView view, final SslErrorHandler, SslError error){
    final AlertDialog.Builder builder = new AlertDialog.Builder(MyWebviewActivity.this);
    Log.d("tag","error toString():"+error.toString());
    Log.d("tag","error getPrimaryError():"+error.getPrimaryError());
    SslCertificate sslCertificate = error.getCertificate();
    Log.d("tag","sslCertificate:"+sslCertificate.toString());
    switch(error.getPrimaryError()){
        case SslError.SSL_DATE_INVALID:
            Log.d("tag",SslError.SSL_DATE_INVALID+" ssl date invalid");
            break;
        case SslError.SSL_IDMISMATCH:
            Log.d("tag",SslError.SSL_IDMISMATCH+" hostname dismatch");
            break;
        case SslError.SSL_EXPIRED:
            Log.d("tag",SSL_IDMISMATCH+" cert has expired");
            break;
        case SslError.SSL_UNTRUSTED:
            Log.d("tag",SSL_UNTRUSTED+" cert is not trusted");
            break;
        case SslError.SSL_INVALID:
            Log.d("tag",SSL_INVALID+" cert is invalid");
            break;
        case SslError.SSL_NOTYETVALID:
            Log.d("tag",SSL_NOTYETVALID+" cert is not yet valid");
            break;
    }
    builder.setTitle("SSL证书错误");
    builder.setMessage("SSL错误码:"+error.getPrimaryError());
    builder.setPositiveButton("继续", new DialogInterface.OnClickListener(){
        @Override
        public void onClick(DialogInterface dialogInterface, int i){
            handler.proceed();
        }
    });
    builder.setNegativeButton("取消", new DialogInterface.OnClickListener(){
        @Override
        public void onClick(DialogInterface dialogInterface, int i){
            hander.cancel();
        }
    });
    final AlertDialog dialog = builder.create();
    dialog.show();
}

不建议直接用handler.proceed(),聚安全的应用安全扫描器会扫出来直接调用handler.proceed()的情况。
如果webview加载https需要强校验服务端证书,可以在 onPageStarted() 中用 HttpsURLConnection 强校验证书的方式来校验服务端证书,如果校验不通过停止加载网页。当然这样会拖慢网页的加载速度,需要进一步优化,具体优化的办法不在本次讨论范围,这里也不详细讲解了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值