golang与TLS实现

在最近的项目中,需要对对方服务器的证书状态进行检查,获取证书上,就需要进行TLS握手,获取到证书信息,在项目中但是使用直接拼出ClientHello包的方式进行TLS握手操作,今天看一些go中的源码中是如何进行TLS握手的。

首先从建立连接开始:tls.DialWithDialer(dialer *net.Dialer,network,addr string ,config *tls.Config),该方法在cryto/tlstls.go文件中。

ClientHello

先上代码

//去除了一些个人认为不是很重要的代码,只留下和tls相关的代码
rawConn, err := dialer.Dial(network, addr)
    if err != nil {
        return nil, err
    }

    colonPos := strings.LastIndex(addr, ":")
    if colonPos == -1 {
        colonPos = len(addr)
    }
    hostname := addr[:colonPos]

    if config == nil {
        config = defaultConfig()
    }
    // If no ServerName is set, infer the ServerName
    // from the hostname we're connecting to.
    if config.ServerName == "" {
        // Make a copy to avoid polluting argument or default.
        c := config.clone()
        c.ServerName = hostname
        config = c
    }

    conn := Client(rawConn, config)

    if timeout == 0 {
        err = conn.Handshake()
    } else {
        go func() {
            errChannel <- conn.Handshake()
        }()

        err = <-errChannel
    }

从代码中可以看到,调用了dialer的拨号方法,得到net包下的Conn结构,然后通过Client(conn net.Conn, config *Config),封装出一个tls包下的Conn结构。在进行TLS连接时,因为现在有很多的公司使用了SNI,因此,在进行tls连接时要指定连接的服务器名称。在tls的源码中,对config中是否添加了ServerName进行了判断,如果没有填写,就使传入的addr中取出服务器地址。接下来就是进行握手(conn.Handshake)

func (c *Conn) Handshake() error

//一些锁操作省却   
if c.isClient {
        c.handshakeErr = c.clientHandshake()
    } else {
        c.handshakeErr = c.serverHandshake()
    }
    if c.handshakeErr == nil {
        c.handshakes++
    }

现在阶段是ClientServer 发送Hello信息。因此我点击c.clientHandshake到这中一探究竟。

clientHandshake() err方法中,进行了ClientHello的信息的生成。首先是判断是否有tls.Config

hello := &clientHelloMsg{
        vers:                         c.config.maxVersion(),
        compressionMethods:           []uint8{compressionNone},
        random:                       make([]byte, 32),
        ocspStapling:                 true,
        scts:                         true,
        serverName:                   hostnameInSNI(c.config.ServerName),
        supportedCurves:              c.config.curvePreferences(),
        supportedPoints:              []uint8{pointFormatUncompressed},
        nextProtoNeg:                 len(c.config.NextProtos) > 0,
        secureRenegotiationSupported: true,
        alpnProtocols:                c.config.NextProtos,
    }

hello是需要发送的clientHello信息,但是在上面的hello信息中缺少了使用的套件的信息,在套件的选择上也很有意思:

NextCipherSuite:
    for _, suiteId := range possibleCipherSuites {
        for _, suite := range cipherSuites {
            if suite.id != suiteId {
                continue
            }
            // Don't advertise TLS 1.2-only cipher suites unless
            // we're attempting TLS 1.2.
            if hello.vers < VersionTLS12 && suite.flags&suiteTLS12 != 0 {
                continue
            }
            hello.cipherSuites = append(hello.cipherSuites, suiteId)
            continue NextCipherSuite
        }
    }

其中possibleCipherSuites是用户在tls.Config中设置的CipherSuites ,在上面的代码中使用到了cipherSuitescipherSuites是go中内置的一些加密套件 :

