diff --git a/.travis.yml b/.travis.yml index eac8dfa..37c25d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,21 +8,22 @@ go: - 1.7 env: - - MODE=gotest - MODE=interop CLIENT=boring - - MODE=interop CLIENT=boring REVISION=origin/master - MODE=interop CLIENT=bogo - - MODE=interop CLIENT=bogo REVISION=origin/master - MODE=interop CLIENT=tstclnt - - MODE=interop CLIENT=tstclnt REVISION=default - MODE=interop CLIENT=mint + - MODE=gotest + - MODE=interop CLIENT=boring REVISION=origin/master + - MODE=interop CLIENT=bogo REVISION=origin/master + - MODE=interop CLIENT=tstclnt REVISION=default matrix: fast_finish: true allow_failures: - - env: MODE=interop CLIENT=bogo REVISION=origin/master - env: MODE=interop CLIENT=boring REVISION=origin/master + - env: MODE=interop CLIENT=bogo REVISION=origin/master - env: MODE=interop CLIENT=tstclnt REVISION=default + - env: MODE=interop CLIENT=mint # broken resumption client install: - if [ "$MODE" = "interop" ]; then ./_dev/tris-localserver/start.sh -d && docker ps -a; fi diff --git a/13.go b/13.go index 9183787..12fb96a 100644 --- a/13.go +++ b/13.go @@ -14,6 +14,7 @@ import ( "io" "os" "runtime/debug" + "time" "golang_org/x/crypto/curve25519" ) @@ -55,6 +56,12 @@ func (hs *serverHandshakeState) doTLS13Handshake() error { } hashSize := hash.Size() + earlySecret, isPSK := hs.checkPSK(hash) + if !isPSK { + earlySecret = hkdfExtract(hash, nil, nil) + } + c.didResume = isPSK + ecdheSecret := deriveECDHESecret(ks, privateKey) if ecdheSecret == nil { c.sendAlert(alertIllegalParameter) @@ -69,9 +76,7 @@ func (hs *serverHandshakeState) doTLS13Handshake() error { return err } - earlySecret := hkdfExtract(hash, nil, nil) handshakeSecret := hkdfExtract(hash, ecdheSecret, earlySecret) - handshakeCtx := hs.finishedHash.Sum() cHandshakeTS := hkdfExpandLabel(hash, handshakeSecret, handshakeCtx, "client handshake traffic secret", hashSize) @@ -91,6 +96,76 @@ func (hs *serverHandshakeState) doTLS13Handshake() error { return err } + if !isPSK { + if err := hs.sendCertificate13(); err != nil { + return err + } + } + + serverFinishedKey := hkdfExpandLabel(hash, sHandshakeTS, nil, "finished", hashSize) + clientFinishedKey := hkdfExpandLabel(hash, cHandshakeTS, nil, "finished", hashSize) + + h := hmac.New(hash.New, serverFinishedKey) + h.Write(hs.finishedHash.Sum()) + verifyData := h.Sum(nil) + serverFinished := &finishedMsg{ + verifyData: verifyData, + } + hs.finishedHash.Write(serverFinished.marshal()) + if _, err := c.writeRecord(recordTypeHandshake, serverFinished.marshal()); err != nil { + return err + } + + hs.masterSecret = hkdfExtract(hash, nil, handshakeSecret) + handshakeCtx = hs.finishedHash.Sum() + + cTrafficSecret0 := hkdfExpandLabel(hash, hs.masterSecret, handshakeCtx, "client application traffic secret", hashSize) + cKey = hkdfExpandLabel(hash, cTrafficSecret0, nil, "key", hs.suite.keyLen) + cIV = hkdfExpandLabel(hash, cTrafficSecret0, nil, "iv", 12) + sTrafficSecret0 := hkdfExpandLabel(hash, hs.masterSecret, handshakeCtx, "server application traffic secret", hashSize) + sKey = hkdfExpandLabel(hash, sTrafficSecret0, nil, "key", hs.suite.keyLen) + sIV = hkdfExpandLabel(hash, sTrafficSecret0, nil, "iv", 12) + + serverCipher = hs.suite.aead(sKey, sIV) + c.out.setCipher(c.vers, serverCipher) + + // TODO(filippo): here we are ready to send Application Data, but we might want it opt-in. + // It will be refactored anyway for 0-RTT. + + if _, err := c.flush(); err != nil { + return err + } + + msg, err := c.readHandshake() + if err != nil { + return err + } + + clientFinished, ok := msg.(*finishedMsg) + if !ok { + c.sendAlert(alertUnexpectedMessage) + return unexpectedMessageError(clientFinished, msg) + } + + h = hmac.New(hash.New, clientFinishedKey) + h.Write(hs.finishedHash.Sum()) + expectedVerifyData := h.Sum(nil) + if len(expectedVerifyData) != len(clientFinished.verifyData) || + subtle.ConstantTimeCompare(expectedVerifyData, clientFinished.verifyData) != 1 { + c.sendAlert(alertHandshakeFailure) + return errors.New("tls: client's Finished message is incorrect") + } + hs.finishedHash.Write(clientFinished.marshal()) + + clientCipher = hs.suite.aead(cKey, cIV) + c.in.setCipher(c.vers, clientCipher) + + return hs.sendSessionTicket13(hash) +} + +func (hs *serverHandshakeState) sendCertificate13() error { + c := hs.c + certMsg := &certificateMsg13{ certificates: hs.cert.Certificate, } @@ -112,7 +187,7 @@ func (hs *serverHandshakeState) doTLS13Handshake() error { } toSign := prepareDigitallySigned(sigHash, "TLS 1.3, server CertificateVerify", hs.finishedHash.Sum()) - signature, err := hs.cert.PrivateKey.(crypto.Signer).Sign(config.rand(), toSign[:], opts) + signature, err := hs.cert.PrivateKey.(crypto.Signer).Sign(c.config.rand(), toSign[:], opts) if err != nil { c.sendAlert(alertInternalError) return err @@ -128,58 +203,6 @@ func (hs *serverHandshakeState) doTLS13Handshake() error { return err } - serverFinishedKey := hkdfExpandLabel(hash, sHandshakeTS, nil, "finished", hashSize) - clientFinishedKey := hkdfExpandLabel(hash, cHandshakeTS, nil, "finished", hashSize) - - h := hmac.New(hash.New, serverFinishedKey) - h.Write(hs.finishedHash.Sum()) - verifyData := h.Sum(nil) - serverFinished := &finishedMsg{ - verifyData: verifyData, - } - hs.finishedHash.Write(serverFinished.marshal()) - if _, err := c.writeRecord(recordTypeHandshake, serverFinished.marshal()); err != nil { - return err - } - - if _, err := c.flush(); err != nil { - return err - } - - msg, err := c.readHandshake() - if err != nil { - return err - } - - clientFinished, ok := msg.(*finishedMsg) - if !ok { - c.sendAlert(alertUnexpectedMessage) - return unexpectedMessageError(clientFinished, msg) - } - h = hmac.New(hash.New, clientFinishedKey) - h.Write(hs.finishedHash.Sum()) - expectedVerifyData := h.Sum(nil) - if len(expectedVerifyData) != len(clientFinished.verifyData) || - subtle.ConstantTimeCompare(expectedVerifyData, clientFinished.verifyData) != 1 { - c.sendAlert(alertHandshakeFailure) - return errors.New("tls: client's Finished message is incorrect") - } - - masterSecret := hkdfExtract(hash, nil, handshakeSecret) - handshakeCtx = hs.finishedHash.Sum() - - cTrafficSecret0 := hkdfExpandLabel(hash, masterSecret, handshakeCtx, "client application traffic secret", hashSize) - cKey = hkdfExpandLabel(hash, cTrafficSecret0, nil, "key", hs.suite.keyLen) - cIV = hkdfExpandLabel(hash, cTrafficSecret0, nil, "iv", 12) - sTrafficSecret0 := hkdfExpandLabel(hash, masterSecret, handshakeCtx, "server application traffic secret", hashSize) - sKey = hkdfExpandLabel(hash, sTrafficSecret0, nil, "key", hs.suite.keyLen) - sIV = hkdfExpandLabel(hash, sTrafficSecret0, nil, "iv", 12) - - clientCipher = hs.suite.aead(cKey, cIV) - c.in.setCipher(c.vers, clientCipher) - serverCipher = hs.suite.aead(sKey, sIV) - c.out.setCipher(c.vers, serverCipher) - return nil } @@ -338,6 +361,121 @@ func hkdfExpandLabel(hash crypto.Hash, secret, hashValue []byte, label string, L return hkdfExpand(hash, secret, hkdfLabel, L) } +// Maximum allowed mismatch between the stated age of a ticket +// and the server-observed one. See +// https://tools.ietf.org/html/draft-ietf-tls-tls13-18#section-4.2.8.2. +const ticketAgeSkewAllowance = 10 * time.Second + +func (hs *serverHandshakeState) checkPSK(hash crypto.Hash) (earlySecret []byte, ok bool) { + if hs.c.config.SessionTicketsDisabled { + return nil, false + } + + foundDHE := false + for _, mode := range hs.clientHello.pskKeyExchangeModes { + if mode == pskDHEKeyExchange { + foundDHE = true + break + } + } + if !foundDHE { + return nil, false + } + + hashSize := hash.Size() + for i := range hs.clientHello.psks { + sessionTicket := append([]uint8{}, hs.clientHello.psks[i].identity...) + serializedTicket, _ := hs.c.decryptTicket(sessionTicket) + if serializedTicket == nil { + continue + } + s := &sessionState13{} + if ok := s.unmarshal(serializedTicket); !ok { + continue + } + if s.vers != hs.c.vers { + continue + } + clientAge := time.Duration(hs.clientHello.psks[i].obfTicketAge-s.ageAdd) * time.Millisecond + serverAge := time.Since(time.Unix(int64(s.createdAt), 0)) + if clientAge-serverAge > ticketAgeSkewAllowance || clientAge-serverAge < -ticketAgeSkewAllowance { + continue + } + if s.hash != uint16(hash) { + continue + } + + earlySecret := hkdfExtract(hash, s.resumptionSecret, nil) + handshakeCtx := hash.New().Sum(nil) + binderKey := hkdfExpandLabel(hash, earlySecret, handshakeCtx, "resumption psk binder key", hashSize) + binderFinishedKey := hkdfExpandLabel(hash, binderKey, nil, "finished", hashSize) + chHash := hash.New() + chHash.Write(hs.clientHello.rawTruncated) + h := hmac.New(hash.New, binderFinishedKey) + h.Write(chHash.Sum(nil)) + expectedBinder := h.Sum(nil) + + if subtle.ConstantTimeCompare(expectedBinder, hs.clientHello.psks[i].binder) == 1 { + hs.hello13.psk = true + hs.hello13.pskIdentity = uint16(i) + return earlySecret, true + } + } + + return nil, false +} + +func (hs *serverHandshakeState) sendSessionTicket13(hash crypto.Hash) error { + c := hs.c + if c.config.SessionTicketsDisabled { + return nil + } + + foundDHE := false + for _, mode := range hs.clientHello.pskKeyExchangeModes { + if mode == pskDHEKeyExchange { + foundDHE = true + break + } + } + if !foundDHE { + return nil + } + + handshakeCtx := hs.finishedHash.Sum() + resumptionSecret := hkdfExpandLabel(hash, hs.masterSecret, handshakeCtx, "resumption master secret", hash.Size()) + + ageAddBuf := make([]byte, 4) + if _, err := io.ReadFull(c.config.rand(), ageAddBuf); err != nil { + c.sendAlert(alertInternalError) + return err + } + sessionState := &sessionState13{ + vers: c.vers, + hash: uint16(hash), + ageAdd: uint32(ageAddBuf[0])<<24 | uint32(ageAddBuf[1])<<16 | + uint32(ageAddBuf[2])<<8 | uint32(ageAddBuf[3]), + createdAt: uint64(time.Now().Unix()), + resumptionSecret: resumptionSecret, + } + + ticket, err := c.encryptTicket(sessionState.marshal()) + if err != nil { + c.sendAlert(alertInternalError) + return err + } + ticketMsg := &newSessionTicketMsg13{ + lifetime: 21600, // TODO(filippo) + ageAdd: sessionState.ageAdd, + ticket: ticket, + } + if _, err := c.writeRecord(recordTypeHandshake, ticketMsg.marshal()); err != nil { + return err + } + + return nil +} + // QuietError is an error wrapper that prevents the verbose handshake log // dump on errors. Exposed for use by GetCertificate. type QuietError struct { diff --git a/_dev/bogo/bogo-client.go b/_dev/bogo/bogo-client.go index 6707303..619e9f0 100644 --- a/_dev/bogo/bogo-client.go +++ b/_dev/bogo/bogo-client.go @@ -10,14 +10,18 @@ import ( ) func main() { + sessionCache := runner.NewLRUClientSessionCache(10) tr := &http.Transport{ DialTLS: func(network, addr string) (net.Conn, error) { return runner.Dial(network, addr, &runner.Config{ InsecureSkipVerify: true, + ClientSessionCache: sessionCache, }) }, + DisableKeepAlives: true, } client := &http.Client{Transport: tr} + resp, err := client.Get("https://" + os.Args[1]) if err != nil { log.Fatal(err) @@ -25,4 +29,13 @@ func main() { if err := resp.Write(os.Stdout); err != nil { log.Fatal(err) } + + // Resumption + resp, err = client.Get("https://" + os.Args[1]) + if err != nil { + log.Fatal(err) + } + if err := resp.Write(os.Stdout); err != nil { + log.Fatal(err) + } } diff --git a/_dev/boring/run.sh b/_dev/boring/run.sh index 7da9c43..e99e58f 100755 --- a/_dev/boring/run.sh +++ b/_dev/boring/run.sh @@ -1,3 +1,8 @@ #! /bin/sh +set -e + +/boringssl/build/tool/bssl client -grease -min-version tls1.3 -max-version tls1.3 \ + -session-out /session -connect "$@" < /httpreq.txt +exec /boringssl/build/tool/bssl client -grease -min-version tls1.3 -max-version tls1.3 \ + -session-in /session -connect "$@" < /httpreq.txt -exec /boringssl/build/tool/bssl s_client -min-version tls1.3 -max-version tls1.3 -connect "$@" < /httpreq.txt diff --git a/_dev/interop.sh b/_dev/interop.sh index 4f9ac30..1d45ab3 100755 --- a/_dev/interop.sh +++ b/_dev/interop.sh @@ -11,7 +11,8 @@ if [ "$1" = "INSTALL" ]; then elif [ "$1" = "RUN" ]; then IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' tris-localserver) - docker run -i --rm tls-tris:$2 $IP:$3 | tee output.txt - grep "Hello TLS 1.3" output.txt + docker run --rm tls-tris:$2 $IP:$3 | tee output.txt + grep "Hello TLS 1.3" output.txt | grep -v "resumed" + grep "Hello TLS 1.3" output.txt | grep "resumed" fi diff --git a/_dev/mint/mint-client.go b/_dev/mint/mint-client.go index d344e3e..b86475a 100644 --- a/_dev/mint/mint-client.go +++ b/_dev/mint/mint-client.go @@ -14,8 +14,10 @@ func main() { DialTLS: func(network, addr string) (net.Conn, error) { return mint.Dial(network, addr, nil) }, + DisableKeepAlives: true, } client := &http.Client{Transport: tr} + resp, err := client.Get("https://" + os.Args[1]) if err != nil { log.Fatal(err) @@ -23,4 +25,13 @@ func main() { if err := resp.Write(os.Stdout); err != nil { log.Fatal(err) } + + // Resumption + resp, err = client.Get("https://" + os.Args[1]) + if err != nil { + log.Fatal(err) + } + if err := resp.Write(os.Stdout); err != nil { + log.Fatal(err) + } } diff --git a/_dev/tris-localserver/Dockerfile b/_dev/tris-localserver/Dockerfile index fd5ef67..4aba119 100644 --- a/_dev/tris-localserver/Dockerfile +++ b/_dev/tris-localserver/Dockerfile @@ -1,10 +1,11 @@ FROM scratch -# GOOS=linux ../go.sh build -v -i . -ADD tris-localserver ./ - ENV TLSDEBUG error EXPOSE 443 EXPOSE 4443 + +# GOOS=linux ../go.sh build -v -i . +ADD tris-localserver ./ + CMD [ "./tris-localserver", "0.0.0.0:443", "0.0.0.0:4443" ] diff --git a/_dev/tris-localserver/server.go b/_dev/tris-localserver/server.go index 6c3fe2a..89aadd3 100644 --- a/_dev/tris-localserver/server.go +++ b/_dev/tris-localserver/server.go @@ -19,7 +19,11 @@ var tlsVersionToName = map[uint16]string{ func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "

