我在《https链接的前几毫秒发生了什么》详细地介绍了https链接的过程,该篇经过抓包工具分析整个过程,本篇将从Chrome源码的角度着重介绍加密和解密的过程,并补充更多的细节。html
Chrome/Chromium是使用BoringSSL作为TLS层的库,它是OpenSSL的一个fork,是Chrome改于openssl以适应本身产品的特色,代码位于src/third_party/boringssl/.nginx
HTTPS链接的第一步——发送Client Hello,浏览器在Client Hello报文里面填充了使用的TLS版本、client随机数、加密列表(cipher suites)和包含了hostname的扩展。git
浏览器支持的TLS版本总共有5个:算法
#define SSL3_VERSION 0x0300 // 3.0
#define TLS1_VERSION 0x0301 // 3.1
#define TLS1_1_VERSION 0x0302 // 3.2
#define TLS1_2_VERSION 0x0303 // 3.3 (TLS 1.2)
#define TLS1_3_VERSION 0x0304 // 3.4 (TLS 1.3)复制代码
最新的版本为TLS 1.3,目前只有Chrome和Firefox支持,nginx 1.13(非稳定版本)/cloudflare支持,当前使用比较普遍的仍是TLS 1.2版本。Chrome在Client Hello里面设置的TLS为1.2:chrome
// hs为SSL_HandShake
hs->client_version =
hs->max_version >= TLS1_2_VERSION ? TLS1_2_VERSION : hs->max_version;复制代码
除了TLS外,还有支持UDP的DTLS:数组
#define DTLS1_VERSION 0xfeff
#define DTLS1_2_VERSION 0xfefd复制代码
打印出来的加密列表cipher suite总共有13个:浏览器
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA
复制代码
这是浏览器支持的加密方式,放在Client Hello里面发给服务端选择一个。上面的每个加密方式都是用的两个字节的数字编号表示,如第一个编号为0xc02B,这个是在RFC5289进行的规定。安全
这一长串的加密名字表示什么呢?以TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256为例,以下图所示:bash
密钥交换使用ECDHE算法,服务身份验证使用RSA算法,数据传输加密使用AES(+GCM),握手使用SHA256检验。服务器
换句话说,证书签名使用RSA,若是证书验证正确,那么将使用ECDHE算法进行密钥交换,保证浏览器和服务拥有相同的私有密钥,而后一方使用这把密钥进行AES数据加密,另外一方使用相同的密钥进行AES数据解密。验证证书签名合法性和密钥交换的身份确认都是使用SHA256这个哈希算法进行检验。具体过程下文展开描述。
接下来,服务端进行Server Hello的响应,包括服务端要使用TLS版本,咱们访问google.com的时候谷歌返回的版本为TLS 1.2(0x303,即十进制的771):
若是服务返回的TLS版本为1.3,那么Chrome将使用1.3版本。
Server Hello还返回一个32个字节的随机数server random,和浏览器发送的随机数client random类似,这种随机数叫作nonce,用于一次性使用,一般会带有时间戳,在后面生成master key的时候用到。
还会返回一个session id用于下次复用当前握手的信息,避免短期内重复握手。
同时返回所选择的加密方式,以下图所示:
谷歌服务器使用了上面举例的加密方式,根据观察,这也是不少服务器选择的方式,这应该是权衡了安全性和计算复杂度的一种比较好的方式。知道了加密方式以后(包括证书是使用RSA签名的),接下来等收到服务发过来的证书后,读取证书并检验证书的合法性。
证书的检验是Post一个Task给TaskScheduler线程独立检验的,其它的握手操做都是在Chrome的IO线程进行,应该是考虑到证书的检验比较复杂,因此搞成异步的。
证书的检验Chrome没有使用BoringSSL提供的API,而是本身实现的,在src/net/cert目录。这个过程是这样的,首先会检验是否在黑名单里,这个黑名单以下源码的注释:
// CloudFlare revoked all certificates issued prior to April 2nd, 2014. Thus
// all certificates where the CN ends with ".cloudflare.com" with a prior
// issuance date are rejected.
//
// The old certs had a lifetime of five years, so this can be removed April
// 2nd, 2019.复制代码
大意是说证书的通用名(一般就是证书的域名)是以.cloudflare.com结尾的证书,而且是2014.4.2前签发的,已经被取消掉了,这些证书有5年的有效期,如今仍然处于有效期,因此须要认为是无效的。
接着检验证书签名的合法性,在Mac上Chrome是调的系统函数SecTrustEvaluate作的检验。检验的过程我在《https链接的前几毫秒发生了什么》已作了详细介绍,大概来讲,先对证书进行SHA256获得一个哈希值,而后用证书的公钥对证书的签名进行解密从中取得另外一个哈希值,若是这两个哈希值相等,说明证书没有被篡改过,确实是权威机构颁发。
通常来讲,所谓数字签名,就是对所发送的内容作一个哈希,而后接收方用内容计算一个哈希值,若是这个值等于签名里的哈希,就说明内容没有被第三方篡改过。而这个签名一般是加密的,在证书里面,这个签名是使用证书的私钥进行加密,任何人均可以拿证书里提供的公钥进行解密,可是任何人没有私钥没法正确地加密,由于私钥和公钥是一一配对的,若是拿另一把私钥进行加密,再拿原先的公钥进行解密一定不是原先的内容。
因此若是签名检验正确,那么发送的内容即证书是合法的(证书里面有域名、公钥等信息)。若是这一步的检验不合法,将返回CERT_STATUS_AUTHORITY_INVALID的错误。
再接着检验证书里指定的Common Name通用名是否匹配,以下图所示:
当前访问的hostname为www.google.co.kr,而证书里面的通用名为*.google.co.kr:
www.google.co.kr包含在通配符*.google.co.kr里的,因此这个检验是经过的。若是不经过浏览器将会显示CERT_STATUS_COMMON_NAME_INVALID的错误。
关于这个通配符,有一个小细节,若是通配符是*.com这种顶级域名的那么认为是不合法的,只容许私人注册的域:(这种支持泛域名的证书会比只支持固定域名的贵)
// Do not allow wildcards for public/ICANN registry controlled domains -
// that is, prevent *.com or *.co.uk as valid presented names, but do not
// prevent *.appspot.com (a private registry controlled domain).复制代码
而后检测证书是否在公共的黑名单里面:
若是是的话返回证书被取消的状态:CERT_STATUS_REVOKED,这些黑名单列表可见blacklist。这些黑名单包括China Internet Network Information Center (CNNIC)等,由于公钥固定致使不安全的缘由,具体能够见文档附上的连接说明。
再接着检查证书是否使用了弱签名算法如SHA1/MD5:
若是的的话,返回CERT_STATUS_WEAK_SIGNATURE_ALGORITHM,由于SHA1和MD5都被认为是不安全的哈希算法,容易被碰撞攻击(如2017年2月23日,Google宣布了一个成功的SHA-1碰撞攻击,发布了两分内容不一样但SHA-1散列值相同的PDF文件做为概念证实,详见维基百科)。
紧接着检验证书是不是赛门铁克颁发的:
// Distrust Symantec-issued certificates, as described at
// https://security.googleblog.com/2017/09/chromes-plan-to-distrust-symantec.html复制代码
若是是Symantec颁发的,将会在Chrome 66版本(2018.4.17发布稳定版本)取消信任,赛门铁克是全球几大证书机构之一,旗下的根证书包括GeoTrust、VeriSign等:
为何谷歌要取消对它的信任,谷歌的blog是这么说的:
During the subsequent investigation, it was revealed that Symantec had entrusted several organizations with the ability to issue certificates without the appropriate or necessary oversight, and had been aware of security deficiencies at these organizations for some time.
大意是说通过调查,在没有被监督的状况下它随意委任几家机构颁发证书。当咱们打开某些网站,控制台提示:
The SSL certificate used to load resources from https://***.com will be distrusted in M70. Once distrusted, users will be prevented from loading these resources. See https://g.co/chrome/symantecpkicerts for more information.
就是由于它们使用了GeoTrust颁发的证书。
Chrome还会进行其它的检验,包括证书的有效期是否过长,以下源码注释:
// For certificates issued after 1 July 2012: 60 months.
// For certificates issued after 1 April 2015: 39 months.
// For certificates issued after 1 March 2018: 825 days.复制代码
还有证书自己的格式是否合法(CERT_STATUS_INVALID)等等。若是是EV加强型证书还有一些特殊的检验,有些证书须要使用在线证书状态协议(OCSP)进行检验。
检验证书的合法性和握手(HandShake)是同步进行的,由于它是运行在独立的线程。正常来讲在Server Hello以后服务发送证书给浏览器进行检验,检验成功才进行下一步的操做,可能Chrome考虑到检验比较耗时,因此弄成异步的。
无论怎么样,在Server Hello以后便进行密钥交换,密钥交换的目的是为了双方共享密钥,使用同一把密钥进行加密和解密。密钥交换的方式有两种RSA和ECDHE,RSA的方式比较简单,浏览器生成一把密钥,而后使用证书RSA的公钥进行加密发给服务端,服务再使用它的密钥进行解密获得密钥,这样就可以共享密钥了,它的缺点是攻击者虽然在发送的过程当中没法破解,可是若是它保存了全部加密的数据,等到证书到期没有被维护之类的缘由致使私钥泄露,那么它就可使用这把私钥去解密以前传送过的全部数据。而使用ECDHE是一种更安全的密钥交换算法。
ECDHE的全称叫Elliptic Curve Diffie–Hellman key Exchange椭圆曲线迪非-赫尔曼密钥交换,它是迪非-赫尔曼密钥交换的变种,使用椭圆曲线加密提升安全性。
迪非-赫尔曼密钥交换的过程是这样的:交换密钥双方甲和乙选取一个基数g,例如g = 2,而后甲和乙产生本身的密钥a和b,甲发送A = g^a和g给乙,乙收到后计算获得共享密钥K = A ^ b = g^(ab),同时把B = g ^ b发给甲,这样甲也能获得共享密钥 K = B ^ a = g ^ (ab)。以下图所示:
因为a和b一般会很大,作a或b次幂会是一个天文数字,因此须要模以一个大素数p。
窃听者可以知道g、A、B,可是不知道任何一方的密钥a或者b,因此他没法知道共享密钥K是什么。为了保证传递的信息不会被人篡改,密钥交换的数据须要使用证书的RSA进行签名。更详细的说明可参见维基百科。
经过幂方的计算值传递,较容易被破解获得双方各自的密钥,这种的安全系数不是很高,因此引入了曲线椭圆加密ECC。ECC和RSA同样也能够看成证书的加密算法,ECC和RSA的共同特色是加密步骤很简单,可是解密很是困难,RSA的困难之处在于把一个大数拆成两个素数相乘,而ECC的难点在于找到一个点的系数。不一样点是ECC的破解难度要远远大于RSA,举例来讲2048位的RSA的破解难度至关于224位的ECC,长度越短就意味着CPU计算消耗越少,速度越快。ECC在很高级别的加密场合有较普遍的应用。愈来愈多的证书使用ECC加密,如*.google.com的域名都是使用的EC加密的证书,相对于其它RSA证书2048位的公钥,EC证书只有256位:
具体来讲,所谓椭圆曲线就是指如下方程:
y^3 = x ^ 2 + ax +b
以下图所示:
上图由一个起始点P计算2P——先画一条线与P点相切,与曲线的-2P点相交,作这个点的反射与曲线的交点就是2P,而计算3P就是2P + P,以下图所示,链接P与2P,与曲线的第三个交点就是-3P,反射一下就获得3P:(任意一条直线与椭圆曲线最多只有3个交点)
依此类推,4P = 3P + P,链接3P与P与曲线的交点的反射就是4P。若是通过n次后最后连线与x轴垂直,说明全部的点已用完,总共有n(或者叫order)个点,在这个计算过程当中会取一个大数p用来作模数,当点的坐标值大于p时就模一下,起始点P(x, y)叫Generator点,再加上方程参数的两个系数ab——{a, b, order, x, y}就构成了一组椭圆曲线的基本参数。
椭圆曲线难以破解的地方在于——给定点P和Q,Q = kP (1 < k < n),想要推导出k是一件很困难的事情(一般n会很大)。
所以使用椭圆曲线加密的密钥交换过程就变成:
中间人或者窃听者可以知道Q1和Q2以及方程系数a、b和起始点P,可是它没法推导出双方各自的密钥x、y,所以它没有办法计算获得共享密钥K = xyP。而且这个破解的难度要远远大于使用幂方的方式。这个就是ECDHE。更详细的信息能够查看这个视频教程。
在实际的实现里,基本参数不是在密钥交换中传递的,而是约定的固定的曲线,在调试过程当中,咱们发现Chrome总共支持3种曲线 :
static const uint16_t kDefaultGroups[] = {
SSL_CURVE_X25519,
SSL_CURVE_SECP256R1,
SSL_CURVE_SECP384R1,
};复制代码
www.google.co.kr使用的是Curve X25519,X25519使用的曲线方程为:
y^2 = x^3 + 486662x2 + x
而*.google.com使用的是Curve secp256r1,简称为P-256,这个是在Server Key Exchange里面指定的:
它的参数组是这样的:
若是转换成十进制的话:
a = 115792089129476408780076832771566570560534619664239564663761773211729002495996
b = 99593677540221402957765480916910020772520766868399186769503856397241456836063
n = 115792089210356248762697446949407573529996955224135760342422259061068512044369复制代码
咱们看到n是一个78位的数字,因此暴力破解k(P = kG,1 < k < n)基本上是不可能的。
肯定基本方程后,双方Q1和Q2值是在Server Key Exchange和Client Key Exchange以公钥的形式进行交换。
为了确保密钥交换不会被篡改,须要进行签名,若是签名使用的是RSA的话,那么方法和验证证书有效性同样。若是证书是EC的证书,那么会使用ECDSA(ecdsa_secp256r1_sha256(0x0403,))进行签名:
具体验证的函数是使用的ECDSA_do_verify这个函数,过程说明可参考维基百科,步骤比较多,这里不深刻讨论。EC证书也有公钥和密钥,最后验证合法的标准是使用公钥解密的签名里面的r值若是等于手动计算的值,则说明正确。
接着Client Key Exchange,Chrome根据曲线类型(x25519或P-256)使用相应的参数和算法生成公钥和密钥对,如X25519的密钥是使用随机数生成的:
有了密钥再计算配套的公钥,而后把公钥保存起来发出去,并计算共享密钥,最核心的代码应该是如下几行:
// Compute the x-coordinate of |peer_key| * |private_key_|.
EC_POINT_mul(group.get(), result.get(), NULL, peer_point.get(),
private_key_.get(), bn_ctx.get()复制代码
使用对方的公钥peer_key * 本身的私钥private_key_,获得K = yQ1.
紧接着用这个共享密钥通过PRF计算获得主密钥master key。咱们能够把某次握手获得的密钥打印出来,以下所示:
链接域名:www.google.com
加密方式:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
曲线名称:SSL_CURVE_X25519
Peer Public Key (64B): 8b4364a862a7a7f19404973237079b692c1208b8ecf7828d9eae2b76e68e5012
Chrome Public Key (64B): cddd4c2d0c9d49903438a953076fb3baebd38cfa4a3b18144365b67756b4c075
Share Key (78B): 653d6e28202ff88dff92db77c91406b7992a0f15325b0192f17a317e7ff71930404dc7d4857f03
Master Key (96B): eb584819ae738a45fe9a2e60734d0ae833dfb2d63a1900ee820a36db27a3844e5b6259e2c84e06fd1474c7e1857989ad
链接域名:www.baidu.com
加密方式:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
曲线名称:SSL_CURVE_SECP256R1
Peer Public Key (142B): 04ac277ce63eb420e9e973c96cdf67e37a5956b949af4b053ca5b1b4b1f884b7f6cadbe2d64a91d43a2e280da528d6b6505bc6be10455e70aeabe569562ccc7bdebc7b5df80705
Chrome Public Key (130B): 04941ec80392f0bf13268a9791e7ee673df0a00af6e59335655b0519fbc575bfcb39eabd80f81118dca4906f776c801aee26f8f4fc195917dc94f9c324886bebc4
Share Key (78B): 52d0f6fc4ecd83107fb8c1cc7fa3f978152c0936c58d8d62d6885f7a672cf87c21212121212103
Mater Key (96B): 1e95a25c356a170c6829ec27a0216c50738b758f93606e8503a2e306796fd99db6ec49f65818a125bba6449b07648262复制代码
密钥交换以后,双方已经有了相同的密钥,而后经过发送Change Cipher Spec通知对方下一个包将会使用以前约定的方式进行加密。因为传送数据指定的是GCM加密,它是一种AEAD的加密方式,Chrome会在Change Cipher的过程当中作AEAD的配置,这个加密方式的特色是会给数据添加认证标签,若是标签对得上说明数据完整没有被破坏。
至此整个TLS握手完成,而后就是发送HTTP请求和接收响应数据了。
数据传送使用的AES加密的特色是使用一把密钥加密,再使用相同的密钥就能够解密,具体加密和解密的过程比较复杂,这里不深刻研究。不过咱们能够把加密前和加密后的数据打印出来,以下图所示:
能够看到这是一个HTTP请求,加密前的数据有572B,加密后的数据有601B,体积增加了5%。
这个请求收到如下解密后的响应数据:
还有紧接着的gzip压缩的数据。
至此整个过程就说明完了,本篇重点说了Chrome是怎么检验证书合法性的、Diff-Hellman算法是怎么样的、椭圆曲线是怎么加密,怎样使用ECDHE进行密钥交换,等等。本文不少东西没有讲得很深刻,都是点到为止,看完本篇应该对HTTPS整一个加密的过程有一个轮廓的了解,而且对一些加密算法原理有所了解。
相关阅读: