https的握手过程

https在传输真正的数据之前,需要客户端和服务端进行一次协议握手,主要配置好两边的私钥和一些初始化工作,大致流程如下图:

android 发送emoji Android 发送https请求_服务端

注意区分第2步骤的证书个公钥,证书由专门的第三方证书机构颁发,一个证书生成后,包含有申请者的机构信息、组织机构、证书有效期和一串hash签名,除此之外还有证书自己的公私钥;上诉第二个步骤中的公钥是服务器的公钥,不要和证书的公钥混淆


使用中遇到的问题

最近项目需要使用https方式访问,项目是用retrofit+okhttp框架,需要把以前的http全部改为https访问;关于https访问配置的方式有三种

  • 信任所有证书
  • 验证某一个特定证书

信任所有证书

直接将以前的http改成https即可,不需要做任何改变,照理说第一种改变https即可,但是我却出现这种异常

Suppressed: javax.net.ssl.SSLHandshakeException: Handshake failed
04-17 18:09:07.892 3160-3160/com.chinamobile.iot.easiercharger W/System.err: 		... 36 more
04-17 18:09:07.892 3160-3160/com.chinamobile.iot.easiercharger W/System.err: 	Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x9e23da80: Failure in SSL library, usually a protocol error
04-17 18:09:07.892 3160-3160/com.chinamobile.iot.easiercharger W/System.err: error:100bd10c:SSL routines:ssl3_get_record:WRONG_VERSION_NUMBER (external/boringssl/src/ssl/s3_pkt.c:311 0xa92997f7:0x00000000)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err:     at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:353)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: 		... 35 more
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x9e23da80: Failure in SSL library, usually a protocol error
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: error:100bd10c:SSL routines:ssl3_get_record:WRONG_VERSION_NUMBER (external/boringssl/src/ssl/s3_pkt.c:311 0xa92997f7:0x00000000)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err:     at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:353)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: 	... 35 more

这种问题原因有多重,可能证书有问题,也可能是自己的代码有问题;我的项目出现这个问题的原因是:

未改变https之前,我的网络接口类似于这种

http://www.power.com:80

改成https后是这种

https://www.power.com:80

改了之后就出现上诉的错误,你们能看出问题所在吗?


那就是端口号


http协议默认使用的端口号是80,https默认的端口是443,而这里我改动https后端口号没变,所以这个域名是访问不通的,除非https通信端口号被改成了80才会通,解决办法就是端口号改成443即可解决

这是我使用第一种方式引发的问题,如果使用自签名的证书或者忽略所有证书,一旦你的端口号有误,也可能出现上诉问题,伙伴们,擦亮眼睛吧!

信任某一特定证书

获取证书

可以从网站上导出证书,具体方法可点击参考,导出比较简单,重要的是格式,博主在导出的.cer格式的证书后,反正asstes目录下,最后以stream方式打开,但是在okhttp使用这个证书文件验证时总出错,报错信息大致是code编码问题,思考了良久和查询了资料,最后确定原因是证书用在okhttp框架里去的时候要永城utf-8格式,以下是解决方案:

  1. 将cer文件里面的key原封不等的拷贝出来
public final static String SSL_KEY = "-----BEGIN CERTIFICATE-----\n" +
            "Fw0xODAxMTYwMDAwMDBaFw0yMDAyMTUxMjAwMDBaMIGdMQswCQYDVQQGEwJDTjEQ\n" +
            "MA4GA1UEBxMHQ2hlbmdkdTE4MDYGA1UEChMvQ2hpbmEgTW9iaWxlIElPVCBDb21w\n" +
            "YW55IExpbWl0ZWQgQ2hlbmdkdSBicmFuY2gxJjAkBgNVBAsTHUluZm9ybWF0aW9u\n" +
            "IFRlY2hub2xvZ3kgQ2VudGVyMRowGAYDVQQDExF3d3cudGF4aWFpZGVzLmNvbTCC\n" +
            "ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOryUl/OGkrUkqSzoimarOPv\n" +
            "V0qQ7EsyS1ny0UZ7jcxnFS7ztOLh/0XIaPvX4e2KWgdcgxL8LbQ/gFKeRr5s6Uub\n" +
            "QUeczE9+CO4ic5opzS76QVJVH0kTSBoB1HBJ0TAV3XhSt+SOF7T5bpJrcCdijw7X\n" +
            "-----END CERTIFICATE-----";
  1. 导入时要进行一次utf编码
InputStream[] key = new InputStream[]{new Buffer().writeUtf8(SSL_KEY_TEST).inputStream()};
  1. 进行证书验证
public static SSLParams setCertificates(InputStream... certificates)
    {
        SSLParams sslParams = new SSLParams();
        try
        {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates)
            {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

                try
                {
                    if (certificate != null){
                        certificate.close();
                    }
                } catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
            SSLContext sslContext = SSLContext.getInstance("TLS");

            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(keyStore);
            sslContext.init
                    (
                            null,
                            trustManagerFactory.getTrustManagers(),
                            new SecureRandom()
                    );

            sslParams.trustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
            sslParams.sSLSocketFactory = sslContext.getSocketFactory();
            return sslParams;
        } catch (Exception e)
        {
            e.printStackTrace();
        }
        return sslParams;
    }

成功后,可以去换一个错误的证书测试一下,不要尝试着去修改正确证书里面的内容,这样会造成第三步骤里面证书验证抛出异常,导致所有证书这块代码都没执行,信任所有证书;建议去其他https网站,按照上面步骤导出一个第三方的cer证书,最后用错误的证书测试会有以下几个字眼,就表示成功;

