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中发送的每一个数据包的组成需要进行了解。