1.前言

15年还在实习的时候,曾看过一篇微信智能心跳的文章,里面谈到心跳设计参考了仍处于Draft的TLS 1.3,最终效果很不错。2018年8月,包含TLS 1.3最终标准的文档RFC8446发布,这时距离TLS 1.2(RFC5246)发布已经过去了10年。

10年的期间,移动设备登上舞台,Let’s Encrypt成立,斯诺登曝光棱镜门,量子计算问世,同时还有大量的新协议和加密组件诞生,例如HTTP2、ChachaPoly1305、DNSSEC……每个新的协议诞生时,都会用上 state-of-the-art 来形容自己,但借用推特上看到的一句话:A hack is not a question of if, just a question of when。

证书成本下降,TLS普及让网络上的加密流量越来越多,但TLS不是坚不可摧的,过往发生的泄漏和攻击事件就是最好的证明。这段时间阅读Go源码的TLS 1.3实现,也借机会升级了博客的Nginx和线上的Nginx,还有代理工具,这里做一下关于TLS 1.3的笔记。

2. RFC8446

搜索TLS1.3时,Cloudflare的博客是最常出现在页面中的,看过一堆关于TLS 1.3的介绍后,笔者觉得这一篇文章最适合用入门:A Detailed Look at RFC 8446 (a.k.a. TLS 1.3)

下面记录一下摘要

2.1 TLS的演变

tls13-01

脱胎于90年代的Netscape SSL,交付IETF后改名为TLS,持续迭代至今,不同版本的TLS最初定义对应的RFC文档如下

  • TLS 1.0:RFC 2246
  • TLS 1.1:RFC 4346
  • TLS 1.2:RFC 5246
  • TLS 1.3:RFC 8446

2.2 TLS 1.2穿着降落伞裤和护肩

过去的几年中TLS遇到了很多问题,除代码实现和缺乏测试外,协议本身也存在着问题。

TLS是由工程师使用数学工具设计出来的。在SSL早期时代,整个行业都还在学习如何正确地设计健壮的安全协议。设计TLS的时候,关于如何设计安全身份验证协议的正式论文,例如Hugo Krawczyk的SIGMA,都尚未发布。TLS是90年代的加密技术,现代加密技术的设计风格已经发生了变化。

学者们尝试证明TLS的安全特性,而却发现了一些反例,变成了真正的漏洞,这些缺陷遍布了纯理论的漏洞(SLOTH 和 CurveSwap),可行漏洞(WeakDH, LogJam, FREAK, SWEET32),再到可实现的重大危险漏洞(POODLE, ROBOT)。

2.3 TLS 1.2很慢

TLS握手过程从1999年TLS标准化以来一直保持不变。在加密数据发送之前(或者重新开始之前的连接的时候),必须消耗2-RTT完成握手,与单独使用HTTP通信相比,HTTPS通信会产生明显延迟,这种延迟会对以性能为主的应用产生负面影响。

2.4 定义TLS 1.3

  • 减少握手等待时间
  • 加密更多的握手
  • 增强对跨协议攻击的抵抗力
  • 移除遗留特性

2.5 修正TLS 1.2的缺陷

即使是TLS 1.2也包含了加密学设计早期的陈旧观点。

TLS 1.3的设计目标之一是通过移除潜在危险设计元素来纠正之前的错误。

2.6 修正密钥交换

TLS是一种混合加密系统。它既使用了对称加密,也是用了非对称加密(公钥加密)。在混合加密系统中,非对称加密用于在两端之间建立一个共享秘钥,然后使用该钥创建对称秘钥,用于加密交换的数据。

一般来说,非对称加密慢且开销大(每个操作在微秒到毫秒级),而对称秘钥加密快且开销小(每个操作在纳秒级)。混合加密体系对开销大的操作只做一次,因此可以在极小的开销下发送大量加密数据。在TLS 1.3中许多工作都是关于改善握手部分,使用非对称加密建立对称秘钥就处于这个阶段。

RSA密钥交换

tls13-02

在TLS的RSA秘钥交换中,共享秘钥由客户端来决定,然后将其使用从证书导出的服务端公钥加密,最后发送给服务器。

DH密钥交换

tls13-03

