Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement stateless resets #141

Merged
merged 5 commits into from
Dec 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't your fault, but I wonder if we should give these non-alerts a special name so we don't acccidentally try to send them.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #149

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)")
}
60 changes: 42 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 CookieProtector.
// 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 CookieProtector 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 protector is used, if this is unused.
CookieProtector CookieProtector
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,
CookieProtector: c.CookieProtector,
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,
CookieProtector: c.config.CookieProtector,
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.CookieProtector == nil {
logf(logTypeHandshake, "RequireCookie set, but no CookieProtector provided. Using default cookie protector. Stateless Retry not possible.")
if c.config.NonBlocking {
logf(logTypeHandshake, "Not possible in non-blocking mode.")
return AlertInternalError
}
var err error
caps.CookieProtector, err = NewDefaultCookieProtector()
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,8 +740,11 @@ func (c *Conn) Handshake() Alert {

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

_, connected = state.(StateConnected)

if c.config.NonBlocking && alert == AlertStatelessRetry {
return AlertStatelessRetry
}
}

c.state = state.(StateConnected)
Expand Down
86 changes: 86 additions & 0 deletions cookie-protector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package mint

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"

"golang.org/x/crypto/hkdf"
)

// CookieProtector is used to create and verify a cookie
type CookieProtector interface {
// NewToken creates a new token
NewToken([]byte) ([]byte, error)
// DecodeToken decodes a token
DecodeToken([]byte) ([]byte, error)
}

const cookieSecretSize = 32
const cookieNonceSize = 32

// The DefaultCookieProtector is a simple implementation for the CookieProtector.
type DefaultCookieProtector struct {
secret []byte
}

var _ CookieProtector = &DefaultCookieProtector{}

// NewDefaultCookieProtector creates a source for source address tokens
func NewDefaultCookieProtector() (CookieProtector, error) {
secret := make([]byte, cookieSecretSize)
if _, err := rand.Read(secret); err != nil {
return nil, err
}
return &DefaultCookieProtector{secret: secret}, nil
}

// NewToken encodes data into a new token.
func (s *DefaultCookieProtector) NewToken(data []byte) ([]byte, error) {
nonce := make([]byte, cookieNonceSize)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
aead, aeadNonce, err := s.createAEAD(nonce)
if err != nil {
return nil, err
}
return append(nonce, aead.Seal(nil, aeadNonce, data, nil)...), nil
}

// DecodeToken decodes a token.
func (s *DefaultCookieProtector) DecodeToken(p []byte) ([]byte, error) {
if len(p) < cookieNonceSize {
return nil, fmt.Errorf("Token too short: %d", len(p))
}
nonce := p[:cookieNonceSize]
aead, aeadNonce, err := s.createAEAD(nonce)
if err != nil {
return nil, err
}
return aead.Open(nil, aeadNonce, p[cookieNonceSize:], nil)
}

func (s *DefaultCookieProtector) createAEAD(nonce []byte) (cipher.AEAD, []byte, error) {
h := hkdf.New(sha256.New, s.secret, nonce, []byte("mint cookie source"))
key := make([]byte, 32) // use a 32 byte key, in order to select AES-256
if _, err := io.ReadFull(h, key); err != nil {
return nil, nil, err
}
aeadNonce := make([]byte, 12)
if _, err := io.ReadFull(h, aeadNonce); err != nil {
return nil, nil, err
}
c, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
aead, err := cipher.NewGCM(c)
if err != nil {
return nil, nil, err
}
return aead, aeadNonce, nil
}
33 changes: 33 additions & 0 deletions cookie-protector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package mint

import (
"bytes"
"testing"
)

func TestCookieProtector(t *testing.T) {
cs, err := NewDefaultCookieProtector()
assertNotError(t, err, "creating the cookie source failed")

t.Run("handling valid tokens", func(t *testing.T) {
cookie := []byte("foobar")
token, err := cs.NewToken(cookie)
assertNotError(t, err, "creating new token failed")
decoded, err := cs.DecodeToken(token)
assertNotError(t, err, "decoding the token failed")
assertDeepEquals(t, cookie, decoded)
})

t.Run("handling invalid tokens", func(t *testing.T) {
_, err := cs.DecodeToken([]byte("too short"))
assertError(t, err, "it should reject too short tokens")
_, err = cs.DecodeToken(append(bytes.Repeat([]byte{0}, cookieNonceSize), []byte("invalid token")...))
assertError(t, err, "it should reject invalid tokens")
// create a valid and modify the nonce
token, err := cs.NewToken([]byte("foobar"))
assertNotError(t, err, "creating new token failed")
token[0]++
_, err = cs.DecodeToken(token)
assertError(t, err, "it should reject a token with the wrong nonce")
})
}
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)
}
Loading