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 host | DNSNames 正常枚举(少于 100 条) |
valid for N names/IP SANs, but none matched | 对应 SAN 数量 ≥ 100 |
not valid for any names | DNSNames 为空 |
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 类型不对、是名字对不上、是数量超了枚举上限、是候选签发者被拒、是信任库为空、还是链建不起来。照着分支改配置或重签证书,比关校验靠谱。