TLS(v1.2)协议

全称 传输层安全协议,上一代是安全套接层(SSL,不安全),用途广泛,最知名的是用于http,使http升级为https协议,最新版本为TLSv1.3。
TLS通过建立客户端和服务器之间的安全通道,确保传输的数据是 机密的、经认证的和未被修改的
TLS工作在 传输层协议和应用层协议之间
TLS通过使用证书和可信证书颁发机构对服务器(或客户端)进行身份验证,从而抵御中间人攻击。

TLS设计目标

满足高效性、跨平台、可扩展和通用性。

通信两端传输的数据应该是安全的,不可伪造和篡改的。

TLS/SSL协议是标准的,任何开发者基于TLS/SSL的RFC设计规范都可以实现该协议,开发者也很容易在应用中引入TLS/SSL协议。

密码学算法是不断迭代的,随着时间的推移,会出现更安全的算法,为了保障持续的安全,TLS/SSL协议允许动态地引入新的算法。由于通信体之间环境是不一样的,协议允许双方协商出都支持的密码学算法。

解决方案必须是高效的,TLS/SSL协议涉及了很多密码学算法的运算,增加了通信延时和机器负载,但现在,有一些新的技术和解决方案在逐步提升TLS/SSL协议的效率。

密码套件

密码套件一般指几种密码学算法的组合。主要包括身份验证算法(通过证书)、密钥协商算法、加密算法或者加密模式、HMAC算法的加密基元、KDF的加密基元等等。密码套件的强度决定了TLS的安全程度,web服务器通常可以配置密码套件,如nginx等,所以现在一些https网站已经禁止使用有风险的密码套件。

TLS 1.3支持几种密码套件:TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256、TLS_AES_128_GCM_SHA256等。

下面使用openssl查看支持的密码套件
openssl ciphers -V | column -t
#选取第一条结果,各列说明如下:
#第一列:数值代表密码套件的编号,每个密码套件的编号由IANA(互联网数字分配机构)定义。
#第二列:代表密码套件的名称,虽然密码套件编号是一致的,不同的TLS/SSL协议实现其使用的名称可能是不一样的。
#第三列:表示该密码套件适用于哪个TLS/SSL版本的协议。
#第四列:表示密钥协商算法。
#第五列:表示身份验证算法。
#第六列:表示加密算法、加密模式、密钥长度。
#第七列:表示HMAC算法。其中AEAD表示采用的是AEAD加密模式(比如AES128-GCM),无须HMAC算法
    1                    2                3       4       5         6              7
#0x13,0x02  -  TLS_AES_256_GCM_SHA384  TLSv1.3  Kx=any  Au=any Enc=AESGCM(256)  Mac=AEAD

密钥块

一般来说,TLS要保证数据库的机密性和完整性,需要用到对称加密算法(相比非对称加密计算较快)和消息认证码(MAC),所以需要一组密钥,这些密钥由 主密钥使用 密钥导出函数(KDF)生成。

密钥块的 个数和密钥块的 长度都依赖于协商出来的对称加密码算法和HMAC算法,也就是由密码套件决定。

例如客户端和服务器端协商出加密算法采用AES-256-CBC,消息认证码采用HMAC,则它们必须持有相同的AES密钥、HMAC密钥、初始化向量等等,这些就称为密钥块。

主密钥和预备主密钥

主密钥由 预备主密钥生成,预备主密钥也采用 密码导出函数(KDF)转换为主密钥,预备主密钥通信双方协商(采用RSA或者DH(ECDH)算法)得出。

先使用预备主密钥转换出固定长度的主密钥,然后主密钥转换出任意数量、任意长度的密钥块。

密钥导出函数(KDF)

常见的KDF有PBKDF2、PRF等。

KDF的输入参数包含输入值和salt,如果输入值和salt是一样的,则输出也是一样的。对于通信双方来说,每一次连接的salt是变化的,但是为了保证输出结果一致,双方每次持有的salt值必须相同。

TLS1.2使用 PRF进行密码推导(TLS1.3 是用的 HKDF),它需要三个参数(secret,label,seed),其中:secret是输入,label是个固定值,seed就是salt,是一个随机值,计算过程大体如下:
P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
                             HMAC_hash(secret, A(2) + seed) +...
                             HMAC_hash(secret, A(i) + seed)
                             
PRF(secret, label, seed) = P_<hash>(secret, label + seed)

/*
 其中,A(0) = seed,A(i) = HMAC_hash(secret, A(i-1))
 hash指的是单向hash函数,如sha256等等。
 label是个固定字符串,ASCII码。
 以sha256为例,一次HMAC_sha256产生的数据为32字节。如果想得到80字节数据,则令i=3,这样会得到
 96字节数据,丢弃后16字节就可得到80字节。
*/

密钥协商

为了安全传输对称加密和MAC用的密钥块,不能再使用另一个密钥去加密这些密钥块,这会陷入加密的无限循环中,我们需要一种动态密钥,每次会话都不一样,这样也具有前向保密性。
这时候就可以使用公开密钥算法了。

每个客户端和服务器端初始化连接的时候生成 预备主密钥,每次的值都是不一样的。
预备主密钥在会话结束后(连接关闭后),会自动释放,不会持久存储。
预备主密钥必须保证是机密的,确保攻击者无法从任何手段得出预备主密钥。

RSA协商

步骤如下:
1.客户端向服务器端发起连接请求,服务器端发送RSA密钥对的公钥给客户端。
2.客户端通过 随机数生成器生成一个预备主密钥,用服务器的公钥加密并发送给服务器端。
3.服务器解密预备主密钥,如果成功解密,则预备主密钥正式生效。

RSA协商虽然步骤简洁,但是不能保证前向安全性(指由于泄露密钥导致历史数据被解密)。

DH协商