在Diffie-Hellman协议中,客户端和服务器都会创建一个公私钥对,然后他们将公钥部分发送给另一方。当每一方都收到了对方的公钥后,他们使用各自的私钥将其组合,获取到一个相同的值,即 pre-master secret。然后服务器使用数字签名来保证交换的数据没有被篡改。

如果客户端和服务器在每一个密钥交换中都选择一个新的秘钥对,那么这种秘钥交换则称为临时秘钥交换。

两种模式都可以让客户端和服务端得到共享密钥,但是RSA模式不满足前向保密的原则:

  • 如果有人记录了加密对话,然后获取服务器的RSA私钥,则可将对话解密
  • 如果对话先被记录下来,而RSA私钥在未来的某个时候被获取后,也可将对话解密

RSA不仅不支持前向保密,而且容易在实现上出错。

TLS 1.3已经移除了RSA密钥交换,保留临时DH密钥交换作为唯一的密钥交换机制。

DH密钥交换参数

DH密钥交换容易受参数选择影响导致安全性问题,例如使用小的DH参数导致容易被破解并解密会话。

TLS 1.3采用了特定的方式,将DH交换参数限制为已知的安全参数,但仍旧保留了一些选项,防止一旦发现这些参数不安全时却难以更新TLS。

2.7 修正加密组件

混合加密系统的另一半是实际的数据加密。这是通过结合通信双方共享的认证码和对称密钥实现的。

现存有许多加密数据的方法,但其中大多数都是错误的。在任何安全通信模式中,都需要加密性和完整性。对称密钥加密用于同时提供加密和完整性,但在TLS 1.2和更早的版本中,这两部分以错误的方式组合,导致安全漏洞。

对称加密通常有两种主要形式:分组加密和流式加密。

  • 分组加密只加密固定大小的消息,短消息需要做填充,长消息需要做拆分,以匹配可加密的块大小。通过使用分组加密来加密计数器序列并将其作为流来将分组加密转换为流式加密的模式,称为计数器模式
  • 流式加密采用固定大小的密钥并使用它来创建任意长度的伪随机数据流,称为密钥流,加密时通过将密钥流的每个位与明文的相应位进行异或来获取密文,解密时通过将密钥流的每个位与密文的相应位进行异或来获取明文。流式加密在软件上易于实现且运行速度快。

CBC模式加密

tls13-04

tls13-05

CBC是一种使用分组加密来加密任意长度数据的方式。

为了数据被篡改,仅仅加密是不够的,数据还需要受到完整性保护。对于CBC模式加密是通过使用消息验证码(message-authentication code,MAC)来完成的,这类似于带有密钥的校验和。加密性强的MAC具有以下特性:除非知道密钥,否则找到与输入匹配的MAC值几乎是不可能的。

有两种方法可以整合 MAC 和 CBC 模式加密:

  • 可先加密,然后用 MAC 加密生成的密文
  • MAC 加密明文,然后加密其输出文件

在 TLS 中,他们选择后者,MAC-then-Encrypt,事实证明这是一个错误的选择。

AEAD模式加密

tls13-06

AEAD为AE的变种,内部同时实现加密和认证,可让接收者验证所收到消息中已加密和未加密信息的完整性。任何企图将有效加密信息与不同上下文结合的篡改都可通过AEAD发现。

TLS 1.3中已经移除了所有可能导致问题的加密组件和加密模式,不能再使用CBC模式加密或不安全的流式加密。TLS 1.3中唯一允许的加密类型是AEAD。

2.8 修正数字签名

TLS的另一个重要部分是认证。在每次连接中,服务器使用具有公钥的数字证书向客户端进行身份验证。

  • 在RSA加密模式下,服务器通过解密pre-master密钥并通过对会话记录计算MAC来证明其对私钥的所有权。
  • 在Diffie-Hellman模式下,服务器使用数字签名证明私钥的所有权。

如果您到目前为止一直在关注此博文,那么应该很容易猜到这么做也是错误的。

PKCS#1 v1.5

Daniel Bleichenbacher致力于识别TLS中的RSA问题。在2006年,他设计出针对TLS中使用的RSA签名的笔纸攻击。后来发现,主要的TLS实现(包括NSS和OpenSSL的实现)也容易受到此攻击。这个问题再次证明了正确实现填充算法有多困难,在这个例子中,RSA签名使用的PKCS#1 v1.5实现填充。

