crypto/tls: implement TLS 1.3 server PSK

This commit is contained in:
Filippo Valsorda 2016-11-21 17:25:27 -05:00 committed by Peter Wu
parent 453bd6af77
commit ee3048cfd2
12 changed files with 255 additions and 80 deletions

View File

@ -8,21 +8,22 @@ go:
- 1.7 - 1.7
env: env:
- MODE=gotest
- MODE=interop CLIENT=boring - MODE=interop CLIENT=boring
- MODE=interop CLIENT=boring REVISION=origin/master
- MODE=interop CLIENT=bogo - MODE=interop CLIENT=bogo
- MODE=interop CLIENT=bogo REVISION=origin/master
- MODE=interop CLIENT=tstclnt - MODE=interop CLIENT=tstclnt
- MODE=interop CLIENT=tstclnt REVISION=default
- MODE=interop CLIENT=mint - 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: matrix:
fast_finish: true fast_finish: true
allow_failures: allow_failures:
- env: MODE=interop CLIENT=bogo REVISION=origin/master
- env: MODE=interop CLIENT=boring 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=tstclnt REVISION=default
- env: MODE=interop CLIENT=mint # broken resumption client
install: install:
- if [ "$MODE" = "interop" ]; then ./_dev/tris-localserver/start.sh -d && docker ps -a; fi - if [ "$MODE" = "interop" ]; then ./_dev/tris-localserver/start.sh -d && docker ps -a; fi

248
13.go
View File

