完整的握手
当客户端和服务器端初次建立TLS握手时(例如浏览器访问HTTPS网站),需要双方建立一个完整的TLS连接,该过程为了保证数据的传输具有完整性和机密性,需要做很多事情,密钥协商出会话密钥,数字签名身份验证,消息验证码MAC等,整个握手阶段比较耗时的地方是密钥协商,需要密集的CPU处理。当客户端和服务器完成一次完整的握手过程后,它们之间发送的数据就会一直有TLS保护,当某一时刻客户端和服务器断开了本次会话连接,那么它们之前连接时协商好的会话密钥(动态密钥)就没用了,消失了,因为要保证前向安全性,客户端和服务器端都不会去保存加密参数。在下一次客户端访问同一个HTTPS网站,也就是再次访问该服务器时,便要进行一次新的完整的握手阶段,这似乎没什么问题,但是当一个网站用户量越来越大后,某一时间段里大量的请求提交,占用了服务器资源,会导致很大的网络延迟。
原因很简单,上面说到,每一次客户端与服务器端完整的握手阶段要做很多事情,需要一些时间,即使是同一个客户端,关闭连接后访问同一个HTTPS网站都要进行一次新的完整握手。握手协议完成后,服务器会在自己的内存中保存本次会话的一些信息:会话标识符,证书、协商出的密码套件、使用的压缩算法(还是提一句,一般不使用- -),主密钥和会话可恢复标识,注意此时会话保持正常连接状态,直到会话被关闭后,这些信息才会消失。
会话恢复
为了解决上面的问题,TLS/SSL协议中提供了会话恢复的方式,允许客户端和服务器端在某次关闭连接后,下一次客户端访问时恢复上一次的会话连接。会话恢复有两种,一种是基于Session ID的恢复,一种是使用SessionTicket TLS扩展。
Session ID会话恢复流程
每一个会话都由一个Session ID标识符标识,这在Client Hello子消息和Server Hello子消息中我们都看到过,作用是根据这个Session ID来进行会话恢复。建立一个TLS连接后客户端和服务器端都会保存Session ID,主要由服务器保存,最后是否支持恢复会话是服务器决定,先来看看Session ID在TLS握手阶段的传递过程:
首先客户端握手阶段发送Client Hello子消息,里面的Session ID值是为空的。
服务器端接收到Client Hello,查看里面的Session ID值,如果值为空,表明双方是第一次握手,需要进行一次完整的握手阶段,然后生成一个新的Session ID,表示本次会话,并放到Server Hello子消息中回应给客户端。
客户端收到Server Hello,将Session ID存放在本地内存种,接着发送密码切换协议和Finished子消息进行校验,确保握手消息没有被篡改,告诉TLS记录层需要的密钥块已经准备好了。
客户端和服务器端各自将Session ID保存在自己本地中,客户端保存在内存里,服务器端保存在Server Cache中。
一次完整的握手阶段结束后,客户端和服务器端都保存有这个Session ID,在本次会话关闭,下一次再次访问相同的HTTPS网站时,客户端浏览器会在Client Hello子消息中附带这个Session ID值,服务器端接收到请求后,将Session ID与自己在Server Cache中保存的Session ID进行匹配,如果匹配成功,那么服务器端就会恢复上一次的TLS连接,使用之前协商过的密钥,不重新进行密钥协商,服务器收到带Session ID的Client Hello且匹配成功后,直接发送ChangeCipherSpec子协议,告诉TLS记录层将连接状态切换成可读和可写,最后发送Finished,会话恢复成功,本次握手为一次简短的握手,而不是完整的一次握手。
回退攻击
要注意的是,即使连接采用了会话恢复的方式,使用之前协商过的密码套件和主密钥,客户端和服务器端在进行简短的握手时还是要校验Finished子消息保证前面的握手消息没有被篡改的。Finished子消息的发送顺序一定是在发送ChangeCipherSpec之后,ChangeCipherSpec密码切换协议的作用是在密钥协商完成后,告诉TLS记录层需要的密钥块已经准备好,接下来TLS记录层协议可以对应用层数据进行加密了,到这一步之后才发送Finished给对方进行校验。对于客户端来说,Finished子消息包含了前面自己发送的和收到的所有握手消息,而对于服务器端来说也是,包含了客户端的Finished子消息。
如果发送的顺序出错,Finished子消息发在了ChangeCipherSpec之前,或者没有发送Finished,会出现“回退攻击”的问题,例如连接过程中,客户端在Client Hello里使用的ProtocolVersion是TLS v1.2,告诉服务器我支持的TLS协议版本最高为1.2版本,此消息在发送过程在被中间方劫持并篡改,将ProtocolVersion修改为较低的版本如TLS v1.0。服务器端在收到ChangeCipherSpec后,由于发送顺序问题(例如Finished发送在了ChangeCipherSpec前面),导致没有去校验Finished握手消息,使用了前面Client Hello中的v1.0版本协议进行加密通信,那么中间方就可以利用低版本协议的问题进行攻击。如果Finished严格发送在了ChangeCipherSpec的后面,在服务器最后接收到客户端的Finished子消息后,会校验出前面的Client Hello,发现支持的最高协议版本号是TLS v1.2,与自己前面收到的Client Hello中不同,即消息被篡改了,那么服务器可以终止握手。
Session ID分布式负载均衡问题
虽然使用Session ID进行会话恢复可以减少一些耗时的步骤,但由于Session ID主要保存在服务器Server Cache中,且没有考虑到多个服务器主机共享Server Cache,会出现一些问题,试想下面的场景:
一个HTTPS网站有多个服务器提供服务,客户端第一次与服务器建立完整的TLS握手,Session ID保存在了提供本次服务的服务器Server Cache中,会话关闭后,客户端访问同一个HTTPS网站,这次连接请求由于分布式负载均衡设定将请求重定位到了其他服务器上(提供同样的网站服务),此时新的服务器Server Cache中没有缓存与客户端匹配的Session ID,导致会话恢复无法进行。
SessionTicket
第二种会话恢复方式是使用SessionTicket TLS扩展,大致过程是这样的,客户端和服务器端建立了一次完整的握手过程后,服务器端将本次的会话数据进行加密,例如前面说到的,会话标识符、证书、密码套件和主密钥等,加密后生成一个ticket票据,并将票据通过NewSessionTicket子消息发送给客户端,由客户端来保存,下一次连接时客户端如果希望恢复上一次会话而不是重新进行握手,就将“票据”一起发送给服务器端,待服务器端解密校验无误后,进行一次简短的握手,恢复上一次会话。
恢复过程
如上图所示,如果客户端和服务器端想使用SessionTicket进行会话恢复,那么在第一次建立TLS握手时,就要发送SessionTicket TLS扩展:
客户端与服务器端第一次连接,在Client Hello子消息中使用空的SessionTicket TLS扩展。
服务器解析出Client Hello中的SessionTicket扩展后,如果选择支持,那么在自己的Server Hello中也附带一个空的SessionTicket扩展,告诉客户端我支持会话恢复。
接下来可以继续进行握手过程,待客户端发送ChangeCipherSpec和Finished子消息后,服务器端将这些加密参数加密生成票据,通过NewSessionTicket子消息发送给客户端。
客户端保存票据,下一次连接如果想通过会话恢复的方式,则可以将票据附带到Client Hello的SessionTicket TLS扩展中去,此时的扩展就不是empty了。
第一次完整的TLS握手完成后,待下一次客户端访问相同的HTTPS网站,如果想要进行会话恢复,则在Client Hello子消息中附带票据到SessionTicket TLS扩展至,服务器收到非空的扩展后,会去校验里面的票据信息,会话恢复完成简短的握手后,服务器端会发送新的NewSessionTicket子消息给客户端,里面是新的票据,为什么会话恢复后要发一个新的票据给客户端?因为在服务器签发的票据中有一个需要更新的属性,票据的有效期,如果校验到票据是过期的,那么本次会话恢复请求失败,Ticket票据的属性存在于NewSessionTicket子消息中。
NewSessionTicket子消息
上面说到,使用SessionTicket方式进行会话恢复,在第一次客户端与服务器端建立完整的TLS握手时,客户端便会发送SessionTicket TLS扩展,在最后客户端发送了ChangeCipherSpec和Finished子消息后,由服务器端生成票据,发送NewSessionTicket子消息给客户端保存,也就是说票据信息存在于该子消息中:
struct {
uint32 ticket_lifetime_hint; //有效期
opaque ticket<0..2^16-1>; //票据数据结构
} NewSessionTicket;
struct {
opaque key_name[16]; //票据加密使用的密钥
opaque iv[16]; //初始化向量
opaque encrypted_state<0..2^16-1>; //票据详细属性
opaque mac[32];
} ticket;
可以看到票据的有效期属性和一个ticket结构,标识票据信息,如使用的加密密钥,初始化向量等,encrypted_state属性十分重要,它存储的是本次会话的信息,属性有密码套件,压缩算法,主密钥和票据过期时间等:(ticket)
struct {
ProtocolVersion protocol_version;
CipherSuite cipher_suite;
compressionMethod compression_method;
opaque master_secret[48];
ClientIdentity client_identity;
uint32 timestamp;
} StatePlaintext;
就是最前面说到的,客户端和服务器端第一次建立完整TLS握手后会保存本次会话的信息,待下次会话恢复用。
————————————————
版权声明:本文为CSDN博主「大力海棠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/justinzengTM/article/details/105491809