美图HTTPS优化探索与实践
HTTPS是互联网安全的基础之一,然而引入HTTPS却会带来性能上的损耗。本文作者深入解析了HTTPS协议优化的各个方面,对实战很有帮助。
2012年斯诺登(EdwardSnowden)爆出棱镜门事件后,互联网安全问题日益得到大家的重视。去年Apple宣布2017年1月1日之前实现所有的App能够安全地接入服务器,这项声明来自于iOS9时代的应用程序安全传输功能(AppTransportSecurity)。逾期没有采用HTTPS的app将无法通过审核并遭到下架。同年美图也在11月完成了全网的HTTPS改造,将服务的安全级别到了一个新的高度。
本文将科普式的介绍HTTPS协议以及美图在HTTPS优化方面的探索与实践。Abstract
限于篇幅,本文不对TLSSSL协议的安全性做过多的假设与讨论,将围绕HTTPS的体验(速度)优化介绍一下几个方面:TLSSSL协议浅析HTTPS优化探索HTTPS优化实践SSL:安全套接字层概述
听到SSL协议很多同学可能立刻会想起一个问题:SSL协议和TLS协议到底有什么区别呢?
HTTPS是基于SSL的加密传输协议,使用SSL来协商加密密钥。SSL协议最早是由网景公司(Netscape)开发,但是随着网景的没落,现在由IETF负责维护,最初的版本也已经重新冠名TLS(安全传输层协议)1。0(1999年)。因此现在大部分协议是基于TLS的,尽管是相似的东西。所以这两个协议其实是同一个东西,叫什么名字都可以。
为了方便记录,本文后续将使用SSL协议来代指SSLTLS协议。
截止目前SSLTLS协议族中有7种协议:
SSLv1,SSLv2,SSLv3,TLSv1。0,TLSv1。1,TLSv1。2,TLSv1。3(draft):SSLv1从未正式公开。SSLv2协议设计有缺陷,不安全。SSLv3老旧过时,缺乏一些新的密钥特性。TLSv1。0在很大程度上是安全的,至少没有曝光重大的安全漏洞。TLSv1。1和TLSv1。2目前都没有著名的安全漏洞曝光。TLSv1。3仍然在草案阶段,而且有待时间检验。
常用Linux发行版默认OpenSSL版本:
协议栈关系
我们先来看看SSL协议到底工作在哪里。
TCP7层协议栈是我们都熟悉的内容,SSL协议是工作在表示层的协议。这也就是说SSL需TCPUDP之上工作;在HTTPFTPSNMP等协议之前创建会话。顾名思义HTTPS就是基于SSL协议的加密的HTTP协议。
SSL协议栈内容
整个SSL协议栈包含了三种类型的协议:握手协议:用于协商SSL密钥记录协议:用于记录SSL会话相关信息警报协议:用于通知对端停止SSL会话
这里我们结合抓包来重点看一下握手协议的工作过程。SSL:握手过程
先来一张图,看一看SSL握手的过程:
图上蓝色部分是TCP的握手,灰色部分是应用层加密的数据传输(HTTPS数据)。可以看到,整个握手过程设计,server与client交互的过程大致有一下几个部分:clienthelloserverhelloclientkeyexchangechangecipherspec
概括起来,前两个过程生成了非对称加密的重要参数,创建了非对称密钥。后两个过程交换了非对称加密的公钥。
欲详细了解握手的过程是如何发生的,交换了哪些内容,请参考我的另一篇博客:
从TCP建立连接到HTTPS接收到第一个数据包,到底发生了什么?〔1〕
接下来让我们通过简单的抓包我们可以看到整个SSL协议握手与创建会话的全过程。
wireshark是我最喜欢的抓包工具之一,另一个是tcpdump。为了生动形象更易阅读,我们用wireshark来抓包了解整个过程。
clienthello(client)serverhellocertificateexchange(server)changecipherspecfinished(client)changecipherspedfihished(server)
通过抓包,往往我们可以清楚地看到从建立TCP连接到TCP连接断开,整个SSL握手的过程。这个过程是艰辛复杂的,从握手开始到真正的加密数据发送,之间有多个RoundTrip。因此我们的优化过程就是从这里开始入手。SSL流量优化
顺着这样一个思路,先来看下优化到底能从哪些地方入手:优化SSL握手之前过程TCP优化优化SSL握手过程Falsestart优化握手流程加快密钥计算优化SSL握手之后过程HTTP2SPDY内容压缩对称加密套件选择优化SSL握手前的过程TCPFastOpen
TCPFastOpen(一下简称TFO)目的在于简化TCP握手的过程,通过一定的协商过程(SYN携带cookie信息)使得下一次握手的时候在SYN包中就可以携带数据,同时Server可以在发出SYNACK之后立即开始发送数据。因此如果我们的SSL握手建立TCP连接的时候能够启用TFO,那么我们的SSL握手流量就可以减少至少一个RTT。
但是,由于Serverlinux内核过于低,有的内核并没有支持TFO。这点在我编译nginx的时候就遇到了服务端内核过低的问题。
TCPFASTOPEN特性在kernel3。6被客户端支持,在kernel3。7被服务端支持。
想要开启TFO,先要检查下你的内核是否支持。TCP参数调优
大家都知道TCP的几个重要的算法,这里要说的就是TCP的慢启动(SlowStart)算法。
由于存在慢启动的特征,开始时的TCP窗口可能较为小,因此如果我们的SSL握手包非常的大,那么潜在的可能就是发生TCP分片,相当于增加了一个额外的RTT。因此,如果能在不影响其他服务的情况下,调整窗口的大小,也是一种优化的思路。
关于一点,我们在后面还会继续说明。优化SSL握手的过程
优化SSL握手过程是整个过程的核心,这里也是重点优化的方向。我们有以下几个思考优化的点:TLSFalseStartHSTSSessionResumptionOCSPStapling证书优化非对称加密运算加速SSLFalseStart
在SSL握手完成之前率先开始加密的application数据交互。这种思想很像刚刚提及的TFO思路。通过节省掉一次交互过程从而节省RTT。实现的基础是client拿到serverradom之后(serverhello)其实就已经有了生成非对称加密所需要的密钥的3个关键:
serverrandom,clientrandom,premastersecret。未开启FalseStart
开启了FalseStart
通过抓包可以发现,在刚收到437号包协商还没结束的时候,应用层的数据已经开始了传输(439442号包),而到443号包才完成了握手。
在现代的SSLFalseStart实现中,主要有两种方式:NPN:NextProtocolNegotiation协议文档〔2〕NPN最早引入falsestart思想,是SSL的扩展部分。不过Chrome51中移除将会NPN,因此应该尽快支持ALPN来替代ALPNApplicationLayerProtocolNegotiationALPN是Google根据HTTP2设计的,计划替代npn的新协议,目的与npn相同,通过协商,优化SSL握手的过程。然而,ALPN需要OpenSSL1。0。2支持,当前主流服务器操作系统基本都没有内置这个版本。所以需要进行升级。
这两者本身设计用于协议协商的,因此也可以用于支持HTTP2。但是他们之间有一些差异:NPN是服务端发送所支持的HTTP协议列表,由客户端选择NPN的协商结果是在ChangeCipherSpec之后加密发送给服务端ALPN是客户端发送所支持的HTTP协议列表,由服务端选择ALPN的协商结果是通过ServerHello明文发给客户端
这两个协议用于SSLFalseStart本身就是使用了副作用。不过,为了开启FalseStart,加密套件必须支持前向保密,例如ECDHERSA加密套件。HTTPStrictTransportSecurity
HTTPStrictTransportSecurity(简称HSTS)是一个安全功能,告诉浏览器只能通过HTTPS访问当前资源,禁止HTTP方式。最初目的防范降级攻击(Downgradeattack)或中间人攻击(Maninthemiddleattack)
我们肯定遇到过这样的情况:访问一个HTTP开头的网站,然后浏览器自动帮我们跳转到了HTTPS页面。这意味着一次潜在的重定向。
HSTS通过内置的preloadlist保存了一份可以定期更新的列表,对于列表中的域名,即使用户之前没有访问过,也会使用HTTPS协议。如果你的服务端开启了HSTS这个选项,那么客户端可以直接访问HTTPS页面,从而避免了一次无谓的跳转,其实也是加速了访问的过程。
这样做的好处是带来安全性的同时,避免了潜在的重定向带来的一次额外RTT。
然而全站HTTPSHSTS,导致一旦更换回HTTP,处于list的老用户会被无限重定向。
chrome查看本地hstspreloadlist的方式是访问chrome:netinternalshsts
SSLSessionResumption
这里使我们优化大点的重点。SessionResumption意为会话恢复。顾名思义,这里的会话正是指恢复先前的SSL会话。通过会话恢复技术我们可以以最小的代价,规避最耗时的握手过程。收益可观。
我们先上抓包:
通过抓包可以看到,开启了SessionResumption的SSL握手,在2个RTT的时候就已经完成了握手。这是如何做到的呢?
我们先来看3个名词概念:SessionID:会话ID,用于唯一标识一个SSL会话。SessionTicket:会话凭据,记录了本次会话相关的密钥与认证信息。SessionCache:会话缓存,用于缓存会话相关的信息。
目前实现会话恢复大致有2种方式:SessionID恢复会话和SessionTicket恢复会话。SessionID方式
TLS握手中生成的SessionID,服务端可以将SessionID和协商后的信息对应存起来。同时,浏览器也可以保存SessionID,并在后续的ClientHello握手中带上它,如果服务端能找到与之匹配的信息,就可以完成一次快速握手。
这样做带来的弊端也很明显:由于目前大多是大型互联网结构。负载均衡中,多机之间往往没有同步Session信息,如果客户端两次请求没有落在同一台机器上就无法找到匹配的信息,导致失败。服务端存储SessionID对应的信息不好控制失效时间,太短起不到作用,太长又占用服务端大量资源。
为了解决这个问题,可以通过IPhash来负载,但是依然容易因为网络环境变化导致握手的失败。通过Sessioncache共享也是一种解决方案,nginx提供了本机的共享,但是跨节点的共享就需要通过一个中心化的Sessioncache来实现。这无疑增加了跟多的成本。SessionTicket方式
SessionTicket是通过服务端安全密钥加密过的会话信息,最终保存在client。浏览器如果在ClientHello时带上了SessionTicket,只要服务器能成功解密就可以完成快速握手。这种方式缓解了SessionID带来的潜在的握手失败的风险。
然而,SessionTicket的加密使得必须保证所有server上的加密密钥相同否则负载均衡到不同机器的时候因为无法解密一样会导致失败。
看似,SessionTicket更容易解决Sessionid的一些痛点。然而在我们的抓包中,很容易发现,因为SessionTicket信息过大,导致TCP分片。特别是在开始阶段门限值小,更容易出现这个问题。OCSPStapling
OCSPstapling是OCSP的一个扩展协议,那么什么是OCSP呢?
在SSL握手阶段,客户端验证证书有效状态通常有两个方式:CRL(CertificateRevocationList,证书撤销名单)OCSP(OnlineCertificateStatusProtocol,在线证书状态协议)
CRL时效性差,而且list会越来越大,浏览器更新通常不及时(以天为单位)。
OCSP是为了解决旧的证书检查协议的弊端的实时协议,实时解析证书的有效性。
然而,某些客户端会在SSL握手阶段进一步协商时,实时查询OCSP接口,并在获得结果前阻塞后续流程,这对性能影响很大。OCSPstapling正是为了解决这些问题:将对于证书的验证过程代理到server上,减少client的压力。查询后打包下发client。同时OCSPstapling支持CertificateTransparency,证书安全性有保障。
通过OpenSSL内置工具可以查询服务端OCSPStalping的状态:
opensslsclientconnectvarycloud。com:443servernamevarycloud。comstatustlsextdebugdevnull21grepiOCSPresponseOCSPresponse:OCSPResponseData:OCSPResponseStatus:successful(0x0)ResponseType:BasicOCSPResponse
同一个请求,第一次可能没有开启OCSPStapling,原因在于server会先去获取OCSP的状态,这需要一个过程。证书优化
证书优化是指减小不必要的握手开销,这里我们先不讨论加密运算的消耗。通常我们有一个证书的原则,证书包含中间证书但是不包含根证书,这是最小化的证书配置,因为根证书浏览器一般都会内置。而中间证书如果不一同下发,那么浏览器会递归的查询请求中间证书,这也是无意义的消耗。
通常来说,我们常用的非对称加密算法是RSA。可以说,RSA也是我们信息安全的基石。然而RSA的密钥长度是非常巨大的,计算量也是惊人的。
一种更新的解决方案是使用DH加密。这也是一种非对称加密。对于DH加密,常常与椭圆曲线一起使用被称为ECDH。通过椭圆曲线优化之后的的DH加密有更好的性能同时占用更小的密钥长度。
ECDH算法使用较短的密钥即可达到相同程度的安全性,这是因为它依赖椭圆曲线而不是对数曲线。ECC是建立在基于椭圆曲线的离散对数问题上的密码体制,给定椭圆曲线上的一个点P,一个整数k,求解QkP很容易;给定一个点P、Q,知道QkP,求整数k确是一个难题。
OpenSSL1。0。2采用了Intel最新的优化成果。这个优化特性只是在1。0。1L之后才加入的,下图表格中OpenSSL的1。0。1e版本ecdh(nistp256)的性能只有2548,而OpenSSL的1。1。0b版本ecdh(nistp256)性能能达到10271,提高了4倍。非对称加密运算加速
RSA和ECDHERSA为什么会消耗CPU资源?
RSA主要是对客户端发回来premastersecret进行解密,它消耗CPU资源的过程是私钥解密的计算;而ECDHERSA则有两个步骤:
生成ECC椭圆曲线的公钥和几个重要的参数;对这几个参数进行签名,客户端要确保参数是服务端发过来的,就是通过RSA的签名来保证。
RSA签名为什么消耗CPU呢?RSA签名同样有两个步骤:
首先它通过SHA1进行Hash计算;对Hash结果进行私钥加密,也就是最终消耗CPU的过程是私钥解密和私钥加密的计算。这两个计算为什么消耗CPU?看下图公式,如果e或者d这个指数是一个接近2的2048次方的天文数字,那就非常消耗CPU。这也就是RSA算法为什么消耗CPU的最直接的数学解释。
对于非对称加密的优化,常见的方式有硬件加速卡GPU加速集群代理计算
专门的硬件优化可以节约大量的时间,这点相信了解计算机原理的同学都能明白。同样GPU代理计算是将计算与握手分离,同样是专用集群专门加速,因此效率更高。
因为对这些具体的硬件没有专门的测试和了解,就不过多的讨论。
优化SSL握手之后的流程
在通过了握手之后,我们的流量进入了安全的通信通道。此时还有可以优化的空间么?对称加密算法优化HTTP2SPDY(TCP多路复用)对称加密算法优化
经过了SSL握手之后的流量就进入了加密的通信通道,因此对称加密算法的效率其实也是十分重要的。然而因为大多数的对称加密算法并没有很大的优化空间,这里的优化往往得不到足够的重视。我们这里以一种特殊的加密算法为例进行介绍。
ChaCha20Poly1305是Google所采用的一种新式加密算法,性能强大,在CPU为精简指令集的ARM平台上尤为显著(ARMv8前效果较明显),在同等配置的手机中表现是AES的4倍。可减少加密解密所产生的数据量进而可以改善用户体验,减少等待时间,节省电池寿命等。
ARMv8之后加入了AES指令。所以在这些平台上的设备,AES方式反而比chacha20方式更快,性能更好。因此作为一个可选的方案,chacha20可能对于一些老旧的机型(向嵌入式大牛咨询过之后了解到,目前来说还有很多在使用armv7的硬件)来说更具优势。
SPDYHTTP2
对于流量的优化还有一种思路,那就是TCPSSL链接的复用。讲到http的多路复用,就绕不开HTTP2。这次,我们从HTTP2的始祖SPDY说起。
传统的http请求模型类似于1:1形式,单个TCP(SSL)链接服务一个HTTP请求,很多情况下,TCP链接资源并没有得到充分的利用。HTTP多路复用能够有效地提高TCP链路的效率。
SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。IETF对谷歌提出的SPDY协议进行了标准化,于2015年5推出了类似于SPDY协议的HTTP2。0协议标准(简称HTTP2)。谷歌因此宣布放弃对SPDY协议的支持,转而支持HTTP2。
简而言之,无论是SPDY还是HTTP2目的都在于提高链接的效率。而且,HTTP2天然支持SSLfalsestart和HTTPS,安全性和效率上都提供了保障。HTTPS优化实践
这里我们将通过数据的对比来检验SSL优化的效果。我们从以下3个测试结果来分析SSL优化的效果。SSLSessionResumptionOCSPstapling加密算法优化与分析测试方案
采用压力测试的方式,对比不同配置下,相同压力服务端的服务能力和延迟表现。
服务端配置CPUInfoE56核心12线程(超线程)32GRAMIntelCorporation82574LGigabitNetwork千兆网卡测试Server版本Linuxkernel2。6。32573。el6。x8664nginxversion:nginx1。10。2OpenSSLOpenSSL1。0。2l压测工具wrk是一个专门用于测试HTTP请求响应的工具。功能强大,但是对于HTTPS支持不够友好。wrkgo是我使用go和自己的测试框架perfm重新实现的一个测试工具,专门增加了HTTPS相关的选项,方便测试。文后有wrkgo链接,欢迎大家一起完善它!(这不是广告)〔3〕SSLSessionResumption
测试结果未开启会话恢复。wrkc24t24d3mHConnection:Closehttps:varycloud。comRunning3mtesthttps:varycloud。com24threadsand24connectionsThreadStatsAvgStdevMaxStdevLatency4。76ms8。34ms174。25ms97。01ReqSec51。9110。4890。0069。50223479requestsin3。00m,67。99MBreadNon2xxor3xxresponses:223479Requestssec:1240。95Transfersec:386。58KB开启会话恢复。wrkc24t24d3mHConnection:Closehttps:varycloud。comRunning3mtesthttps:varycloud。com24threadsand24connectionsThreadStatsAvgStdevMaxStdevLatency1。71ms10。17ms165。85ms98。78ReqSec360。0653。23545。0082。351544216requestsin3。00m,469。78MBreadNon2xxor3xxresponses:1544216Requestssec:8574。25Transfersec:2。61MB
通过对比可以看到,SSL会话恢复如果能够被成功复用,那么提升非常可观。相比基准数据能够提升大约64。
同时由于略过了最消耗CPU的运算过程,可以看到系统的负载明显降低,同时IO明显提升,这说明服务能力得到了提高。
综上可以得出,SessionResumption效果是非常明显的。OCSPStapling测试分析
OCSPStapling是我们感兴趣的另一个特性。下面给出开启OCSPStapling的表现测试数据。具体统计了平均耗时avg,耗时差异diff,数据包大小DataLength。
上图中白框圈中的部分为开启OCSP后握手步骤的差别及耗时相差最多的阶段。表示当前握手步骤和箭头指向的握手步骤是在同一个数据包。
分析
然而测试结十分令我们困惑:
理论上讲,OCSPSapling规避了一次客户端与服务端之间的通信,时间消耗上应该会有提升。然而却出现了下降。通过分析我们发现了端倪。
Client与两者的对比,在经历各自2000请求,发现与开启OCSPStapling的Server通信,统计上多消耗了10。347ms左右,最大的差异是Client收到Certificate后再到ClientKeyExchange,开启OCSPStapling的Server的通信多消耗了5。370ms。
对比两者握手差异,对应任务多了两部分新增加传输CertificateStatus的数据包,包大小为619bytes。验证CertificateStatus的合法性
由于网络传输耗时与所传输的数据量多少成正比,以到serverping的耗时为标准时间的话,从时间上推断传输Certificate加上CertificateStatus耗时大概约为16。184811。488(1514619)151416。1848
多出来的大概0。673ms可认为是验证CertificateStatus合法性的时间(如果不考虑两台服务器在通信时微小的网络差异的话)。
验证内容包括3。2SignedResponseAcceptanceRequirements〔4〕:验证证书一致在OCSPResponse中所指的证书和请求中所指证书一致。验证签名有效OCSPResponse中的签名有效。签名者身份合法签名者的身份和相映应接受请求者匹配。签名者正被授权签名回复。验证时间表示状态被认为是正确的时间(此次更新)足够新。如果有的话,下次更新的时间应该晚于现时时间。
对比未开启OCSP的访问,开启OCSP验证后,完整的握手建连过程中,请求时长会增加5ms左右的时间,用来传输CertificateStatus数据及验证OCSPresponse是合法可信的。加密算法优化调研
出于减少重复劳动的原因,这里直接给出一份ArcSummit上腾讯大牛分享过的一份优化测试文档里的数据:腾讯HTTPS性能优化实践
对于老旧机型,chacha20性能有显著的提升。参考资料
这篇文章的形成参考了很多大牛先前的实践经验与总结。感谢这些巨人的肩膀让我们可以有更广阔的视野:im。ququ的小站关于httphttps的文章与心得〔5〕HTTP2探索第一篇:概念〔6〕腾讯HTTPS性能优化实践开始使用ECC证书〔7〕KeylessSSL:TheNittyGrittyTechnicalDetails〔8〕ECDH算法概述(CNG示例)〔9〕Maninthemiddleattack〔10〕
我是谁:李子昂,WX:Regin(ID:Regin110)
干啥的:美图公司架构通讯核心研发工程师
来源:微信公众号:高可用架构
出处:https:mp。weixin。qq。comsmRcz8o0usoqmcEoGg9btg