在TLS 1.3中,为了支持较新的设计RSA-PSS,已删除PKCS#1 v1.5。

对整个记录进行签名

tls13-07

在TLS 1.2及更早版本中,服务器的签名仅涵盖部分握手。握手的其他部分(尤其是用于协商使用哪种对称加密组件的部分)没有用私钥签名。相反,使用对称MAC来确保握手不被篡改。

tls13-08

这种疏忽导致了许多备受瞩目的漏洞,FREAK、LogJam、CurveSwap等,它们利用了以下两个既存事实:

  • 许多浏览器和服务器仍支持1990年代有意设置的弱加密组件(称为出口加密组件)。
  • 用于协商使用哪个加密组件的握手部分未进行数字签名。

中间人实施降级攻击,令通信双方采用最弱加密组件,再通过暴力攻击计算出密钥。

tls13-09

在TLS 1.3中,这种类型的降级攻击是不可能发生的,因为服务器现签名了整个握手,包括加密组件协商。

2.9 协议简化

在删除了上面列出的不安全功能后,TLS 1.3协议变得更优雅和安全,且易于理解。

在以前的TLS版本中,主要的协商机制是加密组件。加密组件几乎包含了有关连接可以协商的所有内容:

  • 支持的证书类型
  • 用于派生密钥的哈希函数,例如SHA1、SHA256
  • MAC功能,例如具有SHA1、SHA256的HMAC
  • 密钥交换算法,例如RSA、ECDHE
  • 加密组件,例如AES、RC4
  • 加密模式(如果适用),例如CBC

加密组件发展成了庞大的词库,例如:DHE-RC4-MD5或ECDHE-ECDSA-AES-GCM-SHA256。每一个加密组件都有一个编号,并由IANA组织维护。

tls13-10

TLS 1.3删除了许多这些旧功能,从而允许在三个正交协商之间进行清晰的划分:

  • 加密组件+HKDF哈希
  • 密钥交换
  • 签名算法

这种简化的加密组件协商和协商参数集合开辟了新的可能性,使TLS 1.3握手延迟从2-RTT减少到1-RTT,从而实现了性能提升,确保TLS 1.3将会被广泛采用。

2.10 性能优化

tls13-11

在以前的TLS版本中,与新服务器建立连接时,需要2-RTT才能在连接上发送数据。网络延迟小时不会感受到差异,但对于移动网络,延迟可以高达200ms,这时差距就显而易见了。

1-RTT模式

tls13-12

TLS 1.3具有更加简化的加密组件协商和密钥协议选项集,这意味着每个连接都将使用基于DH的密钥协议,并且服务器所支持的参数很容易被猜到(使用X25519或P-256的ECDHE)。客户端可以在ClientHello中发送DH密钥交换参数,而不必等到服务器确认它支持哪些参数。极少数情况下,服务器不支持客户端发送的密钥交换时,可以发送一个HelloRetryRequest告知客户端它支持哪些密钥交换,由于参数列表已经被精简了很多,类似情况很难发生。

0-RTT session恢复

QUIC协议启发了进一步的优化。使客户端可以在其第一条消息中将加密的数据发送到服务器,与未加密的HTTP相比,没有额外的延迟成本。这很重要,一旦TLS 1.3广泛部署,加密的Web肯定会比以前更快捷。

在TLS 1.2中,有两种恢复连接的方法:会话ID和会话tickets。在TLS 1.3中,这些被组合位一个新模式:PSK(预共享密钥)恢复模式。其思路是在建立会话之后,客户端和服务器可以得到称为“恢复主密钥”的共享密钥。这可以使用id(会话ID样式)存储在服务器上,也可以通过仅为服务器所知的密钥(会话ticket样式)进行加密,此会话ticket将发送到客户端并在恢复连接时兑换。

对于恢复的连接,双方共享一个恢复主密钥,因此除了提供前向保密性之外,不需要进行密钥交换。下次客户端连接到服务器时,它可以从上一个会话中获取密钥,并使用它来加密与会话ticket一起发送的应用程序数据到服务器。但这也有缺点。

