互联网安全问题日益严重的今天,HTTPS作为互联网安全的基础之一,变的越来越重要。起初由于苹果要求所有APP必须支持HTTPS,激发各大厂商HTTP升级HTTPS的热情,为了保障用户利益,维护好用户数据安全,提升用户体验,有货自然也不能落于人后,随即开展全站HTTPS的改造升级。本文将介绍有货APP在HTTPS改造升级过程中的所做的工作,重点介绍在TLS握手阶段的一些优化。
HTTPS性能的问题
HTTPS最主要的特点就是安全,HTTPS基于以下三方面提供安全保证:内容加密,身份认证,消息校验。HTTPS很美好,与此同时,HTTPS也会降低用户访问速度,增加网站服务器的计算资源消耗。
HTTPS协议可以简单的认为是HTTP+TLS/SSL。TLS/SSL是安全传输层协议,介于TCP和HTTP之间,TLS/SSL协议通常分为两层:TLS记录协议(TLS Record Protocol)和TLS握手协议(TLS Handshake Protocol)。 TLS记录协议建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。TLS握手协议建立在记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
HTTPS对访问速度的影响主要来自于两方面:
协议增加的网络RTT (round trip time)
加解密相关的计算耗时
加解密相关的资源消耗与选择的加密协议密切相关,为了实现较好的安全性,同时兼顾性能,有货APP客户端增加协议优选机制,详见下文,下面先来看下协议增加的网络RTT。
以浏览器发起HTTPS请求为例,看下HTTP和HTTPS协议增加了哪些RTT。
用户使用HTTP发起请求的网络交互如下:
HTTP协议只需要经过TCP三次握手即可在用户和服务器之间建立连接,发送应用数据。只需要消耗一个RTT。
用户使用HTTPS发起请求的网络交互如下:
HTTPS协议极端情况下,需要多消耗七个RTT,也就是经过八个RTT,才能开始发送应用数据。具体过程如下:
TCP三次握手建立连接。消耗一个RTT。
用户如果输入HTTP访问,需要先302跳转到HTTPS。消耗一个RTT。
302跳转到HTTPS,需要重新建立TCP连接。消耗一个RTT。
TLS完全握手阶段1。消耗一个RTT。
解析CA域名解析。消耗一个RTT。
三次握手建立CA网站的TCP连接。消耗一个RTT。
发起OCSP请求。消耗一个RTT。
TLS完全握手阶段2。消耗一个RTT。
连接成功,开始应用数据交互。
当然,不是所有的HTTPS请求都需要增加七个RTT才能建立连接。但是HTTPS对性能的影响可见一斑,对于有货 APP而言,我们重点做了以下两方面的工作优化HTTPS的性能:
HTTP协议优化
TLS握手优化
HTTP协议优化
HTTPS协议承载于HTTP协议之上,对HTTP协议的优化可以显著提高HTTPS的性能。目前主流的HTTP协议版本是1.1,随着HTTP2的出现和完善,各个平台的支持力度大大提高,为我们升级到HTTP2打下坚实的基础。
HTTP2主要有以下特性:
二进制分帧,数据使用二进制传输,相比于文本传输,更利于解析和优化。
多路复用,同一域名下的请求,共用同一条链路进行传输,有效节省消耗。
头部优化,将头部字段缓存为索引,客户端与服务端维护索引表,通信过程中尽可能采用索引进行通信,收到索引后查询索引表,才能解析出真正的头部信息。
为了支持HTTP2协议,必需要升级客户端的网络库,同时还要兼容网络库原有的一些优化,如HTTPDNS等。我们做了以下工作。
OkHttp对NPN的支持
有货Android端网络库使用的是OkHttp,OkHttp天然支持HTTP2,但是在较新的版本中,OkHttp移除了对NPN协议的支持,转而只支持ALPN协议。
但是有以下几种场景,我们可能还需要使用NPN:
1. ALPN只支持Android 5.0以上,如果要在Android 5.0以下支持HTTP2,必须使用NPN。
2. 理论上nginx可以对ALPN和NPN同时支持,但是部分服务器上的配置可能只支持NPN,并且短时间内不会支持ALPN,必须使用NPN。
这时候我们通过判断Android系统版本,再通过服务端支持的情况与否,决定是否使用HTTP2。如下图:
目前我们公司的服务器无论是腾讯云还是AWS都支持ALPN,但是腾讯云只支持HTTP1.1。优化后效果见下图:
AW ServerHello ALPN
腾讯云ServerHello ALPN
客户端ServerHello ALPN
HTTPS对HTTPDNS的影响
OkHttp使用HTTPDNS的两种方式:
1. OKHttp自带的HTTPDNS接口:使用DNS接口方式过于底层,异常不容易控制,上层无感知。
2. 使用拦截器:可以十分精确的控制异常,对URL中的host进行替换,将域名替换为IP,添加header请求头,值为替换前的域名。
我们主要是用第二种方式来实现,在HTTP的情况下,这种方式不存在任何问题,但是在HTTPS的情况下,这种方式需要修改OkHttp的相关代码。
HTTP2对HTTPDNS的影响
在HTTP2中,请求头中的host已不再是HTTP1.1时代的host了,通过查看协议文档可以看到在HTTP2中使用:authority请求头代替HTTP1.1中的host。
这个问题导致的直接结果就是服务器端拿到的host是IP,而不是域名,如果服务器对host进行校验,那么可能就会出问题。
修改部分网络代码如下:
把真正的host设置到headerlist中。
HTTPS证书校验
在okhttp3.internal.io.RealConnection类中有个方法叫connectTls
无论是调用Platform.get().configureTlsExtensions()配置SSLSocket对象,还是address.hostnameVerifier().verify()进行证书校验,以及address.certificatePinner().check()中,传入的host都是address.url().host(),而这个值却恰恰是我们替换了url中的域名为ip的host,所以此时拿到的值为ip,这时候,带来了两个问题:
1. 当客户端使用HTTPDNS时,请求URL中的host会被替换成HTTPDNS解析出来的IP,所以在证书验证的时候,会出现domain不匹配的情况,导致SSL/TLS握手不成功。
2. 在服务器上存在多张证书的情况下,会存在问题,这就是所说的SNI场景
上述过程中,当客户端使用HTTPDNS时,请求URL中的Host会被替换成HTTPDNS解析出来的IP,导致服务器获取到的域名为解析后的IP,无法找到匹配的证书,只能返回默认的证书或者不返回,所以会出现SSL/TLS握手不成功的错误。
那么我们需要用我们自定义的host作为检验的值,与session中证书带有的做比较:
我们传入的host跟服务器返回的证书host比较,如果是匹配的,认为是校验通过,否则不通过。
TLS握手优化
HTTPS连接建立的过程中,TLS握手也消耗了大量的资源,是我们优化工作的重点关注对象。
TLS握手的过程如下:
其中握手的各个阶段有:
三次握手建立TCP连接。消耗一个RTT。
客户端发送ClientHello,以明文传输请求信息。服务端返回协商的信息结果。服务器也会配置并返回对应的证书链Certificate,用于身份验证与密钥交换。然后会发送ServerHelloDone信息用于通知服务器信息发送结束。消耗一个RTT。
服务器返回的证书验证合法后,客户端选择加密算法,生成协商密钥。并告知服务器。然后客户端发送Finished消息用于通知客户端信息发送结束。服务器收到客户端消息后,生成通信密钥和加密算法进行加密通信。然后发送Finished消息用于通知服务器信息发送结束。消耗一个RTT。
握手阶段结束后,客户端和服务器数据传输开始使用协商密钥进行加密通信。
使用Wireshark对有货APP进行抓包,可以看到HTTPS请求第一次连接情况,如下图:
其中,1是TLS的第一次握手,2是TLS第二次握手。两次握手完全完成后,才开始传递应用数据。此次TLS握手没有经过任何优化。
TLS握手阶段就需要消耗2个RTT,是HTTPS性能低下的主要原因。TLS握手的优化有以下几方面:
优选协议
FalseStart
SessionResumption
Certificate
5. OCSPStapling
优选协议
为了兼顾安全性和性能,有货APP获取服务端与客户端约定的协议列表,优先选择ECDHE-RSA-AES128-GCM-SHA256作为后续使用的协议。
TLS握手的时候,客户端默认设置的SSL参数如下:
那么ClientHello的时候会告知服务端我们支持协议顺序,如下:
服务端在ServerHello的时候,优选指定的加密套件:
False Start
TLS False Start 是指客户端在发送 Change CipherSpec Finished 同时发送应用数据(如 HTTP 请求),服务端在 TLS 握手完成时直接返回应用数据(如 HTTP 响应)。这样,应用数据的发送实际上并未等到握手全部完成。如下图所示:
由上图可知,启用False Start后,TLS握手阶段只需要一个RTT即可完成,对性能的提升还是很显著。False Start需要客户端和服务端支持ALPN协议,用来表明自己的支持的HTTP协议。因为在还没完成握手时就发送了应用数据,最好使用支持前向安全性(ForwardSecrecy)的加密算法以提高安全性。
由上图可以看出,服务端的 ChangeCipherSpec 出现在 68 号包中,但在之前的 62 号包中,客户端已经发出了请求,相当于 TLS 握手只消耗了一个 RTT。
Nginx启用False Start需要做一些简单的配置:
ssl_protocolsTLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_cipherson;
ssl_ciphers’ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA’;
SessionResumption
Session Resumption是指会话复用,将第一次握手算出来的对称密钥存起来,后续请求中直接使用。这样可以减少握手过程中的一个RTT。会话复用握手如下图:
Session Resumption有两种实现方式:Session ID,Session Ticket。两种方式各有优劣,详情如下。
SessionID :TLS 握手中生成的 Session ID。服务端可以将 Session ID 协商后的信息存起来,客户端也保存 Session ID,并在后续的 ClientHello 中带上它,如果服务端能找到与之匹配的信息,就可以完成一次快速握手。其优点在于Session id 是 TLS 协议的标准字段,厂商支持程度好。
SessionTicket:使用只有服务端知道的安全密钥加密会话信息生成Session Ticket,最终保存在浏览器端。客户端在 ClientHello 时带上 Session Ticket,只要服务器能成功解密就可以完成快速握手。
Session ID | Session Ticket | |
优点 | Session ID 是 TLS 协议的标准字段,厂商基本都支持。 | 服务端不需要消耗资源来存储 Session Ticket内容 |
缺点 |
|
|
Nginx启用Session ID需要如下配置:
ssl_session_cache shared:SSL:10m;
ssl_session_timeout1h;
此外多个Nginx之间如何同步session,openresty给出了一种解决方案:ssl_session_store_by_lua,改方案通过将session存储在memcached/redis中实现了跨服务器共享。详见:http://lua-users.org/lists/lua-l/2015-08/msg00141.html
Nginx启用Session Ticket需要如下配置:
#$> openssl rand 48 > file.key
ssl_session_ticketson;
ssl_session_ticket_keyfile.key;
其中多个Nginx之间需要使用同一个file.key。
我们在有货APP里采用了SessionID来实现会话复用。见下图:
可以看到,在ClientHello中,带上Session ID字段,只经过一次RTT即完成TLS握手,随即开始发送应用数据。
Certificate
证书是在握手期间发送的,由于 TCP 初始拥塞窗口的存在,如果证书太长可能会产生额外的往返开销。如果证书没包含中间证书,大部分浏览器可以正常工作,但会暂停验证并根据子证书指定的父证书 URL 自己获取中间证书。这个过程会产生额外的 DNS 解析、建立 TCP 连接等开销,非常影响性能。
证书的最佳实践:
证书链是只包含站点证书和中间证书,不要包含根证书,也不要漏掉中间证书。
减小证书大小,使用 ECC(Elliptic Curve Cryptography,椭圆曲线密码学)证书。256 位的 ECC Key 等同于3072 位的 RSA Key,在确保安全性的同时,体积大幅减小。
OCSP Stapling
OCSP全称在线证书状态检查协议 (rfc6960),用来向 CA 站点查询证书状态,比如是否撤销。通常情况下,浏览器使用 OCSP 协议发起查询请求,CA 返回证书状态内容,然后浏览器接受证书是否可信的状态。这个过程非常消耗时间,因为CA 站点有可能在国外,网络不稳定,RTT 也比较大。浏览器发起 client hello 时会携带一个 certificate statusrequest 的扩展,服务端看到这个扩展后将 OCSP 内容直接返回给浏览器,完成证书状态检查。由于浏览器不需要直接向 CA 站点查询证书状态,这个功能对访问速度的提升非常明显。
努力的方向
QUIC(QuickUDP Internet Connection) ,Google新开发的一个基于UDP的协议,它提供了像TCP一样的传输可靠性保证,可以实现数据传输的0-RTT延迟,灵活的设计使我们可以对它的拥塞控制及流量控制做更多的定制,它还提供了传输的安全性保障,以及像HTTP/2一样的应用数据二进制分帧传输。
使用OpenSSL1.1.1版本,OpenSSL 官方宣布即将发布的新版本 (OpenSSL 1.1.1) 将会提供 TLS 1.3 的支持,而且还会和之前的 1.1.0 版本完全兼容,TLS 1.3 将是 Web 性能以及安全的一个新的里程碑,TLS1.3 带来的 0-RTT 握手,淡化了大家之前对使用 HTTPS 性能上的隐忧。
总结
互联网HTTPS化的趋势已经不可避免,如何在应用HTTPS保障安全性的同时,提高用户的使用体验,仍然是一个重要的话题。有货APP在应用HTTPS的过程中,HTTP1.1协议升级为HTTP2,以及TLS的一些探索性的优化,对于维护用户利益,提高用户体验,起到了重要的作用。优化是一项长期的工作,更是一项没有尽头的工作。所以,继续努力,创造更多的价值。