前不久,一位朋友在我部落格評論中,問到:類似於 Google 那樣電腦訪問使用 AES,手機訪問使用 CHACHA20 的演算法是怎麼實現的(詳情)。最近我研究了一下這個問題,現在我的部落格也支援這個特性了。今天抽空介紹一下我的實現步驟,供喜歡折騰的朋友們參考。
對稱內容加密
我們知道,每個 TLS 會話都是在握手階段通過非對稱加密得出對稱加密金鑰,而本次會話雙方一直會用這個金鑰進行流量的對稱加密。這樣做是出於效能考慮,畢竟對稱加密速度要快得多,更適合全流量使用。
對稱加密演算法有流式、分組兩種。RC4 就是一個常見的流式加密演算法,不過已被證實不再安全,應該停止使用。Google 推出了一種名為 ChaCha20-Poly1305 的流式加密新演算法,已經內建於各大平臺的 Chrome 之中。ChaCha20 除了更安全,還針對 ARM 做了優化,在移動裝置上使用速度更快、更省電。以下是它與 AES-GCM 在加密速度上的對比(via):
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,via):
1 2 3 4 5 6 7 8 9 |
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:
1 |
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 握手中可獲得):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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 (0x0039) 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 (0x0033) Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) 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 (0x0033) Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039) 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 (0x0035) 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,這裡我直接使用最新版:
1 2 3 4 |
wget http://nginx.org/download/nginx-1.9.6.tar.gz tar xzf nginx-1.9.6.tar.gz git clone https://boringssl.googlesource.com/boringssl |
現在,當前目錄下應該有這兩個子目錄:
1 2 |
boringssl/ nginx-1.9.6/ |
確認無誤後,還要做一些準備工作:
1 2 3 4 5 6 7 8 |
# 安裝編譯 BoringSSL 所需的 Golang sudo apt-get install golang # 安裝編譯 Nginx 所需的工具和依賴庫 sudo apt-get install build-essential libpcre3 libpcre3-dev zlib1g-dev # 忽略編譯過程中的 Warning(不加這個,編到一半會因為 Warning 太多無法繼續) export CFLAGS="-Wno-error" |
編譯 BoringSSL:
1 2 3 4 5 6 7 8 9 10 11 |
# 進入 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 cd ../ |
現在可以編譯 Nginx 了:
1 2 3 4 5 6 7 8 |
# 進入 Nginx 原始碼根目錄 cd nginx-1.9.6 # 指定使用 BoringSSL 作為 SSL 庫 ./configure --with-openssl=../boringssl --with-http_v2_module --with-http_ssl_module # 修改時間,避免 Nginx 再次編譯 BoringSSL touch ../boringssl/.openssl/include/openssl/ssl.h |
touch 後可以開始 make 和 make install 了。這期間可能還會遇到這樣一個報錯:
1 |
'SSL_R_BLOCK_CIPHER_PAD_IS_WRONG' undeclared |
這是因為 BoringSSL 刪掉了這個變數。找到報錯檔案 src/event/ngx_event_openssl.c 中對應的位置:
1 |
|| n == SSL_R_BLOCK_CIPHER_PAD_IS_WRONG /* 129 */ |
刪掉這一行,或者加個判斷都可以解決問題:
1 2 3 |
#ifdef SSL_R_BLOCK_CIPHER_PAD_IS_WRONG || n == SSL_R_BLOCK_CIPHER_PAD_IS_WRONG /* 129 */ #endif |
其他應該沒什麼問題了。make install 之前記得先停掉 nginx 服務,不然很可能需要手動殺死之前的 nginx 程式。一切妥當後,參考前文修改 ssl_ciphers 並啟動服務,搞定收工!
本文連結:https://imququ.com/post/optimize-ssl-ciphers-with-boringssl.html,參與評論。
–EOF–