[HTTP 系列] 第 4 篇 —— HTTPS
这里是《写给前端工程师的 HTTP 系列》, 记得有位大佬曾经说过: 大厂前端面试对 HTTP 的要求比 CSS 还要高, 由此可见 HTTP 的重要程度不可小视. 本篇是该系列的第 4 篇 —— HTTPS.
- [HTTP 系列] 第 1 篇 —— 从 TCP/UDP 到 DNS 解析
- [HTTP 系列] 第 2 篇 —— HTTP 协议那些事
- [HTTP 系列] 第 3 篇 —— HTTP 缓存那些事
- [HTTP 系列] 第 4 篇 —— HTTPS
- [HTTP 系列] 第 5 篇 —— 网络安全
- [HTTP 系列] 第 6 篇 —— 从输入 URL 回车到页面呈现
HTTPS 安全四要素
由于 HTTP 天生明文传输的特点, 整个传输过程完全透明, 任何人都能够在链路中截获, 修改或者伪造请求或者响应报文, 数据不具有可信性. 通常认为, 如果通信过程具备了四个特性, 就可以认为是安全的, 这四个特性是: 机密性, 完整性, 身份认证和不可否认.
- 机密性(Secrecy/Confidentiality)是指对数据的保密, 只能由可信的人访问, 对其他人是不可见的秘密, 简单来说就是不能让不相关的人看到不该看的东西.
- 完整性(Integrity, 也叫一致性)是指数据在传输过程中没有被篡改, 不多也不少, 完完整整地保持着原状.
- 身份认证(Authentication)是指确认对方的真实身份, 也就是证明你真的是你, 保证消息只能发送给可信的人.
- 第四个特性是不可否认(Non-repudiation/Undeniable), 也叫不可抵赖, 意思是不能否认已经发生过的行为, 不能说话不算数, 耍赖皮.
对于 HTTPS, 机密性由对称加密保证, 完整性由摘要算法保证, 身份认证和不可否认由非对称加密保证.
什么是 HTTPS
HTTPS 的本质是把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS, 由HTTP over TCP/IP变成了HTTP over SSL/TLS, 让 HTTP 运行在了安全的 SSL/TLS 协议上, 收发报文不再使用 Socket API, 而是调用专门的安全接口.
SSL 即安全套接层(Secure Sockets Layer), 在 OSI 模型中处于第 5 层(会话层), 由网景公司于 1994 年发明, 有 v2 和 v3 两个版本, 而 v1 因为有严重的缺陷从未公开过. SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议, 于是互联网工程组 IETF 在 1999 年把它改名为 TLS(传输层安全, Transport Layer Security), 正式标准化, 目前应用的最广泛的 TLS 是 1.2.
TLS 由记录协议, 握手协议, 警告协议, 变更密码规范协议, 扩展协议等几个子协议组成, 综合使用了对称加密, 非对称加密, 身份认证等许多密码学前沿技术. 浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信, 这些算法的组合被称为密码套件(cipher suite, 也叫加密套件).
下面这个例子中可以看出, 使用的 TLS 是 1.2, 客户端和服务器都支持非常多的密码套件, 密码套件的格式为 密钥交换算法-签名算法-对称加密算法-分组模式-摘要算法, 下面这个例子协商选定的是 ECDHE-RSA-AES256-GCM-SHA384. 即握手时使用 ECDHE 算法进行密钥交换, 用 RSA 签名和身份认证, 握手后的通信使用 AES 对称算法, 密钥长度 256 位, 分组模式是 GCM, 摘要算法 SHA384 用于消息认证和产生随机数.
OpenSSL 是一个著名的开源密码学程序库和工具包, 几乎支持所有公开的加密算法和协议, 已经成为了事实上的标准, 许多应用软件都会使用它作为底层库来实现 TLS 功能, 包括常用的 Web 服务器 Apache, Nginx 等.
加密
实现机密性最常用的手段是加密(encrypt), 就是把消息用某种方式转换成谁也看不懂的乱码, 只有掌握特殊钥匙的人才能再转换出原始文本.
这里的钥匙就叫做密钥(key), 加密前的消息叫明文(plain text/clear text), 加密后的乱码叫密文(cipher text), 使用密钥还原明文的过程叫解密(decrypt), 是加密的反操作, 加密解密的操作过程就是加密算法. 所有的加密算法都是公开的, 任何人都可以去分析研究, 而算法使用的密钥则必须保密.
密钥就是一长串的数字, 但约定俗成的度量单位是位(bit), 而不是字节(byte). 比如, 说密钥长度是 128, 就是 16 字节的二进制串, 密钥长度 1024, 就是 128 字节的二进制串.
按照密钥的使用方式, 加密可以分为两大类: 对称加密和非对称加密.
对称加密
对称加密就是指加密和解密时使用的密钥都是同一个, 是对称的. 只要保证了密钥的安全, 明文通过该密钥加密, 也通过该密钥解密, 外人拿到的只是一段被加密的乱码, 那整个通信过程就可以说具有了机密性.
TLS 里有非常多的对称加密算法可供选择, 比如 RC4, DES, 3DES, AES, ChaCha20 等, 但前三种算法都被认为是不安全的, 通常都禁止使用, 目前常用的只有 AES 和 ChaCha20. AES 的意思是高级加密标准(Advanced Encryption Standard), 密钥长度可以是 128, 192 或 256. 它是 DES 算法的替代者, 安全强度很高, 性能也很好, 而且有的硬件还会做特殊优化, 所以非常流行, 是应用最广泛的对称加密算法. ChaCha20 是 Google 设计的另一种加密算法, 密钥长度固定为 256 位, 纯软件运行性能要超过 AES, 曾经在移动客户端上比较流行, 但 ARMv8 之后也加入了 AES 硬件优化, 所以现在不再具有明显的优势, 但仍然算得上是一个不错的算法.
对称算法还有一个**分组模式(Block cipher, 又称分块加密或块密码)**的概念, 它将明文分成多个等长的模块(block), 使用确定的算法和对称密钥对每组分别加密解密. 这是因为在实际加密中, 一般加密的数据不会只有几百 bit, 而是几 mb, 甚至几 gb. 这样, 加密过程就是每加密 128bit 接着再加密 128bit, 直至将全部数据加密完. 最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data), 在加密的同时增加了认证的功能, 常用的是 GCM, CCM 和 Poly1305.
把上面这些组合起来, 就可以得到 TLS 密码套件中定义的对称加密算法. 比如, AES128-GCM, 意思是密钥长度为 128 位的 AES 算法, 使用的分组模式是 GCM; ChaCha20-Poly1305 的意思是 ChaCha20 算法, 使用的分组模式是 Poly1305.
非对称加密
对称加密看上去好像完美地实现了机密性, 但其中有一个很大的问题: 如何把密钥安全地传递给对方, 术语叫密钥交换. 因为在对称加密算法中只要持有密钥就可以解密. 如果你和网站约定的密钥在传递途中被黑客窃取, 那他就可以在之后随意解密收发的数据, 通信过程也就没有机密性可言了.
因此就出现了非对称加密, 它有两个密钥, 一个叫公钥(public key), 一个叫私钥(private key). 两个密钥是不同的, 不对称, 公钥可以公开给任何人使用, 而私钥必须严格保密. 公钥和私钥有个特别的单向性, 虽然都可以用来加密解密, 但公钥加密后只能用私钥解密, 反过来, 私钥加密后也只能用公钥解密. 非对称加密可以解决密钥交换的问题. 网站秘密保管私钥, 在网上任意分发公钥, 你想要登录网站只要用公钥加密就行了, 密文只能由私钥持有者才能解密. 而黑客因为没有私钥, 所以就无法破解密文.
具体来讲:
- 客户端向服务器发起连接请求,获取服务器公钥和证书。
- 客户端将该证书发送到本地信任中心进行验证,确认证书的合法性。
- 客户端使用服务器公钥,对产生的随机会话密钥进行加密,发送给服务器。
- 服务器收到客户端发送的信息后,使用自己的私钥对信息进行解密,得到会话密钥。
- 服务器使用该会话密钥对数据进行加密和解密。
非对称加密算法要比对称加密算法复杂的多, 在 TLS 里只有很少的几种, 比如 DH, DSA, RSA, ECC 等.
RSA 算法
RSA 可能是其中最著名的一个, 几乎可以说是非对称加密的代名词, 它的安全性基于整数分解的数学难题, 使用两个超大素数的乘积作为生成密钥的材料, 想要从公钥推算出私钥是非常困难的. 以前 RSA 密钥推荐使用 1024 位, 但随着计算机运算能力的提高, 现在 1024 已经不安全, 普遍认为至少要 2048 位.
第一步选择两个大质数 p 和 q, p 不等于 q, 计算 N=p * q;
第二步是根据欧拉函数获取 r, 即 r = φ(N) = φ(p)φ(q) = (p-1)(q-1)
. 欧拉函数 φ(n) 的定义是小于或等于 n 的正整数中与 n 互质(如果两个或两个以上的整数的最大公约数是 1, 则称它们为互质)的数的数目.
举个例子, φ(3 * 5) = 15, 其互质数有 1, 2, 4, 7, 8, 11, 13, 14. 关于欧拉公式的推导可以看这篇文章.
ECC 算法
ECC(Elliptic Curve Cryptography)是非对称加密里的后起之秀, 它基于椭圆曲线离散对数的数学难题, 使用特定的曲线方程和基点生成公钥和私钥, 子算法 ECDHE 用于密钥交换, ECDSA 用于数字签名. 目前比较常用的两个曲线是 P-256(secp256r1, 在 OpenSSL 称为 prime256v1)和 x25519. P-256 是 NIST(美国国家标准技术研究所)和 NSA(美国国家安全局)推荐使用的曲线, 而 x25519 被认为是最安全, 最快速的曲线.
比起 RSA, ECC 在安全强度和性能上都有明显的优势. 160 位的 ECC 相当于 1024 位的 RSA, 而 224 位的 ECC 则相当于 2048 位的 RSA. 因为密钥短, 所以相应的计算量, 消耗的内存和带宽也就少, 加密解密的性能就上去了, 对于现在的移动互联网非常有吸引力.
如下图是两个椭圆曲线 y^2=x^3+7
, y^2=x^3-x
.
ECDHE 算法
先从 ECDHE 算法的名字说起. ECDHE 就是短暂 - 椭圆曲线 - 迪菲 - 赫尔曼算法(ephemeral Elliptic Curve Diffie–Hellman), 里面的关键字是短暂, 椭圆曲线和迪菲 - 赫尔曼, 我先来讲迪菲 - 赫尔曼, 也就是 DH 算法.
DH 算法是一种非对称加密算法, 只能用于密钥交换, 它的数学基础是离散对数(Discrete logarithm). 我们知道指数就是幂运算, 对数是指数的逆运算, 是已知底数和真数(幂结果), 反推出指数. 例如, 如果以 10 作为底数, 那么指数运算是 y=10x, 对数运算是 y=logx, 100 的对数是 2, 2 的对数是 0.301. 对数运算的域是实数, 取值是连续的, 而离散对数顾名思义, 取值是不连续的, 数值都是整数. 既然要取整, 那必然要涉及到模运算, 也就是取余数.
假设有模数 17, 底数 5, 而对数是 3, 那么有 53 % 17 = 6, 因此反过来, 以 5 为底, 17 为模数, 6 的离散对数就是 3. 这里的(17, 5)是离散对数的公共参数, 6 是真数, 3 是对数. 知道了对数, 就可以用幂运算很容易地得到真数, 但反过来, 知道真数却很难推断出对数, 于是就形成了一个单向函数.
知道了离散对数, 我们来看 DH 算法, 假设 Alice 和 Bob 约定使用 DH 算法来交换密钥.
Alice 和 Bob 需要首先确定模数和底数作为算法的参数, 这两个参数是公开的, 用 P 和 G 来代称, 简单起见我们还是用 17 和 5(P=17, G=5). 然后 Alice 和 Bob 各自选择一个随机整数作为私钥(必须在 1 和 P - 2 之间), 严格保密. 比如 Alice 选择 a = 10, Bob 选择 b = 5.
有了 DH 的私钥, Alice 和 Bob 再计算幂作为公钥, 也就是 A = (Ga % P) = 9, B = (Gb % P) = 14, 这里的 A 和 B 完全可以公开, 因为根据离散对数的原理, 从真数反向计算对数 a 和 b 是非常困难的.
交换 DH 公钥之后, Alice 手里有五个数:P=17, G=5, a=10, A=9, B=14, 然后执行一个运算:(Ba % P)= 8.
因为离散对数的幂运算有交换律, Ba = (Gb )a = (Ga)b = Ab, 所以 Bob 计算 Ab % P 也会得到同样的结果 8, 这个就是 Alice 和 Bob 之间的共享秘密, 可以作为会话密钥使用, 也就是 TLS 里的 Pre-Master.
整个通信过程中, Alice 和 Bob 公开了 4 个信息:P, G, A, B, 其中 P, G 是算法的参数, A 和 B 是公钥, 而 a, b 是各自秘密保管的私钥, 无法获取, 所以黑客只能从已知的 P, G, A, B 下手, 计算 9 或 14 的离散对数. 由离散对数的性质就可以知道, 如果 P 非常大, 那么他很难在短时间里破解出私钥 a, b, 所以 Alice 和 Bob 的通信是安全的.
DH 算法有两种实现形式, 一种是已经被废弃的 DH 算法, 也叫 static DH 算法, 另一种是现在常用的 DHE 算法(有时候也叫 EDH). static DH 交换密钥时就只有客户端的公钥会变, 而服务器公钥不变, 在长期通信时就增加了被破解的风险. 而 DHE 算法的关键在于E表示的临时性上(ephemeral), 每次交换密钥时双方的私钥都是随机选择, 临时生成的, 用完就扔掉, 下次通信不会再使用, 相当于一次一密, 具有前向安全.
ECDHE 算法, 就是把 DHE 算法里整数域的离散对数, 替换成了椭圆曲线上的离散对数. 也就是上面 ECC 算法那张图, 因为椭圆曲线离散对数的计算难度比普通的离散对数更大, 那破解起来就更加困难了.
混合加密
虽然非对称加密能解决密钥交换的安全问题, 但由于它们都是基于复杂的数学难题, 运算速度很慢, 而对称加密使用的是位运算, 相对就很快. 即使是 ECC 也要比 AES 差上好几个数量级. 这样虽然保证了安全, 但无法保证速度. 因此, 在实际应用中, TLS 里使用的混合加密方式来保障机密性.
即在通信刚开始的时候使用非对称算法, 比如 RSA, ECDHE, 首先解决密钥交换的问题.
然后用随机数产生对称算法使用的会话密钥(session key), 再用公钥加密. 因为会话密钥很短, 通常只有 16 字节或 32 字节, 所以慢一点也无所谓.
对方拿到密文后用私钥解密, 取出会话密钥. 这样, 双方就实现了对称密钥的安全交换, 后续就不再使用非对称加密, 全都使用对称加密. 这样混合加密就解决了对称加密算法的密钥交换问题, 而且安全和性能兼顾, 完美地实现了机密性.
摘要算法
混合加密保障了数据的机密性, 但仍然无法保障完整性, 因为虽然黑客无法破解机密数据, 但如果给加密后的数据添油加醋, 甚至篡改, 那么最终接收到的数据仍是残缺的. 因此需要一种机制来保障数据的完整性.
实现完整性的手段主要是摘要算法(Digest Algorithm), 也就是常说的散列函数, 哈希函数(Hash Function). 它能够把任意长度的数据压缩成固定长度, 而且独一无二的摘要字符串, 就好像是给这段数据生成了一个数字指纹. 摘要算法实际上是把数据从一个大空间映射到了小空间, 所以就存在冲突(collision, 也叫碰撞)的可能性, 即有两份或多份不同的原文对应相同的摘要. 好的摘要算法必须能够抵抗冲突, 让这种可能性尽量地小.
常见的摘要算法有 MD5(Message-Digest 5), SHA-1(Secure Hash Algorithm 1), 能够生成 16 字节和 20 字节长度的数字摘要. 但这两个算法的安全强度比较低, 不够安全, 在 TLS 里已经被禁止使用了. 目前 TLS 推荐使用的是 SHA-2. SHA-2 实际上是一系列摘要算法的统称, 总共有 6 种, 常用的有 SHA224, SHA256, SHA384, 分别能够生成 28 字节, 32 字节, 48 字节的摘要.
因为摘要算法对输入具有单向性和雪崩效应, 输入的微小不同会导致输出的剧烈变化, 所以也被 TLS 用来生成伪随机数(PRF, pseudo random function).
摘要算法保证了数字摘要和原文是完全等价的. 所以, 我们只要在原文后附上它的摘要, 就能够保证数据的完整性. 比如, 你发了条消息: 转账 1000 元, 然后再加上一个 SHA-2 的摘要. 网站收到后也计算一下消息的摘要, 把这两份指纹做个对比, 如果一致, 就说明消息是完整可信的, 没有被修改.
通过使用摘要算法来保障数据的完整性, 但如果是明文传输, 数据仍然是会被泄露的, 因此完整性必须要建立在机密性之上, 在混合加密系统里用会话密钥加密消息和摘要, 这样黑客无法得知明文, 也就没有办法动手脚了. 这里有个术语, 叫哈希消息认证码(HMAC). 它通过特别计算方式之后产生的消息认证码(MAC), 使用密码散列函数, 同时结合一个加密密钥. 它可以用来保证资料的完整性, 同时可以用来作某个消息的身份验证. 具体的算法可参考 HMAC.
数字签名
似乎加密算法结合摘要算法已经保障了数据的机密性和完整性, 但这里有个漏洞. 上面说的都是黑客可以伪装成网站来窃取你的信息, 但反过来, 黑客也可以冒充你向网站发送支付, 转账等消息, 网站没有办法确认你的身份, 钱可能就这么被偷走了. 现实生活中我们通过签名或印章, 来证明我是我. 这种只能由本人持有, 而其他任何人都不会有的便是私钥.
数字签名的原理其实很简单, 就是把公钥私钥的用法反过来, 之前是公钥加密, 私钥解密, 现在是私钥加密, 公钥解密. 但又因为非对称加密效率太低, 所以私钥只加密原文的摘要, 这样运算量就小的多, 而且得到的数字签名也很小, 方便保管和传输. 签名和公钥一样完全公开, 任何人都可以获取. 但这个签名只有用私钥对应的公钥才能解开, 拿到摘要后, 再比对原文验证完整性, 就可以像签署文件一样证明消息确实是你发的(这个工程的专业术语叫做验签). 这种使用私钥加密摘要的策略, 就能够实现数字签名, 同时实现身份认证和不可否认.
只要你和网站互相交换公钥, 就可以用签名和验签来确认消息的真实性, 因为私钥保密, 黑客不能伪造签名, 就能够保证通信双方的身份. 比如, 你用自己的私钥签名一个消息我是小明. 网站收到后用你的公钥验签, 确认身份没问题, 于是也用它的私钥签名消息我是某宝. 你收到后再用它的公钥验一下, 也没问题, 这样你和网站就都知道对方不是假冒的, 后面就可以用混合加密进行安全通信了.
数字证书和 CA
上面说道只要你和网站互相交换公钥, 就可以用签名和验签来确认消息的真实性. 不知道你是否有疑惑, 因为公钥是公开的, 谁都可以发布公钥, 我们还缺少防止黑客伪造公钥的手段, 也就是说, 怎么来判断这个公钥就是你或者某宝的公钥呢? 这就需要 CA(Certificate Authority, 证书认证机构)来帮助我们. 它具有极高的可信度, 由它来给各个公钥签名, 用自身的信誉来保证公钥无法伪造, 是可信的.
CA 对公钥的签名认证也是有格式的, 不是简单地把公钥绑定在持有者身份上就完事了, 还要包含序列号, 用途, 颁发者, 有效时间等等, 把这些打成一个包再签名, 完整地证明公钥关联的各种信息, 形成数字证书(Certificate).
证书根据等级分为 DV, OV, EV 三种:
类型/区别 | 审核内容 | 颁发周期 | 使用年限 | 浏览器显示形式 | 适用对象 | 价格 |
---|---|---|---|---|---|---|
DV | 域名所有权 | 几分钟-几小时 | 1-2 年 | https + 小锁标志 | 中小型企业网站, 电子商务网站, 电子邮局服务器, 个人网站等 | 免费 |
OV | 域名所有权; 企业信息; | 2-3 个工作日 | 1-2 年 | https + 小锁标志 | 企业网站, 电子商务网站, 证券, 金融机构等 | 2,982.40 元/年 * |
EV | 域名所有权; 企业信息; 第三方数据核查 | 5-7 个工作日 | 一 1-2 年年 | https + 小锁标志 + 绿色网址 + 企业名称 | 银行, 保险, 金融机构, 电子商务网站, 大型企业等 | 6,400 元/年 * |
上述价格节选自阿里云单域名每年的价格, 时间为 2022/03/23, 具体价格以实际购买时的价格为准.
不过, CA 怎么证明自己呢? 这还是信任链的问题. 小一点的 CA 可以让大 CA 签名认证, 但链条的最后, 也就是 Root CA, 就只能自己证明自己了, 这个就叫自签名证书(Self-Signed Certificate)或者根证书(Root Certificate). 你必须相信, 否则整个证书信任链就走不下去了.
有了这个证书体系, 操作系统和浏览器都内置了各大 CA 的根证书, 上网的时候只要服务器发过来它的证书, 就可以验证证书里的签名, 顺着证书链(Certificate Chain)一层层地验证, 直到找到根证书, 就能够确定证书是可信的, 从而里面的公钥也是可信的.
证书体系的弱点
证书体系(PKI, Public Key Infrastructure)虽然是目前整个网络世界的安全基础设施, 但绝对的安全是不存在的, 它也有弱点, 还是关键的信任二字. 如果 CA 失误或者被欺骗, 签发了错误的证书, 虽然证书是真的, 可它代表的网站却是假的. 还有一种更危险的情况, CA 被黑客攻陷, 或者 CA 有恶意, 因为它(即根证书)是信任的源头, 整个信任链里的所有证书也就都不可信了.
针对第一种, 开发出了 CRL(证书吊销列表, Certificate revocation list)和 OCSP(在线证书状态协议, Online Certificate Status Protocol), 及时废止有问题的证书. 对于第二种, 因为涉及的证书太多, 就只能操作系统或者浏览器从根上下狠手了, 撤销对 CA 的信任, 列入黑名单, 这样它颁发的所有证书就都会被认为是不安全的.
ECDHE 详细握手过程
在 HTTP 协议里, 通过三次握手建立连接后, 浏览器会立即发送请求报文. 但现在是 HTTPS 协议, 它需要再用另外一个握手过程, 在 TCP 上建立安全连接, 之后才是收发 HTTP 报文. 在讲 TLS 握手之前, 先简单介绍一下 TLS 协议的组成.
TLS 协议有多个模块组成, 比较常用的有记录协议, 警报协议, 握手协议, 变更密码规范协议等.
记录协议(Record Protocol)规定了 TLS 收发数据的基本单位: 记录(record). 它有点像是 TCP 里的 segment, 所有的其他子协议都需要通过记录协议发出. 但多个记录数据可以在一个 TCP 包里一次性发出, 也并不需要像 TCP 那样返回 ACK.
警报协议(Alert Protocol)的职责是向对方发出警报信息, 有点像是 HTTP 协议里的状态码. 比如, protocol_version 就是不支持旧版本, bad_certificate 就是证书有问题, 收到警报后另一方可以选择继续, 也可以立即终止连接.
握手协议(Handshake Protocol)是 TLS 里最复杂的子协议, 要比 TCP 的 SYN/ACK 复杂的多, 浏览器和服务器会在握手过程中协商 TLS 版本号, 随机数, 密码套件等信息, 然后交换证书和密钥参数, 最终双方协商得到会话密钥, 用于后续的混合加密系统.
变更密码规范协议(Change Cipher Spec Protocol), 它非常简单, 就是一个通知, 告诉对方, 后续的数据都将使用加密保护. 那么反过来, 在它之前, 数据都是明文的.
下面的这张图简要地描述了 TLS 的握手过程, 其中每一个框都是一个记录, 多个记录组合成一个 TCP 包发送. 所以, 最多经过两次消息往返(4 个消息)就可以完成握手, 然后就可以在安全的通信环境里发送 HTTP 报文, 实现 HTTPS 协议.
短暂椭圆曲线迪菲 - 赫尔曼密钥交换(Elliptic Curve Diffie - Hellman key exchange, 缩写为 ECDH), 是一种匿名的密钥合意协议(Key-agreement protocol), 这是迪菲 - 赫尔曼密钥交换的变种, 采用椭圆曲线密码学来加强性能与安全性. 在这个协定下, 双方利用由椭圆曲线密码学建立的公钥与私钥对, 在一个不安全的通道中, 建立起安全的共有加密资料. 临时 ECDH(ECDH Ephemeral, ECDHE)能够提供前向安全性.
阶段 1: Client Hello
在 TCP 建立连接之后, 浏览器会首先发一个 Client Hello 消息, 也就是跟服务器打招呼. 里面有客户端的版本号, 支持的密码套件, 还有一个随机数(Client Random), 用于后续生成会话密钥, 具体所有参数在下面. Client Hello 的目的就是: 我这边有这些这些信息, 你看看哪些是能用的, 关键的随机数可得留着.
- 支持的 SSL 版本
- 客户端生成的一个用于生成主密钥(master key)的 32 字节的随机数(主密钥由客户端和服务端的随机数共同生成)
- 会话 ID
- 加密套件
- 加密算法
- 密钥交换算法
- MAC 算法
- 加密方式(流, 分组)
- 压缩算法(由于压缩会带来安全漏洞(CRIME 攻击, Compression Ratio Info-leak Made Easy), 所以压缩算法这里一般写死成 null, 后面 TLS 1.3 明令禁止使用压缩. 简单说一下攻击原理: 它依赖于攻击者能观察浏览器发送的密文的大小, 并在同时诱导浏览器发起多个精心设计的到目标网站的连接. 攻击者会观察已压缩请求载荷的大小, 其中包括两个浏览器只发送到目标网站的私密 Cookie, 以及攻击者创建的变量内容. 当压缩内容的大小降低时, 攻击者可以推断注入内容的某些部分与源内容的某些部分匹配, 其中包括攻击者想要发掘的私密内容. 使用分治法技术可以用较小的尝试次数解读真正秘密的内容, 需要恢复的字节数会大幅降低)
下面是客户端支持的所有加密套件, 后面服务端可以选择其中的一个作为此次通信使用的加密套件算法.
阶段 2: Server Hello
服务器收到 Client Hello 后, 会返回一个 Server Hello 消息. 把版本号确认一下, 也给出一个随机数(Server Random), 需要客户端也留着, 然后从客户端的列表里选一个作为本次通信使用的密码套件.
- 服务端采纳的本次通讯的 SSL 版本
- 服务端生成的一个用于生成主密钥(master key)的 32 字节的随机数(主密钥由客户端和服务端的随机数共同生成), 它是由随机种子 gmt_unix_time 使用伪随机数函数(PRF)生成的 32 字节随机数.
- 会话 ID: 如果没有建立过连接则对应值为空, 不为空则说明之前建立过对应的连接并缓存.
- 服务端采纳的用于本次通讯的加密套件(从客户端发送的加密套件列表中选出了一个, 下面的例子选出的加密组合是 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ECDHE_RSA 作为密钥交换算法)
- 加密算法
- 密钥交换算法
- MAC 算法
- 加密方式(流, 分组)
- 压缩算法(如果支持压缩的话)
阶段 2: Server Certificate
SSL 服务器将携带自己公钥信息的数字证书和到根 CA 整个链发给客户端通过 Certificate 消息发送给 SSL 客户端(整个公钥文件都发送过去), 客户端使用这个公钥完成以下任务:
- 客户端可以使用该公钥来验证服务端的身份, 因为只有服务端有对应的私钥能解密它的公钥加密的数据;
- 用于对 premaster secret 进行加密, 这个 premaster secret 就是用客户端和服务端生成的 Random 随机数来生成的, 客户端用服务端的公钥对其进行了加密后发送给服务端.
下面这个例子, 我们访问的是百度, 它会给我们从百度的证书到根 CA 证书.
阶段 2: Server Key Exchange
下一步则是密钥交换阶段, Server Key Exchange 消息中包含有密钥交换算法所需要的额外参数, 它是一个可选步骤, 之所以说是可选步骤, 是因为只有在下列场景下这个步骤才会发生:
- 协商采用了 RSA 加密, 但是服务端的证书没有提供 RSA 公钥
- 协商采用了 DH 加密, 但是服务端的证书没有提供 DH 参数
- 协商采用了 fortezza_kea 加密, 但是服务端的证书没有提供参数
总结来说, Server Key Exchange这个步骤是对上一步 Certificate 的一个补充, 为了让整个 SSL 握手过程能正常进行.
阶段 2: Server Hello Done
SSL 服务器发送 Server Hello Done 消息, 通知 SSL 客户端版本和加密套件协商结束. 这样第一个消息往返就结束了(两个 TCP 包), 结果是客户端和服务器通过明文共享了三个信息: Client Random, Server Random 和 Server Params.
阶段 3: Client Key Exchange
客户端这时也拿到了服务器的证书, 开始走证书链逐级验证, 确认证书的真实性, 再用证书公钥验证签名, 就确认了服务器的身份. 利用证书中的公钥加密 SSL 客户端随机生成的 premaster secret(通过之前客户端, 服务端分别生成的随机数生成的), 并通过 Client Key Exchange 消息发送给 SSL 服务器. 注意, 这一步完成后, 客户端和服务端都已经保存了主密钥(之所以这里叫预备主密钥, 是因为还没有投入使用). 这个主密钥会用于之后的 SSL 通信数据的加密. master secret 的伪代码算法如下:
master_secret = PRF(
pre_master_secret,
"master secret",
ClientHello.random + ServerHello.random
);
阶段 3: Change Cipher Spec
客户端发送 Change Cipher Spec(密钥改变协议), 通知 SSL 服务器后续报文将采用协商好的主密钥和加密套件进行加密和 MAC 计算, 即以后咱们都用这个密钥进行通信数据的加密吧. 然后客户端会发送一个 Finished, 表示结束了.
阶段 3: Finished
SSL 客户端计算已交互的握手消息(除 Change Cipher Spec 消息外所有已交互的消息)的 Hash 值, 利用协商好的密钥和加密套件处理 Hash 值(计算并添加 MAC 值, 加密等), 并通过 Finished 消息发送给 SSL 服务器. SSL 服务器利用同样的方法计算已交互的握手消息的 Hash 值, 并与 Finished 消息的解密结果比较, 如果二者相同, 且 MAC 值验证成功, 则证明密钥和加密套件协商成功. 意思就是告诉服务器: 后面都改用对称算法加密通信了, 用的就是打招呼时说的 AES, 加密对不对还得你测一下.
阶段 4: 客户端发送请求
客户端使用主密钥加密数据, 发送给服务端.
阶段 4: Change Cipher Spec
同样地, SSL 服务器发送 Change Cipher Spec 消息, 通知 SSL 客户端后续报文将采用协商好的密钥和加密套件进行加密和 MAC 计算.
阶段 4: Finished
SSL 服务器计算已交互的握手消息的 Hash 值, 利用协商好的密钥和加密套件处理 Hash 值(计算并添加 MAC 值, 加密等), 并通过 Finished 消息发送给 SSL 客户端. SSL 客户端利用同样的方法计算已交互的握手消息的 Hash 值, 并与 Finished 消息的解密结果比较, 如果二者相同, 且 MAC 值验证成功, 则证明密钥和加密套件协商成功.
SSL 客户端接收到 SSL 服务器发送的 Finished 消息后, 如果解密成功, 则可以判断 SSL 服务器是数字证书的拥有者, 即 SSL 服务器身份验证成功, 因为只有拥有私钥的 SSL 服务器才能从 Client Key Exchange 消息中解密得到 premaster secret, 从而间接地实现了 SSL 客户端对 SSL 服务器的身份验证.
阶段 4: 服务端发送响应
最后就是服务端使用协商好的加密算法加密响应数据, 返回给客户端.
双向认证
上面说的是单向认证握手过程, 只认证了服务器的身份, 而没有认证客户端的身份. 这是因为通常单向认证通过后已经建立了安全通信, 用账号, 密码等简单的手段就能够确认用户的真实身份. 但为了防止账号, 密码被盗, 有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书, 实现双向认证, 这样会更加安全. 双向认证的流程也没有太多变化, 只是在Server Hello Done之后, Client Key Exchange之前, 客户端要发送Client Certificate消息, 服务器收到后也把证书链走一遍, 验证客户端的身份.
我记录了一份 gist, 可以看到双向认证的流程.
谈一谈 TLS 1.3
上面说的是都是基于 TLS 1.2, 它发布于 2008 年, 从现在来看在很多方面已经力不从心了. 因此经过近 30 个草案的反复打磨, TLS 1.3 于 2018 年发布, 再次确立了信息安全领域的新标准. TLS 1.3 的三个目标是兼容, 安全与性能.
兼容 1.1 和 1.2
由于 1.1, 1.2 等协议已经出现了很多年, 屎山已经改不动了, 因此必须要求 TLS 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)
更高的安全性
TLS 1.2 在十来年的应用中获得了许多宝贵的经验, 陆续发现了很多的漏洞和加密算法的弱点, 所以 TLS 1.3 就在协议里修补了这些不安全因素. 比如:
- 伪随机数函数由 PRF 升级为 HKDF(HMAC-based Extract-and-Expand Key Derivation Function);
- 明确禁止在记录协议里使用压缩;
- 废除了 RC4, DES 对称加密算法;
- 废除了 ECB, CBC 等传统分组模式;
- 废除了 MD5, SHA1, SHA-224 摘要算法;
- 废除了 RSA, DH 密钥交换算法和许多命名曲线.
这样 TLS1.3 里只保留了 AES, ChaCha20 对称加密算法, 分组模式只能用 AEAD 的 GCM, CCM 和 Poly1305, 摘要算法只能用 SHA256, SHA384, 密钥交换算法只有 ECDHE 和 DHE, 椭圆曲线也被砍到只剩 P-256 和 x25519 等 5 种. 基于此, TLS 1.3 的密码套件数量大幅减少:
这里还要特别说一下废除 RSA 和 DH 密钥交换算法的原因. 上面我们介绍到 ECDHE, 其实还有一种使用 RSA 来做密钥交换, 但是浏览器默认会使用 ECDHE 而不是 RSA 做密钥交换, 这是因为它不具有前向安全(Forward Secrecy). 这是因为 RSA 服务端的私钥是固定的, 一旦私钥被破解了, 加密就不安全了. 而 ECDHE 算法在每次握手时都会生成一对临时的公钥和私钥, 即便是某次被破解了, 也只会影响本次通信, 而不会影响后续的. 所以现在主流的服务器和浏览器在握手阶段都已经不再使用 RSA, 改用 ECDHE, 而 TLS1.3 在协议里明确废除 RSA 和 DH 则在标准层面保证了前向安全.
此外, TLS 1.3 还做了防恶意降级机制, 如果发现中间人恶意将版本降级到 1.2, 服务器的最后八个字节会被设置为 44 4F 57 4E 47 52 44 01, 即 DOWNGRD01, 支持 TLS 1.3 的客户端就会识别到, 然后发出报警.
性能的提升
HTTPS 建立连接时除了要做 TCP 握手, 还要做 TLS 握手, 在 TLS 1.2 中会多花两个消息往返(2-RTT), 可能导致几十毫秒甚至上百毫秒的延迟, 在移动网络中延迟还会更严重. 而 TLS 1.3 因为密码套件大幅度简化(只有 5 个), 也就没有必要再像以前那样走复杂的协商流程了.
此外, TLS1.3 压缩了以前的 Hello 协商过程, 删除了 Key Exchange 消息, 把握手时间减少到了 1-RTT, 效率提高了一倍. 具体的做法还是利用了扩展. 客户端在 Client Hello 消息里直接用 supported_groups 带上支持的曲线, 比如 P-256, x25519, 用key_share带上曲线对应的客户端公钥参数, 用 signature_algorithms 带上签名算法. 服务器收到后在这些扩展里选定一个曲线和参数, 再用 key_share 扩展返回服务器这边的公钥参数, 就实现了双方的密钥交换, 后面的流程就和 TLS 1.2 基本一样了. 如下是 TLS 1.3 握手的概略图.
除了标准的 1-RTT 握手, TLS1.3 还引入了 0-RTT 握手, 用 pre_shared_key 和 early_data 扩展, 在 TCP 连接后立即就建立安全连接发送加密消息.
TLS 1.3 握手分析
在 Client Hello 阶段, 除了跟 TLS 1.2 相同的部分, 还增加了 supported_versions(支持的版本), supported_groups(支持的曲线), key_share(曲线对应的参数) 等字段. 具体可以看
Handshake Protocol: Client Hello Version: TLS 1.2 (0x0303) Random: cebeb6c05403654d66c2329... Cipher Suites (18 suites) Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) Extension: supported_versions (len=9) Supported Version: TLS 1.3 (0x0304) Supported Version: TLS 1.2 (0x0303) Extension: supported_groups (len=14) Supported Groups (6 groups) Supported Group: x25519 (0x001d) Supported Group: secp256r1 (0x0017) Extension: key_share (len=107) Key Share extension Client Key Share Length: 105 Key Share Entry: Group: x25519 Key Share Entry: Group: secp256r1
Server Hello 阶段, 如果确认使用 TLS 1.3, supported_versions 会标明使用的是 TLS 1.3, 然后在 key_share 扩展带上曲线和对应的公钥参数.
Handshake Protocol: Server Hello Version: TLS 1.2 (0x0303) Random: 12d2bce6568b063d3dee2... Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) Extension: supported_versions (len=2) Supported Version: TLS 1.3 (0x0304) Extension: key_share (len=36) Key Share extension Key Share Entry: Group: x25519, Key Exchange length: 32
这时只交换了两条消息, 客户端和服务器就拿到了四个共享信息: Client Random 和 Server Random, Client Params 和 Server Params, 两边就可以各自用 ECDHE 算出 Pre-Master, 再用 HKDF 生成主密钥 Master Secret, 效率比 TLS 1.2 提高了一大截, 因为不需要在 cipher suite 协商了.
在算出主密钥后, 服务器立刻发出 Change Cipher Spec 消息, 比 TLS 1.2 提早进入加密通信, 后面的证书等就都是加密的了, 减少了握手时的明文信息泄露.
这里 TLS1.3 还有一个安全强化措施, 多了个 Certificate Verify 消息, 用服务器的私钥把前面的曲线, 套件, 参数等握手数据加了签名, 作用和 Finished 消息差不多. 但由于是私钥签名, 所以强化了身份认证和和防窜改.
这两个 Hello 消息之后, 客户端验证服务器证书, 再发 Finished 消息, 就正式完成了握手, 开始收发 HTTP 报文.
HTTPS 的优化
我们知道, HTTPS 连接大致上可以划分为两个部分, 第一个是建立连接时的非对称加密握手, 第二个是握手后的对称加密报文传输. 由于目前流行的 AES, ChaCha20 性能都很好, 还有硬件优化, 报文传输的性能损耗可以说是非常地小, 小到几乎可以忽略不计了. 所以, 通常所说的HTTPS 连接慢指的就是刚开始建立连接的那段时间.
在 TCP 建连之后, 正式数据传输之前, HTTPS 比 HTTP 增加了一个 TLS 握手的步骤, 这个步骤最长可以花费两个消息往返, 也就是 2-RTT. 而且在握手消息的网络耗时之外, 还会有其他的一些隐形消耗, 比如:
- 产生用于密钥交换的临时公私钥对(ECDHE);
- 验证证书时访问 CA 获取 CRL 或者 OCSP;
- 非对称加密解密处理Pre-Master.
在最差的情况下, 也就是不做任何的优化措施, HTTPS 建立连接可能会比 HTTP 慢上几百毫秒甚至几秒, 这其中既有网络耗时, 也有计算耗时. 现在已经有了很多行之有效的 HTTPS 优化手段, 运用得好可以把连接的额外耗时降低到几十毫秒甚至是零.
硬件加速
HTTPS 连接是计算密集型, 首先, 你可以选择更快的 CPU, 最好还内建 AES 优化, 这样即可以加速握手, 也可以加速传输.
其次, 你可以选择 SSL 加速卡, 加解密时调用它的 API, 让专门的硬件来做非对称加解密, 分担 CPU 的计算压力. 不过 SSL 加速卡也有一些缺点, 毕竟是固件, 就无法得到实时的升级.
所以, 就出现了第三种硬件加速方式: SSL 加速服务器, 用专门的服务器集群来彻底卸载 TLS 握手时的加密解密计算, 性能自然要比单纯的加速卡要强大的多.
软件加速
软件升级不必多说, 升级下 Nginx, OpenSSL 的版本, 一般都会有提升. 当然 TLS 协议最好就是升级成 TLS 1.3, 如有历史遗留问题暂时只能用 TLS 1.2, 那么握手时使用的密钥交换协议应当尽量选用椭圆曲线的 ECDHE 算法. 它不仅运算速度快, 安全性高, 还支持 False Start(在 TLS 协商第二阶段, 浏览器发送 ChangeCipherSpec 和 Finished 后, 立即发送加密的应用层数据, 而无需等待服务器端的确认), 能够把握手的消息往返由 2-RTT 减少到 1-RTT, 达到与 TLS 1.3 类似的效果. 另外, 椭圆曲线也要选择高性能的曲线, 最好是 x25519, 次优选择是 P-256. 对称加密算法方面, 也可以选用 AES_128_GCM, 它能比 AES_256_GCM 略快一点点.
在 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;
证书优化
除了密钥交换, 握手过程中的证书验证也是一个比较耗时的操作, 服务器需要把自己的证书链全发给客户端, 然后客户端接收后再逐一验证. 这里就有两个优化点, 一个是证书传输, 一个是证书验证.
首先, 服务器的证书应该选择椭圆曲线(ECDSA)证书而不是 RSA 证书, 因为 224 位的 ECC 相当于 2048 位的 RSA, 这即能够节约带宽也能减少客户端的运算量.
客户端的证书验证其实是个很复杂的操作, 除了要公钥解密验证多个证书签名外, 因为证书还有可能会被撤销失效, 客户端有时还会再去访问 CA, 下载 CRL(Certificate revocation list, 证书吊销列表, 由 CA 定期发布, 里面是所有被撤销信任的证书序号, 查询这个列表就可以知道证书是否有效) 或者 OCSP(在线证书状态协议, Online Certificate Status Protocol), 这又会产生 DNS 查询, 建立连接, 收发数据等一系列网络通信, 增加好几个 RTT.
由于 CRL 因为是定期发布, 就有时间窗口的安全隐患, 而且随着吊销证书的增多, 列表会越来越大, 一个 CRL 经常会上 MB. 所以每次下载这么一个大的 map 实在性能低下. 因此现在已经不使用 CRL 了, 都使用 OCSP, CA 发送查询请求, 让 CA 返回证书的有效状态. 但 OCSP 也要多出一次网络请求的消耗, 而且还依赖于 CA 服务器, 如果 CA 服务器很忙, 那响应延迟也是等不起的.
于是就有了 OCSP Stapling, 它可以让服务器预先访问 CA 获取 OCSP 响应, 然后在握手时随着证书一起发给客户端, 免去了客户端连接 CA 服务器查询的时间, 有种 prefetch 內味了.
会话复用与会话票证
TLS 握手的重点是算出主密钥 Master Secret , 而主密钥每次连接都要重新计算, 未免有点太浪费了, 因此, 复用主密钥缓存的做法就叫会话复用(TLS session resumption), 和 HTTP Cache 一样, 也是提高 HTTPS 性能的大杀器, 被浏览器和服务器广泛应用.
会话复用分两种, 第一种叫 Session ID, 就是客户端和服务器首次连接后各自保存一个会话的 ID 号, 内存里存储主密钥和其他相关的信息. 当客户端再次连接时发一个 ID 过来, 服务器就在内存里找, 找到就直接用主密钥恢复会话状态, 跳过证书验证和密钥交换, 只用一个消息往返就可以建立安全通信.
Session ID 是最早出现的会话复用技术, 也是应用最广的, 但它也有缺点, 服务器必须保存每一个客户端的会话数据, 对于拥有百万, 千万级别用户的网站来说存储量就成了大问题, 加重了服务器的负担. 于是, 又出现了第二种 Session Ticket 方案. 它有点类似 HTTP 的 Cookie, 存储的责任由服务器转移到了客户端, 服务器加密会话信息, 用 New Session Ticket 消息发给客户端, 让客户端保存. 重连的时候, 客户端使用扩展 session_ticket 发送 Ticket 而不是 Session ID, 服务器解密后验证有效期, 就可以恢复会话, 开始加密通信. 不过 Session Ticket 方案需要使用一个固定的密钥文件(ticket_key)来加密 Ticket, 为了防止密钥被破解, 保证前向安全, 密钥文件需要定期轮换, 比如设置为一小时或者一天.
预共享密钥
预共享密钥原理和 Session Ticket 差不多, 但在发送 Ticket 的同时会带上应用数据(Early Data), 免去了 1.2 里的服务器确认步骤, 这种方式叫 Pre-shared Key, 简称为 PSK. 但 PSK 也不是完美的, 它为了追求效率而牺牲了一点安全性, 容易受到重放攻击(Replay attack)的威胁. 黑客可以截获 PSK 的数据, 像复读机那样反复向服务器发送. 解决的办法是只允许安全的 GET/HEAD 方法, 在消息里加入时间戳, nonce 验证, 或者一次性票证限制重放. 注意, 在 TLS 1.3 废除了 Session ID 和 Session Ticket 两种方案, 只保留了 Pre-shared Key.
附录: 配置 HTTPS
以 nginx 为例, 监听 443 端口, 并提供证书的路径.
listen 443 ssl; ssl_certificate xxx_rsa.crt; # rsa2048 cert ssl_certificate_key xxx_rsa.key; # rsa2048 private key ssl_certificate xxx_ecc.crt; # ecdsa cert ssl_certificate_key xxx_ecc.key; # ecdsa private ke
为了提高 HTTPS 的安全系数和性能, 还可以强制 Nginx 只支持 TLS1.2 以上的协议, 打开 Session Ticket 会话复用:
ssl_protocols TLSv1.2 TLSv1.3; ssl_session_timeout 5m; ssl_session_tickets on; ssl_session_ticket_key ticket.key;
密码套件的选择方面, 建议是以服务器的套件优先. 这样可以避免恶意客户端故意选择较弱的套件, 降低安全等级, 然后密码套件向 TLS1.3看齐, 只使用 ECDHE, AES 和 ChaCha20, 支持 False Start. 如果客户端硬件没有 AES 优化, 服务器就会顺着客户端的意思, 优先选择与 AES 等价的 ChaCha20 算法, 让客户端能够快一点. 可以通过 SSL Server Test 来测试你的服务器 SSL 的安全性.
ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:!MD5:!SHA1; # 如果你的服务器上使用了 OpenSSL 的分支 BorringSSL, # 那么还可以使用一个特殊的**等价密码组**(Equal preference cipher groups)特性, # 它可以让服务器配置一组**等价**的密码套件, 在这些套件里允许客户端优先选择 ssl_ciphers [ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305];
SNI
在 HTTP 协议里, 多个域名可以同时在一个 IP 地址上运行, 这就是虚拟主机, Web 服务器会使用请求头里的 Host 字段来选择. 但在 HTTPS 里, 因为请求头只有在 TLS 握手之后才能发送, 在握手时就必须选择虚拟主机对应的证书, TLS 无法得知域名的信息, 就只能用 IP 地址来区分. 所以, 最早的时候每个 HTTPS 域名必须使用独立的 IP 地址, 非常不方便.
这需要用到 TLS 的扩展, 给协议加个 SNI(Server Name Indication)的补充条款. 它的作用和 Host 字段差不多, 客户端会在 Client Hello 时带上域名信息, 这样服务器就可以根据名字而不是 IP 地址来选择证书.
重定向跳转
通过重定向跳转技术, 把不安全的 HTTP 网址用 301 或 302 重定向到新的 HTTPS 网站, 这在 Nginx 里也很容易做到, 使用 return 或 rewrite 都可以.
return 301 https://$host$request_uri; #永久重定向 rewrite ^ https://$host$request_uri permanent; #永久重定向
但这种方式有两个问题. 一个是重定向增加了网络成本, 多出了一次请求;另一个是存在安全隐患, 重定向的响应可能会被中间人窜改, 实现会话劫持, 跳转到恶意网站.
不过有一种叫HSTS(HTTP 严格传输安全, HTTP Strict Transport Security)的技术可以消除这种安全隐患. HTTPS 服务器需要在发出的响应头里添加一个Strict-Transport-Security的字段, 再设定一个有效期, 例如:
add_header Strict-Transport-Security max-age=15768000; #182.5days
这相当于告诉浏览器: 我这个网站必须严格使用 HTTPS 协议, 在半年之内(182.5 天)都不允许用 HTTP, 你以后就自己做转换吧, 不要再来麻烦我了. 有了HSTS的指示, 以后浏览器再访问同样的域名的时候就会自动把 URI 里的http改成https, 直接访问安全的 HTTPS 网站. 这样中间人就失去了攻击的机会, 而且对于客户端来说也免去了一次跳转, 加快了连接速度.
小结
TLS 通过以下几点来保证机密性, 完整性, 身份认证和不可否认.
- 摘要算法用来实现完整性, 能够为数据生成独一无二的指纹, 常用的算法是 SHA-2;
- 数字签名是私钥对摘要的加密, 可以由公钥解密后验证, 实现身份认证和不可否认;
- 公钥的分发需要使用数字证书, 必须由 CA 的信任链来验证, 否则就是不可信的;
- 作为信任链的源头 CA 有时也会不可信, 解决办法有 CRL, OCSP, 还有终止信任.
TLS 握手小结:
- HTTPS 协议会先与服务器执行 TCP 握手, 然后执行 TLS 握手, 才能建立安全连接;
- 握手的目标是安全地交换对称密钥, 需要三个随机数, 第三个随机数Pre-Master必须加密传输, 绝对不能让黑客破解;
- Hello消息交换随机数, Key Exchange消息交换Pre-Master;
- Change Cipher Spec之前传输的都是明文, 之后都是对称密钥加密的密文.
对 TLS 1.2 已知的攻击有 BEAST, BREACH, CRIME, TREAK, LUCKY13, POODLE, ROBOT.
PREVIOUS POST
《JS 核心原理解析》笔记
NEXT POST
[HTTP 系列] 第 6 篇 —— 从输入 URL 回车到页面呈现