浅谈SSL/TLS协议及其实现

开篇感谢大神们指引方向,参考资料:

  1. https://www.paolotagliaferri.com/an-overview-of-ssl-tls-secure-sockets-layer-transport-layer-security-tls-1-2/
  2. https://www.paolotagliaferri.com/overview-of-transport-layer-security-protocol-tls-1-3/
  3. https://tls13.ulfheim.net/
  4. 《图解密码技术》
  5. https://tools.ietf.org/html/rfc8446#section-4.6.1

        Web应用通常使用HTTP协议在用户和Web服务器间传输信息。HTTP协议不对传输的数据加密,因此当我们通过HTTP协议访问网页和发送内容时,网络中任何可获取到(通过转发、嗅探等方式)HTTP流量的中间者都可以知晓传输内容。早期,Web应用的主要形式还是用户通过浏览器浏览网站发布的公开信息,这看起没有问题,但随着网站服务越来越多样化,用户需要和网站交互私密信息,如网购时传输的银行卡信息。为了防止其他人获取HTTP流量的内容,产生了SSL/TLS协议,SSL/TLS协议加密HTTP协议的内容,让中间者获取到的是加密后内容。SSL/TLS位于TCP/IP模型TCP的上一层,也可以用来加密传输其他上层协议的数据内容,如SMTP、POP3和FTP,SSL/TLS加上HTTP就是HTTPS。

    本文内容冗长,大家可以选择自己感兴趣的部分阅读,导航如下:

  1. SSL/TLS简介
  2. TLSv1.3协议的规范
  3. 基于Java的协议实现

一、SSL和TLS       

        SSL全称安全套接层(Secure Socket Layer),TLS全称传输层安全(Transport Layer Security)。最早网景公司设计了SSL,当SSL发展到3.0时,IETF(Internet Engineering Task Force)修复了SSL 3.0的漏洞,并在其基础上设计发布了TLS 1.0,也可将其看作SSL 3.1。至今,TLS已经发展到了版本1.3(RFC 8446 -> https://tools.ietf.org/html/rfc8446)。各个版本的SSL和TLS的发布时间如下:

        SSL 1.0 – 由于安全问题未公开发布。

        SSL 2.0 – 1995年发布,2011年弃用。存在公开的安全漏洞。

        SSL 3.0 – 1996年发布,2015年弃用。存在公开的安全漏洞。

        TLS 1.0 – 1999年发布,计划2020年弃用。

        TLS 1.1 – 2006年发布,计划2020年弃用。

        TLS 1.2 – 2008年发布。

        TLS 1.3 – 2018年发布。

        因存在安全问题,SSL基本上已被弃用。很多应用场景中提及SSL实际上就是指TLS,SSL证书(SSL Certificate)实际上是SSL/TLS证书,同时支持SSL协议和TLS协议。本文如未指明协议版本,则为TLS 1.3。

二、协议的规范

1、TLS 1.3

        TLS协议位于HTTP(或其他应用层协议,如SMTP)协议与TCP协议之间,TCP协议及其下层协议都不对传输的数据进行加密,也不对通信双方进行认证。TLS的作用是:1)保护传输数据的机密性(Confidentiality),即使第三方截获了通信数据也无法获知真实内容;2)对服务方或者双方的身份进行认证(Authentication);3)受保护的数据发出后不被篡改(Integrity),至少第三方篡改了数据内容,通信双方能够发觉。

        通过使用现有密码技术(如对称密码、公钥密码、证书、消息认证码等)可以满足上诉三个需求,但是一种密码技术有多种算法,如对称加密算法就有DES、三重DES、AES等,分组密码模式就有ECB、CBC、CFB等,具体使用何种算法需要双方约定。因此,双方除了需要确定协议的版本,还需要协商具体使用的加密算法和参数,即密码套件(Cipher Suite)。接下来需要确认对方的身份,TLS强制客户端认证服务器,但不强制服务器认证客户端(如果服务器需要认证客户端,可向客户端发送CertificateRequest请求)。为了保护上层应用(如HTTP、SMTP)数据的机密性,兼顾加密效率,一般使用对称秘钥加密应用数据,因此还需要协商一个共享的加密秘钥。这些都在TLS的握手协议中完成,握手协议结束后,客户端和服务器就可以使用共享的秘钥和协商好的加密算法加密并发送应用数据。

        总的来说TLS握手协议主要实现三个目的:协商密码套件,协商秘钥和认证。接下来介绍TLS握手协议的过程,下图是客户端和服务器首次握手的过程。

