C/S boringSSL那點事
- 王芳
- 原創
1.背景
Google I/O開發者大會在2015年5月28日和29日於舊金山召開[1]。Google在大會上公布了一些新技術和新產品,其中包含google play、TV等等,其中最為萬眾矚目的是新系統Android M(Android 6.0產品api level 23,全稱Android Marshmallow),文獻[2]中展示了Android M的新特性。當日Google正式發布了Nexus 5X和Nexus 6P兩款手機,LG和華為代工的兩款Nexus手機都將搭載最新的原生安卓6.0系統[3]。
本文所碰到的問題對於移動端Android M SSL方面的研究,源於投資贏家1.0所有券商的項目,甚至對在Android M上使用ssl建立數據加密都是有幫助的。

圖1
如圖1所示,隨意選擇一個券商的mac地址連接出現的投資贏家的首頁,當用戶點擊交易時需要用戶進行交易登錄操作。眾所周知,用戶賬號密碼登錄是安全級別極高的操作,所以需要采用SSL或TLS協議保駕護航以防止用戶請求的數據包被黑客捕獲並且解析。
搭載Android M系統的Nexus手機上在交易登錄時發生了SSL握手失敗的異常,而除了Android M系統之前的所有系統都是毫無問題的(投資贏家上線以來3年之久),華為方面也派出員工拿着未上市的Android M系統的手機到公司聯調查找問題。
問題總結:最新發布的Android M系統發生ssl握手失敗問題,而Android其它系統都沒有該問題。
2.思考與解決
2.1調試代碼查日志
關鍵代碼:
調試到startHandshake()時報錯,且日志信息為:
從日志信息分析並與華為相關工程師了解到,盡管華為方面對谷歌官方提供的Android M源碼加入了自己看門狗等一些新需求,但並沒有更改核心代碼。問題的核心集中鎖定ssl3_get_server_key_exchange:BAD_DH_P_LENGTH(ex
ternal/boringssl/src/ssl/s3_clnt.c:1193。顯然單單看Android的代碼根本無法下手。
2.2 SSL相關知識(非常重要)
如果讀者有興趣可參考《tcp/ip協議詳解》、《密碼學與網絡安全》等書籍研究相關知識,本文關於這些方面內容的介紹只與解決本文問題相關,涉及而不深入。
2.2.1 SSL發展歷史:
1994年,NetScape公司設計了SSL協議(Secure Sockets Layer)的1.0版,但是未發布。
1995年,NetScape公司發布SSL 2.0版,很快發現有嚴重漏洞。
1996年,SSL 3.0版問世,得到大規模應用。
1999年,互聯網標准化組織ISOC接替NetScape公司,發布了SSL的升級版TLS 1.0版。
2006年和2008年,TLS進行了兩次升級,分別為TLS 1.1版和TLS 1.2版。最新的變動是2011年TLS 1.2的修訂版。
目前,應用最廣泛的是TLS 1.0,接下來是SSL 3.0。但是,主流瀏覽器都已經實現了TLS 1.2的支持。TLS 1.0通常被標示為SSL 3.1,TLS 1.1為SSL 3.2,TLS 1.2為SSL 3.3。
2.2.2基本概念
密碼學(cryptography):目的是通過將信息編碼使其不可讀,從而達到安全。
明文(plain text):發送人、接受人和任何訪問消息的人都能理解的消息。
密文(cipher text):明文消息經過某種編碼后,得到密文消息。
加密(encryption):將明文消息變成密文消息。
解密(decryption):將密文消息變成明文消息。
算法:取一個輸入文本,產生一個輸出文本。
加密算法:發送方進行加密的算法。
解密算法:接收方進行解密的算法。
密鑰(key):只有發送方和接收方理解的消息
對稱密鑰加密(Symmetric Key Cryptography):加密與解密使用相同密鑰。
非對稱密鑰加密(Asymmetric Key Cryptography):加密與解密使用不同密鑰。
2.2.3基本運行過程[4]
SSL/TLS協議的基本思路是采用公鑰加密法,也就是說,客戶端先向服務器端索要公鑰,然后用公鑰加密信息,服務器收到密文后,用自己的私鑰解密。主要分為以下三步:
a.客戶端向服務器端索要並驗證公鑰。
b.雙方協商生成"對話密鑰"。
c.雙方采用"對話密鑰"進行加密通信。
2.2.4握手階段的詳細過程
其中2.2.3中a、b兩步又稱為握手階段。
如上圖分為四步:
a.客戶端(通常是瀏覽器)先向服務器發出加密通信的請求,並且提供如下信息:
(1) 支持的協議版本,比如TLS 1.0版。
(2) 一個客戶端生成的隨機數,稍后用於生成"對話密鑰"。
(3) 一個確定會話的會話ID。
(4)一個客戶端可以支持的密碼套件列表。
b.服務器發出回應,其中包含如下信息:
(1) 確認使用的加密通信協議版本,比如TLS 1.0版本。如果瀏覽器與服務器支持的版本不一致,服務器關閉加密通信。
(2) 一個服務器生成的隨機數,稍后用於生成"對話密鑰"。
(3) 確認使用的加密方法,比如RSA公鑰加密。
(4) 服務器證書。
c.客戶端收到服務器回應以后,首先驗證證書。如無誤則向服務器發送以下信息:
(1) 一個隨機數。該隨機數用服務器公鑰加密,防止被竊聽。
(2) 編碼改變通知,表示隨后的信息都將用雙方商定的加密方法和密鑰發送。
(3) 客戶端握手結束通知,表示客戶端的握手階段已經結束。這一項同時也是前面發送的所有內容的hash值,用來供服務器校驗。
d.服務器收到客戶端第三個隨機數pre-master key之后,計算生成本次會話所用的"會話密鑰"。然后,向客戶端最后發送下面信息:
(1)編碼改變通知,表示隨后的信息都將用雙方商定的加密方法和密鑰發送。
(2)服務器握手結束通知,表示服務器的握手階段已經結束。這一項同時也是前面發送的所有內容的hash值,用來供客戶端校驗。
2.2.5常見加解密算法
本文知識粗略地介紹加解密算法的名稱,如果想了解這些算法原理可以研究下密碼學書籍。
對稱加密算法指加密和解密使用相同密鑰的加密算法。常見的對稱加密算法:DES、3DES、DESX、Blowfish、IDEA、RC4、RC5、RC6和AES
非對稱加密算法指加密和解密使用不同密鑰的加密算法,也稱為公私鑰加密。常見的非對稱加密算法:RSA、ECC(移動設備用)、Diffie-Hellman(DH算法)、El Gamal、DSA(數字簽名用)。
Hash散列算法特別的地方在於它是一種單向算法,用戶可以通過Hash算法對目標信息生成一段特定長度的唯一的Hash值,卻不能通過這個Hash值重新獲得目標信息。因此Hash算法常用在不可還原的密碼存儲、信息完整性校驗等。常見的Hash算法:MD2、MD4、MD5、HAVAL、SHA、SHA-1、HMAC、HMAC-MD5、HMAC-SHA1。
2.2.6 boringSSL
在此之前先介紹下openSSL,OpenSSL 是一個強大的安全套接字層密碼庫,囊括主要的密碼算法、常用的密鑰和證書封裝管理功能及SSL協議,並提供豐富的應用程序供測試或其它目的使用。但是openSSL被曝出嚴重的安全漏洞,最有名的是Heartbleed(心臟流血)漏洞,同時openssl代碼非常糟糕。參考文獻[2],Google方面對原有openSSL提交大量的漏洞並進行改造,建立分支命名為boringSSL,當然Android M系統 加上了boringSSL的一些更改。
2.2.7 cipher加密套件[5]
一個加密套件指明了SSL握手階段和通信階段所應該采用的各種算法。這些算法包括:認證算法、密鑰交換算法、對稱算法和摘要算法等。在握手初始化的時候,雙方都會導入各自所認可的多種加密套件。在握手階段,由服務端選擇其中的一種加密套件。
OpenSSL的ciphers命令可以列出所有的加密套件。[6]中是谷歌boringSSL的開源源碼,openssl的加密套件在s3_lib.c的ssl3_ciphers數組中定義。
密碼套件格式:每個套件都以“SSL”或“TLS”開頭,緊跟着的是密鑰交換算法。用“With”這個詞把密鑰交換算法、加密算法、散列算法分開,例如:SSL_DHE_RSA_WITH_DES_CBC_SHA, 表示把DHE_RSA(帶有RSA數字簽名的暫時Diffie-HellMan)定義為密鑰交換算法;把DES_CBC定義為加密算法;把SHA定義為散列算法。如果服務端只設置了一種加密套件,那么客戶端要么接受要么返回錯誤。加密套件的選擇是由服務端做出的。
2.3根據log日志與SSL知識分析
ssl3_get_server_key_exchange:BAD_DH_P_LENGTH (external/boringssl/src/ssl/s3_clnt.c以上日志信息中,Android M之前的系統ssl方面報錯都是在openssl文件中,其次時s3_cInt.c文件為C語言代碼文件並不是java代碼文件。SSL方面為Android系統底層服務,Android系統底層內核是基於linux系統由C語言編寫的。故去尋找谷歌開源的BoringSSL源碼參考[6]源碼地址,果然發現在s3_cInt.c文件中有以下關鍵代碼:
if (DH_num_bits(dh) < 1024) {
OPENSSL_PUT_ERROR(SSL, ssl3_get_server_key_exchange,
SSL_R_BAD_DH_P_LENGTH);
goto err;
}
推測為DH算法產生的dh位數太少所產生的。DH秘鑰交互算法原理如下:
(1)UserA與UserB確定兩個大素數n和g,這兩個數不用保密
(2)UserA選擇另一個大隨機數x,並計算A如下:A=gx mod n
(3)UserA將A發給UserB
(4)UserB選擇另一個大隨機數y,並計算B如下:B=gy mod n
(5)UserB將B發給UserA
(6)計算秘密密鑰K1如下:K1=Bx mod n
(7)計算秘密密鑰K2如下:K2=Ay mod n
K1=K2,因此UserA和UserB可以用其進行加解密。根據它的原理隨機數的位數的產生是由底層BoringSSL庫運算產生控制的,由於Android M系統對DH算法的位數做了更長的限制所以導致Android M系統上投資贏家項目出現SSL_R_BAD_DH_P_LENGTH錯誤而之前的系統都無誤。究其原因:TLS協議中,當客戶端發送可用的cipher suites給服務器之后。服務器在ServerKeyExchange階段時會將選定好的包含DH算法套件的參數發給客戶端,服務器在事先生成好參數就應該符合長度要求並發送給客戶端,此階段在客戶端JAVA層對此檢驗失敗。Google方面在android M系統對DH參數做了更長的限制[7]。
3.解決方案
3.1 DH算法參數配置
既然Google對DH算法的參數做了限制,服務器應對服務器的DH密鑰參數做配置,使符合安全長度要求,建議用2048長度。根據服務器配置有以下思路:
3.1.1 加強DH算法參數
首先需要重新生成這個參數。在開發環境生成DH參數的方法有多種,以OPENSSL為例可參考:$ openssl dhparam -out dhparams.pem 2048。當生成參數后,服務器把生成的參數保存起來(比如寫到配置文件之類),在運行TLS/SSL協議時,在ServerKeyExchange階段將自動讀取這個參數發出。例如像Apache Tomcat容器中可在server.xml中配置Connector標簽的ciphers屬性,具體參考[8]。文獻中列出多種主流web容器的配置方法配置DH算法參數的辦法。
3.1.2升級JDK 8
參考[9], legacy: The JSSE Oracle provider preserves the legacy behavior ( using ephemeral DH keys of sizes 512 bits and 768 bits) of JDK 7 and earlier releases. JDK 7並使用512或者768位,而根據描述Diffie-Hellman (DH) keys of sizes less than 1024 bits have been deprecated because of their insufficient strength. In JDK 8, you can customize the ephemeral DH key size with the system property jdk.tls.ephemeralDHKeySize. JDK8默認支持了1024,所以可以升級JDK 8,而JDK 6、7並不支持1024長度[10]。
3.2摒棄DH算法
根據3中描述,可以去除非對稱算法DH並且采用其它非對稱算法。通過調試查看Android支持的cipher suite如下圖所示:
3.2.1客戶端去除DH算法
在SSL握手階段,客戶端發送clientHello並且發送客戶端所支持的cipher suite,然后服務器來確定采用哪種cipher suite。在客戶端就去除DH算法,那么與服務器協商的cipher suites則不會包含DH算法。
3.2.2服務端去除DH算法
Cipher suites組件的選擇由服務器決定。在服務器端去除其中帶有DH、DHE、ECDHE等與DH衍生的算法關鍵代碼如下:
本文考慮到方案特殊的情況采用服務器摒棄DH算法方案,並且經過多次試驗在Android M系統上投資贏家1.0所支持的券商項目都能正常運行。
4.總結
在投資贏家1.0所支持的幾十家券商項目中,由最新發布的Android M系統發生SSL握手失敗問題,而Android其它系統都沒有該問題。通過查看日志信息如下:ssl3_get_server_key_exchange:BAD_DH_P_LENGTH (external/boringssl/src/ssl/s3_clnt.c。本文介紹了與此相關的SSL握手方面的重要概念,並且查看谷歌開源的boringSSL源碼確定DH算法的位數問題(谷歌方面做了更嚴格的限制),並且給出以上幾種解決方案。本文采用在服務器端摒棄DH算法而采用其它非對稱算法從而解決問題。