HTTP - TLS1.3 初次解读

http HTTP面试题 - TLS1.3 初次解读

引言

HTTP - HTTPS(TLS1.2)中,笔者介绍了目前世界主流的TLS1.2协议的相关知识点,文中从HTTP的缺陷、SSL的历史、信息加密的主要手段、数字证书、以及最为关键的TLS1.2交互过程介绍了现今HTTPS的关键部分内容。

TLS1.3早已在2018年登场,这一节我们来看看根据TLS1.3协议整体大致讲了什么内容。

因为TLS1.2已经做的比较完善,TLS1.3 的主要改进个人认为关键分为三个主要改进目标:兼容安全与性能

时间线

HTTP S时间线

TLS 1.3 改进点

兼容性

TLS1.2 发展了很多年了,基本上多数网络设备对于这个版本的协议产生了依赖性,如果直接用TLS1.3的版本协议替换掉TLS1.2,大量的代理服务器、网关都无法正确处理,TLS1.2 存在巨大的历史包袱。

TLS1.3 显然是没法撼动TLS1.2这个“老皇帝”,那要怎么办捏?因此TLS1.3想到了一招“垂帘听政”,既然没法替换掉,那就借用“TLS1.2”的名号进行传输数据,也就是所谓的“伪装”。

那么该怎么区分 TLS1.2 和 TLS1.3 ?这时候就要用到扩展协议(Extension Protocol),扩充协议有点类似“追加条款”,只支持TLS1.2的服务器,当无法识别扩展协议而被忽略退化为TLS1.2握手,反之则认为可以进行TLS1.3协议握手。注意这里的退化还不止那么简单,TLS1.3 的退化只支持TLS1.2,不支持TLS1.2以下任何版本,所以这一招还偷偷把一些 老古董请下去。

TLS1.3的改进是方式记录头部的Version 字段“固定不变”,以及在进行第一步Hello交互之后需要立刻发送supported_versions的消息表示自己支持 TLS1.3协议,类似信号告诉对方自己实际的 TLS 的版本号,也是新旧协议的主要分水岭。

TLS1.3在握手的时候会出现下面的变化:

Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Extension: supported_versions (len=11)
Supported Version: TLS 1.3 (0x0304)
Supported Version: TLS 1.2 (0x0303)

安全强化:“瘦身”

TLS1.2 虽然在安全方面已经显得十分完善了,但是经过多年的考验还是发现不少的问题。所以 TLS1.3 主要是给TLS1.2遗留问题进行修复,比如说进行了下面的调整:

  • 伪随机数函数由 PRF 升级为 HKDF(HMAC-based Extract-and-Expand Key Derivation Function);
  • 明确禁止在记录协议里使用压缩,因为使用压缩被是存在漏洞的并且有可能被黑客用特殊算法破解;(详情参考文章末尾“TLS1.2 攻击手段”)
  • 废除了 RC4DES 对称加密算法;
  • 废除了 ECBCBC 等传统分组模式;
  • 废除了 MD5SHA1SHA-224 摘要算法;
  • 废除了 RSADH 密钥交换算法和许多命名曲线;
  • ServerHello 之后的所有握手消息采取了加密操作,可见明文大大减少;
  • DSA 证书不再允许在 TLS 1.3 中使用;
    ....

上面这些点更像是“瘦身”,因为废弃了很多被证实不安全的算法。

还记得PRF么?在TLS1.2中用于最后确定会话密钥的关键函数。

在TLS1.3的RFC的原文中定义了下面的加密套件,但是需要注意TLS1.3的加密套件虽然看起来和TLS1.2有部分重合,但是TLS 1.3 为了进一步简化加密套件的概念,这里的加密套件实际上指的是 对称加密密钥的协商,而TLS1.2的加密套件包含了非对称密钥的协商,这种方式已经在TLS1.3禁止使用,所以它是无法和TLS1.2直接套用“兼容”的。

 This specification defines the following cipher suites for use with
   TLS 1.3.

              +------------------------------+-------------+
              | Description                  | Value       |
              +------------------------------+-------------+
              | TLS_AES_128_GCM_SHA256       | {0x13,0x01} |
              |                              |             |
              | TLS_AES_256_GCM_SHA384       | {0x13,0x02} |
              |                              |             |
              | TLS_CHACHA20_POLY1305_SHA256 | {0x13,0x03} |
              |                              |             |
              | TLS_AES_128_CCM_SHA256       | {0x13,0x04} |
              |                              |             |
              | TLS_AES_128_CCM_8_SHA256     | {0x13,0x05} |
              +------------------------------+-------------+

上面的加密套件含义是什么?以TLS_AES_128_CCM_SHA256为例,TLS表明该加密组件用于TLS协议,AES表明使用AES对称加密算法,128表示密钥长度为128位,CCM表明分组加密模式,SHA256是HKDF过程使用的哈希算法。

了解上面的这些内容之后,读者对比TLS1.2的加密套件可能会问为什么要放弃使用DHRSA算法?这里还是涉及TLS1.2到底有哪些攻击手段,为了不分散注意,我把这些内容放到了末尾“扩展知识”中的“已知TLS1.2攻击手段”部分介绍。

从这一部分内容可以发现RSA密钥在不少的算法中都有漏洞,比如最为致命的前向安全性问题,所谓的前向安全性,指的是RSA算法本身的安全性存在漏洞,一旦黑客破解出RSA的密钥的私钥,如果此时黑客刚好有网站此前所有的请求报文,就可以私钥把之前所有的传输加密报文解密,并且获取所有的用户信息,RSA的最大问题在于密钥的变动永远是单方向的。

ECDHE 全面推广

ECDHE加密算法,简单理解是在进行握手的时候使用临时的椭圆函数曲线公钥作为“根”,利用数学公式推导计算交换密钥,而不是传统的非对称加密算法保护堆对称加密密钥和进行密钥协商,每一次TLS连接的所有参与密钥都是临时生成的,哪怕黑客真的手眼通天被破开某个请求的私钥,也只能获取到当前请求的相关信息,无法用于其他请求,因此是具备前向安全性的。

此外TLS 1.3仅支持速度快安全性强的加密标准算法AES,以及性能消耗极低的CHACHA20

注意CHACHA20是和POLY1305搭配使用的,这是由于CHACHA20在进行AEAD运算时,CHACHA20本身不能提供完整性校验功能,因此还需要借助POLY1305这种不耗费性能的MAC算法来提供完整性校验的功能。

分组加密模式则剩下CCM和GCM,淘汰了 ECBCBC 等有已知的攻击方式的不安全算法,目前都是理论上安全的算法,不容易被攻击者破解。

提升性能

性能提升是TLS1.3的一个关键部分,因为随着HTTP/2的性能全面提升和HTTP/3的进一步发展,加密传输的效率也成为了TLS发展的重要瓶颈也是HTTPS连接的瓶颈, TLS1.3 利用扩展字段 extension 做了很多细节改善。我们直接从RFC标准对比TLS1.2和TLS1.3的变动。

TLS1.3 交互步骤:

 Client                                           Server

Key  ^ ClientHello
Exch | + key_share*
     | + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       -------->
                                                  ServerHello  ^ Key
                                                 + key_share*  | Exch
                                            + pre_shared_key*  v
                                        {EncryptedExtensions}  ^  Server
                                        {CertificateRequest*}  v  Params
                                               {Certificate*}  ^
                                         {CertificateVerify*}  | Auth
                                                   {Finished}  v
                               <--------  [Application Data*]
     ^ {Certificate*}
Auth | {CertificateVerify*}
     v {Finished}              -------->
       [Application Data]      <------->  [Application Data]

下面是符号对应的作用

+ Indicates noteworthy extensions sent in the previously noted message.
* Indicates optional or situation-dependent messages/extensions that are not always sent.
{} Indicates messages protected using keys derived from a [sender]_handshake_traffic_secret.
[] Indicates messages protected using keys derived from [sender]_application_traffic_secret_N.

翻译过来就是:

    • 表示该报文中值得注意的extension
    • 表示该内容也可能不被发送
  • {} 表示该内容使用handshake_key加密
  • [] 表示该内容使用application_key加密

可以看到 Key Exchange 被删除了,加密套件的传输直接放到ClientHello里面,通过1-RTT的ClientHelloServerHello双方就可以协商出对称加密密钥(或会话加密密钥)。

