Total Pageviews

Tuesday 10 December 2019

谈谈HTTPS和ssl证书

为什么要使用 HTTPS?HTTPS 提供的不只是加密功能。事实上,许多 Web 新技术,如 HTTP/2Service Worker 等都需要依托 HTTPS 才能实现,相信这也会成为未来的趋势。

什么是 SSL?

从网络模型来看,SSL 在 HTTP 应用层之下,TCP 传输层之上提供了一层传输安全层,确保了经过传输层的数据都是经过加密的。
SSL 作为 HTTPS 的基础最早由网景于 1994 年提出。虽然 HTTP 协议本身提供了基本认证和摘要认证(后来提出)两种客户端认证方案,但是毕竟除了用户名和密码外的所有数据仍然是明文传输的(甚至基本认证只对密码信息做了 Base64 编码),要保证传输的安全性还得依靠 SSL。SSL 本身是一种二进制协议,避免了随随便便一个代理就能读取/修改数据的问题。
随着技术发展,SSL 本身也在不断改进。从 SSL 1.0、SSL 2.0 到 SSL 3.0,再到标准化后的 TLS(目前 TLS 1.3 刚刚定稿),这个协议本身也在完善当中。在本文中,我们按照一般惯例使用 SSL 一词同时指代 SSL 和 TLS 两种协议。

翻开历史书

加密方法离不开数学基础,为了更好地解释加密原理,我们先来了解一下加密的基础知识。当然,如果已经了解了就直接跳过吧。
人类使用加密已经有几千年的历史了。最初,人们使用特定的方法处理文本使其在被截获之后也不会泄露信息。我们通常把这种特定的方法叫做密码。传说凯撒曾使用过一种三位位移密码,也就是将文本中的所有字母替换为这个字母在字母表中后三位的字母。这种方法是一种密码。也就是说,密码 C 接受明文 R 一个参数,输出密文 E。

然而,使用单一密码的加密非常容易被破译,而且一旦被破译,以往和未来的所有密文也不再安全。为了解决这个问题,人们提出了新的方法也就是密钥。用同一种密码加密,输入不同的密钥就会产生不同的结果。比如上文中的三位位移密码,如果把 3 当做密钥,那么不同的密钥(1,2,3,4,5…)就会产生不同的输出。这种时候,就算密码被破译,只要密钥的可能值足够多,要得到明文还是相当困难的。也就是,密码 C 现在接受明文 R 和密钥 K 两个参数,产生密文 E。不同的 R 和 K 组合会产生不同的 E。
然而这种方法仍然有问题。密文的接收方要解密得到明文,必须要得知密码和密钥两个信息。通常密码和密钥组合是变化的(不然就和没有密钥一样了),要正确解密,发送方和接收方每次都必须要交换密码和密钥信息,通常是只交换密钥而密码不变。但密钥存在被截获的可能,要确保安全必须对密钥进行加密,然后加密密钥也需要交换密钥,然后第二个密钥也需要加密…很快这就变成了一个鸡生蛋问题,很明显是不可能实现的。

到目前为止,我们讨论的都是对称加密,也就是加密和解密用的是相同的密码和密钥。对于无限加密的问题,使用对称加密是无解了。此时,我们的救星非对称加密就登场了。
非对称加密可以使用不同的密钥来分别加密和解密文本,它们通常是配对的,我们叫它们公钥和私钥。使用相同的密码时,接受方只需生成一对公钥和私钥,然后保管好自己的私钥并公开公钥。发送方只需要用商定的密码和接收方的公钥作为密钥来加密文本即可。加密后的文本只有使用私钥才能解开,公钥本身是无法解开的。也就是说,密码 C 对于明文 R 和公钥 PK 产生的输出只能由与密码 C 对应的解码 D 和公钥 PK 对应的私钥 SK 才能解密回明文。
这里就不详述数学上的证明了,有兴趣的可以自行了解。
通过这种方式,接收方可以保证密文只能由接受方进行解码,交换公钥并不会破坏加密的安全性。更棒的是,私钥和公钥是可以互换的,即是,使用公钥 PK 加密并使用私钥 SK 解密得到的结果和使用私钥 SK 加密并使用公钥 PK 解密得到的结果是一致的,都是原始明文。
这样的话,我们甚至可以使用非对称加密验证接收方的身份。毕竟只有接收方拥有私钥,可以要求接收方用私钥加密明文并试图用其公钥解密来确保对方就是拥有正确私钥的那一方。如此看来,有了非对称加密,似乎一切都完美了。

等等!

