首发于TLS与OpenSSL

TLS的握手流程

TLS是一个信道建立和信道的表达方式,向下依托于TCP,向上对应用程序服务。随着TLS的发展,DTLS也开始出现,就是同样的TLS逻辑被应用于UDP业务之上。由于客户端的网络千奇百怪,TCP需要普遍长连接的时候几乎就是不靠谱的代名词,肯定是要断线重连机制的。如果处理的不好,还很容易形成SYN攻击或者过多的耗费服务器资源。因此固定长连接的客户端业务,例如游戏场景,大都倾向于使用UDP作为连接承载,在这种情况下,DTLS就有了很大的用武之地。UDP是一种基础协议,很多人选择了UDP通常也并没有明确原因,很多是基于不想去处理TCP那么多显而易见的麻烦的事情。但是最后用了UDP之后,发现要做的事情甚至比TCP麻烦。但是UDP相对于TCP的一个巨大的优势是UDP相当于四层的IP协议,在此之上可以构造更复杂的取代TCP的协议,而不用改动内核代码。市场上确实是UDP也在大量的使用。所以DTLS也仍然有市场。

TLS信道建立的过程是一个双方沟通能力的过程。所有的连接都一定是客户端首先发起的,所以TLS握手的第一个消息肯定是客户端发送的。如果我们来设计协议,我们首先要从TLS的目的来设计整个通信的过程,以最少的通信成本来达到TLS的目的。在整个TLS握手的过程中,客户端需要发送自己支持的应用层列表,支持的密码学套件等交底信息,并且还要产生来源于客户端的随机数,完成属于客户端部分的密码学建立过程。对于服务端来说,事情也是一样的,但是有一个巨大的区别就是服务端需要发送证书给客户端。这个需求模型是最常见的CS需求模型。所以如果根据这个需求模型进行TLS握手的设计,仅仅需要一个RTT,客户端发送一次,服务端发送一次,之后就可以通信了。事实上,TLS1.3就是用这个思路设计的。只是在TLS1.2的时候,设计者活脱脱的设计了两次RTT。其中有个重要的考虑是重协商的支持,但是这个重协商很快就被证明有安全问题,所以即使在TLS1.2的时代,也是一直处于关闭状态,在TLS1.3中就直接取消了。这个协议的设计中,还可以传输客户端证书,可以传输OCSP扩展消息等一些对标准握手流程的修改,但是总体的TLS1.2的握手还是很清爽的。

访问百度的ASCII包内容


访问百度的数据包流程




我们观察访问百度的握手流程。百度的TLS集群系统从技术上讲是非常的高效的。目前也是TLS1.2的流程。从ASCII中,我们能看到在握手的开始出现了两个能辨识的字符串,一个是域名,一个是应用层协议列表。这部分是客户端发送给服务端的,这里的域名就是SNI机制。SNI机制允许同一个端口提供多个可以选择的证书,在客户端的ClientHello中会携带SNI扩展,里面就有选择的域名信息。然后服务端通过域名信息就可以选择出正确的服务证书。第二个可以识别的字符串就是支持的上层应用的列表,也就是ALPN机制,用以告诉服务端跟本客户端的通信可以选择这些不同的应用层协议。之后的服务器回复里面包含了大量的字符串,这些字符串就是证书里面的内容了。实际上ALPN的意义只对协商有意义,通常客户端可能版本比较老,但是服务器一般可以支持比较新的技术。如果服务器支持到最新的技术,完全可以直接通过收到的数据的格式直接判断是什么协议的数据,从而不需要ALPN。

我们能看到的最明显的几个内容是CA的名字,百度的企业名字和这个证书里包含的域名列表。业务层面沟通的核心信息就在这个明文的ASCII了。但是除此之外,还有很多密码学相关的不可读的内容,这些繁杂的密码学算法我们先不去关心细节。只需要知道客户端和服务端双方在使用特定的密码学算法之前都必须各自提供随机性,也就是分别生成随机数。这个随机数从理论上并不一定需要,但是从安全上就一定需要。随机,是安全世界的基石。

