通过Go HTTP Client的报错看标准库https请求

目录

在用 Go 自带的 http client 进行默认 Get 操作的时候,发现如下错误

x509: certificate signed by unknown authority

这个报错来自 crypto/x509 中关于证书签名的验证

负责验证证书的方法签名

func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err error)

关于 x509

这是一种自签名证书,x509证书中包含着一个证书链 (Certificate Chain),包含根证书,中间证书即CA证书,和用户证书。根证书因为需要预装在用户系统中,因此引入中间证书 来更好地普及和扩展证书,因此当系统要验证一个证书是否为合法时的思路就是,先验证中间证书,根据中间证书得到颁发这个中间证书的根证书签名(有可能还是中间证书,因此要一直向上寻找到根证书),然后再通过系统内置的根证书进行验证,如果通过,则由此CA证书颁发的用户证书为可信的有效证书,否则为无效

继续阅读源码

Go 在验证的时候在 crypto/x509 中实现了关于证书链构建与验证

// 当前证书不是根证书时开始 DFS 构建证书链
func (c *Certificate) buildChains(cache map[int][][]*Certificate, currentChain []*Certificate, opts *VerifyOptions) (chains [][]*Certificate, err error) {
    // 查找当前中间证书证书的根,也就是当前证书的根颁发者
    possibleRoots, failedRoot, rootErr := opts.Roots.findVerifiedParents(c)
nextRoot:
    // 遍历每个可能的根证书颁发者
    for _, rootNum := range possibleRoots {
        // 拿到颁发者的证书
        root := opts.Roots.certs[rootNum]

        for _, cert := range currentChain {
            if cert.Equal(root) {
                // 在已有证书链中,不需要验证
                continue nextRoot
            }
        }

        // 不在已有证书链中,需要验证是否为根证书
        err = root.isValid(rootCertificate, currentChain, opts)
        // 即便验证有错误,依然继续后面的验证,因为颁发者只是可能为链的根证书
        if err != nil {
            continue
        }
        // 找到根,将根证书添加到已有链中,并添加到 chains,最后验证 chains 的长度来确定是否成功构建了到跟的证书链
        // appendToFreshChain 将现有链进行复制,容量+1,用来存放 root 证书
        chains = append(chains, appendToFreshChain(currentChain, root))
    }

    // 查找当前证书可能的中间证书颁发者
    possibleIntermediates, failedIntermediate, intermediateErr := opts.Intermediates.findVerifiedParents(c)
nextIntermediate:
    // 查找中间证书与查找根证书类似
    for _, intermediateNum := range possibleIntermediates {
        intermediate := opts.Intermediates.certs[intermediateNum]
        for _, cert := range currentChain {
            if cert.Equal(intermediate) {
                continue nextIntermediate
            }
        }
        err = intermediate.isValid(intermediateCertificate, currentChain, opts)
        if err != nil {
            continue
        }
        var childChains [][]*Certificate
        childChains, ok := cache[intermediateNum]
        if !ok {
            // 深度搜索当前中间证书的上游构建证书链
            childChains, err = intermediate.buildChains(cache, appendToFreshChain(currentChain, intermediate), opts)
            // cache 用于记忆已构建的中间证书链
            cache[intermediateNum] = childChains
        }
        // 添加含有中间证书的链到 chains 构成完整的从根开始的证书链
        chains = append(chains, childChains...)
    }

    // 构建结束,证书链构建成功,有且只有唯一的一条经验证的证书链
    if len(chains) > 0 {
        err = nil
    }

    // 证书链构建失败,错误未知
    if len(chains) == 0 && err == nil {
        hintErr := rootErr
        hintCert := failedRoot
        if hintErr == nil {
            hintErr = intermediateErr
            hintCert = failedIntermediate
        }
        // 上文提到的报错的来源
        err = UnknownAuthorityError{c, hintErr, hintCert}
    }

    // error 已经用命名返回变量err捕捉,直接回传即可
    return
}

好了,现在我们知道这个错误的产生,源于无法构建有效的证书链

那么,这与 http client 有什么联系?我们顺藤摸瓜继续往上看

分别在 crypto/tlshandshake_client.gohandshake_server.go 中都有出现验证证书的过程

// client
c.verifiedChains, err = certs[0].Verify(opts)
// server
chains, err := certs[0].Verify(opts)

这就不得不再次复习一下 https 连接建立的过程了

https 连接的建立

SSL或TLS握手示意图

httpsHandshake

通过图我们可以直观地看到在步骤3和步骤6的时候,会发生证书的验证,在 Go 的 http client中,显然是使用 handshake_client.go 中的 Verify 来验证服务器的证书

