HTTP协议之安全(五)

HTTP使用明文发送报文,本身不具备加密的功能。这在危机四伏的互联网中是十分危险的,在阅读了《图解HTTP》第七章之后,整理了一点关于安全HTTP的内容,主要内容就是理解HTTPS的原理和工作机制。

<!--more-->

1. 危险的万维网

1.1. 数据被窃听

因特网是一个世界范围内的计算机网络,数以亿记的设备通过分组交换机和链路构成的网状系统连接在一起。在开发时我们可以通过代理抓包等方式监听网络请求;如果在某个传输环节中HTTP报文就可能遭受到恶意窥视,因为只需要收集在互联网上流动的数据包就可以知道发送的数据了。

1.2. 通信伪装

HTTP协议中的请求和响应都不会对通信方进行确认,也就是说,任何客户端都可以发起请求,正常情况下服务端只要接受到请求就必然会返回一个响应,这样就会存在下面几个问题

  • 客户端可能请求到伪装的服务端,或者服务器响应了伪装的客户端
  • 无法判定请求的来源,也无法确定客户端的访问权限
  • 无法拒绝无意义的请求,无法阻止DOS攻击

1.3. 报文被纂改

HTTP协议无法证明通信报文的完整性,因此在传输过程中,即使请求或响应的内容遭受到纂改也无法获知。在某些应用场景下,可以通过MD5,SHA-1等散列值校验的方法对报文的完整性进行确定,比如通信双方约定一个加密方式方式,在请求和响应时手动对数据进行加密。

虽然这种方式实现起来比较繁琐且并不是十分可靠,但如果解决了上面提到的数据窃听通信伪装的问题,报文被纂改的风险也会大大降低。

1.4. HTTPS

上面列举了HTTP通信中存在的安全性问题。但是,仅仅依靠HTTP自身来保证整个传输过程中报文的安全性是非常困难的,为了统一解决上面提到的问题,需要在HTTP上再加入加密处理和认证等机制,而这种添加了加密和认证机制的HTTP,即称为HTTPS。《图解HTTP》里面的结论是:

HTTP + 加密 + 认证 + 完整性保护 = HTTPS

2. 加密

前面提到,为了防止传输过程中数据被篡改,或者被第三方窃听,可以对将通信内容(即报文)进行加密。通信是服务器和客户端双方的问题,因此在某一方对报文加密的同时,必须保证另一方能够顺利解密,否则,整个加密就毫无意义。

下面主要探讨的是如何在服务器和客户端实现合理的加密传输方式,并简单探讨一些具体的加密技术实现细节。

2.1. 对称加密算法

对称加密算法是应用较早的加密算法,技术成熟。在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,这就要求解密方事先必须知道加密密钥。

常见的对称加密算法有DESAES,接下来我们实现一个超级简单的对称密钥加密:凯撒密码。相关代码位于github

// 将整个字符串按照约定的偏移量进行移动,并返回新的字符串
function encrypt(val, secretKey) {
    // 利用密钥加密明文
    return val.split('').map(ch => String.fromCharCode(ch.charCodeAt(0) + secretKey % 26)).join('')
}
function decrypt(data, secretKey) {
    // 解密实际上就是使用密钥对加密过程进行逆运算
    return data.split('').map(ch => String.fromCharCode(ch.charCodeAt(0) - secretKey % 26)).join('')
}
function test() {
    const secretKey = 10 // 约定的密钥
    var originData = "hello, I'm originData" // 原始明文
    var secretData = encrypt(originData, secretKey) // 发送方使用密钥加密数据
    console.log(secretData) // rovvy6*S1w*y|sqsxNk~k,不知道密钥的话,无法直接猜出这个密文的原始含义
    console.log(originData === decrypt(secretData, secretKey)) // 接收方使用同样的密钥解密数据
}
test()

基于上述原理,对称加密传输的实现流程大致是:

  • 在建立连接时,由客户端和服务端约定选择一种对称加密方法和对应的密钥
  • 客户端使用约定的对称加密方法和密钥加密数据,服务端则使用该加密方法和密钥解密数据;反之亦然

