可编程软件、安全降级、TLS1.3、ECDH、PSK、端可信认证、防重放
2020年1月9日,微信发布了《2019微信数据报告》年,该报告公布了多项微信年度数据,其中2019年微信月活跃账号数为11.51亿,较去年同期增长6%。公众号打开活跃高峰为晚上约21:00,小游戏打开活跃高峰为晚上20:00 左右。朋友圈内打开最多的地方分别是广州、北京、深圳、上海、成都,朋友圈海外打卡最多的地方分别是首尔、大阪、济州岛、曼谷、新加坡。数据报告下载链接:
链接:https://pan.baidu.com/s/1q-rlY1JkbyUJ7SerQOeaCQ
提取码:hgd4
微信数据一直就是个热点,从刚开始的网页版微信拦截、pad协议、Xpose、hook、公众号文章等内容的爬取,更有新榜、红包发送器等参与微信生态,这个一直是热点区。这么庞大的用户群及社交隐私数据,微信是如何保证其安全的?
最近在处理网络数据分析时,偶尔接触到微信数据包,很好奇就学习了下,发现微信在通信安全领域的确前卫。很多公司还在TLS1.0,对方已经对标TLS3.0使用了mmtls,底层对数据包进行了观察,的确安全。发现微信的https会被解析,但mmtls数据包,代理软件也没分析出来。(TLS文字比较多,另起文章说明)
mmtls说明
mmtls的产生,是http协议存在中间人风险,数据泄露的基础上,微信对标TLS1.3产生的安全协议。中间人攻击文章参考:
https://blog.csdn.net/qq_30247635/article/details/87445740(关注下mitmproxy,可编程式软件,工具类转化为服务类一大亮点,专业人员工具选型要点。)
微信在选择协议上,优先考虑安全性、兼顾低延迟、低资源、高可用、可扩展性。在选型时,发现TLS1.2中每次建立一个安全连接都需要额外的2~1个RTT(全握手需要2-RTT),高频处理对客户体验会产生明显影响,短连接下尤为明显。在TLS1.3草案标准中提出了0-RTT(不额外增加网络延时)建立安全连接的方法,另外TLS协议本身通过版本号、CipherSuite、Extension机制提供了良好的可扩展性。但是,TLS1.3一个对所有软件制定的一个通用协议,标准化的版本还在试验阶段,微信基于TLS1.3的通用体系,定制了自己的mmtls。
概述:源于https,再超越
https=http+tls,mmtls=http+mmtls。mmtls内部,它包含三个子协议:Record协议、Handshake协议、Alert协议。这其实是和TLS类似的。它们之间的关系如下图:
Handshake、Alert协议是Record协议的上层协议,业务层协议(Application Protocol)也是record协议的上层协议,在Record协议包中有一个字段(https的数据包头,类似20、21、22),用来区分当前这个Record数据包的上层协议是Handshake、Alert还是业务协议数据包。
Handshake协议用于完成Client与Server的握手协商,协商出一个对称加密密钥Key以及其他密码材料,用于后续数据加密。
Alert协议用于通知对端发生错误,希望对端关闭连接,目前mmtls为了避免server存在过多TCP Time-Wait状态,Alert消息只会server发送给client,由client主动关闭连接。
RTT概念
RTT为数据完全发送完(完成最后一个比特推送到数据链路上,细细品味)到收到确认信号的时间。
Handshake:握出安全
组合安全:mmtls以及TLS协议需要一个Handshake子协议和Record子协议?其实“认证密钥协商+对称加密传输”这种混合加密结构,是绝大多数加密通信协议的通用结构,在mmtls/TLS中Handshake子协议负责密钥协商, Record子协议负责数据对称加密传输。造成这种混合加密结构的本质原因还是因为单独使用公钥加密组件或对称加密组件都有不可避免的缺点。公钥加密组件计算效率往往远低于对称加密组件,直接使用公钥加密组件加密业务数据,这样的性能损耗任何Server都是无法承受的。
根据TLS1.3的描述,实际上有2种1-RTT的密钥协商方式(1-RTT ECDHE、 1-RTT PSK)和4种0-RTT的密钥协商方式(0-RTT PSK, 0-RTT ECDH, 0-RTT PSK-ECDHE, 0-RTT ECDH-ECDHE),微信在保证安全性和性能的前提下,只保留了三种密钥协商方式(1-RTT ECDHE, 1-RTT PSK, 0-RTT PSK)
一、1-RTT
ECDH密钥协商(1-RTT ECDHE)
首先看一个,会遭受到攻击的密钥协商过程。通信双方Alice和Bob使用ECDH密钥交换协议进行密钥协商,ECDH密钥交换协议拥有两个算法:
密钥生成算法
ECDH_Generate_Key
,输出一个公钥和私钥对(ECDH_pub_key, ECDH_pri_key)
,ECDH_pri_key
需要秘密地保存,ECDH_pub_key
可以公开发送给对方。密钥协商算法
ECDH_compute_key
,以对方的公钥和自己的私钥作为输入,计算出一个密钥Key,ECDH_compute_key
算法使得通信双方计算出的密钥Key是一致的。
这样一来Alice和Bob仅仅通过交换自己的公钥ECDH_pub_key
,就可以在Internet这种公开信道上共享一个相同密钥Key,然后用这个Key作为对称加密算法的密钥,进行加密通信。
但是这种密钥协商算法仍然存在一个问题。当Bob将他的Bob_ECDH_pub_key
发送给Alice时,攻击者可以截获Bob_ECDH_pub_key
,自己运行ECDH_Generate_Key算法
产生一个公钥/私钥对,然后把他产生的公钥发送给Alice。同理,攻击者可以截获Alice发送给Bob的Alice_ECDH_pub_key
,再运行ECDH_Generate_Key算法
产生一个公钥/私钥对,并把这个公钥发送给Bob。Alice和Bob仍然可以执行协议,产生一个密钥Key。但实际上,Alice产生的密钥Key实际上是和攻击者Eve协商的;Bob产生的密钥Key也是和攻击者协商Eve的。这种攻击方法被称为中间人攻击(Man In The Middle Attack)。 产生中间人攻击的原因是协商过程中的数据没有经过端点认证(端可信认证),通信两端不知道收到的协商数据是来自对端还是来自中间人,因此单纯的“密钥协商”是不够的,还需要“带认证的密钥协商”。对数据进行认证其实有对称和非对称的两种方式:基于消息认证码(Message Authentication Code)的对称认证和基于签名算法的非对称认证。消息认证码的认证方式需要一个私密的Key,由于此时没有一个私密的Key,因此ECDH认证密钥协商就是ECDH密钥协商加上数字签名算法。在mmtls中我们采用的数字签名算法为ECDSA。双方密钥协商时,再分别运行签名算法对自己发出的公钥ECDH_pub_key
进行签名。收到信息后,首先验证签名,如果签名正确,则继续进行密钥协商。注意到,由于签名算法中的公钥ECDSA_verify_key
是一直公开的,攻击者没有办法阻止别人获取公钥,除非完全掐断发送方的通信。这样一来,中间人攻击就不存在了,因为Eve无法伪造签名。具体过程如图5所示:
事实上,在实际通信过程中,只需要通信中某一方签名它的协商数据就可以保证不被中间人攻击,mmtls就是只对Server做认证,不对Client做认证,因为微信客户端发布出去后,任何人都可以获得,只要能够保证客户端程序本身的完整性(低版本有hook,app会被破坏),就相当于保证了客户端程序是由官方发布的。
在这一点上,TLS要复杂一些,TLS作为一个通用的安全通信协议,可能会存在一些需要对Client进行认证的场合,因此TLS提供了可选的双方相互认证的能力(客户端不可信时),通过握手协商过程中选择的CipherSuite是什么类型来决定是否要对Server进行认证,通过Server是否发送CertificateRequest握手消息来决定是否要对Client进行认证。由于mmtls不需要对Client做认证,在这块内容上比TLS简洁许多,更加轻量级。
PSK密钥协商(1-RTT PSK, 1-RTT PSK)
PSK是在一次ECDH握手中由server下发的内容,它的大致数据结构为PSK{key,ticket{key}}
,即PSK包含一个用来做对称加密密钥的key明文,以及用ticket_key
对key
进行加密的密文ticket
,当然PSK是在安全信道中下发的,也就是说在网络中进行传输的时候PSK是加密的,中间人是拿不到key
的。其中ticket_key
只有server才知道,由server负责私密保存。
PSK协商比较简单,Client将PSK的ticket{key}
部分发送给Server,由于只有Server才知道ticket_key
,因此key是不会被窃听的。Server拿到ticket后,使用ticket_key
解密得到key,然后Server用基于协商得到的密钥key,对协商数据计算消息认证码来认证,这样就完成了PSK认证密钥协商。PSK认证密钥协商使用的都是对称算法,性能上比ECDH认证密钥协商要好很多。
二、0-RTT
TLS1.3草案中提到了0-RTT密钥协商:在握手协商的过程中就安全地将业务数据传递给对端,(1-RTT ECDHE, 1-RTT PSK)都需要一个额外RTT去获取对称加密key,在这个协商的RTT中是不带有业务数据的,全部都是协商数据。
ECDH密钥协商(0-RTT ECDHE)
0-RTT 握手想要达到的目标是在握手的过程中,捎带业务数据到对端,这里难点是如何在客户端发起协商请求的时候就生成一个可信的对称密钥加密业务数据。(在1-RTT ECDHE中,Client生成一对公私钥(cli_pub_key, cli_pri_key)
,然后将公钥cli_pub_key
传递给Server,然后Server生成一对公私钥(svr_pub_key, svr_pri_key)
并将公钥svr_pub_key
传递给Client,Client收到svr_pub_key
后才能计算出对称密钥。上述过程(svr_pub_key, svr_pri_key)
由于是临时生成的,需要一个RTT将svr_pub_key
传递给客户端。--->固化服务端生成的公私钥
预先生成一对公私钥(static_svr_pub_key, static_svr_pri_key)
并将static_svr_pub_key
预置在Client中,那么Client可以在发起握手前就通过static_svr_pub_ke
和cli_pub_key
生成一个对称密钥SS(Static Secret)
,然后用SS加密第一个业务数据包,这样将SS加密的业务数据包和cli_pub_key
一起传给Server,Server通过cli_pub_key
和static_server_private_key
算出SS,解密业务数据包,这样就达到了0-RTT密钥协商的效果。
ps:ECDH协商中,如果公私钥对都是临时生成的,一般称为ECDHE,因此1-RTT的ECDH协商方式被称为1-RTT ECDHE握手,0-RTT 中有一个静态内置的公钥,因此称为0-RTT ECDH握手。
PSK密钥协商
0-RTT PSK握手比较简单,回顾1-RTT PSK握手,其实在进行1-RTT PSK握手之前,Client已经有一个对称加密密钥key了,就直接拿这个对称加密密钥key加密业务数据,然后将其和握手协商数据ticket{key}
一起传递给Server。
PFS完全前向保密
PFS(perfect forward secrecy),中文可叫做完全前向保密。它要求一个密钥被破解,并不影响其他密钥的安全性,反映的密钥协商过程中,大致的意思是用来产生会话密钥的长期密钥泄露出去,不会造成之前通信时使用的会话密钥的泄露;或者密钥协商方案中不存在长期密钥,所有协商材料都是临时生成的。(很庆幸,2013年就精细化分析了POS密钥体系,IC卡的三层非对称体系、批次密钥的实时刷新,有兴趣单聊。)
0-RTT ECDHE基于static_svr_pri_key保护的数据就只有第一个业务数据包AppData,后续的包都是基于ES(Ephemeral Secret 临时非对称密钥对)对业务数据进行保护的。这样即使static_svr_pri_key泄露,也只有连接的第一个业务数据包能够被解密,提高前向安全性。
0-RTT PSK密钥协商加密的数据的安全性依赖于长期保存密钥ticket_key
,如果ticket_key
泄露,那么所有基于ticket_key
进行保护的数据都将失去保密性,因此同样可以在0-RTT PSK密钥协商的过程中,同时完成ECDHE密钥协商,提高前向安全性。(组合安全PSK)
关键处理
要防范中间人攻击,就必须要对端发送数据进行认证。在1-RTT ECDHE中的认证方式是使用ECDSA签名算法的非对称认证方式,整个过程大致如下:在client和server生成临时公私密钥对的基础上,再次增加密钥对verify_key
和sign_key
是一对ECDSA密钥,然后用签名密钥sign_key
对svr_pub_key
进行签名,得到签名值Signature,并把签名值Signature和svr_pub_key
一起发送给客户端。客户端收到之后,用verify_key
进行验签(),验签成功后才会继续走协商对称密钥的流程。
Verify_Key如何下发给客户端?
TLS是使用证书链的方式来派发公钥(证书),对于微信来说,如果使用证书链的方式来派发Server的公钥(证书),无论自建Root CA还是从CA处申请证书,都会增加成本且在验签过程中会存在额外的资源消耗。由于客户端是由我们自己发布的,我们可以将verify_key
直接内置在客户端(可参考),这样就避免证书链验证带来的时间消耗以及证书链传输带来的带宽消耗。
如何避免签名密钥
sign_key
泄露带来的影响?
如果sign_key
泄露,那么任何人都可以伪造成Server欺骗Client,因为它拿到了sign_key
,它就可以签发任何内容,Client用verify_key
去验证签名必然验签成功。因此sign_key
如果泄露必须要能够对verify_key
进行撤销,重新派发新的公钥。这其实和前一问题是紧密联系的,前一问题是公钥派发问题,本问题是公钥撤销问题。TLS是通过CRL和OCSP两种方式来撤销公钥的,但是这两种方式存在撤销不及时或给验证带来额外延迟的副作用。由于mmtls是通过内置·verify_key·在客户端,必要时通过强制升级客户端的方式就能完成公钥撤销及更新。另外,sign_key
是需要Server高度保密的,一般不会被泄露,对于微信后台来说,类似于sign_key
这样,需要长期私密保存的密钥在之前也有存在,早已形成了一套方法和流程来应对长期私密保存密钥的问题。(如何管理长期密钥?)
用
sign_key
进行签名的内容仅仅只包含svr_pub_key
是否有隐患?
回顾一下,上面描述的带认证的ECDH协商过程,似乎已经足够安全,无懈可击了,但是,面对成亿的客户端发起ECDH握手到成千上万台接入层机器,每台机器对一个TCP连接随机生成不同的ECDH公私钥对,这里试想一种情况,假设某一台机器某一次生成的ECDH私钥svr_pri_key1
泄露,这实际上是可能的,因为临时生成的ECDH公私钥对本身没有做任何保密保存的措施,是明文、短暂地存放在内存中,一般情况没有问题,但在分布式环境,大量机器大量随机生成公私钥对的情况下,难保某一次不被泄露。
私钥泄露,暴漏密钥体系,再次造成中间人攻击:这样用sign_key
(sign_key
是长期保存,且分布式环境共享的)对svr_pub_key1
进行签名得到签名值Signature1,此时攻击者已经拿到svr_pri_key1,svr_pub_key1和Signature1
,这样他就可以实施中间人攻击,让客户端每次拿到的服务器ECDH公钥都是svr_pub_key1(
缺点
)
:客户端随机生成ECDH公私钥对(cli_pub_key, cli_pri_key)
并将cli_pub_key
发给Server,中间人将消息拦截下来,将client_pub_key
替换成自己生成的client_pub_key’
,并将svr_pub_key1
和Signature1回给Client,这样Client就通过计算ECDH_Compute_Key(svr_pub_key1, cli_pri_key)=Key1
, Server通过计算ECDH_Compute_Key(client_pub_key’, svr_pub_key)=Key’
,中间人既可以计算出Key1和Key’,这样它就可以用Key1和Client通信,用Key’和Server进行通信。发生上述被攻击的原因在于一次握手中公钥的签名值被用于另外一次握手中,如果有一种方法能够使得这个签名值和一次握手一一对应,那么就能解决这个问题。(打破固化密钥风险)
解决办法也很简单,就是在握手请求的ClientHello消息中带一个Client_Random
随机值,然后在签名的时候将Client_Random
和svr_pub_key
一起做签名,这样得到的签名值就与Client_Random
对应了。mmtls在实际处理过程中,为了避免Client的随机数生成器有问题。
选型要点
PSK握手全程无非对称运算,Server性能消耗小,但前向安全性弱,ECDHE握手有非对称运算,Server性能消耗大,如何选择呢?
微信长连接在建立时的第一个数据包是不会发送业务数据的,因此使用1-RTT的握手方式,由第一个握手包取代之前的nooping包去探测长连的连通性,这样并不会增加长连的网络延时,因此我们选取在长连接情况下,使用1-RTT ECDHE和1-RTT PSK这两种密钥协商方式。
微信短连接为了兼容老版本的HTTP协议,使用0-RTT的握手方式,如果使用1-RTT的握手方式,短连接至少需要2个RTT才能完成业务数据的传输,导致时延加倍,用户体验较差。两种情况:
(1)客户端没有PSK,为了安全性,这时和长连接的握手方式一样,使用1-RTT ECDHE;
(2)客户端有PSK,这时为了减少网络时延,应该使用0-RTT PSK或0-RTT PSK-ECDHE。
0-RTT无法做到前向安全性0-RTT PSK-ECDHE这种方式,只能保证本短连接业务响应回包的前向安全性,这带来安全性上的优势是比较小的,但是与0-RTT PSK握手方式相比,0-RTT PSK-ECDHE在每次握手对server会多2次ECDH运算和1次ECDSA运算。微信的短连接是非常频繁的,这对性能影响极大,因此综合考虑,在客户端有PSK的情况下,我们选择使用0-RTT PSK握手。由于0-RTT PSK握手安全性依赖ticket_key,
PSK必须要限制过期时间,ticket_key
必须定期轮换。
Record协议
微信选择对称加密算法(如常用的AES-CBC)加密业务数据。AES-CBC这种加密算法只提供保密性,但是并不提供完整性。
AEAD算法:Encrypt和MAC直接集成在一个算法内部,从安全角度看,风险是很高的,故TLS1.3彻底禁止AEAD以外的其他算法。mmtls经过综合考虑,选择了使用AES-GCM这种AEAD类算法,作为协议的认证加密组件,而且AES-GCM也是TLS1.3要求必须实现的算法。
防重放
防重放在金融账务、秒杀团购等有限资源处理中都是系统异常,需要人工干预处理。微信在一些关键业务层面上,已经做了防重放的工作,但如果mmtls能够在下层协议上就做好防重放,那么就能有效减轻业务层的压力(可参考作为通用机制)。
递增的sequence number是一种解决思路,通过变化的sequence numbe可以实现,但规则容易被破解。加密是一种方法。使用AES-GCM进行认证加密,明文变长了,不可避免的会增加一点传输数据的长度。mmtls的做法是将sequence number作为构造AES-GCM算法参数nonce的一部分,利用AES-GCM的算法特性,只要AES-GCM认证解密成功就可以确保sequence number符合预期。
上述防重放思路在1-RTT的握手方式下是没有问题的,但在0-RTT的握手方式,第一个业务数据包和握手数据包一起发送给服务器,对于这第一个数据包的防重放,Server只能完全靠Client发来的数据来判断是否重放,如果客户端发送的数据完全由自己生成,没有包含服务器参与的标识,那么这份数据是无法判断是否为重放数据包的。在TLS1.3给了一个思路来解决上述这个“0-RTT跨连接重放的问题”:在Server处保存一个跨连接的全局状态,每新建一个连接都更新这个全局状态,那么0-RTT握手带来的第一个业务数据也可以由这个跨连接的全局状态来判断是否重放。但是,在一个分布式系统中每新建一个连接都读写这个全局状态,如此频繁的读写,无疑在可用性和性能消耗上都不可接受。
0-RTT跨连接防重放目前没有比较通用、高效的方案。mmtls根据微信特有的后台架构,提出了基于客户端和服务器端时间序列的防重放策略,mmtls能够保证超过一段时间T的重放包被服务器直接解决,而在短时间T内的重放包需要业务框架层来协调支持防重放,这样通过proxy层和logic框架层一起来解决0-RTT PSK请求包防重放问题。在高可用上,同时提供安全级别稍低的有损服务(安全降级)。