Skip to content

HTTPS原理

参考:升级博客到HTTPS,后面用伪代码整理了https的基本原理。

超文本传输安全协议(Hypertext Transfer Protocol Secure)HTTPS相对HTTP提供了更安全的数据传输保障。主要体现在三个方面:

  • 内容加密。客户端到服务器的内容都是以加密形式传输,中间者无法直接查看明文内容。
  • 身份认证。通过校验保证客户端访问的是自己的服务器。
  • 数据完整性。防止内容被第三方冒充或者篡改。

让我们从加密算法开始,了解HTTPS的原理。

对称加密

对称加密,服务端和客户端的加密和解密都使用相同的加密和解密方式,常见的对称加密算法有DESAES等。

对称加密算法效率比较高,使用Node.js内置的crypto演示一下AES对称加密算法

js
const crypto = require('crypto');

// 生成随机的对称加密密钥
const secretKey = '123456';

// 要加密的数据
const dataToEncrypt = 'Hello, World!';

// 创建加密器
const cipher = crypto.createCipher('aes-256-cbc', secretKey);

// 加密数据
let encryptedData = cipher.update(dataToEncrypt, 'utf-8', 'hex');
encryptedData += cipher.final('hex');

console.log('加密后的数据:\n', encryptedData);

// 创建解密器
const decipher = crypto.createDecipher('aes-256-cbc', secretKey);

// 解密数据
let decryptedData = decipher.update(encryptedData, 'hex', 'utf-8');
decryptedData += decipher.final('utf-8');

console.log('解密后的数据:\n', decryptedData);

在上面的代码中,只要知道了secretKey的值,就可以直接创建加密器和解密器。

如果可能在传输过程中秘钥被盗取,中间人获取到了秘钥,就可以解密和伪造数据。

非对称加密

非对称加密算法是一种使用不同的密钥对进行加密和解密的加密算法,常见的非对称密钥加密算法有RSADSA等。

非对称加密算法基于一对相关的密钥,包括公钥和私钥。公钥是公开的,可以被任何人获取;而私钥是保密的,只有密钥的拥有者可以访问。

使用Node.js内置的crypto演示一下RSA非对称加密算法。

首先是使用公钥进行加密的数据,可以通过私钥进行解密。

js
const crypto = require('crypto');

// 生成RSA密钥对
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

console.log('公钥:\n', publicKey);
console.log('私钥:\n', privateKey);

// 要加密的数据
const dataToEncrypt = 'Hello, World!';

// 使用公钥加密数据
const encryptedData = crypto.publicEncrypt(publicKey, Buffer.from(dataToEncrypt, 'utf-8'));
console.log('加密后的数据:\n', encryptedData.toString('base64'));

// 使用私钥解密数据
const decryptedData = crypto.privateDecrypt(privateKey, encryptedData);
console.log('解密后的数据:\n', decryptedData.toString('utf-8'));

然后私钥也可以加密数据,这通常被成为签名,私钥签名后的数据可以使用公钥进行解密,这通常被成为验签

js
// 需要安装一下jsonwebtoken
const jwt = require('jsonwebtoken');

// 生成 RSA 密钥对
const { privateKey, publicKey } = require('crypto').generateKeyPairSync('rsa', {
    modulusLength: 2048,
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

console.log('公钥:\n', publicKey);
console.log('私钥:\n', privateKey);

// 要签名的数据
const originalData = 'Hello, World!'

// 使用私钥生成签名
const token = jwt.sign(originalData, privateKey, { algorithm: 'RS256' });

console.log('生成的签名:\n', token);

// 使用公钥验证签名并获取原始数据
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
    if (err) {
        console.error('验证失败:', err.message);
    } else {
        console.log('验证成功,原始数据:', decoded);
    }
});

可以看出,公钥和私钥本身都具备了加密和解密的功能,只是按照习惯私钥的加密被称为签名而已。

根据生成规则,从私钥推导出公钥是比较简单的,而从公钥推导出私钥是一个困难的数学问题。

下面的代码演示了通过私钥推导出公钥的过程

