使用 BoringSSL 優化 HTTPS 加密演算法選擇

Jerry Qu發表於2015-10-20

前不久,一位朋友在我部落格評論中,問到:類似於 Google 那樣電腦訪問使用 AES,手機訪問使用 CHACHA20 的演算法是怎麼實現的。最近我研究了一下這個問題,現在我的部落格也支援這個特性了。今天抽空介紹一下我的實現步驟,供喜歡折騰的朋友們參考。

使用 BoringSSL 優化 HTTPS 加密演算法選擇

對稱內容加密

我們知道,每個 TLS 會話都是在握手階段通過非對稱加密得出對稱加密金鑰,而本次會話雙方一直會用這個金鑰進行流量的對稱加密。這樣做是出於效能考慮,畢竟對稱加密速度要快得多,更適合全流量使用。

對稱加密演算法有流式、分組兩種。RC4 就是一個常見的流式加密演算法,不過已被證實不再安全,應該停止使用。Google 推出了一種名為 ChaCha20-Poly1305 的流式加密新演算法,已經內建於各大平臺的 Chrome 之中。ChaCha20 除了更安全,還針對 ARM 做了優化,在移動裝置上使用速度更快、更省電。以下是它與 AES-GCM 在加密速度上的對比:

AES-GCM 是目前推薦使用的分組加密模式,它的缺點是計算量大,導致效能和電量開銷比較大。為此,Intel 推出了一個名為 AES NI(Advanced Encryption Standard new instructions)的 x86 指令集擴充套件,從硬體上提供對 AES 的支援。Intel 自家 CPU 從 Westmere 平臺開始支援 AES-NI,目前在 PC 端 AES-NI 的普及率顯然很高。對於支援 AES-NI 的裝置來說,使用 AES-GCM 加密演算法無疑是最優選擇,以下是一份對比(測試使用支援 AES-NI 的 Intel Xeon E3-1220 V2 @ 3.10GHz):

Did 20341000 AES-128-GCM (16 bytes) seal operations in 3000099us (6780109.6 ops/sec): 108.5 MB/s
Did 2356000 AES-128-GCM (1350 bytes) seal operations in 3000761us (785134.2 ops/sec): 1059.9 MB/s
Did 438000 AES-128-GCM (8192 bytes) seal operations in 3002910us (145858.5 ops/sec): 1194.9 MB/s
Did 17839000 AES-256-GCM (16 bytes) seal operations in 3000160us (5946016.2 ops/sec): 95.1 MB/s
Did 2092000 AES-256-GCM (1350 bytes) seal operations in 3000884us (697127.9 ops/sec): 941.1 MB/s
Did 388000 AES-256-GCM (8192 bytes) seal operations in 3004207us (129152.2 ops/sec): 1058.0 MB/s
Did 7779000 ChaCha20-Poly1305 (16 bytes) seal operations in 3000332us (2592713.1 ops/sec): 41.5 MB/s
Did 1139000 ChaCha20-Poly1305 (1350 bytes) seal operations in 3001412us (379488.1 ops/sec): 512.3 MB/s
Did 220000 ChaCha20-Poly1305 (8192 bytes) seal operations in 3006395us (73177.3 ops/sec): 599.5 MB/s

可以看到,儘管純軟體實現的 ChaCha20 演算法已經十分優秀,但跟有 AES-NI 加持的 AES-GCM 比起來還是差距明顯。

綜上,我們很容易想到「僅針對支援 AES-NI 的終端使用 AES-GCM 演算法,否則使用 ChaCha20」無疑是一個非常完美的方案。

BoringSSL

之前文章介紹過,基於 LibreSSL 編譯 Nginx,可以輕鬆地使用 ChaCha20。但問題是一旦配置了 ChaCha20,只要終端支援,無論桌面裝置還是移動裝置都會使用它。

BoringSSL 是 Google 從 OpenSSL 拉出來的一個獨立發展的分支,目前跟 OpenSSL 相比已經有很多不同之處了。BoringSSL 支援了一種名為「等價加密演算法組(Equal preference cipher groups)」的配置,正好可以滿足我們這個需求。

基於 BoringSSL 編譯 Nginx 之後,可以像下面這樣配置 ssl_ciphers:

