Skip to content

Commit

Permalink
implement stateless resets
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Dec 6, 2017
1 parent 1f893e2 commit 1e8da32
Show file tree
Hide file tree
Showing 10 changed files with 565 additions and 441 deletions.
2 changes: 2 additions & 0 deletions alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const (
AlertBadCertificateHashValue Alert = 114
AlertUnknownPSKIdentity Alert = 115
AlertNoApplicationProtocol Alert = 120
AlertStatelessRetry Alert = 253
AlertWouldBlock Alert = 254
AlertNoAlert Alert = 255
)
Expand Down Expand Up @@ -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",
}
Expand Down
2 changes: 1 addition & 1 deletion alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
56 changes: 38 additions & 18 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -719,7 +740,6 @@ func (c *Conn) Handshake() Alert {

c.hState = state
logf(logTypeHandshake, "state is now %s", c.GetHsState())

_, connected = state.(StateConnected)
}

Expand Down
73 changes: 73 additions & 0 deletions cookie-source.go
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 0 additions & 22 deletions extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
28 changes: 2 additions & 26 deletions negotiation.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package mint

import (
"bytes"
"encoding/hex"
"fmt"
"time"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 2 additions & 18 deletions negotiation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")
}
Expand Down
Loading

0 comments on commit 1e8da32

Please sign in to comment.