整个流程看起来比较简单,问题在于:如何安全地约定加密方法和密钥?

对称加密传输时,在建立连接时的报文中,会将钥匙以未加密的方式进行传输,因此就存在钥匙被窃听的风险,一旦密钥和加密方法被窃听,整个对称加密传输就失去意义了。

2.2. 非对称加密算法

对称加密的缺点在于通信双方需要交换密钥,我们来看看非对称加密算法是如何避免这个问题的。

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法

接下来我们简单地实现一个非对称密钥加密算法RSA,相关代码位于github


// 参考 https://blog.csdn.net/weixin_37887248/article/details/82805508
// RSA原理:RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥
// (1)选择两个不同的大素数p和q;
// (2)计算乘积n=pq和Φ(n)=(p-1)(q-1);
// (3)选择大于1小于Φ(n)的随机整数e,使得gcd(e,Φ(n))=1;注:gcd即最大公约数。
// (4)计算d使得d*e=1mod Φ(n);注:即d*e mod Φ(n) =1。
// (5)对每一个密钥k=(n,p,q,d,e),定义加密变换为Ek(x)=xe mod n,解密变换为Dk(x)=yd mod n,这里x,y∈Zn;
// (6)p,q销毁,以{e,n}为公开密钥,{d,n}为私有密钥。
function rsa(baseNum, key, message) {
    if (baseNum < 1 || key < 1) {
        return 0;
    } 
    let rsaMessage = 0;
    rsaMessage = Math.round(Math.pow(message, key)) % baseNum;
    return rsaMessage;
}
function test() {
    var baseNum = 3 * 11 // n=pq=33, Φ(n)=(p-1)(q-1) = 20
    // 选择大于1小于Φ(n)的随机整数e,满足gcd(e,20)=1,且则d*e=1 mod Φ(n), 可取e=3,d=7
    // 当然这里publicKey和privateKey互换也是满足的
    var publicKey = 3
    var privateKey = 7

    // 因此取计算结果{baseNum, publicKey}作为公钥,{baseNum, privateKey}作为私钥
    var msg = 24 // 需要加密的数据
    // 只需要约定一个加密算法,发送方使用公钥进行加密
    var encodeMsg = rsa(baseNum, publicKey, msg)
    // 接收方使用私钥进行解密,私钥不参与通信且很难被破解,因此可以保证整个数据的安全
    var decodeMsg = rsa(baseNum, privateKey, encodeMsg);
    // decodeMsg === msg,数据还原
    console.log({ msg, encodeMsg, decodeMsg, }) 
}
test()

基于上述原理,非对称加密传输的实现流程大致是:

  • 服务端在建立连接时,将自己的公钥发送给客户端
  • 客户端使用服务端的公钥加密数据,服务器接收到数据后,使用自己的私钥解密数据
  • 反之亦然,如果客户端提供了自己的公钥,服务端也可以使用相同的方式加密发送给客户端的数据。

在实际传输过程中,公钥是可以任意传输和发布的,而私钥不经过网络传输且不能让任何其他人知道。因此非对称密钥加密也称为公开密钥加密

在公开密钥加密中,发送密文的一方使用对方的公有密钥进行加密,而接收密文的一方则使用自己的私有密钥进行解密。公开密钥加密不需要发送用来解密的私有密钥,因此不用担心想对称密钥加密那样担心私有密钥被窃听。

但是,由于客户端和服务端都使用了各自的加密和解密方式,因此在加解密效率要比对称密钥加密慢一些。

2.3. HTTPS中的加密

考虑到上述两种加密方式各自的优点和缺点,HTTPS采用的是两者并用的混合加密机制,保证安全性与处理速度的兼得,即:

  • 使用非对称加密方式传递对称加密方式的密钥S
  • 在确保约定的密钥S是安全的前提下,使用对称加密方式进行加密

但是使用非对称密钥加密的方式传输密钥S仍旧存在通信伪装的安全问题:无法保证非对称密钥加密时传输,消息发送方使用的是货真价实的公钥。

