HTTPS原理及OKHTTP对HTTPS的支持

3,986 阅读6分钟

HTTPS原理

我们先看一下定义,来自wikipedia的一个介绍:

HTTPS (also called HTTP over Transport Layer Security (TLS), HTTP over SSL, and HTTP Secure) is a communications protocol for secure communication over a computer network which is widely used on the Internet. HTTPS consists of communication over Hypertext Transfer Protocol (HTTP) within a connection encrypted by Transport Layer Security, or its predecessor, Secure Sockets Layer. The main motivation for HTTPS is authentication of the visited website and protection of the privacy and integrity of the exchanged data.

从这个定义中我们可以看出,HTTPS是包含了HTTP协议及SSL /TLS协议这两部分内容,简单的理解就是基于SSL/TLS进行HTTP的加密传输。HTTP是一个应用层的协议,定义了很多请求和响应方通信的遵循的规则,这部分内容可以从HTTP权威指南这部巨作中得到很详细的介绍,这里就不赘述了。其实主要还是想探讨一下SSL/TLS协议的一些具体细节,毕竟这是HTTPS区别于HTTP最大的地方,首先我们来看一下一个SSL/TLS完整的握手过程。

SSL/TLS握手过程

很复杂的交互过程,但是理解下来就是用非对称加密的手段传递密钥,然后用密钥进行对称加密传递数据。在这个握手过程中最重要的就是证书校验,其他就是正常的数据交互过程。如何校验一个证书合法有很大的文章,处理不好就会让你的网络失去了安全性。一个证书的校验,主要包括以下几个方面:

  • 第一,校验证书是否是由客户端中“受信任的根证书颁发机构”颁发;
  • 第二,校验证书是否在上级证书的吊销列表;
  • 第三,校验证书是否过期;
  • 第四,校验证书域名是否一致。

一天我们的QA妹子气愤愤的找到我说,为啥别人的APP可以用Charles抓到HTTPS的包,为啥我们的不能,我心中窃喜的告诉她只能说明我们技高一筹了。具体如何做到的后面我会分享一下我们的做法,先讨论一下Charles如何实现https的抓包的,这里面涉及到一个中间人攻击的问题。

一个针对SSL的中间人攻击过程如下:

image.png

中间人其实是做了一个偷梁换柱的动作,核心是如何欺骗客户端,从而让客户端能够放心的与中间人进行数据交互而没有任何察觉。我们来看Charles如何做到HTTPS抓包的,网上有很多Charles如何抓HTTPS包的教程,几步就搞定了,其中最核心的就是:

将私有CA签发的数字证书安装到手机中并且作为受信任证书保存

自签发一个证书实现上述二、三、四条校验规则很简单,要把这个证书安装到手机端信任列表必须得到用户的许可,这里不好绕过,但是鉴于大部分用户的网络安全意识比较差,有时也会稀里糊涂的信任了,那我们作为APP的开发人员,能否避免这种情况的发生呢?

其实也很简单,我们把服务端的证书内置在我们的APP里,我们在做服务端证书校验的时候只比对是否和这个证书完全相同,不同就直接抛错,那中间人便没有办法绕过证书进行攻击。但是这里面也有一个问题就是服务端的证书可能会过期或者升级,而且服务端往往为了提高网络的安全性,证书的有效时间不会设置太长,这样APP就会因为这个证书的事情频繁发版,也很痛苦。(前段时间我司IOS的APP就是因为授权企业用户的证书没有及时更新,导致大家无法正常打开APP,血的教训导致我们不想重走这条路)可能你又想到了,我们可以把证书配置在后端,有更新的时候直接去下载不就完了,那我们的证书下载没有没拦截的风险吗,一旦拦截,我们所有的证书校验都会失效,比直接信任手机内置的证书更可怕。我们既不想只信任我们服务器的证书,又不想信任手机上所有的 CA 证书。有个不错的的信任方式是把签发我们服务器的证书的根证书导出打包到APP中,这样虽然不能做到百分之百的证书无漏洞,但是相比于信任手机中几百个证书,我们只信任一个风险会小很多,这也就是我们的QA妹子用Charles抓不了我们的包的原因。~~~

OKHTTP

作为一个Android开发者,我们来看一下广泛使用的网络库OKHTTP对于HTTPS的支持。下面这段话摘自OKHTTP对于HTTPS的介绍中(地址请戳):

OkHttp attempts to balance two competing concerns:

  • Connectivity to as many hosts as possible. That includes advanced hosts that run the latest versions of boringssl and less out of date hosts running older versions of OpenSSL.
  • Security of the connection. This includes verification of the remote webserver with certificates and the privacy of data exchanged with strong ciphers.

几个与HTTPS相关的API:

SSLSocketFactory:

安全套接层工厂,用于创建SSLSocket。默认的SSLSocket是信任手机内置信任的证书列表,我们可以通过OKHttpClient.Builder的sslSocketFactory方法定义我们自己的信任策略,比如实现上面提到的我们只信任服务端证书的根证书,代码实现如下:

/**
     * 载入证书
     */
    public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) {
        try {
//用我们的证书创建一个keystore
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = "server"+Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                try {
                    if (certificate != null) {
                        certificate.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
//创建一个trustmanager,只信任我们创建的keystore
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            sslContext.init(
                    null,
                    trustManagerFactory.getTrustManagers(),
                    new SecureRandom()
            );
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

X509TrustManager:

public interface X509TrustManager extends TrustManager {
    void checkClientTrusted(X509Certificate[] var1, String var2) throws CertificateException;

    void checkServerTrusted(X509Certificate[] var1, String var2) throws CertificateException;

    X509Certificate[] getAcceptedIssuers();
}

checkServerTrusted方式实现了对于服务端校验,这里一般使用系统默认的实现,有些教程讲到这样配置ssl

private static synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
    try {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[]{
                new X509TrustManager() {
                    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

                    }

                    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    }

                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        }, null);
        return sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
        throw new AssertionError();
    }
}

千万不能这么做,这样将你是没有做任何校验的,这里推荐使用系统默认的,他会在校验过程中发现有异常直接抛出。

HostnameVerifier:

public interface HostnameVerifier {
    boolean verify(String var1, SSLSession var2);
}

这个接口主要实现对于域名的校验,OKHTTP实现了一个OkHostnameVerifier,对于证书中的IP及Host做了各种正则匹配,默认情况下使用的是这个策略。有时你遇到了一些奇怪的校验问题,大部分教程会教你这样:

OKHttpClient.Builder.hostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                })

其实这样你是完全放弃了hostname的校验,这也是相当不安全的。