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 (intoServerNamePayload::IpAddress) but then act like the client sent noserver_nameextension」,于是sni是None。
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。