TLS1.3握手过程

        客户端先向服务器发起Client Hello访问请求。Client Hello携带的信息有:1)客户端随机数,2)支持的密码套件清单,2)秘钥交换算法及其公钥,3)支持的协议版本。

       秘钥交换是通信双方协商共享秘钥的过程,需要通过数学技术来实现,如图中的X25519(Curve25519)椭圆曲线和我们熟悉的Diffie-Hellman。通信双方各自计算一对公私钥,将公钥发送给对方,自己保存私钥且不能告知他人,双方通过自己的私钥和对方的公钥可以计算出同一个密码数字,这一密码数字可直接作为或间接生成对称密码的秘钥。没有私钥的中间者即使截获了公钥也无法计算出这个密码数字。在发送Client Hello之前,客户端计算用于密钥交换的公私钥,并将共钥放在Client Hello中发送给服务器。

        服务器收到客户端的Client Hello后,会回复一个Server Hello,包含的信息有:1)服务器随机数,2)选择的密码套件,3)秘钥交换的公钥,4)选择的协议版本。

        至此,服务器和客户端都拥有了计算共享秘钥的全部信息,发送Change Cipher Spec后,就可加密通信内容了(上图握手过程中双线框中内容已加密)。

        那么此后用于加密内容的共享对称密钥又是如何通过这些信息计算得到呢?由于作者是个数学渣渣,本文只能作简单介绍。服务器或客户端收到对的公钥后,再结合自己的私钥,使用算法(如Diffie-Hellman)计算得到同一个共享秘钥。将共享秘钥以及Client Hello和Server Hello的哈希值(服务器和客户端随机数在此可能发挥了盐的作用,防止计算出相同的哈希值)输入给生成会话秘钥的算法,如HKDF-Extract和HKDF-Expand-Label。服务器和客户端得到相同的handshake secret、client handshake traffic secret、server handshake traffic secret、client handshake key、server handshake key、client handshake IV和server handshake IV。此后握手协议中发送的证书和结束消息都是用此处生成的握手协议会话秘钥加密的,即上图中的(4)和(6)。

        计算完握手协议会话秘钥后,服务器会将表明自己身份的证书(Server Certificate),用证书私钥加密的握手消息哈希值(Certificate Verification)和握手协议结束消息(Server Finish)等内容打包,并用server handshake key和server handshake IV加密发送给客户端。客户端收到后解密,得到证书中的公钥。用该公钥解密Certificate Verification并验证其值是否正确,可以确认服务器是否拥有证书的私钥,从而实现了对服务器身份的认证。服务器发送的Server Finish的内容为用server handshake traffic secret加密的从图中(1)到(4)所有的握手协议消息的哈希值。

        客户端切换密码后也会给服务器发送一个用client handshake key 和client handshake IV加密的客户端握手协议结束消息(Client Finish),该消息的内容为用client_handshake_traffic_secret加密的从图中(1)到(5)所有握手协议消息的哈希值。服务器和客户端发送的握手结束消息可用于验证秘钥交换是否成功。

        秘钥交换成功后,服务器和客户端就会使用同样的算法和参数计算加密应用数据的会话秘钥。将计算握手协议会话秘钥时得到的handshake secret和上图中(1)到(4)所有握手协议消息的哈希值(SHA256)输入给生成会话秘钥的算法,得到client application key、server application key、client application IV和server application IV。

        此后,服务器用server application key和server application IV加密发送给客户端的应用数据,客户端用client application key和client application IV加密发送给服务器的应用数据。

        注意,上述整个过程中进行了两次会话秘钥计算,分别得到了加密后续握手协议内容(图中(4)和(6))的会话秘钥和加密应用数据(Application Data)的会话秘钥。Client Finish和Server Finish被两层加密,如上图过程(4),客户端需要先用server handshake key和server handshake IV解密整个包,然后用server handshake traffic secret解密被加密的Server Finish部分得到Server Finish的明文,(6)同理。