js
const crypto = require('crypto');

// 生成 RSA 密钥对
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

console.log('私钥:\n', privateKey);
console.log('公钥:\n', publicKey);

// 从私钥推导出公钥
const derivedPublicKey = crypto.createPublicKey(privateKey);
const res = derivedPublicKey.export({ type: 'spki', format: 'pem' })
console.log('推导的公钥:\n', res);
console.log(publicKey === res);

因此,一般会将公钥暴露出去,而私钥要严格保密,只让自己知道(这也是他们取名的由来)。

在非对称加密过程中,服务器将公钥发送给浏览器,浏览器就可以用这个的公钥加密数据,由于私钥只有服务器知道,这样就不用担心浏览器->服务器传输过程中的数据被中间人破解了。

同理,浏览器如果也有自己的公钥和私钥,同样可以将公钥发给服务器,这样服务器也可以用公钥加密需要传输的数据,这样也保证了服务器->浏览器数据传输的保密性。

但实际上,一个服务器可能要面对数百万浏览器的访问,非对称加密需要消耗大量的性能,因此实际上HTTPS结合了对称和非对称两种加密方式:

  • 在握手阶段使用公开密钥加密选择一种对称加密算法和密钥
  • 然后在后续的通信阶段使用选择的对称加密算法和密钥进行通信。

OK,这样解决了效率的问题,但还有一个安全问题没有解决。

安全地传输公钥

假设服务器有一对非对称秘钥,包含公钥A和私钥A1,传输过程中有一个中间人,他自己也有一对非对称秘钥,包含公钥B和私钥B1

  • 服务器将公钥A发送给浏览器,被中间人拦截了,中间人实际上将他自己的公钥B发给了浏览器
  • 浏览器拿到了公钥B,加密了对称密钥X,发送给服务器,被中间人拦截了,中间人通过自己的私钥B1,解密获得了X,然后将X用服务器的公钥A进行加密,发送给服务器
  • 服务器使用自己的私钥A1解密获得了对称密钥X,开始通信

从这个过程中可以看出,即使中间人没有服务器的私钥A1,他也可以通过伪造拿到对称密钥X。

因此,浏览器必须要确认收到的公钥是目标网站服务器自己的,而不是中间人的!

如何确认呢?这就需要使用数字证书

数字证书

数字证书由可信的第三方机构(证书颁发机构,Certificate Authority,简称CA)签发,每个CA也都有一对公钥和私钥。

操作系统一般内置了常见的CA的公钥,不经过网络传输,可以认为CA的公钥是可信的。

证书包含签名发起者(也就是网站服务器拥有者)的公钥及相关信息,

  • 证书版本号:标识证书的版本信息。
  • 序列号:唯一标识证书的序列号,用于区分不同的证书。
  • 颁发者信息:包含颁发证书的证书颁发机构(CA)的相关信息,如颁发者的名称、机构信息等。
  • 有效期:指定证书的有效期限,包括起始日期和截止日期。
  • 主体信息:包含证书持有者(签名发起者)的相关信息,如名称、电子邮件地址等。
  • 公钥信息:包含证书持有者的公钥及其算法参数。
  • 扩展信息:可选的扩展字段,用于存储其他的证书相关信息,如主题备用名称、密钥用途等。

这些原始内容会由CA的私钥进行签名,称为数字签名,因此完整的证书内容还包括

  • 签名算法标识:指示用于生成证书签名的算法,如RSA、SHA-256等。
  • 数字签名值:使用证书颁发机构的私钥对证书的内容进行签名生成的签名值。

服务器会在响应请求的时候,将数字证书发送给浏览器。

中间人不能够将数字证书替换成自己的证书,因为证书是由CA发布的,包含固定的域名,浏览器收到证书之后,判断一下域名就行了。

浏览器收到证书之后,会使用对应的CA公钥,对数字证书上面的数字签名进行验签。

那么只要验签通过,就可以认为证书里面的原始内容没有被修改————即数字证书内容里面的公钥确实是目标网站服务器自己的。