步骤如下:
1.客户端向服务器端发起连接请求。
2.服务器端生成一个RSA密钥对,并将公钥发送给客户端。
3.服务器端生成DH参数和服务器DH密钥对,用RSA私钥签名DH参数和服务器DH公钥,最后将签名值、DH参数、服务器DH公钥发送给客户端。
4.客户端通过服务器RSA的公钥验证签名,获取到DH参数和服务器DH公钥。
5.客户端通过DH参数生成客户端的DH密钥对,并将客户端DH公钥发送给服务器端。
6.客户端通过客户端DH私钥和服务器端DH公钥计算出预备主密钥。
7.服务器端接收到客户端的DH公钥,与自己的DH私钥计算出预备主密钥。
自此协商完成。

静态DH

每次会话时,服务器每次发送的DH参数和DH公钥是一样的(省去计算时间),这称为静态DH算法,它不能保证前向安全性。

动态DH(DHE)

通信双方每次连接的时候,服务器通过DH参数生成的服务器DH密钥对是不一样的,在会话结束后,服务器DH密钥对也会失效,由于每次会话DH密钥不一样,所以这种方式能提供前向安全性。

TLS协议套件

TLS由多个协议组成,最主要的两个是 记录协议握手协议。记录协议是底层协议,握手协议是高层协议。记录协议定义了一种 数据包格式封装来自更高层协议的数据,并将该数据发送给另一方。握手协议是TLS的 密钥协商协议

握手协议(TLS高层协议总称)

握手协议是TLS的核心协议,是客户端和服务器建立会话密钥以便发起安全通信的过程。握手协议有四个并行协议: 握手协议(TLS Handshaking Protocols)、 警告协议(Alert Protocol)、 应用层协议(Application Data Protocol)、 密码切换协议(Change Cipher Spec Protocol)。

握手协议主要的工作就是客户端和服务器端协商出双方都认可的密码套件,基于密码套件协商出加密参数。握手协议的一条消息可以由多个子消息组成,多个子消息最终由TLS记录层协议封装成一条消息(或多条消息)交给TCP处理。

握手协议

在TLS握手过程中,客户端和服务器扮演不同的角色,双方进行协商。客户端提出了一些配置(TLS版本和密码套件,优先顺序等),服务器选择要使用的配置。服务器一般遵循客户端的偏好,但也可能选择其他的配置。
客户端和服务器握手流程示例如下:
客户端与服务器建立TLS连接,每次TLS会话会生成一个DH密钥对,发送给服务器支持的加密算法和客户端DH公钥,还包括32字节的随机数和可选信息(附加参数等)。

服务器端生成DH(Diffie-Hellman)密钥对,并根据服务器的DH私钥和发送过来的客户端的DH公钥计算协商的预备主密钥secret,用KDF导出主密钥keys。服务器端响应ServerHello消息,内容有:用于加密TLS记录的密码、服务器DH公钥、32字节的随机数、证书、ClientHello和ServerHello中所有消息的签名(使用与证书公钥相对应的私钥计算)、同样信息的MAC以及签名。MAC使用公共密钥计算。

客户端接收到ServerHello消息时,首先先验证证书的有效性,验证签名,计算DH的会话密钥并从中得到对称密钥,验证服务器发送的MAC。一旦完成所有验证,客户端就准备好将加密的消息发送到服务器。

工作流程

1.客户端、服务器互相交换 hello子消息,该消息交换 随机值和支持的 密码套件列表, 协商出密码套件以及对应的算法,检查会话是否可恢复。
2.交换证书和密码学信息,允许服务器和客户端互相校验身份,分为服务器身份验证和客户端身份验证,一般只验证服务器证书。
3.交换密码学参数,客户端和服务器根据参数计算出相同的 预备主密钥
4.通过 预备主密钥服务器、客户端的随机值生成 主密钥
5.握手协议提供 加密参数记录协议
6.客户端和服务器端校验对方的 Finished子消息,避免握手协议的消息被篡改。

用Wireshark抓取一次完整的TLS v1.2握手过程如下:

子消息

握手协议由很多子消息构成(如上图),握手协议中的子消息必须按照 特定的顺序发送,对于客户端和服务器端来说,如果收不到 特定顺序的消息就会产生一个致命的错误,比如客户端发送ClientHello消息后,下一个接收到的消息必须是服务器发送的ServerHello消息。
子消息结构如下:
//消息类型枚举
enum {
    hello_request(0), client_hello(1), server_hello(2),
    certificate(11), server_key_exchange (12),
    certificate_request(13), server_hello_done(14),
    certificate_verify(15), client_key_exchange(16),
    finished(20), (255)
} HandshakeType;

struct {
    //消息类型
    HandshakeType msg_type;
    //消息长度
    uint24 length;
    //消息内容
    select (HandshakeType) {
        case hello_request:       HelloRequest;
        case client_hello:        ClientHello;
        case server_hello:        ServerHello;
        case certificate:         Certificate;
        case server_key_exchange: ServerKeyExchange;
        case certificate_request: CertificateRequest;
        case server_hello_done:   ServerHelloDone;
        case certificate_verify:  CertificateVerify;
        case client_key_exchange: ClientKeyExchange;
        case finished:            Finished;
        //下面的子消息与扩展有关
        case certificate_url:     CertificateURL;
        case certificate_status:  CertificateStatus;
        case session_ticket:      NewSessionTicket;
        //......
    } body;
} Handshake;

几个重要的子消息

