golang與TLS實現
在最近的項目中,需要對對方服務器的證書狀態進行檢查,獲取證書上,就需要進行TLS握手,獲取到證書信息,在項目中但是使用直接拼出ClientHello包的方式進行TLS握手操作,今天看一些go中的源碼中是如何進行TLS握手的。
首先從建立連接開始:tls.DialWithDialer(dialer *net.Dialer,network,addr string ,config *tls.Config)
,該方法在cryto/tls
的tls.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++
}
現在階段是Client
向Server
發送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
,在上面的代碼中使用到了cipherSuites
,cipherSuites
是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
指定的範圍中。但是如果ServerHello
和ClientHello
兩方商量出來的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
中完成了ClientKeyExchange
、CertificateVerify
、ChangeCipherSpec
等操作。
在源碼中,如果沒有拿到證書信息嗎,也會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
只能解析RSA
和ECDSA
類型的公鑰證書。
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中發送的每一個數據包的組成需要進行了解。