之后,就可以使用服务器的公钥加密对称算法的秘钥,发送给服务端,之后就可以愉快地进行加密消息传送了。

签名和验签

更详细的数字签名和验签过程如下

发送方:

  1. 发送方对原始内容进行摘要计算,通常使用哈希函数,如SHA-256,生成消息的唯一指纹。

    • 哈希摘要算法的的特点在于:只有输入相同的明文数据经过相同的数据摘要算法才能得到相同的摘要。如果内容有变化,则会得到完全不同的摘要。
    • 进行摘要计算的原因主要是缩短内容长度,优化验签性能
  2. 发送方使用自己的私钥对摘要进行加密,生成数字签名。

  3. 发送方将原始内容(未加密)和数字签名、以及对应的摘要算法一起发送给接收方。

接收方:

  1. 接收方接收到原始内容和数字签名。

  2. 接收方使用发送方的公钥对数字签名进行解密,得到解密后的摘要。

  3. 接收方对接收到的原始内容,按照约定的摘要算法进行计算,得到自己的摘要。

  4. 接收方将两个摘要进行比对,如果匹配,则消息的完整性得到验证,并且可以信任消息来自于发送方。

签名和验签的过程可以查看下面这张图,来源:cheapsslsecurity.com

最后一个问题,既然无法修改证书上的数据,那么中间人能不能像之前那样狸猫换太子,使用一个包含他自己公钥的证书来替代原始网站服务器的证书呢?

答案是现在行不通了。因为证书是不能被伪造的,在签发证书时就会验证域名及域名所有者的身份,浏览器收到证书后,除了上面的验签,还会进行下面的检查

  • 检查SSL证书是否由浏览器中“受信任的根证书颁发机构”颁发
  • 检查部署SSL证书的网站域名是否与证书中一致
  • 检查SSL证书中的证书吊销列表,证书是否被颁发机构吊销
  • 检查此SSL证书是否过期
  • 浏览器会到欺诈网站数据库查询此网站是否被列入黑名单

即使中间人换了一个其他域名的证书,浏览器对比一下请求的域名,一下就拒绝了。

替换完整的证书也不行(证书校验不通过),修改原始证书的部分内容也不行(证书验签不通过),那么,通过数字证书安全传输服务器公钥的目的就达到了。

根证书

上面提到的CA公钥保存在操作系统中,实际上就是我们常说的系统根证书。

系统根证书是一种在计算机系统中用于验证数字证书有效性的证书。

根证书的存在是为了建立信任关系,确保其他数字证书的有效性。

根证书是数字证书体系的基础,它们由受信任的证书颁发机构(CA)签发,并被内置在操作系统或应用程序中,作为信任链的根节点。

信任链是由一系列数字证书构成的链条,链接了一个信任的根证书到最终要验证的证书。

验证证书的有效性时,系统会追溯信任链,确保每个证书都由直接或间接信任的根证书签发。

这也是抓包工具能够抓取HTTPS的原因,开发者需要将抓包工具的证书信任为根证书。这样,抓包工具就可以充当一个中间人

  • 在与服务端的通信中,抓包工具作为正常的客户端接收和发送HTTPS消息
  • 在与本地应用的通信中,抓包工具作为服务端,发送和接收消息,这就要求其证书需要被系统认证,这样浏览器才可以完成与他的HTTPS连接

mixed content警告

参考

"Mixed content" 是指在 HTTPS 页面上同时加载了通过不安全的 HTTP 协议加载的内容。

这些HTTP的内容是不受加密保护的,恶意攻击者还是可以替换这部分内容用来攻击

分为被动混合内容和主动混合内容

  • 被动混合内容包括img、audio、video、object的subresource等标签嵌入的内容
  • 主动混合内容包括script、link、iframe、xhr、fetch、css中所有url引入的、object的data属性等地方引入的内容

网站管理员应该努力确保页面上所有资源都通过安全的 HTTPS 协议加载,以维护整个网站的安全性。

这可能包括将 HTTP 资源更改为 HTTPS,或者通过 Content Security Policy(内容安全策略)等机制来阻止加载不安全的资源。