ClientHello
当客户端第一次连接到服务器的时候,发送的第一条消息就是ClientHello,结构如下:
struct {
    //客户端支持的TLS最高版本号,也就是客户端支持<=client_version的TLS版本
    ProtocolVersion client_version;
    //客户端的随机数,用来生成预备主密钥,计算主密钥和密钥块,校验完整的握手消息,防止重放攻击等
    Random random;
    /*
     和会话恢复有关。客户端和服务器完成一次握手,服务器会发送一个会话ID给客户端,
     下次连接的时候客户端会发送该会话ID给服务器,如果服务器端校验存在该会话ID,
     就会恢复上一次连接,从而减少握手过程,提升效率。
     对于一次全新的连接来说,客户端传递的session_id为空。
    */
    SessionID session_id;
    //客户端支持的密码套件列表,排在第一个的优先选择。
    CipherSuite cipher_suites<2..2^16-2>;
    //客户端支持的压缩算法,和TLS记录协议一样,一般不启用压缩算法。
    CompressionMethod compression_methods<1..2^8-1>;
    //TLS协议支持扩展,方便日后的升级。
    select (extensions_present) {
        case false:
          struct {};
        case true:
          //扩展内容
          Extension extensions<0..2^16-1>;
    };
} ClientHello;
ServerHello
服务器端接收到客户端的Clinet Hello消息后,根据客户端传递过来的密码套件,结合服务器端配置的密码套件,选择一个双方都支持的密码套件,如果匹配错误,则握手失败。
struct {
    //服务器根据客户端支持的版本选择一个双方都支持的版本
    ProtocolVersion Server_version;
    //服务器的随机数
    Random random;
    /*
     如果客户端传输的session_id不为空,则服务器会从缓存中寻找是否存在同样的session_id,
     如果找到表示可以进行会话恢复,可以复用上一个连接。如果没有找到,则进行一个完整的握手过程,
     传递一个新的session_id。
    */
    SessionID session_id;
    //服务器根据客户端支持的密码套件列表,选择一个双方都支持的密码套件。
    CipherSuite cipher_suite;
    //根据客户端支持的压缩算法,选择一个压缩算法,一般不启用压缩算法。
    CompressionMethod compression_method;
    //服务器根据客户端发来的扩展进行处理,客户端没有发送的扩展不能出现在此列表中。
    select (extensions_present) {
        case false:
          struct {};
        case true:
          //扩展内容
          Extension extensions<0..2^16-1>;
    };
} ServerHello;
Certificate
服务器发送ServerHello消息后,一般会立刻发送Certificate子消息,Certificate消息是可选的,根据协商出来的密码套件,服务器选择是否发送证书消息。在HTTPS网站中一般服务器会发送证书,如果协商出的密码套件是DH_anon或者ECDH_anon,则服务器不发送该子消息。