func (c *Conn) Handshake() error {
    // 当前连接的握手必须互斥,因此这里加了锁
    c.handshakeMutex.Lock()
    defer c.handshakeMutex.Unlock()
    if err := c.handshakeErr; err != nil {
        return err
    }
    if c.handshakeComplete {
        return nil
    }
    c.in.Lock()
    defer c.in.Unlock()
    if c.isClient {
        // 是客户端的握手
        c.handshakeErr = c.clientHandshake()
    } else {
        // 是服务端握手
        c.handshakeErr = c.serverHandshake()
    }
    if c.handshakeErr == nil {
        c.handshakes++
    } else {
        // If an error occurred during the hadshake try to flush the
        // alert that might be left in the buffer.
        c.flush()
    }
    if c.handshakeErr == nil && !c.handshakeComplete {
        panic("handshake should have had a result.")
    }
    return c.handshakeErr
}

conn.go 的源码中我们可以发现使用 isClient 属性来区分是客户端握手还是服务端的握手。在开发环境中,可能我们需要临时使用自己的自签名证书来进行 https 通信, 如何跳过这个验证证书的过程呢?我们接着 clientHandshake() 往下看

关于 TLS 会话

当然,我们不能每次建立 https 通信的时候都重新握一次手,因此通过为 https 引入状态将 TLS 握手状态在用会话记录下来,能够大大减少服务器的压力, 早期的 TLS 会使用 Session ID 来记录,因为负载均衡的存在,事实上只有第一次握手时被分配到的物理机器上才有客户端 Session ID 的记录,当第二次访问的时候, 我们并不能指望负载均衡算法能够再次将请求分配给同一台物理机器,其中一种解决方案是负责握手的服务器将 Session ID 存储到 redis 或者 memcached 集, 当第二次访问的时候 统一去查询缓存,但让服务器在一段时间记住状态可能会暴露潜在的扩展性问题。于是,TLS v1.2 引入了会话凭证 (session ticket) ,将会话状态的存储交给客户端,第一次握手结束时, 服务器会下发经过会话密钥 (Session key) 加密的会话凭证,之前会话的相关状态都会保存在会话凭证中并由客户端保存,当第二次进行握手的时候,客户端需要将本地缓存的会话凭证包含在客户端握手消息中, 只要服务器能够共享同一会话密钥,那么这台服务器就能够解密得到之前的会话状态,因为会话密钥不具备向前安全性,因此需要定期轮换更新密钥

客户端握手过程

func (c *Conn) clientHandshake() error {
    if c.config == nil {
        // 没有传入自定义的client设置,则使用默认设置
        c.config = defaultConfig()
    }
    // 可能是首次握手,因此相关的会话状态需要更新
    c.didResume = false
    // 构造 Client Hello 报文消息
    hello, err := makeClientHello(c.config)
    if err != nil {
        return err
    }
    if c.handshakes > 0 {
        hello.secureRenegotiation = c.clientFinished[:]
    }
    var session *ClientSessionState
    var cacheKey string
    sessionCache := c.config.ClientSessionCache
    // 检查是否支持会话凭证
    if c.config.SessionTicketsDisabled {
        sessionCache = nil
    }
    if sessionCache != nil {
        hello.ticketSupported = true
    }
    // 如果已经保存了会话凭证,且处于最开始的 "Client Hello" 阶段,如果处于发送客户凭证(step 5)的阶段,则跳过会话恢复
    if sessionCache != nil && c.handshakes == 0 {
        // 尝试得到先前 TLS 的会话状态
        cacheKey = clientSessionCacheKey(c.conn.RemoteAddr(), c.config)
        candidateSession, ok := sessionCache.Get(cacheKey)
        if ok {
            // 检查之前会话使用的加密组件是否能继续延用
            cipherSuiteOk := false
            for _, id := range hello.cipherSuites {
                if id == candidateSession.cipherSuite {
                    cipherSuiteOk = true
                    break
                }
            }
            versOk := candidateSession.vers >= c.config.minVersion() &&
                candidateSession.vers <= c.config.maxVersion()
            if versOk && cipherSuiteOk {
                session = candidateSession
            }
        }
    }
    if session != nil
        hello.sessionTicket = session.sessionTicket
        // 生成随机的 session ID,如果服务器拒绝了客户端的会话凭证并触发一次完整的握手,那么我们可以通过这个 ID 探知此服务器是否支持会话恢复
        hello.sessionId = make([]byte, 16)
        if _, err := io.ReadFull(c.config.rand(), hello.sessionId); err != nil {
            return errors.New("tls: short read from Rand: " + err.Error())
        }
    }
    hs := &clientHandshakeState{
        c:       c,
        hello:   hello,
        session: session,
    }
    // 这里正式开始进行握手
    if err = hs.handshake(); err != nil {
        return err
    }
    // 如果成功完成握手,服务器下发了新的会话凭证,那么则更新本地缓存的会话凭证
    if sessionCache != nil && hs.session != nil && session != hs.session {
        sessionCache.Put(cacheKey, hs.session)
    }
    return nil
}

