HTTPS 性能優化技巧

認識SSL/TLS

SSL和TLS都是用於保障端到端之間連接的安全性。SSL最初是由Netscape開發的,後來爲了使得該安全協議更加開放和自由,更名爲TLS,並被標準化到RFC中,現在主流的是TLS 1.2版本。

從上圖,可以看出SSL/TLS是介於應用層和傳輸層之間,並且分爲握手層(Handshake Layer)和記錄層(Record Layer)。

  • 握手層:端與端之間協商密碼套件、連接狀態。
  • 記錄層:對數據的封裝,數據交給傳輸層之前,會經過分片-壓縮-認證-加密。
算法選擇

TLS中可被配置的算法分類:

  1. 數字簽名:RSA、DSA
  2. 流加密:RC4
  3. 分組加密:DES、AES
  4. 認證加密:GCM
  5. 公鑰加密:RSA
  6. 消息認證碼:SHA
  7. **交換:Diffie–Hellman

密碼套件決定了會使用到的算法,例如執行「openssl ciphers -v 'ALL' | grep ECDHE-RSA-AES128-GCM-SHA256」:

ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(128) Mac=AEAD

表明該算法是在TLS 1.2中支持的,**交換採用ECDH(EC是指採用橢圓曲線的DH),數字簽名採用RSA,加密採用128位**長度的AESGCM,消息認證碼採用AEAD(AEAD是一種新的加密形式,把加密和消息認證碼結合到一起,而不是某個算法,例如使用AES並採用GCM模式加密,就能夠爲數據提供保密性、完整性的保障)。
如何理解完整性?
A 將明文M加密後爲MC,發給B,B解密,得到明文。 如果此時有中間人C,將MC替換爲CMC(雖然C不知道A怎麼加密的,但這沒關係),B將CMC解密,得到明文(那麼B拿到的其實是錯誤的明文)。 所以需要引入消息認證碼,B才能夠判斷收到的密文是否被篡改過。 這裏你可能會問:那如果C同時僞造消息認證碼呢? 這個就得看MAC和加密是如何配合的了,詳情可以查看認證加密中的Approaches to Authenticated Encryption章節。