你可能会发现一些问题。如果有一个聪明的中间人(或者说网络中的某一台代理),他可能可以轻松破解非对称加密。
对的!试想以下的场景: A 和 B
作为两位优秀的魔法少女常常需要互通信件,然而不巧邪恶的 E 同学总是试图偷看 A 和 B 的对话,于是 A 和 B 决定使用非对称加密来加密信件以解决问题。
然而 E 同学想出了一个办法。A 向 B 发送信件时,B 会先向 A 发送她的公钥以便 A 加密文本,这时 E 将其截获,记下 B 的公钥然后自己生成一套公钥和私钥并将自己的公钥伪装成 B 的信件重新发送给 A。不知情的 A 会用 E 的公钥加密文本并发回给 B。E 可以再次截获信件,此时他利用自己的私钥便可以轻松读取信中的内容了。然后 E 将信件重新使用 B 的公钥加密并发送给 B,这样 B 也不会发现异常,认为自己成功与 A 完成了一次沟通,殊不知 E 已经读取到了信的内容。当 B 要向 A 寄信时,E 可以如法炮制读取 A 和 B 的对话。
怎么办才能避免 E 读取内容呢?幸好,我们有一种方法可以在 E 偷偷摸摸更换密钥时就发现问题并及时停止。要使用这种方法,我们需要有请我们的下一位选手登场。

欢迎,公正的第三方

现在,作为第三方的 C 登场了。他会作为一个验证人帮助信件的接收方验证收到的信件有没有经过篡改以及是不是真的由 B 发出的。
整个过程这样进行。C 先生成一对公私钥,然后将公钥亲自告诉 A 和 B。A 发信给 B 时,C 会提前把 B 的公钥和一些 B 和 C 的基本信息放在一起,然后用自己的私钥加密这段信息并将密文交给 B。B 随后只需将这段密文交给 A 即可。A 随后会试着使用 C 的公钥解开密文,如果成功了,那就说明信件没有经过篡改,并且 A 也安全地获得了 B 的公钥。如果失败了,就说明有人篡改了这段密文,信件即作废。当 B 给 A 寄信时将整个过程反过来即可。看,这样我们就可以安全的交换密钥而不用担心内容被窃听了。可怜的 E 现在束手无策。

说好的 HTTPS 去哪了?

别激动,其实 A、B、C、E 的故事就是一次典型的 HTTPS 连接。
慢慢来,实际情况会比上面的故事稍稍复杂一些。我们把 A 看作客户端,通常情况下也就是浏览器,B 看作服务器,E 看作网络链路上某台邪恶的代理,C 是一位公正的第三方,顺便,我们把 C 交给 B 的那段含有一些基本信息和 B 的公钥的密文叫做数字证书。现在,我们可以把 C 叫做证书颁发机构。
在真实情况下,数字证书会包含一些加密的基本信息。毕竟数字证书并没有统一的标准,这些加密信息会用来告诉 A 如何解密数字证书。当然这些信息不会被加密,不然 A 完全无法解密数字证书。因此现实情况下,C 会对数字证书包含的所有内容建立一个摘要(你可以把它当做简介),然后只对这个摘要用自己的私钥进行加密。这个过程叫做签名。当 A 收到证书后,会解密这段摘要,然后自己通过相同的算法独立算出证书的摘要——如果是一样的,那就没问题了。
那么现在开始连接。A 向 B 发出了一个请求。在建立连接后,B 会将数字证书发送给 A。A 会验证数字证书(甚至会询问 C 确保这张证书没有作废)。如果成功,A 会利用从证书中取得的 B 的公钥在加密环境下协商出一个一致的临时密钥,接下来双方会用这个密钥进行对称加密来互相交流。
等等!在这种情况下,B 没有 A 的公钥,他是怎么回答 A 来完成临时密钥的协商的呢?啊哈!实际上 B 并不需要回复 A 来完成协商。在整个过程中,只需要 A 向 B 发送一条加密信息,双方即可计算出相同的临时密钥。至于技术细节,我们稍后就谈。
那为什么需要临时密钥呢?那是因为非对称加密需要的计算量远大于对称加密。只需要保证密钥不泄露,对称加密是安全的。利用非对称加密来交换对称加密密钥完美解决了密钥交换问题,两种加密方法的混合使用也保证了安全和性能的平衡。
这就完成了!现在 A 和 B 可以使用相同的密钥进行对称加密通信在这个基础上,A 和 B 可以使用标准的 HTTP 协议进行交流。同时由于没能截获密钥,可怜的 E 同学再次扑了个空。我们成功完成了一次加密的 HTTPS 通信。