服务器发送证书一般有两个目的:一个是身份验证,另一个是证书中包含服务器的公钥,该公钥结合密码套件的密钥协商算法协商出预备主密钥。
消息结构如下:
opaque ASN.1Cert<1..2^24-1>;
struct {
    ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;
每个ASN.1Cert结构都是一张证书,排在第一个的是服务器实体的证书,其余的是中间证书。
证书结构见: HTTPS证书

证书中包含数字签名算法,之前客户端发送ClientHello的时候回包含signature_algorithms扩展,里边是客户端支持的数字签名算法,如果客户端支持的签名算法里没有证书的签名算法,则握手直接失败。
ServerKeyExchange
可选的,如果证书包含的信息不足以进行密钥交换,那么需要发送此种消息。一般是DHE、ECDHE相关密码套件需要发送这种子消息,因为客户端每次连接服务器都需要发送动态DH的参数和公钥等信息(证书中没有),传递这些DH的信息需要用服务器私钥(和证书中的服务器公钥是一对)进行签名。

但DH_anon、ECDH_anon(不验证身份)需要发送此子消息,因为它们没有Certificate子消息,也就是没有证书携带DH信息。
此外,RSA密码套件不需要此子消息,因为客户端可以计算预备主密钥,用服务器公钥加密,然后服务器用私钥解密即可得到预备主密钥。
DH_DSS和DH_RSA也不需要此子消息,Certificate子消息中已经包含DH信息。
消息的结构如下:
struct {
    //判断密码套件中的密钥协商算法
    select (KeyExchangeAlgorithm) {
        //不需要身份验证,需要动态传递DH信息和DH公钥
        case dh_anon:
            ServerDHParams params;
        
        //动态DH
        case dhe_dss:
        case dhe_rsa:
            ServerDHParams params;
            //数字签名结构
            digitally-signed struct {
                //客户端随机数
                opaque client_random[32];
                //服务器随机数
                opaque server_random[32];
                //DH参数和公钥
                ServerDHParams params;
            } signed_params;
        
        //RSA、DH_DSS和DH_RSA无须传递Server Key Exchange子消息
        case rsa:
        case dh_dss:
        case dh_rsa:
        struct {} ;
        
        //如果是动态ECDH协商算法
        case ec_diffie_hellman:
            //ECC域信息
            ServerECDHParams   params;
            //对ServerECDHParams的签名
            Signature          signed_params;
    };
} ServerKeyExchange;
//服务器支持的密码套件,根据服务器配置的密码套件决定,这里举了个例子
enum {
    dhe_dss, dhe_rsa, dh_anon, rsa, dh_dss, dh_rsa, ec_diffie_hellman
} KeyExchangeAlgorithm;
//DH信息,包括参数和公钥
struct {
    opaque dh_p<1..2^16-1>;
    opaque dh_g<1..2^16-1>;
    opaque dh_Ys<1..2^16-1>;
} ServerDHParams;
//ECDHE参数和公钥
struct {
    //ECDH参数,ECC曲线
    ECParameters   curve_params;
    //ECC公钥
    ECPoint        public;
} ServerECDHParams;
//ECC曲线
struct {
    //曲线类型
    ECCurveType   curve_type;
    select (curve_type) {
        case explicit_prime:
        case explicit_char2:
        //使用定义好的命名曲线,选择客户端和服务器都支持的命名曲线
        case named_curve:
            NamedCurve namedcurve;
    };
} ECParameters;
//ECC公钥
struct {
    opaque point <1..2^8-1>;
} ECPoint;
//曲线类型
enum {
    explicit_prime (1),
    explicit_char2 (2),
    named_curve (3),
    reserved(248..255)
} ECCurveType;
//支持的命名曲线
enum {
    secp256k1 (22), secp256r1 (23), secp384r1 (24),
} NamedCurve;
ServerHelloDone
这个消息是在ServerHello之后发送,是个空消息,格式如下:
//发送该消息的目的是通知客户端 服务器已经发送足够的消息,下面可以进行预备主密钥的协商。
//客户端收到该消息,可以进行证书校验、密钥协商等操作。
struct { } ServerHelloDone;
ClientKeyExchange
客户端收到服务器的ServerHelloDone消息之后发送该消息,作用是协商预备主密钥。有两种方法:
1.客户端通过RSA/ECDSP加密预备主密钥,发送给服务器,服务器用私钥解密得到预备主密钥。
2.客户端服务器发送的动态DH参数得到客户端DH公钥,发送给服务器,双方根据对方的DH公钥和自己的DH私钥计算出相同的预备主密钥,需要选用双方都支持的命名曲线。

消息结构如下:
struct {
    //对应ServerKeyExchange中的KeyExchangeAlgorithm结构
    select (KeyExchangeAlgorithm) {
        case rsa:
            EncryptedPreMasterSecret;
        case dhe_dss:
        case dhe_rsa:
        case dh_dss:
        case dh_rsa:
        case dh_anon:
            ClientDiffieHellmanPublic;
        case ec_diffie_hellman:
            ClientECDiffieHellmanPublic;
        } exchange_keys;
  } ClientKeyExchange;
//加密后的预备主密钥,使用RSAES-PKCS1-v1_5加密方式。
struct {
    public-key-encrypted PreMasterSecret pre_master_secret;
} EncryptedPreMasterSecret;
//预备主密钥
struct {
    //应该传递客户端ClientHello消息传递的版本号,服务器需要校验此版本号等不等于ClientHello消息传递的版本号
    ProtocolVersion client_version;
    //一个46字节的随机数
    opaque random[46];
} PreMasterSecret;

//公钥编码方式
//implicit表示客户端DH公钥隐藏在证书中,explicit表示需要显式的传递公钥,传递的DH公钥没有任何加密处理。
enum { implicit, explicit } PublicValueEncoding;
//DH协商算法的公钥,如果客户端也有证书并把自己的证书发给服务器校验(implicit),那么该消息为空
struct {
    select (PublicValueEncoding) {
        //隐式
        case implicit:
        struct { };
        //显式,传递客户端DH公钥
        case explicit:
            opaque dh_Yc<1..2^16-1>;
    } dh_public;
} ClientDiffieHellmanPublic;

//ECDHE密钥协商算法的ECDH公钥
struct {
    select (PublicValueEncoding) {
        //隐式
        case implicit:
        struct { };
        //显式,传递客户端ECDH公钥
        case explicit:
            ECPoint ecdh_Yc;
    } ecdh_public;
} ClientECDiffieHellmanPublic;
//公钥结构
struct {
    opaque point <1..2^8-1>;
} ECPoint;
Finished
Finished子消息必须在ChangeCipherSpec子消息之后发送,如果没有按照此顺序,则会产生一个致命错误,中断握手。ChangeCipherSpec子消息发送后,表示已经准备好进行加密了,所以Finished子消息发送的是加密后的数据。Finished子消息的目的是为了确认所有的握手消息没有被篡改。
消息结构如下:
//verify_data通过PRF函数计算
struct {
    opaque verify_data[verify_data_length];
} Finished;

//第一个参数master_secret是主密钥
//第二个参数labfinished_labelel,如果是客户端,则是"client finished";如果是服务器,则是"server finished"
/*第三个参数handshake_messages是所有的握手协议消息。在TLS/SSL协议中,一般是客户端先发送
  Finished消息,服务器后发送。对于客户端来说,handshake_messages的内容包含所有发送的消息和接收到的消息,
  但不包括自己发送的Finished消息。对于服务器来说,handshake_messages的内容从Client Hello
  消息开始截止到Finished消息之前的所有消息,也包括客户端的Finished子消息。
  handshake_messages不包括Change Cipher Spec消息和Alert消息。
*/
verify_data = PRF(master_secret, finished_label, Hash(handshake_messages))
      [0..verify_data_length-1];

计算主密钥和密钥块

客户端和服务器协商出预备主密钥后,就可以计算主密钥,在ChangeCipherSpec协议消息发送之前,客户端和服务器需要计算出 主密钥
通过KDF函数计算主密钥参数如下, 计算完成后,客户端和服务器必须从内存中清除预备主密钥
//pre_master_secret 预备主密钥
//master secret是三个参数(secret,label,seed)中的label,固定值
//ClientHello.random,ServerHello.random 客户端服务器的随机数,客户端随机数在前,服务器随机数在后
SecurityParameters.master_secret = PRF(pre_master_secret,
        "master secret",
        ClientHello.random + ServerHello.random);
//计算出的主密钥长度是48字节 [0..47]
计算出主密钥之后需要计算密钥块,公式如下:
//第一个参数是主密钥
//第二个参数是label,固定为key expansion
//第三个参数是客户端和服务器的随机数,服务器随机数在前,客户端随机数在后
key_block = PRF(SecurityParameters.master_secret,
            "key expansion",
            SecurityParameters.server_random + SecurityParameters.client_random);
//密钥块可以拆分为密钥值,重要的有6个
//mac用
client_write_MAC_key[SecurityParameters.mac_key_length]
server_write_MAC_key[SecurityParameters.mac_key_length]
//对称加密用
client_write_key[SecurityParameters.enc_key_length]
server_write_key[SecurityParameters.enc_key_length]
//分组加密的初始化向量IV
client_write_IV[SecurityParameters.fixed_iv_length]
server_write_IV[SecurityParameters.fixed_iv_length]

警告协议

客户端和服务器端建立一条连接后,会通过握手协议协商密钥块,在协商和认证过程中,可能会产生错误。错误信息由警告协议处理,警告协议有多个错误,某些错误可能是致命的,会直接终止客户端和服务器端的连接。
警告协议的消息格式由两部分组成: 警告错误级别警告协议的详细描述信息
// AlertLevel表示警告错误级别
enum { warning(1), fatal(2), (255) } AlertLevel;

// 警告协议的详细描述消息
enum {
    close_notify(0),
    unexpected_message(10),
    bad_record_mac(20),
    decryption_failed_RESERVED(21),
    record_overflow(22),
    decompression_failure(30),
    handshake_failure(40),
    no_certificate_RESERVED(41),
    bad_certificate(42),
    unsupported_certificate(43),
    certificate_revoked(44),
    certificate_expired(45),
    certificate_unknown(46),
    illegal_parameter(47),
    unknown_ca(48),
    access_denied(49),
    decode_error(50),
    decrypt_error(51),
    export_restriction_RESERVED(60),
    protocol_version(70),
    insufficient_security(71),
    internal_error(80),
    user_canceled(90),
    no_renegotiation(100),
    unsupported_extension(110),
    (255)
} AlertDescription;

// 警告协议包含两部分
struct {
    AlertLevel level;
    AlertDescription description;
} Alert;

密码切换协议(TLS1.3中已废弃)

该协议的作用就是通知TLS记录协议其加密学所需要的密钥块已经准备好了,一个TLS连接一旦客户端和服务器端发出了 密码切换协议子协议,TLS记录层协议就可以对 应用层协议进行加密保护了。

TLS记录协议在处理 握手协议密码切换协议警告协议的时候,因为此时密钥块还没有准备好,所以这三个子协议是明文传输的,TLS记录协议仅仅对三个子协议添加消息头。
密码切换协议对应的子消息就是 Change Cipher Spec,一般情况下由客户端首先发送该消息,格式如下:
struct {
    enum { change_cipher_spec(1), (255) } type;
} ChangeCipherSpec;

应用层协议

应用层协议就是TLS/SSL记录层协议的上层协议,包括HTTP、FTP、SMTP等应用层协议。TLS记录协议加密保护的主要信息就是应用层协议数据,TLS记录协议会给应用层协议添加MAC验证码数据(取决于不同的加密模式)。

记录协议

所有数据以一系列TLS记录发送,称为TLS数据包。TLS记录协议(记录层)本质上是一个传输协议,与传输数据的含义无关;

TLS记录协议首先用于握手期间交换的数据。一旦握手完成,并且双方共享一个会话密钥,应用程序数据就会被分割成块,作为TLS记录的一部分进行传输。

报文结构

TLS记录协议的数据块最多16KB,结构如下:
第一字节表示发送的数据类型22(0x16)表示握手数据,23(0x17)表示加密数据,21(0x15)表示警告,20(0x14)表示密码切换
第二字节和第三字节的值被称为协议版本(Protocol Version),例如TLS v1.2这两个字节 为0x0303。
第四字节和第五字节表示将要作为传输的数据的长度,长度不能大于 字节(16KB)。
其余的字节是要传输的数据(也称为有效载荷),其长度等于由记录的第四字节和第五字节编码的值。

会话

客户端和服务器端会构建一条TCP连接,每条连接都是一个会话,会话有不同的状态,状态贯穿了整个TLS/SSL协议处理流程。

加密参数

也就是 SecurityParameters, 加密参数是TLS/SSL协议中最重要的数据结构,客户端和服务器商量出密码套件,然后基于密码套件协商出所有的加密参数,加密参数中最重要的是 主密钥
struct {
    //表示操作方是客户端或服务器
    ConnectionEnd         entity;
    //伪随机函数prf,用来将预备主密钥转换为主密钥,主密钥转位密钥块
    PRFAlgorithm          prf_algorithm;
    //加密函数,aes等
    BulkCipherAlgorithm   bulk_cipher_algorithm;
    //记录协议使用加密模式,三种
    CipherType            cipher_type;
    //加密算法密钥的长度
    uint8                 enc_key_length;
    //加密数据的长度
    uint8                 block_length;
    //盐长度
    uint8                 fixed_iv_length;
    uint8                 record_iv_length;
    //MAC算法
    MACAlgorithm          mac_algorithm;
    //MAC值的长度
    uint8                 mac_length;
    //MAC算法使用的密钥长度
    uint8                 mac_key_length;
    //记录协议的压缩算法,一般不启用
    CompressionMethod     compression_algorithm;
    //主密钥
    opaque                master_secret[48];
    //连接阶段,客户端向服务器传递的32字节随机数
    opaque                client_random[32];
    //连接阶段,服务器向客户端传递的32字节随机数
    opaque                server_random[32];
} SecurityParameters;

// 服务器端和客户端
enum { server, client } ConnectionEnd;
// 伪随机函数,在TLS v1.2 协议中,PRF函数默认加密基元是SHA256
enum { tls_prf_sha256 } PRFAlgorithm;
// 加密算法,比较流行的是aes算法
enum { null, rc4, 3des, aes } BulkCipherAlgorithm;
// 加密模式,aead是新型的加密模式,包含了消息验证码的处理
enum { stream, block, aead } CipherType;
// 消息验证码算法
enum { null, hmac_md5, hmac_sha1, hmac_sha256, hmac_sha384, hmac_sha512}
MACAlgorithm;
// 压缩方法
enum { null(0), (255) } CompressionMethod;
加密参数各个元素的值是TLS/SSL握手协议进行填充的。客户端和服务器端会协商出一个 密码套件,基于密码套件填充 加密参数各个子元素的值。TLS记录协议主要 基于加密参数的值进行加密解密,最主要的就是 密钥块
客户端和服务器端分别有自己的加密参数。客户端使用自己的write MAC key、write encryption key、write IV密钥块加密消息并发送,服务器接收到消息后,使用客户端的write MAC key、write encryption key、write IV的密钥块解密消息。对客户端来说,收到消息则使用服务器的对应值来解密。

密钥块包含6个具体的元素,分别是:
//客户端MAC密钥、加密密钥、初始化向量
client write MAC key
client write encryption key
client write IV
//服务器MAC密钥、加密密钥、初始化向量
server write MAC key
server write encryption key
server write IV

连接状态

每个TLS连接有4个 连接状态的概念,分别是:
待读状态(pending read states)
待写状态(pending write states)
可读状态(current read states)
可写状态(current write states)
在客户端和服务器初始化连接的时候, 客户端和服务器的连接状态是待读状态和待写状态(客户端和服务器分别保持自己的连接状态)。一旦所有的 加密参数已经准备好,那么客户端进入可读状态,服务器进入可写状态,只有连接状态是可读状态和可写状态,才会进行 数据加密和完整性保护

客户端和服务器分别发送 密码切换协议消息后,连接状态就会切换。客户端和服务器在没有发送 密码切换协议消息之前,所有的握手消息都是明文处理的,没有机密性和完整性保护。

对于TLS记录协议来说,每个连接状态由4个部分组成。
compression state:压缩状态,一般不启用压缩。
cipher state:每个连接使用的加密算法(三种加密模式)和加密算法使用的密钥块。
MAC key:每个连接的MAC密钥。
序列号每个TLS记录协议消息都有一个序列号,客户端和服务器各自维护一个序列号序列号从0开始。序列号只参与MAC计算序列号的作用是为了防止重放攻击。

当客户端连接至服务器端后,在内存中初始化两个变量( client_send和client_recv),client_send记录所有 客户端已经发送的数据块总量,client_recv记录所有 客户端已经接收到数据块总量,客户端第一次发送和接收,client_send和client_recv的值都是0。之后客户端每发送一个消息后,client_send+1,每接收一个消息,client_recv+1。( 客户端消息MAC计算的时候包含client_send的值

服务器端接收到客户端连接后,在内存中也初始化两个变量( server_send和server_recv),server_send记录所有 服务器已经发送的数据块总量,server_recv记录所有 服务器已经接收到数据块总量。服务器端第一次接收和发送,server_recv和server_send值都是0,之后服务器每发送一条消息,server_send+1,每接收一条消息,server_recv+1。( 服务器消息MAC计算的时候包含server_send的值

服务器收到客户端消息后,解密客户端消息后,接着计算MAC的值,进行完整性校验,需要使用server_recv变量的值,校验成功则回应一条消息给客户端。

客户端收到服务器消息后,解密服务器消息后,接着计算MAC的值,进行完整性校验,需要使用client_recv的值,处理成功后,client_recv+1。

客户端和服务器端不断递增自己和对方的序列号, 如果正确处理,client_send的值等于server_recv的值,client_recv的值等于server_send的值

假设客户端的client_send=3(发了4条数据),服务器的server_recv也是3,此时有一个攻击者想重放第4条信息(client_send=3),服务器收到一条新消息,server_recv+=1,而攻击者重放的消息client_send=3,校验MAC时会报错,这就防止了重放攻击。

工作流程

记录协议主要有4部分的处理:
1.数据分块。
2.压缩。
3.加密和完整性保护,主要包含三种模式(流加密模式、分组模式、AEAD模式)。
4.在加密完的数据前添加 消息头和其他信息(根据加密模式不同而不同)。

数据分块

所有上层协议的数据进入TLS记录协议后,先把消息拆分成块,每个块的限制最大16KB,结构如下:
// 协商出的版本号
struct {
    //大版本号,如TLS v1.2 这里是1
    uint8 major;
    //小版本号,如TLS v1.2 这里是2
    uint8 minor;
} ProtocolVersion;

// TLS高层协议的4个协议,密码交换协议,警告协议,握手协议,应用层协议
enum {
    change_cipher_spec(20),
    alert(21),
    handshake(22),
    application_data(23)
} ContentType;

//数据分块后的数据结构
struct {
    //协议类型,4个中的一种
    ContentType type;
    //版本
    ProtocolVersion version;
    //载荷长度,fragment的长度
    uint16 length;
    //载荷
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

压缩

压缩是将TLSPlaintext转换为TLSCompressed,但一般协议中不启用,其压缩后格式如下:
//和TLSPlaintext结构一致,如果不压缩两者是一样的
struct {
    ContentType type;
    ProtocolVersion version;
    uint16 length;
    opaque fragment[TLSCompressed.length];
} TLSCompressed;

加密保护

数据经过前几步之后来到加密层,将TLSCompressed转换为TLSCiphertext,格式如下:
struct {
    ContentType type;
    ProtocolVersion version;
    //加密后fragment的长度
    uint16 length;
    //加密后的数据,包括MAC
    select (SecurityParameters.cipher_type) {
        //流密码
        case stream: GenericStreamCipher;
        //分组密码
        case block:  GenericBlockCipher;
        //AEAD
        case aead:   GenericAEADCipher;
    } fragment;
} TLSCiphertext;
流密码模式
现在比较少用,GenericStreamCipher结构如下:
stream-ciphered struct {
    opaque content[TLSCompressed.length];
    opaque MAC[SecurityParameters.mac_length];
} GenericStreamCipher;
其中,MAC值是由 序列号、消息头、TLSCompressed.fragment进行HMAC运算得到,对 TLSCompressed.fragment、MAC值加密得到密文,再在密文前拼接上 消息头,得到GenericStreamCipher。整体是 先MAC后加密
//MAC的计算过程
//客户端和服务器各有一个MAC_write_key,MAC的长度由加密参数决定。
MAC(MAC_write_key, seq_num +//序列号
    TLSCompressed.type +TLSCompressed.version +TLSCompressed.length +//消息头
    TLSCompressed.fragment);//消息明文
分组密码模式
分组模式是比较常用的加密模式,需要考虑初始化向量和填充值(分组密码需要补齐为blocksize的倍数),初始化向量的长度由 加密参数record_iv_length参数决定,初始化向量的值必须是随机的,否则可能会遇到安全攻击。
struct {
    //初始化向量,长度一般和blocksize一致
    opaque IV[SecurityParameters.record_iv_length];
    //加密数据
    block-ciphered struct {
        //消息内容
        opaque content[TLSCompressed.length];
        //MAC值
        opaque MAC[SecurityParameters.mac_length];
        //填充值
        uint8 padding[GenericBlockCipher.padding_length];
        //填充长度
        uint8 padding_length;
    };
} GenericBlockCipher;
生成GenericBlockCipher的过程是将 TLSCompressed.fragment、MAC、填充值组合在一起再进行加密,最后再加密数据前拼上 消息头IV,采取的也是 先MAC后加密。
MAC计算过程如下,和流密码模式一致
//MAC的计算过程
//客户端和服务器各有一个MAC_write_key,MAC的长度由加密参数决定。
MAC(MAC_write_key, seq_num +//序列号
    TLSCompressed.type +TLSCompressed.version +TLSCompressed.length +//消息头
    TLSCompressed.fragment);//消息明文
AEAD模式
AEAD将加密和完整性保护一起实现,不用额外考虑HMAC算法, 安全性更高。AEAD密码套件示例如下:
#openssl v3.0查看TLS密码套件
openssl ciphers -V 

AEAD模式

加密算法

密码套件

GCM

AES-128-GCM

例如DHE-RSA-AES128-GCM-SHA256

ChaCha20-Ploy1305

ChaCha20-Ploy1305

例如ECDHE-ECDSA-CHACHA20-POLY1305

加密后的结构如下:
struct {
    //nonce,长度取决于SecurityParameters.record_iv_length
    opaque nonce_explicit[SecurityParameters.record_iv_length];
    aead-ciphered struct {
        opaque content[TLSCompressed.length];
    };
} GenericAEADCipher;
AEAD加密是将 序列号+消息头作为关联数据,联合 TLSCompressed.fragment加密,然后在加密数据前面拼接上 消息头+nonce得到。
//AEAD加密函数
//write_key MAC用,这里不需要
//nonce:随机数,明文传输
//plaintext,明文,=TLSCompressed.fragment
//关联数据,验证消息完整性,=序列号+消息头=seq_num +TLSCompressed.type +TLSCompressed.version +TLSCompressed.length
AEADEncrypted = AEAD-Encrypt(write_key, nonce, plaintext, additional_data)
解密过程,如果完整性校验失败则会报错
TLSCompressed.fragment = AEAD-Decrypt(write_key, nonce, AEADEncrypted,
    additional_data)

TLS协议扩展

扩展是为了TLS以后的发展而设计的,扩展是协议中一个可插拔的单元,每个扩展由IANA统一注册和管理。
客户端可以根据自己的需求发送多个扩展给服务器,扩展列表消息包含在Client Hello消息中。
服务器解析Client Hello消息中的扩展,并在Server Hello消息中返回相同类型的扩展,注意有些扩展不一定在Server Hello消息中响应。

客户端和服务器Hello消息包含扩展信息,扩展是向下兼容的,表示客户端和服务器端并没有严格要求一定要支持某个扩展。比如客户端Client Hello消息中附带了一个扩展,服务器端接收到该扩展,如果没有明白该扩展的含义,可以忽略不处理,并不影响后续的握手;如果服务器明白该扩展的含义,可以在Server Hello以同样的扩展响应客户端Client Hello消息。
服务器响应的扩展必须是客户端扩展请求的子集,客户端没有发送的扩展不能出现在服务器Server Hello消息中,否则会产生一个致命错误。

扩展类型名称

扩展类型编号

RFC引用

server_name

0

RFC 6066

max_fragment_length

1

RFC 6066

client_certificate_url

2

RFC 6066

trusted_ca_keys

3

RFC 6066

truncated_hmac

4

RFC 6066

status_request

5

RFC 6066

supported_groups

10

RFC 7919

ec_point_formats

11

RFC-ietf-tls-rfc4492bis-17

signature_algorithms

13

RFC 5246

application_layer_protocol_negotiation(ALPN)

16

RFC 7301

signed_certificate_timestamp

18

RFC 6962

extended_master_secret

23

RFC 7627

SessionTicket TLS

35

RFC 4507

renegotiation_info

65281

RFC 5746

Extension

struct {
    //扩展类型,占2字节
    ExtensionType extension_type;
    //扩展数据
    opaque extension_data<0..2^16-1>;
} Extension;

enum {
    //类似于http中host请求头的作用
    server_name(0),
    max_fragment_length(1),
    client_certificate_url(2),
    trusted_ca_keys(3),
    truncated_hmac(4),
    //和OCSP有关
    status_request(5),
    //关于ECC的扩展
    supported_groups(10),
    //关于ECC的扩展
    ec_point_formats(11),
    //用于客户端向服务器列出自己支持的签名算法和摘要算法
    signature_algorithms(13),
    //使得客户端和服务器端能够协商出一个应用层协议,例如http1.1和http2,选择双方都支持的
    application_layer_protocol_negotiation(16),
    //和证书透明度有关,每一张服务器实体证书都可以由CA机构或服务器实体提交给CT日志服务器从而获得证书SCT信息。
    signed_certificate_timestamp(18),
    extended_master_secret(23),
    //用于会话恢复
    session_ticket(35),
    //和重新协商有关
    renegotiation_info(65281),
    (65535)
} ExtensionType;
//扩展列表
Extension extensions<0..2^16-1>;

TLS会话恢复

客户端和服务器握手成功后会建立一个TLS连接,只要客户端和服务器不主动关闭该连接,应用层数据请求就一直受该连接保护,一旦客户端和服务器关闭该连接,那么客户端下次访问服务器的时候就需要重新握手,建立一个新的连接。握手的过程非常消耗时间和CPU,所以会话恢复机制就是为了省去重新握手的时间和减少资源消耗。
目前TLS v1.2中有两种恢复机制,分别是Session ID 和SessionTicket TLS扩展。

Session

Session称为会话,它相当于TLS连接的状态,内容如下:
会话标识符(session identifier):每个会话都有唯一编号。
证书(peer certificate):对端的证书,一般为空。
压缩算法(compression method):一般不启用。
密码套件(cipher spec):客户端和服务器协商的密码套件。
主密钥(master secret):每个会话会保存一个主密钥(master_secret)。
会话可恢复标识(is resumable):表示某个会话是否可恢复。

SessionID

客户端发送Client Hello消息时,会传递一个Session ID参数,如果是一次完整的握手,不是会话恢复的TLS连接,那么这个Session ID为空,服务器在收到消息后,会生成一个Session ID,并在Sever Hello中传递给客户端,客户端仅在内存中保留Session ID。在双方发送Finished子消息后,会话结束,服务器将Session ID保存在Session Cache中,Session Cache相当于一个哈希表,键为Session ID,值为Session的信息。
会话恢复的过程:
客户端再次请求相同的网站,如果客户端内存中存在该网站对应的Session ID,则Client Hello消息附带该Session ID。服务器接收到该请求后,检查Session Cache是否能够匹配健值为Session ID的会话,如果没有或者不可恢复会话,则进行完整的握手协议,同时生成一个新的Session ID返回给客户端。如果能够恢复本次连接,则直接发送ChangeCipherSpec和Finished子消息,不进行密钥协商过程,因为主密钥存在于Session Cache中。最终客户端也发送ChangeCipherSpec和Finished子消息,表示会话恢复成功。
注意点:
在恢复的TLS连接中,虽然主密钥和前一次连接的一致, 但客户端和服务器最终生成的密钥块和先前的密钥块是不一致的原因在于通过PRF生成密钥块的时候,客户端和服务器的 随机数 不同于前一次连接,这也有效地增强了安全性。
在恢复会话完成后,也要校验客户端和服务器端的Finished消息,避免握手消息被篡改。
恢复会话的时候,本次连接协商出的密码套件必须和会话中的密码套件是一致的,否则就要进行完整的握手。是否恢复成功取决于客户端和服务器,即使存在可以恢复的会话,服务器也可以要求进行完整的握手。
会话中并不保存扩展信息,所以每个扩展必须充分考虑会话恢复的情况。
Session ID是明文传输的,服务器Session ID不应该包含隐私数据。

SessionTicket

SessionTicket的出现是为了解决Session ID的缺点,它由SessionTicket TLS扩展实现。
Session ID会话信息存储在服务器,对于访问量非常大的服务器,内存消耗非常大。
Session ID会话恢复应付不了负载均衡和分布式服务的情况,因为它不能跨主机访问。
工作方式:
服务器将会话信息加密后以票据(ticket)的方式发送给 客户端保存服务器本身不存储会话信息
客户端接收到票据后将其存储到内存中(不解密),如果想恢复会话,则下一次连接需要将票据发送给服务器,服务器解密后,经过验证无误则可以进行会话恢复。

SessionTicket TLS扩展

对于一个新的连接,客户端ClientHello消息发送一个空的SessionTicket TLS扩展,表示想获得一个票据,服务器在SeverHello中响应一个空的SessionTicket TLS扩展。如果不想服务器支持此类会话恢复,则ClientHello消息不发送此扩展,服务器也不会处理。
如果服务器支持SessionTicket会话恢复,只要客户端发送了SessionTicket TLS扩展(无论是不是空),那么服务器会发送New Session Ticket子消息,该消息包含一个票据,客户端收到以后保存起来以备后用。

如果之前连接过一次,再次连接时想要进行会话恢复,则客户端ClientHello消息发送一个非空的SessionTicket TLS扩展,服务器收到扩展后,对票据进行解密、校验,确认无误后发送一个空的SessionTicket TLS扩展。接着发送一个New Session Ticket子消息 更新票据。然后客户端和服务器校验Finished子消息表示恢复会话的握手完成。

New Session Ticket子消息

由服务器发送该子消息,如果ServerHello子消息中包含SessionTicket TLS扩展,则必须发送此消息,如果不包含扩展,则不能发送。如果发送该消息则必须在Change Cipher Spec前发送。
如果服务器端成功校验客户端发送的票据,必须重新生成一个票据,然后通过New Session Ticket子消息发送新票据,客户端在下一次连接的时候应该发送新的票据。
消息结构如下:
struct {
    //票据的有效期
    uint32 ticket_lifetime_hint;
    //票据内容
    opaque ticket<0..2^16-1>;
} NewSessionTicket;
struct {
    //包含至少一组密钥,每组包含AES-128-CBC和HMAC-SH-256的密钥,其中有一组用于票据加密
    opaque key_name[16];
    //AES的IV向量
    opaque iv[16];
    //加密后的会话信息
    opaque encrypted_state<0..2^16-1>;
    //摘要值,对encrypted_state、key_name、iv进行MAC运算
    opaque mac[32];
} ticket;
//加密前的会话信息结构
struct {
    //TLS版本
    ProtocolVersion protocol_version;
    //密码套件
    CipherSuite cipher_suite;
    //压缩算法
    CompressionMethod compression_method;
    //主密钥
    opaque master_secret[48];
    //客户端的标识符
    ClientIdentity client_identity;
    //票据过期时间
    uint32 timestamp;
} StatePlaintext;

两种会话恢复机制共存

(1)如果服务器想使用SessionTicket机制,那么服务器Server Hello可以不发送Session ID。
(2)如果服务器不想使用SessionTicket机制,那么Server Hello消息中不包含SessionTicket扩展即可,此时应该生成Session ID发送给客户端。
(3)对于客户端来说,SessionTicket恢复的优先级应该更高,如果服务器同时发送了票据和Session ID,应该使用SessionTicket。
(4)如果服务器同时发送了票据和Session ID,为了方便切换两种会话恢复方式,客户端应该同时发送票据和Session ID。服务器接收后,如果在Session Cache中存在Session ID,则响应同样的Session ID给客户端,同时也发送票据给客户端。

参考:《深入浅出HTTPS:从原理到实战》虞卫东

猜你喜欢

转载自blog.csdn.net/qq_32076957/article/details/128946495