Rust in Everything
基于 rustls 0.23.46

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 对类型。

💬 关联讨论

发起讨论
加载中...