写点什么

HTTP - TLS1.3 初次解读

作者:懒时小窝
  • 2022 年 9 月 26 日
    广东
  • 本文字数:26528 字

    阅读完需:约 87 分钟

引言

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


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


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

时间线

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 HelloVersion: 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 的加密套件可能会问为什么要放弃使用 DH RSA 算法?这里还是涉及 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 ^ ClientHelloExch | + 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、RecordHandshake:协议负责协商使用的 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


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 python3import hashlibimport hmacfrom 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 ^ ClientHelloExch | + 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 的网页截图:


2.1 Client_hello

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


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


Key  ^ ClientHelloExch | + key_share*     | + signature_algorithms*     | + psk_key_exchange_modes*     v + pre_shared_key*       -------->
复制代码


抓包报文:


Handshake Protocol: Client HelloVersion: TLS 1.2 (0x0303)Random: bab59bd2b9007afce54bdb6e3802c8b30d78b833e4aa21d7242858a08fae5da5Session ID: 23d30d7ff75c05fdc4d7f49c0a4e84fc88dd123e167c91f9e5e3d29bc85cc46eCipher 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 Random Server RandomClient Params Server Params,加上 HKDF 算法就可以算出对称加密的密钥,所以有了会话密钥之后,后续的传输都是对成加密的密文,进一步提高安全性。


HKDF 是对于 PRF 的改进,这里简单回忆 TLS1.2 的 PRF 算法,结合 TLS1.3 得出下面的步骤:通过 key_share 传递的椭圆曲线函数密钥+Client Params Server Params,得出 pre_master_secret (这两个椭圆曲线公钥+复杂算法 = 随机数,叫做 PreMaster=pre_master_secret ),然后结合 Client Random Server 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: d545f21e4c2e759a2b45735e554c789a5869c7921fbf780dc69f5ecd3df9a60fExtension: 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 提早进入加密通信这意味着后面的证书等信息都是加密的了,减少了握手时的明文信息泄露。


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



由于后续的内容都是加密传输,抓包看到的也都是密文并且没法分析,所以我们根据 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,里面包含其他与密钥协商无关的扩展数据给客户端。


2.6 CertificateRequest(CR)

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


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


这里继续结合之前 TLS1.2 所学,介绍 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 本身就是和密码学息息相关的标准,所以如果要吃透 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 优化]]



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][2]该计划的正式名称为“US-984XN”。


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


关于 PRISM 的报道,是在美国政府持续秘密要求威讯向国家安全局提供所有客户每日电话记录的消息曝光后不久出现的。[7][2]泄露这些绝密文件的是国家安全局合约外包商员工爱德华·斯诺登,于 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 的一些重点,如果有任何疑问或者任何错误欢迎私信或者评论指导,一起共同进步。

参考文章

用户头像

懒时小窝

关注

赐他一块白石,石头上写着新名 2020.09.23 加入

如果我们想要知道自己想要做什么,必须先找到自己的白色石头。欢迎关注个人公众号“懒时小窝”,不传播焦虑,只分享和思考有价值的内容。

评论

发布
暂无评论
HTTP - TLS1.3 初次解读_懒时小窝_InfoQ写作社区