@ -14,6 +14,7 @@ import (
"io" "io"
"os" "os"
"runtime/debug" "runtime/debug"
"time"
"golang_org/x/crypto/curve25519" "golang_org/x/crypto/curve25519"
) )
@ -55,6 +56,12 @@ func (hs *serverHandshakeState) doTLS13Handshake() error {
} }
hashSize := hash.Size() hashSize := hash.Size()
earlySecret, isPSK := hs.checkPSK(hash)
if !isPSK {
earlySecret = hkdfExtract(hash, nil, nil)
}
c.didResume = isPSK
ecdheSecret := deriveECDHESecret(ks, privateKey) ecdheSecret := deriveECDHESecret(ks, privateKey)
if ecdheSecret == nil { if ecdheSecret == nil {
c.sendAlert(alertIllegalParameter) c.sendAlert(alertIllegalParameter)
@ -69,9 +76,7 @@ func (hs *serverHandshakeState) doTLS13Handshake() error {
return err return err
} }
earlySecret := hkdfExtract(hash, nil, nil)
handshakeSecret := hkdfExtract(hash, ecdheSecret, earlySecret) handshakeSecret := hkdfExtract(hash, ecdheSecret, earlySecret)
handshakeCtx := hs.finishedHash.Sum() handshakeCtx := hs.finishedHash.Sum()
cHandshakeTS := hkdfExpandLabel(hash, handshakeSecret, handshakeCtx, "client handshake traffic secret", hashSize) cHandshakeTS := hkdfExpandLabel(hash, handshakeSecret, handshakeCtx, "client handshake traffic secret", hashSize)
@ -91,6 +96,76 @@ func (hs *serverHandshakeState) doTLS13Handshake() error {
return err 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{ certMsg := &certificateMsg13{
certificates: hs.cert.Certificate, certificates: hs.cert.Certificate,
} }
@ -112,7 +187,7 @@ func (hs *serverHandshakeState) doTLS13Handshake() error {
} }
toSign := prepareDigitallySigned(sigHash, "TLS 1.3, server CertificateVerify", hs.finishedHash.Sum()) 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 { if err != nil {
c.sendAlert(alertInternalError) c.sendAlert(alertInternalError)
return err return err
@ -128,58 +203,6 @@ func (hs *serverHandshakeState) doTLS13Handshake() error {
return err 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 return nil
} }
@ -338,6 +361,121 @@ func hkdfExpandLabel(hash crypto.Hash, secret, hashValue []byte, label string, L
return hkdfExpand(hash, secret, hkdfLabel, 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 // QuietError is an error wrapper that prevents the verbose handshake log
// dump on errors. Exposed for use by GetCertificate. // dump on errors. Exposed for use by GetCertificate.
type QuietError struct { type QuietError struct {

View File

@ -10,14 +10,18 @@ import (
) )
func main() { func main() {
sessionCache := runner.NewLRUClientSessionCache(10)
tr := &http.Transport{ tr := &http.Transport{
DialTLS: func(network, addr string) (net.Conn, error) { DialTLS: func(network, addr string) (net.Conn, error) {
return runner.Dial(network, addr, &runner.Config{ return runner.Dial(network, addr, &runner.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
ClientSessionCache: sessionCache,
}) })
}, },
DisableKeepAlives: true,
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get("https://" + os.Args[1]) resp, err := client.Get("https://" + os.Args[1])
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -25,4 +29,13 @@ func main() {
if err := resp.Write(os.Stdout); err != nil { if err := resp.Write(os.Stdout); err != nil {
log.Fatal(err) 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)
}
} }

View File

@ -1,3 +1,8 @@
#! /bin/sh #! /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

View File

@ -11,7 +11,8 @@ if [ "$1" = "INSTALL" ]; then
elif [ "$1" = "RUN" ]; then elif [ "$1" = "RUN" ]; then
IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' tris-localserver) IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' tris-localserver)
docker run -i --rm tls-tris:$2 $IP:$3 | tee output.txt docker run --rm tls-tris:$2 $IP:$3 | tee output.txt
grep "Hello TLS 1.3" output.txt grep "Hello TLS 1.3" output.txt | grep -v "resumed"
grep "Hello TLS 1.3" output.txt | grep "resumed"
fi fi

View File

@ -14,8 +14,10 @@ func main() {
DialTLS: func(network, addr string) (net.Conn, error) { DialTLS: func(network, addr string) (net.Conn, error) {
return mint.Dial(network, addr, nil) return mint.Dial(network, addr, nil)
}, },
DisableKeepAlives: true,
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
resp, err := client.Get("https://" + os.Args[1]) resp, err := client.Get("https://" + os.Args[1])
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -23,4 +25,13 @@ func main() {
if err := resp.Write(os.Stdout); err != nil { if err := resp.Write(os.Stdout); err != nil {
log.Fatal(err) 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)
}
} }

View File

@ -1,10 +1,11 @@
FROM scratch FROM scratch
# GOOS=linux ../go.sh build -v -i .
ADD tris-localserver ./
ENV TLSDEBUG error ENV TLSDEBUG error
EXPOSE 443 EXPOSE 443
EXPOSE 4443 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" ] CMD [ "./tris-localserver", "0.0.0.0:443", "0.0.0.0:4443" ]

View File

@ -19,7 +19,11 @@ var tlsVersionToName = map[uint16]string{
func main() { func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<!DOCTYPE html><p>Hello TLS %s _o/\n", tlsVersionToName[r.TLS.Version]) resumed := ""
if r.TLS.DidResume {
resumed = " [resumed]"
}
fmt.Fprintf(w, "<!DOCTYPE html><p>Hello TLS %s%s _o/\n", tlsVersionToName[r.TLS.Version], resumed)
}) })
http.HandleFunc("/ch", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/ch", func(w http.ResponseWriter, r *http.Request) {

View File

@ -15,7 +15,10 @@ ENV USE_64=1 NSS_ENABLE_TLS_1_3=1
# ARG REVISION=3e7b53b18112 # ARG REVISION=3e7b53b18112
# Draft 18 # Draft 18
ARG REVISION=b6dfef6d0ff0 # ARG REVISION=b6dfef6d0ff0
# tstclnt resumption
ARG REVISION=460a0a1e009f
RUN cd nss && hg pull RUN cd nss && hg pull
RUN cd nss && hg checkout -C $REVISION RUN cd nss && hg checkout -C $REVISION

View File

@ -5,4 +5,4 @@ shift
HOST="${ADDR[0]}" HOST="${ADDR[0]}"
PORT="${ADDR[1]}" 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 "$@"

View File

@ -278,7 +278,7 @@ Curves:
hs.hello.scts = hs.cert.SignedCertificateTimestamps 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() err = committer.Commit()
if err != nil { if err != nil {
return false, err return false, err
@ -353,9 +353,10 @@ func (hs *serverHandshakeState) checkForResumption() bool {
return false return false
} }
var ok bool sessionTicket := append([]uint8{}, hs.clientHello.sessionTicket...)
var sessionTicket = append([]uint8{}, hs.clientHello.sessionTicket...) serializedState, usedOldKey := c.decryptTicket(sessionTicket)
if hs.sessionState, ok = c.decryptTicket(sessionTicket); !ok { hs.sessionState = &sessionState{usedOldKey: usedOldKey}
if ok := hs.sessionState.unmarshal(serializedState); !ok {
return false return false
} }
@ -730,7 +731,7 @@ func (hs *serverHandshakeState) sendSessionTicket() error {
masterSecret: hs.masterSecret, masterSecret: hs.masterSecret,
certificates: hs.certsFromClient, certificates: hs.certsFromClient,
} }
m.ticket, err = c.encryptTicket(&state) m.ticket, err = c.encryptTicket(state.marshal())
if err != nil { if err != nil {
return err return err
} }

View File

@ -193,8 +193,7 @@ func (s *sessionState13) unmarshal(data []byte) bool {
return int(l) == len(s.resumptionSecret) return int(l) == len(s.resumptionSecret)
} }
func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) { func (c *Conn) encryptTicket(serialized []byte) ([]byte, error) {
serialized := state.marshal()
encrypted := make([]byte, ticketKeyNameLen+aes.BlockSize+len(serialized)+sha256.Size) encrypted := make([]byte, ticketKeyNameLen+aes.BlockSize+len(serialized)+sha256.Size)
keyName := encrypted[:ticketKeyNameLen] keyName := encrypted[:ticketKeyNameLen]
iv := encrypted[ticketKeyNameLen : ticketKeyNameLen+aes.BlockSize] iv := encrypted[ticketKeyNameLen : ticketKeyNameLen+aes.BlockSize]
@ -218,7 +217,7 @@ func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) {
return encrypted, nil 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 || if c.config.SessionTicketsDisabled ||
len(encrypted) < ticketKeyNameLen+aes.BlockSize+sha256.Size { len(encrypted) < ticketKeyNameLen+aes.BlockSize+sha256.Size {
return nil, false return nil, false
@ -258,7 +257,5 @@ func (c *Conn) decryptTicket(encrypted []byte) (*sessionState, bool) {
plaintext := ciphertext plaintext := ciphertext
cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext) cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext)
state := &sessionState{usedOldKey: keyIndex > 0} return plaintext, keyIndex > 0
ok := state.unmarshal(plaintext)
return state, ok
} }