Rust in Everything
基于 rustls 0.23.46

received fatal alert: AccessDenied

主线位置:握手主线里服务端处理 ClientHello 的那一步:选好版本,接着选证书,再选套件。ResolvesServerCert 怎么被调用、ResolvesServerCertUsingSni 的 add 做什么检查,见服务端 TLS 1.3,那一页已经演示过「SNI 不在表里」。本页补上两种更隐蔽的来路(IP 连接、客户端不发 SNI),以及怎么让报错变得有用。

sequenceDiagram
  participant C as 客户端
  participant S as 服务端
  C->>S: ClientHello SNI 或无
  Note over S: cert_resolver 返回 None
  S->>C: alert access_denied
  Note over S: no server certificate chain resolved
  Note over C: AlertReceived AccessDenied

实验 crate:exp-rustls/pitb-lab。src/bin/p15_sni.rs 在内存里握手;src/bin/p15_tcp.rs 在 127.0.0.1 上开一个同样配置的服务端,给 Homebrew 的 OpenSSL 4.1.0 s_client 连。服务端的 ResolvesServerCertUsingSni 里只登记了 a.test。ring provider,TLS 1.3。M2 Max、macOS 27、rustc 1.99.0,release。

现象

== ResolvesServerCertUsingSni with only a.test
connect a.test: Ok
connect c.test: server error: unexpected error: no server certificate chain resolved
    debug: General("no server certificate chain resolved")
    (client then sees: received fatal alert: AccessDenied)
connect 127.0.0.1 (no SNI sent): server error: unexpected error: no server certificate chain resolved
    debug: General("no server certificate chain resolved")
    (client then sees: received fatal alert: AccessDenied)
connect a.test, enable_sni = false: server error: unexpected error: no server certificate chain resolved
    debug: General("no server certificate chain resolved")
    (client then sees: received fatal alert: AccessDenied)

三种失败,客户端看到的都是 AccessDenied。字面意思是「拒绝访问」,很容易让人去查防火墙、客户端证书或者权限配置,而真正的原因是服务端没有证书可给。

换成 OpenSSL 客户端,连同一个服务端:

== s_client -servername c.test
80FF2DEC01000000:error:0A000419:SSL routines:ssl3_read_bytes:tlsv1 alert access denied:ssl/record/rec_layer_s3.c:910:SSL alert number 49
== s_client -servername 127.0.0.1
80FF2DEC01000000:error:0A000419:SSL routines:ssl3_read_bytes:tlsv1 alert access denied:ssl/record/rec_layer_s3.c:910:SSL alert number 49

服务端这边记下的 SNI:

server: sni=Some("a.test") result=Ok(())
server: sni=Some("c.test") result=Err(Custom { kind: InvalidData, error: General("no server certificate chain resolved") })
server: sni=None result=Err(Custom { kind: InvalidData, error: General("no server certificate chain resolved") })

s_client -servername 127.0.0.1 真的把 127.0.0.1 放进了 server_name 扩展,rustls 服务端记下的却是 None。

原因

解析器返回 None 就是 access_denied。 process_client_hello 之后,服务端在 server/hs.rs 调 cert_resolver:

            let certkey = self
                .config
                .cert_resolver
                .resolve(client_hello);

            certkey.ok_or_else(|| {
                cx.common.send_fatal_alert(
                    AlertDescription::AccessDenied,
                    Error::General("no server certificate chain resolved".to_owned()),
                )
            })?

服务端拿到一个描述原因的 Error::General,客户端只拿到告警码。ResolvesServerCertUsingSni::resolve 查表,没有 SNI 时直接返回 None,注释是「This kind of resolver requires SNI」。

三种情况为什么都没有 SNI 可查。

  • 名字不在表里:表里只有 a.test。
  • 客户端用 IP 连:rustls 客户端的 ServerName::IpAddress 不发 SNI(服务器证书里讲过);ClientConfig::enable_sni = false 也不发。
  • 客户端把 IP 塞进了 SNI:RFC 6066 不允许,process_client_hello 的注释写着「we choose to successfully parse these (into ServerNamePayload::IpAddress) but then act like the client sent no server_name extension」,于是 sni 是 None。