Trust anchor for certification path not found.

服务端验证

以上讲述的都是客户端验证服务端的证书,客户端只包含了服务端的公钥;而双向验证则会多了几点,详细请看下:

  1. 客户端发送自己的SSL版本信息、支持加密算法等信息
  2. 服务端收到后发送SSL版本信息、支持加密算法等信息,并且发送自己的公钥证书给客户端
  3. 客户端会验证公钥证书的合法性,CA机构颁布、过期或者是否信任
  4. 如果不通过将会终止
  5. 服务端会要求客户端发送客户端自己的公钥证书给服务端,收到后,服务端会对其证书进行验证
  6. 验证通过后,会获得客户端的公钥;至此,客户端有自己的私钥和服务端的公钥;服务端有自己的私钥和客户端的公钥
  7. 客户端会发送自己所支持的对称加密方案,服务端会选择一个加密程度最高的算法
  8. 选择后,会用客户端的公钥加密选择的方案,加密后发送给客户端
  9. 客户端用自己的私钥进行解密,并使用该加密方案产生初始的加密秘钥
  10. 客户端使用服务端的公钥加密该对称私钥,发送给服务端
  11. 服务端收到加密对称私钥后,用自己的私钥解密后会获得对称秘钥,
  12. 最后,两边都获得了对称秘钥,使用该秘钥进行通信
    单向认证没有服务端对客户端的证书验证,并且在第8步骤是明文发送加密方案

代码完成

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 
    //读取客户端公钥
    InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
    //keystore加载秘钥和密码
    keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
    ksIn.close();
    //初始化SSLContext
    SSLContext sslContext = SSLContext.getInstance(“TLS”);
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X.509");
    keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
    sslContext.init(keyManagerFactory.getKeyManagers(), null, null); 
 
    sslSocketFactory = sslContext.getSocketFactory();

证书验证

  • 获取证书管理类CertificateFactory和KeyStore
  • 使用证书管理类CertificateFactory和证书初始化Keystore
  • 获取证书信任管理类TrustManagerFactory,将Keystore里面的证书添加到证书信任管理类中
  • SSL上下文类SSLContext用证书信任管理类进行初始化
  • 将SSL添加到okhttpbuilder里面即可

HTTPS如何抵御中间人

所谓中间人,也就是在https握手阶段,分配对称秘钥时,对服务端,将自己伪装成客户端,对客户端伪装成服务端,使他们相互无感知,从而窃取、修改他们传递的内容。

详细来说,就是第二步,如果服务端将自己公钥以明文方式发送给客户端,被中间人拦截后,保存下来,自己在生成一对非对称加密公私钥,将自己的公钥发送给客户端,这样客户端完全不知道拿到的公钥不是服务端,客户端在用公钥加密自己对称加密的私钥,发送到服务端,再次被拦截,中间人用自己的私钥解密就拿到了客户端发送的私钥,这样就毫无安全可言了!

所以,问题的根本,如何保证收到的服务端的公钥确定是正确的,来自服务端的?

CA证书

CA证书由专门的机构颁发,这些是公开可信任的!服务端在部署HTTPS前,需要将自己的信息提供的CA证书机构,其中包括服务端公钥,域名,证书有效期,组织机构,地域,证书序列号,颁发机构等信息。这些信息正文,通过散列算法H,形成信息摘要,CA对生成的信息摘要,通过CA自身的非对称加密的私钥进行加密,得到密文,此密文称之为数字签名(CA对信息摘要进行了签名),信息正文、数字签名,放在一起,形成数字证书。
对于这些公开可信任机构颁发的证书,系统一般都集成了这些证书,也就是说客户端提前就存储了CA证书的公钥;证书验证的步骤

  1. 客户端向服务端发起HTTPS请求
  2. 服务端发送将证书信息正文和证书加密后的数字签名
数字签名,是证书的信息正文hash散列值后,再用CA的私钥加密后的签名
  1. 客户端拿到证书后,与系统中集成或者提前保存的CA信息进行对比,发现又相同的CA信息,就用该CA信息中心的公钥进行解密,获得到证书的信息正文的hash值;在将证书信息正文hash一次,与解密得到的hash对比,相等就说明是对的,反之则说明被串改了

中间人拦截到证书也知道CA公钥怎么办?

此时,若中间人拦截拿到证书后,也知道CA公钥的前提下,就算中间人修改证书内容还是替换证书内容,客户端拿到篡改的证书后,
一是解密用到的CA公钥是没办法解密中间人的东西,这是走不通的,因为CA公钥只能解密CA私钥加密的东西,而CA私钥中间人拿不到,服务端保存的
二是就算客户端用CA公钥解密成功后,他会发现hash值和明文的hash值是不等的,因为它篡改了内容

中间人篡改整个证书

假如A服务端申请了一个CA证书A,B也去弄一个CA机构颁发的证书B,然后B拦截A,将证书替换为自己的B证书,客户端拿到证书后怎么办?

例如,抓包工具Charles就是利用这一点,在抓取Https包时,我们一般客户端会先按照Charles的证书并且信任,这样Charles抓取到包后,使用自己的证书发送给客户端,客户端如果信任所有的证书情况下,是无感知的,相当于中间人完美拦截!

当然,解决办法就是不信任呗!不要手贱去安装一些第三方的证书,说不定什么时候就被攻击了!

如果不小心点了怎么办,一种办法是向本文前面那要,客户端固定某一个证书,解密后与客户端自己保存的对比,这样就解决了!

其次,证书机构颁发的一般都有网站信息,这点很简单一比就会发现不适自己的网站!校验域名!