加密算法

对称加密的局限性

在对称加密(例如:AES)中,加/解密使用相同的秘钥。这会带来 2 个问题:

  1. 秘钥分发问题:发送方和接受方必须通过某种安全方式共享秘钥。如果秘钥在传输时被截获,通信安全性就会被破坏。
  2. 秘钥管理问题:如果有多对用户需要通信,每对用户都需要一个独立的秘钥,秘钥数量会随着用户规模急剧增加(10个用户需要 45 个秘钥, n*(n-1)/2 )。

非对称加密算法

  • 公钥:公开分享,用于加密数据或者验证签名
  • 私钥:严格保密,用于解密数据或者生成签名

公钥和私钥在数学上是相关的,但是从公钥推导出私钥在计算上是不可行的(基于大整数分解或者离散对数)。发送方使用接收方的公钥进行加密,接收方使用自己的私钥才能解密,因此不需要传递秘钥。

非对称加密的缺点是运算速度慢。

为什么我们常常说公钥加密私钥解密

其实公钥和私钥都能用来加密和解密。也就是说:同一对钥匙,公钥加密只能私钥解密,私钥加密只能公钥解密。那么我们平常为什么不说私钥加密呢?因为公钥是公开的,人手一份公钥,这不就等于没有加密么?因此在实践中,不用私钥进行加密。私钥的一般用途是签名。

中间人攻击

RSA算法中的中间人攻击

例如电信运营商的DNS劫持(或者HTTP劫持),篡改网页加入自己的广告,最好的解决方案是采用HTTPS协议(多了一个步骤SSL证书)。

域名劫持:通过劫持掉域名的 DNS 解析结果,将 HTTP 请求劫持到特定 IP 上,使得客户端和攻击者的服务器建立 TCP 连接,而非和目标服务器直接连接,这样攻击者就可以对内容进行窃取或篡改。在极端的情况下甚至攻击者可能伪造目标网站页面进行钓鱼攻击。一般而言,用户上网的 DNS 服务器都是运营商分配的,所以,在这个节点上,运营商可以为所欲为。例如,访问 http://jiankang.qq.com/index.html ,正常 DNS 应该返回腾讯的 ip,而 DNS 劫持后,会返回一个运营商的中间服务器 ip。访问该服务器会一致性的返回302,让用户浏览器跳转到预处理好的带广告的网页,在该网页中再通过 iframe 打开用户原来访问的地址。

HTTP劫持/直接流量修改:在数据通路上对页面进行固定的内容插入,比如广告弹窗等。在这种情况下,虽然客户端和服务器是直接建立的连接,但是数据内容依然可能遭到野蛮破坏。例如在运营商的路由器节点上,设置协议检测,一旦发现是 HTTP 请求,而且是 html 类型请求,则拦截处理。后续做法往往分为 2 种,1 种是类似 DNS 劫持返回 302 让用户浏览器跳转到另外的地址,还有 1 种是在服务器返回的 HTML 数据中插入 js 或 dom 节点(广告)。

HTTPS 原理

普通的 HTTP 协议存在 3 个问题:

  1. 泄密。数据明文传输
  2. 篡改。例如植入广告
  3. 假冒。访问的站点是假网站,例如域名劫持、钓鱼网站

解决泄密的问题可以使用加密。但是无论采用对称加密还是非对称加密都存在中间人攻击。对于对称加密,截获了秘钥就可以为所欲为;对于非对称加密,中间人在服务器发送公钥给客户端的时候将其替换为自己的公钥发送给客户端,客户端对公钥的真实性是毫无感知的,它下次发送数据的时候采用中间人的公钥进行加密,中间人拿到数据后用自己的私钥进行解密,然后可以修改客户端的数据,用自己的公钥加密后发送给服务器(同样服务器也不知道中间人是假的客户端)。对于服务器来说,中间人是真正的客户端;而对于客户端来说,中间人是真正的服务器

为了解决中间人攻击,客户端在首次和服务器进行通信的时候,服务器需要展示自己的证书证明自己是真正的服务器。证书是由权威机构(CA)颁发的、无法伪造的。客户端拿到证书后就需要验证证书的有效性,还可以拿到证书中服务器的公钥信息用于协商对称秘钥。

证书颁发了,那怎么防止证书在传输过程中不被篡改呢?万一中间人截获到数字证书,把公钥改成自己的,那岂不是仍然无法保证安全了?这个时候就需要数字签名了。

服务器在向 CA 申请证书的时候,CA 在颁发证书的同时会连同证书和证书的摘要(经过CA机构自己的私钥进行加密的)一同发送给服务器。

申请证书的流程

下图可以看到 CA 给服务器颁发证书的时候是有自己的专属“公章”的。

CA 生成对证书生成摘要信息,用私钥进行加密得出数字签名

下图所示是知乎的证书,采用了 2048 位的 RSA 算法:

知乎的证书