接下来就是技术细节!

在加密建立的过程中,算法不同会导致实际实现的不同——尽管并没有太大差别。
简单来看,我们可以将其分为 RSA 算法和 DH 算法。两种算法建立加密的过程我们都会介绍。

开始时两种加密算法的实现是一致的。浏览器准备发出一个请求到一台服务器。首先浏览器打开了到该服务器 443 端口的 TCP 连接,准备开始 SSL 握手。
随后,浏览器生成一串随机数并附上自己支持的协议版本和加密方法等一并发送给服务器。协议版本即指 SSL/TLS 协议版本,而加密方式是指实现加密使用的具体算法,也就是密码。这个过程被称为 ClientHello
接着,服务器会发回确定下来的协议版本和加密方式,以及一串由服务器生成的随机数及数字证书。此时,如果双方没有共同支持的协议版本或加密方式,连接就会断开,浏览器显示错误信息。这个过程被称为 SeverHello
浏览器会检查服务器证书。数字证书目前没有一个统一的标准,不过使用最广泛的标准是 X.509,版本 3。浏览器会解析证书,检查其签发者是否是可信的,并会试图使用该签发者的公钥来检查证书是否经过篡改。浏览器还会检查证书是否到期、证书包含的主机名和当前主机名是否匹配,甚至会向证书颁发机构的在线服务询问证书是否被注销。一旦有任何一条检查出现问题,浏览器就会中断连接并显示错误信息。
浏览器是如何检查证书颁发机构是否可信的?基本上,在每一台联网设备里,都有一套可信机构列表。这些机构和它们的公钥被直接存储于每一台计算机中,因此不用担心被篡改(还记得上面的故事里 C 是亲自告诉 A 和 B 他的公钥的么?)。当然,用户可以添加自己的证书来让浏览器信任自己签发的证书,但这仅限于单台计算机。通常,这些在列表中的机构还会为其他机构签发证书。此时,这些其他机构签发的证书也会被系统所信任,这便构成了证书链。
通常情况下,浏览器还会检查一些其他信息,如证书透明度信息等,就不再概述了。

不同算法的不同实现在这里开始出现分歧。我们先来介绍 RSA 算法的实现。
在这时,浏览器产生整个过程中的最后一个随机数,并将它使用服务器的公钥加密,发送给服务器,即 Client Key Exchange
这时,双方会通过已有的 3 个随机数各自计算出临时密钥。如果没有差错,双方的计算结果应该是相同的。随后,双方会互发 Change Cipher Spec 消息确认接下来的数据传输已经可以切换到加密方式。
为什么需要随机数?因为数字证书本身是静态的,要保障安全性,我们需要每次连接时都有不同的密钥,这个密钥便是由双方提出的随机数计算而得的,这种方式增强了连接的安全性。

对于 DH 算法,此时服务器会利用私钥将浏览器生成的随机数、服务器随机数和服务器 DH 参数签名,生成服务器签名,并随后发送服务器 DH 参数和服务器签名,这是 Server Key Exchange
紧接着浏览器也会发送客户端 DH 参数(Client Key Exchange),随后双方即可独立计算出临时密钥,切换至加密协议(Change Cipher Spec)。

最后,两种加密算法的实现又变得一致。双方会互换这一轮握手过程中发送/接受到的信息的散列值以确保在握手过程中没有数据被恶意替换,这是 Finished
通过抓包可以看出两种实现的不同。
到这里,浏览器和服务器已经成功建立了安全的 SSL 连接。接下来,标准的 HTTP 协议就可以由 SSL 传输安全层负载进行传输了。从 TCP 传输层来看,这些数据是原始的二进制数据,即使有人截获也无法将其解密。

还有一些小问题…

通常情况下,你要访问一个网站,浏览器会在发给服务器的请求中附带一个 host 字段来告诉服务器要访问哪个网站。这对于一个服务器上托管了多个网站的情况(很常见)很有用,服务器可以立刻明白你要访问哪个网站并把对应的数据发回浏览器。
但是,在访问使用 HTTPS 的网站时,SSL 握手的过程中浏览器并不会告诉服务器要访问哪个网站(这是在 HTTP 协议下发出的,而 HTTP 通信会在 SSL 连接建立之后才开始)。这会导致服务器不知道你需要哪张证书且往往会将错误的证书发送给你。这通常会导致证书验证失败。
为了解决这一问题,人们提出了 SNI,一个 TLS 拓展协议。SNI 会在 SSL 握手时就向服务器发送要访问的主机名,以便服务器发回正确的证书。目前绝大部分服务器软件和客户端都支持这一拓展协议。

