Rust in Everything
基于 rustls 0.23.46

received fatal alert: HandshakeFailure / ProtocolVersion

主线位置:握手主线的第一步。客户端的 ClientHello 列出它会的版本和套件,由服务端挑。rustls 只会 TLS 1.3 和 1.2、只会 ECDHE 加 AEAD,为什么这样设计见设计取舍,那一页已经用 TLS 1.1、1.0 和 CBC 套件的服务器各试过一次。本页补上几种同样常见的老配置,讲客户端这边能看到什么、该去哪里找原因。

flowchart TB
  ch["ClientHello"] --> s{"服务端能选什么"}
  s -->|没有共同版本| pv["protocol_version"]
  s -->|没有共同套件| hf["handshake_failure"]
  s -->|ECDHE 加 GCM| ok["TLS 1.2 成功"]
  pv --> err["AlertReceived"]
  hf --> err
  err --> app["客户端只见告警码"]

实验:exp-rustls/pitb-lab 的 p14_legacy(rustls 客户端,ring provider,经 StreamOwned 走 127.0.0.1)加 legacy.sh。服务端是本机 Homebrew 的 OpenSSL 4.1.0 s_server,证书是 pitb-lab/certs 下 OpenSSL 生成的 RSA 2048 证书和它的 CA。每组第一行是 s_server 的参数和客户端的版本配置,第二行是 rustls 客户端打印的错误,第三行是 s_server 日志里的原因。M2 Max、macOS 27、rustc 1.99.0,release。

现象

== s_server -tls1_2 -cipher AES128-GCM-SHA256 | client default
rustls error: received fatal alert: HandshakeFailure  (debug: Custom { kind: InvalidData, error: AlertReceived(HandshakeFailure) })
error:0A0000C1:SSL routines:tls_post_process_client_hello:no shared cipher:ssl/statem/statem_srvr.c:2726:
== s_server -tls1_2 -cipher DHE-RSA-AES128-GCM-SHA256 | client default
rustls error: received fatal alert: HandshakeFailure  (debug: Custom { kind: InvalidData, error: AlertReceived(HandshakeFailure) })
error:0A0000C1:SSL routines:tls_post_process_client_hello:no shared cipher:ssl/statem/statem_srvr.c:2726:
== s_server -tls1_2 -cipher ECDHE-RSA-AES128-SHA | client default
rustls error: received fatal alert: HandshakeFailure  (debug: Custom { kind: InvalidData, error: AlertReceived(HandshakeFailure) })
error:0A0000C1:SSL routines:tls_post_process_client_hello:no shared cipher:ssl/statem/statem_srvr.c:2726:
== s_server -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 | client default
rustls ok: Some(TLSv1_2) Some(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), read 64 bytes
== s_server -tls1_2 | client tls13
rustls error: received fatal alert: ProtocolVersion  (debug: Custom { kind: InvalidData, error: AlertReceived(ProtocolVersion) })
error:0A000102:SSL routines:tls_early_post_process_client_hello:unsupported protocol:ssl/statem/statem_srvr.c:2121:
== s_server -tls1_1 -cipher ALL:@SECLEVEL=0 | client default
rustls error: received fatal alert: ProtocolVersion  (debug: Custom { kind: InvalidData, error: AlertReceived(ProtocolVersion) })
error:0A000102:SSL routines:tls_early_post_process_client_hello:unsupported protocol:ssl/statem/statem_srvr.c:2121:
== s_server -tls1_3 | client tls13
rustls ok: Some(TLSv1_3) Some(TLS13_AES_256_GCM_SHA384), read 64 bytes
  • 只有 RSA 密钥交换(AES128-GCM-SHA256,没有 ECDHE)、只有有限域 DHE(DHE-RSA-...)、只有 CBC(ECDHE-RSA-AES128-SHA):三种完全不同的原因,客户端看到的是同一个 HandshakeFailure。
  • 服务器只开 TLS 1.2、客户端只配了 TLS 1.3(with_protocol_versions(&[&TLS13]),或者编译时没开 tls12 feature);服务器只有 TLS 1.1:同一个 ProtocolVersion。
  • 错误外面包着一层 io::Error(Custom { kind: InvalidData, ... }),因为走的是 StreamOwned 的 Write;里面才是 rustls 的 AlertReceived。

打开 rustls 的日志也没有更多东西。同一个 RSA 密钥交换的例子,RUST_LOG=debug:

[... DEBUG rustls::client::hs] No cached session for DnsName("localhost")
[... DEBUG rustls::client::hs] Not resuming any session
rustls error: received fatal alert: HandshakeFailure  (debug: Custom { kind: InvalidData, error: AlertReceived(HandshakeFailure) })

原因

