上一篇:《基于 TLS 1.3的微信安全通信协议 mmtls 介绍(上)》
经过上面的 Handshake 过程,此时 Client 和 Server 已经协商出了一致的对称加密密钥 pre_master_key
,那么接下来就可以直接用这个 pre_master_key
作为密钥,选择一种对称加密算法(如常用的 AES-CBC)加密业务数据,将密文发送给 Server。是否真的就这么简单呢?实际上如果真的按这个过程进行加密通信是有很多安全漏洞的。
"加密并不是认证"在密码学中是一个简单的共识,但对于我们很多程序员来说,并不知道这句话的意义。加密是隐藏信息,使得在没有正确密钥的情况下,信息变得难以读懂,加密算法提供保密性,上面所述的 AES-CBC 这种算法只是提供保密性,即防止信息被窃听。在信息安全领域,消息认证(message authentication)或数据源认证(data origin authentication)表示数据在传输过程中没有被修改(完整性),并且接收消息的实体能够验证消息的源(端点认证)。AES-CBC 这种加密算法只提供保密性,但是并不提供完整性。这似乎有点违反直觉,好像对端发给我一段密文,如果我能够解密成功,通过过程就是安全的,实则不然,就拿 AES-CBC 加密一段数据,如果中间人篡改部分密文,只要不篡改 padding 部分,大部分时候仍旧能够正常解密,只是得到的明文和原始明文不一样。现实中也有对消息追加 CRC 校验来解决密文被篡改问题的,实际上经过精心构造,即使有 CRC 校验仍然能够被绕过。本质的原因是在于进行加密安全通信过程,只使用了提供保密性的对称加密组件,没有使用提供消息完整性的密码学组件。
因此只要在用对称加密算法加密明文后,再用消息认证码算法对密文计算一次消息认证码,将密文和消息认证码发送给 Server,Server 进行验证,这样就能保证安全性了。实际上加密过程和计算消息认证码的过程,到底应该如何组合,谁先谁后,在密码学发展的历史上先后出现了三种组合方式:(1) Encrypt-and-MAC (2) MAC-then-Encrypt (3) Encrypt-then-MAC,根据最新密码学动态,目前学术界已经一致同意 Encrypt-then-MAC 是最安全的,也就是先加密后算消息认证码的方式。鉴于这个陷阱如此险恶,因此就有人提出将 Encrypt 和 MAC 直接集成在一个算法内部,让有经验的密码专家在算法内部解决安全问题,不让算法使用者选择,这就是这就是 AEAD(Authenticated-Encryption With Addtional data)类的算法。TLS1.3 彻底禁止 AEAD 以外的其他算法。mmtls 经过综合考虑,选择了使用 AES-GCM 这种 AEAD 类算法,作为协议的认证加密组件,而且 AES-GCM 也是 TLS1.3 要求必须实现的算法。
TLS1.3 明确要求通信双方使用的对称加密 Key 不能完全一样,否则在一些对称加密算法下会被完全攻破,即使是使用 AES-GCM 算法,如果通信双方使用完全相同的加密密钥进行通信,在使用的时候也要小心翼翼的保证一些额外条件,否则会泄露部分明文信息。另外,AES 算法的初始化向量(IV)如何构造也是很有讲究的,一旦用错就会有安全漏洞。也就是说,对于 handshake 协议协商得到的 pre_master_secret
不能直接作为双方进行对称加密密钥,需要经过某种扩展变换,得到六个对称加密参数:
Client Write MAC Key (用于 Client 算消息认证码,以及 Server 验证消息认证码)
Server Write MAC Key (用于 Server 算消息认证码,以及 Client 验证消息认证码)
Client Write Encryption Key(用做 Client 做加密,以及 Server 解密)
Server Write Encryption Key(用做 Server 做加密,以及 Client 解密)
Client Write IV (Client 加密时使用的初始化向量)
Server Write IV (Server 加密时使用的初始化向量)
当然,使用 AES-GCM 作为对称加密组件,MAC Key 和 Encryption Key 只需要一个就可以了。
握手生成的 pre_master_secret
只有 48 个字节,上述几个加密参数的长度加起来肯定就超过 48 字节了,所以需要一个函数来把 48 字节延长到需要的长度,在密码学中专门有一类算法承担密钥扩展的功能,称为密钥衍生函数(Key Derivation Function)。TLS1.3 使用的 HKDF 做密钥扩展,mmtls 也是选用的 HKDF 做密钥扩展。
在前文中,我用 pre_master_secret
代表握手协商得到的对称密钥,在 TLS1.2 之前确实叫这个名字,但是在 TLS1.3 中由于需要支持 0-RTT 握手,协商出来的对称密钥可能会有两个,分别称为 Static Secret(SS) 和 Ephemeral Secret(ES)。从 TLS1.3 文档中截取一张图进行说明一下:
上图中 Key Exchange 就是代表握手的方式,在 1-RTT ECDHE 握手方式下,
ES=SS = ECDH_Compute_Key(svr_pub_key, cli_pri_key);
在 0-RTT ECDH 下,
SS=ECDH_Compute_Key(static_svr_pub_key, cli_pri_key),
ES=ECDH_Compute_Key(svr_pub_Key, cli_pri_Key);
在 0-RTT/1-RTT PSK 握手下,
ES=SS=pre-shared key;
在 0-RTT PSK-ECDHE 握手下,
SS=pre-shared key,
ES=ECDH_Compute_Key(svr_pub_key, cli_pri_key);
前面说过 mmtls 使用的密钥扩展组件为 HKDF,该组件定义了两个函数来保证扩展出来的密钥具有伪随机性、唯一性、不能逆推原密钥、可扩展任意长度密钥。两个函数分别是:
HKDF-Extract( salt, initial-keying-material )
该函数的作用是对 initial-keying-material 进行处理,保证它的熵均匀分别,足够的伪随机。
HKDF-Expand( pseudorandom key, info, out_key_length )
参数 pseudorandom key 是已经足够伪随机的密钥扩展材料,HKDF-Extract 的返回值可以作为 pseudorandom key
,info
用来区分扩展出来的 Key 是做什么用,out_key_length
表示希望扩展输出的 key 有多长。mmtls 最终使用的密钥是有 HKDF-Expand 扩展出来的。mmtls 把 info 参数分为:length,label,handshake_hash
。其中 length 等于 out_key_length
,label
是标记密钥用途的固定字符串,handshake_hash
表示握手消息的 hash 值,这样扩展出来的密钥保证连接内唯一。
TLS1.3 草案中定义的密钥扩展方式比较繁琐,如上图所示。为了得到最终认证加密的对称密钥,需要做 3 次 HDKF-Extract 和 4 次 HKDF-Expand 操作,实际测试发现,这种密钥扩展方式对性能影响是很大的,尤其在 PSK 握手情况(PSK 握手没有非对称运算)这种密钥扩展方式成为性能瓶颈。TLS1.3 之所以把密钥扩展搞这么复杂,本质上还是因为 TLS1.3 是一个通用的协议框架,具体的协商算法是可以选择的,在有些协商算法下,协商出来的 pre_master_key
(SS 和 ES)就不满足某些特性(如随机性不够),因此为了保证无论选择什么协商算法,用它来进行通信都是安全的,TLS1.3 就在密钥扩展上做了额外的工作。而 mmtls 没有 TLS1.3 这种包袱,可以针对微信自己的网络通信特点进行优化(前面在握手方式选择上就有体现)。mmtls 在不降低安全性的前提下,对 TLS1.3 的密钥扩展做了精简,使得性能上较 TLS1.3 的密钥扩展方式有明显提升。
在 mmtls 中,pre_master_key
(SS 和 ES) 经过密钥扩展,得到了一个长度为 2*enc_key_length 2*iv_length
的一段 buffer,用 key_block
表示,其中:
client_write_key = key_block[0...enc_key_length-1]
client_write_key = key_block[enc_key_length...2*enc_key_length-1]
client_write_IV = key_block[2*enc_key_length...2*enc_key_length iv_length-1]
server_write_IV = key_block[2*enc_key_length iv_length...2*enc_key_length 2*iv_length-1]
重放攻击(Replay Attacks)是指攻击者发送一个接收方已经正常接收过的包,由于重防的数据包是过去的一个有效数据,如果没有防重放的处理,接收方是没办法辨别出来的。防重放在有些业务是必须要处理的,比如:如果收发消息业务没有做防重放处理,就会出现消息重复发送的问题;如果转账业务没有做防重放处理,就会重现重复转账问题。微信在一些关键业务层面上,已经做了防重放的工作,但如果 mmtls 能够在下层协议上就做好防重放,那么就能有效减轻业务层的压力,同时为目前没有做防重放的业务提供一个安全保障。
防重放的解决思路是为连接上的每一个业务包都编一个递增的 sequence number,这样只要服务器检查到新收到的数据包的 sequence number 小于等于之前收到的数据包的 sequence number,就可以断定新收到的数据包为重放包。当然 sequence number 是必须要经过认证的,也就是说 sequence number 要不能被篡改,否则攻击者把 sequence number 改大,就绕过这个防重放检测逻辑了。可以将 sequence number 作为明文的一部分,使用 AES-GCM 进行认证加密,明文变长了,不可避免的会增加一点传输数据的长度。实际上,mmtls 的做法是将 sequence number 作为构造 AES-GCM 算法参数 nonce 的一部分,利用 AES-GCM 的算法特性,只要 AES-GCM 认证解密成功就可以确保 sequence number 符合预期。
上述防重放思路在 1-RTT 的握手方式下是没有问题的,因为在 1-RTT 握手下,第一个 RTT 是没有业务数据的,可以在这个 RTT 下由 Client 和 Server 共同决定开始计算 sequence number 的起点。但是在 0-RTT 的握手方式,第一个业务数据包和握手数据包一起发送给服务器,对于这第一个数据包的防重放,Server 只能完全靠 Client 发来的数据来判断是否重放,如果客户端发送的数据完全由自己生成,没有包含服务器参与的标识,那么这份数据是无法判断是否为重放数据包的。在 TLS1.3 给了一个思路来解决上述这个"0-RTT 跨连接重放的问题":在 Server 处保存一个跨连接的全局状态,每新建一个连接都更新这个全局状态,那么 0-RTT 握手带来的第一个业务数据也可以由这个跨连接的全局状态来判断是否重放。
但是,在一个分布式系统中每新建一个连接都读写这个全局状态,如此频繁的读写,无疑在可用性和性能消耗上都不可接受。事实上,0-RTT 跨连接防重放确实困难,目前没有比较通用、高效的方案。其实在 Google 的 QUIC crypto protocol 中也存在 0-RTT 跨连接重放的问题,由于 QUIC 主要应用在 Chrome 浏览器上,在浏览器上访问网站时,建连接的第一个请求一般是 GET 而不是 POST,所以 0-RTT 加密的数据不涉及多少敏感性,被重放也只是刷新一次页面而已,所以其选择了不解决 0-RTT 防重放的问题。但是微信短连接是 POST 请求,带给 Server 的都是上层的业务数据,因此 0-RTT 防重放是必须要解决的问题。mmtls 根据微信特有的后台架构,提出了基于客户端和服务器端时间序列的防重放策略,mmtls 能够保证超过一段时间 T 的重放包被服务器直接解决,而在短时间 T 内的重放包需要业务框架层来协调支持防重放,这样通过 proxy 层和 logic 框架层一起来解决 0-RTT PSK 请求包防重放问题,限于篇幅,详细方案此处不展开介绍。
mmtls 是参考 TLS1.3 草案标准设计与实现的,使用 ECDH 来做密钥协商,ECDSA 进行签名验证,AES-GCM 作为对称加密算法来对业务数据包进行认证加密,使用 HKDF 进行密钥扩展,摘要算法为 SHA256。另外,结合具体的使用场景,mmtls 在 TLS1.3 的基础上主要做了以下几方面的工作:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。