在 SSL 加密开始后,在 TCP 传输层上传输的就是完完全全的二进制数据。对于代理来说,这不是什么好事。对于这样的数据,它们完全不知道应该向哪里转发。
当然,盲转发代理是不可取的。盲转发是指对数据包不做处理直接转发的行为。这会彻底地破坏 HTTP/1.1 中引入的长连接,在拖慢速度的同时还大大加重的服务器的负担。因此通常情况下代理会解析 HTTP 头以获得一些连接的基本信息。
一种解决方案是使用 HTTPS SSL 隧道协议,这通常是使用 HTTP CONNECT 方法实现的,具体细节就不再详述了。除此之外也有很多方法,有兴趣可以自行了解。

附录:HTTPS 部署指北

活学活用了解一下。
现在,要为你的网站申请证书已经变得非常简单了。虽然上文描述的过程有些复杂,但所幸你不用亲自实现所有功能。主流的服务器软件只要经过一些简单的配置即可启用 HTTPS。通常,你只需要先申请,待证书签发后部署到服务器即可。

部署 HTTPS 的第一步是挑选符合要求的证书签发机构和合适的证书种类。不同的签发机构之间,以及同一机构签发的不同种类的证书之间,所需的价格是不同的。
证书种类是什么?真实的情况的确比上文提到的情况要再复杂一点。总的来说,证书分为 DV、OV、EV 三类。DV 证书只能提供最基本的加密功能,价格最低;而 OV 和 EV 证书会在签发前验证申请者的身份,流程更繁琐,价格也更高,但是这类证书可以提供验证网站身份的功能(比如浏览器会对不同种类的证书提供视觉上的差异,见下图),而且在出现安全问题时(比如签发机构的私钥泄露,虽然几率很小,但是有可能)的赔付金额也更高。

对于签发机构,我推荐博客类网站还是选择 Let’s Encrypt,这是由 Mozilla、思科、Akamai、IdenTrust 和 EFF 等组织发起的公共、免费的 SSL 项目。它所签发的 DV 证书完全免费,而且支持通配符和多域名。唯一的缺点是它只支持 3 个月的有效期,不过到期后可以免费续期
什么是通配符?浏览器在验证证书时,会确保证书中指定的域名和当前访问的域名相符,而根域名(a.com)和子域名(b.a.com)会被当做不同的域名来处理,不同的子域名(b.a.com 和 c.a.com)也会被当做不同的域名。通配符证书(*.a.com)可以同时匹配不同的子域名,减少了所需签发证书的数量。但是,根域名和通配符子域名不会匹配,都需要签发证书。同时,Let’s Encrypt 支持的多域名可以在同一张证书中包含多个域名,大大减少了所需证书的数量.
要使用 Let’s Encrypt所签发的证书,我们可以使用 acme.sh,这是一个基于 Let’s Encrypt 接口封装的开源命令行工具,支持多域名/通配符的申请,也支持自动续期。使用 acme.sh,我们可以使用这样的命令来申请证书:
.acme.sh/acme.sh --issue -d mydomain.com -d *.mydomain.com
关于acme.sh 的详细信息可以前往 Github.com/neilpang/acme.sh 了解。
稍等片刻,当证书签发下来,我们往往会得到两个文件,一个是你的私钥,一个是证书。
等等,这个私钥哪里来的?通常情况下,为了方便申请,用于申请证书的工具有时会自动为你生成一对私钥公钥,然后将私钥转交给你,而将公钥交给签发机构。当然大多数情况下你可以选择自己生成,然后将公钥提交给签发机构,自己保存好私钥。
这时,我们就可以把证书放到服务器上了,然后配置服务器来开启 HTTPS。不同的web服务器的配置过程通常不同,我也不再赘述了.如果你使用 Nginx,可以使用如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server{
listen 443 ssl http2;
# 其他配置...
# 如果是 HTTP 就跳转到 HTTPS
if ($server_port !~ 443){
rewrite ^(/.*)$ https://$host$1 permanent;
}
# 证书和私钥路径
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# SSL 参数
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4:!DH:!DHE;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_buffer_size 1400;
ssl_stapling on;
ssl_stapling_verify on;
# 其他配置...
}
 
最后,我们还需要对网页文件进行修改,尤其是页面上所有引用的资源必须同样使用 HTTPS,否则浏览器就不会显示安全小绿锁。
此文是对我了解到的一些 HTTPS 相关知识的总结.
-------------'

