SSL(Secure Sockets Layer,安全套接层)协议最初由 Netscape(网景公司)在 1994 年设计并开发,为了给 HTTP 提供一个安全的传输层。
到 1999 年,因为 SSL 应用广泛,已经成为 Internet 上的事实标准。所以 IETF(Internet Engineering Task Force,互联网工程任务组)在 SSL 的基础上将其标准化为 TLS(Transport Layer Security,传输层安全协议)协议。
目前,我们最常见的是 TLS v1.2 版本,而最新的 v1.3(2018 年,RFC8446)版本有望成为有史以来最安全,但也最复杂的 TLS 协议。
TLS 协议是 CIAA 网络安全模型的具体实现,基于混合加密方案和 PKI/CA 数字证书技术制定了一套安全通信协议标准,所以理解 TLS 协议之前需要先对 CIAA 安全模型有一定的认识。
TLS 主要由 2 个部分组成:
TLS 记录数据(TLS Record):是一种数据结构,用于将应用层协议的数据分割成多个 TLS Records。数据结构的概览如下图所示:
TLS 握手协议(TLS Handshake):是一种协议,用于在 TLS 通信的初始阶段进行身份验证和协商安全参数。协议处理流程如下图所示:
当 TCP 连接建立后,TLS 握手的第一步由 Client 发起,发送 ClientHello Msg 到 Server。
Client Hello Msg 包含以下内容:
Server 收到了 ClientHello Msg 后,比对 Server 支持的 TLS 版本和 Cipher Suites,然会回复 ServerHello Msg,以此来完成 Cipher Suites 协商阶段。
Server Hello Msg 包含以下内容:
ssl_ciphers HIGH:!aNULL:!MD5;
配置项。可选的 Session ID 会话恢复,是指在一次完整协商的连接断开时,Client 和 Server 都会将 TLS Session 协商的安全参数保留一段时间,用于后续的会话恢复,起到类似 Cache 的功能。希望恢复会话的 Client 需要将相应的 Session ID 放入 Client Hello Msg 发出。如果 Server 愿意恢复会话,则将相同的 Session ID 放入 Server Hello Msg 返回。
Server Certificate Msg 包含以下内容:
Server Hello Done Msg:向 Client 发送 Server Hello Done 表示 Hello 协商阶段完成。
Client 收到 Server Hello Msg 后,会对收到的 Server CA 证书进行合法性验证。只有通过验证才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证的内容包括:
Client 完成了 Server CA 证书的合法性验证之后,就可以从 Server CA 证书中得到了 Server Public Key,继而开始非对称加密行为。具体采用的非对称加密算法由 Server 选择的 Cipher Suites 决定,例如:ECDHE。
Client Key Exchange Msg:内含了 Client 本地运算生成的 Pre-Master 随机数,并用 Server Public Key 加密,然后发送给 Server,再由 Server Private Key 解密。
此时,Client 就获得了混合加密方案所需要的全部通信密钥信息,包括:
Fuc(random_C, random_S, Pre-Master)
计算得到。使用 3 个随机数来计算,一方面为了防止 “random_C” 被中间人猜出,另一方也增加 Master Secret 的随机性。Change Cipher Spec Msg:Client 通知 Server 后续的通信都采用协商的通信密钥和加密算法进行加密通信。
Encrypted Handshake Msg / Finished Msg:TLS v1.2 采用了 MAC(Message authentication code,消息认证码)来确保 Handshake 流程未被篡改。具体的行为是:对 Client 之前已经发送过的所有 Msgs,再次使用协商好的 Master Secret 对称密钥进行 HASH 计算得到数字摘要,最后发送给 Server 用于最后的安全握手验证。
Server 收到 Client Key Exchange Msg 后,使用 Server Private Key 解密得到 Client 的 Pre-Master 随机数,并基于之前交换的两个明文随机数 random_C 和 random_S,同样可以计算出协商 Master Secret 对称密钥。
Server 收到 Encrypted Handshake Msg 后,同样通过协商好的加密算法进行解密,然后再对 Server 收到的所有 Msgs 使用同样的 Master Secret 对称密钥进行一次数字摘要计算。
最后通过对比数据签名,来最终验证数据的完整性。如果失败,则触发致命的 Alert 异常:Bad Record MAC(Message authentication code,消息认证码),表示安全通道建立过程中有恶意篡改行为。
Change Cipher Spec Msg:验证通过之后,Server 同样发送该消息以告知 Client 后续的通信都采用协商的通信密钥与算法进行加密通信。
Encrypted Handshake Msg / Finished Msg:Server 也会以同样的方式计算出数字签名并发送给 Client。
至此,Client 和 Server 双方都拥有了合法的 Master Secret 对成密钥,接下来进入到业务数据的对称加密安全传输阶段。整个过程通常需要几百毫秒。
相对于 TLS v1.2 而言,v1.3 是迄今为止最大的一次改动,主要的改进目的如下:
引入的新特性如下:
目前最新的 Chrome 和 Firefox 都已支持 TLS 1.3,但需要手动开启。下面是各大浏览器的 TLS 1.3 支持情况:
测试网站是否开启了 TLS 1.3:
$ git clone --depth 1 https://github.com/drwetter/testssl.sh.git
$ cd testssl.sh
$ ./testssl.sh -p <domainName>
Testing protocols via sockets except NPN+ALPN
SSLv2 not offered (OK)
SSLv3 not offered (OK)
TLS 1 offered
TLS 1.1 offered
TLS 1.2 offered (OK)
TLS 1.3 offered (OK): draft 28, draft 27, draft 26
NPN/SPDY h2, spdy/3.1, http/1.1 (advertised)
ALPN/HTTP2 h2, spdy/3.1, http/1.1 (offered)
TLS v1.2 中的 Handshake 流程并没有实现完全加密,协商加密算法类型和随机数阶段、以及握手结束(Encrypted Handshake Msg / Finished Msg)都是明文的,通过对称 MAC(Message authentication code,消息认证码)来确保握手未被篡改。
这种疏忽导致了许多备受瞩目的安全漏洞(e.g. FREAK、LogJam etc.),所以在 TLS v1.3 中,会对整个握手进行非对称加密。
以最简单的 FREAK 攻击为例,它利用了以下 2 个漏洞:
使得 FREAK 中间人可以篡改 Client 和 Server 最终选用的 Supported ciphers(加密套件),让双方都降级了加密强度(HIGH => LOW => EXPORT)。然后中间人就可以暴力破解 Encrypted Handshake Message 得到 3 个随机数,并计算出 Master Secret 对称密钥信息 ,最终就可以伪造了彼此的 Finished Message,即 MAC 值。如下图所示。
在 TLS v1.3 中,这种类型的安全降级攻击是不可能的,因为整个握手流程都进行了加密,同时 v1.3 还删除了那些不安全的加密算法,包括:
RSA 非对称加密算法是由 Rivest,Shamir 和 Adelman 在 1977 年发现的,一直都被视为是密码学领域的重大成就之一。但现在看来,RSA 存在不满足前向保密(Forward Secret)的严重缺陷。即:如果中间人记录存储了加密对话数据,后面假如某一天中间人通过 Heartbleed(心脏出血)之类的技术窃取了 Server 的 RSA Private Key,那么中间人依然可以将对话解密。
因此,TLS 1.3 移除了 RSA,而仅采用了临时 Diffie-Hellman 作为唯一的秘钥交换机制。
临时 Diffie-Hellman 由 Diffie 和 Hellman 在 1976 年发明,要求 Client 和 Server 都创建一对非对称密钥,并且都交换彼此的 Public Key,一旦收到了对方的 Public Key,那么就会与自己的 Private Key 进行组合,最后以相同的 Pre-Master Secret 值作为结尾。
所谓 “临时”,指的是在每个 TLS 会话中,协商密钥所使用的 Pre-Master Secret 参数都是临时生成的,可以实现每个会话的密钥唯一。因此,即使在以后的时间里,攻击者获得了以前的临时密钥,也无法利用这些密钥来破解之前或之后的会话。即使一个密钥被泄漏,也不会影响其他会话的安全性。
在 Web 领域,传输延迟(Transmission Latency)是重要的性能指标之一,低延迟意味着更流畅的页面加载以及更快的 API 响应速度。而一个完整的 HTTPS 连接的建立大概需要以下 4 步:
可见在 TLS v1.2 中,新建一个完整的 HTTPS 连接最少需要 4 个 RTT(Round-Trip Time 往返时延),而重连则可以通过 TLS 的会话恢复机制节省 1 个 RTT。
在 TLS v1.3 中,由于仅支持向前保密的临时 Diffie-Hellman 对称加密算法,所以 Client 可以在一条 Msg 中就完成 Diffie-Hellman 密钥共享,即:只需要一次往返( 1-RTT )就可以完成握手。
另外,TLS v1.3 在会话恢复时,Client 会将 Server 发送过来的 Session Ticket 进行计算,组成一个新的 PSK (PreSharedKey,预共享密钥)。Client 将 PSK 缓存在本地。会话恢复时,在 Client Hello Msg 带上 PSK 扩展,同时通过之前 Client 发送的 Finished Msg 计算出 Resumption Secret(恢复密钥)。通过该密钥加密数据发送给 Server,然后 Server 就会从 Session Ticket 中算出 PSK,使用它来解密刚才发过来的加密数据。至此完成了该 0-RTT 会话恢复的过程。
并非所有的 Client 和 Server 都支持相同版本的 TLS,因此大多数 Server 都会同时支持多个版本,并且进行协商。TLS 的版本协商非常简单。Client 会通知 Server 它支持的协议的最新版本,Server 则会回复支持的协议版本,如果存在交集则协商成功。否则,连接失败。虽然版本协商的过程很简单,但事实证明,很多连接场景并未能正确地实现这一功能,从而导致安全事故。
OpenSSL 最新的 1.1.1 版本提供了 TLS 1.3 的支持,而且和 1.1.0 版本完全兼容。在特定的 Linux 发行版中,可能需要手动安装。
以 CentOS7 为例,检查是否开启了 TLS 1.3:
$ openssl s_client --help | grep tls1_3
如果没有,则需要手动安装:
$ cd /opt
$ wget https://github.com/openssl/openssl/archive/OpenSSL_1_1_1-stable.zip
$ unzip OpenSSL_1_1_1-stable.zip
$ ./config enable-tls1_3 --prefix=/usr/local/openssl
$ make && make install
$ mv /usr/bin/openssl /usr/bin/openssl.old
$ mv /usr/lib64/openssl /usr/lib64/openssl.old
$ mv /usr/lib64/libssl.so /usr/lib64/libssl.so.old
$ ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl
$ ln -s /usr/local/openssl/include/openssl /usr/include/openssl
$ ln -s /usr/local/openssl/lib/libssl.so /usr/lib64/libssl.so
$ echo "/usr/local/openssl/lib" >> /etc/ld.so.conf
$ ldconfig -v
$ openssl version
$ openssl s_client --help | grep tls1_3
HTTPS 就是 HTTP 与 TLS 的组合,本质为 HTTP over SSL/TLS。在以往的文章中,我们已经分别介绍了 HTTP 和 TLS 协议,所以在这里主要关注 HTTPS 的 2 种安全认证方式,并梳理 HTTPS 连接建立流程。
双向认证和单向认证类似,主要是额外增加了 Server 对 Client 的认证。如下图红色部分。
早期的 SSLv2 根据经典的 PKI 标准进行设计,它默认认为一台 HTTP Server(或者说一个 IP 地址)只会提供一个服务(只有一个 Domain Name),所以在 SSL 握手时,Client 无需指明具体的 Domain Name,Server 就会把默认的 CA 证书返回。
然而在 Apache 等 HTTP Server 中应用了 VirtualHosts 之后,就出现了一个 IP 会对应多个 Domain Name 的情况。为了支持 VirtualHosts,HTTP/1.1 Header 协议增加了 Host 字段,相应的 TLS 也需要类似的手段,否则会出现 “公共名称不匹配错误(Unable to communicate securely with peer: requested domain name does not match the server’s certificate.)”。即:虽然 Client 到达了 HTTP Server 的正确 IP 地址,但由于 CA 证书上的 Domain Name 与 Web 的 Domain Name 不匹配导致无法建立安全连接。
为了解决这个问题,TLS 在 v1.0 版本中增加了 SNI(Server Name Indication,服务器名称指示,RFC 4366)扩展,它包含在 TLS Hello 握手流程中,以确保 Client 能够指定要访问的 Domain Name。举例来说,假设:网站 https://www.example.com、https://www.something.com、https://www.another-website.com、https://www.example.io 被托管在相同的 HTTP Server 中,通过基于 Hostname 的 VirtualHosts 对外提供服务。此时。,如果 TLS 的 SNI 扩展指定为 https://www.example.com,那么 HTTP Server 就会返回 https://www.example.com 的 CA 证书,继而建立正确的安全连接。
SSLKEYLOGFILE=ssl_log.txt curl \
--cacert ~/ca_01.pem \
--resolve www.app1.com:443:172.18.22.68 \
-X GET "https://www.app1.com:443/" \
-H 'Content-type: application/json' \
-H 'Accept: application/json' \
-H 'host: www.app1.com'
SNI 作为 TLS Client Hello Msg 的 扩展字段,这意味着在 TLS v1.2 中,SNI 是明文的。也就是说,任何监视 Client 和 Server 之间连接的攻击者都可以读取到 SNI 信息,并以此了解到 Client 正在与哪个 Domain Name 建立 HTTPS 连接,即便攻击者无法解密进一步的通信内容,但攻击者仍然可以利用 SNI 信息,例如:建立一个钓鱼网站来欺骗用户。
由此,就提出了 ESNI(加密的 SNI),通过加密 client_hello 消息的 SNI 部分(仅此部分),来保护 SNI 的私密性,确保攻击者无法监视到 SNI 明文信息。另外,ESNI 的加密密钥必须以其他方式进行传输。
具体而言,HTTP Server 会在其 DNS 记录中添加一个用于 ESNI 的 Public Key。这样,当 Client 查找到正确的 HTTP Server IP 地址时,同时也能得到对应的 Public Key。
例如:当用户在浏览器中输入 URL:https://www.bobisawesome.example.com 时,浏览器将通过以下流程加载网站:
但需要注意的是,即表如此 ESNI 也并非是绝对安全的,因为常规的 DNS 通信未加密,存在 “地址簿伪装“ 攻击风险。即使安装了 ESNI,攻击者仍然可以查看用户正在查询的 DNS 记录,并确定他们正在访问哪些网站。
所以更进一步的,还可能需要安全的 DNS 方案,常见的有:
安装编译环境:
$ yum -y groupinstall "Development Tools"
$ yum -y install libev libev-devel zlib zlib-devel openssl openssl-devel git
安装 OpenSSL:
$ mkdir /var/tmp
$ cd /var/tmp
$ wget https://openssl.org/source/openssl-1.0.2.tar.gz
$ tar -zxf openssl-1.0.2.tar.gz
$ cd openssl-1.0.2
$ mkdir /opt/openssl
$ ./config --prefix=/opt/openssl
$ make
$ make test
$ make install
安装 nghttp2:
$ git clone https://github.com/tatsuhiro-t/nghttp2.git
$ cd nghttp2
$ autoreconf -i
$ automake
$ autoconf
$ ./configure
$ make
$ make install
$ echo '/usr/local/lib' > /etc/ld.so.conf.d/custom-libs.conf
$ ldconfig
$ ldconfig -p| grep libnghttp2
安装 curl:
$ cd /var/tmp
$ git clone https://github.com/bagder/curl.git
$ cd curl
$ ./buildconf
$ ./configure --with-ssl=/opt/openssl --with-nghttp2=/usr/local --disable-file --without-pic --disable-shared
$ make
验证:
$ /var/tmp/curl/src/curl --version
curl 7.70.0-DEV (x86_64-unknown-linux-gnu) libcurl/7.70.0-DEV OpenSSL/1.0.2o nghttp2/1.41.0-DEV
Release-Date: [unreleased]
Protocols: dict ftp ftps gopher http https imap imaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS HTTP2 HTTPS-proxy IPv6 Largefile NTLM NTLM_WB SSL TLS-SRP UnixSockets
curl 从 7.52.0 版本开始也已经支持 TLS 1.3 了,curl 7.61.0 及以上在 TLS 握手过程中协商 TLS 版本时,curl 默认使用 TLS 1.3,但也取决于 curl 正在使用的 TLS 库及其版本,例如:要求 OpenSSL 1.1.1 版本以上。
安装新版 libcurl 的 yum 源:
$ rpm -ivh http://mirror.city-fan.org/ftp/contrib/yum-repo/city-fan.org-release-1-13.rhel6.noarch.rpm
升级:
$ yum upgrade libcurl
升级完成后可以卸载此 yum 源:
$ rpm -e city-fan.org-release
curl [options] [URL...]
由以上过程可以知道,没有 SNI 的情况下,服务器无法预知客户端到底请求的是哪一个域名的服务。SNI 的 TLS 扩展通过发送虚拟域名做为 TLS 协商的一部分修正了这个问题,在 Client Hello 阶段,通过 SNI 扩展,将域名信息提前告诉服务器,服务器根据域名取得对应的证书返回给客户端已完成校验过程。
curl 7.18.1+ & openssl 0.9.8j+ 的组合就可以支持 SNI 了:
curl \
--cacert /root/CA/nginx1.com/cacert.pem \
-X GET "https://webserver.com:8443/" \
-H 'Content-type: application/json' \
-H 'Accept: application/json' \
-H 'host: nginx1.com'
如果没有配置 DNS 解析的话可以使用 curl 7.21.3 支持的 --resolve 参数:
curl \
--cacert /root/CA/nginx1.com/cacert.pem \
--resolve webserver.com:8443:127.0.0.1 \
-X GET "https://webserver.com:8443/" \
-H 'Content-type: application/json' \
-H 'Accept: application/json' \
-H 'host: nginx1.com'
–resolve 主要用于直接定位到 IP 地址进行访问,对于一个 Domain Name 有多个服务器(多个不同的 IP)的服务来说,这个参数可以指定的访问某个设备。