登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go TLS 握手失败时怎么查看证书链和 SNI

来源:17golang原创

时间:2026-09-07 18:57:47 397浏览 收藏

Go 的 TLS 握手失败,先不要把 InsecureSkipVerify 当修复方案。最常见的根因是三件事没有对齐:TCP 实际连接的地址、客户端发送的 SNI 主机名,以及服务端证书的 DNS SAN。排查时应先用 HandshakeContext 固定超时,再从 ConnectionState 读取证书链和 SNI;如果怀疑证书名称不匹配,只在临时诊断中跳过默认校验并手动验证,正式配置仍然保留校验。

要点速览
  • tls.Config.ServerName 是客户端发送的 SNI 和证书主机名校验的重要输入,不一定等于拨号用的 IP。
  • ConnectionState.PeerCertificates 按对端发送顺序保存证书,第一张是叶子证书;VerifiedChains 只有正常验证成功时才有意义。
  • 服务端可在 GetConfigForClient 中记录 ClientHelloInfo.ServerName,判断请求是否命中了正确的虚拟主机证书。

先把连接地址、SNI 和证书名称分开

例如服务通过固定 IP 暴露,但证书签给 api.example.com,客户端可以拨号 203.0.113.10:443,同时把 ServerName 设置为 api.example.com。这三个值的职责不同:地址负责找到 TCP 端点,SNI 告诉服务端要访问哪个虚拟主机,证书校验则确认叶子证书的 SAN 是否覆盖这个主机名。只把地址写成 IP,往往会得到“证书对 IP 不生效”的错误。

先让握手错误稳定、可记录

不要等第一次 ReadWrite 隐式触发握手。显式调用 HandshakeContext 能把超时和证书错误集中到同一个检查点:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

cfg := &tls.Config{
	// 发送 SNI,并让默认校验按该主机名检查证书。
	ServerName: "api.example.com",
}

raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", "203.0.113.10:443")
if err != nil {
	log.Fatal(err)
}
defer raw.Close()

conn := tls.Client(raw, cfg)
if err := conn.HandshakeContext(ctx); err != nil {
	// 保留原始错误,后面按 hostname、CA 或协议层分类。
	log.Printf("TLS handshake failed: %v", err)
	return
}
defer conn.Close()

如果这里出现 x509: certificate is valid for ... not api.example.com,优先检查 ServerName 与证书 SAN;如果是 x509: certificate signed by unknown authority,再检查系统根证书或 RootCAs。这两类错误都不应该通过关闭校验来掩盖。

从 ConnectionState 读出叶子证书和实际 SNI

握手成功后,ConnectionState 是最直接的观测入口。下面只输出排查需要的字段:不要把完整证书内容或私密握手材料写入普通业务日志。

state := conn.ConnectionState()
fmt.Printf("version=%s sni=%q verified=%t\n",
	tls.VersionName(state.Version), state.ServerName, len(state.VerifiedChains) > 0)

for i, cert := range state.PeerCertificates {
	// 第一张是叶子证书,后面通常是服务端发送的中间证书。
	fmt.Printf("cert[%d] subject=%q issuer=%q dns=%v\n",
		i, cert.Subject.CommonName, cert.Issuer.CommonName, cert.DNSNames)
}

if len(state.PeerCertificates) > 0 {
	// 诊断名称匹配;生产握手仍应使用默认证书校验。
	if err := state.PeerCertificates[0].VerifyHostname(cfg.ServerName); err != nil {
		log.Printf("hostname mismatch: %v", err)
	}
}

PeerCertificates 是对端实际发送的链,第一项是叶子证书;VerifiedChains 是 Go 根据根证书构建出的可信链。若使用了 InsecureSkipVerify,握手虽然可能继续,但 VerifiedChains 不会替你证明证书可信。

证书链不清楚时,用临时诊断配置隔离问题

当默认验证在握手阶段就失败,应用通常拿不到可供打印的成功连接状态。可以复制一份只用于排查的配置,设置 InsecureSkipVerify: true 让握手完成,再用 VerifyHostname 检查主机名并记录服务端实际发来的链:

debugCfg := &tls.Config{
	ServerName:         "api.example.com",
	InsecureSkipVerify: true, // 仅限临时诊断,不能进入生产配置。
}

debugConn := tls.Client(rawConn, debugCfg)
if err := debugConn.HandshakeContext(ctx); err != nil {
	log.Printf("even diagnostic handshake failed: %v", err)
	return
}
state := debugConn.ConnectionState()
if len(state.PeerCertificates) == 0 {
	log.Print("server returned no certificate")
	return
}
if err := state.PeerCertificates[0].VerifyHostname(debugCfg.ServerName); err != nil {
	log.Printf("SNI/certificate name mismatch: %v", err)
}

这一步只能回答“服务端发了什么证书、名称是否匹配”,不能回答“证书是否被信任”。根证书缺失、中间证书没发全等问题,仍要回到正常配置,用正确的 RootCAs 和服务端证书链修复。

服务端直接记录客户端送来的 SNI

多域名服务应在服务端看 ClientHelloInfo.ServerName,而不是猜客户端访问了哪个域名。GetConfigForClient 收到 ClientHello 后可以按 SNI 选择配置,也可以先记录它:

serverCfg := &tls.Config{
	Certificates: []tls.Certificate{defaultCert},
	GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
		// 空字符串表示客户端没有发送 SNI。
		log.Printf("client SNI=%q", hello.ServerName)
		return nil, nil // nil 表示继续使用当前配置。
	},
}

如果日志里的 SNI 为空,或者 SNI 与预期域名不同,服务端可能返回默认站点证书,随后客户端报主机名不匹配。若 SNI 正确但仍返回错误证书,再检查 Certificates 链是否以叶子证书开头、GetCertificateGetConfigForClient 是否选择了错误的配置。

按错误信息做最后一轮复查

现象优先检查不要先做的事
证书对某主机名不生效ServerName、SAN、服务端 SNI 选证书不要直接设 InsecureSkipVerify
unknown authority系统根证书、中间证书、RootCAs不要把信任问题改成名称问题
服务端收到空 SNI客户端 tls.Config.ServerName 是否为空不要只更换拨号 IP
握手超时或协议错误端口是否真的是 TLS、网络超时和服务端日志不要反复重试同一配置

排查完成后删除诊断配置,恢复默认证书验证,并保留一次握手错误、SNI、证书 subject/issuer 和 SAN 的结构化记录。这样下一次再遇到 TLS 握手失败时,先看“连到哪里、带了什么 SNI、收到了哪条链”,通常比盲目改 TLS 版本更快找到原因。

Go TLS 握手中连接地址、ServerName、ClientHelloInfo、证书链和 ConnectionState 的静态关系
图1:TLS 排查时把连接端点、SNI、握手观测和证书链放在同一张关系图中。
Go 服务端按 ClientHelloInfo.ServerName 选择证书配置并返回证书链的静态关系
图2:服务端 SNI 与 GetConfigForClient、证书配置和客户端主机名校验的关系。

相关问题

ServerName 必须和拨号地址相同吗?

不必须。拨号地址可以是 IP 或内部解析地址,ServerName 应填写证书和虚拟主机使用的 DNS 名称。

为什么 PeerCertificates 有内容但 VerifiedChains 为空?

常见原因是使用了 InsecureSkipVerify,或服务端场景没有启用对应的客户端证书验证。它们代表“收到的链”和“验证得到的链”,不能混为一谈。

服务端没有收到 SNI 怎么办?

检查客户端的 tls.Config.ServerName 是否为空,并确认应用没有只创建裸 tls.Client 而遗漏目标主机名。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>