与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.0 | 1995年 | 已于2011年弃用 |
SSL 3.0 | 1996年 | 已于2015年弃用 |
TLS 1.0 | 1999年 | 已于2021年弃用 |
TLS 1.1 | 2006年 | 已于2021年弃用 |
TLS 1.2 | 2008年 | - |
TLS 1.3 | 2018年 | - |
TLS加密的核心在非对称加密,关于非对称加密的原理可以参考文章“不给力啊,老湿!”:RSA加密与破解中所解释的(在此缅怀Vamei大师,他的文章帮助了无数人),在这里我们只需要知道非对称加密和传统加密解密过程的差异就可以了。
一般加密都是对称加密,即存在一个秘钥,加密方将数据通过秘钥加密,之后将加密后的数据交给接收方。接收方拿到了数据之后使用同一个秘钥将加密数据解密,就能得到原始数据了。它的问题在于发送方无法将秘钥交给接收方,因为秘钥在传输过程中可能就已经泄漏了,一旦秘钥泄漏数据也就不再安全。
非对称加密则不一样,它存在两个秘钥,一个称之为公钥,另一个相对应的叫私钥。加密方只需要将数据通过公钥加密,之后公钥无法解密数据,只有私钥才能解密数据。这样接收方只需要事先将公钥交给加密方,甚至直接将公钥广播出去,然后发送方将数据用公钥加密后交给接收方,最后接收方拿到数据之后用只有自己才有的私钥将数据解密即可。非对称加密解决了秘钥泄漏的问题,只需要将公钥交出去而保管好私钥,就避免了秘钥在传输过程中一旦泄漏密文全部不安全的问题。
HTTPS同时用到了对称加密和非对称加密。我们知道对称加密需要双方知道同一个秘钥,HTTPS协议通过非对称加密先保证双方拥有一个安全的对称加密秘钥,之后双方就使用传统的对称加密进行数据加密传输并解密即可。那么HTTPS是如何让双方获得一个安全的对称加密秘钥的呢,这就是非对称加密的工作了,它的流程如下
看似很完美是吧,但其实上面流程存在着一个问题。
如果客户端和服务端中间存在着一个中间人,它可以实现窃取并篡改数据。具体流程如下
据此,客户端、中间人、服务端都知道了对称加密秘钥Key,之后中间人可以在客户端和服务端之间获得加密数据并解密,得到了数据之后它也可以篡改一部分再使用对称秘钥Key加密了发给另一方。由此,MITM窃取到了数据并且可以篡改数据,数据安全性荡然无存。
那我们该怎么办呢?分析一下可以知道,出现如下问题的根本原因在于客户端没有核实服务端的身份就把Key发过去了,只需要客户端验证一下服务端的身份,如果服务端身份有问题(例如上面的MITM)就拒绝后续的操作。在现实世界我们使用签名来验证一个人的身份,在HTTPS中也是类似的方式,叫做数字签名。
在HTTPS中,服务器想要支持HTTPS,需要先去CA申请一份数字证书,数字证书包含了证书持有者身份、证书有效期、公钥等等信息。在客户端向服务端发起请求之后,服务端其实返回的不仅仅是一个公钥,而是一份完整的数字证书(公钥也包含在证书里面了)。客户端接收到了数字证书之后,对证书的信息进行校验,例如是否过期等等。数字证书的格式一般都是基于X.509标准。
但是单纯的证书并不能保证服务端身份的可信,因为中间人也可以先拿到服务端的证书,然后替换证书的过期时间、服务端公钥等信息,之后生成自己的证书再发给客户端。数字证书实现服务端身份可靠性的方式是通过数字签名实现的,具体方式如下
这样一来,数字证书就包含了两部分:
因此,客户端在拿到了数字证书之后,只需要类似的将证书信息计算一下得到哈希值,之后用CA机构的公钥解密那个随数字证书携带而来的数字签名,查看数字签名解密出来的值和自己计算的哈希值是否一致,如果一致就说明这份数字证书没有被篡改过。
客户端是如何知道CA机构的公钥的?这一般是随操作系统或浏览器自带的,也就是说存在客户端本地的。
我们知道,原始数据一旦发生了变化,则哈希值必然发生变化,只有证书本身信息没有经过任何修改,那么它的哈希值才不会变,签名也才不会变。因此只要签名正确,证书的信息就正确,只要签名对不上,那么证书就有问题。
其实哈希算法本身是一定会有冲突的,所以原始数据发生了变化则哈希值必然发生变化这句话是不严谨的,可能会出现两份数据拥有同一个哈希值的情况。例如MD5和SHA-1就已经被王小云教授证明是不安全的,可以考虑使用SHA256等更安全的哈希算法。
有了数字签名之后,中间人就无法修改数字证书的任何信息了,中间人可以获取到数字证书,但是却不能修改它。因为只要中间人修改了数字证书,数字证书的哈希值就一定会发生变化,因为中间人又没有CA机构的私钥,所以它即使重新计算了数字证书的哈希值,也没办法重新生成一份新的数字签名。假如客户端收到了一份被篡改信息的证书,那么新证书的哈希值和原始证书的哈希值计算出来的数字签名的解密结果肯定是不一样的,此时客户端就可以拒绝后续的操作。因为中间人无法创建数字签名,它也就无法修改数字证书的任何内容,那么客户端就一定能拿到真正服务端公钥,这样数据传输就是绝对安全的了。
整个HTTPS通信的流程大致如下
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)的时候中间人能够知道客户端想要访问的主机名。
我们已经知道,服务器在给客户端发送公钥的时候会把公钥放在一个数字证书里面,这个数字证书还会附带有一个通过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 AlgorithmCertificate Signature
其中比较重要的就是公钥、私钥持有人身份、签名、有效时间等等信息。数字证书在文件系统中一般以DER或PEM格式保存,其中DER是二进制格式,PEM是对DER格式做了base64计算后得到的文本格式。常见的证书格式如下
格式 | 含义 |
---|---|
.pem | PEM格式 |
.key | PEM格式的私钥文档 |
.pub | PEM格式的公钥文档 |
.crt | PEM格式的公钥证书文档,也可能是DER |
.cer | DER格式的公钥证书文档,也可能是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-keygen
生成,默认生成在~/.ssh
文件夹下,包含一个id_rsa
和一个id_rsa.pub
文件。
公钥使用空格分为了三部分
私钥是DER格式的(和前面的数字证书使用一样的格式),中间部分的内容信息一样是base64编码的,同样包含了一些质数计算信息。
这里的公钥私钥是不能互换的,虽然在数学上RSA的两个秘钥是对等的,即任意一个秘钥加密的结果都可以用另一个秘钥解密。但是在工程上两个秘钥并不对等,因为工程上的秘钥既包含了数学秘钥也包含数学公钥,而工程公钥只包含了数学公钥,因此一旦互换程序直接报错了。当然,是可以手动的把工程公钥私钥的数学公钥私钥解析出来自己计算的,这是没有问题的。
大前端进阶系列之HTTPS详解
HTTPS公钥到底存放在哪里
HTTPS 是如何保护你的安全的
X.509 公钥证书的格式标准
ssh-keygen生成的id_rsa文档的格式
在 RSA 加密中既然公钥和私钥是可逆的,为什么都是把公钥给别人,而不把私钥给别人,自己保存好公钥?