与 Windows 大量使用二进制文件和数据传输不同,HTTP(Hypertext Transfer Protocol,超文本传输协议)与 Linux/Unix 一样都是是基于文本的,这也导致其在传输过程中十分容易被解析和篡改,于是 HTTPS(Hypertext transfer protocol secure)应运而生。HTTP 协议一般跑在 TCP 协议之上,而 HTTPS 协议就是在原来的 TCP 和 HTTP 之间增加一层加密的操作,加密解密由 Clinet 端和 Server 端完成,由此保证了 HTTP 协议的内容不被中间人获取和篡改。可见 HTTPS 的核心就是这一段加密和解密,它是通过 SSL(Secure Sockets Layer)安全套接层和 TLS(Transport Layer Security)传输层安全协议实现的。

SSL 与 TLS
网景公司在 1994 推出 HTTPS 协议,由 SSL 协议进行加密,这就是 SSL 的起源。SSL 有 1.0、2.0、3.0 标准,后来 IETF 将 SSL 标准化并称其为 TLS,因此 SSL 与 TLS 其实是一个东西。SSL 和 TLS 的一些版本如下

协议发布时间状态
SSL 1.0未公布未公布
SSL 2.01995 年已于 2011 年弃用
SSL 3.01996 年已于 2015 年弃用
TLS 1.01999 年已于 2021 年弃用
TLS 1.12006 年已于 2021 年弃用
TLS 1.22008 年-
TLS 1.32018 年-

TLS 加密的核心在非对称加密,关于非对称加密的原理可以参考文章 “不给力啊,老湿!”:RSA 加密与破解 中所解释的(在此缅怀 Vamei 大师,他的文章帮助了无数人),在这里我们只需要知道非对称加密和传统加密解密过程的差异就可以了。

对称加密与非对称加密

一般加密都是对称加密,即存在一个秘钥,加密方将数据通过秘钥加密,之后将加密后的数据交给接收方。接收方拿到了数据之后使用同一个秘钥将加密数据解密,就能得到原始数据了。它的问题在于发送方无法将秘钥交给接收方,因为秘钥在传输过程中可能就已经泄漏了,一旦秘钥泄漏数据也就不再安全。

非对称加密则不一样,它存在两个秘钥,一个称之为公钥,另一个相对应的叫私钥。加密方只需要将数据通过公钥加密,之后公钥无法解密数据,只有私钥才能解密数据。这样接收方只需要事先将公钥交给加密方,甚至直接将公钥广播出去,然后发送方将数据用公钥加密后交给接收方,最后接收方拿到数据之后用只有自己才有的私钥将数据解密即可。非对称加密解决了秘钥泄漏的问题,只需要将公钥交出去而保管好私钥,就避免了秘钥在传输过程中一旦泄漏密文全部不安全的问题。

对称加密与非对称加密的结合

HTTPS 同时用到了对称加密和非对称加密。我们知道对称加密需要双方知道同一个秘钥,HTTPS 协议通过非对称加密先保证双方拥有一个安全的对称加密秘钥,之后双方就使用传统的对称加密进行数据加密传输并解密即可。那么 HTTPS 是如何让双方获得一个安全的对称加密秘钥的呢,这就是非对称加密的工作了,它的流程如下

  1. 首先,服务端拥有非对称加密公钥(PubKey)和私钥(PriKey)一对
  2. 客户端发起请求,服务端将 PubKey 发送给客户端
  3. 客户端生成一个对称加密秘钥 Key,并将这个秘钥 Key 通过服务端的 PubKey 进行加密,将加密后的数据发给服务端
  4. 服务端用自己的 PriKey 将客户端发过来的数据解密,拿到客户端生成的对称秘钥 Key
  5. 客户端服务端后面使用这个对称密钥 Key 进行加密通信即可

看似很完美是吧,但其实上面流程存在着一个问题。

中间人攻击(Man-in-the-middle attack,MITM)

如果客户端和服务端中间存在着一个中间人,它可以实现窃取并篡改数据。具体流程如下

  1. 首先,服务端拥有非对称加密公钥(PubKey)和私钥(PriKey)一对
  2. 存在一个中间人(MITM),他自己也拥有一套公私钥,我们称之为 PubKeyMITM 和 PriKeyMITM
  3. 客户端向服务端发起请求
  4. 请求没有到达服务端而是到了 MITM,MITM 此时也向服务端发起请求
  5. 服务端将 PubKey 返回给中间人,中间人将 PubKeyMITM 返回给客户端
  6. 客户端生成一个对称加密秘钥 Key,并将这个秘钥使用中间人的 PubKeyMITM(客户端会以为这是服务端的 PubKey)加密发给中间人
  7. 中间人用自己的 PriKeyMITM 将客户端加密后的对称密钥 Key 解析出来,然后用服务端的 PubKey 将秘钥 Key 加密,并发送给服务端
  8. 服务端用自己的 PriKey 将中间人发送过来的加密数据解密,得到客户端生成的对称加密 Key

