diff --git a/alert.go b/alert.go index 5e31035..430e455 100644 --- a/alert.go +++ b/alert.go @@ -46,6 +46,7 @@ const ( AlertBadCertificateHashValue Alert = 114 AlertUnknownPSKIdentity Alert = 115 AlertNoApplicationProtocol Alert = 120 + AlertStatelessRetry Alert = 253 AlertWouldBlock Alert = 254 AlertNoAlert Alert = 255 ) @@ -82,6 +83,7 @@ var alertText = map[Alert]string{ AlertUnknownPSKIdentity: "unknown PSK identity", AlertNoApplicationProtocol: "no application protocol", AlertNoRenegotiation: "no renegotiation", + AlertStatelessRetry: "stateless retry", AlertWouldBlock: "would have blocked", AlertNoAlert: "no alert", } diff --git a/alert_test.go b/alert_test.go index 725c9c0..7d70a34 100644 --- a/alert_test.go +++ b/alert_test.go @@ -7,5 +7,5 @@ import ( func TestAlert(t *testing.T) { assertEquals(t, AlertCloseNotify.String(), "close notify") assertEquals(t, AlertCloseNotify.Error(), "close notify") - assertEquals(t, Alert(0xfd).String(), "alert(253)") + assertEquals(t, Alert(0xfc).String(), "alert(252)") } diff --git a/conn.go b/conn.go index 08eb58d..d428b24 100644 --- a/conn.go +++ b/conn.go @@ -36,16 +36,20 @@ type PreSharedKeyCache interface { Size() int } -type PSKMapCache map[string]PreSharedKey - -// A CookieHandler does two things: -// - generates a byte string that is sent as a part of a cookie to the client in the HelloRetryRequest -// - validates this byte string echoed by the client in the ClientHello +// A CookieHandler can be used to give the application more fine-grained control over Cookies. +// Generate receives the Conn as an argument, so the CookieHandler can decide when to send the cookie based on that, and offload state to the client by encoding that into the Cookie. +// When the client echoes the Cookie, Validate is called. The application can then recover the state from the cookie. type CookieHandler interface { + // Generate a byte string that is sent as a part of a cookie to the client in the HelloRetryRequest + // If Generate returns nil, mint will not send a HelloRetryRequest. Generate(*Conn) ([]byte, error) + // Validate is called when receiving a ClientHello containing a Cookie. + // If validation failed, the handshake is aborted. Validate(*Conn, []byte) bool } +type PSKMapCache map[string]PreSharedKey + func (cache PSKMapCache) Get(key string) (psk PreSharedKey, ok bool) { psk, ok = cache[key] return @@ -74,9 +78,16 @@ type Config struct { AllowEarlyData bool // Require the client to echo a cookie. RequireCookie bool - // If cookies are required and no CookieHandler is set, a default cookie handler is used. - // The default cookie handler uses 32 random bytes as a cookie. - CookieHandler CookieHandler + // A CookieHandler can be used to set and validate a cookie. + // The cookie returned by the CookieHandler will be part of the cookie sent on the wire, and encoded using the CookieSource. + // If no CookieHandler is set, mint will always send a cookie. + // The CookieHandler can be used to decide on a per-connection basis, if a cookie should be sent. + CookieHandler CookieHandler + // The CookieSource is used to encrypt / decrypt cookies. + // It should make sure that the Cookie cannot be read and tampered with by the client. + // If non-blocking mode is used, and cookies are required, this field has to be set. + // In blocking mode, a default cookie source is used, if this is unused. + CookieSource CookieSource RequireClientAuth bool // Shared fields @@ -110,6 +121,8 @@ func (c *Config) Clone() *Config { EarlyDataLifetime: c.EarlyDataLifetime, AllowEarlyData: c.AllowEarlyData, RequireCookie: c.RequireCookie, + CookieHandler: c.CookieHandler, + CookieSource: c.CookieSource, RequireClientAuth: c.RequireClientAuth, Certificates: c.Certificates, @@ -611,6 +624,7 @@ func (c *Conn) HandshakeSetup() Alert { PSKModes: c.config.PSKModes, AllowEarlyData: c.config.AllowEarlyData, RequireCookie: c.config.RequireCookie, + CookieSource: c.config.CookieSource, CookieHandler: c.config.CookieHandler, RequireClientAuth: c.config.RequireClientAuth, NextProtos: c.config.NextProtos, @@ -623,10 +637,6 @@ func (c *Conn) HandshakeSetup() Alert { EarlyData: c.EarlyData, } - if caps.RequireCookie && caps.CookieHandler == nil { - caps.CookieHandler = &defaultCookieHandler{} - } - if c.isClient { state, actions, alert = ClientStateStart{Caps: caps, Opts: opts}.Next(nil) if alert != AlertNoAlert { @@ -642,6 +652,19 @@ func (c *Conn) HandshakeSetup() Alert { } } } else { + if c.config.RequireCookie && c.config.CookieSource == nil { + logf(logTypeHandshake, "RequireCookie set, but no CookieSource provided. Using default cookie source. Stateless Retry not possible.") + if c.config.NonBlocking { + logf(logTypeHandshake, "Not possible in non-blocking mode.") + return AlertInternalError + } + var err error + caps.CookieSource, err = newDefaultCookieSource() + if err != nil { + logf(logTypeHandshake, "Error initializing cookie source: %v", alert) + return AlertInternalError + } + } state = ServerStateStart{Caps: caps, conn: c} } @@ -671,7 +694,7 @@ func (c *Conn) Handshake() Alert { var alert Alert if c.hState == nil { - logf(logTypeHandshake, "%s First time through handshake, setting up", label) + logf(logTypeHandshake, "%s First time through handshake (or after stateless retry), setting up", label) alert = c.HandshakeSetup() if alert != AlertNoAlert { return alert @@ -701,16 +724,14 @@ func (c *Conn) Handshake() Alert { // Advance the state machine state, actions, alert = state.Next(hm) - - if alert != AlertNoAlert { + if alert != AlertNoAlert && alert != AlertStatelessRetry { logf(logTypeHandshake, "Error in state transition: %v", alert) return alert } for index, action := range actions { logf(logTypeHandshake, "%s taking next action (%d)", label, index) - alert = c.takeAction(action) - if alert != AlertNoAlert { + if alert := c.takeAction(action); alert != AlertNoAlert { logf(logTypeHandshake, "Error during handshake actions: %v", alert) c.sendAlert(alert) return alert @@ -719,7 +740,6 @@ func (c *Conn) Handshake() Alert { c.hState = state logf(logTypeHandshake, "state is now %s", c.GetHsState()) - _, connected = state.(StateConnected) } diff --git a/cookie-source.go b/cookie-source.go new file mode 100644 index 0000000..50345d7 --- /dev/null +++ b/cookie-source.go @@ -0,0 +1,73 @@ +package mint + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +// CookieSource is used to create and verify source address tokens +type CookieSource interface { + // NewToken creates a new token + NewToken([]byte) ([]byte, error) + // DecodeToken decodes a token + DecodeToken([]byte) ([]byte, error) +} + +type defaultCookieSource struct { + aead cipher.AEAD +} + +const tokenKeySize = 16 +const tokenNonceSize = 16 + +// newDefaultCookieSource creates a source for source address tokens +func newDefaultCookieSource() (CookieSource, error) { + secret := make([]byte, 32) + if _, err := rand.Read(secret); err != nil { + return nil, err + } + key, err := deriveKey(secret) + if err != nil { + return nil, err + } + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aead, err := cipher.NewGCMWithNonceSize(c, tokenNonceSize) + if err != nil { + return nil, err + } + return &defaultCookieSource{aead: aead}, nil +} + +func (s *defaultCookieSource) NewToken(data []byte) ([]byte, error) { + nonce := make([]byte, tokenNonceSize) + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + return s.aead.Seal(nonce, nonce, data, nil), nil +} + +func (s *defaultCookieSource) DecodeToken(p []byte) ([]byte, error) { + if len(p) < tokenNonceSize { + return nil, fmt.Errorf("Token too short: %d", len(p)) + } + nonce := p[:tokenNonceSize] + return s.aead.Open(nil, nonce, p[tokenNonceSize:], nil) +} + +func deriveKey(secret []byte) ([]byte, error) { + r := hkdf.New(sha256.New, secret, nil, []byte("mint TLS 1.3 cookie token key")) + key := make([]byte, tokenKeySize) + if _, err := io.ReadFull(r, key); err != nil { + return nil, err + } + return key, nil +} diff --git a/extensions.go b/extensions.go index 1dbe7bd..f239e16 100644 --- a/extensions.go +++ b/extensions.go @@ -562,25 +562,3 @@ func (c CookieExtension) Marshal() ([]byte, error) { func (c *CookieExtension) Unmarshal(data []byte) (int, error) { return syntax.Unmarshal(data, c) } - -// defaultCookieLength is the default length of a cookie -const defaultCookieLength = 32 - -type defaultCookieHandler struct { - data []byte -} - -var _ CookieHandler = &defaultCookieHandler{} - -// NewRandomCookie generates a cookie with DefaultCookieLength bytes of random data -func (h *defaultCookieHandler) Generate(*Conn) ([]byte, error) { - h.data = make([]byte, defaultCookieLength) - if _, err := prng.Read(h.data); err != nil { - return nil, err - } - return h.data, nil -} - -func (h *defaultCookieHandler) Validate(_ *Conn, data []byte) bool { - return bytes.Equal(h.data, data) -} diff --git a/negotiation.go b/negotiation.go index f4ead72..5383648 100644 --- a/negotiation.go +++ b/negotiation.go @@ -1,7 +1,6 @@ package mint import ( - "bytes" "encoding/hex" "fmt" "time" @@ -52,7 +51,7 @@ const ( ticketAgeTolerance uint32 = 5 * 1000 // five seconds in milliseconds ) -func PSKNegotiation(identities []PSKIdentity, binders []PSKBinderEntry, context []byte, psks PreSharedKeyCache) (bool, int, *PreSharedKey, CipherSuiteParams, error) { +func PSKNegotiation(identities []PSKIdentity, psks PreSharedKeyCache) (bool, int, *PreSharedKey, CipherSuiteParams, error) { logf(logTypeNegotiation, "Negotiating PSK offered=[%d] supported=[%d]", len(identities), psks.Size()) for i, id := range identities { identityHex := hex.EncodeToString(id.Identity) @@ -81,30 +80,7 @@ func PSKNegotiation(identities []PSKIdentity, binders []PSKBinderEntry, context params, ok := cipherSuiteMap[psk.CipherSuite] if !ok { - err := fmt.Errorf("tls.cryptoinit: Unsupported ciphersuite from PSK [%04x]", psk.CipherSuite) - return false, 0, nil, CipherSuiteParams{}, err - } - - // Compute binder - binderLabel := labelExternalBinder - if psk.IsResumption { - binderLabel = labelResumptionBinder - } - - h0 := params.Hash.New().Sum(nil) - zero := bytes.Repeat([]byte{0}, params.Hash.Size()) - earlySecret := HkdfExtract(params.Hash, zero, psk.Key) - binderKey := deriveSecret(params, earlySecret, binderLabel, h0) - - // context = ClientHello[truncated] - // context = ClientHello1 + HelloRetryRequest + ClientHello2[truncated] - ctxHash := params.Hash.New() - ctxHash.Write(context) - - binder := computeFinishedData(params, binderKey, ctxHash.Sum(nil)) - if !bytes.Equal(binder, binders[i].Binder) { - logf(logTypeNegotiation, "Binder check failed for identity %x; [%x] != [%x]", psk.Identity, binder, binders[i].Binder) - return false, 0, nil, CipherSuiteParams{}, fmt.Errorf("Binder check failed identity %x", psk.Identity) + return false, 0, nil, CipherSuiteParams{}, fmt.Errorf("tls.cryptoinit: Unsupported ciphersuite from PSK [%04x]", psk.CipherSuite) } logf(logTypeNegotiation, "Using PSK with identity %x", psk.Identity) diff --git a/negotiation_test.go b/negotiation_test.go index 5649e0d..840661a 100644 --- a/negotiation_test.go +++ b/negotiation_test.go @@ -55,21 +55,10 @@ func TestDHNegotiation(t *testing.T) { } func TestPSKNegotiation(t *testing.T) { - chTrunc := unhex("0001020304050607") - binderValue := unhex("13a468af471adc19b94dcc0b888135423a11911f2c13050238b579d0f19d41c9") - identities := []PSKIdentity{ {Identity: []byte{0, 1, 2, 3}}, {Identity: []byte{4, 5, 6, 7}}, } - binders := []PSKBinderEntry{ - {Binder: binderValue}, - {Binder: binderValue}, - } - badBinders := []PSKBinderEntry{ - {Binder: []byte{}}, - {Binder: []byte{}}, - } psks := &PSKMapCache{ "04050607": { CipherSuite: TLS_AES_128_GCM_SHA256, @@ -79,20 +68,15 @@ func TestPSKNegotiation(t *testing.T) { } // Test successful negotiation - ok, selected, psk, params, err := PSKNegotiation(identities, binders, chTrunc, psks) + ok, selected, psk, params, err := PSKNegotiation(identities, psks) assertEquals(t, ok, true) assertEquals(t, selected, 1) assertNotNil(t, psk, "PSK not set") assertEquals(t, params.Suite, psk.CipherSuite) assertNotError(t, err, "Valid PSK negotiation failed") - // Test negotiation failure on binder value failure - ok, _, _, _, err = PSKNegotiation(identities, badBinders, chTrunc, psks) - assertEquals(t, ok, false) - assertError(t, err, "Failed to error on binder failure") - // Test negotiation failure on no PSK overlap - ok, _, _, _, err = PSKNegotiation(identities, binders, chTrunc, &PSKMapCache{}) + ok, _, _, _, err = PSKNegotiation(identities, &PSKMapCache{}) assertEquals(t, ok, false) assertNotError(t, err, "Errored on PSK negotiation failure") } diff --git a/server-state-machine.go b/server-state-machine.go index 60df9b6..9175cee 100644 --- a/server-state-machine.go +++ b/server-state-machine.go @@ -2,8 +2,11 @@ package mint import ( "bytes" + "fmt" "hash" "reflect" + + "github.com/bifurcation/mint/syntax" ) // Server State Machine @@ -57,13 +60,18 @@ import ( // WAIT_FINISHED RekeyIn; RekeyOut; // CONNECTED StoreTicket || (RekeyIn; [RekeyOut]) +// A cookie can be sent to the client in a HRR. +// It contains two fields: +// 1. The MintCookie: This field is used by mint itself to store the hash of initial client hello. +// 2. The ApplicationCookie: This opaque value can be provided by the application (by setting a Config.CookieHandler) +type cookie struct { + MintCookie []byte `tls:"head=2"` + ApplicationCookie []byte `tls:"head=2"` +} + type ServerStateStart struct { Caps Capabilities conn *Conn - - cookieSent bool - firstClientHello *HandshakeMessage - helloRetryRequest *HandshakeMessage } func (state ServerStateStart) Next(hm *HandshakeMessage) (HandshakeState, []HandshakeAction, Alert) { @@ -113,6 +121,8 @@ func (state ServerStateStart) Next(hm *HandshakeMessage) (HandshakeState, []Hand ch.Extensions.Find(clientPSKModes) ch.Extensions.Find(clientCookie) + clientSentCookie := len(clientCookie.Cookie) > 0 + if gotServerName { connParams.ServerName = string(*serverName) } @@ -129,9 +139,29 @@ func (state ServerStateStart) Next(hm *HandshakeMessage) (HandshakeState, []Hand return nil, nil, AlertProtocolVersion } - if state.Caps.RequireCookie && state.cookieSent && !state.Caps.CookieHandler.Validate(state.conn, clientCookie.Cookie) { - logf(logTypeHandshake, "[ServerStateStart] Cookie mismatch") - return nil, nil, AlertAccessDenied + // The client sent a cookie. So this is probably the second ClientHello (sent as a response to a HRR) + var firstClientHello *HandshakeMessage + if clientSentCookie { + plainCookie, err := state.Caps.CookieSource.DecodeToken(clientCookie.Cookie) + if err != nil { + logf(logTypeHandshake, fmt.Sprintf("[ServerStateStart] Error decoding token [%v]", err)) + return nil, nil, AlertDecryptError + } + cookie := &cookie{} + if _, err := syntax.Unmarshal(plainCookie, cookie); err != nil { // this should never happen + logf(logTypeHandshake, fmt.Sprintf("[ServerStateStart] Error unmarshaling cookie [%v]", err)) + return nil, nil, AlertInternalError + } + // restore the hash of initial ClientHello from the cookie + firstClientHello = &HandshakeMessage{ + msgType: HandshakeTypeMessageHash, + body: cookie.MintCookie, + } + // have the application validate its part of the cookie + if state.Caps.CookieHandler != nil && !state.Caps.CookieHandler.Validate(state.conn, cookie.ApplicationCookie) { + logf(logTypeHandshake, "[ServerStateStart] Cookie mismatch") + return nil, nil, AlertAccessDenied + } } // Figure out if we can do DH @@ -142,26 +172,51 @@ func (state ServerStateStart) Next(hm *HandshakeMessage) (HandshakeState, []Hand var selectedPSK int var psk *PreSharedKey var params CipherSuiteParams - if len(clientPSK.Identities) > 0 { - contextBase := []byte{} - if state.helloRetryRequest != nil { - chBytes := state.firstClientHello.Marshal() - hrrBytes := state.helloRetryRequest.Marshal() - contextBase = append(chBytes, hrrBytes...) - } - chTrunc, err := ch.Truncated() + if len(clientPSK.Identities) > 0 { + canDoPSK, selectedPSK, psk, params, err = PSKNegotiation(clientPSK.Identities, state.Caps.PSKs) if err != nil { - logf(logTypeHandshake, "[ServerStateStart] Error computing truncated ClientHello [%v]", err) - return nil, nil, AlertDecodeError + logf(logTypeHandshake, "[ServerStateStart] Error in PSK negotiation [%v]", err) + return nil, nil, AlertInternalError } - context := append(contextBase, chTrunc...) + if canDoPSK { + // Compute binder + binderLabel := labelExternalBinder + if psk.IsResumption { + binderLabel = labelResumptionBinder + } - canDoPSK, selectedPSK, psk, params, err = PSKNegotiation(clientPSK.Identities, clientPSK.Binders, context, state.Caps.PSKs) - if err != nil { - logf(logTypeHandshake, "[ServerStateStart] Error in PSK negotiation [%v]", err) - return nil, nil, AlertInternalError + h0 := params.Hash.New().Sum(nil) + zero := bytes.Repeat([]byte{0}, params.Hash.Size()) + earlySecret := HkdfExtract(params.Hash, zero, psk.Key) + binderKey := deriveSecret(params, earlySecret, binderLabel, h0) + + // context = ClientHello[truncated] + // context = ClientHello1 + HelloRetryRequest + ClientHello2[truncated] + ctxHash := params.Hash.New() + if clientSentCookie { // if this ClientHello contains a cookie, we did a stateless retry. Now need to recover the + ctxHash.Write(firstClientHello.Marshal()) + // fill in the cookie sent by the client. Needed to calculate the correct hash + cookieExt := &CookieExtension{Cookie: clientCookie.Cookie} + hrr, err := state.generateHRR(params.Suite, cookieExt) + if err != nil { + return nil, nil, AlertInternalError + } + ctxHash.Write(hrr.Marshal()) + } + chTrunc, err := ch.Truncated() + if err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error computing truncated ClientHello [%v]", err) + return nil, nil, AlertDecodeError + } + ctxHash.Write(chTrunc) + + binder := computeFinishedData(params, binderKey, ctxHash.Sum(nil)) + if !bytes.Equal(binder, clientPSK.Binders[selectedPSK].Binder) { + logf(logTypeNegotiation, "Binder check failed for identity %x; [%x] != [%x]", psk.Identity, binder, clientPSK.Binders[selectedPSK].Binder) + return nil, nil, AlertDecryptError + } } } @@ -175,61 +230,66 @@ func (state ServerStateStart) Next(hm *HandshakeMessage) (HandshakeState, []Hand return nil, nil, AlertHandshakeFailure } - // Send a cookie if required - // NB: Need to do this here because it's after ciphersuite selection, which - // has to be after PSK selection. - // XXX: Doing this statefully for now, could be stateless - var cookieData []byte - if state.Caps.RequireCookie && !state.cookieSent { - var err error - cookieData, err = state.Caps.CookieHandler.Generate(state.conn) - if err != nil { - logf(logTypeHandshake, "[ServerStateStart] Error generating cookie [%v]", err) - return nil, nil, AlertInternalError + var helloRetryRequest *HandshakeMessage + if state.Caps.RequireCookie { + // Send a cookie if required + // NB: Need to do this here because it's after ciphersuite selection, which + // has to be after PSK selection. + var shouldSendHRR bool + var cookieExt *CookieExtension + if !clientSentCookie { // this is the first ClientHello that we receive + var appCookie []byte + if state.Caps.CookieHandler == nil { // if Config.RequireCookie is set, but no CookieHandler was provided, we definitely need to send a cookie + shouldSendHRR = true + } else { // if the CookieHandler was set, we just send a cookie when the application provides one + var err error + appCookie, err = state.Caps.CookieHandler.Generate(state.conn) + if err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error generating cookie [%v]", err) + return nil, nil, AlertInternalError + } + shouldSendHRR = appCookie != nil + } + if shouldSendHRR { + params := cipherSuiteMap[connParams.CipherSuite] + h := params.Hash.New() + h.Write(clientHello.Marshal()) + plainCookie, err := syntax.Marshal(cookie{ + MintCookie: h.Sum(nil), + ApplicationCookie: appCookie, + }) + if err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error marshalling cookie [%v]", err) + return nil, nil, AlertInternalError + } + cookieData, err := state.Caps.CookieSource.NewToken(plainCookie) + if err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error encoding cookie [%v]", err) + return nil, nil, AlertInternalError + } + cookieExt = &CookieExtension{Cookie: cookieData} + } + } else { + cookieExt = &CookieExtension{Cookie: clientCookie.Cookie} } - } - if cookieData != nil { + + // Generate a HRR. We will need it in both of the two cases: + // 1. We need to send a Cookie. Then this HRR will be sent on the wire + // 2. We need to validate a cookie. Then we need its hash // Ignoring errors because everything here is newly constructed, so there // shouldn't be marshal errors - hrr := &HelloRetryRequestBody{ - Version: supportedVersion, - CipherSuite: connParams.CipherSuite, - } - hrr.Extensions.Add(&CookieExtension{Cookie: cookieData}) - - // Run the external extension handler. - if state.Caps.ExtensionHandler != nil { - err := state.Caps.ExtensionHandler.Send(HandshakeTypeHelloRetryRequest, &hrr.Extensions) + if shouldSendHRR || clientSentCookie { + helloRetryRequest, err = state.generateHRR(connParams.CipherSuite, cookieExt) if err != nil { - logf(logTypeHandshake, "[ServerStateStart] Error running external extension sender [%v]", err) return nil, nil, AlertInternalError } } - helloRetryRequest, err := HandshakeMessageFromBody(hrr) - if err != nil { - logf(logTypeHandshake, "[ServerStateStart] Error marshaling HRR [%v]", err) - return nil, nil, AlertInternalError - } - - params := cipherSuiteMap[connParams.CipherSuite] - h := params.Hash.New() - h.Write(clientHello.Marshal()) - firstClientHello := &HandshakeMessage{ - msgType: HandshakeTypeMessageHash, - body: h.Sum(nil), - } - - nextState := ServerStateStart{ - Caps: state.Caps, - conn: state.conn, - cookieSent: true, - firstClientHello: firstClientHello, - helloRetryRequest: helloRetryRequest, + if shouldSendHRR { + toSend := []HandshakeAction{SendHandshakeMessage{helloRetryRequest}} + logf(logTypeHandshake, "[ServerStateStart] -> [ServerStateStart]") + return state, toSend, AlertStatelessRetry } - toSend := []HandshakeAction{SendHandshakeMessage{helloRetryRequest}} - logf(logTypeHandshake, "[ServerStateStart] -> [ServerStateStart]") - return nextState, toSend, AlertNoAlert } // If we've got no entropy to make keys from, fail @@ -303,12 +363,38 @@ func (state ServerStateStart) Next(hm *HandshakeMessage) (HandshakeState, []Hand certScheme: certScheme, clientEarlyTrafficSecret: clientEarlyTrafficSecret, - firstClientHello: state.firstClientHello, - helloRetryRequest: state.helloRetryRequest, + firstClientHello: firstClientHello, + helloRetryRequest: helloRetryRequest, clientHello: clientHello, }.Next(nil) } +func (state *ServerStateStart) generateHRR(cs CipherSuite, cookieExt *CookieExtension) (*HandshakeMessage, error) { + var helloRetryRequest *HandshakeMessage + hrr := &HelloRetryRequestBody{ + Version: supportedVersion, + CipherSuite: cs, + } + if err := hrr.Extensions.Add(cookieExt); err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error adding CookieExtension [%v]", err) + return nil, err + } + // Run the external extension handler. + if state.Caps.ExtensionHandler != nil { + err := state.Caps.ExtensionHandler.Send(HandshakeTypeHelloRetryRequest, &hrr.Extensions) + if err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error running external extension sender [%v]", err) + return nil, err + } + } + helloRetryRequest, err := HandshakeMessageFromBody(hrr) + if err != nil { + logf(logTypeHandshake, "[ServerStateStart] Error marshaling HRR [%v]", err) + return nil, err + } + return helloRetryRequest, nil +} + type ServerStateNegotiated struct { Caps Capabilities Params ConnectionParameters diff --git a/state-machine.go b/state-machine.go index 4eb468c..206c821 100644 --- a/state-machine.go +++ b/state-machine.go @@ -60,6 +60,7 @@ type Capabilities struct { NextProtos []string AllowEarlyData bool RequireCookie bool + CookieSource CookieSource CookieHandler CookieHandler RequireClientAuth bool } diff --git a/state-machine_test.go b/state-machine_test.go index 754f9c7..525ec7a 100644 --- a/state-machine_test.go +++ b/state-machine_test.go @@ -6,309 +6,313 @@ import ( "testing" ) -var ( - stateMachineIntegrationCases = map[string]struct { - clientCapabilities Capabilities - clientOptions ConnectionOptions - serverCapabilities Capabilities - clientStateSequence []HandshakeState - serverStateSequence []HandshakeState - }{ - "normal": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - Certificates: certificates, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitCertCR{}, - ClientStateWaitCV{}, - ClientStateWaitFinished{}, - StateConnected{}, - }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateWaitFinished{}, - StateConnected{}, - }, - }, +// TODO: Track instructions other than state changes +func messagesFromActions(instructions []HandshakeAction) []*HandshakeMessage { + msgs := []*HandshakeMessage{} + for _, instr := range instructions { + msg, ok := instr.(SendHandshakeMessage) + if !ok { + continue + } + msgs = append(msgs, msg.Message) + } + return msgs +} - "helloRetryRequest": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - Certificates: certificates, - RequireCookie: true, - CookieHandler: &defaultCookieHandler{}, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitCertCR{}, - ClientStateWaitCV{}, - ClientStateWaitFinished{}, - StateConnected{}, - }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateStart{}, - ServerStateWaitFinished{}, - StateConnected{}, - }, - }, +// TODO: Unit tests for individual states +func TestStateMachineIntegration(t *testing.T) { + cookieSource, err := newDefaultCookieSource() + assertNotError(t, err, "error creating cookie source") - // PSK case, no early data - "psk": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{ - "example.com": psk, + var ( + stateMachineIntegrationCases = map[string]struct { + clientCapabilities Capabilities + clientOptions ConnectionOptions + serverCapabilities Capabilities + clientStateSequence []HandshakeState + serverStateSequence []HandshakeState + }{ + "normal": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + Certificates: certificates, + CookieSource: cookieSource, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitCertCR{}, + ClientStateWaitCV{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateWaitFinished{}, + StateConnected{}, }, }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{ - "00010203": psk, - }, - Certificates: certificates, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitFinished{}, - StateConnected{}, - }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateWaitFinished{}, - StateConnected{}, - }, - }, - // PSK case, with early data - "pskWithEarlyData": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{ - "example.com": psk, + "helloRetryRequest": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + Certificates: certificates, + RequireCookie: true, + CookieSource: cookieSource, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitCertCR{}, + ClientStateWaitCV{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateStart{}, + ServerStateWaitFinished{}, + StateConnected{}, }, }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - EarlyData: []byte{0, 1, 2, 3}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{ - "00010203": psk, - }, - Certificates: certificates, - AllowEarlyData: true, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitFinished{}, - StateConnected{}, - }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateWaitEOED{}, - ServerStateWaitFinished{}, - StateConnected{}, - }, - }, - // PSK case, server rejects PSK - "pskRejected": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{ - "example.com": psk, + // PSK case, no early data + "psk": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{ + "example.com": psk, + }, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{ + "00010203": psk, + }, + Certificates: certificates, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateWaitFinished{}, + StateConnected{}, }, }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - Certificates: certificates, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitCertCR{}, - ClientStateWaitCV{}, - ClientStateWaitFinished{}, - StateConnected{}, - }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateWaitFinished{}, - StateConnected{}, - }, - }, - // Client auth, successful - "clientAuth": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - Certificates: certificates, - }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - Certificates: certificates, - RequireClientAuth: true, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitCertCR{}, - ClientStateWaitCert{}, - ClientStateWaitCV{}, - ClientStateWaitFinished{}, - StateConnected{}, - }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateWaitCert{}, - ServerStateWaitCV{}, - ServerStateWaitFinished{}, - StateConnected{}, + // PSK case, with early data + "pskWithEarlyData": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{ + "example.com": psk, + }, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + EarlyData: []byte{0, 1, 2, 3}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{ + "00010203": psk, + }, + Certificates: certificates, + AllowEarlyData: true, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateWaitEOED{}, + ServerStateWaitFinished{}, + StateConnected{}, + }, }, - }, - // Client auth, no certificate found - "clientAuthNoCertificate": { - clientCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - }, - clientOptions: ConnectionOptions{ - ServerName: "example.com", - NextProtos: []string{"h2"}, - }, - serverCapabilities: Capabilities{ - Groups: []NamedGroup{P256}, - SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, - PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, - CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, - PSKs: &PSKMapCache{}, - Certificates: certificates, - RequireClientAuth: true, - }, - clientStateSequence: []HandshakeState{ - ClientStateStart{}, - ClientStateWaitSH{}, - ClientStateWaitEE{}, - ClientStateWaitCertCR{}, - ClientStateWaitCert{}, - ClientStateWaitCV{}, - ClientStateWaitFinished{}, - StateConnected{}, + // PSK case, server rejects PSK + "pskRejected": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{ + "example.com": psk, + }, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + Certificates: certificates, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitCertCR{}, + ClientStateWaitCV{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateWaitFinished{}, + StateConnected{}, + }, }, - serverStateSequence: []HandshakeState{ - ServerStateStart{}, - ServerStateWaitCert{}, - ServerStateWaitFinished{}, - StateConnected{}, + + // Client auth, successful + "clientAuth": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + Certificates: certificates, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + Certificates: certificates, + RequireClientAuth: true, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitCertCR{}, + ClientStateWaitCert{}, + ClientStateWaitCV{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateWaitCert{}, + ServerStateWaitCV{}, + ServerStateWaitFinished{}, + StateConnected{}, + }, }, - }, - } -) -// TODO: Track instructions other than state changes -func messagesFromActions(instructions []HandshakeAction) []*HandshakeMessage { - msgs := []*HandshakeMessage{} - for _, instr := range instructions { - msg, ok := instr.(SendHandshakeMessage) - if !ok { - continue + // Client auth, no certificate found + "clientAuthNoCertificate": { + clientCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + }, + clientOptions: ConnectionOptions{ + ServerName: "example.com", + NextProtos: []string{"h2"}, + }, + serverCapabilities: Capabilities{ + Groups: []NamedGroup{P256}, + SignatureSchemes: []SignatureScheme{RSA_PSS_SHA256}, + PSKModes: []PSKKeyExchangeMode{PSKModeDHEKE}, + CipherSuites: []CipherSuite{TLS_AES_128_GCM_SHA256}, + PSKs: &PSKMapCache{}, + Certificates: certificates, + RequireClientAuth: true, + }, + clientStateSequence: []HandshakeState{ + ClientStateStart{}, + ClientStateWaitSH{}, + ClientStateWaitEE{}, + ClientStateWaitCertCR{}, + ClientStateWaitCert{}, + ClientStateWaitCV{}, + ClientStateWaitFinished{}, + StateConnected{}, + }, + serverStateSequence: []HandshakeState{ + ServerStateStart{}, + ServerStateWaitCert{}, + ServerStateWaitFinished{}, + StateConnected{}, + }, + }, } - msgs = append(msgs, msg.Message) - } - return msgs -} + ) -// TODO: Unit tests for individual states -func TestStateMachineIntegration(t *testing.T) { for caseName, params := range stateMachineIntegrationCases { t.Run(caseName, func(t *testing.T) { @@ -342,7 +346,7 @@ func TestStateMachineIntegration(t *testing.T) { t.Logf("C->S: %d", body.msgType) serverState, serverInstr, alert = serverState.Next(body) serverResponses := messagesFromActions(serverInstr) - assert(t, alert == AlertNoAlert, fmt.Sprintf("Alert from server [%v]", alert)) + assert(t, alert == AlertNoAlert || alert == AlertStatelessRetry, fmt.Sprintf("Alert from server [%v]", alert)) serverStateSequence = append(serverStateSequence, serverState) t.Logf("Server: %s", reflect.TypeOf(serverState).Name()) serverToSend = append(serverToSend, serverResponses...)