如果是客户端类型的握手,那么客户端会发出 Client Hello 的报文给服务端

func (hs *clientHandshakeState) handshake() error {
    c := hs.c
    // 发送 ClientHello 报文
    if _, err := c.writeRecord(recordTypeHandshake, hs.hello.marshal()); err != nil {
        return err
    }
    msg, err := c.readHandshake()
    if err != nil {
        return err
    }
    var ok bool
    if hs.serverHello, ok = msg.(*serverHelloMsg); !ok {
        c.sendAlert(alertUnexpectedMessage)
        return unexpectedMessageError(hs.serverHello, msg)
    }
    // ...
    // 解析服务端发回的 ServerHello 报文
    isResume, err := hs.processServerHello()
    if err != nil {
        return err
    }

    if isResume {
        // 恢复已有的会话状态,不需要继续进行握手
        if err := hs.establishKeys(); err != nil {
            return err
        }
        if err := hs.readSessionTicket(); err != nil {
            return err
        }
        if err := hs.readFinished(c.serverFinished[:]); err != nil {
            return err
        }
        c.clientFinishedIsFirst = false
        if err := hs.sendFinished(c.clientFinished[:]); err != nil {
            return err
        }
        if _, err := c.flush(); err != nil {
            return err
        }
    } else {
        // 首次握手,继续完成完整的握手,从服务端的报文中解析获取证书凭证
        if err := hs.doFullHandshake(); err != nil {
            return err
        }
        if err := hs.establishKeys(); err != nil {
            return err
        }
        if err := hs.sendFinished(c.clientFinished[:]); err != nil {
            return err
        }
        if _, err := c.flush(); err != nil {
            return err
        }
        c.clientFinishedIsFirst = true
        // 拿到会话凭证,下次请求就不需要完整的走一遍握手了
        if err := hs.readSessionTicket(); err != nil {
            return err
        }
        if err := hs.readFinished(c.serverFinished[:]); err != nil {
            return err
        }
    }
    c.ekm = ekmFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.hello.random, hs.serverHello.random)
    c.didResume = isResume
    c.handshakeComplete = true
    return nil
}

现在思路逐渐清晰,如果想要跳过证书验证的步骤,使用自签名的证书,只需要跳过第一次完整握手时验证证书凭证即可

hs.doFullHandshake() 继续往下看

func (hs *clientHandshakeState) doFullHandshake() error {
    // 检查报文中包含的证书内容
    // ...
    if c.handshakes == 0 {
        // 初次握手,需要验证服务端证书
        certs := make([]*x509.Certificate, len(certMsg.certificates))
        for i, asn1Data := range certMsg.certificates {
            cert, err := x509.ParseCertificate(asn1Data)
            if err != nil {
                c.sendAlert(alertBadCertificate)
                return errors.New("tls: failed to parse certificate from server: " + err.Error())
            }
            certs[i] = cert
        }
        // 重点:只需通过修改 config 即可跳过这个验证步骤!!
        if !c.config.InsecureSkipVerify {
            opts := x509.VerifyOptions{
                Roots:         c.config.RootCAs,
                CurrentTime:   c.config.time(),
                DNSName:       c.config.ServerName,
                Intermediates: x509.NewCertPool(),
            }
            for i, cert := range certs {
                if i == 0 {
                    continue
                }
                opts.Intermediates.AddCert(cert)
            }
            // 这个是我们想跳过的步骤
            c.verifiedChains, err = certs[0].Verify(opts)
            if err != nil {
                c.sendAlert(alertBadCertificate)
                return err
            }
        }
    }
    // ...
}

最后,我们只需要几行代码配置一下我们的 HTTP Client 即可跳过证书的验证

var (
    once     sync.Once
    instance *http.Client
)

func New() *http.Client {
    once.Do(func() {
        tr := &http.Transport{
            // 简单配置一下
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
        instance = &http.Client{
            Timeout:   8 * time.Second,
            Transport: tr,
        }
    })
    return instance
}

如果想要区分生产环境和开发环境,只需要简单用 go build 在编译时区分一下环境即可

通过一个报错追根溯源,借机将 https 的请求过程在标准库源码的基础上又复习了一遍,可谓收获颇丰, 如有疏忽,恳请各位大佬指正。

参考资料

商业转载请联系作者获得授权,非商业转载请注明出处,谢谢合作!

联系方式:tecker_[email protected]