cert.Verify() 返回 nil 不代表证书可信,因其仅执行基础链式信任和时间校验,不检查OCSP/CRL吊销状态、不验证SAN匹配(需显式传DNSName),且若RootCAs为nil会退至不可控系统根池;生产环境必须同时设置VerifyOptions.Roots和DNSName,并自行实现吊销检查。

Go 的 x509.CertPool 和 Verify 方法能完成标准证书链验证,但默认不校验 OCSP/CRL、不强制检查 SAN 匹配细节,生产环境必须手动补全逻辑。先说几个核心判断:很多开发者看到 cert.Verify() 返回 nil 就以为万事大吉,但事实远没那么简单。这个函数只做了最基础的校验,它不会检查证书是否被吊销,也不会验证 SAN 是否匹配,甚至可能因为 RootCAs 为 nil 而退到不可控的系统根池。所以,生产环境必须同时设置 VerifyOptions.Roots 和 DNSName,并自行实现吊销检查。
为什么 cert.Verify() 返回 nil 也不代表证书可信
标准调用 cert.Verify(&x509.VerifyOptions{}) 只做基础链式信任和时间校验,你猜它忽略了什么?
- 检查证书是否被吊销(OCSP 或 CRL 默认关闭)
- 验证
DNSNames是否覆盖当前访问域名(VerifyOptions.DNSName必须显式传入,否则跳过 SAN 匹配) - 拒绝自签名根证书未加入
RootCAs的情况(若RootCAs == nil,会 fallback 到系统根池,可能误信)
实操建议:始终传入非空 RootCAs,且 DNSName 设为实际目标主机名;如需吊销检查,必须自己实现 VerifyPeerCertificate 回调并调用 ocsp.Request 或解析 CRL 分发点。
VerifyOptions.Roots 和 VerifyOptions.DNSName 必须同时设置
这两个字段是联动的:缺一不可,否则验证结果毫无意义。
Roots必须是包含可信 CA 的*x509.CertPool,不能为nil—— 否则 Go 会退到系统根证书池,行为不可控DNSName必须设为你要访问的域名(如"api.internal"),不能留空或传 IP 地址(除非证书 SAN 明确含该 IP)- 若服务端证书使用通配符(如
*.svc.cluster.local),DNSName必须完全匹配通配规则(foo.svc.cluster.local✅,foo.bar.svc.cluster.local❌)
证书链顺序错误导致 Verify 失败但无提示
Go 要求传入的 rawCerts(如从 tls.Conn.ConnectionState().PeerCertificates 拿到的)必须是完整、有序的链:叶证书在前,中间 CA 在后,根 CA 可省略(因由 Roots 提供)。
- 常见错误:只传了叶证书,没附中间证书 →
Verify找不到签发者,返回 “unknown authority” - 更隐蔽的问题:中间证书顺序颠倒(如 root 在前、intermediate 在后)→
Verify可能静默失败或返回不完整链 - 调试方法:用
openssl s_client -connect host:port -showcerts看服务端实际发了哪些证书,按顺序拼成 PEM 文件再测试
自定义 VerifyPeerCertificate 时 rawCerts 是原始字节,不是解析后对象
这个回调函数收到的 rawCerts [][]byte 是未解析的 DER 数据,不能直接调用 NotAfter 或 DNSNames —— 必须先用 x509.ParseCertificate 解析。
- 第一个元素
rawCerts[0]是叶证书,后续是中间链;根证书通常不在其中 - 解析失败(如私钥格式错、PEM 封装不对)会导致 panic,务必加
recover或提前校验 - 若想校验证书指纹,应在解析后取
cert.Signature或cert.Raw做 SHA256,而非对rawCerts[0]直接哈希(PEM 头尾会影响结果)
最容易被忽略的是:VerifyPeerCertificate 回调里不做 cert.Verify 调用,就等于绕过了整条信任链校验 —— 它只负责“你能拿到什么”,不代表“它可信”。