如果在传输过程中,中间方伪造了公开密钥,然后拦截了密文并使用中间方自己的私钥解密数据,整个加密也没有意义,解决这个问题的办法是使用数字证书

3. 证书认证

认证的目的在于检验公匙是否合法,该过程主要是通过第三方权威机构CA来进行认证的,使用证书验证通信双方的身份,也就解决了通信伪装的问题。

由于在公有密钥加密的方式中,服务端和客户端可以各自交换双方的公钥,因此这里就存在对客户端的认证和对服务端的认证,先来看看对服务端的认证。

3.1. 服务器认证

服务器运营人员向第三方数字证书认证机构提出公钥的申请,该机构在确定申请服务器的身份之后,

  • 会为该申请服务器分配一个公钥
  • 然后对公钥和证书信息做hash操作生成摘要
  • 最后使用机构自身的私钥对摘要做数字签名,这样生成的文件称为证书

因此,服务器成功申请公钥之后,获取到的是一份已被签名的公钥证书,在与客户端的通信中,会将这份已被签名的证书发送给客户端。

客户端在接受到证书之后,会使用认证机构的公钥对证书上面的数字签名进行验证,如果通过验证则证明该服务器的公钥是合法的,然后就可以使用非对称加密方式传递共享加密方式的私有密钥,最后以对称密钥加密方式进行会话。

等等!这里有个BUG啊:如果是认证结构直接向客户端发送公钥,那不也同样存在认证机构的公开密钥为伪造的风险吗,这不就成了一个死循环?

原来,为了安全地将机构的公开密钥传递给客户端,大多数浏览器开发商在发布版本的时候,会事先在浏览器内部植入常用受信任的认证机构的公钥。正因如此,浏览器才能直接对服务器公钥证书上的数字签名进行认证。

这样,在认证机构的签名认证机制下,就可以让客户端确认服务器的实际身份而不用担心自己正在与一个伪装的攻击者进行会话了。

3.2. 证书校验

前面提到了客户端会使用认证机构的公钥进行证书校验,保证服务器的公钥是受信任的,那么,为什么证书可以确保公开密钥的真实性呢?

这是因为,证书是不能被伪造的,在签发证书时就会验证域名及域名所有者的身份。浏览器和操作系统内置了默认信任的证书,验证证书的过程大致如下

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

3.3. 客户端认证

从上面的认证流程可以看见,认证的关键在于为自身的公开密匙申请数字签名证书,通常,这是需要支付一定的费用的。HTTPS允许使用客户端证书,其机制与服务器认证大致相同。

但是,让每个用户都自费安装证书的做法无疑是一件非常有挑战的事情。因此,客户端认证的情况一般只出现在安全性要求非常高的特殊场景。我们接触到的大部分场景都只需要认证服务端公钥,然后传输对称加密,在后续的通信中,使用对称加密通信即可。

4. SSL

​ HTTPS并非是一种新的协议,而是在HTTP应用层与TCP传输层之间添加了SSL和TSL安全层而已。 SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。

4.1. HTTPS的通信流程

前面提到了在HTTPS中,

  • 使用对称加密来加密通信内容,防止通信内容被窃听

  • 使用非对称加密来传输对称加密约定的密钥,防止密钥被泄漏

  • 使用证书认证的非对称加密的公钥,防止通信伪装

现在简单看一看整个HTTPS建立SSL连接的通信流程。

  • 客户端发起SSL通信请求,请求报文中包含客户端支持的SSL版本,加密组件列表(所使用的加密算法以及密钥长度)
  • 服务器接收到SSL通信,在响应报文中附带服务器支持的SSL版本,并从客户端的加密组件筛选出相关内容并返回给客户端
  • 之后服务器向服务器发送经过认证的公钥证书,最后向客户端发送SSL握手协商部分结束的信息
  • 客户端对公钥证书进行验证,如果合法,则生成一个叫做Pre-master secret的随机密码串(我的理解是这就是约定的对称加密算法密钥),然后使用服务器的公开密钥进行加密,并发送给服务器
  • 客户端还需要发送一条报文提醒服务器之后的报文都采用Pre-master secret进行加密。
  • 客户端还需要发送一条经过加密的Finished报文表示握手协商阶段的结束,当然,真正的握手协商成功还得看服务器能够顺利解密这条报文。
  • 服务器同样发送提醒报文和Finished报文。
  • 当服务器和客户端的Finished报文交换结束,则表示整个SSL连接建立完成,之后的通信都处于加密认证保护的阶段。