2.11 可重放性

tls13-13

0-RTT数据中没有交互,它由客户端发送,服务器使用,无需任何交互。从性能上来说有好处,但需要付出代价:可重放性。如果攻击者捕获了发送给服务器的0-RTT数据包,则他们重放该数据包,并且服务器可能会将其视为有效。这可能会产生意想不到的负面后果,例如在重放数据中执行更改服务器状态的任何操作。作为客户端,您可以尝试通过仅将“安全”请求放入0-RTT数据来防止这种情况,例如不更改服务器状态的操作,如HTTP GET请求。

为了防止出现这种故障情况,TLS 1.3还会在会话凭据中包含流逝时间。如果差异太大,则要么是客户端接近光速,要么是数据被重放,无论哪种情况,服务器都应拒绝0-RTT数据。

2.12 可部署性

TLS 1.3与TLS 1.2及更早版本完全不同,但要广泛部署,必须与现有软件向后兼容。TLS 1.3从起草到最终发布花费了这么长时间的原因之一是,某些现有软件(即Middlebox)无法很好地适应新变化。即使是对TLS 1.3协议的细微更改(例如消除冗余的ChangeCipherSpec消息,将版本从0x0303更改为0x0304),也最终会导致部分人的连接问题。

尽管TLS规范内置了将来的灵活性,但某些实现对如何处理将来的TLS版本做出了错误的假设。导致这种变化的现象称为骨化,这篇文章详细描述了这个问题:Wh TLS 1.3 isn’t in browsers yet 。为了适应这些变化,TLS 1.3被修改为看起来很像TLS 1.2会话恢复(至少在线路上)。这导致协议功能更强大,却不够优美。这是您升级部署最广泛的协议之一所要付出的代价。

2.13 结论

Yet another state-of-the-art protocol.

3. 部署

这里默认采用TLS 1.2和TLS 1.3,不再支持TLS 1.1及更早的版本。

3.1 Nginx

CentOS 7

CentOS 7最新版本为 7.7.1908,openssl版本为OpenSSL 1.0.2k-fips 26 Jan 2017,只能启用TLS 1.2和ALPN,从nginx官方repo中安装的nginx-1.17.9也是使用该版本openssl构建,因此需要手动编译才能启用TLS 1.3。如下所示:

首先获取nginx.repo,并启用mainline版本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

然后安装nginx,执行nginx -V命令获取configure配置。

1
2
yum install nginx -y
nginx -V

获取到输出

1
2
3
4
5
nginx version: nginx/1.17.9
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
built with OpenSSL 1.1.1e  17 Mar 2020
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'

进入到/usr/local/src目录,分别下载nginx-1.17.9和openssl-1.1.1e,解压到当前目录。

1
2
3
4
wget https://nginx.org/download/nginx-1.17.9.tar.gz
tar -zxvf nginx-1.17.9.tar.gz
wget https://www.openssl.org/source/openssl-1.1.1e.tar.gz
tar -zxvf openssl-1.1.1e.tar.gz

nginx源码目录为 /usr/local/src/nginx-1.17.9,oepnssl目录为 /usr/local/src/openssl-1.1.1e

进入nginx目录,在上述的configure配置最后添加一行 –with-openssl=/usr/local/src/openssl-1.1.1e,然后执行make构建程序,静态编译nginx

1
2
3
cd /usr/local/src/nginx-1.17.9
--prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' --with-openssl=/usr/local/src/openssl-1.1.1e
make -j 4

编译完成后,可执行程序位于 /usr/local/src/nginx-1.17.9/objs/nginx,将其拷贝到 /usr/sbin/nginx 替换官方repo程序安装的nginx,即可使用

CentOS 8

在CentOS 8中,openssl版本为OpenSSL 1.1.1c FIPS 28 May 2019,可支持TLS 1.3,从nginx官方repo中安装的nginx-1.17.9已使用GCC 8.3.1和该版本openssl构建,可直接启用TLS 1.3。

TLS配置