据此,客户端、中间人、服务端都知道了对称加密秘钥 Key,之后中间人可以在客户端和服务端之间获得加密数据并解密,得到了数据之后它也可以篡改一部分再使用对称秘钥 Key 加密了发给另一方。由此,MITM 窃取到了数据并且可以篡改数据,数据安全性荡然无存。

那我们该怎么办呢?分析一下可以知道,出现如下问题的根本原因在于客户端没有核实服务端的身份就把 Key 发过去了,只需要客户端验证一下服务端的身份,如果服务端身份有问题(例如上面的 MITM)就拒绝后续的操作。在现实世界我们使用签名来验证一个人的身份,在 HTTPS 中也是类似的方式,叫做数字签名

证书颁发机构(Certificate Authority,CA)

在 HTTPS 中,服务器想要支持 HTTPS,需要先去 CA 申请一份数字证书,数字证书包含了证书持有者身份、证书有效期、公钥等等信息。在客户端向服务端发起请求之后,服务端其实返回的不仅仅是一个公钥,而是一份完整的数字证书(公钥也包含在证书里面了)。客户端接收到了数字证书之后,对证书的信息进行校验,例如是否过期等等。数字证书的格式一般都是基于 X.509 标准。

但是单纯的证书并不能保证服务端身份的可信,因为中间人也可以先拿到服务端的证书,然后替换证书的过期时间、服务端公钥等信息,之后生成自己的证书再发给客户端。数字证书实现服务端身份可靠性的方式是通过数字签名实现的,具体方式如下

  1. CA 机构自己也有一套公私秘钥
  2. 在签发证书时,CA 会根据数字证书的信息计算出这些信息对应的哈希值
  3. 将哈希值使用 CA 的私钥进行加密,得到的结果就是这个证书的数字签名

这样一来,数字证书就包含了两部分:

  1. 数字证书的信息,例如持有者身份、过期时间、服务端公钥等等;
  2. 由以上信息计算得到的并通过 CA 私钥加密过的证书数字签名

因此,客户端在拿到了数字证书之后,只需要类似的将证书信息计算一下得到哈希值,之后用 CA 机构的公钥解密那个随数字证书携带而来的数字签名,查看数字签名解密出来的值和自己计算的哈希值是否一致,如果一致就说明这份数字证书没有被篡改过。

客户端是如何知道 CA 机构的公钥的?这一般是随操作系统或浏览器自带的,也就是说存在客户端本地的。

我们知道,原始数据一旦发生了变化,则哈希值必然发生变化,只有证书本身信息没有经过任何修改,那么它的哈希值才不会变,签名也才不会变。因此只要签名正确,证书的信息就正确,只要签名对不上,那么证书就有问题。

其实哈希算法本身是一定会有冲突的,所以原始数据发生了变化则哈希值必然发生变化这句话是不严谨的,可能会出现两份数据拥有同一个哈希值的情况。例如 MD5 和 SHA-1 就已经被王小云教授证明是不安全的,可以考虑使用 SHA256 等更安全的哈希算法。

有了数字签名之后,中间人就无法修改数字证书的任何信息了,中间人可以获取到数字证书,但是却不能修改它。因为只要中间人修改了数字证书,数字证书的哈希值就一定会发生变化,因为中间人又没有 CA 机构的私钥,所以它即使重新计算了数字证书的哈希值,也没办法重新生成一份新的数字签名。假如客户端收到了一份被篡改信息的证书,那么新证书的哈希值和原始证书的哈希值计算出来的数字签名的解密结果肯定是不一样的,此时客户端就可以拒绝后续的操作。因为中间人无法创建数字签名,它也就无法修改数字证书的任何内容,那么客户端就一定能拿到真正服务端公钥,这样数据传输就是绝对安全的了。

整个 HTTPS 通信的流程大致如下

Server Name Indication(SNI)

Server Name Indication(SNI)是 TLS 的一个扩展协议,由于 TLS 连接创建在 HTTP 报文发送之前,所以服务器无法再像以前那样通过 HTTP 请求报文中的 HOSTNAME 来区分一个 IP 地址和端口对应的多个主机域名(即虚拟主机,在一个端口上跑多个 web 服务)。

在 HTTP 协议的早期,一个主机只能运行一个 HTTP 服务,因为主机的 IP 地址+80 端口无法区分多个主机。后来人们在 HTTP 的请求中添加了 HOSTNAME 即主机名,主机通过这个这个主机名来把多个 HTTP 服务区分开来,这样一台主机也可以运行多个不同的 HTTP 服务了。但是 HTTPS 破坏了这一方案,因为 HTTPS 的数据是加密的,主机无法知道这个请求它所希望访问的目标服务是哪个,所以早期一台主机只能部署一个 HTTPS 服务。

