项目

通用

个人资料

操作

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,例如 ,其中前缀用于文件排序,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 密钥和 ECHConfig
bssl 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 文件。将命令输出发送到例如

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 并使用 $echconfig
curl -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 中修复)

gstrauss 4 个月前更新 · 7 次修订