TLSv1.3 概述
背景
SSL 是1994年网景公司提出,主要解决安全传输从0到1的过程,真正被大规模应用是1996年发布的 SSLv3,经过了几年的发展,在1999年被IETF纳入标准化,改名叫 TLS,其实本质是一样的,TLSv1.0 跟 SSLv3 没有太多差异,TLSv1.1 做了一些 bug 修复和支持更多的参数,TLSv1.2 基于 TLSv1.1 做了更多的扩展和算法上改进,从2008年到今年近10年的时间,TLSv1.3 在今年3月份被 IETF 讨论组评正式纳入标准化,8月初出了 RFC8446,但是 TLSv1.3 在2014年已经被提出来了,经历了4年时间的讨论和优化,但是因为 TLSv1.2 已经被大量应用,一些网络设备并不兼容之前提出的TLSv1.3草案,所以后来做了很多次优化,在第28个草案时才确定没有问题,正式纳入标准化。
趋势
TLS 最大的应用就是 HTTPS,我们来看看 chrome 统计的 HTTPS 网页趋势,2015年的时候,大多数国家的HTTPS网页加载次数只占了不到 50%,2016年美国这个占比到了将近 60%,去年就已经超过70%, 目前美国的统计数据是 85%,可见,离 100% 已经很近了,从图上可以看出,日本目前接近 70%,这里面没有统计到中国的数据,我估计也就 40% 左右,落后其他国家三年左右时间,空间还有很大,而且未来HTTPS趋势是非常明显的。至于国内大多数用户不愿意使用HTTPS的原因,无非几个:安全意识、技术难度、性能和用户体验、成本等等原因,这方面阿里云 CDN 也在努力帮助用户解决这些技术问题,在这里不就展开了。
握手原理
TLSv1.2 握手原理
完成握手
先来看看 TLSv1.2 的完整握手流程:
SSL 握手之前是 TCP 握手,这里面没画出来,SSL 握手总是以 ClientHello 消息开始,就跟 TCP 握手总是以 SYN 包开始一样。
整个握手过程分为两个过程:
- 参数协商
主要通过 Client Hello 和 Server Hello消息来协商,Client Hello 提供客户端支持的参数(比如:密钥套件、签名算法、应用层协议列表等等),另外还包含一些必要的参数(比如:客户端随机数、SNI、session id 等等),服务器从中选取自己支持的参数,然后在 Server Hello 消息中返回,告知客户端本次会话要使用哪些参数。 - 密钥交换
主要通过 Server Key Exchange 和 Client Key Exchange 来交换,其中 Server Key Exchange 是用来发送服务器椭圆曲线算法的参数、公钥信息以及该信息的签名,Client Key Exchange 用来发送客户端椭圆曲线算法的公钥信息,双方得到对方椭圆曲线算法公钥之后,与自己本地生成的临时私钥,通过椭圆曲线算法生成预主密钥,再导出主密钥和会话密钥,握手完成之后通过会话密钥来加密通信。
可以看出,完整的握手需要2个 RTT,而且每次握手都用到了非对称加密算法签名或者解密的操作,比较耗时和耗 CPU。假如1个 RTT 需要 100ms 的话,TLSv1.2 的完整握手时间就需要 200ms,再加上 HTTP 请求时间,那首字节时间就是 300ms 左右了。
SSL 的握手消息虽然比较多,但很多消息都是放在一个 TCP 包中发送的,从抓包可以看出完整的 SSL 握手需要2个 RTT。
会话恢复
刚才通过完整的 SSL 握手可以看出几个缺点:
- 2个 RTT,首包时间较长
- 每次都要做非对称加密运算,比较耗 CPU,影响性能
- 每次都要传证书,证书一般都比较大,浪费带宽
SSL 握手的目的是协商参数和会话密钥,如果能把这些会话参数缓存起来那就可以没有必要每次都传证书和做非对称加密运算,这样就可以提高握手性能,而且可以将 RTT 减到1个,提高用户体验。
TLSv1.2 有两种会话恢复的方式:Session ID 和 Session Ticket
- Session ID
每次完整握手之后都将协商好的会话参数缓存在客户端和服务器中,客户端下次握手时会将上次握手的 Session ID 带上,服务器通过 Session ID 查询是否有会话缓存,有的话就直接复用,没有的话就重新走完整握手流程。
但是 Session ID 这种方式也有缺点,比较难支持分布式缓存以及耗费服务器的内存。
- Session Ticket
Session Ticket 的原理可以理解为跟 HTTP cookie 一样,服务器将协商好的会话参数和密钥加密后发送给客户端,客户端下次握手时会将这个 Session Ticket 带上,服务器解密成功就复用上次的会话参数和密钥,否则就重新走完整握手流程。可以看出 Session Ticket 这种方式不需要服务器缓存什么,天生支持分布式环境,有很大的优势。
这种方式有一个缺点是并不是所有客户端都支持,支持率比较低,但随着客户端版本的更新迭代,以后各种客户端会都支持。因为 Session ID 客户端支持率比较高,所以目前这两种方式都在使用。
这是我们一个CDN节点上了分布式 Session ID 复用的效果,Session ID 复用的比例在没有开启分布式缓存时只占了 3% 左右,复用率很低,上了分布式 Session 缓存之后,这个比例提升到 20% 多。握手时间也从 75ms左右降低到 65ms 左右,性能提升效果还是很明显的。
TLSv1.3 握手原理
完整握手
刚才讲到 TLSv1.2 握手主要是两步:参数协商和密钥交换,TLSv1.3 做了优化,把这两步合并成一步来完成了:
Client Hello 包含了客户端支持的参数信息,以及椭圆曲线算法的公钥信息,服务器通过 Server Hello 来响应选取的参数信息和服务器椭圆曲线公钥信息,删除了 TLSv1.2 中的 Server Key Exchange 和 Client Key Exchange,这样通过1个 RTT 就可以完成了参数协商和密钥交换,双方都知道了对方的椭圆曲线公钥,然后用自己的临时私钥就可以计算出预主密钥(PMS),接着就可以导出数据通信的加密密钥,但不是像 TLSv1.2 那样的主密钥和会话密钥,TLSv1.3 有5个主要的密钥,一会儿我们再细讲。
这样,假如1个 RTT 需要 100ms 的话,TLSv1.3 完整握手时间只需要 100ms,HTTP 的首字节时间只需要200ms,比 TLSv1.2 就少了1个 RTT。这个是一个非常重要的改进。
会话恢复
TLSv1.3 的会话恢复跟 TLSv1.2 不一样,取消了 Sesion ID 的方式,采用 PSK 机制,类似 TLSv1.2 的 Session Ticket,并在 Session Ticket 中添加了过期时间。
TLSv1.2 是通过 Client Hello 的 SessionTicket 扩展选项来传输这个加密的会话缓存参数,但在 TLSv1.3 里面是通过 PSK(pre_shared_key)扩展选项来传输。
服务器收到 PSK 之后解密成功就可以复用会话了,不需要再重新传输证书和协商密钥了,跟 TLSv1.2 的会话恢复一样,只需要1个 RTT 就可以完成握手,提高了握手的性能。
抓包截图可以看到:
- 上面是 TLSv1.3 完整握手,下面是 TLSv1.3 会话恢复握手,都是1个 RTT 就完成。
- Server Hello 之后的数据包都是加密的。
- 连响应的证书是什么证书都看不到,调试比较不方便。
0-RTT
我们再来看看 TLSv1.3 另一个重要特性 0-RTT:
所谓 0-RTT 是指在会话复用的基础上做到的,在完整握手时没法做到 0-RTT,这是因为请求是用 PSK 导出的一个叫做 early_data 的密钥加密的,服务器收到之后可以用 PSK 导出的 ealy_data 密钥解密,从而得出请求明文数据,这样就可以做到 0-RTT。
但是 TLSv1.3 的 0-RTT 是牺牲了一定的安全性的,没法做到完全前向安全(PFS),因为知道了 SessionTicket 的加密 key 就可以导出 early_data 密钥,从而可以解密出 0-RTT 数据。比如带有鉴权信息的请求被中间网络设备解密出来了可能就导致盗链这些问题;另外 0-RTT 也有重放攻击的问题,所以 TLSv1.3 的 0-RTT 并不是必须的,只是协议层面支持了 0-RTT 模式,对于请求信息不敏感的业务可以使用 0-RTT 来提高性能。目前呢,Chrome 浏览器并不支持 0-RTT 模式,Firefox 浏览器支持,但是我配置了并没有抓到 0-RTT 的包。服务器方面:tengine 已经支持了 0-RTT,nginx 和 openresty 还没有支持 0-RTT。
RTT 对比
目前,TLS 主要应用在 HTTPS 和 HTTP/2,这个表是一个完整 HTTP/2 请求从 TCP 开始所需的 RTT 对比,可以看出,不管是 TLSv1.2 还是 TLSv1.3、首次连接还是会话复用,TCP 握手和 HTTP 请求各 1 个 RTT 是不可避免的,能优化的就只有 TLS 握手了,从刚才讲的 TLSv1.2 和 TLSv1.3 的握手原理可以知道,在首次连接和会话复用情况下,TLSv1.3 都比 TLSv1.2 少一个 RTT。
核心改进
主要差异
总结一下相比 TLSv1.2,TLSv1.3 主要的差异有哪些:
握手时间:同等情况下,TLSv1.3 比 TLSv1.2 少一个 RTT
应用数据:在会话复用场景下,支持 0-RTT 发送应用数据
握手消息:从 ServerHello 之后都是密文。
TLSv1.3 协议版本的协商是从扩展选项里面选的,不是从 ClientHello 消息的 version 字段协商的,这是因为中间有一些网络设备对 version 字段做了识别和限制,如果对 version 进行升级,那 TLSv1.3的数据包可能在一些网络设备中被当成异常包,不利于 TLSv1.3 的部署,所以 TLSv1.3 使用了 ClientHello 的一个新加的扩展字段 supported_versions 来协商协议版本。TLSv1.3 的记录头以及握手消息中 version 字段跟 TLSv1.2 一样的。另外一个握手层面的差异就是 TLSv1.3 禁止重协商。
会话复用机制:弃用了 Session ID 方式的会话复用,采用 PSK 机制的会话复用。
密钥算法:TLSv1.3 只支持 PFS (即完全前向安全)的密钥交换算法,禁用 RSA 这种密钥交换算法,这是因为使用 RSA 密钥交换的话,如果拿到 SSL 私钥就可以解密抓包数据,不具备完全前向安全性。对称密钥算法只采用 AEAD 类型的加密算法,像 CBC 模式的 AES、RC4 这些算法在 TLSv1.3 是禁用的。
密钥导出算法:TLSv1.3 使用新设计的叫做 HKDF 的算法,而 TLSv1.2 是使用PRF算法,稍后我们再来看看这两种算法的差别。
密钥套件
TLSv1.3 目前定义了这5个密钥套件,从表面上看已经隐藏了密钥交换算法了,因为都是使用椭圆曲线密钥交换算法。
密钥导出函数 PRF
我们来看看 TLSv1.2 的密钥导出算法 PRF,PRF 主要目的是用预主密钥、客户端随机数、服务器随机数导出主密钥,再从主密钥、双方随机数导出会话密钥,最终用会话密钥来加密通信。
对于 RSA 来说,预主密钥是客户端生成,用服务器证书公钥加密之后发给服务器,服务器用证书对应的私钥来解密得到。
对于椭圆曲线算法来说,预主密钥是双方通过椭圆曲线算法来生成的,双方各自生成临时公私钥对,保留私钥,将公钥发给对方,然后就可以用自己的私钥以及对方的公钥通过椭圆曲线算法来生成预主密钥。
可以看出,只要我们知道预主密钥或者主密钥便可以解密抓包数据,所以 TLSv1.2 抓包解密调试只需要一个主密钥即可,SSLKEYLOG 就是将主密钥导出来,在 Wireshark 里面导入就可以解密相应的抓包数据。
另外,Session ID 缓存和 Session Ticket 里面保存的也是主密钥,而不是会话密钥,这样每次会话复用的时候再用双方的随机数和主密钥导出会话密钥,从而实现每次加密通信的会话密钥不一样,即使一个会话被破解了也不会影响到另一个会话。
HKDF
但是在 TLSv1.3里面,不再使用 PRF 这种算法了,而是采用更标准的 HKDF 算法来进行密钥的推导。
而且在 TLSv1.3 中对密钥进行了更细粒度的优化,每个阶段或者方向的加密都不是使用同一个密钥,我们知道TLSv1.3 在 ServerHello 消息之后的数据都是加密的,那握手期间服务器给客户端发送的消息用 server_handshake_traffic_secret 通过 HKDF 算法导出的密钥加密的,客户端发送给服务器的握手消息是用 client_handshake_traffic_secret 通过 HKDF 算法导出的密钥加密的。这两个密钥是通过 Handshake Secret 密钥来导出的,而 Handshake Secret 密钥又是由 PMS (预主密钥)和 Early Secret 密钥导出,然后通过 Handshake Secret 密钥导出主密钥 Master Secret。
再由主密钥 Master Secret 导出这几个密钥:
client_application_traffic_secret:用来导出客户端发送给服务器应用数据的对称加密密钥
server_application_traffic_secret:用来导出服务器发送给客户端应用数据的对称加密密钥
resumption_master_secret:sessoin ticket 里的主密钥
在会话复用的时候略有差别,主要是要导出 0-RTT 时 early_data 要加密的密钥 client_ealy_traffic_secret,这个是由 PSK 来导出的。
可以看出要解密 TLSv1.3 需要5个密钥才行。
应用实践
支持 TLSv1.3 的服务器
服务器要使用 TLSv1.3 并不难,tengine-2.2.2 开始支持 TLSv1.3,nginx-1.13.8 开始支持 TLSv1.3,但是TLSv1.3 的核心实现是在 openssl 库,但目前 openssl 稳定版本并不支持 TLSv1.3,只有 openssl-1.1.1 的预发版本或者开发版本上才支持 TLSv1.3,我们下载支持 TLSv1.3 的 openssl 代码之后,在编译参数中需要加上 enable-tls1_3,编译出 openssl 动态库或者静态库,在 tengine 中依赖支持这个 openssl 库就可以使用 TLSv1.3 了。
需要注意的是 openssl 的一些接口做了改动,比如之前是一个函数,现在变成了宏,这个就导致 lua 中不能调用这个 ffi 接口了,如果自己的业务中使用了老版本 openssl 的 ffi 接口需要特别注意。
Tengine 配置 TLSv1.3
编译出支持 TLSv1.3 的 tengine 后,只需要做一下配置就可以了,在 ssl_protocols 指令中加了 TLSv1.3,同时在 ssl_ciphers 指令中加上 TLSv1.3 相关的密钥套件。
但是这里有一个坑:
ssl_protocols 这个指令只在默认 server 中生效,在同一个 IP 的其他 server 块并不生效,比如有这样的需求:两个域名一个开启 TLSv1.3,另一个不开启 TLSv1.3,目前开源 tengine 和 nginx 是做不到的。
我们来看看为什么做不到。
这是在 tengine 里面处理一个 HTTPS 请求的大致流程,先经过握手,握手成功之后再处理 HTTP 请求。
在握手流程中,openssl 提高了几个回调函数接口让我们介入,最主要的接口有这么几个:
client_hello_cb、get_session_cb、servername_cb、cert_cb。
client_hello_cb 是收到 ClientHello 消息后执行的第一个回调,然后选择协议版本和选择密钥套件,接着调用 get_session_cb 来获取缓存的 Session,在这个阶段我们可以做分布式缓存的获取,解决分布式环境中Session ID复用率低的问题。
openssl 解析完SNI后调用 servername_cb,这个回调主要用来切换 server 块配置和 SSL_CTX,实现多个 server 块不同 SSL 配置的问题,但可以看出协议版本和密钥套件早已经选择好了,在这个阶段做 SSL_CTX 切换对这两个配置并没有作用。
最后调用 cert_cb 来切换证书,在这个阶段可以用 lua 介入,实现证书的热加载。
要想实现协议版本和密钥套件域名定制的话,必须要在 client_hello_cb 这个阶段进行切换,但是 client_hello_cb 这个回调是新版 openssl 才支持。
所以,我们内部已经实现了这个接口,在这个阶段切换 server 块配置和 SSL_CTX,从而实现所有 SSL 配置的热加载。这部分的实现后面我会提到开源 tengine 中,有需要的话到时关注一下。
支持 TLSv1.3 的浏览器
看一下目前支持 TLSv1.3 的浏览器,主要是 Chrome 和 Firefox,Chrome 是 63 之后开始支持,Firefox 是 61 版本之后支持,但每个版本支持的 TLSv1.3 的 draft 版本可能不一样,目前 Chrome 主要支持 draft23 和 draft28。Firefox 支持 draft28。IE 和 Safri 目前还没有支持 TLSv1.3,但未来也会支持的。
Chrome 开启 TLSv1.3
在 chrome://flags 开关配置中选择开启的 TLSv1.3 哪个草案版本,然后重启 chrome 后生效。
访问一个开启 TLSv1.3 域名之后,打开 chrome 的开发者工具调试面板,便可以看出当前 SSL 连接是否已经采用了 TLSv1.3
Firefox 开启 TLSv1.3
在 Firefox 的配置中心将 security.tls.version.max 设置成 4,就可以开启 TLS 1.3 了,目前 Firefox 只支持了最新的 TLS 1.3 Draft28
同样的,在 Firefox 浏览器中访问一个开启 TLSv1.3 的域名之后,打开 Firefox 的调试工具,也可以看出是否已经使用了 TLSv1.3
TLSv1.3 调试
最后来看看平时怎么用 openssl 来调试 TLSv1.3
用 openssl 这两个指令来启动 server 端和客户端,通过参数 tls1_3 参数来启动 TLSv1.3,用 early_data 参数来实现 0-RTT 的调试。用 keylogfile 参数来记录 TLSv1.3 里面用到的密钥。
这是TLSv1.3 的 keylog,可以看出主要有这5个密钥,其中:
CLIENT_EARLY_TRAFFIC_SECRET 是 0-RTT 数据的加密密钥;
SERVER_HANDSHAKE_TRAFFIC_SECRET 是 Server 端加密握手消息用的密钥;
SERVER_TRAFFIC_SECRET_0 是 Server 端加密应用数据的密钥;
CLIENT_HANDSHAKE_TRAFFIC_SECRET 是 Client 端加密握手消息用的密钥;
CLIENT_TRAFFIC_SECRET_0 是 Client 端加密应用数据的密钥。
然后我们用 wireshark 导入这个 keylog 文件,就可以实现 TLSv1.3 数据包的解密了。
这个是 TLSv1.3 0-RTT 解密后的截图,可以看出在 TCP 握手之后,0-RTT 的数据在 Client Hello 之后就发送了。
有兴趣的话,可以自己动手调试一下。
最后插播个招聘广告:如果你对 tengine/nginx/openssl 这些技术感兴趣,欢迎自荐或者推荐。
(全文完)