从双方的数据包上可以看到这是一次典型的正常的TLS握手过程。客户端一个ClientHello,交代一下自己的信息,然后服务端回一个ServerHello,根据自己的能力核对并且回应选择客户端发来的备选方案。紧跟着的是证书,这里传送的证书就是在浏览器里面看到的证书文件,在浏览器可以把这个证书用证书导出程序导出为一个文件,到处之后的文件就会发现与服务器上的证书文件是一模一样的。可见几乎就是一个明文的传输(只是存储上使用了简单的Base64编码)。这一步的证书传输是我最不能忍受的,我问了无数遍为什么不压缩?后来发现了TLS本来是有压缩功能的,但是被全世界因为安全问题给禁止了。这样就很尴尬,这么大的一个明文证书,在每一次TLS握手请求的时候都要全量传输。服务器的流量消耗是非常惊人的。虽然有各种复用方法,但是这也不能弥补初次建立TLS连接的流量损耗问题的本质。而且很关键的问题是这个压缩需要客户端和服务端同时支持,哪怕双方都同时发送一个gzip压缩过格式的证书,对于流量的节省来说也是成倍的提高(但是会有一次解压缩的代价)。理论上如此容易的事情被无限制的推迟,这也就是标准的尴尬。如果专用的客户端就完全可以自己来做这种证书的优化。如果把TLS不看成是一个标准,而是一个技术参考,那么就可以任意的修改。很多大型企业的内部通信协议都是通过这种思路修改得来的,万变不离其宗。

证书之后就是一个Server Key Exchange的消息,这个消息是用于真正的密码学运算的消息。紧接着就是Server Change Cipher消息,这个消息根据定义是不属于TLS握手。它对应的数据传输层协议的格式标志也不是TLS握手的标志,对应的,TLS握手的完整性检查也就不会作用在这个消息上。因为紧跟着这个消息的就是一个抓包不能显示的消息。这个消息叫做FInish消息,是服务端和客户端对握手至今的所有数据的二进制层面的哈希计算的结果。这一步的主要功能是一个放置MIM(Man in the Middle)的中间人攻击。任何人在握手过程的中间修改了握手的内容,都是可以被这个哈希校验检查出来的,只要不是客户端和服务端同时在协议的层面进行修改,就可以从协议的设计层面完全的组织中间人的改包攻击。但是这也不是说就是不能中间人。很多企业都是在企业出口的时候装载了TLS Termination装置。你认为你在和百度进行HTTPS通信,其实自己看证书,信任的却是公司的内部证书。这是因为公司在出口的时候完整的把内外的TLS请求进行了代理解包,然后使用自己作为中介跟内外通信。这整个过程是完全公开的,因为TLS协议本身是不可能允许他做到不留痕迹。典型的表现就是我们需要信任企业的证书,因为跟我们沟通的不是真实的对端证书代表的身份,而是企业的共同的出口身份。这样企业就可以完整的解包HTTPS流量并且进行审计了。

整个协议在设计的时候有一个比较有意思的必要消息,就是这个Change Cipher Spec消息,服务端和客户端都需要发。我们在设计集群的时候,我们知道是不能在中间修改TLS握手的数据包的,原因是Finish校验,但是这个Change Cipher Spec却并不参加Finish校验。所以我们在集群中可以相对宽松的利用这个消息,修改这个消息,甚至是代理这个消息。这个消息里面并没有什么密码学相关的内容,只是一个纯粹的通知。这个通知的意思是,我这边已经完成了密码学上下文的沟通,分对称机密已经结束了,我已经使用刚才沟通的参数成功的建立了对称加密的信道。所以在OpenSSL里,发布这个消息的同时是对应着对称加密密码学上下文的建立的。我们看到客户端在收到服务端的Key Exchange消息之后紧跟一个Change Cipher Spec,客户端也会回复一个Client Key Exchange消息,紧跟着就是客户端的Change Cipher Spec和Finish消息,这个意义与服务端是相同的。Key Exchange消息就是非对称加密协商对称加密信道的核心逻辑,用于交换密码学参数的关键步骤。


