Android HTTPS详解

来源:互联网 发布:欧元符号 mac 编辑:程序博客网 时间:2024/05/16 18:08

HTTPS原理

HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTP数据都是在SSL/TLS协议封装之上进行传输的。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也属于应用层协议。所以,研究HTTPS协议原理,最终就是研究SSL/TLS协议。

不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:

窃听风险:第三方可以获知通信内容。篡改风险:第三方可以修改通知内容。冒充风险:第三方可以冒充他人身份参与通信。

SSL/TLS协议是为了解决这三大风险而设计的,希望达到:

所有信息都是加密传输,第三方无法窃听。具有校验机制,一旦被篡改,通信双方都会立刻发现。配备身份证书,防止身份被冒充。

基本的运行过程:

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。但是这里需要了解两个问题的解决方案。如何保证公钥不被篡改?解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。公钥加密计算量太大,如何减少耗用的时间?解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。

因此,SSL/TLS协议的基本过程是这样的:

客户端向服务器端索要并验证公钥。双方协商生成“对话密钥”。双方采用“对话密钥”进行加密通信。

上面过程的前两布,又称为“握手阶段”。
握手阶段的详细过程

“握手阶段”涉及四次通信,需要注意的是,“握手阶段”的所有通信都是明文的。
客户端发出请求(ClientHello)

首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步中,客户端主要向服务器提供以下信息:

支持的协议版本,比如TLS 1.0版一个客户端生成的随机数,稍后用于生成“对话密钥”。支持的加密方法,比如RSA公钥加密。支持的压缩方法。

这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应用向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。
服务器回应(ServerHello)

服务器收到客户端请求后,向客户端发出回应,这叫做ServerHello。服务器的回应包含以下内容:

确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。一个服务器生成的随机数,稍后用于生成“对话密钥”。确认使用的加密方法,比如RSA公钥加密。服务器证书。

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供“客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。
客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁发,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项消息。

一个随机数。该随机数用服务器公钥加密,防止被窃听。编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。客户端握手结束通知,表示客户端的握手阶段已经结束。这一项通常也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项随机数,是整个握手阶段出现的第三个随机数,又称“pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把“会话密钥”。
服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的“会话密钥”。然后,向客户端最后发送下面信息。

编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发生的所有内容的hash值,用来供客户端校验。

握手结束

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用“会话密钥”加密内容。
服务器基于Nginx搭建HTTPS虚拟站点

之前一篇文章详细介绍了在服务器端如何生成SSL证书,并基于Nginx搭建HTTPS服务器,链接:Nginx搭建HTTPS服务器。
Android实现HTTPS通信

之前使用了HttpClient来实现HTTPS通信,而且代码中有大量无关代码,自己回顾看起来都特别混乱.所以,这里只列出HttpUrlConnection实现HTTPS通信的关键代码。
CA认证的数字证书网站

我们以百度的https网址(https://m.baidu.com/)为例,示例源码如下:

public void startHttpsConnection() {    HttpsURLConnection httpsURLConnection = null;    BufferedReader reader = null;    try {        URL url = new URL("https://m.baidu.com/");        httpsURLConnection = (HttpsURLConnection) url.openConnection();        httpsURLConnection.setConnectTimeout(5000);        httpsURLConnection.setDoInput(true);        httpsURLConnection.setUseCaches(false);        httpsURLConnection.connect();        reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));        StringBuilder sBuilder = new StringBuilder();        String line;        while ((line = reader.readLine()) != null) {            sBuilder.append(line);        }        Log.e("TAG", "Wiki content=" + sBuilder.toString());    } catch (MalformedURLException e) {        e.printStackTrace();    } catch (IOException e) {        e.printStackTrace();    } finally {        if (httpsURLConnection != null) {            httpsURLConnection.disconnect();        }        if (reader != null) {            try {                reader.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }}

由于百度是有CA授权的数字证书,所以这里我们就是简单的使用HttpsUrlConnection对其进行访问,就实现了HTTPS通信。
自签名的数字证书网站

由于CA认证是需要收费的,所以有些网站为了节约成本,采用自签名的数字证书,伟大的12306目前依然是这么干的。如果我们用上述代码访问自签名的网站会有什么问题呢?
截取一段crash信息如下:

04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:409)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.Connection.upgradeToTls(Connection.java:153)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.Connection.connect(Connection.java:114)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:298)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:259)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:89)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:161)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.genius.wzy.MainActivity.startHttpsConnection(MainActivity.java:58)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.genius.wzy.MainActivity$1.run(MainActivity.java:34)04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at java.lang.Thread.run(Thread.java:841)