解决办法是将 HOSTNAME 放在 SNI 中,由客户端发送给服务端,这样服务端就知道客户端是想要与哪一个主机创建连接了。不过这也产生了一个问题,即 SNI 无法保证安全,被中间人攻击(MITH attack)的时候中间人能够知道客户端想要访问的主机名。

数字证书的格式标准:X.509

我们已经知道,服务器在给客户端发送公钥的时候会把公钥放在一个数字证书里面,这个数字证书还会附带有一个通过 CA 计算出来的数字签名,同时还包含了一些其它的有用信息,服务端只需要把这个包含了大量数据的数字证书发送给客户端即可。那么这个数字证书的格式是什么样的呢?数字证书的格式是由 X.509 标准设计的,它的格式大致如下

Certificate
    Version Number
    Serial Number
    Signature Algorithm ID
    Issuer Name
    Validity period
        Not Before
        Not After
    Subject name
    Subject Public Key Info
        Public Key Algorithm
        Subject Public Key
    Issuer Unique Identifier (optional)
    Subject Unique Identifier (optional)
    Extensions (optional)
...
Certificate Signature Algorithm
Certificate Signature

其中比较重要的就是公钥、私钥持有人身份、签名、有效时间等等信息。数字证书在文件系统中一般以DERPEM格式保存,其中 DER 是二进制格式,PEM 是对 DER 格式做了 base64 计算后得到的文本格式。常见的证书格式如下

格式含义
.pemPEM 格式
.keyPEM 格式的私钥文档
.pubPEM 格式的公钥文档
.crtPEM 格式的公钥证书文档,也可能是 DER
.cerDER 格式的公钥证书文档,也可能是 PEM

我们最常见的就是 pem(Privacy Enhanced Mail)格式的文档,例如如下的 Nginx 服务器的配置就使用了大量的 PEM 文件

server {
    listen      443 ssl http2;
    listen [::]:443 ssl http2;
    server_name  www.nosuchfield.com;

    ssl on;
    ssl_certificate     /etc/letsencrypt/live/www.nosuchfield.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.nosuchfield.com/privkey.pem;
    ssl_dhparam         /etc/ssl/certs/dhparams.pem;
    ssl_protocols       SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        autoindex            on;
        autoindex_exact_size off;
        autoindex_localtime  on;
        charset              utf-8;
        root                 /home/www;
    }

    auth_basic "Private Property";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

它的 HTTPS 的配置含义如下

配置含义
cert.pem服务端证书
chain.pem浏览器需要的所有证书但不包括服务端证书,比如根证书和中间证书
fullchain.pem包括了 cert.pem 和 chain.pem 的内容
privkey.pem证书的私钥
ssl_protocols服务端支持的 SSL/TLS 协议版本
ssl_ciphers支持的加密算法,具体语法参考

例如如上服务器的 fullchain.pem 文件就如下(隐去了证书内容)

-----BEGIN CERTIFICATE-----
xxx
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
xxx
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
xxx
-----END CERTIFICATE-----

privkey.pem 文件如下

-----BEGIN PRIVATE KEY-----
xxx
-----END PRIVATE KEY-----

SSH 的公私钥

SSH 协议也会有一对公私钥,一般可以通过命令ssh-keygen生成,默认生成在~/.ssh文件夹下,包含一个id_rsa和一个id_rsa.pub文件。

公钥使用空格分为了三部分

  1. ssh-rsa
  2. 公钥本身,使用了 base64 进行编码,包含了一些质数计算的信息
  3. 邮箱地址

私钥是 DER 格式的(和前面的数字证书使用一样的格式),中间部分的内容信息一样是 base64 编码的,同样包含了一些质数计算信息。

这里的公钥私钥是不能互换的,虽然在数学上 RSA 的两个秘钥是对等的,即任意一个秘钥加密的结果都可以用另一个秘钥解密。但是在工程上两个秘钥并不对等,因为工程上的秘钥既包含了数学秘钥也包含数学公钥,而工程公钥只包含了数学公钥,因此一旦互换程序直接报错了。当然,是可以手动的把工程公钥私钥的数学公钥私钥解析出来自己计算的,这是没有问题的。

参考

大前端进阶系列之 HTTPS 详解
HTTPS 公钥到底存放在哪里
HTTPS 是如何保护你的安全的
X.509 公钥证书的格式标准
ssh-keygen 生成的 id_rsa 文档的格式
在 RSA 加密中既然公钥和私钥是可逆的,为什么都是把公钥给别人,而不把私钥给别人,自己保存好公钥?