TLS 加密客户端Hello (ECH)¶
实验性
开发参考
TLS 加密客户端Hello (当前 IETF 草案)
https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html
"虽然 TLS 1.3 (RFC8446) 加密了大部分握手,包括服务器证书,但在线攻击者有多种方式可以了解连接的私人信息。ClientHello 消息中明文的服务器名称指示 (SNI) 扩展泄露了给定连接的目标域,这可能是 TLS 1.3 中未加密的最敏感信息。"
"...指定了一个新的 TLS 扩展,称为加密客户端Hello (ECH),它允许客户端将其 ClientHello 加密以用于此类部署。这保护了 SNI 和其他可能敏感的字段,例如 ALPN 列表 (RFC7301)。"
尽管 ECH 规范尚未最终确定,但 ECH 已开发多年。现代 Firefox 和 Chrome 浏览器以及 cURL (当 cURL 使用支持 ECH 的 TLS 库构建时) 都支持 ECH。
lighttpd 1.4.77 mod_openssl 在 lighttpd mod_openssl 使用支持 ECH 的 TLS 库在服务器端构建时支持 ECH,目前是 BoringSSL 或 OpenSSL (feature/ech 开发分支)。(参见下面的说明。) 由于 ECH 规范尚未最终确定,并且 TLS 库对 ECH 的支持仍在开发中,lighttpd mod_openssl 对 ECH 的支持是实验性的。请在 lighttpd 论坛 中提供反馈。(依赖其他 TLS 库的 lighttpd TLS 模块目前不支持 ECH。GnuTLS 和 mbedTLS 目前不支持 ECH。NSS 和 WolfSSL 目前对服务器端的 ECH 支持有限或不完整。)
要使 TLS ECH 工作,客户端和服务器必须使用 TLSv1.3,支持 ECH (当然!),并且客户端必须能够找到包含用于加密 ClientHello 的公钥的 ECHConfig。(参见下面的“发布 ECHConfigs”。)
具有相同 ECHConfig 的一组虚拟主机被称为 ECH 匿名集。对于每个 ECH 匿名集,都有一个 public_name,它是在使用加密客户端Hello 进行 TLS 连接时要使用的明文 SNI。
TLS ECH 选项¶
ssl.ech-opts¶
ssl.ech-opts = ( "keydir" => "/path/to/echkeys", )
"keydir"
包含 ECH 密钥和 ECHConfigs 的 *.ech
PEM 文件的目录路径 (启用 ECH 所必需)"refresh"
keydir 刷新间隔,用于重新读取 ECH 密钥和 ECHConfigs (默认 300 秒 (5 分钟)) (刷新可能延迟多达 64 秒)"public-names"
公共主机列表。如果已定义,则所有其他主机都是隐式私有的,并且客户端只能通过使用 ECH 访问。"trial-decrypt"
禁用/启用试探性解密 (默认:启用) 该选项也可以通过 ssl.openssl.ssl-conf-cmd += ("Options" => "-ECHTrialDecrypt")
覆盖
ssl.ech-public-name¶
ssl.ech-public-name
为包含此选项的 $HTTP["host"]
定义一个 public_name。其目的和效果是标记 $HTTP["host"]
为私有,并且客户端只能通过使用 ECH 访问。此指令允许选择性地将主机标记为仅限 ECH。要隐式地将所有未列出的主机标记为仅限 ECH,请使用 ssl.ech-opts
@"public-names"。注意:主机可以具有 ECHConfig 定义的不同的 ECH public_name,此指令不会更改 ECHConfig 定义的 public_name。
重要:对于仅限 ECH 的主机,要使其保持仅限 ECH,它们必须不可通过 HTTP 访问。除了上述标记仅限 ECH 的主机外,还需要在包含 ssl.engine = "enable"
的 $SERVER["socket"]
内部,而不是在全局配置范围内部 (除非全局范围包含 ssl.engine = "enable"
且没有 ssl.engine = "disable"
的 $SERVER["socket"]
配置) 定义仅限 ECH 的主机。
试探性解密是 ECH 规范的一部分,但并非必需。在 ECH 规范讨论中,一些 CDN 曾推动避免试探性解密,以减少滥用试探性解密进行 DoS 攻击的影响。同样,一些 CDN 曾推动使用 ECHConfig 中的 confid_id,它理论上可以作为跟踪向量。bssl generate_ech
可用于生成具有特定 config_id 的 ECHConfig,并且如果管理员选择使用相同的 config_id 生成所有密钥,则必须启用试探性解密以支持多个密钥。但是,除非您关注过关于这些 ECH 功能的争论或有特定的安全需求并了解这如何影响这些需求,否则建议使用随机的 config_id,在这种情况下,您可能会选择禁用试探性解密,以获得与许多 CDN 相似的 ECH 支持级别。
管理 ECH 密钥¶
https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni
lighttpd 从配置的 ssl.ech-opts
"keydir"
中名为 *.ech
的文件中读取 ECH 密钥和 ECHConfigs。*.ech
文件列表按字母顺序排序 (不区分大小写),然后按顺序解析文件。虽然除了以 *.ech
结尾之外没有强制性命名,但 lighttpd 建议将文件命名为 <prefix>@<public_name>.ech
,例如 0@example.com.ech,其中前缀用于文件排序,public_name 用于标识相同 public_name 的密钥。
ECH 密钥应频繁轮换,例如每 30 分钟一次,并在 DNS 中以较短的 TTL 发布,例如 3600 秒(60 分钟)。如果使用这些时间范围,那么在任何给定时间可能会有 3 个不同的密钥处于活动状态,因此 lighttpd 支持每个 public_name 多个 ECH 密钥。对于给定的 public_name,只有最近修改的文件(即使用最近生成的密钥的文件)中的 ECHConfig 才会在 TLS HelloRetryRequest (HRR) 中发送给客户端。如果相同 public_name 的文件都具有相同的修改时间,lighttpd 将只发送该 public_name 排序在第一个的文件中的 ECHConfig。lighttpd 默认每 900 秒(15 分钟)重新读取 ECH 密钥和 ECHConfigs,并且可以通过 ssl.ech-opts
"refresh"
配置,最短可达每 64 秒一次。
一个每 30 分钟运行一次的示例脚本,用于轮换密钥然后生成新密钥
#!/bin/sh set -e cd /path/to/keydir for i in 1@*.ech; do mv $i 2@${i#1@}; done for i in 0@*.ech; do mv $i 1@${i#0@}; done # Then, generate new key(s) as 0@*.ech, # e.g. using 'openssl ech ...' or 'bssl generate_ech ...'
发布 ECHConfigs¶
服务绑定框架 (SVCB) 被用于发布 ECHConfigs。
https://datatracker.ietf.org/doc/html/draft-ietf-tls-svcb-ech
https://datatracker.ietf.org/doc/draft-ietf-tls-wkech/
从每个新生成的文件中提取 base64 编码的 ECHConfig (参见上面的“管理 ECH 密钥”),然后对于每个可通过 ECH 访问的主机 (不仅是 public_name),将结果发布到 DNS HTTPS 资源记录中,例如 HTTPS 资源记录:alpn="h2" ech="......"cat /path/to/0@example.com.ech | perl -e '$/=undef; $f=<>; $f =~ /-----BEGIN ECHCONFIG-----(.*?)-----END ECHCONFIG-----/s && ($echconfig = $1) =~ s/\s//gs; print $echconfig;'
请查阅您的 DNS 提供商的说明,了解如何自动化 DNS 更新。为了更强的安全立场,在受保护的(非面向公众的)机器上生成 ECH 密钥和 ECHConfigs,并从受保护的主机发布到 DNS。从受保护的主机,将生成的 ECH 密钥和 ECHConfigs 推送到每个 Web 主机上的暂存目录,在那里,像上面这样的脚本将在轮换旧的 ECH 密钥和 ECHConfigs 后,将新的 ECH 密钥和 ECHConfigs 移入 keydir。
构建支持 ECH 的 lighttpd¶
为了使 lighttpd mod_openssl 支持 ECH,lighttpd mod_openssl 必须针对支持 ECH 的 TLS 库进行构建。
构建支持 ECH 的 TLS 库,然后从源代码构建 lighttpd (从源代码安装)
构建支持 ECH 的 BoringSSL¶
git clone https://boringssl.googlesource.com/boringssl cd boringssl cmake -DCMAKE_BUILD_TYPE=Release -GNinja -B build -DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_SHARED_LIBS=1 ninja -C build cmake --install build cd .. # lighttpd ./configure --with-openssl --with-openssl-includes=/usr/local/include --with-openssl-libs=/usr/local/lib64 # (change /usr/local in all above commands to your target installation location)
BoringSSL 工具 'bssl' 可用于生成 ECH 密钥和 ECHConfigbssl generate_ech -out-ech-config-list /path/to/bssl.echconfiglist -out-ech-config /path/to/bssl.echconfig -out-private-key /path/to/bssl.pkey -public-name example.com -config-id $((RANDOM % 255))
然而,使用椭圆曲线 X25519 和 KEM EVP_hpke_x25519_hkdf_sha256() (BoringSSL) 生成原始私钥。硬编码这些参数不方便用户使用,所以我希望 BoringSSL 未来能改进这个接口。以下脚本将获取 bssl 生成的文件的二进制内容,并将其转换为更具描述性的 ASN.1 TLV,然后进行 base64 编码,生成一个可与 lighttpd mod_openssl 一起使用的 PEM 文件。将命令输出发送到例如 0@example.com.ech
bssl_pkey=/path/to/bssl.pkey bssl_echconfig=/path/to/bssl.echconfig echo "-----BEGIN PRIVATE KEY-----" (perl -e 'print STDOUT "\x30\x2E\x02\x01\x00\x30\x05\x06\x03\x2B\x65\x6E\x04\x22\x04\x20"'; cat $bssl_pkey) | base64 echo "-----END PRIVATE KEY-----" echo "-----BEGIN ECHCONFIG-----" cat $bssl_echconfig | perl -e 'sysread(STDIN, $echconfig, 1024); print STDOUT pack('n',length($echconfig)), $echconfig;' | base64 echo "-----END ECHCONFIG-----" # output should be saved in a file 0@example.com.ech and placed in the ssl.ech-opts "keydir" configured in lighttpd.conf
构建支持 ECH 的 OpenSSL¶
OpenSSL ECH 支持仍在 OpenSSL 仓库的 feature/ech 分支上开发中。
https://github.com/defo-project/
https://github.com/defo-project/ech-dev-utils
OpenSSL 工具 'openssl' 可用于生成 ECH 密钥和 ECHConfig。必须指定 -public_name
才能生成可用的 echconfig。openssl ech -public_name example.com -pemout /path/to/0@example.com.ech
OpenSSL 工具 'openssl' 可用于打印 ECHConfig。openssl ech -pemin /path/to/0@example.com.ech
OpenSSL 工具 'openssl' 可用于调试 ECHConfig 结构。openssl asn1parse -inform pem -in /path/to/0@example.com.ech
在没有 DNS 的情况下测试 ECH¶
如果使用构建了 ECH 支持的 cURL,可以告知 cURL 使用 ECHConfig $echconfig (参见上面的“发布 ECHConfigs”)
例如,访问 localhost 上一个仅限 ECH 的主机 baz.example.com 并使用 $echconfigcurl -vvv --connect-to baz.example.com:443:localhost:443 https://baz.example.com/index.html --ech ecl:ecl:$echconfig
(我用于测试的 curl 版本可能存在一个开发错误,需要我重复 "ecl:ecl:...")
(错误 https://github.com/curl/curl/issues/16006 已在 curl 8.12 中修复)