对比的TLS 1.2协议传输流程:

      ClientHello                  -------->
                                                      ServerHello
                                                     Certificate*
                                               ServerKeyExchange*
                                              CertificateRequest*
                                   <--------      ServerHelloDone
      Certificate*
      ClientKeyExchange
      CertificateVerify*
      [ChangeCipherSpec]
      Finished                     -------->
                                               [ChangeCipherSpec]
                                   <--------             Finished
      Application Data             <------->     Application Data

重点优化

除了TLS握手流程的调整,TLS1.3 还存在下面的一些重要优化和改进。

子协议优化

TLS 1.3包括3个子协议——Alert、Handshake、Record
Handshake:协议负责协商使用的TLS版本、加密算法、哈希算法、密钥材料和其他与通信过程有关的信息,对服务器进行身份认证,对客户端进行可选的身份认证,最后对整个握手阶段信息进行完整性校验以防范中间人攻击,是整个TLS协议的核心。

Alert:协议负责对接收到的报文进行加密解密,将其分片为合适的长度后转发给其他协议层。Alert层负责处理TLS连接过程中的各种异常情况,对每种情况发送一个alert报文,报文中附加一些错误处理需要的必要信息,TLS 1.3中定义了30种alert报文。

Alert 协议内容会在下文介绍。

Record:负责处理消息传输与握手阶段中的异常情况。Record 协议内容也会在下文进行介绍。

和TLS1.2不同的是TLS1.3 删除了变更密码规范协议(Change Cipher Spec Protocol) ,密钥的使用和改变随着服务器和客户端状态的改变自然进行。

TLS 1.2 协议的主要子协议内容:

  • 记录协议(Record Protocol)
  • 警报协议(Alert Protocol)
  • 握手协议(Handshake Protocol)
  • 变更密码规范协议(Change Cipher Spec Protocol)

PSK(pre_shared_key)新身份认证机制

PSK

PSK 是一种需要一定满足条件的身份认证机制,主要作用有三点:

  • 提高身份认证的速度。
  • 取代会话Session Id 进行会话重用。
  • 0-RTT握手。(一定的安全性作为代价)

PSK和(EC)DHE密钥是TLS1.3其中一种主要密钥交换算法,PSK可以与(EC)DHE密钥交换一起使用,两者并不冲突,比如PSK可以与(EC)DHE结合提供更强的安全性,当然PSK也可以单独使用,单独使用的时候主要作为TLS1.3的会话重用以及实现0-RTT。

注意,如果服务器是通过PSK进行认证,那么它不会发送证书或证书验证消息,从下面的交换步骤 了解到, 当客户机通过PSK尝试复用连接时,应该向服务器提供一个 “key_share “扩展放在扩展字段,同时允许服务单拒绝连接复用,并且在需要时回退到完整握手。

PSK可以认为是对于身份认证这一步骤进行加速的方式,也可以看作是Session Ticket机制(也叫做SSL session resumption)的一个升级。

TLS握手结束后,服务器可以发送一个NST(new_session_ticket)的报文给客户端,该报文中记录PSK的值、名字和有效期等信息,双方下一次建立连接可以使用该PSK值作为初始密钥材料。

SSL session resumption 的原理是在服务端缓存所有的session,客户端之后在每次和服务端握手时通过Session ID完成session resumption。

而在RFC的标准中pre_shared_key(PSK)的主要使用流程如下:

  
          Client                                               Server
    # 初始连接
   Initial Handshake:
          ClientHello
          + key_share               -------->
                                                          ServerHello
                                                          + key_share
                                                {EncryptedExtensions}
                                                {CertificateRequest*}
                                                       {Certificate*}
                                                 {CertificateVerify*}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Certificate*}
          {CertificateVerify*}
          {Finished}                -------->
                                    <--------      [NewSessionTicket]
          [Application Data]        <------->      [Application Data]

    # 第二次连接
   Subsequent Handshake:
          ClientHello
          + key_share*
          + pre_shared_key          -------->
                                                          ServerHello
                                                     + pre_shared_key
                                                         + key_share*
                                                {EncryptedExtensions}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Finished}                -------->
          [Application Data]        <------->      [Application Data]
注意:上面的处理流程中,PSK处理方式看起来类似Cookie,但是实际上差别很大,完全不是这么一回事。

直接对比找不同,PSK的密钥交换方式把证书校验、身份校验,密钥计算等比较费时间的校验这些步骤进行简化

 {CertificateRequest*}
{Certificate*}
{CertificateVerify*}

0-RTT

0RTT

PSK(pre_shared_key)出现的另一个主要目的是实现0-RTT。

所谓的0-RTT,指的是在TLS1.2当中,为了协商密钥和加密算法,需要耗费2个RTT的时间进行密钥的交换和确认(握手时间消耗),所以TLS1.2效率比较低。

为了优这一步骤TLS1.3 使用“Extension”字段,在简化加密套件的前提下,把加密套件所需要的信息通过第一次的ClientHello就完成传输,同时在ServerHello返回之后后续的所有请求都是加密传输,进一步提高SSL握手效率。

所以TLS1.3 实现了四次握手转为三次握手,在初次交互的时候由于双方没有 Key Share,所以依然需要1-RTT的数据交换操作,但是一旦双方都持有PSK就可以复用连接并且缩短至0-RTT。

注意:0-RTT 的代价是防不住重放攻击,这一点在“扩展知识”介绍协议的一些细节。

在RFC的标准中,客户端通过第一次返回的PSK传输来实现0-RTT验证,如果验证失败则立刻停止握手,通过下面的交互步骤可以发现从握手开始的那一刻就已经是加密传输了,所以整个HTTPS的校验很快。

但是0-RTT有个致命问题,那就是无法完全防住重放攻击,当然解决0-RTT的副作用办法也有很多种,比如只允许幂等安全的 GET / HEAD 方法,在消息里加入类似token校验的时间戳验证、“nonce”验证,或者“一次性票证”等限制重放攻击。

psk_key_exchange_modes

psk_key_exchange_modes 是为了配合PSK而出现的,也叫做预共享密钥交换模式,为了使用 PSK,客户端还必须发送“psk_key_exchange_modes”扩展名。这个扩展的语义是客户端仅支持在这些模式下使用 PSK。

如何防止 psk_key_exchange_modes 被滥用?RFC规定如果客户端选择 psk_key_exchange_modes ,但是并没有在扩展字段中传递 pre_shared_key(或者模式指定为psk_ke),则服务器应该立刻停止握手步骤,确保服务器不会使用客户端并没有指定的密钥交换而出现信息泄漏的风险。

psk_key_exchange_modes 有两个可选项:

  • psk_ke:表示仅 PSK 密钥创建,服务器此时不允许提供"psk_share"。
  • psk_dhe_ke:通过 EC)DHE 密钥创建的 PSK。在这种模式下,客户端和服务器必须提供“key_share”值。

更多psk_key_exchange_modes可以阅读RFC文档了解。

AEAD(Authenticated_Encrypted_with_associated_data)加密方式

TLS 1.3仅支持AEAD来校验数据完整性,AEAD包含对称加密和MAC计算两部分,TLS1.3的AEAD分为对称加密加密算法AEAD实质将完整性校验和数据加密两种功能集成在同一算法中完成

AEAD 功能提供统一的加密和认证操作,将明文转换为经过身份验证的密文然后再返回。每个加密记录由一个明文头和一个加密的正文组成,它本身包含一个类型和可选的填充。

之所以出现AEAD作为完整性校验和加密功能的“合体”,是因为在 TLS1.2中的CBC分块传输加密以及MAC校验完整性的方式是存在漏洞的,所以这个加密算法是一个“合体版”。

这里肯定会有疑问,CBC是啥?为啥说它是存在漏洞的?这一块涉及密码学和加密学基础,个人在“扩展知识”中的“安全密钥参考文章”中找到不错的文章,对于密码学感兴趣可以补充这部分内容。

具体请看“密码安全参考文章”部分。

TLS 1.3 支持的AEAD算法有三种:AES-CCMAES-GCMChaCha20-Poly1305

AEAD算法AEAD方法优缺点对比使用场景举例
AES-CCM(AES-CTR + CBC-MAC)Encrypt-and-MAC受限于CBC模式特性,不能充分发挥并行处理的优势TLS 1.2/1.3、IPsec、WLAN WPA2、BT LE等
AES-GCM<br/>(AES-CTR + Galois-MAC)Encrypt-then-MAC可以充分利用并行处理提高效率TLS 1.2/1.3、IPsec、WLAN WPA3、SSH等

