HTTP 的一些缺点,其中的“无状态”在加入 Cookie 后得到了解决,而另两个缺点——“明文”和“不安全”仅凭 HTTP 自身是无力解决的,需要引入新的 HTTPS 协议。
由于 HTTP 天生“明文”的特点,整个传输过程完全透明,任何人都能够在链路中截获、修改或者伪造请求/响应报文,数据不具有可信性。
比如“代理服务”作为 HTTP 通信的中间人,在数据上下行的时候可以添加或删除部分头字段,也可以使用黑白名单过滤 body 里的关键字,甚至直接发送虚假的请求、响应,而浏览器和源服务器都没有办法判断报文的真伪。
如果通信过程具备了四个特性,就可以认为是“安全”的,这四个特性是:机密性、完整性,身份认证和不可否认。
HTTPS 其实是一个“非常简单”的协议,RFC 文档很小,只有短短的 7 页,里面规定了新的协议名“https”,默认端口号 443,其他的像请求——应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。除了协议名“http”和端口号 80 这两点不同,HTTPS 协议在语法、语义上和 HTTP 完全一样,优缺点也“照单全收”(当然要除去“明文”和“不安全”)。
HTTPS 与 HTTP 最大的区别,它能够鉴别危险的网站,并且尽最大可能保证上网安全,防御黑客对信息的窃听、篡改或者“钓鱼”、伪造。
HTTPS 做到机密性、完整性的原理就在于,把 HTTP 下层的传输协议由 TCP/IP
换成了 SSL/TLS
,由“HTTP over TCP/IP
”变成了“HTTP over SSL/TLS
”,让 HTTP 运行在了安全的 SSL/TLS
协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。
HTTPS 本身并没有什么本事,全是靠着后面的 SSL/TLS
“撑腰”。
SSL 即安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层)。
年份 | 协议版本 |
---|---|
1994 | SSLV3/v2 |
1999 | SSLV3.1 => TLS1.0 |
2006 | TLS1.1 |
2008 | TLS1.2 |
2018 | TLS1.3 |
每个新版本都紧跟密码学的发展和互联网的现状,持续强化安全和性能,已经成为了信息安全领域中的权威标准。
目前应用的最广泛的 TLS(传输层安全,Transport Layer Security) 是 1.2,而之前的协议(
TLS1.1/1.0
、SSLv3/v2
)都已经被认为是不安全的,各大浏览器在 2020 年左右停止支持。
TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。
浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信,这些算法的组合被称为“密码套件”(cipher suite,也叫加密套件)。TLS 的密码套件命名非常规范,格式很固定。基本的形式是“密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法”。
比如“ECDHE-RSA-AES256-GCM-SHA384
”密码套件的意思就是:握手时使用 ECDHE 算法进行密钥交换,用 RSA 签名和身份认证,握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM,摘要算法 SHA384 用于消息认证和产生随机数。”
在OpenSSL里的密码套件定义与TLS略有不同,TLS里的形式是“TLS_ECDHE_RSA_WITH_AES256_GCM_SHA384”,增加前缀并用WITH分开了握手和通信的算法。
除了HTTP,
SSL/TLS
也可以承载其他的应用协议,例如FTP=>FTPS,LDAP=>LDAPS等。
说到 TLS,就不能不谈到 OpenSSL,它是一个著名的开源密码学程序库和工具包,几乎支持所有公开的加密算法和协议,已经成为了事实上的标准,许多应用软件都会使用它作为底层库来实现 TLS 功能,包括常用的 Web 服务器 Apache、Nginx 等。
OpenSSL 是从另一个开源库 SSLeay 发展出来的,曾经考虑命名为“OpenTLS”,但当时(1998 年)TLS 还未正式确立,而 SSL 早已广为人知,所以最终使用了“OpenSSL”的名字。OpenSSL注明的“心脏出血”(Heart Bleed)漏洞,出现在
1.0.1
版本。
OpenSSL 目前有三个主要的分支,1.0.2
和 1.1.0
都在2019年底不再维护,最新的长期支持版本是 1.1.1
。
由于 OpenSSL 是开源的,所以它还有一些代码分支,比如 Google 的 BoringSSL、OpenBSD 的 LibreSSL,这些分支在 OpenSSL 的基础上删除了一些老旧代码,也增加了一些新特性,虽然背后有“大金主”,但离取代 OpenSSL 还差得很远。
Mozilla开发了另一个著名的开源密码库NSS(Network Security Services)。
实现机密性最常用的手段是“加密”(encrypt),把消息用某种方式转换成乱码,只有掌握特殊“钥匙”的人才能再转换出原始文本。
这里的“钥匙”就叫做“密钥”(key),加密前的消息叫“明文”(plain text/clear text),加密后的乱码叫“密文”(cipher text),使用密钥还原明文的过程叫“解密”(decrypt),是加密的反操作,加密解密的操作过程就是“加密算法”。
所有的加密算法都是公开的,任何人都可以去分析研究,而算法使用的“密钥”则必须保密。
由于 HTTPS、TLS 都运行在计算机上,所以“密钥”就是一长串的数字,但约定俗成的度量单位是“位”(bit),而不是“字节”(byte)。比如:
按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。
“对称加密”就是指加密和解密时使用密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。
TLS 里有非常多的对称加密算法,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。
对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文。
最早有 ECB、CBC、CFB、OFB 等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不用了。
最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data),在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。
把这些组合起来,就可以得到 TLS 密码套件中定义的对称加密算法。比如:
对称加密有一个很大的问题:如何把密钥安全地传递给对方,术语叫“密钥交换”。因为在对称加密算法中只要持有密钥就可以解密。
如果密钥在传递途中被黑客窃取,那他就可以在之后随意解密收发的数据,通信过程也就没有机密性可言了。
只用对称加密算法,是绝对无法解决密钥交换的问题的。所以,就出现了非对称加密(也叫公钥加密算法)。
它有两个密钥,一个叫“公钥”(public key),一个叫“私钥”(private key)。两个密钥是不同的,“不对称”,公钥可以公开给任何人使用,而私钥必须严格保密。
公钥和私钥有个特别的“单向”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。
非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。
非对称加密算法的设计要比对称算法难得多,在 TLS 里只有很少的几种,比如 DH、DSA、RSA、ECC 等。
10 年前 RSA 密钥的推荐长度是 1024,但随着计算机运算能力的提高,现在 1024 已经不安全,普遍认为至少要 2048 位。
目前比较常用的两个曲线是 P-256(secp256r1,在 OpenSSL 称为 prime256v1)和 x25519。
P-256 是 NIST(美国国家标准技术研究所)和 NSA(美国国家安全局)推荐使用的曲线,而 x25519 被认为是最安全、最快速的曲线。
比起 RSA,ECC 在安全强度和性能上都有明显的优势。
因为密钥短,所以相应的计算量、消耗的内存和带宽也就少,加密解密的性能就上去了,对于现在的移动互联网非常有吸引力。
虽然非对称加密没有“密钥交换”的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。
对比实验数据:
aes_128_cbc enc/dec 1000 times : 0.97ms, 13.11MB/s
rsa_1024 enc/dec 1000 times : 138.59ms, 93.80KB/s
rsa_1024/aes ratio = 143.17
rsa_2048 enc/dec 1000 times : 840.35ms, 15.47KB/s
rsa_2048/aes ratio = 868.13
RSA 的运算速度是非常慢的,2048 位的加解密大约是 15KB/S(微秒或毫秒级),而 AES128 则是 13MB/S(纳秒级),差了几百倍。
把对称加密和非对称加密结合起来,两者互相取长补短,即能高效地加密解密,又能安全地密钥交换。这就是现在 TLS 里使用的混合加密方式。
这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性。后面还有完整性、身份认证、不可否认等特性没有实现,所以现在的通信还不是绝对安全。
在机密性的基础上必须加上完整性、身份认证等特性,才能实现真正的安全。
实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)。
把摘要算法近似地理解成一种特殊的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
可以把摘要算法理解成特殊的“单向”加密算法,它只有算法,没有密钥,加密后的数据无法解密,不能从摘要逆推出原文。
摘要算法实际上是把数据从一个“大空间”映射到了“小空间”,所以就存在“冲突”(collision,也叫碰撞)的可能性,就如同现实中的指纹一样,可能会有两份不同的原文对应相同的摘要。好的摘要算法必须能够“抵抗冲突”,让这种可能性尽量地小。
因为摘要算法对输入具有“单向性”和“雪崩效应”,输入的微小不同会导致输出的剧烈变化,所以也被 TLS 用来生成伪随机数(PRF,pseudo random function)。
MD5(Message-Digest 5)、SHA-1(Secure Hash Algorithm 1),是最常用的两个摘要算法,能够生成 16 字节和 20 字节长度的数字摘要。但这两个算法的安全强度比较低,不够安全,在 TLS 里已经被禁止使用了。
目前 TLS 推荐使用SHA-2,它是一系列摘要算法的统称,总共有 6 种,常用的有 SHA224、SHA256、SHA384,分别能够生成 28 字节、32 字节、48 字节的摘要。
摘要算法保证了“数字摘要”和原文是完全等价的。所以,只要在原文后附上它的摘要,就能够保证数据的完整性。
不过摘要算法不具有机密性,如果明文传输,那么黑客可以修改消息后把摘要也一起改了,网站还是鉴别不出完整性。
所以,真正的完整性必须要建立在机密性之上,在混合加密系统里用会话密钥加密消息和摘要,这样黑客无法得知明文,也就没有办法动手脚了。这有个术语,叫哈希消息认证码(HMAC)。
加密算法结合摘要算法的通信过程可以说是比较安全了。但这里还有漏洞,就是通信的两个端点(endpoint)。
黑客可以伪装成网站来窃取信息,可以伪装成用户,向网站发送支付、转账等消息。
在 TLS 的非对称加密里的“私钥”能够在数字世界里证明身份,使用私钥再加上摘要算法,就能够实现“数字签名”,同时实现“身份认证”和“不可否认”。
数字签名的原理,就是把公钥私钥的用法反过来,之前是公钥加密、私钥解密,现在是私钥加密、公钥解密。因为非对称加密效率太低,所以私钥只加密原文的摘要,这样运算量就小的多,而且得到的数字签名也很小,方便保管和传输。
签名和公钥一样完全公开,任何人都可以获取。但这个签名只有用私钥对应的公钥才能解开,拿到摘要后,再比对原文验证完整性。
只要和网站互相交换公钥,就可以用“签名”和“验签”来确认消息的真实性,因为私钥保密,黑客不能伪造签名,就能够保证通信双方的身份。
比如,你用自己的私钥签名一个消息“我是小明”。网站收到后用你的公钥验签,确认身份没问题,于是也用它的私钥签名消息“我是某宝”。你收到后再用它的公钥验一下,也没问题,这样你和网站就都知道对方不是假冒的,后面就可以用混合加密进行安全通信了。
综合使用对称加密、非对称加密和摘要算法,实现了安全的四大特性,还有一个“公钥的信任”问题。
因为谁都可以发布公钥,我们还缺少防止黑客伪造公钥的手段,也就是说,怎么来判断这个公钥就是你或者某宝的公钥呢?
可以用类似密钥交换的方法来解决公钥认证问题,用别的私钥来给公钥签名,显然,这又会陷入“无穷递归”。
找一个公认的可信第三方,让它作为“信任的起点,递归的终点”,构建起公钥的信任链。这个“第三方”就是我们常说的 CA(Certificate Authority,证书认证机构),由它来给各个公钥签名,用自身的信誉来保证公钥无法伪造,是可信的。
CA 对公钥的签名认证也是有格式的,把序列号、用途、颁发者、有效时间等打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。
知名的 CA 全世界就那么几家,比如 DigiCert、VeriSign、Entrust、Let’s Encrypt(著名免费CA,只颁发DV证书) 等,它们签发的证书分 DV、OV、EV 三种,区别在于可信程度。
证书的格式遵循X509 v3标准,有两种编码方式,一种是二进制的DER,另一种是ASCII码的PEM。
CA 怎么证明自己,又是信任链的问题。小 CA 可以让大 CA 签名认证,但链条的最后 Root CA,就只能自己证明自己,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。
有了证书体系,操作系统和浏览器都内置了各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的。
证书体系(PKI,Public Key Infrastructure)虽然是目前整个网络世界的安全基础设施,但绝对的安全是不存在的,它也有弱点,还是关键的“信任”二字。
所以,需要再给证书体系打上一些补丁。
TLS 包含几个子协议,由几个不同职责的模块组成,比较常用的有记录协议、警报协议、握手协议、变更密码规范协议等。
protocol_version
就是不支持旧版本,bad_certificate
就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接。SYN/ACK
复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统。下面的这张图简要地描述了 TLS 的握手过程,其中每一个“框”都是一个记录,多个记录组合成一个 TCP 包发送。所以,最多经过两次消息往返(4 个消息)就可以完成握手,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。
在 TCP 建立连接之后,浏览器会首先发一个“Client Hello
”消息,也就是跟服务器“打招呼”。里面有客户端的版本号、支持的密码套件,还有一个随机数(Client Random),用于后续生成会话密钥。
Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Random: 1cbf803321fd2623408dfe…
Cipher Suites (17 suites)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
服务器收到“Client Hello
”后,会返回一个“Server Hello
”消息。把版本号对一下,也给出一个随机数(Server Random),然后从客户端的列表里选一个作为本次通信使用的密码套件,在这里它选择了“TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
”。
Handshake Protocol: Server Hello
Version: TLS 1.2 (0x0303)
Random: 0e6320f21bae50842e96…
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
然后,服务器为了证明自己的身份,就把证书也发给了客户端(Server Certificate)。
接下来是一个关键的操作,因为服务器选择了 ECDHE 算法,所以它会在证书后发送“Server Key Exchange
”消息,里面是公钥(Server Params),用来实现密钥交换算法,再加上自己的私钥签名认证。
Handshake Protocol: Server Key Exchange
EC Diffie-Hellman Server Params
Curve Type: named_curve (0x03)
Named Curve: x25519 (0x001d)
Pubkey: 3b39deaf00217894e...
Signature Algorithm: rsa_pkcs1_sha512 (0x0601)
Signature: 37141adac38ea4...
之后是“Server Hello Done
”消息。
这样第一个消息往返就结束了(两个 TCP 包),结果是客户端和服务器通过明文共享了三个信息:Client Random
、Server Random
和 Server Params
。
客户端这时也拿到了服务器的证书,这就要开始走证书链逐级验证,确认证书的真实性,再用证书公钥验证签名,就确认了服务器的身份。
然后,客户端按照密码套件的要求,也生成一个公钥(Client Params),用“Client Key Exchange
”消息发给服务器。
Handshake Protocol: Client Key Exchange
EC Diffie-Hellman Client Params
Pubkey: 8c674d0e08dc27b5eaa…
现在客户端和服务器手里都拿到了密钥交换算法的两个参数(Client Params
、Server Params
),就用 ECDHE
算法算出了“Pre-Master
”,也是一个随机数。
ECDHE
算法可以保证即使黑客截获了之前的参数,也是绝对算不出这个随机数的。
现在客户端和服务器手里有了三个随机数:Client Random
、Server Random
和 Pre-Master
。用这三个作为原始材料,就可以生成用于加密会话的主密钥,叫“Master Secret
”。
而黑客因为拿不到“Pre-Master”,所以也就得不到主密钥。
为了保证真正的“完全随机”“不可预测”,把三个不可靠的随机数混合起来,那么“随机”的程度就非常高了,足够让黑客难以猜测。
“Master Secret”的计算公式:
master_secret = PRF(pre_master_secret, "master secret", ClientHello.random + ServerHello.random)
“PRF”是伪随机数函数,它基于密码套件里的最后一个参数,通过摘要算法来再一次强化“Master Secret”的随机性。
主密钥有 48 字节,但它也不是最终用于通信的会话密钥,还会再用 PRF 扩展出更多的密钥,比如客户端发送用的会话密钥(client_write_key
)、服务器发送用的会话密钥(server_write_key
)等,避免只用一个密钥带来的安全隐患。
有了主密钥和派生的会话密钥,握手就快结束了。客户端发一个“Change Cipher Spec
”,然后再发一个“Finished
”消息,把之前所有发送的数据做个摘要,再加密一下,让服务器做个验证。
服务器也是同样的操作,发“Change Cipher Spec
”和“Finished
”消息,双方都验证加密解密 OK,握手正式结束,后面就收发被加密的 HTTP 请求和响应了。
上面的握手是如今主流的 TLS 握手过程,这与传统的握手有两点不同。
Server Key Exchange
”消息。Finished
”确认握手完毕,立即就发出 HTTP 报文,省去了一个消息往返的时间浪费。这个叫“TLS False Start
”和“TCP Fast Open
”有点像,都是不等连接完全建立就提前发应用数据,提高传输的效率。大体的流程没有变,只是“Pre-Master
”不再需要用算法生成,而是客户端直接生成随机数,然后用服务器的公钥加密,通过“Client Key Exchange
”消息发给服务器。服务器再用私钥解密,这样双方也实现了共享三个随机数,就可以生成主密钥。
上面过程是“单向认证”握手过程,只认证了服务器的身份,而没有认证客户端的身份。这是因为通常单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。
但为了防止账号、密码被盗,有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书,实现“双向认证”,这样会更加安全。
双向认证的流程也没有太多变化,只是在“Server Hello Done
”之后,“Client Key Exchange
”之前,客户端要发送“Client Certificate
”消息,服务器收到后也把证书链走一遍,验证客户端的身份。
TLS1.3 的三个主要改进目标:兼容、安全与性能。
由于 1.1、1.2 等协议已经出现了很多年,很多应用软件、中间代理(官方称为“MiddleBox”)只认老的记录协议格式,更新改造很困难,甚至是不可行(设备僵化)。
在早期的试验中发现,一旦变更了记录头字段里的版本号,也就是由 0x303(TLS1.2)改为 0x304(TLS1.3)的话,大量的代理服务器、网关都无法正确处理,最终导致 TLS 握手失败。
为了保证这些被广泛部署的“老设备”能够继续使用,避免新协议带来的“冲击”,TLS1.3 不得不做出妥协,保持现有的记录格式不变,通过“伪装”来实现兼容,使得 TLS1.3 看上去“像是”TLS1.2。
区分 1.2 和 1.3 用到一个新的扩展协议(Extension Protocol),通过在记录末尾添加一系列的“扩展字段”来增加新的功能,老版本的 TLS 不认识它可以直接忽略,这就实现了“后向兼容”。
在记录头的 Version 字段被兼容性“固定”的情况下,只要是 TLS1.3 协议,握手的“Hello”消息后面就必须有“supported_versions”扩展,它标记了 TLS 的版本号,使用它就能区分新旧协议。
Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Extension: supported_versions (len=11)
Supported Version: TLS 1.3 (0x0304)
Supported Version: TLS 1.2 (0x0303)
TLS1.3 利用扩展实现了许多重要的功能,比如“supported_groups
”“key_share
”“signature_algorithms
”“server_name
”等。
TLS1.2 在十来年的应用中获得了许多宝贵的经验,陆续发现了很多的漏洞和加密算法的弱点,所以 TLS1.3 就在协议里修补了这些不安全因素。比如:
TLS1.3 里只保留了:
现在的 TLS1.3 里只有 5 个套件,无论是客户端还是服务器都不会再犯“选择困难症”了。
废除 RSA 和 DH 密钥交换算法的原因,浏览器默认会使用 ECDHE 而不是 RSA 做密钥交换,这是因为它不具有“前向安全”(Forward Secrecy)。
假设有这么一个很有耐心的黑客,一直在长期收集混合加密系统收发的所有报文。如果加密系统使用服务器证书里的 RSA 做密钥交换,一旦私钥泄露或被破解(使用社会工程学或者巨型计算机),那么黑客就能够使用私钥解密出之前所有报文的“Pre-Master”,再算出会话密钥,破解所有密文。这就是所谓的“今日截获,明日破解”。而 ECDHE 算法在每次握手时都会生成一对临时的公钥和私钥,每次通信的密钥对都是不同的,也就是“一次一密”,即使黑客花大力气破解了这一次的会话密钥,也只是这次通信被攻击,之前的历史消息不会受到影响,仍然是安全的。
所以现在主流的服务器和浏览器在握手阶段都已经不再使用 RSA,改用 ECDHE,而 TLS1.3 在协议里明确废除 RSA 和 DH 则在标准层面保证了“前向安全”。
HTTPS 建立连接时除了要做 TCP 握手,还要做 TLS 握手,在 1.2 中会多花两个消息往返(2-RTT),可能导致几十毫秒甚至上百毫秒的延迟,在移动网络中延迟还会更严重。
现在因为密码套件大幅度简化,也就没有必要再像以前那样走复杂的协商流程了。TLS1.3 压缩了以前的“Hello”协商过程,删除了“Key Exchange
”消息,把握手时间减少到了“1-RTT”,效率提高了一倍。
具体的做法还是利用了扩展。
客户端在“Client Hello
”消息里直接用“supported_groups
”带上支持的曲线,比如 P-256、x25519,用“key_share
”带上曲线对应的客户端公钥参数,用“signature_algorithms
”带上签名算法。
服务器收到后在这些扩展里选定一个曲线和参数,再用“key_share
”扩展返回服务器这边的公钥参数,就实现了双方的密钥交换,后面的流程就和 1.2 基本一样了。
除了标准的“1-RTT”握手,TLS1.3 还引入了“0-RTT”握手,用“pre_shared_key
”和“early_data
”扩展,在 TCP 连接后立即就建立安全连接发送加密消息,不过这需要有一些前提条件。
HTTPS 连接大致上可以划分为两个部分,
目前流行的 AES、ChaCha20 性能都很好,还有硬件优化,报文传输的性能损耗小到几乎忽略不计。所以,通常所说的“HTTPS 连接慢”指的就是刚开始建立连接的那段时间。
在 TCP 建连之后,正式数据传输之前,HTTPS 比 HTTP 增加了一个 TLS 握手的步骤,这个步骤最长可以花费两个消息往返,也就是 2-RTT
。而且在握手消息的网络耗时之外,还会有其他的一些“隐形”消耗,比如:
在最差的情况下,也就是不做任何的优化措施,HTTPS 建立连接可能会比 HTTP 慢上几百毫秒甚至几秒,这其中既有网络耗时,也有计算耗时,就会让人产生“打开一个 HTTPS 网站好慢啊”的感觉。
现在已经有了很多行之有效的 HTTPS 优化手段,运用得好可以把连接的额外耗时降低到几十毫秒甚至是“零”。
把 TLS 握手过程中影响性能的部分都标记了出来,对照着它就可以“有的放矢”地来优化 HTTPS。
在计算机世界里的“优化”可以分成“硬件优化”和“软件优化”两种方式。
HTTPS 连接是计算密集型:
“SSL 加速卡”有一些缺点,比如升级慢、支持算法有限,不能灵活定制解决方案等。
第三种硬件加速方式:“SSL 加速服务器”,用专门的服务器集群来彻底“卸载”TLS 握手时的加密解密计算,性能自然要比单纯的“加速卡”要强大的多。
硬件优化方式中除了 CPU,其他的方式还要有一些开发适配工作,有一定的实施难度。
比如,“加速服务器”中关键的一点是通信必须是“异步”的,不能阻塞应用服务器,否则加速就没有意义了。所以,软件优化的方式相对来说更可行一些,性价比高。
软件方面的优化还可以再分成两部分:一个是软件升级,一个是协议优化。
软件升级实施起来比较简单,就是把现在正在使用的软件尽量升级到最新版本,比如把 Linux 内核由 2.x 升级到 4.x,把 Nginx 由 1.6 升级到 1.16,把 OpenSSL 由 1.0.1 升级到 1.1.0/1.1.1。
这些软件在更新版本的时候都会做性能优化、修复错误,只要运维能够主动配合,这种软件优化是最容易做的,也是最容易达成优化效果的。
从上面的 TLS 握手图中可以看到影响性能的一些环节,协议优化就要从这些方面着手。
在 Nginx 里可以用“ssl_ciphers”“ssl_ecdh_curve”等指令配置服务器使用的密码套件和椭圆曲线,把优先使用的放在前面,例如:
ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:EECDH+CHACHA20;
ssl_ecdh_curve X25519:P-256;
握手过程中的证书验证也是一个比较耗时的操作,服务器需要把自己的证书链全发给客户端,然后客户端接收后再逐一验证。这里就有两个优化点:
CRL(Certificate revocation list,证书吊销列表)由 CA 定期发布,里面是所有被撤销信任的证书序号,查询这个列表就可以知道证书是否有效。但 CRL 因为是“定期”发布,就有“时间窗口”的安全隐患,而且随着吊销证书的增多,列表会越来越大,一个 CRL 经常会上 MB。
每次需要预先下载几 M 的“无用数据”才能连接网站,实用性实在是太低了。所以,现在 CRL 基本上不用了,取而代之的是 OCSP(在线证书状态协议,Online Certificate Status Protocol),向 CA 发送查询请求,让 CA 返回证书的有效状态。
但 OCSP 也要多出一次网络请求的消耗,而且还依赖于 CA 服务器,如果 CA 服务器很忙,那响应延迟也是等不起的。于是又出来了一个“补丁”,叫“OCSP Stapling”(OCSP 装订),它可以让服务器预先访问 CA 获取 OCSP 响应,然后在握手时随着证书一起发给客户端,免去了客户端连接 CA 服务器查询的时间。
HTTPS 建立连接的过程:先是 TCP 三次握手,然后是 TLS 一次握手。之后一次握手的重点是算出主密钥“Master Secret”,而主密钥每次连接都要重新计算,如果能够把主密钥缓存一下“重用”,不就可以免去了握手和计算的成本了。
这种做法就叫“会话复用”(TLS session resumption),和 HTTP Cache 一样,也是提高 HTTPS 性能的“大杀器”,被浏览器和服务器广泛应用。
会话复用分两种(TLS1.3中删除了):
“Session ID”是最早出现的会话复用技术,也是应用最广的,但它也有缺点,服务器必须保存每一个客户端的会话数据,对于拥有百万、千万级别用户的网站来说存储量就成了大问题,加重了服务器的负担。
“Session Ticket”方案需要使用一个固定的密钥文件(ticket_key)来加密 Ticket,为了防止密钥被破解,保证“前向安全”,密钥文件需要定期轮换,比如设置为一小时或者一天。
“False Start”“Session ID”“Session Ticket”等方式只能实现 1-RTT,而 TLS1.3 更进一步实现了“0-RTT”,原理和“Session Ticket”差不多,但在发送 Ticket 的同时会带上应用数据(Early Data),免去了 1.2 里的服务器确认步骤,这种方式叫“Pre-shared Key”,简称为“PSK”。
但“PSK”也不是完美的,它为了追求效率而牺牲了一点安全性,容易受到“重放攻击”(Replay attack)的威胁。黑客可以截获“PSK”的数据,像复读机那样反复向服务器发送。
解决的办法是只允许安全的 GET/HEAD 方法,在消息里加入时间戳、“nonce”验证,或者“一次性票证”限制重放。
这些手段都逐渐“挤压”了纯明文 HTTP 的生存空间,HTTPS 的大潮无法阻挡,目前国内外的许多知名大站都已经实现了“全站 HTTPS”。
阻碍 HTTPS 实施的因素有三个比较流行的观点:“慢、贵、难”。
根据 Google 等公司的评估,在经过适当优化之后,HTTPS 的额外 CPU 成本小于 1%,额外的网络成本小于 2%,可以说是与无加密的 HTTP 相差无几。
要把网站从 HTTP 切换到 HTTPS,首先要做的就是为网站申请一张证书。
大型网站出于信誉、公司形象的考虑,通常会选择向传统的 CA 申请证书,例如 DigiCert、GlobalSign,而中小型网站完全可以选择使用“Let’s Encrypt”这样的免费证书,效果也完全不输于那些收费的证书。
“Let’s Encrypt”一直在推动证书的自动化部署,为此还实现了专门的 ACME 协议(RFC8555)。有很多的客户端软件可以完成申请、验证、下载、更新的“一条龙”操作,比如 Certbot
、acme.sh
等等,都可以在“Let’s Encrypt”网站上找到,用法很简单,相关的文档也很详细,几分钟就能完成申请。
注意事项。
可以在 crontab 里加个每周或每月任务,发送更新请求,不过很多 ACME 客户端会自动添加这样的定期任务,完全不用你操心。
配置 Web 服务器,在 443 端口上开启 HTTPS 服务了。
这在 Nginx 上非常简单,只要在“listen”指令后面加上参数“ssl”,再配上刚才的证书文件就可以实现最基本的 HTTPS。
为了提高 HTTPS 的安全系数和性能,你还可以强制 Nginx 只支持 TLS1.2 以上的协议,打开“Session Ticket”会话复用。
密码套件的选择方面,我给你的建议是以服务器的套件优先。这样可以避免恶意客户端故意选择较弱的套件、降低安全等级,然后密码套件向 TLS1.3“看齐”,只使用 ECDHE、AES 和 ChaCha20,支持“False Start”。
如果客户端硬件没有 AES 优化,服务器就会顺着客户端的意思,优先选择与 AES“等价”的 ChaCha20 算法,让客户端能够快一点。
配置 HTTPS 服务时还有一个“虚拟主机”的问题需要解决。
最早的时候每个 HTTPS 域名必须使用独立的 IP 地址,非常不方便。用 TLS 的“扩展”,给协议加个 SNI(Server Name Indication)的“补充条款”。它的作用和 Host 字段差不多,客户端会在“Client Hello
”时带上域名信息,这样服务器就可以根据名字而不是 IP 地址来选择证书。
SNI使用明文表示域名,也就是提前暴露了一部分HTTPS的信息,有安全隐患,容易被“中间人”发起拒绝攻击,被认为是TLS盔甲上的最后一个缝隙,目前正在起草ESNI规范。
Nginx 很早就基于 SNI 特性支持了 HTTPS 的虚拟主机,但在 OpenResty 里可还以编写 Lua 脚本,利用 Redis、MySQL 等数据库更灵活快速地加载证书。
有了 HTTPS 服务,原来的 HTTP 站点也不能马上弃用,很多用户习惯在地址栏里直接敲域名(或者是旧的书签、超链接),默认使用 HTTP 协议访问。
所以,需要用“重定向跳转”技术,把不安全的 HTTP 网址用 301 或 302“重定向”到新的 HTTPS 网站,这在 Nginx 里也很容易做到,使用“return”或“rewrite”都可以。但这种方式有两个问题。
“HSTS”(HTTP 严格传输安全,HTTP Strict Transport Security)的技术可以消除重定向的安全隐患。HTTPS 服务器需要在发出的响应头里添加一个“Strict-Transport-Security”
的字段,再设定一个有效期,例如:
Strict-Transport-Security: max-age=15768000; includeSubDomains
这相当于告诉浏览器:我这个网站必须严格使用 HTTPS 协议,在半年之内(182.5 天)都不允许用 HTTP,你以后就自己做转换吧,不要再来麻烦我了。
有了“HSTS”的指示,以后浏览器再访问同样的域名的时候就会自动把 URI 里的“http”改成“https”,直接访问安全的 HTTPS 网站。这样“中间人”就失去了攻击的机会,而且对于客户端来说也免去了一次跳转,加快了连接速度。
“HSTS”无法防止黑客对第一次访问的攻击,所有Chrome等楼兰器还内置了一个“HSTS preload”的列表(
chrome://net-internals/#hsts
),只要域名在这个列表里,无论何时都会强制使用HTTPS访问。