TLS1.3作为升级的版本,根据需求主要是在两个方面做了整改,一个是安全方面,一个就是性能,下面我们就来看一下。
升级之处
安全:
- 删除了不安全的密码组件,例如MD5。
- 为了前向安全性,密钥协商过程不再使用RSA和静态DH,ECDH,只选用DHE或者ECDHE。
- 设计了新的密钥派生函数HKDF代替了PRF函数。
- 记录层进行加密和MAC时固定为AEAD模式。
性能:
- 重新设计了握手层流程,让冷启动从2RTT变化1RTT。
- 设计了PSK会话恢复机制代替了原来的Session会话恢复机制。
- 0-RTT
基本流程
对于TLS1.3的流程如下,最主要分为两种方式,一种是基于key_share和signature_algorithms来选择加密的算法,另一种是基于预共享密钥PSK的会话恢复。
握手层冷启动
1 | Key ^ ClientHello |
我们首先来看第一种
- 客户端发送ClinetHello,在key_share和signature_algorithms中声明自己支持的DHE或者ECDHE算法类型对应的具体参数。
- 服务端返回ServerHello,选择自己支持的算法和自己的参数。
- 服务端发送EncryptedExtensions,包含不需要建立加密上下文并且和证书无关的拓展。注意从本条消息开始都会被加密传输。
- 如果想要验证客户端认证,服务端就会发送CertificateRequest消息。
- 服务端发送Certificate消息,包含自己的证书。
- 服务端发送CertificateVerify消息。对之前握手的消息做Hash后用证书私钥进行签名,进行显示认证表明自己持有证书私钥。
- 服务端发送Finished消息对之前握手消息做校验,以及验证协商密钥的正确性。
- 此时服务端已经可以发送加密的应用数据了。
- 如果需要客户端验证,就会发送客户端Certificate消息。
- 如果客户端发送了Certificate消息,就会发送CertificateVerify来表明客户端持有证书私钥。
- 客户端同样发送Finished消息,进行校验。
- 客户端可以发送加密的应用消息。
我们观察整个流程,冷启动仅需要1-RTT。因为在ClientHello时已经携带了DHE(ECDHE)的参数,让第一次握手有更多的作用。这也是因为在密钥协商时抛弃了RSA算法,因为对于RSA需要服务端先下发公钥,所以需要至少2-RTT。
握手层热启动
我们再看一下会话恢复的情况。对于1.3使用的是PSK机制,其中PSK可以从上次握手时生成,或者由使用者预置。
看一下流程:
1 | ClientHello |
客户端携带PSK发送给服务端,服务端如果接受就返回ServerHello,以及加密的EncryptedExtensions,Finished消息。之后两者就能进行应用数据传输,整个流程需要一个1-RTT。
记录层
流程为数据分段,填充(为了隐藏流量),加密和完整性保护(AEAD),添加消息头。
与1.2最主要的不同就是抛弃的块模式和流模式,固定为AEAD,并对AEAD做了一些调整,这里就不展开说了。
变化分析
HKDF密钥派生函数
1.3重新设计了密钥派生函数,对于HKDF函数分为Extract和Expand过程,Extract过程增加密钥材料的随机性,Expand进行拓展,在1.2中PRF函数只有Expand的过程,它默认密钥材料的随机性是足够的,然而这是不一定的可能ECC协商出来的随机分布是不够均匀的。
Extract过程:
1 | HKDF-Extract(salt, IKM) -> PRK |
Expand过程:
1 | HKDF-Expand(PRK, info, L) -> OKM |
以及最终使用的Derive-Secret函数与Expand所对应的关系:
1 | HKDF-Expand-Label(Secret, Label, Context, Length) = |
密钥变化
1.3对于密钥做了更加详细的分类,在每一个使用部分的密钥实际上都是不同的,都会通过HKDF函数做变化,这也是密码学上的安全要求,一个密钥只使用在一种功能上。
一次握手的密钥变化如下,很复杂,可以只大概了解下有什么密钥:
1 | 0 |
- HKDF-Extract 画在图上,它为从顶部获取 Salt 参数,从左侧获取 IKM 参数,它的输出是底部,和右侧输出的名称。
- Derive-Secret 的 Secret 参数由传入的箭头指示。例如,Early Secret 是生成 client_early_traffic_secret 的 Secret。
- “0” 表示将 Hash.length 字节的字符串设置为零。
其中有三个Secret,是从Extract中得来:
1 | Early Secret = HKDF-Extract(salt, IKM) = HKDF-Extract(0, PSK) (有PSK的情况)= HKDF-Extract(0,0) (没有PSK的情况) |
八个可能用到的密钥:
1 | client_early_traffic_secret = Derive-Secret(Early Secret, "c e traffic", ClientHello) |
- early_exporter_master_secret和exporter_master_secre导出密钥,用户可以自定义使用方式。
- resumption_master_secret用于会话恢复。
- client_early_traffic_secret用于加密early_data,在0-RTT模式中使用。
- client_handshake_traffic_secret和server_handshake_traffic_secret用于握手时的加密。
- client_application_traffic_secret_0和server_application_traffic_secret_0用于加密应用流量数据。
3,4,5的密钥想要加密数据还需要一次Expand过程生成write_key和iv来作为真正加密数据的密钥,生成方式如下
1 | [sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length) |
PSK会话恢复机制
1.3中使用PSK机制代替了Session机制,那么PSK是如何生成并使用的?
先看下Server发送的NewSessionTicket
1 | struct { |
其中ticket的值是从上一节提到的resumptition_master_secret中计算出来的
1 | PskIdentity.identity = ticket |
客户端接受到NewSessionTicket后会计算一个binder值,Truncate会取出ClientHello1中的binder_list,避免陷入无限循环。利用这个值来绑定上下文。
1 | PskBinderEntry = HMAC(binder_key, Transcript-Hash(Truncate(ClientHello1))) |
然后Client在会话恢复时将identity和binder包含在ClientHello的拓展中发送给Server,Server再验证binder的完整性,比对identity,选择是否接受PSK,从而生成新的handshake_traffic和application密钥进行会话恢复。
可以看到如果仅仅是这样的会话恢复,相对于1.2来说只是更安全(因为不再使用master_secret做传递,而是利用HKDF派生),性能上没有大的提升,所以就会涉及到下面的0-RTT。
0-RTT
对于TLS1.3还有一个很大的变化,那就是0-RTT模式。什么是0-RTT呢,就是基于PSK会话恢复时,PSK会作为Early Secret的IKM,然后从Early Secret生成client_early_traffic_secret,再由client_early_traffic_secret生成key和iv用来加密0-RTT发送的early_data。Client在ClientHello中携带early_data拓展选项,并发送由client_early_traffic_secret加密的数据,如果服务端接受early_data,那么就成功完成了0-RTT传输。
但是0-RTT为了效率降低了安全性,存在重放问题,因为在客户端发送early_data时,只有客户端参数决定,服务端没有参与到其中,那么中间人就可以截获这个ClientHello包以及加密数据,而服务端每次都能成功解析这个包,中间人就可以进行重放ddos或者重复敏感操作。除了这种最简单的重放,还有针对分布式系统的重放攻击,这里就不展开说了。
实例分析
通过上面的分析,我们可以看出TLS具有极强的通用性与可拓展性,但是针对一些具体的使用场景能否进行特殊化的改造,这里我们以微信mmtls作为例子来分析下。我们回顾TLS的过程,身份认证的过程是采用建立PKI体系,做到全球的通用性,但是针对完全可控的微信程序,可以直接将签名的公钥预埋在客户端中,这样就不用建立或者申请证书,通过强制更新客户端,来做公钥的维护,这样就很大程度简略的流程并提升了效率。
还有针对0-RTT的问题,对于TLS1.3本身协议上来说是无解的,更多的只能从应用层来做缓解,例如cloudflare针对0-RTT将禁止非幂等的操作像POST,PUT,只允许不带参数的GET请求。mmtls也是根据这些应用行为,有选择性的使用0-RTT模式,例如只在端连接使用。chrome甚至选择不解决0-RTT重放,因为大部分浏览器的操作都是GET先,安全性交由其他方面来做。
总结
整个TLS完成了密码学上的安全,并结合了软件工程的理念,做到了分层抽象,模块化等等优秀的设计,在通用性上也确保了更多的场景能够被覆盖,提升了互联网的安全性,基本完成了它的设计目标。
当然作为使用者,当我们自己在设计(魔改)安全协议的时候,依然需要小心谨慎,密码学的快速发展已经让大部分程序员很难理解其原理,甚至正确使用密码学黑盒工具也是困难的,所以我们更需要的是怀着严谨的态度去使用密码学工具,紧跟主流,避免陷入闭门造车的困境。还有根据本身的应用场景我们是否能够吸取TLS的精华,结合自己的业务需求去做特质化,省略一些臃肿的包袱,做到定制优化都是可以考虑的。
TLS中的奥义远远不止我所阐述的,短短两篇文章也不能尽善尽美地描绘,希望能够对每一个读者有所帮助,更加有条理地理解其奥义。