var cipherSuites = []*cipherSuite{
    // Ciphersuite order is chosen so that ECDHE comes before plain RSA
    // and RC4 comes before AES-CBC (because of the Lucky13 attack).
    {TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheRSAKA, suiteECDHE | suiteTLS12, nil, nil, aeadAESGCM},
    {TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheECDSAKA, suiteECDHE | suiteECDSA | suiteTLS12, nil, nil, aeadAESGCM},
    {TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 32, 0, 4, ecdheRSAKA, suiteECDHE | suiteTLS12 | suiteSHA384, nil, nil, aeadAESGCM},
    {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 32, 0, 4, ecdheECDSAKA, suiteECDHE | suiteECDSA | suiteTLS12 | suiteSHA384, nil, nil, aeadAESGCM},
    {TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, suiteECDHE | suiteDefaultOff, cipherRC4, macSHA1, nil},
    {TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, suiteECDHE | suiteECDSA | suiteDefaultOff, cipherRC4, macSHA1, nil},
    {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, suiteECDHE, cipherAES, macSHA1, nil},
    {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherAES, macSHA1, nil},
    {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheRSAKA, suiteECDHE, cipherAES, macSHA1, nil},
    {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherAES, macSHA1, nil},
    {TLS_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, rsaKA, suiteTLS12, nil, nil, aeadAESGCM},
    {TLS_RSA_WITH_AES_256_GCM_SHA384, 32, 0, 4, rsaKA, suiteTLS12 | suiteSHA384, nil, nil, aeadAESGCM},
    {TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, suiteDefaultOff, cipherRC4, macSHA1, nil},
    {TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, 0, cipherAES, macSHA1, nil},
    {TLS_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, rsaKA, 0, cipherAES, macSHA1, nil},
    {TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, suiteECDHE, cipher3DES, macSHA1, nil},
    {TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, 0, cipher3DES, macSHA1, nil},
}

在上面的循环中,会对用户在config中所填写的加密套件进行筛选,首先会把不是上面所列举的9的加密套件去除,然后根据用户在config中使用的最大版本进行筛选套件,筛选条件:如果不使用TSLv1.2,那么将TLS1.2才支持的加密套件移除。然后对ClientHello中的随机数进行填充。随后是一些对Session的填写,当所有的应该填充的数据后,使用writeRecord()发送ClientHello信息。

获取ServerHello信息

在发送完ClientHello信息后使用c.readHandshake() ,获取从服务器过来的ServerHello信息。然后是使用类型强转serverHello, ok := msg.(*serverHelloMsg)判断得到的信息是否是ServerHello类型的数据。如果不是ServerHello 则发送Alert终止这次TLS握手。

然后根据SeverHello中选择的TLS版本和ClientHello中的版本范围进行校验。看服务器发送过来的TLS版本是否在ClientHello指定的范围中。但是如果ServerHelloClientHello两方商量出来的TLS版本小于TLSv1.0,客户端就发送Alert 终止当前握手。换句话说,使用go进行对服务器访问,如果服务器只支持SSL2、SSL3,该访问将无法完成。(虽然这种情况不常见)。

vers, ok := c.config.mutualVersion(serverHello.vers)
    if !ok || vers < VersionTLS10 {
        // TLS 1.0 is the minimum version supported as a client.
        c.sendAlert(alertProtocolVersion)
        return fmt.Errorf("tls: server selected unsupported protocol version %x", serverHello.vers)
    }

在确定了使用哪个协议之后,就要确定使用哪个加密套件了。suite := mutualCipherSuite(hello.cipherSuites, serverHello.cipherSuite)

suite := mutualCipherSuite(hello.cipherSuites, serverHello.cipherSuite)
if suite == nil {
    c.sendAlert(alertHandshakeFailure)
    return errors.New("tls: server chose an unconfigured cipher suite")
}

如果没有合适的加密套件,也会发送Alert()终止这次握手。mutualCipherSuite()就是在ClientHello中去查找是否有ServerHello发送过来的套件:

func mutualCipherSuite(have []uint16, want uint16) *cipherSuite {
    for _, id := range have {
        if id == want {
            for _, suite := range cipherSuites {
                if suite.id == want {
                    return suite
                }
            }
            return nil
        }
    }
    return nil
}

这些验证完成后,生成握手信息。用于客户端的秘钥交换。根据是否商谈握手,需要做不同的查找。

if isResume || len(c.config.Certificates) == 0 {
        hs.finishedHash.discardHandshakeBuffer()
    }

    hs.finishedHash.Write(hs.hello.marshal())
    hs.finishedHash.Write(hs.serverHello.marshal())

    c.buffering = true
    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
        }
    }

    if sessionCache != nil && hs.session != nil && session != hs.session {
        sessionCache.Put(cacheKey, hs.session)
    }

    c.didResume = isResume
    c.handshakeComplete = true
    c.cipherSuite = suite.id
    return nil
}