在TLS握手和數據傳輸的不同階段會採用相應的算法:

  • 服務端身份驗證:數字簽名(RSA、ECDSA)
  • **交換:RSA/**交換算法(ECDH)
  • 加密/解密:流加密(RC4)和分組加密(3DES/AES/AESGCM)
  • 生成消息認證碼:SHA/AEAD

不知是否有人發現並沒有提到壓縮算法,如果google下TLS壓縮優化相關的內容,會發現沒有,因爲目前在TLS 1.2 RFC中,關於壓縮方法的結構定義爲enum { null(0), (255) } CompressionMethod;,即只有null方法(不進行壓縮)。目前存在對TLS壓縮的攻擊,可能是基於此原因,TLS壓縮目前只是個概念性的東西,沒有被真正應用起來。

如何選擇算法——安全性

通常加密算法的安全性依賴於**的長度,且不同加密算法,即使**長度相同,但提供的安全性也可能是不同的,相關資料:key size。所以並沒有一個標準的歸一化方法去衡量所有的加密算法,但是有來自世界上各個組織/機構對不同類型算法安全性的評估,可以看下這個網站:https://www.keylength.com/。

執行「openssl ciphers -v 'ALL' | wc -l」會發現有100+個密碼套件(不同openssl版本提供的密碼套件有點差異),然而,實際只會使用到其中一部分,因爲openssl提供的不少算法是不安全的,需要排除掉。

執行「openssl ciphers -v 'HIGH MEDIUM !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !CAMELLIA !IDEA !SEED !RC4' | wc -l」,發現只剩下50+個密碼套件。

篩選後剩下的密碼套件還是挺多的,一個個做性能測試的話,會GG的= =。其實可以根據需要支持的客戶端,再篩選出主流的密碼套件。網址:https://www.ssllabs.com/ssltest/clients.html,提供了絕大部分客戶端對TLS的支持情況,點擊相應的User agent可以查看到其支持的密碼套件,並且各套件的安全性也被標註出來了。

網址:https://www.ssllabs.com/ssltest/,可以用於測試服務器的SSL配置情況,並會給出得分,如下圖google的得分爲A:

如何選擇算法——性能

以下性能測試都是選取主流的算法進行。

數字簽名:ECDSA vs RSA

需要先分別生成採用ECDSA和RSA的簽名證書。

生成ECDSA自簽名的證書:

openssl ecparam -name prime256v1 -genkey -out ec_key.pem openssl req -new -x509 -key ec_key.pem -out cert.pem -days 365

-param_enc參數使用默認的named_curve就可以了,如果使用explicit,會發現生成的證書nginx能配置成功,但客戶端連接時會出現handshake error。
生成RSA簽名的證書:

openssl req -newkey rsa:2048 -nodes -keyout rsa_key.pem -x509 -days 365 -out cert.pem

執行openssl speed rsa2048 ecdsap256測試下:

                 sign    verify    sign/s verify/s rsa 2048 bits 0.000834s 0.000024s   1198.9  41031.9                              sign    verify   sign/s  verify/s 256 bit ecdsa (nistp256)   0.0000s   0.0001s  21302.5   9728.5

可以看到簽名性能ECDSA > RSA,而驗證性能RSA > ECDSA。

測試環境:

  • 服務端:1臺虛擬機CentOS 4核 openresty 2個worker
  • 客戶端:4臺虛擬機CentOS 4/2/2/2核(手頭只有這些虛擬機= =), 用shell腳本模擬併發的ab -c 800 -n 800(併發的ab實例數=2*CPU_NUM),使用time命令獲取消耗的時間
  • 測試頁面562字節,目標是測試數字簽名的性能,所以頁面小點,避免加密/解密、數據傳輸佔用太多時間

多臺客戶端如何同時啓動?ctrl+tab,命令+回車……
爲什麼不用jmeter?我用了1Master3Slave的jmeter分佈式壓測發現,jmeter對於在該場景(CPU bound)下的性能測試不行,服務端壓力上不去。
在相同的請求量下,RSA簽名會使服務端CPU佔用更高,所以這次測試需要在兩種簽名的壓測下,服務端CPU都保持在90%以上(不然的話,對ECDSA就不公平了)。
爲何openresty是2個worker?因爲開4個的話,ECDSA的壓測沒法使openresty4個worker的CPU消耗達到90%。
ECDHE-ECDSA-AES128-GCM-SHA256,服務端CPU佔比90%,結果:

客戶端(CPU核數標識) 4 2 2 2
第一次 11.988 17.334 9.161 7.748
第二次 12.524 13.750 12.129 7.582
第三次 11.836 17.991 9.195 10.023
第四次 11.617 7.081 9.168 8.919

ECDHE-RSA-AES128-GCM-SHA256,服務端CPU佔比100%,結果:

客戶端(CPU核數標識) 4 2 2 2
第一次 12.704 21.088 18.232 6.134
第二次 13.355 21.071 26.990 6.102
第三次 14.638 16.009 11.669 6.071
第四次 13.913 21.061 21.271 5.108

從表格中的數據可以看出ECDSA的性能要比RSA好點,這裏ECDSA的測試尚未壓榨完服務端呢。從openssl speed的結果也可以看出ECDSA的簽名性能是要遠超過RSA的,而且簽名是在服務端做的,所以面對海量的客戶端,服務端應該選擇使用ECDSA。

**交換:RSA vs ECDHE

測試環境同上,但只使用了4/2核兩臺客戶端機器發請求。證書使用的是生成的RSA證書,ECDSA證書能用到的**交換算法只能是ECDHE。

AES256-GCM-SHA384,服務端CPU佔比100%,結果:

客戶端(CPU核數標識) 4 2
第一次 12.144 15.737
第二次 12.133 15.452
第三次 11.902 16.145
第四次 11.614 16.133

ECDHE-RSA-AES256-GCM-SHA384,服務端CPU佔比100%,結果:

客戶端(CPU核數標識) 4 2
第一次 11.950 16.213
第二次 12.488 16.666
第三次 12.167 16.378
第四次 13.784 16.484

從表格中的數據可以看出ECDHE與RSA的性能差不多。ECDHE比RSA要多了一次端到端的傳輸,還會用到RSA對DH參數進行簽名和驗證;而RSA**交換則會使用到RSA的加密/解密,具體可看如下CloudFlare的兩張圖,圖片來自Keyless SSL: The Nitty Gritty Technical Details:
ECDHE支持前向保密(Forward Secrecy),簡單理解:中間人可以保存下來客戶端和服務端之間的所有通信數據,如果使用RSA握手,那麼未來某一天,中間人如果獲取到了服務端的私鑰,就可以解密所有之前採集的通信數據了;如果採用ECDHE握手的話,就可以避免這個問題。而且使用ECDHE握手的話,還有可能開啓TLS false start的特性(下文中會提到)。
RSA握手:

ECDHE握手:

所以**交換算法ECDHE會更好些。

對稱加密:AES256-GCM vs AES256 vs AES128-GCM vs 3DES

測試環境同上,但只使用了4核一臺客戶端機器發請求,ab參數爲 ab -n 2000 -c 10 ,ab實例4個,測試頁面153K。因爲是要壓測對應用層數據的加密解密性能,所以連接數少,但每個連接的請求數多。

ECDHE-RSA-AES256-GCM-SHA384,服務端CPU佔比94%,結果:

客戶端(CPU核數標識) 4
第一次 17.972
第二次 18.863
第三次 18.761
第四次 19.345

ECDHE-RSA-AES256-SHA384,服務端CPU佔比98%,結果:

客戶端(CPU核數標識) 4
第一次 20.490
第二次 19.575
第三次 19.725
第四次 20.262

ECDHE-RSA-AES128-GCM-SHA256,服務端CPU佔比92%,結果:

客戶端(CPU核數標識) 4
第一次 17.886
第二次 18.449
第三次 17.897
第四次 18.371

DES-CBC3-SHA,服務端CPU佔比100%,結果(太慢了,就測了兩個=。=):

客戶端(CPU核數標識) 4
第一次 52.262
第二次 51.476

從表格中的數據可以看出AES128GCM > AES256GCM > AES256 > 3DES。

消息認證碼:SHA256 vs SHA1 vs AEAD

測試環境同上。

AES256-SHA256,服務端CPU佔比100%,結果:

客戶端(CPU核數標識) 4
第一次 18.544
第二次 18.309
第三次 18.594
第四次 18.670

AES256-SHA,服務端CPU佔比98%,結果:

客戶端(CPU核數標識) 4
第一次 15.418
第二次 15.071
第三次 16.614
第四次 16.146

AES256-GCM-SHA384,服務端CPU佔比95%,結果:

客戶端(CPU核數標識) 4
第一次 14.443
第二次 15.669
第三次 15.880
第四次 15.960

從結果中可以看出AES256-GCM-SHA384 > AES256-SHA > AES256-SHA256。

會話恢復

Session Cache

客戶端希望恢復先前的session,或者複製一個存在的session,可以在ClientHello中帶上Session ID,如果服務端能夠在它的Session Cache中找到相應的Session ID的session-state(存儲協商好的密碼套件等信息),並且願意使用該Session ID重建連接,那麼服務端會發送一個帶有相同Session ID的ServerHello。

目前Nginx 只支持單機Session Cache,Openresty 支持分佈式Session Cache,但處於實驗階段。

Session Ticket

Session Cache需要服務端緩存Session相關的信息,對服務端存在存取壓力,而且還有分佈式Session Cache問題。 對於支持Session Ticket的客戶端,服務端可以通過某種機制將session-state加密後作爲ticket發給客戶端。客戶端憑藉該ticket就可以恢復先前的會話了。


類似於HTTP中用Json Web TOken作爲cookie-session的另一種選擇。


OCSP(在線證書狀態協議) stapling

當客戶端在握手環節接受到服務端的證書時,除了對證書進行簽名驗證,還需要知道證書是否被吊銷了,那麼需要向證書中指定的OCSP url發送OCSP查詢請求。

對於同一份服務端證書,如果每個客戶端都自己去查詢一次證書狀態就浪費了。所以,OCSP stapling就是爲了解決這一問題,由服務端查詢到證書狀態(通常會緩存一段時間),並返回給客戶端(客戶端會在本地校驗這個證書狀態是否真實)。

在nginx的配置中,可以選擇性的配置是否對OCSP response做校驗,防止將非法的證書狀態發送給客戶端。如果設置了校驗,ssl_trusted_certificate參數需要爲包含所有中間證書+根證書的文件。

如下圖是對nginx請求OCSP Server的抓包,可以看到發了個http的ocsp請求:

下圖是對nginx在發送證書給客戶端時,帶上的證書狀態的抓包:

TLS緩衝區調優

nginx默認的ssl_buffer_size是16K(TLS Record Layer最大的分片),即一個TLS Record的大小,如果HTTP的數據是160K,那麼就會被拆分爲10個TLS Record(每個TLS Record會被TCP層拆分爲多個TCP包傳輸)發送給客戶端。

如果TLS Record Size過大的話,拆分的TCP包也會較多,傳輸時,如果出現TCP丟包,整個TLS Record到達客戶端的時間就會加長,客戶端必須等待完整的TLS Record收到才能進行解密。

如果TLS Record Size小一些的話,TCP丟包影響的TLS Record佔比就會小很多,到達客戶端的TLS Record就會多些,客戶端乾等着的時間就相對少了。但是,TLS Record Head的負載就增加了,可能還會降低連接的吞吐量。

假設ssl_buffer_size設置爲1460byte:

通常,在TCP慢啓動的過程中,TLS Record Size小點好,因爲這個時候TCP連接的擁塞窗口cwnd較小,TCP連接吞吐量也小。而在TCP連接結束慢啓動之後,TLS Record Size就可以增大一些了,因爲這個時候吞吐量上來了。所以更希望能夠動態的調整nginx中ssl_buffer_size的大小,目前官方nginx還不支持,不過cloudflare爲nginx打了個patch,以支持動態的調整TLS Record Size:Optimizing TLS over TCP to reduce latency。

TLS False Start

某一端在發送 Change Cipher Spec、Finished 之後,可以立即發送應用數據,無需等待另一端的 Change Cipher Spec、Finished 。這樣,應用數據的發送實際上並未等到握手全部完成,從而節省出一個RTT時間。

完整握手時,Client Side False Start:

簡短握手時,Server Side False Start:

Client Side False Start需要的條件:

  • 客戶端和服務端都需要支持NPN/ALPN(瀏覽器要求)
  • 需要採用支持前向保密的密碼套件,即使用ECDHE進行**交換(RFC7918中有規定)
其他優化
  • TCP優化,畢竟SSL數據也是基於TCP進行傳輸的
  • 證書優化,採用ECDSA證書、服務器發送給客戶端的證書鏈包含所有中間證書
  • 硬件配置優化,例如使用SSL加速器
總結

本文是個人近段時間學習到的關於HTTPS性能優化的總結,推薦閱讀HTTPS權威指南和High Performance Browser Networking以瞭解更多內容。

推薦的密碼套件列表:

openssl ciphers -v 'ECDHE+ECDSA ECDHE AESGCM AES HIGH MEDIUM !kDH !kECDH !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !CAMELLIA !IDEA !SEED !RC4 !3DES'

其他額外的密碼套件,比如需要支持IE6,可以放在密碼套件列表末尾。

自己寫了個go程序用於檢測密碼套件列表支持/不支持的客戶端:sslciphersuitescheck

本文地址:http://www.linuxprobe.com/https-performance-optimization.html