如下所示,仅启用TLS 1.2和TLS 1.3,使用Let’s Encrypt提供的RSA和ECC证书,可应用于http和stream服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ssl_certificate /etc/nginx/cert/rsa.pem;
ssl_certificate_key /etc/nginx/cert/rsa.key;
ssl_certificate /etc/nginx/cert/ecc.pem;
ssl_certificate_key /etc/nginx/cert/ecc.key;
ssl_dhparam /etc/nginx/cert/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096

ssl_protocols TLSv1.2 TLSv1.3;# Requires nginx >= 1.13.0 else use TLSv1.2
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_ecdh_curve auto; # Requires nginx >= 1.1.0
ssl_session_timeout 720m;
ssl_session_cache shared:SSL:32m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

Nginx作为TLS前端还可以代理其他协议,例如http、websocket、socks5等底层使用TCP的协议,都可以通过套一层TLS实现加密,前提是不需要TLS层提供的一些信息。例如启用TLS的HTTP2服务就需要TLS层的ALPN才能完成协议切换,目前不可直接拆分,否则Web服务只能使用HTTP 1.1。

3.2 Caddy

当前CentOS 7的epel源自带Caddy 1.0.3版本可安装,CentOS 8需要手动下载Caddy安装。

Golang在1.11版本标准库中引入了TLS 1.3,Caddy使用Golang编写,同样具备标准库的功能。

Caddy最大的优点就是自动化的证书部署,默认占用80和443端口,读取配置文件中的域名后,自动从Let’s Encrypt申请和维护相关证书,如下所示配置一个web服务

1
2
3
test.wbuntu.com {
    root /usr/share/caddy
}

即可自动获取 test.wbuntu.com 的DV证书,重定向80端口请求至443。

Caddy相比nginx在功能上比较薄弱,主要作为HTTP服务器,胜在证书自动化维护和更快的迭代速度(伴随Golang更新),且支持实验性的QUIC(也可以称为HTTP3),但实测下来QUIC在公网链路上并不稳定。

3.3 Cloudflare

如果域名服务器使用了Cloudflare,可以通过CDN直接使用它的证书和TLS功能,用户和Cloudflare的CDN之间使用TLS加密,后端服务器和Cloudflare间可以明文传输或者使用自签名证书加密。

Cloudflare支持HTTP和Websocket两种协议,不支持HTTP CONNECT请求。

4. TLS承载应用层协议

在阅读Go标准库源码时,笔者发现目前TLS库都是以底层为可靠的TCP连接为基础的,QUIC未纳入Go标准库,可能是因为协议定义和实现尚未稳定,TLS层需要重新适配底层QUIC传输协议才能正常工作,因此QUIC或者其他基于UDP的协议就不在目前讨论范围内。

HTTP服务通常占用80和443端口,这也导致许多防火墙仅允许这两个端口的流量通过,甚至会做包过滤,核查是否为HTTP数据包或TLS数据包,因此使用TLS时,443端口的服用就显得很重要了。

我们知道:

  • HTTP 1.1协议及Websocket协议都可以根据HTTP头部中的HOST实现分流,提供虚拟主机的功能。
  • HTTP2包含了HTTP 1.1的所有特性,但需要TLS层的APLN支持。
  • RFC 3546中引入了SNI用于选择和验证服务器,TLS 1.2及TLS 1.3都可以通过ClientHello获取到客户端需要访问的服务器名。

因此可以利用Nginx的两个模块:ngx_stream_ssl_preread_modulengx_stream_ssl_module 解析ClientHellp,根据TLS层SNI执行数据分流,实现443端口的复用。如下所示:

  • 域名配置:
    • wbuntu.com及www.wbuntu.com用于博客,支持HTTP 1.1和HTTP2,同时承载一些websocket流量
    • 001.wbuntu.com用于提供HTTP代理,基于HTTP 1.1,因为nginx不支持CONNECT请求,使用gost提供服务
    • 002.wbuntu.com用于提供socks5代理,使用gost提供服务器
    • 003.wbuntu.com用于提供trojan代理
  • 域名解析:
    • wbuntu.com及www.wbuntu.com需要解析A记录,供浏览器访问
    • 其他私有应用协议可不解析A记录,直接在客户端配置服务器的SNI,提高隐蔽性
  • 证书:Let‘s Encrypt泛域名证书,后续添加新的子域名时,无需重新签发证书

