RSA异步代理计算
从第一弹的分析可以知道,HTTPS协议中最消耗CPU计算资源的就是密钥交换过程中的RSA计算。也是我们优化的最主要对象。
如何优化呢?思路如下:
- 算法分离。将最消耗CPU计算的过程分离出来,释放本地CPU,提升整体吞吐性能。
- 并行计算。使用SSL硬件加速卡或者空闲CPU并行计算,提升计算效率。
- 异步代理。算法分离和计算的过程是异步的,不需要同步等待SSL加速计算的结果返回。
下面详细介绍一下上述步骤:
算法分离
算法分离的核心思想是:分析加密算法的完整过程,将最消耗CPU的计算过程剥离出来,避免在本地CPU上进行同步计算。
需要分离哪些算法呢?
由于我们现在的证书主要使用RSA签名,暂时没有ECDSA签名证书,所以当前设计只针对RSA签名证书。
RSA签名证书最常用的密钥交换算法是ECDHE_RSA,DHE_RSA和RSA。所以我们需要重点解决的就是这三个算法。
由于DHE_RSA算法性能较差,所以优先推荐使用ECDHE_RSA和RSA密钥交换算法。下面详细描述一下两个算法的具体分离过程。
ECDHE_RSA密钥交换算法分离
SSL完全握手过程中,ECDHE_RSA涉及到的最消耗CPU的握手消息为ServerKeyExchange。
那ServerKeyExchange消息为什么需要大量的CPU计算?它需要处理哪些内容呢?主要是如下两步:
- 选择ECC(椭圆曲线密码)的曲线类型、基点、曲线系数等参数,并根据这些参数生成公钥。
- 对曲线参数和公钥进行RSA签名。
由之前的分析得知,这里的RSA签名过程需要使用2048位长度的私钥对数据进行加密,非常消耗CPU。
RSA密钥交换算法分离
RSA密钥交换算法的过程相对简单,因为没有ECC参数及公钥生成的过程。根据RFC5246描述,客户端使用RSA公钥对premaster内容进行加密,服务端需要使用私钥解密premaster key,从而生成最终的master key。
同样地根据之前的分析,RSA解密相比SHA256计算要消耗更多的CPU计算量。
并行计算
为了提升单位时间T内处理的性能,有两个思路:
- 减少单个请求的计算时间。
- 提升请求并发计算能力。即能够同时处理更多个请求
减少单个请求的计算时间通过采用更高频率和性能的CPU或者专用硬件加速卡的方案能够解决,本文不多做介绍。
提升请求并发计算能力是指同一时刻使用多个CPU或者多个硬件加速卡方案实现性能的提升。
显然,如果使用更多个数的CPU和硬件加速单元,并行计算能力就得到了显著提升。同样单位时间T内,能够处理的请求数变成了:
4*T / T1
相比串行计算,性能提升了4倍。
异步请求
Nginx的当前进程必须等待openssl完成ServerKeyExchange或者premaster secret的处理后才能返回进行其他工作。
同步请求的弊端是:
- 由于该过程需要消耗大量的CPU,nginx整体性能受到严重制约。
- 除非同步计算的能力非常强,否则即使将过程分离到其他硬件或者CPU完成,由于进程间或者网络间的开销,同步过程也会严重制约nginx的整体性能。
所以上述计算过程需要异步进行,即在openssl进行高强度CPU计算时,比如处理serverKeyExchange或者premaster secret消息,nginx当前进程无需等待计算结果的返回,可以马上执行其他工作。
异步请求的过程:
- Nginx接收到请求1后,调用RSA_sign。
- RSA_sign此时会调用RSA_private_encrypt,然后直接返回,不需等待RSA的签名结果。
- Nginx此时可以处理其他请求。
- RSA_private_encrypt是RSA签名的核心函数,主要是使用RSA私钥对哈希值进行加密。它的最主要计算过程还是大数的模幂计算。
- 最消耗CPU的计算由于已经被分离到其他CPU或者硬件加速卡,所以不会消耗本地CPU,同时由于这个过程是异步的,也不会阻塞上层的NGINX。
RSA异步代理计算的工程实现
工程实现的难点主要体现在对openssl和nginx核心代码的掌控上。可以概括成如下几点:
- 需要学习和理解的知识量大。包含 ssl3.0到tls1.2协议,pki体系,pkcs标准,x509标准,ECC标准,光RFC的阅读涉及至少30个以上,常用的比如5246,5280,4492等。
- openssl代码量大、旧、乱、深。
- 大。代码行数超过50万行。因为要实现不同协议版本,不同算法组合,还要跨平台,支持各种硬件,所以代码量非常庞大。
- 旧。openssl有很多历史遗留的无用代码,比如一些过时的算法、系统及加速硬件。
- 乱。风格不良不统一,充斥着大量宏定义,宏开关,缺少注释等。
- 深。由于涉及到版本和算法很多,本身就比较难懂,又进行了一系列的高层抽象和封装,比如EVP,ssl23,ssl3系列等。
提到了这么多openssl不好的地方,网上甚至有一些文章公开嘲笑甚至辱骂openssl,但是在我的心里却一直认为,一份开源免费却守护着虚拟世界安全的代码,值得每一个人尊敬和崇拜。
- openssl虽然非常重要,但是互联网上关于openssl和HTTPS代码工程方面有深度有价值的参考资料几乎为零。
- 需要修改nginx事件框架实现SSL完全握手的优化。nginx虽然代码优良,参考资料也多,但是代码有很多细节设计得比较巧妙,修改事件框架很容易踩坑。
计算架构的变化
RSA计算方式的变化必然会导致计算架构的变化。其中现在默认的广泛使用的方式又叫本机CPU同步计算架构。
本机同步计算架构
这里的同步是指上层应用比如nginx必须等待CPU执行完RSA计算后才能返回执行其他工作。
这里需要注意的是,即使将同步模型的CPU换成SSL硬件加速卡,对性能的提升也非常有限,不到30%。
异步代理计算架构
异步代理计算架构的特点将最消耗性能的RSA计算分离出来,使用并行计算能力更强的方案替代本机CPU完成计算,同时整个过程是异步的,上层应用程序(NGINX)不需要等待RSA计算结果的返回就能接收其他请求。
openssl协议栈的改造
由于RSA计算和非对称密钥交换有关且发生在SSL完全握手的过程中。同时由于openssl本身只能支持RSA同步计算,不支持异步调用,如果要实现RSA的异步代理计算,必须要对openssl的协议栈进行改造。
前面也提到过openssl的一些问题,事实也被业界诟病了很久,直到heartbleed漏洞爆发后,google和openbsd先后推出了openssl的fork版本,那么我们该如何做出选择呢?
openssl,boringssl, libressl的选择。
boringssl
最开始打算选用boringssl,它是google推出的基于openssl的fork 版本。它的优点是代码风格良好,代码精简(只有不到12W行C代码 7W行头文件),代码健壮 。
但是很快我们就放弃了boringssl,原因是:
- boringssl虽然是一个开源库,但它只是面向google自家使用,并不提供通用的兼容性和稳定性保证,官方网站明确说明不提供API,ABI的可靠性保证。
- Google在安全方面的激进策略和国内产品的保守落后现状容易产生矛盾。比如及早在boringssl中移除SSLv3,RC4,Spdy等的支持。但是国内还有很多客户端只支持SSLV3等协议和ciphersuite。Google一心想推动协议和算法朝着更加安全、高效的方向前进,无奈部分老旧客户端拖了严重后腿,为了考虑这部分用户需求,我们不敢轻易使用boringssl。
总得来说,boringssl适合阅读,学习,但不适合用于面向广大客户端的业务。
libressl
libressl 是openbsd推出的基于openssl的fork版本,从名字也能看出来,它的目的是想取代openssl。
它基本上具备了boringssl的优点,比如代码量精简,风格良好,更加安全等特点。最重要的是,已经有一些关键系统(openbsd, OS X10.11)等,使用了libressl。证明这已经是一个工业级的可靠的开源库了。
所以在前期的部署过程中,我们选用了libressl,但是后来压力测试发现它有一个致命的缺点,ECDHE的性能非常差,只有openssl的1/4左右。为什么会这样呢?我猜测原因可能是跟intel针对ecdhe算法进行了一个很大的优化,它将算法专利捐给了openssl,但是并未捐献给libressl。导致libressl无法直接使用这一优化算法。
openssl
由于上述两个库的重大缺陷,我们最终还是回归了openssl。
nginx事件机制的改造
nginx的模块功能很丰富也很强大,一共有11个可以介入的阶段(phase),包括起始的读取请求内容的阶段NGX_HTTP_POST_READ_PHASE,到最后一个打印日志阶段NGX_HTTP_LOG_PHASE,其中只有7个phase可以编写插入自定义模块。
出于通用性的考虑,nginx模块实现时机有一个限制,那就是必须在http 的header全部解析完成之后,自定义模块才能够工作。所以存在如下局限:
- 模块介入时机有限。比如无法干预TCP及ssl握手阶段的交互。
- 部分功能会影响性能。假如我们需要增加一个IP黑名单,事实上在tcp accept时就可以开始禁止该IP连接,不需要等到HTTP的头部数据解析完后才开始工作,因此Accpet之后的所有开销都是浪费的。特别是攻击规模比较大的时候,这部分对性能的影响也比较严重。
也正是由于第一个局限,我们无法通过添加一个自定义模块,而必须对nginx的核心事件代码进行改造,才能完成我们的性能优化目标。
nginx SSL握手事件的改造
nginx官方版本中的的SSL握手事件代码集中在src/event/ngx_event_openssl.c中,当然调用过程还是需要由ngx_http_request.c中发起,TCP握手完成后,调用ngx_http_ssl_handshake开始整个握手过程。
整个SSL握手事件的入口是ngx_ssl_handshake,有两个出口:
- ngx_http_close_connection(),握手异常,关闭连接。
- ngx_http_wait_request_handler(),握手成功,开始等待应用层的HTTP数据。
改造的ngx ssl握手事件不会改变或者删除原有逻辑,只是增加了远程异步代理计算的逻辑。
RSA异步代理性能优化结论
最终通过RSA异步代理计算,nginx ecdhe_rsa完全握手性能提升了3.5倍,由18000qps提升到了65000qps。
对称加解密的优化
虽然之前性能分析里提到了相比非对称密钥交换算法来讲,对称加密算法的性能非常卓越(好1到2个数量级),但是如果应用层传输内容较大的话,特别是移动端的CPU计算能力较弱,对称加密算法对性能的影响也不容忽视。
如何优化呢?通过异步代理的方式显然不可能。原因是:会极大降低用户访问速度。由于应用层的每一个字节都需要对称加解密,使用异步的方式实现会严重降低加解密的实时性。
那有没有同步的优化方式呢?有。类似SSL硬件加速卡,intel针对AES算法实现硬件加速,并将它集成到了CPU指令里。
AES-NI指令
AES-NI是intel推出的针对AES对称加密算法进行优化的一系列指令,通过硬件计算实现计算速度的提升。
如何测试AES-NI的性能呢?通过环境变量。
aes-ni: OPENSSL_ia32cap="~0x200000200000000" openssl speed -elapsed -evp
aes-128-gcm 或者在代码里将 crypto/evp/e_aes.c # define AESNI_CAPABLE
(OPENSSL_ia32cap_P[1]&(1<<(57-32)))进行设置。
aesni对性能的提升约20%, 由4.3W提升到5.1W。
这里需要注意的是,如果需要单独使用openssl的API进行AES对称加解密,最好使用aes evp API,这样才会默认开启AES-NI指令。
chacha20-poly1305
chacha20-poly1305是由Dan Bernstein发明,并且由google推出的一种带身份认证的对称加密算法。其中chacha20是指对称加密算法,poly1305指身份认证算法。这个算法是对没有AES硬件加速功能的移动平台的补充,比如ARM芯片。
从google公布的数据来看,chacha20-poly1305能够提升30%以上的加解密性能,节省移动端耗电量。
当然,如果手机端支持AES-NI指令的话,chacha20就没有优势了。
我们最开始选用libressl的一个重要原因也是它支持chacha20-poly1305,openssl虽然暂时不支持,不过最近发布的版本应该马上就会支持了。
session resume
HTTPS最消耗性能的阶段就是完全握手,不管是对用户的访问速度还是CPU资源消耗,避免完全握手的发生都能够极大地提升性能。
SSL协议目前提供两种机制来实现简化握手,避免完全握手的发生:
- session cache
- session ticket
session cache
session cache的原理
SSL2.0引入了session identifier机制,如果客户端使用的SSL协议版本大于2.0(全部浏览器都支持,包括IE6),那么server端在收到client hello消息时会生成一个32字节长度的ID(SSL2.0以后ID是0到48字节长度),保存在缓存并且将生成的session id通过server hello消息发送给用户。
客户端在后续的SSL握手请求中通过client hello消息发送session id,server端获取到ID后会从本地或者集群缓存中查找,如果ID查找命中,表明这个session 是可以信任的,能够复用。SSL握手提前完成,不需要继续处理完全握手需要的密钥交换等消耗CPU资源的步骤,同时节省了一个RTT。
分布式session cache的应用
Session identifier支持得非常广泛。但nginx目前只支持内置缓存及单机进程间共享的session缓存,在多服务器的接入架构下,单机的session缓存几乎是无效的。
针对这种场景,TGW支持四层会话保持,这样在会话保持期间内的client都会落到相同的机器,显著地提升了session cache的命中率。
session ticket
session ticket的原理
session tickets (RFC5077)是一种不需要server端保存session状态信息的session恢复机制。客户端在client hello消息里发送empty session ticket extension表示支持session ticket机制,服务端的nginx在server hello里也会发送一条empty sesson ticket 消息表示支持。这样在完全握手快要结束时,nginx会发送new session ticket消息生成一个新的ticket。
客户端在后续的请求过程中会在client hello包里携带这个ticket,如果nginx能够正确解密这个ticket,标明session能够复用。握手完成,同时发送new session ticket更新ticket。即每次发送请求的ticket都不同。
分布式session ticket的应用
在多个STGW接入的环境下,同样存在不同用户的session ticket无法被正确处理的问题。为了解决这个问题,STGW配置了全局的session ticket key,即针对全部STGW的nginx,使用相同的key来进行加解密。相同客户端的session ticket,不管下次落到哪台nginx,都能被正确处理,实现简化握手。
机制
优点
缺点
session ticket
server端不需要保存状态,不需要维护内存
1. 只是 TLS 协议的一个扩展特性,目前的支持率不是很广泛,只有 60% 左右。 2.session ticket 需要维护一个全局的 key 来加解密,需要考虑 KEY 的安全性和部署效率。
session cache
session id 是 TLS 协议的标准字段,市面上的浏览器全部都支持 session cache
1.需要消耗服务端内存来存储 session 内容。2. 目前的开源软件包括 nginx,apache 只支持单机多进程间共享缓存,不支持多机间分布式缓存。
结论
- 提升session resume比率,尽量实现分布式session cache及session ticket,减少SSL完全握手的发生。不仅节省网络RTT,提升用户访问速度,也避免了非对称密钥交换的发生,减少了CPU的消耗。
- 通过异步代理完成RSA的私钥计算。ssl完全握手性能由18000qps提升到了63000qps,提升了~3.5倍。节省了接入机器成本,提升了业务的活动运营及防攻击能力。
- 使用性能更高,更安全的对称加密算法,AES-GCM,CHACHA20-POLY1305。
- 开启对称算法加速指令AES-NI。