HKDF(HMAC_based_key_derivation_function)

HKDF是对于PRF算法的进一步升级,用于取代PRF函数计算主密钥“Master Secret”获取更高的安全性以及随机性。HKDF在PRF基础上通过使用协商出来的密钥材料,和握手阶段报文的哈希值作为输入,可以输出安全性更强的新密钥。

HKDF包括 extract_then_expand 的两阶段过程,extract过程增加密钥材料的随机性能,expand则是加固密钥材料的安全性,注意PRF投入使用中实际只是实现了HKDF的expand部分,所以不算是完整的HKDF算法。

最后可以看一份wiki上使用Python实现的HKDF代码案例:

#!/usr/bin/env python3
import hashlib
import hmac
from math import ceil

hash_len = 32

def hmac_sha256(key, data):
    return hmac.new(key, data, hashlib.sha256).digest()

def hkdf(length: int, ikm, salt: bytes = b"", info: bytes = b"") -> bytes:
    """Key derivation function"""
    if len(salt) == 0:
        salt = bytes([0] * hash_len)
    prk = hmac_sha256(salt, ikm)
    t = b""
    okm = b""
    for i in range(ceil(length / hash_len)):
        t = hmac_sha256(prk, t + info + bytes([i + 1]))
        okm += t
    return okm[:length]

okm = hkdf(length=42,
           ikm=bytes.fromhex('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'),
           salt=bytes.fromhex('000102030405060708090a0b0c'),
           info=bytes.fromhex('f0f1f2f3f4f5f6f7f8f9'))
assert okm == bytes.fromhex(
    '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865')

DHE密钥协商机制

TLS 1.3 对于ECHED算法本身做了一些改良,比如在椭圆曲线函数选择上,TLS1.3做的比TLS1.2更加完善,在TLS1.2中的流程是双方先由客户端选择支持算法算法,然后服务端通过选择确定使用ECHED算法,然后由 服务端最终选择的函数曲线,所以步骤较为固定,双方各需要根据协定好的函数曲线选择合适的参数。

而TLS1.3 则是由 双方共同决定,因为在客户端和服务端都需要传递一些提示信息,详细内容可以看下面的TLS1.3抓包(这里截取部分内容介绍),对于椭圆域DH是椭圆曲线和基点的值,同选定加密组件一样,TLS 1.3定义了几组gp值,利用Group进行标识,因为Hello阶段还是明文,所以下面使用了x25519 就是推荐使用的安全椭圆曲线函数,具备安全通知的同时可以达到协商的目的。

Extension: key_share (len=38)
    Key Share extension
        Key Share Entry: Group: x25519, Key Exchange length: 32
            Key Exchange: b6fd2a1b723b0b11c648b3bee3f5412323423f28fa3ab797e57a4cde3ab1fe28

整个协商的过程和PSK的协商有点类似,都是先生成一个列表,然后每个Group 生成密钥的交换参数,内容同样和PSK放到了扩展字段 key_share 当中,服务端决定好DH密钥之后,再通过Group封装返回。

其他改进

New Session Ticket(NST)报文

NST 的内容属于 Post-Handshake Messages 目录的一部分。

New Session Ticket(NST)这部分内容个人建议学习的时候对比TLS1.2 的Session Id,Session Ticket以及TLS1.3 的PSK了解整个会话重用的过程,而这里因为篇幅所限仅仅概括一下 NST主要的功能和作用:

  1. NST主要用于参与PSK密钥的计算,双方通常会在进行 application_key(主密钥) 密钥协商计算完成之后,计算出 resumption_key(恢复会话主键),然后使用 resumption_key 与 PSK初始值做HKDF计算才会得到真正的PSK值,用于下一次TLS连接的建立。
  2. NST报文在连接建立后(即客户端发送完Finished报文后),由服务端发送给客户端,包含PSK初始值和PSK名字、有效期、用途限定等信息。
  3. NewSessionTicket 使用 server_application_traffic_secret 加密,通过 resumption_key 和 HKDF 算法计算出PSK的公式如下:
   HKDF-Expand-Label(resumption_master_secret,
                        "resumption", ticket_nonce, Hash.length)

关于这个结构详细的更多内容可以参考:4.6.1 New Session Ticket Message.

原文描述:
At any time after the server has received the client Finished message, it MAY send a NewSessionTicket message. This message creates a unique association between the ticket value and a secret PSK derived from the resumption master secret (see Section 7).

在服务器收到客户端的 Finished 消息后的任何时候,它都可以发送 NewSessionTicket 消息。 此消息在票证值和从恢复主密钥派生的秘密 PSK 之间创建唯一关联(参见第 7 节)。

Record 子协议

Record 子协议可以从这部分开始看起:5. Record Protocol

Record 有点儿类似TCP报文格式的定义,在TLS1.2提到过HTTPS是处于TCP和HTTP中间的“夹层协议”,因为TCP是HTTP的下层协议,所以HTTPS的报文和数据内容格式,和TCP的报文基本类似,但是要比TCP的报文要简单不少。

Record协议的标准部分一上来就给这个协议做了一个定义:

   The TLS record protocol takes messages to be transmitted, fragments
   the data into manageable blocks, protects the records, and transmits
   the result.  Received data is verified, decrypted, reassembled, and
   then delivered to higher-level clients.

    TLS 记录协议将要传输的消息分片,将(加密)数据分成可管理的块,保护记录并传输结果。 收到数据经过验证、解密、重组,然后传递给更高级别的客户端。

Record 协议也是使用了分片的思想,同时分片的内容是需要加解密的报文。Recod 对于报文一些下面的限制:

  • 每个record都有长度限制。
  • 不同的密钥消息内容不能存在于一个Record当中,一些变更信息都要通过单独的Key发送。
  • 对于Alert 的消息处理比较特殊,不能进行分片。
  • application data:对TLS协议不可见,所以没有对应的处理规则。
  • handshake message:握手信息,传输数据中间不能夹杂其他信息。

加密解密使用AEAD,使用AEAD加密机制和协商出来的加密算法对消息报文进行加密 。
AEADEncrypted = AEAD-Encrypt(write_key(即加密密钥), nonce, plaintext),

下面是有关AEAD加密的流程图:

TLS 1.3 中 AEAD 算法将单个密钥,随机数,明文作为输入附加参数,整体流程和TLS1.2有点类似,在细节上优化和处理等。

TLS 1.2 实际上也有AEAD加密,TLS 1.3 的加密在细节上做了调整,比如Nonce的生成方式变了,序列号在TLS1.2是additional_data,到了TLS1.3 算到了Nonce当中,并且可以发现TLS1.3参与计算的additional_data 的两个头部字段是固定字节 (opaque_type = 23、legacy_record_version = 0x0303)

加密和解密的过程是刚好反过来的后TLS 1.3通过TLS对消息报文填充来阻止攻击者获知传送消息的长度等信息,所以在双方解密报文的时候还需要多做一步,那就是把末尾的0给去掉才能正确解密。

Alert 协议扩展

标准部分可以从这里开始看:6. Alert,TLS 1.3 更多是对于Alert 协议扩展,这里简单对比一下TLS1.2的Alert内容,TLS 1.3 Alert 枚举定义如下:

      enum {
          close_notify(0),
          unexpected_message(10),
          bad_record_mac(20),
          record_overflow(22),
          handshake_failure(40),
          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),
          protocol_version(70),
          insufficient_security(71),
          internal_error(80),
          inappropriate_fallback(86),
          user_canceled(90),
          missing_extension(109),
          unsupported_extension(110),
          unrecognized_name(112),
          bad_certificate_status_response(113),
          unknown_psk_identity(115),
          certificate_required(116),
          no_application_protocol(120),
          (255)
      } AlertDescription;

TLS 1.2 Alert 枚举定义:

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;

可以看到TLS1.3的Alert 依然是对于TLS1.2的扩展,这里举两个新增的例子:

  • unrecognized_name(112):当没有识别出服务器时,由服务器通过“server_name”扩展名发送给客户端提供的名称(参考 [RFC6066])。
  • unknown_psk_identity:当需要建立 PSK 会话重用连接但客户端没有提供受到认可的的 PSK 身份时由服务器发送(注意可选发送), 服务器可能会改为发送“decrypt_error”警报以仅指示无效的 PSK 身份。

