SSL和TLS都是用於保障端到端之間連接的安全性。SSL最初是由Netscape開發的,後來爲了使得該安全協議更加開放和自由,更名爲TLS,並被標準化到RFC中,現在主流的是TLS 1.2版本。
從上圖,可以看出SSL/TLS是介於應用層和傳輸層之間,並且分爲握手層(Handshake Layer)和記錄層(Record Layer)。
TLS中可被配置的算法分類:
密碼套件決定了會使用到的算法,例如執行「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握手和數據傳輸的不同階段會採用相應的算法:
不知是否有人發現並沒有提到壓縮算法,如果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。
測試環境:
多臺客戶端如何同時啓動?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 url發送OCSP查詢請求。
對於同一份服務端證書,如果每個客戶端都自己去查詢一次證書狀態就浪費了。所以,OCSP stapling就是爲了解決這一問題,由服務端查詢到證書狀態(通常會緩存一段時間),並返回給客戶端(客戶端會在本地校驗這個證書狀態是否真實)。
在nginx的配置中,可以選擇性的配置是否對OCSP response做校驗,防止將非法的證書狀態發送給客戶端。如果設置了校驗,ssl_trusted_certificate參數需要爲包含所有中間證書+根證書的文件。
如下圖是對nginx請求OCSP Server的抓包,可以看到發了個http的ocsp請求:
下圖是對nginx在發送證書給客戶端時,帶上的證書狀態的抓包:
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。
某一端在發送 Change Cipher Spec、Finished 之後,可以立即發送應用數據,無需等待另一端的 Change Cipher Spec、Finished 。這樣,應用數據的發送實際上並未等到握手全部完成,從而節省出一個RTT時間。
完整握手時,Client Side False Start:
簡短握手時,Server Side False Start:
Client Side False Start需要的條件:
本文是個人近段時間學習到的關於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