传输
传输(Transport Security)
系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充?
前文中笔者已经为传输安全层挖下了不少坑,譬如:基于信道的认证是怎样实现的?为什么 HTTPS 是绝大部分信息系统防御通信被窃听和篡改的唯一可行手段?传输安全层难道不也是一种自动化的加密吗?为何说客户端如何加密都不能代替 HTTPS?
本节将以“假设链路上的安全得不到保障,攻击者如何摧毁之前认证、授权、凭证、保密中所提到的种种安全机制”为场景,讲解传输层安全所要解决的问题,同时也是对前面这些疑问句的回答。
摘要、加密与签名
我们从 JWT 令牌的一小段“题外话”来引出现代密码学算法的三种主要用途:摘要、加密与签名。JWT 令牌携带信息的可信度源自于它是被签名过的信息,因此是不可篡改的,是令牌签发者真实意图的体现。然而,你是否了解过签名具体做了什么?为什么有签名就能够让负载中的信息变得不可篡改和不可抵赖呢?要解释数字签名(Digital Signature),必须先从密码学算法的另外两种基础应用“摘要”和“加密”说起。
摘要也称之为数字摘要(Digital Digest)或数字指纹(Digital Fingerprint)。JWT 令牌中默认的签名信息是对令牌头、负载和密钥三者通过令牌头中指定的哈希算法(HMAC SHA256)计算出来的摘要值,如下所示:
signature = SHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)
理想的哈希算法都具备两个特性:一是易变性,这是指算法的输入端发生了任何一点细微变动,都会引发雪崩效应(Avalanche Effect),使得输出端的结果产生极大的变化。这个特性常被用来做校验,保护信息未被篡改,譬如互联网上下载大文件,常会附有一个哈希校验码,以确保下载下来的文件没有因网络或其他原因与原文件产生任何偏差。二是不可逆性,摘要的过程是单向的,不可能从摘要的结果中逆向还原出输入值来。这点只要具备初中数学知识就能想明白,世间的信息有无穷多种,而摘要的结果无论其位数是 32、128、512 Bits,再大也总归是个有限的数字,因此输入数据与输出的摘要结果必然不是一一对应的关系,如果我把一部电影做摘要形成 256 Bits 的哈希值,应该没有人会指望能从这个哈希值中还原出一部电影的。偶尔能听到 MD5、SHA1 或其他哈希算法被破解了的新闻,这里的“破解”并不是“解密”的意思,而是指找到了该算法的高效率碰撞方法,能够在合理的时间内生成两个摘要结果相同的输入比特流,但并不能指定这两个输入流中的某一个,更不代表碰撞产生的比特流就会是原来的输入源。
由这两个特点可见,摘要的意义是在源信息不泄漏的前提下辨别其真伪。易变性保证了从公开的特征上可以甄别出是否来自于源信息,不可逆性保证了从公开的特征并不会暴露出源信息,这与今天用做身份甄别的指纹、面容和虹膜的生物特征是具有高度可比性的。在一些场合中,摘要也会被借用来做加密(如保密中介绍的慢哈希 Bcrypt 算法)和签名(如 JWT 签名中的 HMAC SHA256 算法),但在严格意义上看,摘要与这两者是有本质的区别。
加密与摘要的本质区别在于加密是可逆的,逆过程就是解密。在经典密码学时代,加密的安全主要是依靠机密性来保证的,即依靠保护加密算法或算法的执行参数不被泄漏来保障信息的安全。而现代密码学不依靠机密性,加解密算法都是完全公开的,安全建立在特定问题的计算复杂度之上,具体是指算法根据输入端计算输出结果耗费的算力资源很小,但根据输出端的结果反过来推算原本的输入,耗费的算力就极其庞大。一个经常在课堂中用来说明计算复杂度的例子是大数的质因数分解,我们可以轻而易举的地(以 O(nlogn)的复杂度)计算出两个大素数的乘积,譬如:
97667323933 * 128764321253 = 12576066674829627448049
根据算术基本定理,质因数的分解形式是唯一的,且前面计算条件中给出的运算因子已经是质数,所以 12,576,066,674,829,627,448,049 的分解形式就只有唯一的形式,即上面所示的唯一答案。然而如何对大数进行质因数分解,迄今没有找到多项式时间的算法,甚至无法确切地知道这个问题属于哪个复杂度类(Complexity Class)。所以尽管这个过程理论上一定是可逆的,但实际上算力差异决定了逆过程无法实现。(注:24 位十进制数的因数分解完全在现代计算机的暴力处理能力范围内,这里只是举例。但目前很多计算机科学家都相信大数分解问题就是一种P!=NP的证例,尽管也并没有人能证明它一定不存在多项式时间的解法。除了质因数分解外,离散对数和椭圆曲线也是具备实用性的复杂问题)
根据加密与解密是否采用同一个密钥,现代密码学算法可分为对称加密算法和非对称加密两大类型,这两类算法各自有很明确的优劣势与应用场景。对称加密的缺点显而易见,加密和解密使用相同的密钥,当通信的成员数量增加时,为保证两两通信都都采用独立的密钥,密钥数量就与成员数量的平方成正比,这必然面临密钥管理的难题。而更尴尬的难题是当通信双方原本就不存在安全的信道时,如何才能将一个只能让通信双方才能知道的密钥传输给对方?如果有通道可以安全地传输密钥,那为何不使用现有的通道传输信息?这个“蛋鸡悖论”曾在很长的时间里严重阻碍了密码学在真实世界中推广应用。
20 世纪 70 年代中后期出现的非对称加密算法从根本上解决了密钥分发的难题,它将密钥分成公钥和私钥,公钥可以完全公开,无须安全传输的保证。私钥由用户自行保管,不参与任何通信传输。根据这两个密钥加解密方式的不同,使得算法可以提供两种不同的功能:
公钥加密,私钥解密,这种就是加密,用于向私钥所有者发送信息,这个信息可能被他人篡改,但是无法被他人得知。如果甲想给乙发一个安全保密的数据,那么应该甲乙各自有一个私钥,甲先用乙的公钥加密这段数据,再用自己的私钥加密这段加密后的数据。最后再发给乙,这样确保了内容即不会被读取,也不能被篡改。
私钥加密,公钥解密,这种就是签名,用于让所有公钥所有者验证私钥所有者的身份,并且用来防止私钥所有者发布的内容被篡改。但是不用来保证内容不被他人获得。
这两种用途理论上肯定是成立的,现实中却一般不成立,单靠非对称加密算法,既做不了加密也做不了签名。原因是不论是加密还是解密,非对称加密算法的计算复杂度都相当高,性能比对称加密要差上好几个数量级(不是好几倍)。加解密性能不仅影响速度,还导致了现行的非对称加密算法都没有支持分组加密模式。分组是指由于明文长度与密钥长度在安全上具有相关性,通俗地说就是多长的密钥决定了它能加密多长的明文,如果明文太短就需要进行填充,太长就需要进行分组。因非对称加密本身的效率所限,难以支持分组,所以主流的非对称加密算法都只能加密不超过密钥长度的数据,这决定了非对称加密不能直接用于大量数据的加密。
在加密方面,现在一般会结合对称与非对称加密的优点,以混合加密来保护信道安全,具体做法是用非对称加密来安全地传递少量数据给通信的另一方,然后再以这些数据为密钥,采用对称加密来安全高效地大量加密传输数据,这种由多种加密算法组合的应用形式被称为“密码学套件”。非对称加密在这个场景中发挥的作用称为“密钥协商”。
在签名方面,现在一般会结合摘要与非对称加密的优点,以对摘要结果做加密的形式来保证签名的适用性。由于对任何长度的输入源做摘要之后都能得到固定长度的结果,所以只要对摘要的结果进行签名,即相当于对整个输入源进行了背书,保证一旦内容遭到篡改,摘要结果就会变化,签名也就马上失效了。
表 5-1 汇总了前面提到的三种算法,并列举了它们的主要特征、用途和局限性。
表 5-1 三种密码学算法的对比
类型 | 特点 | 常见实现 | 主要用途 | 主要局限 |
---|---|---|---|---|
哈希摘要 | 不可逆,即不能解密,所以并不是加密算法,只是一些场景把它当作加密算法使用。 易变性,输入发生 1 Bit 变动,就可能导致输出结果 50%的内容发生改变。 无论输入长度多少,输出长度固定(2 的 N 次幂)。 | MD2/4/5/6、SHA0/1/256/512 | 摘要 | 无法解密 |
对称加密 | 加密是指加密和解密是一样的密钥。 设计难度相对较小,执行速度相对较块。 加密明文长度不受限制。 | DES、AES、RC4、IDEA | 加密 | 要解决如何把密钥安全地传递给解密者。 |
非对称加密 | 加密和解密使用的是不同的密钥。 明文长度不能超过公钥长度。 | RSA、BCDSA、ElGamal | 签名、传递密钥 | 性能与加密明文长度受限。 |
现在,让我们再回到开篇关于 JWT 令牌的几个问题中来。有了哈希摘要、对称和非对称加密,JWT 令牌的签名就能保证负载中的信息不可篡改、不可抵赖吗?其实还是不行的,这个场景里,数字签名的安全性仍存在一个致命的漏洞:公钥虽然是公开的,但在网络世界里“公开”具体是一种什么操作?如何保证每一个获取公钥的服务,拿到的公钥就是授权服务器所希望它拿到的?
在网络传输是不可信任的前提下,公钥在网络传输过程中可能已经被篡改,如果获取公钥的网络请求被攻击者截获并篡改,返回了攻击者自己的公钥,那以后攻击者就可以用自己的私钥来签名,让资源服务器无条件信任它的所有行为了。现实世界中公开公钥,可以通过打电话、发邮件、短信息、登报纸、同时发布在多个网站上等等,很多网络通信之外的途径来达成,但在程序与网络的世界中,就必须找到一种可信任的公开方法,而且这种方法不能依赖加密来实现,否则又将陷入蛋鸡问题之中。
数字证书
当我们无法以“签名”的手段来达成信任时,就只能求助于其他途径。不妨想想真实的世界中,我们是如何达成信任的,其实不外乎以下两种:
- 基于共同私密信息的信任
譬如某个陌生号码找你,说是你的老同学,生病了要找你借钱。你能够信任他的方式是向对方询问一些你们两个应该知道,且只有你们两个知道的私密信息,如果对方能够回答上来,他有可能真的是你的老同学,否则他十有八九就是个诈骗犯。 - 基于权威公证人的信任
如果有个陌生人找你,说他是警察,让你把存款转到他们的安全账号上。你能够信任他的方式是去一趟公安局,如果公安局担保他确实是个警察,那他有可能真的是警察,否则他十有八九就是个诈骗犯。
回到网络世界中,我们并不能假设授权服务器和资源服务器是互相认识的,所以通常不太会采用第一种方式,而第二种就是目前标准的保证公钥可信分发的标准,这个标准有一个名字:公开密钥基础设施(Public Key Infrastructure,PKI)。
额外知识:公开密钥基础设施
又称公开密钥基础架构、公钥基础建设、公钥基础设施、公开密码匙基础建设或公钥基础架构,是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。
密码学上,公开密钥基础建设借着数字证书认证中心(Certificate Authority,CA)将用户的个人身份跟公开密钥链接在一起。对每个证书中心用户的身份必须是唯一的。链接关系通过注册和发布过程创建,根据担保级别的差异,创建过程可由 CA 的各种软件或者在人为监督下完成。PKI 的确定链接关系的这一角色称为注册管理中心(Registration Authority,RA)。RA 确保公开密钥和个人身份链接,可以防抵赖。
咱们不必纠缠于 PKI 概念上的内容,只要知道里面定义的“数字证书认证中心”相当于前面例子中“权威公证人”的角色,是负责发放和管理数字证书的权威机构即可。任何人包括你我都可以签发证书,只是不权威罢了。CA 作为受信任的第三方,承担公钥体系中公钥的合法性检验的责任。可是,这里和现实世界仍然有一些区别,现实世界你去找公安局,那大楼不大可能是剧场布景冒认的;而网络世界,在假设所有网络传输都有可能被截获冒认的前提下,“去 CA 中心进行认证”本身也是一种网络操作,这与之前的“去获取公钥”本质上不是没什么差别吗?其实还是有差别的,世间公钥成千上万不可枚举,而权威的 CA 中心则应是可数的,“可数”意味着可以不通过网络,而是在浏览器与操作系统出厂时就预置好,或者提前安装好(如银行的证书),图 5-15 是笔者机器上现存的根证书。
图 5-15 Windows 系统的 CA 证书
到这里出现了本节的主角之一:证书(Certificate),证书是权威 CA 中心对特定公钥信息的一种公证载体,也可以理解为是权威 CA 对特定公钥未被篡改的签名背书。由于客户的机器上已经预置了这些权威 CA 中心本身的证书(称为 CA 证书或者根证书),使得我们能够在不依靠网络的前提下,使用根证书里面的公钥信息对其所签发的证书中的签名进行确认。到此,终于打破了鸡生蛋、蛋生鸡的循环,使得整套数字签名体系有了坚实的逻辑基础。
PKI 中采用的证书格式是X.509 标准格式,它定义了证书中应该包含哪些信息,并描述了这些信息是如何编码的,里面最关键的就是认证机构的数字签名和公钥信息两项内容。一个数字证书具体包含以下内容:
版本号(Version):指出该证书使用了哪种版本的 X.509 标准(版本 1、版本 2 或是版本 3),版本号会影响证书中的一些特定信息,目前的版本为 3。
Version: 3 (0x2)
序列号(Serial Number): 由证书颁发者分配的本证书的唯一标识符。
Serial Number: 04:00:00:00:00:01:15:4b:5a:c3:94
签名算法标识符(Signature Algorithm ID):用于签发证书的算法标识,由对象标识符加上相关的参数组成,用于说明本证书所用的数字签名算法。譬如,SHA1 和 RSA 的对象标识符就用来说明该数字签名是利用 RSA 对 SHA1 的摘要结果进行加密。
Signature Algorithm: sha1WithRSAEncryption
认证机构的数字签名(Certificate Signature):这是使用证书发布者私钥生成的签名,以确保这个证书在发放之后没有被篡改过。
认证机构(Issuer Name): 证书颁发者的可识别名。
Issuer: C=BE, O=GlobalSign nv-sa, CN=GlobalSign Organization Validation CA - SHA256 - G2
有效期限(Validity Period): 证书起始日期和时间以及终止日期和时间,指明证书在这两个时间内有效。
Validity
Not Before: Nov 21 08:00:00 2020 GMT
Not After : Nov 22 07:59:59 2021 GMT
主题信息(Subject):证书持有人唯一的标识符(Distinguished Name),这个名字在整个互联网上应该是唯一的,通常使用的是网站的域名。
Subject: C=CN, ST=GuangDong, L=Zhuhai, O=Awosome-Fenix, CN=*.icyfenix.cn
公钥信息(Public-Key): 包括证书持有人的公钥、算法(指明密钥属于哪种密码系统)的标识符和其他相关的密钥参数。
传输安全层
到此为止,数字签名的安全性已经可以完全自洽了,但相信你大概也已经感受到了这条信任链的复杂与烦琐,如果从确定加密算法、生成密钥、公钥分发、CA 认证、核验公钥、签名、验证,每一个步骤都要由最终用户来完成的话,这种意义的“安全”估计只能一直是存于实验室中的阳春白雪。如何把这套烦琐的技术体系自动化地应用于无处不在的网络通信之中,便是本节的主题。
在计算机科学里,隔离复杂性的最有效手段(没有之一)就是分层,如果一层不够就再加一层,这点在网络中更是体现得淋漓尽致。OSI 模型、TCP/IP 模型将网络从物理特性(比特流)开始,逐层封装隔离,到了 HTTP 协议这种面向应用的协议里,使用者就已经不会去关心网卡/交换机如何处理数据帧、MAC 地址;不会去关心 ARP 如何做地址转换;不会去关心 IP 寻址、TCP 传输控制等细节。想要在网络世界中让用户无感知地实现安全通信,最合理的做法就是在传输层之上、应用层之下加入专门的安全层来实现,这样对上层原本基于 HTTP 的 Web 应用来说,影响甚至是无法察觉的。构建传输安全层这个想法,几乎可以说是和万维网的历史一样长,早在 1994 年,就已经有公司开始着手去实践了:
- 1994 年,网景(Netscape)公司开发了 SSL 协议(Secure Sockets Layer)的 1.0 版,这是构建传输安全层的起源,但是 SSL 1.0 从未正式对外发布过。
- 1995 年,Netscape 把 SSL 升级到 2.0 版,正式对外发布,但是刚刚发布不久就被发现有严重漏洞,所以并未大规模使用。
- 1996 年,修补好漏洞的 SSL 3.0 对外发布,这个版本得到了广泛的应用,很快成为 Web 网络安全层的事实标准。
- 1999 年,互联网标准化组织接替 Netscape,将 SSL 改名 TLS(Transport Layer Security)后形成了传输安全层的国际标准。第一个正式的版本是RFC 2246定义的 TLS 1.0,该版 TLS 的生命周期极长,直至笔者写下这段文字的 2020 年 3 月,主流浏览器(Chrome、Firefox、IE、Safari)才刚刚宣布同时停止 TLS 1.0/1.1 的支持。而讽刺的是,由于停止后许多政府网站无法被浏览,此时又正值新冠病毒(COVID-19)爆发期,Firefox 紧急发布公告宣布撤回该改动,TLS 1.0 的生命还在顽强延续。
- 2006 年,TLS 的第一个升级版 1.1 发布(RFC 4346),但却沦为了被遗忘的孩子,很少人使用 TLS 1.1,甚至到了 TLS 1.1 从来没有已知的协议漏洞被提出的程度。
- 2008 年,TLS 1.1 发布 2 年之后,TLS 1.2 标准发布(RFC 5246),迄今超过 90%的互联网 HTTPS 流量是由 TLS 1.2 所支持的,现在仍在使用的浏览器几乎都完美支持了该协议。
- 2018 年,最新的 TLS 1.3(RFC 8446)发布,比起前面版本相对温和的升级,TLS 1.3 做了出了一些激烈的改动,修改了从 1.0 起一直没有大变化的两轮四次(2-RTT)握手,首次连接仅需一轮(1-RTT)握手即可完成,在有连接复用支持时,甚至将 TLS 1.2 原本的 1-RTT 下降到了 0-RTT,显著提升了访问速度。
接下来,笔者以 TLS 1.2 为例,介绍传输安全层是如何保障所有信息都是第三方无法窃听(加密传输)、无法篡改(一旦篡改通信算法会立刻发现)、无法冒充(证书验证身份)的。TLS 1.2 在传输之前的握手过程一共需要进行上下两轮、共计四次通信,时序如图 5-16 所示。
图 5-16 TLS 连接握手时序
客户端请求:Client Hello
客户端向服务器请求进行加密通信,在这个请求里面,它会以明文的形式,向服务端提供以下信息。- 支持的协议版本,譬如 TLS 1.2。但是要注意,1.0 至 3.0 分别代表 SSL1.0 至 3.0,TLS1.0 则是 3.1,一直到 TLS1.3 的 3.4。
- 一个客户端生成的 32 Bytes 随机数,这个随机数将稍后用于产生加密的密钥。
- 一个可选的 SessionID,注意不要和前面 Cookie-Session 机制混淆了,这个 SessionID 是指传输安全层的 Session,是为了 TLS 的连接复用而设计的。
- 一系列支持的密码学算法套件,例如
TLS_RSA_WITH_AES_128_GCM_SHA256
,代表着密钥交换算法是 RSA,加密算法是 AES128-GCM,消息认证码算法是 SHA256 - 一系列支持的数据压缩算法。
- 其他可扩展的信息,为了保证协议的稳定,后续对协议的功能扩展大多都添加到这个变长结构中。譬如 TLS 1.0 中由于发送的数据并不包含服务器的域名地址,导致了一台服务器只能安装一张数字证书,这对虚拟主机来说就很不方便,所以 TLS 1.1 起就增加了名为“Server Name”的扩展信息,以便一台服务器给不同的站点安装不同的证书。
服务器回应:Server Hello
服务器接收到客户端的通信请求后,如果客户端声明支持的协议版本和加密算法组合与服务端相匹配的话,就向客户端发出回应。如果不匹配,将会返回一个握手失败的警告提示。这次回应同样以明文发送的,包括以下信息:- 服务端确认使用的 TLS 协议版本。
- 第二个 32 Bytes 的随机数,稍后用于产生加密的密钥。
- 一个 SessionID,以后可通过连接复用减少一轮握手。
- 服务端在列表中选定的密码学算法套件。
- 服务端在列表中选定的数据压缩方法。
- 其他可扩展的信息。
- 如果协商出的加密算法组合是依赖证书认证的,服务端还要发送出自己的 X.509 证书,而证书中的公钥是什么,也必须根据协商的加密算法组合来决定。
- 密钥协商消息,这部分内容对于不同密码学套件有着不同的价值,譬如对于 ECDH + anon 这样得密钥协商算法组合(基于椭圆曲线的ECDH 算法可以在双方通信都公开的情况下协商出一组只有通信双方知道的密钥)就不需要依赖证书中的公钥,而是通过 Server Key Exchange 消息协商出密钥。
客户端确认:Client Handshake Finished
由于密码学套件的组合复杂多样,这里仅以 RSA 算法为密钥交换算法为例介绍后续过程。
客户端收到服务器应答后,先要验证服务器的证书合法性。如果证书不是可信机构颁布的,或者证书中信息存在问题,譬如域名与实际域名不一致、或者证书已经过期、或通过在线证书状态协议得知证书已被吊销,等等,都会向访问者显示一个“证书不可信任”的警告,由用户自行选择是否还要继续通信。如果证书没有问题,客户端就会从证书中取出服务器的公钥,并向服务器发送以下信息:- 客户端证书(可选)。部分服务端并不是面向全公众,只对特定的客户端提供服务,此时客户端需要发送它自身的证书来证明身份。如果不发送,或者验证不通过,服务端可自行决定是否要继续握手,或者返回一个握手失败的信息。客户端需要证书的 TLS 通信也称为“双向 TLS”(Mutual TLS,常简写为 mTLS),这是云原生基础设施的主要认证方法,也是基于信道认证的最主流形式。
- 第三个 32 Bytes 的随机数,这个随机数不再是明文发送,而是以服务端传过来的公钥加密的,它被称为 PreMasterSecret,将与前两次发送的随机数一起,根据特定算法计算出 48 Bytes 的 MasterSecret ,这个 MasterSecret 即为后续内容传输时的对称加密算法所采用的私钥。
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供服务器校验。
服务端确认:Server Handshake Finished
服务端向客户端回应最后的确认通知,包括以下信息。- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的哈希值,以供客户端校验。
至此,整个 TLS 握手阶段宣告完成,一个安全的连接就已成功建立。每一个连接建立时,客户端和服务端均通过上面的握手过程协商出了许多信息,譬如一个只有双方才知道的随机产生的密钥、传输过程中要采用的对称加密算法(例子中的 AES128)、压缩算法等,此后该连接的通信将使用此密钥和加密算法进行加密、解密和压缩。这种处理方式对上层协议的功能上完全透明的,在传输性能上会有下降,但在功能上完全不会感知到有 TLS 的存在。建立在这层安全传输层之上的 HTTP 协议,就被称为“HTTP over SSL/TLS”,也即是大家所熟知的 HTTPS。
从上面握手协商的过程中我们还可以得知,HTTPS 并非不是只有“启用了 HTTPS”和“未启用 HTTPS”的差别,采用不同的协议版本、不同的密码学套件、证书是否有效、服务端/客户端对面对无效证书时的处理策略如何都导致了不同 HTTPS 站点的安全强度的不同,因此并不能说只要启用了 HTTPS 就必定能够安枕无忧。