certificate not valid for name
主线位置:校验主线的最后一步。路径构建成功之后,rustls 拿
ClientConnection::new时给的ServerName去和叶子证书的 subjectAltName(SAN)比。比较规则(通配符、大小写、末尾的点)和ServerName的解析表见服务器证书;本页讲最常撞上的三种情形和修法。
flowchart TB
s["主机字符串"] --> p{"ServerName try_from"}
p -->|带端口 括号 scheme| bad["InvalidDnsNameError"]
p -->|DNS 名| dn["DnsName"]
p -->|IP 字面量| ip["IpAddress"]
dn --> sd["只比 SAN 的 dNSName"]
ip --> si["只比 SAN 的 iPAddress"]
sd --> miss["NotValidForNameContext"]
si --> miss
cn["Subject CN"] -.->|不看| miss
实验 crate:exp-rustls/pita-lab,src/bin/p04_names.rs。根 CA 直接签叶子,SAN 由 rcgen 按字符串生成(能解析成 IP 的写成 iPAddress,否则写成 dNSName)。M2 Max、macOS 27、rustc 1.99.0,release,ring provider。
现象
IP 和 DNS 名互不相通。 证书给 localhost 签的,客户端按 127.0.0.1 连;或者反过来:
(server then sees: received fatal alert: BadCertificate)
SAN [localhost], connect 127.0.0.1: client error: invalid peer certificate: certificate not valid for name "127.0.0.1"; certificate is only valid for DnsName("localhost")
debug: InvalidCertificate(NotValidForNameContext { expected: IpAddress(V4(Ipv4Addr([127, 0, 0, 1]))), presented: ["DnsName(\"localhost\")"] })
(server then sees: received fatal alert: BadCertificate)
SAN [127.0.0.1], connect localhost: client error: invalid peer certificate: certificate not valid for name "localhost"; certificate is only valid for IpAddress(127.0.0.1)
debug: InvalidCertificate(NotValidForNameContext { expected: DnsName("localhost"), presented: ["IpAddress(127.0.0.1)"] })
内网里用短名连接也是同一类:
SAN [db.internal], connect db: client error: invalid peer certificate: certificate not valid for name "db"; certificate is only valid for DnsName("db.internal")
只有 CN、没有 SAN 的证书。 老的内部 CA 常这样签:
CN=localhost, no SAN: client error: invalid peer certificate: certificate not valid for name "localhost"; certificate is not valid for any names (according to its subjectAltName extension)
debug: InvalidCertificate(NotValidForNameContext { expected: DnsName("localhost"), presented: [] })
ServerName 本身就造不出来。 从 URL 或配置里拿主机名时多带了东西:
"[::1]" -> Err(InvalidDnsNameError) / invalid dns name
"example.com:443" -> Err(InvalidDnsNameError) / invalid dns name
"https://example.com" -> Err(InvalidDnsNameError) / invalid dns name
"example.com/" -> Err(InvalidDnsNameError) / invalid dns name
" example.com" -> Err(InvalidDnsNameError) / invalid dns name
这一种在握手之前就失败,Display 只有一句 invalid dns name,不告诉你哪个字符不对。
原因
客户端校验完路径,调 verify_server_name,它直接交给 rustls-webpki 的 verify_is_valid_for_subject_name:
match server_name {
ServerName::DnsName(dns_name) => verify_dns_names(dns_name, &self.inner),
// IP addresses are not compared against the subject field;
// only against Subject Alternative Names.
ServerName::IpAddress(ip_address) => verify_ip_address_names(ip_address, &self.inner),
_ => Err(Error::UnsupportedNameType),
}
ServerName是什么类型,就只和 SAN 里的同类条目比。DNS 名不会去匹配iPAddress;IP 也不会去匹配写成字符串的dNSName "127.0.0.1",实验里专门造了一张:SAN dNSName "127.0.0.1", connect 127.0.0.1: client error: invalid peer certificate: certificate not valid for name "127.0.0.1"; certificate is only valid for DnsName("127.0.0.1")两条路都只读 SAN 扩展,Subject 里的 CN 完全不看。没有 SAN 时
presented是空列表,这就是第二个例子里那句「not valid for any names」。匹配失败时 rustls-webpki 把证书里所有 SAN 交回来,rustls 的
pki_error包成NotValidForNameContext,告警是bad_certificate。
ServerName 的解析在 pki-types 的 TryFrom<&str> for ServerName:先按 DNS 名解析,失败再按 IpAddr 解析,两样都不是就 InvalidDnsNameError。:、/、方括号、空格都不是 DNS 名里允许的字符,带端口的 example.com:443 和带括号的 [::1] 也不是合法的 IP 字面量。大小写会保留("Example.COM" -> Ok(DnsName("Example.COM"))),比较时不区分。
修法
签发证书时把所有会用来连接的名字都写进 SAN,DNS 名和 IP 各写各的:
SAN [localhost, 127.0.0.1, ::1], connect 127.0.0.1: Ok
SAN [localhost, 127.0.0.1, ::1], connect ::1: Ok
rcgen 的 CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into(), "::1".into()]) 会把后两个写成 iPAddress。用 OpenSSL 签发时在 subjectAltName 里写 DNS:localhost, IP:127.0.0.1。只有 CN 的旧证书只能重签,rustls 没有开关让它去读 CN。
客户端用和证书一致的名字连接。 证书是给 db.internal 签的,就用 db.internal 建 ServerName,TCP 连哪个地址是另一回事:
let name = ServerName::try_from("db.internal")?;
let sock = TcpStream::connect("10.0.0.5:5432")?;
let conn = ClientConnection::new(cfg, name.to_owned())?;
ServerName 和套接字地址是分开的两个参数,正是为了这种情况。实验里用 db.internal 的证书、按这个写法连 127.0.0.1 上的服务端:ServerName db.internal over TcpStream to 127.0.0.1:56842: handshake done。
从 URL 里取主机名再给 ServerName:去掉 scheme、路径、端口,IPv6 再去掉方括号。实验里从 https://[::1]:8443/x 取出来的是 "::1",解析成 IpAddress。手里已经有 IpAddr 时直接 ServerName::from(ip),不必转成字符串再解析。
名字是 IP 时客户端不发 SNI,服务端如果按 SNI 选证书,会拿不到名字;那一侧的表现见服务端找不到证书。
怎么检查
看证书里到底有哪些名字。 用 OpenSSL 打印 Subject 和 SAN:
### cn_only
No extensions in certificate
subject=CN=localhost
### full_san
subject=CN=leaf
X509v3 Subject Alternative Name:
DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
命令是 openssl x509 -inform der -in <证书> -noout -subject -ext subjectAltName(PEM 去掉 -inform der)。没有 X509v3 Subject Alternative Name 这一行,rustls 就不可能匹配上任何名字。
看报错里的 expected 和 presented。 Debug 输出直接列出两边:expected 是 DnsName 还是 IpAddress,presented 里有没有同类的条目。类型不一致就是 IP 对 DNS 名;presented: [] 就是只有 CN。
InvalidDnsNameError 时打印传进去的字符串,用 {:?} 而不是 {},前后的空格和隐藏字符才看得出来。
小结
- 名字只和 SAN 的同类条目比:IP 比
iPAddress,DNS 名比dNSName,CN 不看。 - 证书只有
localhost却按127.0.0.1连、或只有 CN 没有 SAN,报certificate not valid for name ...(NotValidForNameContext),服务端看到BadCertificate。 ServerName::try_from不接受端口、scheme、路径、方括号和空格,报invalid dns name。- 修法:签发时把 DNS 名和 IP 都写进 SAN;
ServerName用证书上的名字,和 TCP 地址分开给。 - 用
openssl x509 -noout -ext subjectAltName看证书,用报错的expected/presented对类型。