From 7e1760cc7cf5cd282790f4ccf137d61fc400106c Mon Sep 17 00:00:00 2001 From: Watson Ladd Date: Thu, 18 May 2017 16:33:09 -0700 Subject: [PATCH] Add EMS support to tls-tris see RFC7627 --- _dev/tls_examples/Makefile | 7 +++ _dev/tls_examples/ems_client.go | 75 +++++++++++++++++++++++++++++++++ _dev/tris-testclient/client.go | 1 + alert.go | 1 + common.go | 7 +++ conn.go | 2 + handshake_client.go | 29 ++++++++++--- handshake_messages.go | 35 +++++++++++++-- handshake_messages_test.go | 13 +++++- handshake_server.go | 12 +++++- prf.go | 24 +++++++---- prf_test.go | 3 +- ticket.go | 12 +++++- tls_test.go | 2 + 14 files changed, 201 insertions(+), 22 deletions(-) create mode 100644 _dev/tls_examples/Makefile create mode 100644 _dev/tls_examples/ems_client.go diff --git a/_dev/tls_examples/Makefile b/_dev/tls_examples/Makefile new file mode 100644 index 0000000..6a49d52 --- /dev/null +++ b/_dev/tls_examples/Makefile @@ -0,0 +1,7 @@ +# Used to build examples. Must be compiled from current directory and after "make build-all" + +build: + GOROOT=../GOROOT/linux_amd64 go build ems_client.go + +run: + ./ems_client google.com:443 diff --git a/_dev/tls_examples/ems_client.go b/_dev/tls_examples/ems_client.go new file mode 100644 index 0000000..f99a50d --- /dev/null +++ b/_dev/tls_examples/ems_client.go @@ -0,0 +1,75 @@ +package main + +import ( + "bufio" + "crypto/tls" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" +) + +var tlsVersionToName = map[string]uint16{ + "tls10": tls.VersionTLS10, + "tls11": tls.VersionTLS11, + "tls12": tls.VersionTLS12, + "tls13": tls.VersionTLS13, +} + +// Usage client args host:port +func main() { + var version string + var addr string + var enableEMS bool + var resume bool + var config tls.Config + var cache tls.ClientSessionCache + cache = tls.NewLRUClientSessionCache(0) + flag.StringVar(&version, "version", "tls12", "Version of TLS to use") + flag.BoolVar(&enableEMS, "m", false, "Enable EMS") + flag.BoolVar(&resume, "r", false, "Attempt Resumption") + flag.Parse() + config.MinVersion = tlsVersionToName[version] + config.MaxVersion = tlsVersionToName[version] + config.InsecureSkipVerify = true + config.UseExtendedMasterSecret = !enableEMS + config.ClientSessionCache = cache + var iters int + if resume { + iters = 2 + } else { + iters = 1 + } + addr = flag.Arg(0) + for ; iters > 0; iters-- { + conn, err := tls.Dial("tcp", addr, &config) + if err != nil { + fmt.Println("Error %s", err) + os.Exit(1) + } + var req http.Request + var response *http.Response + req.Method = "GET" + req.URL, err = url.Parse("https://" + addr + "/") + if err != nil { + fmt.Println("Failed to parse url") + os.Exit(1) + } + req.Write(conn) + reader := bufio.NewReader(conn) + response, err = http.ReadResponse(reader, nil) + if err != nil { + fmt.Println("HTTP problem") + fmt.Println(err) + os.Exit(1) + } + io.Copy(os.Stdout, response.Body) + conn.Close() + if resume && iters == 2 { + fmt.Println("Attempting resumption") + } + } + os.Exit(0) +} diff --git a/_dev/tris-testclient/client.go b/_dev/tris-testclient/client.go index c1d25dc..e28eef3 100644 --- a/_dev/tris-testclient/client.go +++ b/_dev/tris-testclient/client.go @@ -92,6 +92,7 @@ func result() { } } +// Usage client args host:port func main() { var keylog_file string var enable_rsa, enable_ecdsa, client_auth bool diff --git a/alert.go b/alert.go index e4529b5..e2fe2bd 100644 --- a/alert.go +++ b/alert.go @@ -38,6 +38,7 @@ const ( alertInappropriateFallback alert = 86 alertUserCanceled alert = 90 alertNoRenegotiation alert = 100 + alertUnsupportedExtension alert = 110 alertCertificateRequired alert = 116 alertNoApplicationProtocol alert = 120 alertSuccess alert = 255 // dummy value returned by unmarshal functions diff --git a/common.go b/common.go index ef6f4ac..9b37fa4 100644 --- a/common.go +++ b/common.go @@ -84,6 +84,7 @@ const ( extensionSignatureAlgorithms uint16 = 13 extensionALPN uint16 = 16 extensionSCT uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6 + extensionEMS uint16 = 23 extensionSessionTicket uint16 = 35 extensionPreSharedKey uint16 = 41 extensionEarlyData uint16 = 42 @@ -259,6 +260,7 @@ type ClientSessionState struct { masterSecret []byte // MasterSecret generated by client on a full handshake serverCertificates []*x509.Certificate // Certificate chain presented by the server verifiedChains [][]*x509.Certificate // Certificate chains we built for verification + useEMS bool // State of extended master secret } // ClientSessionCache is a cache of ClientSessionState objects that can be used @@ -641,6 +643,10 @@ type Config struct { // for new tickets and any subsequent keys can be used to decrypt old // tickets. sessionTicketKeys []ticketKey + + // UseExtendedMasterSecret indicates whether or not the connection + // should use the extended master secret computation if available + UseExtendedMasterSecret bool } // ticketKeyNameLen is the number of bytes of identifier that is prepended to @@ -711,6 +717,7 @@ func (c *Config) Clone() *Config { AcceptDelegatedCredential: c.AcceptDelegatedCredential, GetDelegatedCredential: c.GetDelegatedCredential, sessionTicketKeys: sessionTicketKeys, + UseExtendedMasterSecret: c.UseExtendedMasterSecret, } } diff --git a/conn.go b/conn.go index b463412..701bd76 100644 --- a/conn.go +++ b/conn.go @@ -66,6 +66,8 @@ type Conn struct { // renegotiation extension. (This is meaningless as a server because // renegotiation is not supported in that case.) secureRenegotiation bool + // indicates wether extended MasterSecret extension is used (see RFC7627) + useEMS bool // clientFinishedIsFirst is true if the client sent the first Finished // message during the most recent handshake. This is recorded because diff --git a/handshake_client.go b/handshake_client.go index be05734..d8781d9 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -67,6 +67,7 @@ func makeClientHello(config *Config) (*clientHelloMsg, error) { secureRenegotiationSupported: true, delegatedCredential: config.AcceptDelegatedCredential, alpnProtocols: config.NextProtos, + extendedMSSupported: config.UseExtendedMasterSecret, } possibleCipherSuites := config.cipherSuites() hello.cipherSuites = make([]uint16, 0, len(possibleCipherSuites)) @@ -589,6 +590,13 @@ func (hs *clientHandshakeState) doFullHandshake() error { return err } } + c.useEMS = hs.serverHello.extendedMSSupported + hs.masterSecret = masterFromPreMasterSecret(c.vers, hs.suite, preMasterSecret, hs.hello.random, hs.serverHello.random, hs.finishedHash, c.useEMS) + + if err := c.config.writeKeyLog("CLIENT_RANDOM", hs.hello.random, hs.masterSecret); err != nil { + c.sendAlert(alertInternalError) + return errors.New("tls: failed to write to key log: " + err.Error()) + } if chainToSend != nil && len(chainToSend.Certificate) > 0 { certVerify := &certificateVerifyMsg{ @@ -631,12 +639,6 @@ func (hs *clientHandshakeState) doFullHandshake() error { } } - hs.masterSecret = masterFromPreMasterSecret(c.vers, hs.suite, preMasterSecret, hs.hello.random, hs.serverHello.random) - if err := c.config.writeKeyLog("CLIENT_RANDOM", hs.hello.random, hs.masterSecret); err != nil { - c.sendAlert(alertInternalError) - return errors.New("tls: failed to write to key log: " + err.Error()) - } - hs.finishedHash.discardHandshakeBuffer() return nil @@ -697,6 +699,16 @@ func (hs *clientHandshakeState) processServerHello() (bool, error) { } } + if hs.serverHello.extendedMSSupported { + if hs.hello.extendedMSSupported { + c.useEMS = true + } else { + // server wants to calculate master secret in a different way than client + c.sendAlert(alertUnsupportedExtension) + return false, errors.New("tls: unexpected extension (EMS) received in SH") + } + } + clientDidNPN := hs.hello.nextProtoNeg clientDidALPN := len(hs.hello.alpnProtocols) > 0 serverHasNPN := hs.serverHello.nextProtoNeg @@ -727,6 +739,10 @@ func (hs *clientHandshakeState) processServerHello() (bool, error) { return false, nil } + if hs.session.useEMS != c.useEMS { + return false, errors.New("differing EMS state") + } + if hs.session.vers != c.vers { c.sendAlert(alertHandshakeFailure) return false, errors.New("tls: server resumed a session with a different version") @@ -797,6 +813,7 @@ func (hs *clientHandshakeState) readSessionTicket() error { masterSecret: hs.masterSecret, serverCertificates: c.peerCertificates, verifiedChains: c.verifiedChains, + useEMS: c.useEMS, } return nil diff --git a/handshake_messages.go b/handshake_messages.go index 411f7a4..8181f27 100644 --- a/handshake_messages.go +++ b/handshake_messages.go @@ -50,6 +50,7 @@ type clientHelloMsg struct { pskKeyExchangeModes []uint8 earlyData bool delegatedCredential bool + extendedMSSupported bool // RFC7627 } // Function used for signature_algorithms and signature_algorithrms_cert @@ -129,7 +130,8 @@ func (m *clientHelloMsg) equal(i interface{}) bool { eqKeyShares(m.keyShares, m1.keyShares) && eqUint16s(m.supportedVersions, m1.supportedVersions) && m.earlyData == m1.earlyData && - m.delegatedCredential == m1.delegatedCredential + m.delegatedCredential == m1.delegatedCredential && + m.extendedMSSupported == m1.extendedMSSupported } func (m *clientHelloMsg) marshal() []byte { @@ -141,7 +143,6 @@ func (m *clientHelloMsg) marshal() []byte { numExtensions := 0 extensionsLength := 0 - // Indicates wether to send signature_algorithms_cert extension if m.nextProtoNeg { numExtensions++ } @@ -208,6 +209,9 @@ func (m *clientHelloMsg) marshal() []byte { if m.delegatedCredential { numExtensions++ } + if m.extendedMSSupported { + numExtensions++ + } if numExtensions > 0 { extensionsLength += 4 * numExtensions length += 2 + extensionsLength @@ -429,6 +433,10 @@ func (m *clientHelloMsg) marshal() []byte { binary.BigEndian.PutUint16(z, extensionDelegatedCredential) z = z[4:] } + if m.extendedMSSupported { + binary.BigEndian.PutUint16(z, extensionEMS) + z = z[4:] + } m.raw = x @@ -494,6 +502,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) alert { m.pskKeyExchangeModes = nil m.earlyData = false m.delegatedCredential = false + m.extendedMSSupported = false if len(data) == 0 { // ClientHello is optionally followed by extension data @@ -761,6 +770,12 @@ func (m *clientHelloMsg) unmarshal(data []byte) alert { case extensionDelegatedCredential: // https://tools.ietf.org/html/draft-ietf-tls-subcerts-02 m.delegatedCredential = true + case extensionEMS: + // RFC 7627 + m.extendedMSSupported = true + if length != 0 { + return alertDecodeError + } } data = data[length:] bindersOffset += length @@ -793,6 +808,9 @@ type serverHelloMsg struct { keyShare keyShare psk bool pskIdentity uint16 + + // RFC7627 + extendedMSSupported bool } func (m *serverHelloMsg) equal(i interface{}) bool { @@ -826,7 +844,8 @@ func (m *serverHelloMsg) equal(i interface{}) bool { m.keyShare.group == m1.keyShare.group && bytes.Equal(m.keyShare.data, m1.keyShare.data) && m.psk == m1.psk && - m.pskIdentity == m1.pskIdentity + m.pskIdentity == m1.pskIdentity && + m.extendedMSSupported == m1.extendedMSSupported } func (m *serverHelloMsg) marshal() []byte { @@ -857,6 +876,9 @@ func (m *serverHelloMsg) marshal() []byte { extensionsLength += 1 + len(m.secureRenegotiation) numExtensions++ } + if m.extendedMSSupported { + numExtensions++ + } if alpnLen := len(m.alpnProtocol); alpnLen > 0 { if alpnLen >= 256 { panic("invalid ALPN protocol") @@ -1017,6 +1039,10 @@ func (m *serverHelloMsg) marshal() []byte { z[5] = byte(m.pskIdentity) z = z[6:] } + if m.extendedMSSupported { + binary.BigEndian.PutUint16(z, extensionEMS) + z = z[4:] + } m.raw = x @@ -1053,6 +1079,7 @@ func (m *serverHelloMsg) unmarshal(data []byte) alert { m.keyShare.data = nil m.psk = false m.pskIdentity = 0 + m.extendedMSSupported = false if len(data) == 0 { // ServerHello is optionally followed by extension data @@ -1194,6 +1221,8 @@ func (m *serverHelloMsg) unmarshal(data []byte) alert { } m.psk = true m.pskIdentity = uint16(data[0])<<8 | uint16(data[1]) + case extensionEMS: + m.extendedMSSupported = true } data = data[length:] } diff --git a/handshake_messages_test.go b/handshake_messages_test.go index 60ae6e7..310af22 100644 --- a/handshake_messages_test.go +++ b/handshake_messages_test.go @@ -171,7 +171,12 @@ func (*clientHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value { if rand.Intn(10) > 5 { m.earlyData = true } - + if rand.Intn(10) > 5 { + m.delegatedCredential = true + } + if rand.Intn(10) > 5 { + m.extendedMSSupported = true + } return reflect.ValueOf(m) } @@ -199,6 +204,9 @@ func (*serverHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value { if rand.Intn(10) > 5 { m.ticketSupported = true } + if rand.Intn(10) > 5 { + m.extendedMSSupported = true + } m.alpnProtocol = randomString(rand.Intn(32)+1, rand) if rand.Intn(10) > 5 { @@ -344,6 +352,9 @@ func (*sessionState) Generate(rand *rand.Rand, size int) reflect.Value { s.vers = uint16(rand.Intn(10000)) s.cipherSuite = uint16(rand.Intn(10000)) s.masterSecret = randomBytes(rand.Intn(100), rand) + if rand.Intn(10) > 5 { + s.usedEMS = true + } numCerts := rand.Intn(20) s.certificates = make([][]byte, numCerts) for i := 0; i < numCerts; i++ { diff --git a/handshake_server.go b/handshake_server.go index 127d8b5..569925b 100644 --- a/handshake_server.go +++ b/handshake_server.go @@ -409,6 +409,11 @@ func (hs *serverHandshakeState) checkForResumption() bool { return false } + // Do not resume connections where client support for EMS has changed + if (hs.clientHello.extendedMSSupported && c.config.UseExtendedMasterSecret) != hs.sessionState.usedEMS { + return false + } + cipherSuiteOk := false // Check that the client is still offering the ciphersuite in the session. for _, id := range hs.clientHello.cipherSuites { @@ -446,6 +451,7 @@ func (hs *serverHandshakeState) doResumeHandshake() error { // that we're doing a resumption. hs.hello.sessionId = hs.clientHello.sessionId hs.hello.ticketSupported = hs.sessionState.usedOldKey + hs.hello.extendedMSSupported = hs.clientHello.extendedMSSupported && c.config.UseExtendedMasterSecret hs.finishedHash = newFinishedHash(c.vers, hs.suite) hs.finishedHash.discardHandshakeBuffer() hs.finishedHash.Write(hs.clientHello.marshal()) @@ -461,6 +467,7 @@ func (hs *serverHandshakeState) doResumeHandshake() error { } hs.masterSecret = hs.sessionState.masterSecret + c.useEMS = hs.sessionState.usedEMS return nil } @@ -474,6 +481,7 @@ func (hs *serverHandshakeState) doFullHandshake() error { hs.hello.ticketSupported = hs.clientHello.ticketSupported && !c.config.SessionTicketsDisabled hs.hello.cipherSuite = hs.suite.id + hs.hello.extendedMSSupported = hs.clientHello.extendedMSSupported && c.config.UseExtendedMasterSecret hs.finishedHash = newFinishedHash(hs.c.vers, hs.suite) if c.config.ClientAuth == NoClientCert { @@ -607,7 +615,8 @@ func (hs *serverHandshakeState) doFullHandshake() error { } return err } - hs.masterSecret = masterFromPreMasterSecret(c.vers, hs.suite, preMasterSecret, hs.clientHello.random, hs.hello.random) + c.useEMS = hs.hello.extendedMSSupported + hs.masterSecret = masterFromPreMasterSecret(c.vers, hs.suite, preMasterSecret, hs.clientHello.random, hs.hello.random, hs.finishedHash, c.useEMS) if err := c.config.writeKeyLog("CLIENT_RANDOM", hs.clientHello.random, hs.masterSecret); err != nil { c.sendAlert(alertInternalError) return err @@ -737,6 +746,7 @@ func (hs *serverHandshakeState) sendSessionTicket() error { cipherSuite: hs.suite.id, masterSecret: hs.masterSecret, certificates: hs.certsFromClient, + usedEMS: c.useEMS, } m.ticket, err = c.encryptTicket(state.marshal()) if err != nil { diff --git a/prf.go b/prf.go index 0751909..00f4035 100644 --- a/prf.go +++ b/prf.go @@ -117,6 +117,7 @@ var masterSecretLabel = []byte("master secret") var keyExpansionLabel = []byte("key expansion") var clientFinishedLabel = []byte("client finished") var serverFinishedLabel = []byte("server finished") +var extendedMasterSecretLabel = []byte("extended master secret") func prfAndHashForVersion(version uint16, suite *cipherSuite) (func(result, secret, label, seed []byte), crypto.Hash) { switch version { @@ -141,14 +142,21 @@ func prfForVersion(version uint16, suite *cipherSuite) func(result, secret, labe // masterFromPreMasterSecret generates the master secret from the pre-master // secret. See http://tools.ietf.org/html/rfc5246#section-8.1 -func masterFromPreMasterSecret(version uint16, suite *cipherSuite, preMasterSecret, clientRandom, serverRandom []byte) []byte { - seed := make([]byte, 0, len(clientRandom)+len(serverRandom)) - seed = append(seed, clientRandom...) - seed = append(seed, serverRandom...) - - masterSecret := make([]byte, masterSecretLength) - prfForVersion(version, suite)(masterSecret, preMasterSecret, masterSecretLabel, seed) - return masterSecret +func masterFromPreMasterSecret(version uint16, suite *cipherSuite, preMasterSecret, clientRandom, serverRandom []byte, fin finishedHash, ems bool) []byte { + if ems { + session_hash := fin.Sum() + masterSecret := make([]byte, masterSecretLength) + prfForVersion(version, suite)(masterSecret, preMasterSecret, extendedMasterSecretLabel, session_hash) + return masterSecret + } else { + seed := make([]byte, 0, len(clientRandom)+len(serverRandom)) + seed = append(seed, clientRandom...) + seed = append(seed, serverRandom...) + + masterSecret := make([]byte, masterSecretLength) + prfForVersion(version, suite)(masterSecret, preMasterSecret, masterSecretLabel, seed) + return masterSecret + } } // keysFromMasterSecret generates the connection keys from the master diff --git a/prf_test.go b/prf_test.go index 0a1b1bc..5943e3d 100644 --- a/prf_test.go +++ b/prf_test.go @@ -49,8 +49,9 @@ func TestKeysFromPreMasterSecret(t *testing.T) { in, _ := hex.DecodeString(test.preMasterSecret) clientRandom, _ := hex.DecodeString(test.clientRandom) serverRandom, _ := hex.DecodeString(test.serverRandom) + fin := newFinishedHash(test.version, test.suite) - masterSecret := masterFromPreMasterSecret(test.version, test.suite, in, clientRandom, serverRandom) + masterSecret := masterFromPreMasterSecret(test.version, test.suite, in, clientRandom, serverRandom, fin, false) if s := hex.EncodeToString(masterSecret); s != test.masterSecret { t.Errorf("#%d: bad master secret %s, want %s", i, s, test.masterSecret) continue diff --git a/ticket.go b/ticket.go index 32b40d8..0f67c19 100644 --- a/ticket.go +++ b/ticket.go @@ -37,6 +37,7 @@ type SessionTicketSealer interface { type sessionState struct { vers uint16 cipherSuite uint16 + usedEMS bool masterSecret []byte certificates [][]byte // usedOldKey is true if the ticket from which this session came from @@ -51,6 +52,7 @@ func (s *sessionState) equal(i interface{}) bool { } if s.vers != s1.vers || + s.usedEMS != s1.usedEMS || s.cipherSuite != s1.cipherSuite || !bytes.Equal(s.masterSecret, s1.masterSecret) { return false @@ -77,7 +79,12 @@ func (s *sessionState) marshal() []byte { ret := make([]byte, length) x := ret - x[0] = byte(s.vers >> 8) + was_used := byte(0) + if s.usedEMS { + was_used = byte(0x80) + } + + x[0] = byte(s.vers>>8) | byte(was_used) x[1] = byte(s.vers) x[2] = byte(s.cipherSuite >> 8) x[3] = byte(s.cipherSuite) @@ -108,8 +115,9 @@ func (s *sessionState) unmarshal(data []byte) alert { return alertDecodeError } - s.vers = uint16(data[0])<<8 | uint16(data[1]) + s.vers = (uint16(data[0])<<8 | uint16(data[1])) & 0x7fff s.cipherSuite = uint16(data[2])<<8 | uint16(data[3]) + s.usedEMS = (data[0] & 0x80) == 0x80 masterSecretLen := int(data[4])<<8 | int(data[5]) data = data[6:] if len(data) < masterSecretLen { diff --git a/tls_test.go b/tls_test.go index 119b386..1b6a445 100644 --- a/tls_test.go +++ b/tls_test.go @@ -715,6 +715,8 @@ func TestCloneNonFuncFields(t *testing.T) { // TODO case "AcceptDelegatedCredential": f.Set(reflect.ValueOf(false)) + case "UseExtendedMasterSecret": + f.Set(reflect.ValueOf(false)) default: t.Errorf("all fields must be accounted for, but saw unknown field %q", fn) }