在[升级博客到HTTPS](https://www.shymean.com/article/升级博客到HTTPS#2. 理解https)这篇文章中,整理了建立HTTPS链接的伪代码实现。

// HTTPS建立连接的伪代码

// step1: 客户端请求
var 客户端支持的算法 = client.获取支持的加密和hash算法();
client.发送(客户端支持的算法);

// step2: 服务端发送证书
var { 客户端支持的算法} = server.解析响应()
var { 对称加密算法, hash算法 } = server.选择一种客户端支持的加密算法(客户端支持的算法);
var 证书 = server.获取证书(发证机构, 公钥);
server.发送(证书, hash算法, 对称加密算法);

// step3: 客户端收到证书
var { 证书, hash算法, 对称加密算法 } = client.解析响应();
if(client.检测证书合法性(证书)){
    var {公钥} = client.获取证书公钥(证书)
    var 对称算法密钥 = client.生成对称算法密码(random())

    var 消息M = client.使用公钥加密密钥(公钥, 对称算法密钥)
    var 消息验证值V = client.生成hash验证值(hash算法, 对称算法密钥);

    client.发送(消息M, 消息验证值V)
}else {
    连接失败;
}

// step4: 服务端收到消息和验证值
var { 消息M, 消息验证值V } = server.解析响应();
var 解密结果R = server.使用私钥解密(消息M)
var 消息验证值V1 = server.生成hash验证值(hash算法, 解密结果R);
if (消息验证值V1 == 消息验证值V){
    var 对称算法密钥 = 解密结果R; 

    var 握手终止报文 = server.生成报文(对称加密算法, 对称算法密钥, "握手终止消息");
    server.发送(握手终止报文);
}else {
    客户端发送的对称算法密钥 != 服务端收到的对称算法密钥
    连接失败
}

// step5: 客户端接收到握手终止消息
var { 报文 } = client.解析响应();
let { 握手终止报文 } = client.解析报文(对称加密算法, 对称算法密钥, 报文);
if (握手终止报文){
    对称算法密钥传输成功
    开始对称加解密传输
}else {
    对称算法使用对称算法密钥解密失败
}

4.2. SSL带来的问题

SSL带来安全通信的同时,也带来了一个问题:当使用SSL时,HTTP的处理速度会变慢。造成慢的原因有两个:

  • 通信连接的复杂度导致整理上处理通信量的增加
  • 在服务器和客户端都必须进行加密和解密,尽管在后续的通信中使用共享加密方式,但不可避免的是同样会消耗过多的服务器和客户端的CPU和硬件资源

因此,尽管HTTPS安全可靠,但是在常规的非敏感信息的请求中,更多地还是使用HTTP通信,只有在包含个人信息等敏感信息时,才建议使用HTTPS进行加密通信。

5. 小结

本文

  • 首先整理了HTTP通信传输中可能遇见的几个安全问题,
  • 然后介绍了使用对称加密和非对称加密两种避免数据窃听和数据被篡改的问题,并给出了简单实现解释这两种算法的区别
  • 然后介绍了使用证书认证避免非对称加密中存在的通信伪装问题,然后介绍了服务端验证证书的生成和校验流程
  • 最后介绍了建立HTTPS连接时的具体流程,主要包括:约定对称加密算法和密钥、证书验证、使用公钥发送对称密钥和校验消息、对称密钥发送并校验成功后开始进入正式连接阶段

至此,整个HTTPS的学习到此结束,其中还有诸如具体的加密实现方式细节,数字签名的生成以及数字证书的等级等方面的知识,只是简单了解,并没有记录和深入,需要进一步学习和整理。

NodeJS中的http模块浏览器中的跨域