这篇blog是关于tls1.2协议handshake部分的介绍和分析,主要内容来自于对rfc5246的梳理。
TLS握手协议作用主要有以下几点:
一个完整的TLS握手过程如下图所示:
第一轮握手包括以下几个步骤:
第二轮握手则包括下面这些:
常见的扩展有:
证书不在tls协议范围内,更多内容可以参考everything-pki
当server需要认证客户端身份时,server需要发送这个消息,具体字段其实也不太重要
第一轮握手以Server发送Server Hello done作为结束,接下来由client继续表演。该消息没有消息体。
当server要求认证客户端身份时,client端首先需要发送client证书过来,和Server Certificate消息类似。
这个消息的内容取决于协商的密钥交换算法
master_secret = PRF(pre_master_secret, "master secret", ClientHello.random + ServerHello.random) [0..47];
注意这里master secret的计算依赖pre_master_secret、client random和server random,
这个消息作用前文也解释了,当Server端要求Client进行身份认证时,Client端除了发送Certificate之外,这时还会发送一个CertificateVerify消息,里面带有对目前连接上所有消息的签名,以表明自己拥有证书。需要注意的是该消息的发送时机,是在client key exchange后,因为签名消息里也要包含client key exchange消息。
ChangeCipherSpec 作用是为了告知对端接下来的消息是加密后的密文了。其本身并不属于handshake消息类型,在tls1.3中也被移除了。
一旦一端完成了密钥交换,在发完[ChangeCipherSpec]消息后,就会发送Finished消息,作用了为了让对端验证身份认证和密钥交换过程成功进行,其实就是对所有handshake消息采用master_secret计算一个MAC,然后和application data一样使用client write key加密发给对端。
需要特别说明的时从Finished消息开始,消息内容才是加密后的内容,finished消息之前的所有消息对中间人都是可见的,这就给MITM以机会篡改握手消息进行降级攻击,使得client和server协商出一个并非本意的弱密钥,MITM破解后只要对Finished消息的hash进行重新计算,就可以在client和server无感知的情况下控制整个会话,这也是FREAK attack的攻击原理。
根据cloudflare的数据,60%的tls连接是全新连接,剩下40%连接都是从之前连接恢复出的连接,意味有这40%的网页访问请求行为在短时间内还好重复发生。Session resumption机制就是tls用于从之前创建过的连接恢复出连接的机制,从避免身份认证、密钥交换等一些列代价高的操作。tls1.2中有两种用于恢复连接的机制。
session ID机制要求server端生成并维护一个session id到session状态信息的cache,并在server hello信息里将session id返回给客户端,客户端下次握手时在client hello里带上这个session id。server那到这个session id后从cache中将上次session的状态恢复出来,直接完成握手。因此一个带session id的握手将会是如下图所示:
可以看出session id可以使得握手过程减少一个rtt,代价就是这个session cache,为了解决这个问题tls在RFC5077中引入session ticket机制
思想很简单,把存储session信息的代价分散到客户端就好了,从而避免在server端维护session cache的代价。放客户端肯定不能明文存储,所以就加密后再给客户端。
具体来说: 第一次进行tls握手时候,支持session ticket的客户端在client hello里带上空的session ticket 扩展,server端回复的server hello里也会带上空的session ticket扩展,双方都知道彼此认识这个扩展。 然后server端完成密钥交换后,发送一个NewSessionTicket握手消息给client端,里面包含server端加密后的session信息(ticket)以及ticket的过期时间。
client端收到NewSessionTicket后将当前session信息和server给的session ticket相关联并保存下来。后续想要恢复session时,client在client hello的session ticket扩展中带上之前保存的ticket,server收到后如果能够正确解析ticket,则可以从中恢复出之前的server端session信息,至此两端都恢复出之前的session状态,完成握手。
当然ticket完全可能被攻击者所截获,攻击者如果拿着client的ticket去尝试和server恢复session时由于没有master_secret,因此失败在Finished消息阶段。
关于这个session状态,协议中并没有强制规定包括哪些,但是协议给了建议要保存的一些状态,如下所示,其中最主要的就是master_secret和timestamp。
struct {
ProtocolVersion protocol_version;
CipherSuite cipher_suite;
CompressionMethod compression_method;
opaque master_secret[48];
ClientIdentity client_identity;
uint32 timestamp;
} StatePlaintext;
由于ticket对client是透明的,因此协议并也没有强制规定ticket应该怎么构成,只做了下格式和密码学保护上的实现建议,如下:
struct {
opaque key_name[16];
opaque iv[16];
opaque encrypted_state<0..2^16-1>;
opaque mac[32];
} ticket;
首先状态信息需要安全加密并用mac保护,因此涉及到两个key,一个key用来AES加密state,一个key用来生成mac,key_name用来告诉server 这两个key是什么,协议还建议server启动的时候去生成随机的key_name以及对应的keys。
前面有提到tls的版本协商机制,这里再展开多说一点:设计上很简单,举个例子,client支持tls1.2,server只支持tls1.0,server应该将告诉client用tls1.0通信,非常简单的版本协商机制。问题就出在一些不合作的server,发现不支持client的版本后,直接给断掉连接了,让浏览器怎么办?在2015/16年之前,浏览器为了兼容这些不老实的server纷纷采用的版本fallback机制,即在server中止握手后尝试一个更低的版本,直至连接建立成功,这么做一方面损坏了用户体验(重试好几次可能),另一方面给版本回退攻击开了口子,攻击者可以阻断连接,给client造成server只支持低版本tls的假象,然后利用低版本tls的漏洞破解tls session。
这个版本协商机制要求server实现要能够正确的做到后向兼容,应对未来的情况,通常来说是很难搞对,因为缺少反馈机制,搞错了在当时也没人知道,只有等到几年后协议升级, 新的client出现问题才暴露,这时候错误的server已经在市场是广泛存在了,升级还需要花个几年才能纠正错误行为。因此tls这个简单的版本协商机制,看起来设计上简单,其实是一种典型的protocol design anti-pattern。
如果一种机制虽然设计上是灵活的,但是实际上从来没变过,那么肯定会有人会认为他是不变的,而网络协议能够正确work涉及整个链路上包括client、中间件、server等所有参与方的协同工作,一旦有环节这么干了,就导致了protocol ossification,好的设计要能规避掉实现方出错的可能。我们会在tls1.3内容里再次看到tls1.2设计导致的协议骨化问题,以及tls1.3的解决方案。
关于tls后向兼容的话题可以参考cloudflare的这篇blog