2、TLS 1.3 VS. TLS1.2

        由于很多应用还在使用TLS 1.2,我们此处对比一下两个版本的协议。TLS1.3版本直接将秘钥交换的公钥放在了服务器和客户端的Hello中,而TLS 1.2中客户端和服务器需要单独发送Client Key Exchange和Server Key Exchange。下图是wireshark捕获的TLS1.2和TLS1.3的Client Hello消息的内容,可以看到TLS1.3的Extensions中多了key_share。

 下图是wireshark截获的TLS1.2的Client Key Exchange消息。

        TLS 1.3最大的改进是尽可能将消息打包在一起发送,从而减少了交互轮次,提升了效率,更能适应智能移动设备和物联网设备的需求。下图是Paolo Tagliaferri的博客中两个版本协议的交互过程,左图是1.2版本,右图是1.3版本,可以看到1.3版本明显交互的次数和耗时都更少。

下图是wireshark截获的TLS1.2(上)和TLS1.3(下)版本协议的握手过程的消息,明显可以看到TLS1.3的交互消息更少。

TLS1.2

TLS1.3

        此外,TLS 1.3可选的密码套件较TLS 1.2更少,不再支持一些已经过时和不安全的密码套件。此外,TLS 1.3不支持用RSA进行秘钥交换。

        关于兼容性。为了防止因互联网中的中间设备还未升级至TLS 1.3导致协议无法识别,即使使用TLS 1.3,通信方也会将报文伪装成TLS 1.2的应用数据包,即报文头的版本号依然TLS 1.2,类型为0x17。但是,伪装成TLS 1.2的TLS 1.3需要在最后一个字段标明真实的报文类型。

3、其他

        客户端大概率会多次访问同一个服务器,因此可以通过简化再次访问时的握手过程来进一步提高效率。上诉过程中,服务器发送完Server Finish后,可以向客户端发送会话票证(Session Ticket),从这个票证可以计算出预共享秘钥(Pre-Shared Key),客户端可以使用Pre-Shared Key向服务器验证并恢复之前的连接。图中不包含发送会话票证的过程,详情可以参考参考资料3

        上图的流程没有服务器对客户端的认证,服务器可通过向客户端发送CertificateRequest请求对客户端进行认证,认证过程与客户端对服务器的认证过程类似。

        本文没有讨论证书为什么能证明通信方的身份,有兴趣的读者可以参考《图解密码技术》第10章的内容。