TLS 1.3 抓包

抓包将会参考TLS1.3 的交互流程进行介绍:

 Client                                           Server

Key  ^ ClientHello
Exch | + key_share*
     | + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       -------->
                                                  ServerHello  ^ Key
                                                 + key_share*  | Exch
                                            + pre_shared_key*  v
                                        {EncryptedExtensions}  ^  Server
                                        {CertificateRequest*}  v  Params
                                               {Certificate*}  ^
                                         {CertificateVerify*}  | Auth
                                                   {Finished}  v
                               <--------  [Application Data*]
     ^ {Certificate*}
Auth | {CertificateVerify*}
     v {Finished}              -------->
       [Application Data]      <------->  [Application Data]

这里偷懒找了一个已经支持TLS1.3的个人博客直接拿来用了,访问速度确实挺快的。首先我们需要在浏览器按下F12,通过检查Security的部分,以此查看目标网站是否支持TLS1.3。

测试网站:https://halfrost.com/tls1-3_start/。除了这个网站之外,也可以直接用最为熟知的全球最大同性交友网站 github,也是支持TLS1.3的。

下面是否是否支持TLS1.3的网页截图:

TLS1.3支持网页截图

2.1 Client_hello

首先我们看WireShanker抓包,个人使用了 WIFI所以抓的是WIFI对应的端口流量。在 TLS1.3 中通过 ClientHelloServssser Hello的扩展字段进行密钥交换,简化了KeyExchange,实际上就是“新瓶装旧酒”换了个位置。

TLS 1.3 的Client Hello 大致是这样描述的:

Key  ^ ClientHello
Exch | + key_share*
     | + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       -------->

抓包报文:

Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Random: bab59bd2b9007afce54bdb6e3802c8b30d78b833e4aa21d7242858a08fae5da5
Session ID: 23d30d7ff75c05fdc4d7f49c0a4e84fc88dd123e167c91f9e5e3d29bc85cc46e
Cipher Suites (19 suites)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)
Extension: signature_algorithms (len=26)
    Signature Hash Algorithms (12 algorithms)
        Signature Algorithm: rsa_pss_rsae_sha256 (0x0804)
        Signature Algorithm: ecdsa_secp256r1_sha256 (0x0403)
        Signature Algorithm: ed25519 (0x0807)
Extension: supported_versions (len=5)
    Supported Version: TLS 1.3 (0x0304)
    Supported Version: TLS 1.2 (0x0303)
Extension: supported_groups (len=10)
    Supported Groups (4 groups)
        Supported Group: x25519 (0x001d)
        Supported Group: secp256r1 (0x0017)
        Supported Group: secp384r1 (0x0018)
        Supported Group: secp521r1 (0x0019)
Extension: key_share (len=38)
    Key Share extension
        Key Share Entry: Group: x25519, Key Exchange length: 32
            Key Exchange: b6fd2a1b723b0b11c648b3bee3f5412323423f28fa3ab797e57a4cde3ab1fe28
            

首先握手协议的前面几行介绍了整个报文的长度,以及Version: TLS 1.2 (0x0303)版本协议,由于TLS1.3 需要伪装成TLS1.2 向后兼容,所以这个版本号需要固定为0x0303,而扩展字段·supported_versions 则设计为专为TLS1.2升级TLS1.3使用,Supported Version版本从大到小排列,最终确认使用TLS1.3协议。

没用的知识:注意这里在TLS1.3刚发布的时候,Chrome或者FireFox浏览器可能因为浏览器的过旧的原因退化到TLS1.2握手,当时需要升级最新版本。

接着我们简单介绍其他参数:

client_random 依然是作为后续对称加密曲线重要参数。

Session ID:TLS1.3中不再使用Session ID进行会话恢复,这一特性已经和预共享密钥PSK合并了,PSK也是TLS1.3推荐的密钥交换方式之一。这里设置这个字段的意义主要也是为了兼容之前版本,同时 Client 发现如果服务端存在 TLS 1.3 版本之前的 Server 设置的缓存 Session ID,那么这个字段必须要填上对应 ID 值保持一致。

兼容模式下这个值必须是非空的,所以如果Client不能支持TLS1.3,那么需要重新生成一个32字节的值。但是如果支持TLS1.3,Session ID 必须是一个长度为0的矢量。

总之RFC设置了一些细节规定Session ID兼容和使用问题,在TLS1.3中我们的重心更应该放在PSK上,因为Session ID 是非常传统的会话握手关键字段。

signature_algorithms 标识了客户端所选择的算法,上面看似很多的加密套件,实际上在TLS1.3 中能选择加密只有一个巴掌的数量,传输一些其他的加密算法同样是为了兼容和有可能的重新协商握手考虑。

Cipher Suites 指的是使用的非对称加密套件,客户端提供列表由服务端进行选择。

supported_groups 表示的是ECDHE算法所需要的内容信息,比如用于计算出椭圆函数曲线公钥

psk_key_exchange_modes :由于本次抓包并不满足传递PSK的条件,所以这里看不到此参数。

pre_shared_key:本次抓包不满足PSK的条件,所以扩展字段 并没有传递 key_share。

可以看到TLS1.3 的 ClientHello相比于TLS1.2 看上去多了很多东西,但是实际上整体流程就是把TLS1.2的KeyExchange部分全部加入到了扩展字段当中。

Client Hello 的抓包到此告一段落,下面看看Sever Hello 多了哪些内容。

2.2 Server Hello

接下来是Server Hello,Supported Version 说明服务器识别了客户端的TLS1.3请求,故使用 Supported Version TLS 1.3 说明自身支持使用TLS 1.3进行握手。

这里注意末尾的Change Cipher Spec Message,服务器收到了客户端传输的数据之后,此时基本就已经有了客户端的加密套件和所需信息:Client RandomServer RandomClient ParamsServer Params,加上 HKDF 算法就可以算出对称加密的密钥,所以有了会话密钥之后,后续的传输都是对成加密的密文,进一步提高安全性。

HKDF 是对于 PRF的改进,这里简单回忆TLS1.2的PRF算法,结合TLS1.3得出下面的步骤:通过key_share传递的椭圆曲线函数密钥+Client ParamsServer Params,得出 pre_master_secret (这两个椭圆曲线公钥+复杂算法 = 随机数,叫做 PreMaster=pre_master_secret ),然后结合Client RandomServer Random+pre_master_secret 三个参数计算出对称加密密钥 master_secret 。HKDF所做的改进则是这个主密钥的计算过程,加入更多的变化,让这个主密钥更加具备随机性和难以猜测。

 master_secret = PRF(pre_master_secret, "master secret",
                          ClientHello.random + ServerHello.random)
                          [0..47];

TLS1.3 这里用 HKDF 对整个会话主密钥的计算做了改良。具体可以看 RFC 5869:基于 HMAC 的提取和扩展密钥派生函数 (HKDF) (rfc-editor.org)

HKDF-Extract(salt, IKM) -> PRK

HKDF-Expand(PRK, info, L) -> OKM

最后来看看Server Hello的报文:

Version: TLS 1.2 (0x0303)
Handshake Protocol: Server Hello
    Version: TLS 1.2 (0x0303)
    Random: 79982ab6bfaca82f4d8bce215262e597ed7c326e1e5dd5974f943ee566d2a03e
    Session ID: 1ebba1d46f1fc351617977ca86ba9441946ac19dcf4849848b589c64e24b1ffa
    Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
Extension: key_share (len=36)
    Key Share extension
        Key Exchange: d545f21e4c2e759a2b45735e554c789a5869c7921fbf780dc69f5ecd3df9a60f
Extension: supported_versions (len=2)
    Supported Version: TLS 1.3 (0x0304)
TLSv1.3 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec
    Version: TLS 1.2 (0x0303)
    Change Cipher Spec Message

2.3 Change Cipher Spec

计算出对称加密需要的主密钥之后,服务端会立马返回Change Cipher Spec”消息告知后面的内容都是密文传输,比 TLS1.2 提早进入加密通信这意味着后面的证书等信息都是加密的了,减少了握手时的明文信息泄露。

因为都是加密传输所以内容十分简短,这里简单截个图:

