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..8f64053 100644 --- a/conn.go +++ b/conn.go @@ -74,9 +74,16 @@ type Config struct { AllowEarlyData bool // Require the client to echo a cookie. RequireCookie bool + // TODO: fix comment here: no more default cookie handler // 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 + CookieHandler CookieHandler + // The CookieSource is used to encrypt / decrypt cookies. + // TODO: implement a check for that + // If non-blocking mode is used, and cookies are required, this field has to be set. + // TODO: implement that + // In blocking mode, a default cookie source is used, if this is unused. + CookieSource CookieSource RequireClientAuth bool // Shared fields @@ -110,6 +117,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 +620,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 +633,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 +648,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 client state: %v", alert) + return AlertInternalError + } + } state = ServerStateStart{Caps: caps, conn: c} } @@ -671,7 +690,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 @@ -687,6 +706,7 @@ func (c *Conn) Handshake() Alert { for !connected { // Read a handshake message + logf(logTypeHandshake, "readmessage") hm, err := c.hIn.ReadMessage() if err == WouldBlock { logf(logTypeHandshake, "%s Would block reading message: %v", label, err) @@ -701,16 +721,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 @@ -720,6 +738,13 @@ func (c *Conn) Handshake() Alert { c.hState = state logf(logTypeHandshake, "state is now %s", c.GetHsState()) + // if alert == AlertStatelessRetry { + // logf(logTypeHandshake, "Doing a retry") + // if c.config.NonBlocking { + // return AlertStatelessRetry + // } + // } + _, 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..b18cdf1 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, AlertAccessDenied + } + cookie := &cookie{} + if _, err := syntax.Unmarshal(plainCookie, cookie); err != nil { + logf(logTypeHandshake, fmt.Sprintf("[ServerStateStart] Error unmarshaling cookie [%v]", err)) + return nil, nil, AlertAccessDenied + } + // 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, AlertInternalError + } } } @@ -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), + if shouldSendHRR { + toSend := []HandshakeAction{SendHandshakeMessage{helloRetryRequest}} + logf(logTypeHandshake, "[ServerStateStart] -> [ServerStateStart]") + return state, toSend, AlertStatelessRetry } - - nextState := ServerStateStart{ - Caps: state.Caps, - conn: state.conn, - cookieSent: true, - firstClientHello: firstClientHello, - helloRetryRequest: helloRetryRequest, - } - 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...)