在TLS1.2的握手过程中,密码学参数的沟通是一个主要的功能。我们知道TLS1.2的时候非对称加密算法已经只剩下椭圆曲线和RSA了,这两种都可以用于密钥交换和证书签发。但是出于安全性考虑,RSA已经逐渐不再被应用到密钥交换的应用中了,但是RSA证书是仍然有应用的。RSA如果作为密钥交换,如果有人在中间进行抓包,当服务器的私钥泄漏之后,中间所有的流量都可以被解密。这是因为RSA通过作为密钥交换时,服务端生成的关键密码学参数是由服务器的私钥加密过然后传输给客户端的。如果私钥泄漏,并且中间的抓包结果被保存,就可以直接解出历史数据。但是如果使用椭圆曲线,由于椭圆曲线的中间密码学参数是从来不会在信道中传输的,相关上下文都是在客户端和服务端本地保存,所以即使被完全抓包,并且私钥泄漏,之前的TLS连接的数据也不会丢失。这就是椭圆曲线在安全性上显著优于RSA的地方。所以用在密钥交换的时候,RSA已经基本让位于ECDHE,ECDHE已经成为目前几乎唯一可以选择的密钥交换方式。在证书的方面,椭圆曲线签发的证书的大小是远小于RSA证书的,椭圆曲线的sign速度(私钥加密数据的速度)是远大于RSA的sign速度的,差别在十倍的数量级。但是RSA的verify速度是远大于椭圆曲线的veirfy速度的,差别也在十倍的数量级。由于一般在服务器端存储证书,证书在TLS握手的整个过程中都是提供一个sign的操作功能,而verify的操作是在用户端,所以大部分服务器会倾向于使用椭圆曲线证书,同时减小大小和提高sign的速度。

我们就以ECDSA为案例,分析一遍TLS握手的密码学相关流程。典型案例仍然是访问百度的抓包。





这是ClientHello中携带的支持的密码学套件的列表,可以看到客户端优先支持的都是椭圆曲线,其次才是RSA的证书,再其次才是RSA的密钥交换。一个很关键的密码学参数就是客户端的随机数。这个随机数的随机性是密码学保密性的关键,有28个字节。随后服务端在回复ServerHello的时候,选择了一个密码学套件,同时也回复了一个28个字节的随机数,这个随机数的随机性要求必须要与客户端发来的随机数没有任何关系。

接下来,服务器发送了证书,也就是certificate消息给客户端。这个证书本质上是一个公钥,一个证书必须同时有公钥和私钥,公钥是发送出去任意使用的,私钥是解密别人使用自己的公钥加密的数据的内容的。这里传输的证书一个密码学上的最重要的意义是将公钥传输给客户端,如此后续客户端的消息就可以使用公钥加密传输回服务端。



紧跟着ServerHello把自己的随机数也发送给客户端之后,Server就需要构造并且传输非对称加密算法需要的参数了。这里就是Server Key Exchange,里面的最重要的参数就是ECDHE算法需要构造的密码学参数。可以看到一个是椭圆曲线的名字:secp256r1。这个椭圆曲线不但在HTTPS中,还在比特币中有广泛的应用,所有的椭圆曲线实际上都是方程:y2=x3+ax+b 。这里面两个参数,这个椭圆曲线是一大类的曲线,对于secp256r1来说,a=0, b=7。所以对于secp256r1来说,这条曲线是y2=x3+7。a,b参数的取值并不是随便取的,必须要满足一定的条件,并且一条密码学曲线都是固定取值的。不同的a,b会得到非常不一样形状的图形,不同的图形有不同的用途,我们这里讨论的secp256r1曲线的图形如下:

这条曲线的形状如图。在密码学上,所有的椭圆曲线都是使用的有限域版本GF(p),所以p也是一个椭圆曲线的一个值。对于secp256k1来说,他的p=2^256-2^32-2^9-2^8-2^7-2^6-2^4-1,椭圆曲线的方程是y2=x3+7 mod p。

椭圆曲线之所以被选择出来是因为他有众多非常有意思的特性,例如在椭圆曲线上拉一条直线,经过三个点,那三个点的和是0(那就看曲线怎么定义和这个操作了,这里是数论里的阿贝尔群)。本质上,使用椭圆曲线是使用了椭圆曲线的性质来得到一个数学运算系统,最终在参与密码学计算的是数论系统,与椭圆曲线就没有太大关系了,但是仍然可以用曲线来理解,因为数论运算是基于椭圆曲线构造的。