可以看到,访问自签名证书的网站,Android直接会throw SSLHandshakeException,原因就是12306的数字证书不被Android系统的信任。想解决这个问题,有如下几种方法。
让HttpsURLConnection信任所有的CA证书

这是网上资源最多也是最不靠谱的解决方案。具体实现方法如下。

Step1. 实现X509TrustManager接口,在接口实现中跳过客户端和服务器端认证。

public class TrustAllCertsManager implements X509TrustManager {    @Override    public void checkClientTrusted(X509Certificate[] chain, String authType)            throws CertificateException {        // Do nothing -> accept any certificates    }    @Override    public void checkServerTrusted(X509Certificate[] chain, String authType)            throws CertificateException {        // Do nothing -> accept any certificates    }    @Override    public X509Certificate[] getAcceptedIssuers() {        return new X509Certificate[0];    }}

Step2. 实现HostnameVerifier接口,不进行url和服务器主机名的验证。

public class VerifyEverythingHostnameVerifier implements HostnameVerifier {    @Override    public boolean verify(String hostname, SSLSession session) {        return true;    }}

Step3. 基于上面实现的TrustAllCertsManager修改HttpsURLConnection类的默认SSL socket factory。

TrustManager[] trustManager = new TrustManager[] {new TrustEverythingTrustManager()};SSLContext sslContext = null;try {    sslContext = SSLContext.getInstance("SSL");    sslContext.init(null, trustManager, new java.security.SecureRandom());} catch (NoSuchAlgorithmException e) {    // do nothing}catch (KeyManagementException e) {    // do nothing}HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

Setp4. 实例化HttpsUrlConnection,并设置HostnameVerifier为上面实现的VerifyEverythingHostnameVerifier。

httpsURLConnection = (HttpsURLConnection) url.openConnection();httpsURLConnection.setHostnameVerifier(new VerifyEverythingHostnameVerifier());

上述四个步骤,就可以让你无障碍的访问自签名的HTTPS网站了,例如12306。但是,这种方式虽然简单,但是会导致严重的安全问题,例如臭名昭著的中间人攻击。

中间人攻击    虽然上述方案使用了HTTPS,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。

所以,千万不要在生产代码中使用上述方法解决HTTPS无法连接的问题。
让HttpsURLConnection信任指定的CA证书

为了防止上面方案可能导致的“中间人攻击”,我们可以事先下载服务器端公钥证书,然后将公钥证书编译到Android应用中,由应用自己来验证证书。也就是我们来教会HttpsUrlConnection来认识特定的自签名网站。还是以12306网站为例。

Step1. 下载12306的服务器公钥证书

12306提供了公钥的下载地址:12306根证书下载地址

Step2. 将下载的证书放到应用的assets目录下.

app->src->main->assets->srca.cer
(ps:使用Android Studio的同学需要特别注意默认asserts目录的位置)。

Setp3. 构造特定的TrustManager[]数组.

private TrustManager[] createTrustManager() {    BufferedInputStream cerInputStream = null;    try {        // 获取客户端存放的服务器公钥证书        cerInputStream = new BufferedInputStream(getAssets().open("srca.cer"));        // 根据公钥证书生成Certificate对象        CertificateFactory cf = CertificateFactory.getInstance("X.509");        Certificate ca = cf.generateCertificate(cerInputStream);        Log.e("TAG", "ca=" + ((X509Certificate) ca).getSubjectDN());        // 生成包含当前CA证书的keystore        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());        keyStore.load(null, null);        keyStore.setCertificateEntry("ca", ca);        // 使用包含指定CA证书的keystore生成TrustManager[]数组        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);        tmf.init(keyStore);        return tmf.getTrustManagers();    } catch (CertificateException e) {        e.printStackTrace();    } catch (IOException e) {        e.printStackTrace();    } catch (KeyStoreException e) {        e.printStackTrace();    } catch (NoSuchAlgorithmException e) {        e.printStackTrace();    } finally {        if (cerInputStream != null) {            try {                cerInputStream.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }    return null;}

Step4. 初始化SSLContext.

SSLContext sc = SSLContext.getInstance("SSL");TrustManager[] trustManagers = createTrustManager();if (trustManagers == null) {    Log.e("TAG", "tmf create failed!");    return;}sc.init(null, trustManagers, new SecureRandom());URL url = new URL("https://kyfw.12306.cn/otn/login/init");HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

参考文献

[1] http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html

1 0
原创粉丝点击