编程 Go 的 x509 报错是 verify.go 里写死的分支:按文案反推证书校验根因

2026-11-08 00:04:32

Go 的 x509 报错是 verify.go 里写死的分支:按文案反推证书校验根因

线上 HTTPS 握手失败,日志里往往只有一句 x509: ...。很多人看到就去换证书、重签、加 -insecure,试一圈才发现方向错了。

crypto/x509/verify.go 里这些文案不是拼出来的信息,是 if/else 分支里写死的字符串(本文对照 master 分支 commit e87d2f5)。同一句前缀后面,根因可能完全不同。按文案反推,比盲试快得多。

HostnameError:一组文案,五种根因

HostnameError.Error() 的结构大致如下(省略了枚举循环):

func (h HostnameError) Error() string {
c := h.Certificate
maxNamesIncluded := 100

if !c.hasSANExtension() && matchHostnames(c.Subject.CommonName, splitHostname(h.Host)) {
return "x509: certificate relies on legacy Common Name field, use SANs instead"
}

var valid strings.Builder
if ip := net.ParseIP(h.Host); ip != nil {
if len(c.IPAddresses) == 0 {
return "x509: cannot validate certificate for " + h.Host +
" because it doesn't contain any IP SANs"
}
if len(c.IPAddresses) >= maxNamesIncluded {
return fmt.Sprintf("x509: certificate is valid for %d IP SANs, but none matched %s",
len(c.IPAddresses), h.Host)
}
// 枚举 IPAddresses
} else {
if len(c.DNSNames) >= maxNamesIncluded {
return fmt.Sprintf("x509: certificate is valid for %d names, but none matched %s",
len(c.DNSNames), h.Host)
}
valid.WriteString(strings.Join(c.DNSNames, ", "))
}

if valid.Len() == 0 {
return "x509: certificate is not valid for any names, but wanted to match " + h.Host
}
return "x509: certificate is valid for " + valid.String() + ", not " + h.Host
}

几个容易看漏的地方:

IP 分支在 DNS 分支之前。 证书里只有 DNS:example.com,你却用 https://1.2.3.4 直连,拿到的是 doesn't contain any IP SANs,而不是把 DNS 名列出来。这条文案只说明「没有 IP SAN」,不说明证书里什么都没有——别据此判断证书是空的。

100 是 maxNamesIncluded 硬编码在函数体里的常量,不是配置项、不是环境变量。所以 SAN 一多,报错反而变短:certificate is valid for 187 names, but none matched foo —— 名字一个都不列,只给数量。想看到具体名字,只能自己解析证书。IP SAN 分支同理。

not valid for any names 对应 valid.Len() == 0,即 DNSNames 为空。比如证书只签了 IP SAN,或者只有 CN 而压根没有 SAN 扩展。

文案片段触发条件
relies on legacy Common Name field无 SAN 扩展,且 CN 匹配主机名
doesn't contain any IP SANs目标是 IP,IPAddresses 为空
certificate is valid for A, B, not hostDNSNames 正常枚举(少于 100 条)
valid for N names/IP SANs, but none matched对应 SAN 数量 ≥ 100
not valid for any namesDNSNames 为空

unknown authority 后面的括号

s := "x509: certificate signed by unknown authority"
if e.hintErr != nil {
// certName 依次取 CN、Organization[0]、serial:
s += fmt.Sprintf(" (possibly because of %q while trying to verify candidate authority certificate %q)",
e.hintErr, certName)
}

带 possibly because of 说明 Go 其实找到了候选签发者,但拒绝了它:签名验不过,或者那张候选证书本身无效(比如已过期)。这跟「完全没有这个 CA」是两回事,排查方向也不同——前者去看 issuer 那张证书,后者去看信任库。

没有这个后缀,才是链上根本找不到能签发它的证书。

时间类

isValid 里两个分支:

current time %s is before %s   // now.UTC() vs NotBefore
current time %s is after %s    // now.UTC() vs NotAfter

外层文案是 x509: certificate has expired or is not yet valid: 。注意用的是 now.UTC(),容器时钟漂移会直接把这里打成「not yet valid」。

系统根与链

SystemRootsError 的文案是 x509: failed to load system roots and no roots provided,带 Err 时追加 ; 。只在 opts.Roots == nil 且 systemRootsPool() 返回 nil 时出现。

这是容器场景的典型分型:scratch / distroless 镜像里没有 /etc/ssl/certs,信任库是空的。现象是所有外部 HTTPS 都失败,不只是某一个域名——这个特征比文案本身更好用。

NoValidChains 的文案是 x509: no valid chains built,Detail 里可能带 %d candidate chains with incompatible key usage 或 %d candidate chains with invalid policies。链找到了,但 ExtKeyUsage 或 policy 对不上。另外 InvalidReason 的注释写明 NameConstraintsWithoutSANs 已是 legacy,不再返回。

版本边界

  • Go 1.15:默认不再把 CN 当主机名兜底。发布说明原话是「disabled by default」,可用 GODEBUG=x509ignoreCN=0 临时打开;CN 本身是非法主机名时,即使打开也永远忽略。
  • Go 1.17:GODEBUG=x509ignoreCN=0 开关被移除,没有任何回退手段。
  • Go 1.18 起:crypto/x509 拒绝 SHA-1 签名的证书,自签根证书除外。

所以看到 legacy Common Name field,先确认二进制是哪个 Go 版本编的,再决定是改代码还是重签证书。

最小排查命令

# 数证书张数:1 张 = 服务端只发了叶子,没带中间证书
openssl s_client -connect host:443 -servername host -showcerts </dev/null | grep -c 'BEGIN CERTIFICATE'

# 看 subject / issuer / 有效期 / SAN
openssl s_client -connect host:443 -servername host </dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# 用指定 CA 验证叶子
openssl verify -CAfile ca.pem leaf.pem

第一条只有 1 张时,基本可以锁定 unknown authority 的来源:服务端 Nginx/Apache 的证书链没拼全,叶子后面缺中间证书。

别把 InsecureSkipVerify 当解法

它关掉的是整条链的校验:签发者、有效期、主机名,一个都不剩。线上开这个开关等于把中间人攻击的门留着,而且它不会告诉你原本哪一环坏了。

上面这些文案已经把「哪一环」说清楚了:是 SAN 类型不对、是名字对不上、是数量超了枚举上限、是候选签发者被拒、是信任库为空、还是链建不起来。照着分支改配置或重签证书,比关校验靠谱。

复制全文 生成海报 x509 TLS 证书 Go 运维 排错

推荐文章

程序员茄子在线接单