golang與TLS實現

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中發送的每一個數據包的組成需要進行了解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章