如果不是商谈握手。进行区别对待。应为如果是商谈握手,那么之前已经完成了一次完整的握手状态,因此不需要重新做完成的握手,否则需要完成完整的握手即:doFullHandshake()

doFullHandshake

CertificateVerify

doFullHandshake中完成了ClientKeyExchangeCertificateVerifyChangeCipherSpec等操作。

在源码中,如果没有拿到证书信息吗,也会Alert()终止这次握手。并且,如果是第一次握手,将去对证书进行验证的有效性进行验证:

if c.handshakes == 0 {
        // If this is the first handshake on a connection, process and
        // (optionally) verify the server's certificates.
        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
            }
        }

        switch certs[0].PublicKey.(type) {
        case *rsa.PublicKey, *ecdsa.PublicKey:
            break
        default:
            c.sendAlert(alertUnsupportedCertificate)
            return fmt.Errorf("tls: server's certificate contains an unsupported type of public key: %T", certs[0].PublicKey)
        }

        c.peerCertificates = certs
    } else {
        // This is a renegotiation handshake. We require that the
        // server's identity (i.e. leaf certificate) is unchanged and
        // thus any previous trust decision is still valid.
        //
        // See https://mitls.org/pages/attacks/3SHAKE for the
        // motivation behind this requirement.
        if !bytes.Equal(c.peerCertificates[0].Raw, certMsg.certificates[0]) {
            c.sendAlert(alertBadCertificate)
            return errors.New("tls: server's identity changed during renegotiation")
        }
    }

先看不是第一次握手,根据注释说明,只要验证服务器的叶子证书没有改变就可以了。如果是第一次握手,拿到证书并且能够生成go中的证书结构。(但是,有些证书是无法解析成go中的证书结构,但使用Openssl可以展示出证书结构)。如证书无法解析同样的发送Alert终止握手。如果用户在生成tls/Config对象时没有将InsecureSkipVerify设置为true时,将使用在Config中设置的RootCAs,并且把从服务器传过来的非叶子证书,添加到中间证书的池中,使用设置的根证书和中间证书对叶子证书进行验证。如果没有通过验证也发送Alert终止握手。当验证通过后,获取证书的公钥算法,go只能解析RSAECDSA类型的公钥证书。

OCSPStapling

如果服务器提供ocspStapling信息,在doFullHandshake 中也将对ocspstaping信息验证。如没有获取到OCSP信息那么也会发送Alert终止握手。

ServerKeyExchangeMsg

获取服务器端的秘钥交换信息:

skx, ok := msg.(*serverKeyExchangeMsg)
    if ok {
        hs.finishedHash.Write(skx.marshal())
        err = keyAgreement.processServerKeyExchange(c.config, hs.hello, hs.serverHello, c.peerCertificates[0], skx)
        if err != nil {
            c.sendAlert(alertUnexpectedMessage)
            return err
        }

        msg, err = c.readHandshake()
        if err != nil {
            return err
        }
    }

在该阶段只是简单处理ServerKeyExchangeMsg,对双方发送的数据进行验证。如果验证不过就发送Alert终止握手。

CertificateRequestMsg

这种请求在我们进行平常的网页浏览的时候是不会出现的,但是在进行一些金融交易的时候,有些人需要使用银行随卡一起发放的U盾,在U盾上进行确认,其实在U盾中有一张个人的数字证书,银行服务器需要校验这张证书上的内容,以便完成交易。

ServerHelloDone

当接收到ServerHelloDone时表示握手协商已经完成,以后的数据将全部进行加密处理。

结语

整个Client端的半握手,基本就是这样了,但是,在上面的讲解过程中,对发送完ClientHello后,Client发送的ClientKeyExchagne的数据结构构成,不是很清除,因此有很多的hs.finishedHash.Write(shd.marshal())这类的写数据操作的作用没有将讲清楚。虽然现在对TLS握手的过程有了一定的了解,但是还是要对TLS中发送的每一个数据包的组成需要进行了解。