Change Cipher Spec

由于后续的内容都是加密传输,抓包看到的也都是密文并且没法分析,所以我们根据RFC标准流程进行简单介绍。

2.4 HelloRetryRequest

如果服务端无法识别客户端的加密参数信息,那么此时服务端会送一个HelloRetryRequest的信息,要求客户端重新发送符合要求的CH报文。

HelloRetryRequest具有与 ServerHello 消息相同的格式,和 legacy_version,legacy_session_id_echo、cipher_suite 和 legacy_compression_method 等字段具有相同的含义。

收到 HelloRetryRequest 后,客户端必须检查 legacy_version(TLS Version)、legacy_session_id_echo、cipher_suite 和 legacy_compression_method,然后处理扩展字段“supported_versions”确定版本开始。

RFC中规定,如果 HelloRetryRequest 不会导致 ClientHello 发生任何变化,则客户端必须使用“illegal_parameter”警报中止握手。如果客户端在同一连接中接收到第二个 HelloRetryRequest(即,ClientHello 本身就是响应 HelloRetryRequest 的地方),它必须使用“unexpected_message”警报中止握手。

HelloRetryRequest可以看作是服务端不认识客户端加密算法的情况下,进行再一次的密钥协商和TLS1.3握手尝试。

2.5 Encypted Extension

服务器在使用加密传输之后,接着会传输Encypted,里面包含其他与密钥协商无关的扩展数据给客户端。

Encypted Extension

2.6 CertificateRequest(CR)

如果使用公钥证书进行身份认证,服务端此时需要发送Certificate报文(传递自己的证书信息)和上面提到的Certificate Verify(CV)报文,在报文里面使用自己的证书私钥对之前的报文进行HMAC签名证明自己持有该证书,之后传输给给客户端。

在[[HTTP面试题 - HTTPS 优化]]中可以了解到,CR的过程是可以被提前到握手之前的,下面这个流程如果经过了HTTPS优化是可以做到客户端发送请求进行HTTPS握手之前完成CA验证,提高整个握手效率。

这里继续结合之前TLS1.2所学,介绍CR的过程流程图:

CR过程

2.7 CertificateVerify(CV)

CertificateVerify 将会结合之前所有的数据加上HMAC摘要算法做一个证明,然后在报文里面使用自己的证书私钥加密,然后传给CA进行加密处理,再一次证明自己可信程度。

2.8 Finished

服务端发送Finished报文。表明服务端到客户端信道的握手阶段结束,理论上不得再由该信道发送任何握手报文。到达这一步,之后的内容是开始传输 Application Data,也就是应用程序数据,这些已经超出TLS协议的范畴了,不多分析:

2.9 Certificate

这里的Certificate指的是客户端收到服务端索要证书的请求开始发送证书,注意不要和前面的步骤混淆。

2.10 Finished

客户端发送Finished报文,表明握手阶段结束,双方可以进行正式通讯了。

Finished报文使用会话密钥以及之前上述所有握手信息进行HMAC签名,校验签名可以检验握手阶段的完整性,也可以验证双方是否协商出了一致的密钥,同时进一步验证服务端的安全性。

注意以上所有握手阶段的报文都是由record协议层加解密、分片、填充、转发的。

在这个过程中,如果发生了任何错误(如:服务端证书验证失败、完整性校验错误),则会发送一个alert报文(警报),转交给alert协议层进行错误处理。

TLS 1.3 流程图

我们分析完抓包之后,笔者对于整个TLS 1.3交互流程图画一个流程图,TLS1.3 要比TLS1.2简单很多,少了一次握手的也是最为直观的感受:

TLS 1.3 流程图

扩展知识

更深入的学习

因为TLS本身就是和密码学息息相关的标准,所以如果要吃透TLS必然需要对于密码学进一步探究,如果要研究密码学,可以参考这一篇文章:# TLS协议分析 与 现代加密通信协议设计

去研究密码学十分耗费精力,如果精力有限,可以看看微信在基于TLS1.3 早期草案(文章2017年,但是2018年TLS1.3才正式定版)的而设计的mmtls介绍文章,虽然过去很多年,但是对于RFC标准的落地实践依然具有一定的参考价值:
基于TLS1.3的微信安全通信协议mmtls介绍.md

如果具备一定的英文水平,并且想看看老外如何介绍TLS1.3,个人比较建议找找老外的文章,比较推荐cloud flare发布的TLS1.3介绍合集,算是比较受到广泛认可的资料。

老外做出来的东西肯定是外国人更能领悟,毕竟学习和生活环境以及思考方式都有很大差异,外国人理解起来总是快那么一些。

cloud flare公司对TLS 1.3的介绍博文合集(英文)

如果想看TLS1.3 是如何一步步讨论出来的,除了看还有人专门整理了一个Github,当然没除非真的很闲,否则不建议看这一份资料:

 TLS 1.3草案合集,可以从这里检索到最新版的TLS1.3草案(英文)

最后是16年NDSS会议研究TLS,这部分内容很多已经无法访问了,但是有些内容还可以看个大概。

16年NDSS会议研究TLS 1.3的论文合集(英文)

最后如果还想再硬核一些,想更深入了解TLS1.3标准部分的细节内容,可以从霜神大佬的系列文章有关TLS1.3介绍,很多内容是对于RFCTLS 1.3的整理,可以借助TLS1.3的文档理解:

https://halfrost.com/https_tls1-3_handshake/

JDK 11 和 TLS1.3

原文:https://blog.gypsyengineer.com/en/security/an-example-of-tls-13-client-and-server-on-java.html

作者从事JAVA安全库的工作长达6年,提供的信息比较准确。因为TLS1.3 是2018年出现的,所以作为JDK8的钉子户,如果要使用JAVA对接TLS1.3必须要 JDK 11

但是需要注意JDK11版本的TLS1.3协议支持并不是十分完善,在 JEP332中提供了下面的支持

  • Protocol version negotiation(协议版本协商)
  • Full handshake for both client and server sides(客户端和服务器端的完全握手)
  • Session resumption(会话重用)
  • Key and IV update(密钥和 IV 更新)
  • Updated OCSP stapling(OCSP 重新修订)
  • Backward compatibility mode(向后兼容模式)
  • Required extensions and algorithms(所需的扩展和算法)
  • Two new cipher suites: TLS_AES_128_GCM_SHA256 and TLS_AES_256_GCM_SHA384(新的密码套件)
  • RSASSA-PSS signature algorithms(签名算法)
  • Both SSLSocket and SSLEngine(SSLSocket 和 SSLEngine)