Nginx+SSL+HTTP2配置简明指南

在服务器上装了Nginx,配置了HTTPS和HTTP2,也是碰到了许多坑,这篇文章也算是一个简单的记录,目标是拿到SSL Lab的A+评分。
我有懒癌,小白速退。

在最最最前面

如果你有一台全新的云服务器,并且准备安装Nginx,不管有没有打算开HTTPS和HTTP2,尽可能安装Nginx 1.10.2+,也可以安装1.11.x的主线版本,并在编译Nginx的时候加入HTTP2模块,同时在编译 Nginx 时指定 OpenSSL 源码目录(1.0.2+),而不是使用系统自带的版本,这样可以开启ALPN,保证在新的浏览器上也能使用HTTP2。就算暂时不打算开启,谁知道你以后需不需要呢…
这样编译Nginx就能加入HTTP2模块和SSL模块:
也可以加入其他模块,看你的需求了。
开始
其实非常简单,搞到你的证书,然后在Nginx配置server块里加上SSL就好了。
但是现在访问HTTP还是未加密,除非手动输入HTTPS。于是加入跳转解决来问题:
同样放在server块里,不过从评论了解到 Nginx 官方更推荐另开一个 server 块来解决跳转。
这样就搞定了HTTPS的配置,先去SSL Lab里面溜溜.

进一步优化

首先我们要开启HSTS。
科普一下,一般情况下我们输入域名访问网站,若是不指定协议,浏览器会默认使用HTTP协议,然后才因为我们上一节的设置转到HTTPS协议。这就给了中间人攻击可乘之机。
使用HSTS(HTTP严格传输安全),在第一次访问后浏览器就会知道你的站点长时间开启HTTPS。这样,在你指定的过期时间之前浏览器就会在内部强制307到HTTPS,这样就避免了HTTP跳转的问题。
开启HSTS:
max-age参数规定了这一设定的过期时间, includeSubdomains参数表明所有子域名也需要开启HSTS,always参数表明Nginx在任何情况下都会输出这一头部。加入server块即可。
当然需要注意,开启HSTS之后,设置过期之前你站点的证书不能失效,不能试图在服务器端强制走HTTP,这都会导致站点无法访问。
而且开启HSTS后,第一次访问站点仍然会走HTTP转HTTPS,毕竟浏览器不知道嘛。HSTS Preload可以解决这个问题。
谷歌维护了一份HSTS Preload List,且这一列表各大浏览器都承认。在这一列表里的域名浏览器会知道直接使用HTTPS,这样就完全避免了走HTTP。
任何人都可以申请将自己的域名加入这一列表。但如果要想把自己的域名 加进 这个列表,需要满足以下条件:
  • 有效的证书
  • 将所有 HTTP 流量重定向到 HTTPS
  • 确保所有子域名都启用了 HTTPS
  • 输出 HSTS 响应头,且
    • max-age 至少需要 18 周(10886400 秒)
    • 必须指定 includeSubdomains 参数
    • 必须指定 preload 参数
而且貌似满足了这些条件申请也不一定通过。Anyway,申请地址(科学上网):https://hstspreload.appspot.com
注意一旦加入这一列表,撤销据说异常困难,只有在你保证可以长时间提供HTTPS服务的情况下才应该申请加入。
至少我是不打算申请了…比较咱小站没必要,何况我的子域名解析到了好几台服务器上,恐怕一时半会儿也不能保证全部HTTPS,慢慢来吧。
下一步是使用更安全的加密算法。
很简单,把这些配置加入server块:
这会让服务器使用更强的加密算法(如果客户端支持)。另外建议,直接让服务器关闭对IE 6的连接好了。IE 6只支持部分弱加密算法,应该被淘汰了。
然后开启SSL缓存。
开启SSL缓存可以让浏览器在一定时间里缓存SSL加密。这样,在这段时间里发起的SSL连接就可以减少相当的加密时间,这对减轻服务器和客户端计算压力都有不小的好处。
同样放进server块:
这样就好了。做到目前这一步,SSL就已经配置好了。最后一步,加入HTTP2。
也是非常简单。更改listen:
这样就开启了HTTP2。注意,Ngnix 1.10.1及以下版本还是不要开启了,会有POST Bug,详见https://imququ.com/post/nginx-http2-post-bug.html
最后重启Ngnix,到SSL Lab里面清除缓存,再跑一次.
当然到了这一步也还是有优化的地方的,比如说加入HTTP Public Key Pinning(HPKP)。然而有些设置实在太麻烦,何况没必要,还会牺牲兼容性,得不偿失.