TLS算法組合
在TLS中,5類算法組合在一起,稱為一個CipherSuite:
-
認證算法
-
加密算法
-
消息認證碼算法 簡稱MAC
-
密鑰交換算法
-
密鑰衍生算法
比較常見的算法組合是 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 和 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 都是ECDHE 做密鑰交換,使用RSA做認證,SHA256做PRF算法。
一個使用AES128-CBC做加密算法,用HMAC做MAC。
一個使用AES128-GCM做加密算法,MAC由於GCM作為一種AEAD模式並不需要。
SSL協議的握手過程
第一步,愛麗絲給出協議版本號、一個客戶端生成的隨機數(Client random),以及客戶端支持的加密方法。
第二步,鮑勃確認雙方使用的加密方法,並給出數字證書、以及一個服務器生成的隨機數(Server random)。
第三步,愛麗絲確認數字證書(對證書信息進行md5或者hash后的編號==用證書機構的公鑰對加密的證書編號解密后的證書編號)有效,然后生成一個新的隨機數(Premaster secret),並使用數字證書中的公鑰(鮑勃的公鑰),加密這個隨機數,發給鮑勃。
第四步,鮑勃使用自己的私鑰,獲取愛麗絲發來的隨機數(即Premaster secret)。
第五步,愛麗絲和鮑勃根據約定的加密方法,使用前面的三個隨機數,生成"對話密鑰"(session key),用來加密接下來的整個對話過程。
https要使客戶端與服務器端的通信過程得到安全保證,必須使用對稱加密算法並且每個客戶端的算法都不一樣,需要一個協商過程,但是協商對稱加密算法的過程,需要使用非對稱加密算法來保證安全,直接使用非對稱加密的過程本身也不安全,會有中間人篡改公鑰的可能性,所以客戶端與服務器不直接使用公鑰,而是使用數字證書簽發機構頒發的證書來保證非對稱加密過程本身的安全。這樣通過這些機制協商出一個對稱加密算法,就此雙方使用該算法進行加密解密。從而解決了客戶端與服務器端之間的通信安全問題。
Java 對SSL的支持
JDK7的client端只支持TLS1.0,服務端則支持TLS1.2。
JDK8完全支持TLS1.2。
JDK7不支持GCM算法。
JDK8支持GCM算法,但性能極差極差極差,按Netty的說法:
-
Java 8u60以前多版本,只能處理1 MB/s。
-
Java 8u60 開始,10倍的性能提升,10-20 MB/s。
-
但比起 OpenSSL的 ~200 MB/s,還差着一個量級。
Netty 對SSL的支持
Netty既支持JDK SSL,也支持Google的boringssl, 這是OpenSSL 的一個fork,更少的代碼,更多的功能。
依賴netty-tcnative-boringssl-static-linux-x86_64.jar即可,它里面已包含了相關的so文件,再也不用管Linux里裝沒裝OpenSSL,OpenSSL啥版本了。
性能問題的出現
JDK7的JMeter HTTPS客戶端,連接JDK8的Netty服務端時,速度還可以。
JDK8的JMeter HTTPS客戶端,則非常慢,非常慢,非常吃客戶端的CPU。
按套路,在JMeter端增加啟動參數 -Djavax.net.debug=ssl,handshake debug 握手過程。
(OpenSSL那邊這個參數加了沒用)
*** ClientHello, TLSv1.2,可以看到,Client端先發起協商,帶了一堆可選協議
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256…]
*** ServerHello, TLSv1.2 然后服務端回選定一個
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
還可以看到,傳輸同樣的數據,不同客戶端/服務端組合下有不同的紀錄:
Client: JDK7 JDK SSL + Server: JDK7/8 JDK SSL
**TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
WRITE: TLSv1 Application Data, length = 32
WRITE: TLSv1 Application Data, length = 304
READ: TLSv1 Application Data, length = 32
READ: TLSv1 Application Data, length = 96
READ: TLSv1 Application Data, length = 32
READ: TLSv1 Application Data, length = 10336
Client: JDK8 JDK SSL + Server: JDK8 Open SSL
** TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
Thread Group 1-1, WRITE: TLSv1.2 Application Data, length = 300
Thread Group 1-1, READ: TLSv1.2 Application Data, length = 92
Thread Group 1-1, READ: TLSv1.2 Application Data, length = 10337
原因分析
JMeter Https 用的是JDK8 SSL,很不幸的和服務端的OpenSSL協商出一個JDK8實現超慢的TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。
對於服務端/客戶端都是基於Netty + boringssl的RPC框架,使用TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 仍然是好的,畢竟更安全。
但Https接口,如果不確定對端的是什么,JDK7 SSL or JDK8 SSL or OpenSSL,為免協商出一個超慢的GCM算法,Server端需要通過配置,才決定要不要把GCM放進可選列表里。
解決方法
平時是這樣寫的:
SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider( SslProvider. OPENSSL).build();
如果不要開GCM,那把ReferenceCountedOpenSslContext里面的DEFAULT_CIPHERS抄出來,刪掉兩個GCM的。
List<String> ciphers = Lists.newArrayList(“ECDHE-RSA-AES128-SHA”, “ECDHE-RSA-AES256-SHA”, “AES128-SHA”, “AES256-SHA”, “DES-CBC3-SHA”);
SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).sslProvider( SslProvider.OPENSSL).ciphers(ciphers).build();
總結
-
OpenSSL(boringssl)比JDK SSL 快10倍,10倍!!! 所以Netty下盡量都要使用OpenSSL。
-
在確定兩端都使用OpenSSL時,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 仍然是好的,畢竟更安全,也是主流。
-
對端如果是JDK8 SSL時,Server端要把GCM算法從可選列表里拿掉。