ssl_ciphers [ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305]:[ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:DES-CBC3-SHA;

方括號之中的配置就是「等價加密演算法組」,用豎線隔開的兩種演算法,會被自動應用於最合適的場景(支援 AES-NI 優先使用 AES-GCM,否則優先使用 ChaCha20)。最後的 DES-CBC3-SHA 是為了支援 Windows XP 上的 IE8 而加上去的。

下圖中,同樣是訪問本部落格,左側是 Mac Chrome 的截圖,右側是 iPhone Chrome 的截圖:

可以看到,只有移動端才使用了 ChaCha20。

這裡簡單介紹一下「等價加密演算法組」的原理:我們知道客戶端建立 TLS 連線時,在傳送的 Client Hello 中會帶上自己支援的加密演算法,供服務端從中挑選。由於老舊客戶端會支援一些不安全的加密演算法,為了提高傳輸安全,通常會在服務端指定一個可用演算法列表,最終使用的加密型別取決於二者的交集,並按服務端優先順序取第一個;如果沒有交集,直接終止會話。在 Nginx 中這個功能通過將 ssl_prefer_server_ciphers 設定為 on 開啟。

那麼問題來了,對於同時支援 AES-GCM 和 ChaCha20 的 Chrome 來說,服務端列表無論把哪個放前面都會導致另外一個完全沒機會被選中。而「等價加密演算法組」的意義在於,等價組內的演算法具有相同優先順序。這樣,客戶端可以把想要優先使用的加密演算法放在前面。舉例說明二者的區別(開啟ssl_prefer_server_ciphers 條件下):

  • 不支援等價組時,如果服務端列表是:A、B、C,瀏覽器 1 支援:A、B,最終使用 A;瀏覽器 2 支援 B、A,最終使用 A,瀏覽器 3 支援 C、A,最終使用 A;
  • 支援等價組時,如果服務端列表是:[A|B]、C,瀏覽器 1 支援:A、B,最終使用 A;瀏覽器 2 支援 B、A,最終使用 B,瀏覽器 3 支援 C、A,最終使用 A;

可以看到,開啟 ssl_prefer_server_ciphers 可以讓會話使用最安全的加密演算法(前提是服務端配置正確),而「等價加密演算法組」還可以讓瀏覽器有區域性調整的許可權。

補充一下通過 Mac Chrome 和 iPhone Chrome 分別訪問我的部落格傳送的加密演算法列表(使用 wireshark 抓包,在 Client Hello 握手中可獲得):

Mac Chrome:

Cipher Suites (17 suites)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc14)
Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc13)
Cipher Suite: TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc15)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0×0039)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0×0033)
Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0×0035)
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)

iPhone Chrome:

Cipher Suites (15 suites)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc14)
Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc13)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0×0033)
Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0×0039)
Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0×0035)
Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)

可以看到,Chrome 瀏覽器確實是會在不同平臺上傳送不同順序的演算法列表,只要服務端配置了「等價加密演算法組」就可以實現本文所描述的功能。

最後提醒大家:現階段 BoringSSL 不支援 OCSP Stapling。改用 BoringSSL 後,如果在 ssllabs 測試中發現這一項變成 off 不要吃驚。

詳細配置步驟

之所以把這部分內容放在最後,是因為折騰起來有點費勁,嫌麻煩的同學可以直接忽略之後所有內容。

以下步驟在我兩臺系統為 Ubuntu 14.04.3 的 VPS 上都能正常執行。如果你遇到了問題,請留言指出。

首先,獲取編譯所需的 Nginx 和 BoringSSL 原始碼,Nginx 從 1.7.4 開始支援 BoringSSL,這裡我直接使用最新版:

SHELLwget http://nginx.org/download/nginx-1.9.5.tar.gz
tar xzf nginx-1.9.5.tar.gz

git clone https://boringssl.googlesource.com/boringssl

現在,當前目錄下應該有這兩個子目錄:

boringssl/
nginx-1.9.5/

確認無誤後,還要做一些準備工作:

SHELL# 安裝編譯 BoringSSL 所需的 Golang
sudo apt-get install golang

# 忽略編譯過程中的 Warning(不加這個,編到一半會因為 Warning 太多無法繼續)
export CFLAGS="-Wno-error"

編譯 BoringSSL:

SHELL# 進入 BoringSSL 原始碼根目錄
cd boringssl

# 建立 build 目錄並編譯,完成後回到 BoringSSL 原始碼根目錄
mkdir build && cd build && cmake ../ && make && cd ../

# 建立 .openssl 目錄,並將庫檔案和編譯後的檔案放進去
mkdir -p .openssl/lib && cd .openssl && ln -s ../include . && cd ../
cp build/crypto/libcrypto.a build/ssl/libssl.a .openssl/lib

現在可以編譯 Nginx 了:

SHELL# 進入 Nginx 原始碼根目錄
cd nginx-1.9.5

# 修改時間,避免 Nginx 再次編譯 BoringSSL
touch ../boringssl/.openssl/include/openssl/ssl.h

# 指定使用 BoringSSL 作為 SSL 庫
./configure --with-openssl=../boringssl --with-http_v2_module --with-http_ssl_module

一切無誤後可以開始 make 和 make install 了。這期間可能還會遇到這樣一個報錯:

‘SSL_R_BLOCK_CIPHER_PAD_IS_WRONG’ undeclared

這是因為 BoringSSL 刪掉了這個變數。找到報錯檔案中對應的位置,例如:

    || n == SSL_R_BLOCK_CIPHER_PAD_IS_WRONG                  /*  129 */

刪掉這一行,或者加個判斷都可以解決問題:

SHELL#ifdef SSL_R_BLOCK_CIPHER_PAD_IS_WRONG
    || n == SSL_R_BLOCK_CIPHER_PAD_IS_WRONG                  /*  129 */
#endif

其他應該沒什麼問題了。make install 之前記得先停掉 nginx 服務,不然很可能需要手動殺死之前的 nginx 程式。一切妥當後,參考前文修改ssl_ciphers 並啟動服務,搞定收工!

相關文章