不支持的内容(注意以JDK11版本为例):

  • 0-RTT data 0-RTT 数据
  • Post-handshake authentication握手后认证
  • Signed certificate timestamps (SCT) 签名证书时间戳 (SCT)
  • ChaCha20/Poly1305 cipher suites (targeted to Java 12) ChaCha20/Poly1305 密码套件(针对 Java 12
  • x25519/x448 elliptic curve algorithms(针对 Java 12
  • edDSA signature algorithms(针对 Java 12

Java 11 没有为 TLS 1.3 引入新的公共类和方法。 它只是为新协议名称、密码套件等添加了几个新常量,使用常量的好处是只需要升级使用的算法和升级JDK等操作,代码逻辑不需要进行改进,这十分方便。

Java 13 中的 TLS 增强

作者还是上面那一位老哥(白嫖知识感觉真爽):https://blog.gypsyengineer.com/en/security/tls-enhancements-in-java-13.html

Java 13 于 2019 年 9 月 13 日发布。虽然新的 Java 不包含安全库中的重大更新,但在 TLS 实现中有几个值得注意的更新。 下面看看几个需要关注的升级点:

第一个升级点:默认加密套件的选择顺序

这里不多废话,直接看变化:
改变之前:

  • ECDHE-ECDSA
  • ECDHE-RSA
  • RSA
  • ECDH-ECDSA
  • ECDH-RSA
  • DHE-RSA
  • DHE-DSS

改变之后:

  • ECDHE-ECDSA
  • ECDHE-RSA
  • DHE-RSA
  • DHE-DSS
  • ECDH-ECDSA
  • ECDH-RSA
  • RSA

简单来说就是具备前向安全性以及加密安全程度更高的算法往前调整,TLS1.3已经把RSA废弃,这里个人并不太理解为什么JAVA不把RSA直接干掉。

第二个升级点:椭圆函数曲线升级

X25519 和 X448 是到目前为止公认最为安全的两个椭圆函数曲线,JDK13 Java 13 支持 TLS 版本 1.0、1.1、1.2 和 1.3 的 x25519 和 x448 椭圆曲线,x25519 优先级最高,而 x448 则遵循可选曲线分支,最终的顺序是:x25519, secp256r1, secp384r1, secp521r1, x448, ... 这些细节被放到`jdk.tls.namedGroups当中。

第三个升级点:无状态服务

所谓的无状态服务,实际上指的是0-RTT,实现的方式是JDK官方为了给TLS 连接加速,推荐使用PSK密钥交换算法进行连接。

下面是JAVA代码的案例,只是它使用了新的常量“TLSv1.3”和“TLS_AES_128_GCM_SHA256”:

package com.gypsyengineer.tlsbunny.jsse;

import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;

/*
 * Don't forget to set the following system properties when you run the class:
 *
 *     javax.net.ssl.keyStore
 *     javax.net.ssl.keyStorePassword
 *     javax.net.ssl.trustStore
 *     javax.net.ssl.trustStorePassword
 *
 * More details can be found in JSSE docs.
 *
 * For example:
 * 
 *     java -cp classes \
 *         -Djavax.net.ssl.keyStore=keystore \
 *         -Djavax.net.ssl.keyStorePassword=passphrase \
 *         -Djavax.net.ssl.trustStore=keystore \
 *         -Djavax.net.ssl.trustStorePassword=passphrase \
 *             com.gypsyengineer.tlsbunny.jsse.TLSv13Test
 *
 * For testing purposes, you can download the keystore file from 
 *
 *     https://github.com/openjdk/jdk/tree/master/test/jdk/javax/net/ssl/etc
 */
public class TLSv13Test {

    private static final int delay = 1000; // in millis
    private static final String[] protocols = new String[] {"TLSv1.3"};
    private static final String[] cipher_suites = new String[] {"TLS_AES_128_GCM_SHA256"};
    private static final String message =
            "Like most of life's problems, this one can be solved with bending!";

    public static void main(String[] args) throws Exception {
        try (EchoServer server = EchoServer.create()) {
            new Thread(server).start();
            Thread.sleep(delay);

            try (SSLSocket socket = createSocket("localhost", server.port())) {
                InputStream is = new BufferedInputStream(socket.getInputStream());
                OutputStream os = new BufferedOutputStream(socket.getOutputStream());
                os.write(message.getBytes());
                os.flush();
                byte[] data = new byte[2048];
                int len = is.read(data);
                if (len <= 0) {
                    throw new IOException("no data received");
                }
                System.out.printf("client received %d bytes: %s%n",
                        len, new String(data, 0, len));
            }
        }
    }

    public static SSLSocket createSocket(String host, int port) throws IOException {
        SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault()
                .createSocket(host, port);
        socket.setEnabledProtocols(protocols);
        socket.setEnabledCipherSuites(cipher_suites);
        return socket;
    }

    public static class EchoServer implements Runnable, AutoCloseable {

        private static final int FREE_PORT = 0;

        private final SSLServerSocket sslServerSocket;

        private EchoServer(SSLServerSocket sslServerSocket) {
            this.sslServerSocket = sslServerSocket;
        }

        public int port() {
            return sslServerSocket.getLocalPort();
        }

        @Override
        public void close() throws IOException {
            if (sslServerSocket != null && !sslServerSocket.isClosed()) {
                sslServerSocket.close();
            }
        }

        @Override
        public void run() {
            System.out.printf("server started on port %d%n", port());

            try (SSLSocket socket = (SSLSocket) sslServerSocket.accept()) {
                System.out.println("accepted");
                InputStream is = new BufferedInputStream(socket.getInputStream());
                OutputStream os = new BufferedOutputStream(socket.getOutputStream());
                byte[] data = new byte[2048];
                int len = is.read(data);
                if (len <= 0) {
                    throw new IOException("no data received");
                }
                System.out.printf("server received %d bytes: %s%n",
                        len, new String(data, 0, len));
                os.write(data, 0, len);
                os.flush();
            } catch (Exception e) {
                System.out.printf("exception: %s%n", e.getMessage());
            }

            System.out.println("server stopped");
        }

        public static EchoServer create() throws IOException {
            return create(FREE_PORT);
        }

        public static EchoServer create(int port) throws IOException {
            SSLServerSocket socket = (SSLServerSocket)
                    SSLServerSocketFactory.getDefault().createServerSocket(port);
            socket.setEnabledProtocols(protocols);
            socket.setEnabledCipherSuites(cipher_suites);
            return new EchoServer(socket);
        }
    }
}

HTTPS VS HTTP 性能

HTTP vs HTTPS — Test them both yourself

这两个页面 加载时间再个人的对比结果有点大跌眼镜,下面的两个页面都是在Edge浏览器下测试的,会发现HTTPS居然要比HTTP快?这是为什么?个人的猜测是这个网站的HTTP是未经过优化的,而HTTPS是经过优化的,所以整个加载过程反而比HTTP快很多。

而原因可能是多方面的,比如0-RTT,HTTP/2 等,这个测试结果是告诉我们不要以绝对的眼光看待某件事情。

[[HTTP面试题 - HTTPS 优化]]

HTTP测速

HTTPS测速

Replay Attacks on 0-RTT

原文:https://www.rfc-editor.org/rfc/rfc8446#appendix-E.5

这里仅个人理解简单说明一下,0-RTT最大的威胁在于弱安全性,而最大的威胁是重放攻击,什么是重放攻击?这里官方给出了一些潜在攻击手段。

官方将0-RTT 的攻击分为两个大类,第一个大类是简单复制非幂等性请求进行重放攻击(比如付款,转账等只能做一次的操作),第二个大类则是大量利用重放测试幂等性接口,比如资源耗尽或者定向攻击,或者利用0-RTT缓存测试资源在不同的服务器是否有不同表现。

第一类攻击可以通过共享状态来防止 0-RTT 数据最多被接受一次,但是并不说所有的运营商部署都会照做,也取决于服务器的实现。假设攻击者利用了早期ClientHello并且重放到A和B服务器,A是可以处理的,但是请求会被B服务器拒绝,假设攻击者挡掉A返回的ServerHello,那么此时消息继续重放,流量将会被迫交给B并且由B完成,那么整个服务器的请求都会被重复执行。

第一类攻击的处理关键是永远保证0-RTT只会被接受一次。 也就是说0-RTT 数据不能在一个连接中复制和重复使用(即服务器不会为同一连接处理两次相同的数据)。

第二类攻击比较针对业务下手,所以TLS是帮不上忙的,需要服务端应用程序通过应用代码解决,比如利用0-RTT 重放获取账户密码等敏感信息,这些接口通常都是幂等性的。幂等性在TLS1.3上无法保证安全,放任重放攻击是很容易遭到信息泄露。比如比较常见的做法是缓存处理校验或者直接从服务端做Nginx过滤等。

已知TLS1.2攻击手段

TLS攻击手段

所谓道高一尺,魔高一丈,我们补充一些有关TLS1.2以前(包含TLS1.2)的攻击手段,借此从侧面了解为什么TLS1.3一下子废弃了一大票算法,这些问题都和历史漏洞有着很深的渊源。

BEAST、BREACH、CRIME、FREAK、LUCKY13、POODLE、ROBOT。

BEAST:BEAST (CVE-2011-3389) 是一种明文攻击,通过从 SSL/TLS 加密的会话中获取受害者的 COOKIE 值(通过进行一次会话劫持攻击),进而篡改一个加密算法的 CBC(密码块链)的模式以实现攻击目录,其主要针对 TLS1.0 和更早版本的协议中的对称加密算法 CBC 模式。

CRIME:CRIME通过在受害者的浏览器中运行JavaScript代码并同时监听HTTPS传输数据,能够解密会话Cookie,主要针对TLS压缩。Server端可以通过关闭SSL/TLS压缩和HTTP压缩来避免CRIME/BREACH攻击,这也是TLS 1.3废弃压缩算法的原因,关闭这个压缩的影响无关紧要,因为HTTP/2和HTTP/3的首部压缩做的更为优秀和通用。

BREACH:BREACH攻击是CRIME攻击的升级版,攻击方法和CRIME相同,不同的是BREACH利用的不是SSL/TLS压缩,而是HTTP压缩。所以要抵御BREACH攻击必须禁用HTTP压缩

FREAK:衍生自美国的出口级密钥,512位的RSA密钥,这种加密方式可以便于情报机构和特殊机构破解利用(斯诺登事件爆出之前干的事情,见怪不怪了),这个漏洞编号为CVE-2015-0204,人们将它命名为FREAK(Factoring Attack on RSA-EXPORT Keys),

FREAK攻击经过检测有至少30%的网站存在出口级RSA加密漏洞,在上世纪90年代,破解512位的密钥需要出动超级电脑。而今天,我们只需要花费7小时+约100美金,就可以轻松搞定这种加密机制。

这里衍生出RSA的中间人攻击手段,作为一个扩展:

FREAK漏洞与POODLE(贵宾犬)漏洞的相似性
FREAK漏洞:利用了出口级RSA加密的算法版本。
POODLE(贵宾犬):通过降级套件的方式回退版本攻击,强迫终端用低版本SSL/T LS。
主要的区别是一个是针对出口级RSA算法,另一个利用SSL协议本身的低版本不安全性漏洞做手脚,比如TLS1.0和TLS1.1。

内容来源:“历史遗留”漏洞:浅析新型SSL/TLS漏洞FREAK - 腾讯云开发者社区-腾讯云 (tencent.com)

POODLE漏洞:古老但广泛应用的SSL 3.0加密协议中存在被称为POODLE(Padding Oracle On Downgraded Legacy Encryption)的严重漏洞,该漏洞允许攻击者解密加密连接的内容。

ROBOT:ROBOT是个首字母缩写,是丹尼尔·布雷琴巴赫于1998年发现的,意思是布雷琴巴赫Oracle威胁重现(Return Of Bleichenbacher’s Oracle Threat)。这是一个在1998年就发现的漏洞,该漏洞允许使用服务端的私钥执行RSA解密和签名操作。

更多内容可以阅读:# TLS ROBOT Attack漏洞

降级攻击

我们观察上面的的这些漏洞,会发现针对SSL协议的降级攻击是十分常见的,那么TLS1.3 是如何防范降级攻击的?

我们先看看降级攻击是上面意思,只要看下面这个图即可:

攻击过程如下:

  1. 客户端发送请求的时候,攻击者把客户端的加密算法替换成存在安全漏洞的低安全性算法,以此欺骗服务端降级返回弱安全密钥交换算法。
  2. 攻击者利用弱安全性密钥交换算法进行暴力破解,然后伪造Finish Message,双方按照密钥进行正常交互,这样后续所有的对称加密都是“透明传输”。
  3. 最终黑客通过这些数据获取到用户隐私数据。

针对这个攻击过程,我们接着分析TLS1.3 就会发现虽然Server Hello 之后的消息是密文传输的,但是在Client Hello 和Server Hello 中传输的还是明文,所以所有的加密算法是公开的,同样TLS1.3的ServerHello.random也是明文,可以通过wireshark抓包看到,所以TLS1.3 降级攻击是存在隐患的?

我们接着看一下TLS1.3 如何定义降级保护:

The cryptographic parameters should be the same on both sides and should be the same as if the peers had been communicating in the absence of an attack

上面这段话的意思是说,双方安全通信的前提是密码对等参数对等,注意这两个条件是“短路”的,其中任意一个条件不满足,则应该立刻停止握手,交给Alert处理。

首先是密码对等,所谓的密钥对等实现方式是在Verify里面把前面所有的参数合起来加一个HMAC进行签名校对,在RFC的文档中有下面的这样一段代码:

  finished_key =
       HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)

   Structure of this message:

      struct {
          opaque verify_data[Hash.length];
      } Finished;

   The verify_data value is computed as follows:

      verify_data =
          HMAC(finished_key,
               Transcript-Hash(Handshake Context,
                               Certificate*, CertificateVerify*))

上面的代码要求客户端和服务器都发送一个包含所有先前握手消息的 MAC 的 Finished 消息。其实就是TLS1.3握手过程的 CertificateVerify 这一步,verify_data 代表了这个计算过程的伪代码。

在以前版本的 TLS 中,verify_data 始终为 12 个八位字节长。在 TLS 1.3 中,它是 Hash 的 HMAC 输出的大小,然后用于握手。

有了密码对等是不够的,因为这些内容攻击者都可以在传输过程中伪造Finish 轻易替换,下面我们来看参数对等是如何进一步处理的。

这时候我们可以翻到 https://www.rfc-editor.org/rfc/rfc8446#section-4.1.3 这一部分,大致浏览看到RFC 的内容,会发现TLS的想出了一招在Server Random里面做手脚。

我们找到核心部分,Server Random 一旦被更改,那么服务器最后的8位字节会按照下面的规则替换:

  • 如果协商 TLS 1.2,那么最后 8 个字节必须是 44 4F 57 4E 47 52 44 01
  • 如果协商 TLS 1.1 或更旧的协议版本,那么最后 8 个字节必须是 44 4F 57 4E 47 52 44 00

也就是说如果Server Random不等于上面提到的任何一个值,则服务端必须要终止连接。同时这些个被改写的字节码实际上也有本身的意义,这几个字节编码翻译过来的前面七个字节表示DOWNGRD,这串单词的的含义是:降级

我们可以这样理解:

  • 我是服务器,我支持TLS1.3,我从一个客户端得到一个连接,它说它只支持 TLS 1.2 或更低版本,但是实际上我支持更高级的版本,我会把这些信息改写到Server Random的最后一段,你如果看到了确认一下哈。
  • 我是客户端,我收到了服务端的回信,但是我明明是TLS1.3 握手,怎么服务端让我用TLS1.2呢?不对劲,喔,服务端让我检查ServerHello.random是否包含“DOWNGRD”(降级)消息,我看了一下果然有,那么此时肯定是第三者在中间捣蛋,我和服务端需要立刻终止连接。

等等,还是有问题,你会问Server.random不是明文传输么,黑客删了怎么办?其实这种处理方式就是改了删了就让它改了无所谓,故意暴露弱点给黑客,如果被篡改我就立马再返回信息里面说明服务被降级了,客户端发现服务都被降级了,自己发的是TLS1.3,结果被换成TLS1.2了立刻停止握手。

而如果删了Server.random,别忘了还有椭圆函数曲线的加密算法是必须要Server Random 这个参数的,没了它对称加密密钥的计算进行不下去,也是有问题的。最终我发现发现密码对等参数对等两个条件其中一个被破坏了,双方没法正常交流,最终实现会话加密的安全。

不管是删了,改了,换了,都能检查出问题,最终降级保护实现。

最后留一个小问题:自然情况下TLS握手碰到协议末尾字节是 DOWNGRD + 00/01”的概率是多少?

前向安全:斯诺登事件

一些历史记忆,现在已经被人们逐渐淡忘了。斯诺登2013的“棱镜事件”。具体可以看维基百科的介绍:https://zh.wikipedia.org/wiki/%E7%A8%9C%E9%8F%A1%E8%A8%88%E7%95%AB

颜色越深监视越多

个人感受是互联网是老美发明的,利用互联网干坏事也是很正常的事情,而这些历史放到现在不过是云烟,因为大部分人的个人信息安全现在来看基本等于0。

棱镜计划(英语:PRISM)是一项由美国国家安全局自2007年开始实施的绝密级网络监控监听计划。1该计划的正式名称为“US-984XN”。

根据报导,泄露的文件中描述PRISM计划能够对即时通信和既存资料进行深度的监听。[5]许可的监听对象包括任何在美国以外地区使用参与计划公司服务的客户,或是任何与国外人士通信的美国公民。[5]国家安全局在PRISM计划中可以获得数据电子邮件、视频和语音交谈、视频、照片、VoIP交谈内容、文件传输、登录通知,以及社交网络细节,并透过各种联网设备,如智能手机、电子式手表等各式联网设备对特定目标进行攻击。[5]综合情报文件《总统每日简报》中在2012年中的1,477个计划里使用了来自棱镜计划的资料。

关于PRISM的报道,是在美国政府持续秘密要求威讯向国家安全局提供所有客户每日电话记录的消息曝光后不久出现的。7泄露这些绝密文件的是国家安全局合约外包商员工爱德华·斯诺登,于2013年6月6日在英国《卫报》和美国《华盛顿邮报》公开。

密钥安全参考文章

TLS 1.3 报文定义

TLS 1.3 一共定义了 12种报文,实际上大部分报文都是从TLS1.2继承下来的。

其他问题

根据这篇文章提出的一些补充疑问,以及提供相关解答。

  1. TLS1.3 里的密码套件没有指定密钥交换算法和签名算法,那么在握手的时候会不会有问题呢?
  2. 为什么 RSA 密钥交换不具有“前向安全”。
  3. PSK 真的安全吗?

第一个问题回答:TLS1.3精简了加密算法,通过 support_groupskey_sharesignature_algorithms 这些参数就能判断出密钥交换算法和签名算法,不用在cipher suite中协商了。

第二个问题回答:client key exchage会使用RSA公钥加密pre master后传给服务端,一旦私钥被破解,那么之前的信息都会被破译,根本原因还是在于RSA的这一对公钥私钥并不是临时的,而是生成出来就永久存放的,所以以往的做法是服务端针对。

第三个问题来自于个人所参考的文章一个评论的提问,下面是原文:

觉得psk不安全,前面连rsa私钥安全性都信不过,这里竟然信任一个本地临时存储的密钥。
通过rsa传送密码我认为不是私钥安全性问题,而是有两个另外的原因:
1) rsa密钥由单方生成,另一端无条件接受,这样的密钥接收端无法信任另一端的随机数能力、密钥管理能力。dh是两端参与,各自放一个随机数进去(而且各端还要对另一端发过来的因子做一定的检查),所以安全性更高
2) 完美前向兼容性。dh生成密钥以后,各自把临时随机数删除,密钥就只有内存应用,网络上也不怕破解。

PSK 第一眼看上去长得很像Cookie,但是实际上和Cookie的安全性是云泥之别,我们看看PSK是如何保证随机性和安全性的:

  1. 首先Client发一个NewSessionTicket,这里面会包含刷新的ticket,就是用来做Psk用的,也就是说每次完成握手之后都会超过过期时间都会刷新掉票证,也就是所谓的“一次性票证”,一次性票证的官方建议时间是不超过24小时。
  2. 注意连接握手中间有一个verify 的过程,这是双向都需要进行的一次校验,这一步要把之前的所有参数合并,用HMAC做一个密码进行比对。
  3. 每次握手的HandleShake会被Hash计算动态生成crypt key,同时Psk分还为PSK_ES与PSK_SS,两个Psk的生命周期也不一样,前者只会要求生成密码,后者则是握手后用于下一次访问携带的PSK。

post-authentication 机制

post-authentication机制 用于客户端确认是否向服务端提供信息,主要是用于客户端认证增强,在握手流程当中,CH的报文会额外加入扩展字段post_handshake_anth,客户端会在收到CR之后再发送CT、 CV报文给服务端进行身份认证。

但是这个机制存在许多未被解决问题,具体介绍之前我们先说结果是不建议 使用或者禁用,下面这篇文章讨论可以看到一些答案:

https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-http2-tls13

笔者这里挑了讨论中比较重要的内容来说明为什么不建议使用:

TLS 1.2 with HTTP/1.1:使用 TLS 重新协商。这产生了非常多的遗留问题导致无法重新协商。

TLS 1.3 with HTTP/1.1:使用 TLS 握手后身份验证(post-authentication机制)。没有被广泛实施,官方的文档https://tools.ietf.org/html/draft-ietf-httpbis-http2-tls13ye 没有解释内部的冲突和歧义问题,所以根本问题还是破坏 HTTP/2,破坏文档规范。

直接问题:任何带有 HTTP/2 的 TLS 版本上述机制都不起作用,因为 HTTP/2 是多路复用的。握手后客户端证书身份验证本质上是一种几乎无法奏效的 hack,因为 HTTP/1.1 是一个足够简单的协议,可以抵御分层违规。https://tools.ietf.org/html/draft-ietf-httpbis-http2-secondary-certs描述了一种填补这一空白的机制,但它仍然是一个草案。

PS:客户端认证在实际应用上非常少,所以可以看到基于客户端认证的内容无论是RFC还是实际的文章讨论都不是特别详细,和在服务端认证上的细节则非常庞大。

总结

最后我们在对比TLS1.2 和 TLS 1.3,看看主要做了哪些更新:

  • 提高TLS握手效率简化握手流程,从4次握手削减为3次握手,利用了新出现的PSK密钥交换算法,实现了0-RTT。
  • TLS 1.3 只支持 (EC)DHE 的密钥协商算法,直接干掉了 RSA 密钥交换算法(减少学习成本,笑),加密套件削减为5个,并且所有基于公钥的密钥交换算法现在都能提供前向安全。此外从另一方面看因为都是用 (EC)DHE 的密钥协商算法,实际上也是直接废弃了非对称密钥加密模式。取而代之的是基于数学推导的椭圆曲线密钥。
  • 删除对称加密密钥的MAC以及CBC分组加密,原因可以看“已知TLS1.2攻击手段”。
  • 改进PRF算法为 HPDF算法,并且删减了一大半的被证实不安全的加密算法。
  • 为了前后兼容,TLS1.3使用TLS1.2的头部信息伪装成为TLS1.2,通过扩展字段的方式完成新的密钥传输和加密算法协商等操作。
  • 子协议削减一个大类。
    .....

写在最后

这一篇内容花了不少时间,因为网上涉及的大部分资料都是讲TLS1.2的,TLS1.3 真正深入解读的资料并不多,并且因为英文比较渣,在RFC文档的资料研究对比理解下花了不少功夫。

建议各位学技术的小伙伴们要好好磨练英语呀,不然在某个领域的深度研究上真的寸步难行。

这篇解读并不专业,更多是按照个人的思路简单整理了TLS1.3 的一些重点,如果有任何疑问或者任何错误欢迎私信或者评论指导,一起共同进步。

参考文章

1 篇内容引用

赐他一块白石,石上写着新名

154 声望
31 粉丝
0 条评论
推荐阅读
【Linux】Linux命令快速学习神器tldr、cheat介绍和使用
本文介绍tldr和Cheat等实用工具的安装和使用,这些工具虽然本身不能替代man、info等命令,但是在很多时候想要快速学习和掌握命令但是忘记常见用法非常有帮助。

Xander阅读 200

封面图
Chrome 103支持使用本地字体,纯前端导出PDF优化
在前端导出PDF,解决中文乱码一直是一个头疼的问题。要解决这个问题,需要将ttf等字体文件内容注册到页面PDF生成器中。但是之前网页是没有权限直接获取客户机器字体文件,这时就需要从服务器下载字体文件或者提示...

葡萄城技术团队3阅读 13.9k

基于QUIC协议的HTTP/3,你了解多少?
前言了解一下HTTP发展史:HTTP/0.9-HTTP/1.0-HTTP/1.1-HTTP/2.0多个TCP连接 {代码...} Keep-alive {代码...} 管线化 {代码...} 多路复用 {代码...} {代码...} 并行多路复用的请求和响应不会相互阻塞尽管传输多个...

Henryk2阅读 1.1k评论 1

gitlab-ce将https修改为http
索性我们禁用gitlab的https功能,将期恢复为http。后期我们再在部署一个nginx进行数据转发,然后在nginx上起用https并设置证书。这样应该就规避了gitlab的证书错误问题。

myskies1阅读 629

设计模式那些事(3)——使用建造者模式封装go的http库
{代码...} 具体实现 {代码...} 使用 {代码...}

爆裂Gopher1阅读 756

封面图
Hertz 性能持续优化,如何准确进行 Hertz 压测?这里有一份性能测试指南
2021 年 9 月 8 日,字节跳动宣布正式开源 CloudWeGo。CloudWeGo 是一套字节跳动内部微服务中间件集合,具备高性能、强扩展性和稳定性的特点,专注于解决微服务通信与治理的难题,满足不同业务在不同场景的诉求。...

CloudWeGo1阅读 695

封面图
为什么使用 golang http包 会把 linux 句柄打满?
请求 https 的地址,为了绕过 tls ,加上了 TLSClientConfig: &amp;tls.Config{InsecureSkipVerify: true} 配置

阿兵云原生阅读 805

封面图

赐他一块白石,石上写着新名

154 声望
31 粉丝
宣传栏