服务器在与客户端通信的时候,就会将数字证书和数字签名出示给客户端了。客户端拿到数字证书和数字签名后,先通过操作系统或者浏览器内置信任的CA机构找到对应CA机构的公钥对数字签名进行解密,然后采用同样的摘要算法计算数字证书的摘要,如果自己计算的摘要与服务器发来的摘要一致,则证书是没有被篡改过的!这样就防止了篡改!第三方拿不到CA机构的私钥,也就无法对摘要进行加密,如果是第三方伪造的签名自然也在客户端也就无法解密,这就防止了伪造!所以数字签名就是通过这种机制来保证数字证书被篡改和被伪造。

客户端和服务器协商秘钥的流程

这里需要注意一点,一个是 CA 机构的公钥,内置在客户端,用来解密数字签名!另一个是目标服务器的公钥,在数字证书内容里,用来协商对称密钥!

其实 HTTPS 的出现就是结合了上面的手段:客户端请求服务器的证书(公钥),服务器返回证书,客户端拿到这个证书后校验(通过内置的CA),校验成功后生成一个随机数,然后使用公钥对随机数进行加密后发送,服务器收到后使用自己的私钥解密。接下来双方的通信都是基于该随机数的对称加密。

HTTPS 的各个步骤

https 的各个阶段

前置条件:服务器正确配置了数字证书(可以自己制作,也可以向组织申请)。自己颁发的证书需要客户端验证通过(地址栏警告,例如早期的 12306),才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥(创建 https 服务器需要指定证书(公钥)和私钥)。

  1. 客户端发起 HTTPS 请求。
  2. 服务器向客户端传送证书。这里的证书其实是密钥对中的公钥,只是包含了很多信息,如证书的颁发机构,过期时间等。
  3. 客户端验证证书,这部分工作是由客户端的 TLS 来完成的。如果证书校验不通过则弹出警告,提示证书存在问题;有效则生成一个随机数,使用证书(服务器公钥)对这个随机值进行加密。
    1. 验证证书链:检查证书是否由受信任的证书颁发机构(CA) 签发。客户端会逐层验证证书链(服务器证书-中间证书-根证书),如果链中的任何一个证书无效或者不受信任,则验证失败。
    2. 根证书信任库:浏览器/OS 内置了一个受信任的根证书列表(Trust Store)。如果证书链的根证书不在信任列表中,则验证失败。
    3. 验证有效期:证书已过期和尚未生效都验证失败。[Not Before, Not After]
    4. 验证域名:检查证书中的 SAN 或者 CN 是否和请求的域名匹配。
    5. 检查证书是否吊销:通过证书吊销列表(CRL) 或者 OSCP(在线证书状态协议) 查询证书状态。
    6. 签名验证:客户端使用证书链中的上级证书的公钥验证证书的签名。
  4. 传送加密信息。这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
  5. 服务端解密信息。服务端用私钥解密后,得到了客户端传过来的随机值。这个随机值就是对称加密的秘钥。
  6. 传输加密后的信息。服务器发送对称加密的内容
  7. 客户端解密信息。客户端用之前发送给服务器的秘钥进行解密

客户端对证书签名进行验证

  • 证书的签名是由证书颁发机构(CA)使用自己的私钥对证书内容进行加密生成的。
  • 客户端使用 CA 的公钥解密签名,得到证书的哈希值。
  • 客户端重新计算证书内容(DER 编码部分,不包含签名部分)的哈希值(签名算法在证书中有),并与解密得到的哈希值进行对比。
  • 如果两者一致,说明证书由未被篡改并且可信的 CA 签发。

针对 HTTPS 的一些实践

编写 https 服务器

首先使用 openssl 生成证书

1
openssl req -x509 -newkey rsa:4096 -keyout localhost-privkey.pem -out localhost-cert.pem -days 365 -nodes

out 指定证书文件(主要是公钥,还包含其他元数据,例如有效期、颁发者), keyout 指定私钥文件,可以从证书文件中提取出公钥。我们可以粗浅认为 cert.pem 就是公钥。

接下来使用证书文件和私钥文件创建 HTTPS 服务器

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.mjs
import https from 'https';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// 获取当前文件路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 读取证书和私钥
const options = {
key: fs.readFileSync(path.join(__dirname, 'localhost-privkey.pem')),
cert: fs.readFileSync(path.join(__dirname, 'localhost-cert.pem')),
};

// 创建 HTTPS 服务器
const server = https.createServer(options, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, HTTPS World!\n');
});

// 监听端口
server.listen(443, () => {
console.log('HTTPS server running on https://localhost:443');
});

接下来在浏览器中访问 https://localhost:443,就可以看到页面了。可以使用 curl 命令查看发送 https 请求的详细信息:

使用curl查看发送 https 请求的详细信息

为什么 Charles 能够抓取 HTTPS 包

使用 Charles 抓取 HTTPS 包

HTTPS 抓包的原理还是挺简单的,简单来说,就是 Charles 作为“中间人代理”,拿到了服务器证书公钥和 HTTPS 连接的对称密钥,前提是客户端选择信任并安装Charles的CA证书,否则客户端就会“报警”并中止连接。这样看来,HTTPS还是很安全的。