当这个椭圆曲线算法用于非对称加密的密钥交换的时候,我们知道两个人都能看到对方的公钥,并且都知道自己的私钥。椭圆曲线算法在使用的时候能够看到与RSA的一个最大的不同,就是RSA需要一个私钥,椭圆曲线并不需要。对于椭圆曲线做密钥交换,每一次通信的私钥和公钥都是临时生成的,这也是椭圆曲线比RSA安全性高的原因。因为RSA一旦私钥泄漏,历史的加密数据都能破解,而椭圆曲线不能。每一次通信都用完全不同的私钥公钥对进行信道协商。所以对于椭圆曲线来说,在每一次通信的时候都会首先生成这个私钥和公钥,生成私钥的方法就是在椭圆曲线上取一个点,根据上面说过的三点和等于0的特性,让两个点重合,重合之后,这条曲线就是椭圆的切线,与椭圆相交于两个点。非切点的那个点就是私钥,切点取反再多切线得到一个新的切点,如果多次取反做切点就得到了公钥点。Q=NG,G是私钥点,N是做切线的次数,Q是公钥。已知Q,算不出G,就是椭圆曲线生成公钥私钥的原理了。这里面对于椭圆曲线来说,N是一个公开的常数,双方都知道并且相同,也就是说,这个数学难题是已知了一个点,求N次反向的求切线运算得到的那个点。这个点是计算难度上得不出来的。也就是说已知一点曲线上的切点,得到切线的难度比已知一个随意点,对曲线做切线的计算度复杂很多,多到多次计算就是计算不可能问题。

当椭圆曲线生成的公钥和私钥用于交换的时候,因为双方已经互换了公钥,这个计算过程就变成了双方同时用自己的私钥乘以对方的公钥。椭圆曲线的神奇特性保证了得到的结果是一样的,也就是说是得到了同一个点。这个点就是协商得到的对称加密的密钥。

这个其实也是很好理解。Q1*G2=Q1*(Q2*N)=Q1*N*Q2=G1*Q2=对称加密的密钥

所以我们看到在Server Key Exchange中的pubkey域就是这里生成的公钥,客户端随后也会回复一个动态生成的公钥,双方都发送了公钥就相当于互相传递了对称加密的密钥,并且外人是无法破解的。我们可以看到对于椭圆曲线的握手(ECDHE),在Server Key Exchange一步还有一个哈希结果的传递,这一步在RSA握手中是没有的(整个Server Key Exchange步骤在RSA协商中都是没有的)。因为双方有个本质的区别是ECDHE的握手过程是没有私钥参与的,使用的所谓的私钥是动态生成的临时随机数。而RSA的握手方式是一定要配置私钥的,这个私钥就是证书的私钥。这也从另一方面说明了RSA的握手方式只能用RSA的证书。RSA握手的信道建立的过程中包含了证书验证的过程,而ECDHE的验证过程需要在这一步单独提供。从抓包的结果能看到服务器使用的rsa_pkcs1_sha256的签名方法给出了一个签名,这就是普通的证书签名,就是用证书私钥加密的结果从公钥可以解密。客户端收到这个签名之后用公钥解密就能得到是否是预先共享的内容,例如客户端提前发送的随机数,从而就能做到服务端验证工作。

随后客户端也会回复一样的Client Key Exchange,还有双方互换的非握手流程的Change CIpher数据包。这个Change Cipher是不携带任何数据的,也不参与最后的哈希完整性计算。只是一个宣告信道开始的流程。最后双方都会发送一个已经用协商好的对称加密密钥加密过的握手数据包,就是TLS Finish。这个TLS Finish数据包保证了整个握手过程中,数据不能被篡改。任何中间人改动了中间的某一个数据,都逃脱不掉最后的哈希运算的检查。因为这个哈希运算是完整的计算从握手开始到结束的所有内容的。

编辑于 2018-06-03

文章被以下专栏收录