摘要: HTTPS 时代已经来临,TLSv1.3 已经标准化,将来各大浏览器会逐渐支持 TLSv1.3,本主题将分享 TLSv1.3 核心原理,以及如何结合 Tengine 让 HTTPS 更快、更安全。nginx
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 也在努力帮助用户解决这些技术问题,在这里不就展开了。chrome
握手原理
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 Ticketcookie
Session ID
每次完整握手以后都将协商好的会话参数缓存在客户端和服务器中,客户端下次握手时会将上次握手的 Session ID 带上,服务器经过 Session ID 查询是否有会话缓存,有的话就直接复用,没有的话就从新走完整握手流程。
可是 Session ID 这种方式也有缺点,比较难支持分布式缓存以及耗费服务器的内存。网络
Session Ticket
Session Ticket 的原理能够理解为跟 HTTP cookie 同样,服务器将协商好的会话参数和密钥加密后发送给客户端,客户端下次握手时会将这个 Session Ticket 带上,服务器解密成功就复用上次的会话参数和密钥,不然就从新走完整握手流程。能够看出 Session Ticket 这种方式不须要服务器缓存什么,天生支持分布式环境,有很大的优点。
这种方式有一个缺点是并非全部客户端都支持,支持率比较低,但随着客户端版本的更新迭代,之后各类客户端会都支持。由于 Session ID 客户端支持率比较高,因此目前这两种方式都在使用。session
这是咱们一个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.三、首次链接仍是会话复用,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 以后就发送了。
有兴趣的话,能够本身动手调试一下。
(全文完)
本文为云栖社区原创内容,未经容许不得转载。