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算法而采用其它非对称算法从而解决问题。