📝NOTE
冷知识:IP 形式的 SNI 从 0.23.5 起被当成「没有 SNI」

f0d33d13(「Ignore server_name extension containing IP address」,首次发布在 0.23.5)之前,SNI 里的名字只按 DNS 名解析,IP 解析不成 DNS 名,走进 Unknown 分支并打一条 Illegal SNI hostname received 的警告。提交说明交代了原因:「This works around quality-of-implementation issues in OpenSSL and Apple SecureTransport: they send server_name extensions containing IP addresses. RFC6066 specifically disallows that.」并说这和 LibreSSL 的做法类似。现在这类 ClientHello 能正常握手,代价是证书解析器看不到这个 IP,按 SNI 选证书的服务端得为「没有 SNI」准备一张证书,上面 s_client -servername 127.0.0.1 那一行就是这种情况。

修法

给解析器一张兜底证书。 包一层:先查 SNI 表,查不到就返回默认证书,并且记一条日志:

#[derive(Debug)]
struct SniWithDefault {
  by_sni: ResolvesServerCertUsingSni,
  default: Arc<CertifiedKey>,
}

impl ResolvesServerCert for SniWithDefault {
  fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
    let sni = hello.server_name().map(str::to_owned);
    let hit = self.by_sni.resolve(hello);
    if hit.is_none() {
      eprintln!("  [server log] no certificate for SNI {sni:?}, using default");
    }
    Some(hit.unwrap_or_else(|| self.default.clone()))
  }
}

默认证书的 SAN 是 127.0.0.1 和 default.test,用 with_cert_resolver 装上,再跑一遍:

== SNI table + default certificate (SAN 127.0.0.1, default.test)
connect a.test: Ok
  [server log] no certificate for SNI Some("c.test"), using default
connect c.test: client error: invalid peer certificate: certificate not valid for name "c.test"; certificate is only valid for IpAddress(127.0.0.1) or DnsName("default.test")
    debug: InvalidCertificate(NotValidForNameContext { expected: DnsName("c.test"), presented: ["IpAddress(127.0.0.1)", "DnsName(\"default.test\")"] })
    (server then sees: received fatal alert: BadCertificate)
  [server log] no certificate for SNI None, using default
connect 127.0.0.1 (no SNI sent): Ok

用 IP 连的客户端握手成功了。连 c.test 的仍然失败,但失败的地方换到了客户端的证书校验,报错直接说出「证书只对 127.0.0.1 和 default.test 有效」,比 AccessDenied 好查得多(名字匹配的规则见 NotValidForName)。兜底证书不会让错误的名字通过校验:客户端照样比对名字。

要在选证书之前做 IO(查数据库、从远端取证书),resolve 是同步的,改用 Acceptor 先读出 ClientHello,再决定用哪个 ServerConfig,见服务端 TLS 1.3。

怎么检查

  • 客户端看到 AccessDenied: 先去看服务端日志里有没有 no server certificate chain resolved。有,就是证书解析器返回了 None。
  • 确认客户端发了什么 SNI: 服务端 ServerConnection::server_name() 在握手失败后也能读(上面的 sni=Some("c.test") 就是失败之后读的);它是 None,就是客户端用 IP、关了 SNI,或者把 IP 塞进了 SNI。
  • 用 OpenSSL 复现: openssl s_client -connect host:port -servername <名字> 逐个名字试,tlsv1 alert access denied 和 rustls 客户端的 AccessDenied 是同一件事。

小结

  • 服务端的证书解析器返回 None 时发 access_denied,客户端只看到 AccessDenied,原因 no server certificate chain resolved 只在服务端。
  • ResolvesServerCertUsingSni 没有 SNI 就返回 None:IP 连接、enable_sni = false、SNI 里放 IP(0.23.5 起被当作没有 SNI)都会撞上。
  • 包一层带默认证书的解析器,并记录未命中的 SNI;名字不对的客户端会得到一个说得清楚的 NotValidForName。

💬 关联讨论

发起讨论
加载中...