三、Java中协议的实现

        Java SE的安全组件JSSE(Java Secure Socket Extension,Java安全套接字扩展)提供了完全基于Java的SSL、TLS和DTLS通信框架及实现,可用于实现安全网络通信,服务器和客户端都可以用其创建和管理SSL/TLS连接。JSSE主要内容有:SSLContext、SSLSocket/SSLServerSocket及其工厂类SSLSocketFactory/SSLServerSocketFactory、SSLEngine、KeyManager和TrustManager等。而这些内容大多是接口或抽象类,说明JSSE是一个开放的标准,用户可以实现和扩展自己的JSSE,或根据应用需求使用不同的第三方库。SunJSSE就是Oracle公司自己的实现,包括对JSSE接口的实现和抽象类的扩展、密码套件的实现、对握手过程中Hello、秘钥交换、身份验证等消息的封装等。

        Java从JDK 11开始支持TLS 1.3(JEP 332,http://openjdk.java.net/jeps/332),老版本的Java使用TLS 1.3时需要使用第三方库,如OpenJSSE。

    本文用Java实现一个最简单的HTTPS服务器(SimpleHTTPSServer)和客户端(SimpleHTTPSClient),当然写代码难免综合参考多篇StackOverflow和CSDN的帖子,如有雷同不是巧合。 实验环境为java 11.0.10,此处可获取工程源码。SimpleHTTPSServer和SimpleHTTPSClient代码如下:

package com.company;

import com.sun.net.httpserver.*;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.security.*;
import java.security.cert.CertificateException;

public class SimpleHTTPSServer {
    private static final String key_password = "123456";

    public static void main(String[] args) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
        //将服务开在1443端口上
        InetSocketAddress address = new InetSocketAddress(1443);
        //服务器搭建需要考虑多线程异步等问题,不是本文关注点,为了更简略,直接使用HttpsServer快速搭建一个服务器
        HttpsServer httpsServer = HttpsServer.create(address, 0); //sun.net.httpserver.HttpsServerImpl

        char[] password = key_password.toCharArray();
        //1. 用KeyStore载入文件中密钥和证书
        KeyStore keyStore = KeyStore.getInstance("JKS");
        FileInputStream fileInputStream = new FileInputStream("simple.jks");
        keyStore.load(fileInputStream, password);

        //2. 生成证书管理器,可用于获取发送给对方的证书
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, password);
        KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); //sun.security.ssl.SunX509KeyManagerImpl

        //3. 生成证书信任管理器,可用于检查客户端和服务器的证书
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); //sun.security.ssl.X509TrustManagerImpl

        //4. 生成SSL/TLS上下文
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(keyManagers, trustManagers, null);



        //5. 为服务器配置与SSL/TLS有关的参数
        httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext){
            @Override
            public void configure(HttpsParameters params) {
                SSLContext context = getSSLContext();
                SSLEngine engine = context.createSSLEngine(); //sun.security.ssl.SSLEngineImpl
                System.out.println(engine.getClass().getName());
                params.setNeedClientAuth(false);
                String[] enabledCipherSuites = engine.getEnabledCipherSuites();
                System.out.println("Enabled CipherSuites : ");
                for(String s : enabledCipherSuites){
                    System.out.println(s);
                }
                params.setCipherSuites(enabledCipherSuites);
                String[] enabledProtocols = engine.getEnabledProtocols();
                System.out.println("Enabled Protocols : ");
                for(String s : enabledProtocols){
                    System.out.println(s);
                }
                params.setProtocols(enabledProtocols);

                SSLParameters sslParameters = context.getSupportedSSLParameters();
                params.setSSLParameters(sslParameters);
            }
        });

        httpsServer.createContext("/test", new TestHandler());
        httpsServer.setExecutor(null);
        httpsServer.start();
    }

    //处理对路劲/test的访问请求
    public static class TestHandler implements HttpHandler{

        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String response = "Hello, Test :) ";
            exchange.sendResponseHeaders(200, response.getBytes().length);
            OutputStream outputStream = exchange.getResponseBody();
            outputStream.write(response.getBytes());
            outputStream.close();
        }
    }
}

package com.company;

import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class SimpleHTTPClient {
    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, KeyManagementException {
        String urlStr = "https://127.0.0.1:1443/test";
        URL url = new URL(urlStr);
        //1. 获取一个访问Https的连接
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();

        //2. 实现验证服务器的逻辑,此处信任所有的服务器
        X509TrustManager trustManager = new X509ExtendedTrustManager() {

            @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];
            }

            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {

            }

            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {

            }

            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {

            }

            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {

            }
        };

        //3. 生成SSL/TLS上下文
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        //服务器不认证客户端,所以可以将KeyManager设为null
        sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom());

        //4. 得到一个SSL套接字工厂并绑定到HttpsURLConnection上
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        con.setSSLSocketFactory(sslSocketFactory);


        //5. 读取服务器返回的内容并打印
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String in;
        while((in = bufferedReader.readLine()) != null){
            System.out.println(in);
        }
        bufferedReader.close();
    }
}

以上是个人在学习过程中的总结,欢迎大家的指正和交流,抱拳感谢:)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值