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 Nov 8, 2017
1 parent 417e005 commit e29ba77
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 433 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)")
}
45 changes: 35 additions & 10 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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}
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
}

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 e29ba77

Please sign in to comment.