Nginx配置stream服务如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
map $ssl_preread_server_name $prereadTargetBackend {
    wbuntu.com  127.0.0.1:4443;
    www.wbuntu.com  127.0.0.1:4443;
    003.wbuntu.com  127.0.0.1:8443;
    001.wbuntu.com 127.0.0.1:8444;
    002wbuntu.com 127.0.0.1:8444;
}

server {
    listen 443 reuseport;
    listen [::]:443 reuseport;

    ssl_preread on;
    proxy_pass $prereadTargetBackend;
}

map $ssl_server_name $targetBackend {
    002.wbuntu.com 127.0.0.1:8005;
    001.wbuntu.com 127.0.0.1:8006;
}

server {
    listen 127.0.0.1:8444 ssl reuseport;
    listen [::1]:8444 ssl reuseport;
    access_log /var/log/nginx/stream-tls.access.log main;
    error_log /var/log/nginx/stream-tls.error.log;
    include /etc/nginx/stream.conf;
    proxy_pass $targetBackend;
}

数据流转如下

  • 最外层使用ngx_stream_ssl_preread_module,根据 $ssl_preread_server_name 获取域名,透传数据
  • wbutnu.com与www.wbuntu.com的TLS层不可剥离,分流至本地HTTPS服务,支持HTTP 1.1、HTTP2以及Websocket协议,即 nginx:443 -> nginx:1443
  • 003.wbuntu.com的TLS层不可剥离,分流至本地trojan服务,即 nginx:443 ->trojan:8443
  • 001.wbuntu.com和001.wbuntu.com的TLS层可剥离,分流至另一个nginx TLS服务,该服务使用ngx_stream_ssl_module,负责TLS层加解密,根据 $ssl_server_name 获取域名,转发应用层数据,即 nginx:443 -> nginx:8444 -> gost:8005和gost:8006

但这样配置下,本地服务器接收到的数据中的远程IP都会是宿主机IP,可以通过proxy protocol传递真实客户端IP,具体配置参考nginx文档:Accepting the PROXY Protocol。原理还是发送方通过代理协议附加真实客户端IP,接收端解析数据获取真实IP。

5. AES-GCM与ChaCha20-Poly1305

tls13-14

在部署TLS 1.3时遇到了一个有趣的问题,笔者购买的一台VPS同时缺失AES和AVX2指令集,宛如奔腾处理器,真的是活久见。

通常的加密库如Go标准库crypto,C语言实现的libsodium,都会使用硬件指令加速,例如

  • 使用AES、PCLMULQDQ实现AES-GCM
  • 使用AVX2、SSE系列指令实现ChaCha20-Poly1305或xChaCha20-Poly1305
  • 缺失硬件指令时,回退到软件实现

桌面平台很早就支持了上述的加速指令,但嵌入式平台则一直缺失AES指令,直到ARMv8架构面世(也就是现在常用的64位ARM处理器),带来了AES-NI支持。而苹果家的设备如iPhone 6s/6s Plus,甚至配备了专门的AES模块,用于整机存储加密。

在部署nginx和caddy时发现:

  • caddy使用Go编写,由于Go内部会自动探测CPU指令集,在TLS 1.3握手时,若硬件支持加速AES-GCM,则优先使用AES-GCM,否则优先使用ChaCha20-Poly1305
  • Nginx依赖OpenSSL实现加密,即使硬件不支持加速AES-GCM,也会优先使用软件实现的AES-GCM

最终结果是使用nginx时,CPU占用率经常飙升。

Cloudflare同样有一篇文章对比AES-GCM与ChaCha20-Poly1305:Do the ChaCha: better mobile performance with cryptography

结论是:

  • 软件实现时,ChaCha20-Poly1305效率高于AES-GCM
  • 硬件支持AES-GCM加速时,AES-GCM效率更高,适合笔记本或台式机的场景
  • 不支持AES的嵌入式平台更适合使用ChaCha20-Poly1305

但到了今天,风水轮流转,嵌入式硬件开始支持AES-NI,而桌面CPU迎来了AVX512指令集。

能利用好硬件加速的加密组件才是好的加密组件,优秀的软件和库都应该充分探测和挖掘硬件特性。