Hello TLS %s _o/\n", tlsVersionToName[r.TLS.Version]) + resumed := "" + if r.TLS.DidResume { + resumed = " [resumed]" + } + fmt.Fprintf(w, "

Hello TLS %s%s _o/\n", tlsVersionToName[r.TLS.Version], resumed) }) http.HandleFunc("/ch", func(w http.ResponseWriter, r *http.Request) { diff --git a/_dev/tstclnt/Dockerfile b/_dev/tstclnt/Dockerfile index 530be04..164455e 100644 --- a/_dev/tstclnt/Dockerfile +++ b/_dev/tstclnt/Dockerfile @@ -15,7 +15,10 @@ ENV USE_64=1 NSS_ENABLE_TLS_1_3=1 # ARG REVISION=3e7b53b18112 # Draft 18 -ARG REVISION=b6dfef6d0ff0 +# ARG REVISION=b6dfef6d0ff0 + +# tstclnt resumption +ARG REVISION=460a0a1e009f RUN cd nss && hg pull RUN cd nss && hg checkout -C $REVISION diff --git a/_dev/tstclnt/run.sh b/_dev/tstclnt/run.sh index 8ea9600..352bc9b 100755 --- a/_dev/tstclnt/run.sh +++ b/_dev/tstclnt/run.sh @@ -5,4 +5,4 @@ shift HOST="${ADDR[0]}" PORT="${ADDR[1]}" -exec /dist/OBJ-PATH/bin/tstclnt -D -V tls1.3:tls1.3 -o -O -h $HOST -p $PORT -v "$@" < /httpreq.txt +exec /dist/OBJ-PATH/bin/tstclnt -D -V tls1.3:tls1.3 -o -O -h $HOST -p $PORT -v -A /httpreq.txt -L 2 "$@" diff --git a/handshake_server.go b/handshake_server.go index 9ce3075..8388076 100644 --- a/handshake_server.go +++ b/handshake_server.go @@ -278,7 +278,7 @@ Curves: hs.hello.scts = hs.cert.SignedCertificateTimestamps } - if committer, ok := c.conn.(Committer); ok { + if committer, ok := c.conn.(Committer); ok { // TODO: probably committing too early err = committer.Commit() if err != nil { return false, err @@ -353,9 +353,10 @@ func (hs *serverHandshakeState) checkForResumption() bool { return false } - var ok bool - var sessionTicket = append([]uint8{}, hs.clientHello.sessionTicket...) - if hs.sessionState, ok = c.decryptTicket(sessionTicket); !ok { + sessionTicket := append([]uint8{}, hs.clientHello.sessionTicket...) + serializedState, usedOldKey := c.decryptTicket(sessionTicket) + hs.sessionState = &sessionState{usedOldKey: usedOldKey} + if ok := hs.sessionState.unmarshal(serializedState); !ok { return false } @@ -730,7 +731,7 @@ func (hs *serverHandshakeState) sendSessionTicket() error { masterSecret: hs.masterSecret, certificates: hs.certsFromClient, } - m.ticket, err = c.encryptTicket(&state) + m.ticket, err = c.encryptTicket(state.marshal()) if err != nil { return err } diff --git a/ticket.go b/ticket.go index c0d79d7..1215f41 100644 --- a/ticket.go +++ b/ticket.go @@ -193,8 +193,7 @@ func (s *sessionState13) unmarshal(data []byte) bool { return int(l) == len(s.resumptionSecret) } -func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) { - serialized := state.marshal() +func (c *Conn) encryptTicket(serialized []byte) ([]byte, error) { encrypted := make([]byte, ticketKeyNameLen+aes.BlockSize+len(serialized)+sha256.Size) keyName := encrypted[:ticketKeyNameLen] iv := encrypted[ticketKeyNameLen : ticketKeyNameLen+aes.BlockSize] @@ -218,7 +217,7 @@ func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) { return encrypted, nil } -func (c *Conn) decryptTicket(encrypted []byte) (*sessionState, bool) { +func (c *Conn) decryptTicket(encrypted []byte) (serialized []byte, usedOldKey bool) { if c.config.SessionTicketsDisabled || len(encrypted) < ticketKeyNameLen+aes.BlockSize+sha256.Size { return nil, false @@ -258,7 +257,5 @@ func (c *Conn) decryptTicket(encrypted []byte) (*sessionState, bool) { plaintext := ciphertext cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext) - state := &sessionState{usedOldKey: keyIndex > 0} - ok := state.unmarshal(plaintext) - return state, ok + return plaintext, keyIndex > 0 }