判断是服务端做的。 客户端发出 ClientHello 之后就在等 ServerHello。老服务器在 ClientHello 里找不到自己能用的版本或套件,回一个致命告警就关连接。告警只有一个字节的代码,no shared cipher、unsupported protocol 这些原因只写在服务端自己的日志里。

rustls 收到告警就原样交出来。 process_alert 只对警告级别的告警打日志,致命告警直接变成错误:

        // Warnings are nonfatal for TLS1.2, but outlawed in TLS1.3
        // (except, for no good reason, user_cancelled).
        let err = Error::AlertReceived(alert.description);
        if alert.level == AlertLevel::Warning {
            // ...
            return Ok(());
        }

        Err(err)

所以上面的 debug 日志里看不到告警那一行。

rustls 能提供的就这么多。 版本只有 ALL_VERSIONS 里的 TLS 1.3 和(开了 tls12 时的)1.2;ring provider 的 ALL_CIPHER_SUITES 是三个 1.3 套件加六个 TLS_ECDHE_* 的 1.2 套件,全是 GCM 或 ChaCha20-Poly1305。aws-lc-rs 那边的列表也是这九个(套件与密钥交换)。老服务器只要不在这个交集里,就只能是上面两种告警之一。

📝NOTE
冷知识:rustls 其实会做 DHE,只是内置的 provider 不带

c8c56a7a(「Implement FFDHE support」,首次发布在 0.23.0)给 TLS 1.2 加上了有限域 Diffie-Hellman:KeyExchangeAlgorithm 从此有 DHE 和 ECDHE 两个值,文档注释限定为「with only known parameters as defined in [RFC 7919]」,即只认 RFC 7919 里命名的那几组参数,不接受服务器自定义的素数。提交说明的第二条是「Make server avoid cipher suites with kx without common kx groups with client」。不过 ring 和 aws-lc-rs 两个 provider 一个 DHE_* 套件也没有:仓库里唯一的 TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 在 openssl-tests/src/ffdhe.rs,注释写着「(test-only)」,用来和 OpenSSL 互通测试。上面 DHE-RSA-AES128-GCM-SHA256 那一组失败,缺的是 provider 里的套件,不是协议层的代码。

修法

先弄清是哪一种。 拿本机 OpenSSL 当探针去连那台服务器,看它接受什么。对上面只有 RSA 密钥交换的那台:

New, TLSv1.2, Cipher is AES128-GCM-SHA256

套件名里没有 ECDHE,就是 rustls 没有的 RSA 密钥交换。s_client 加 -tls1_2、-tls1_1 可以逐个版本试,加 -cipher 可以逐个套件试。

版本不对,先看自己这边。 「服务器只开 1.2、客户端只配 1.3」那一组是客户端自己造成的:用 with_safe_default_protocol_versions()(1.3 和 1.2 都有),并确认 tls12 feature 开着(默认开)。上面 client default 对 -tls1_2 服务器的几组就是这样配的,ECDHE-GCM 那组成功。

套件不对,只能改服务端。 给服务器加上任意一个 ECDHE-*-AES*-GCM-* 或 ECDHE-*-CHACHA20-POLY1305 套件,上面第四组就是只加了一个 ECDHE-RSA-AES128-GCM-SHA256 的结果。服务器只有 TLS 1.1 / 1.0、或者只有 RSA 密钥交换、CBC 套件,rustls 这一侧没有任何开关能打开它们;内置 provider 也没有 DHE 套件(见上面的冷知识)。改不了服务器时,就只能让这条连接不经过 rustls。

怎么检查

  • 看告警码: HandshakeFailure 先怀疑套件,ProtocolVersion 先怀疑版本。同一个告警可以有好几种原因,只能缩小范围。
  • 看服务端日志: 能拿到的话,那里有真正的原因(no shared cipher、unsupported protocol)。
  • 用 openssl s_client 探: 记下服务器接受的版本和套件,和 rustls 的九个套件对照。
  • 看自己的配置: ClientConfig 的版本列表里有没有 TLS 1.2,cargo tree -e features -i rustls 里有没有 tls12。

小结

  • 老服务器在服务端拒绝 ClientHello,rustls 客户端只收到一个告警:没有共同套件是 HandshakeFailure,没有共同版本是 ProtocolVersion;rustls 的日志里也没有更多信息。
  • RSA 密钥交换、DHE、CBC 三种老配置都落在 HandshakeFailure;客户端关掉 TLS 1.2 也会撞上 ProtocolVersion。
  • 用 openssl s_client 探出服务器接受什么;版本问题先查自己的配置,套件问题只能让服务器加一个 ECDHE + AEAD 套件。
  • rustls 协议层支持 RFC 7919 的 DHE,但内置的 ring 和 aws-lc-rs provider 都没有 DHE 套件。

💬 关联讨论

发起讨论
加载中...