在用 Go 自带的 http client 进行默认 Get 操作的时候,发现如下错误
1 x509: certificate signed by unknown authority
这个报错来自 crypto/x509 中关于证书签名的验证
负责验证证书的方法签名
1 func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err error )
关于 x509 这是一种自签名证书,x509证书中包含着一个证书链 (Certificate Chain),包含根证书,中间证书即CA证书,和用户证书。根证书因为需要预装在用户系统中,因此引入中间证书 来更好地普及和扩展证书,因此当系统要验证一个证书是否为合法时的思路就是,先验证中间证书,根据中间证书得到颁发这个中间证书的根证书签名(有可能还是中间证书,因此要一直向上寻找到根证书),然后再通过系统内置的根证书进行验证,如果通过,则由此CA证书颁发的用户证书为可信的有效证书,否则为无效
继续阅读源码
Go 在验证的时候在 crypto/x509 中实现了关于证书链构建与验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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 = 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[intermediateNum] = childChains } 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} } return }
好了,现在我们知道这个错误的产生,源于无法构建有效的证书链
那么,这与 http client 有什么联系?我们顺藤摸瓜继续往上看
分别在 crypto/tls 的 handshake_client.go 和 handshake_server.go 中都有出现验证证书的过程
1 2 3 4 c.verifiedChains, err = certs[0 ].Verify(opts) chains, err := certs[0 ].Verify(opts)
这就不得不再次复习一下 https 连接建立的过程了
https 连接的建立 SSL或TLS握手示意图
通过图我们可以直观地看到在步骤3和步骤6的时候,会发生证书的验证,在 Go 的 http client中,显然是使用 handshake_client.go 中的 Verify 来验证服务器的证书
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 { 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) 加密的会话凭证,之前会话的相关状态都会保存在会话凭证中并由客户端保存,当第二次进行握手的时候,客户端需要将本地缓存的会话凭证包含在客户端握手消息中, 只要服务器能够共享同一会话密钥,那么这台服务器就能够解密得到之前的会话状态,因为会话密钥不具备向前安全性,因此需要定期轮换更新密钥
客户端握手过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 func (c *Conn) clientHandshake() error { if c.config == nil { c.config = defaultConfig() } c.didResume = false 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 } if sessionCache != nil && c.handshakes == 0 { 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 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 的报文给服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func (hs *clientHandshakeState) handshake() error { c := hs.c 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) } 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() 继续往下看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 } 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 即可跳过证书的验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 的请求过程在标准库源码的基础上又复习了一遍,可谓收获颇丰, 如有疏忽,恳请各位大佬指正。
参考资料
商业转载请联系作者获得授权,非商业转载请注明出处,谢谢合作!
联系方式:[email protected]