互联网通信技术的发展应用,让信息无远弗届,连接随时发生。关注【融云全球互联网通信云】了解更多
但与此同时,互联网通信也暴露出了一系列安全问题。我们需要有针对性地应用相关安全技术,增强通信系统的安全性和可靠性,为保护用户个人信息安全筑起防护墙。
在融云互联网通信安全系列文章首篇“链路安全”中,我们介绍了关于链路加密的相关技术和实践。在传输即时通信消息时启用 TLS 链路加密,保证消息在到达服务器前无法被窃听和篡改;使用 CA 认证机制,杜绝中间人攻击。
这些都是提升客户端到服务器之间数据传输安全性的有效办法,但是未能解决用户间通信隐私性和安全性面临的风险问题。因为在将数据传输到服务器之后,所有有权访问此服务器的人,包括员工、相关供应商及其他有关人员甚至黑客,都有可能读取到用户的数据。
因此,端到端加密技术被大力推动,其出现后很快被大众所认知并接受。在 WhatsApp、Signal、Telegram 等即时通信软件中都有所应用。
今天,我们分享互联网通信安全系列文章的第三篇,带领大家一文读懂端到端消息加密。
端到端加密方案设计思路
说到端到端加密,我们首先想到的解决方案是,在发送端发送消息前对整个消息进行加密,接收端接收到消息后进行解密。这样,消息中转服务器就无法获取我们的消息内容了。
事实上,这确实是端到端加密中消息收发的简化版解决方案,只是我们在实际应用中要更加复杂,效果也更加安全。
我们需要先解决的前置问题是,如何安全地传递用于消息加解密的密钥。
答案是用非对称加密的方式传输密钥,与 SSL / TLS 中安全交换密钥的方式类似。
非对称加密传输对称加密密钥的算法,一般归结两种方式:一种是以 RSA、ECC 等为主的,公钥加密私钥解密的方式,本质是加解密的算法;另一种是以 DH、ECDH 为主的生成共享密钥的方式,本质是通过计算协商一个共同的密钥而不是加解密算法。
大部分即时通信软件中的端到端加密都采用生成共享密钥的方式来传输会话密钥。这是为什么呢?
此处为了方便理解,附上 DH 算法介绍:
基于原根的定义及性质,可以定义 Diffie-Hellman 密钥交换算法。该算法描述如下:
- 有两个全局公开的参数,一个素数 q 和一个整数 a, a 是 q 的一个原根。
- 假设用户 A 和 B 希望交换一个密钥,用户 A 选择一个作为私有密钥的随机数 XA(XA
,并计算公开密钥 YA=a^XA mod q。A 对 XA 的值保密存放而使 YA 能被 B 公开获得。类似地,用户 B 选择一个私有的随机数 XB
,并计算公开密钥 YB=a^XB mod q。B 对 XB 的值保密存放而使 YB 能被 A 公开获得。
- 用户 A 产生共享秘密密钥的计算方式是 K = (YB)^XA mod q。同样,用户 B 产生共享秘密密钥的计算是 K = (YA)^XB mod q。这两个计算产生相同的结果。
推导过程如下:
K = (YB)^XA mod q
= (a^XB mod q)^XA mod q
= (a^XB)^XA mod q (根据取模运算规则得到)
= a^(XBXA) mod q
= (a^XA)^XB mod q
= (a^XA mod q)^XB mod q
= (YA)^XB mod q
因此相当于双方已经交换了一个相同的秘密密钥。
4. 因为 XA 和 XB 是保密的,一个敌对方可以利用的参数只有 q,a,YA 和 YB。因而敌对方被迫取离散对数来确定密钥。例如,要获取用户 B 的秘密密钥,敌对方必须先计算 XB,然后再使用用户 B 采用的同样方法计算其秘密密钥 K。
Diffie-Hellman 密钥交换算法的安全性依赖于这样一个事实:虽然计算以一个素数为模的指数相对容易,但计算离散对数却很困难。对于大的素数,计算出离散对数几乎是不可能的。
简要描述一下 DH 共享密钥的过程如下:
其中“密钥 S”即为最终的共享密钥。
总结而言,端到端加密采用生成共享密钥的方式来传输会话密钥有如下几个原因:
- 如果采用 RSA、ECC 等公钥加密私钥解密的方式传输密钥,需要在创建会话时生成临时密钥,并通过对方公钥加密后传输到接收端。这就需要完全保证消息的可靠性,如果该消息在任何一个环节中丢失或损坏,则后续通信都无法进行。或者,需要采用更为可靠的传输方案,通常做法为需要接收端在线,通过各种确认来保证这个可靠性。而采用共享密钥的方式则只需要知道对方的公钥,就可以完成生成共享密钥,并不一定需要对方在线。
- 如果已经生成的临时对称密钥丢失,则需要重新协商密钥。而采用共享密钥的方式则只需要知道对方的公钥,就可以完成生成共享密钥,不需要重新协商。
- 采用公钥加密私钥解密的方式至少会比生成共享密钥方式多一次交换对称密钥的通信过程。
- 密钥协商方式,不仅仅可以完成两个点之间的密钥协商,还可以延展到多人之间的共同协商出相同的密钥,这样能满足多人群体沟通的需求。
端到端加密初步方案
我们结合对于 DH 算法这种共享密钥方式的认知(公钥可随意公开),先设计一个简单的端到端消息加密的过程。
- 在客户端 APP 首次安装时,基于服务器公开的两个全局的参数,生成自己的 DH 公钥和私钥。
- 将自己的公钥上传证书服务器,证书服务器上保存用户标识与其公钥的关系。私钥则保存在客户端上。
- 首次给对方发送消息或首次接收到对方消息时,便到证书服务器查询对方的公钥。
- 根据对方公钥和自己的私钥计算出共享密钥。
- 后续与对方所有的消息都基于这个密钥和相同的对称加解密算法进行加密解密操作。
至此我们完成了一个简单的端到端消息加密方案,在这个方案中我们引入了一个第三方的用于存储用户公钥的角色,这个角色的存在可以让任何一方都不用关心对方的在线状态,随时给对方发送加密过消息,而消息转发服务器无法解密消息。
接下来,我们针对这个简单方案存在的各种问题,进行分析和优化。
端到端加密方案优化
HMAC
在消息传输过程中,双方需要确认彼此消息的完整性,简单的做法就是将消息进行 Hash,得到的 Hash 值附加到消息后,随消息一起发送;对端接收后,同样进行 Hash,来验证消息是否被篡改。
关键点在于不同数据得到的 Hash 值一定不同,其中带密钥的 Hash 值就是 MAC。
另外,为了避免使用同样的 Hash 函数对相同数据进行操作总是得出同样的值,额外加入一个密钥,这样使用不同密钥就可以得出不同的 MAC。当然,这个密钥是两个对端都知道的。
这样,我们就得到了基于加密 Hash 的消息完整性认证的算法——Hash-based MAC。
ECDH
DH 算法是以离散对数的数学难题为基础的,随着计算机计算能力逐步增强,我们要不停地使用更大的数以增加破解难度,目前业界普遍认为至少需要使用 2048 位 DH 算法才具备更好的安全性。
在此我们引入 ECDH 算法替换 DH 算法。ECDH 密钥协商算法是 ECC 算法和 DH 密钥交换原理结合使用。ECC 是建立在基于椭圆曲线的离散对数问题上的密码体制。在相同破解难度下,ECC 具有更小长度的密钥和更快的正向计算速度优势。
我们系统上的 ECDH 可以直接采用目前公开的 sepc256kl 和 Curve25519 曲线,而无需服务再提供公开大数参数。
前向安全
在消息传输过程中,如果协商好的密钥泄露了,就意味着所有信息都将暴露于风险之下。为了防止这种情况发生,我们需要每次加密使用的密钥都与上一次不同,且不可以反向推导得出之前的密钥。
此处引入一个 Hash 算法,这个 Hash 算法可以通过输入一个密钥导出另外一个离散性更大的密钥,每次发送消息时都是用上次的消息密钥进行 Hash 运算得出本次密钥,由于 Hash 算法具有单向不可逆的特性,因此就无法通过本次的密钥推导之前的密钥。从感观上,这就像一个棘轮,棘轮就是一种特殊的齿轮,他只能往一个方向转下去,而不能往回转。
双棘轮
出于极致的安全性要求,我们会考虑前向安全和后向安全。如何保证在某次通信中,被破解出来的密钥,不能破解出之前的消息,而且在一定周期内,这个破解出来的密钥将不会再起作用。
介于此我们再引入另外一个棘轮来保证其向后的安全性。这就是大名鼎鼎的 Signal protocol 中的双棘轮算法。
双棘轮算法包含一个 KDF 棘轮和一个 DH 棘轮。
KDF 全称(Key derivation function) 密钥导出函数,用于从一个原始的密钥导出一个或多个密钥。本质上就是 Hash 函数,通常用来将短密码变成长密码。另外 KDF 需要加“盐”(salt),用于防彩虹表,出于 Hash 的特性,这个“盐”的长度至少要大于 Hash 结果长度。
KDF (原密钥,盐) = 导出密钥
KDF 棘轮就是运用 KDF 算法,设计出一种密钥不断变化的效果,流程如下:
(KDF 棘轮流程)
第一步,将初始密钥使用 KDF 算法导出新的密钥,新密钥被切成两部分,前半部分作为下一次 KDF 计算的输入,后半部分作为消息密钥。
每迭代一次(也可以说棘轮步进一次),就会生成新的消息密钥。
由于 KDF 算法的单向性,通过这条消息的密钥无法倒推出上一条消息密钥。这就保证了密钥的前向安全。但是如果 KDF 中的盐被掌握,那么它就可以按照这种算法计算出以后所有的消息密钥。
为了保证后向安全,就要设计一种方法,使每次迭代时引入的盐是随机的,从而保证每次的消息密钥是不可以向后推算的。
由前面介绍的 DH 算法得知,两对密钥对可以通过 DH 协议生成一个安全的协商密钥,如果更换其中一个密钥对,新的协商密钥也会变化。
根据这个方法,我们可以设计出一个安全更新盐的方法。我们在证书服务器增加一个临时公钥证书,这个临时证书是按照接收双方标识构建的临时公钥对,即每个人的每个单人会话都具备一个临时公钥。每进行一个消息轮回,就更新一次己方的临时公钥,同时根据另外一方的临时公钥和己方的私钥进行协商,并将协商出的密钥作为盐,使得 KDF 棘轮算法生成的消息密钥具有后向安全性。
在初始时我们无法预测出每个人所有的新二人会话,那么我们就可以规定创建新的二人会话时,发起方首先生成一个新的临时 DH 公私钥对,并向服务器上传自己的临时 DH 公钥;其次发送方用接收方公布的长期公钥与自己的临时私钥协商出密钥作为消息加密的密钥,对消息进行加密;最后接收方首次接收到消息后用自己的长期公钥和发送方的临时私钥计算得出消息密钥,并在首次回复消息时生成临时公私钥,同时上传临时公钥。
问题是,如果接收端不在线,而发送端每条消息都去更新己方的临时公钥证书,就会导致发出去的这些消息,在接收端上线并收取后无法被正常解密。
为了解决这个问题,我们需要规定:只有在发出消息并得到对方回复后才更新临时证书,若对方不回复消息则不去更新临时证书。接收端能回复消息就表示其已经上线并接收完消息,这样就可以保证离线消息或者消息乱序也可以被对方正常解析。这种方法就是双棘轮算法中的另外一个 DH 棘轮。
X3DH
对比最初的方案,为了满足消息的前向安全和后向安全,我们增加了双棘轮算法,在原基础方案上为每个人增加了一组会话级别临时 DH 密钥,每个人都拥有一个长期密钥和一组临时密钥。
但是,由于长期密钥无法被更换,所以方案依然存在着安全隐患。因此,Signal protocol 设计了一种更为复杂和安全的 DH 密钥交换过程,称之为 X3DH,即 DH 协议的 3 倍扩展版。
在 X3DH 协议里,每个人都要创建 3 种密钥对,分别如下:
- 身份密钥对(Identity Key Pair) —— 一个长期的符合 DH 协议的密钥对,用户注册时创建,与用户身份绑定;
- 已签名的预共享密钥(Signed Pre Key) ——一个中期的符合 DH 协议的密钥对,用户注册时创建,由身份密钥签名,并定期进行轮换,此密钥可能是为了保护身份密钥不被泄露;
- 一次性预共享密钥(One-Time Pre Keys) —— 一次性使用的 Curve25519 密钥对队列,安装时生成,不足时补充。
所有人都要将这 3 种密钥对的公钥上传到服务器上,以便其他人发起会话时使用。
假如 Alice 要给 Bob 发送消息,首先要和 Bob 确定消息密钥,流程大致如下:
- Alice 要创建一个临时密钥对(ephemeral key),我们设成 EPK-A,此密钥对是为了后面棘轮算法准备,在此处作用不大;
- Alice 从服务器获取 Bob 的三种密钥对的公钥:身份密钥对IPK-B;已签名的预共享密钥 SPK-B;一次性预共享密钥 OPK-B;
- Alice 开始使用 DH 协议计算协商密钥,要引入参数包括:自己创建的两个密钥对的私钥,以及 Bob 的三个公钥。然后用类似排列组合的方式,将自己的私钥与对方的公钥分别带入 DH 算法计算。
DH1 = DH(IPK-A, SPK-B)
DH2 = DH(EPK-A, IPK-B)
DH3 = DH(EPK-A, SPK-B)
DH4 = DH(IPK-A, OPK-B)
如图所示:
然后将计算得到的四个值,前后连接起来,就得到了初始密钥,如下:
DH = DH1 || DH2 || DH3 || DH4
注:“||”代表连接符,比如 456 || 123 = 456123
但是 DH 这个密钥太长,不适合作为消息密钥,所以对这个初始密钥进行一次 KDF 计算,以衍生出固定长度的消息密钥 S
S = KDF(DH1 || DH2 || DH3 || DH4)
这一步,Alice 终于计算出了消息密钥 S。
- Alice 使用消息密钥 S 对消息进行加密,连同自己的身份公钥 IPK-A 和临时公钥 EPK-A 一同发给 Bob。
- Bob 收到 Alice 的信息后,取出 Alice 的 2 个公钥,连同自己的密钥,使用与 Alice 相同的算法计算消息密钥 S。
- Bob 和 Alice 使用消息密钥进行加密通讯。
由上可知,X3DH 实际是复杂版的 DH 协议。
至此,我们简单介绍了 Signal Protocol 中最为核心的 X3DH 协议与双棘轮算法,基本上可以满足前向安全和后向安全。当然,真实的处理过程会更为复杂和安全。
群聊的端到端加密方案
在即时通讯场景中,除了二人之间的聊天以外,还有一个重要的场景就是群聊,那么群体之间的消息如何做端到端加密呢?
我们再次回到 DH 密钥协商算法上的推导过程。显然,多方情况下依然可以继续使用 DH 密钥协商算法,这就是群聊中端到端加密的基础。
而在 Signal Protocol 在群组聊天中的设计与二人聊天又有所不同,由于群聊的保密性要求相对低一些,只采用了 KDF 链棘轮+公钥签名来进行加密通讯以保障加密的前向安全。
通讯流程如下:
- 每个群组成员都要首先生成随机 32 字节的 KDF 链密钥(Chain Key),用于生成消息密钥,以保障消息密钥的前向安全性,同时还要生成一个随机 Curve25519 签名密钥对,用于消息签名。
- 每个群组成员用向其它成员单独加密发送链密钥(Chain Key)和签名公钥。此时每一个成员都拥有群内所有成员的链密钥和签名公钥。
- 当一名成员发送消息时,首先用 KDF 链棘轮算法生成的消息密钥加密消息,然后使用私钥签名,再将消息发给服务器,由服务器发送给其它成员。
- 其它成员收到加密消息后,首先使用发送人的签名公钥验证,验证成功后,使用相应的链密钥生成消息密钥,并用消息密钥解密。
- 当群组成员离开时,所有的群组成员都清除自己链密钥和签名公钥并重新生成,再次单独发给每一位成员。这样操作,离开的成员就无法查看群组内的消息了。
由上可知,一个人在不同的群组里,会生成不同的链密钥和签名密钥对,以保障群组之间的隔离。在每个群组中,每个成员还要存储其它成员的 KDF 链和签名公钥,如果群组成员过多,加解密运算量非常大,会影响发送和接收速度,同时密钥管理数据库也会非常大,读取效率也会降低。
所以,群组聊天使用 Signal Protocol 协议,群人数不宜太多。
端到端加密方案补充说明
上面我们介绍了即时通信中二人聊天和群组聊天的端到端加密全部过程。但是正常情况下端到端消息加密只是加密消息的实际负载部分,而消息的控制层则不会被加密,因为消息转发服务器需要根据控制信息进行消息转发或路由。
为了防止消息被定向分析(分析用户什么时间向谁发送了消息,或接收了谁的消息),我们依然需要对整体即时通信的长连接链路进行加密保护,防止信息被中间网络设备截获并分析;为了防止密钥服务器被中间人攻击,也需要开启链路加密保护。