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

Conversation

marten-seemann
Copy link
Collaborator

This PR changes the server state machine such that sending HelloRetryRequests can be performed statelessly.

In non-blocking mode, Handshake() will return a new alert, AlertStatelessRetry. When receiving this return, an app can discard all mint-related state (i.e. delete Conn garbage collected). When not using non-blocking mode, the handshake is performed as stateless as possible, at no point the initial ClientHello and the HelloRetryRequest will be stored.

The cookie that is sent to the client in the HelloRetryRequest is composed of two parts: In the mint-part of the cookie, we store the hash of the initial ClientHello. The second part can be used to store an application-defined cookie (used when Config.CookieHandler is set). The application has to provide a way to encrypt this cookie (it should use authenticated encryption for this). This can be done by setting Config.CookieSource. In blocking mode, we default to use AES-GCM if no CookieSource is set.

@ekr, @bifurcation: Please tell me what you think about this. The stateless handshake works for me with this PR, but I can't guarantee I didn't miss anything subtle here. My current understanding of the TLS 1.3 draft doesn't go a lot farther than what I need for my mint changes and the QUIC integration.

@ekr
Copy link
Collaborator

ekr commented Nov 8, 2017

@marten-seemann this seems to fail in CI...

Copy link
Collaborator

@ekr ekr left a comment

Choose a reason for hiding this comment

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

  1. I haven't reviewed the tests yet.
  2. This seems generally sound structurally, but I had technical comments about the internals.

I can review the tests once they work in CI

@@ -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

conn.go Outdated
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

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

This TODO seems like it needs fixing.

conn.go Outdated
// 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

These TODOs also seem to need fixing

conn.go Outdated
var err error
caps.CookieSource, err = newDefaultCookieSource()
if err != nil {
logf(logTypeHandshake, "Error initializing client state: %v", alert)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this say server state?

conn.go Outdated
@@ -687,6 +706,7 @@ func (c *Conn) Handshake() Alert {

for !connected {
// Read a handshake message
logf(logTypeHandshake, "readmessage")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe make this |logTypeVerbose| and make the text clearer.

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think you want AccessDenied here, but rather DecryptError or IllegalParameter

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would put a TODO here because if you were to change your key, this wouldn't work.

if _, err := syntax.Unmarshal(plainCookie, cookie); err != nil {
logf(logTypeHandshake, fmt.Sprintf("[ServerStateStart] Error unmarshaling cookie [%v]", err))
return nil, nil, AlertAccessDenied
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be internal_error, because it can't happen.

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am surprised this is internal error. @bifurcation ?

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems like decrypt_error.

logf(logTypeHandshake, "[ServerStateStart] Error generating cookie [%v]", err)
return nil, nil, AlertInternalError
}
shouldSendHRR = appCookie != nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this idiomatic go? I would have done if....

@marten-seemann marten-seemann force-pushed the stateless-reset branch 3 times, most recently from a1ccfc5 to 533628f Compare November 9, 2017 02:27
@marten-seemann
Copy link
Collaborator Author

Hi, @ekr thanks for the review! Seems like I forgot a bunch of TODOs and a few debug statements. I removed those, and added a bunch of documentation about Cookies, CookieSources and CookieHandler.
The reason the tests were failing is that CircleCI apparently uses Go 1.8 if no config file is provided, and the test helper functions (t.Helper()) require Go 1.9. I added a basic config file.

I really like the idea of storing the negotiated parameters in the cookie as well. By doing so, I was able to remove a rather ugly hack that was necessary to make PSK mode work. Is there anything else besides the CipherSuite that we would want to store in the cookie?

The code for the cookie source is what we copied from Google's QUIC implementation 1.5 years ago or so. I changed the cookie source to use a nonce size of 32 byte, as you suggested, and also removed the HKDF. I'm not sure though why're suggesting using a per-message key, I thought using the same key will be ok as long as the nonce is not reused. Can you please explain?
I also made the DefaultCookieSource public. When using non-blocking mode, an app will have to provide its own implementation for the CookieSource. The DefaultCookieSource should work fine in easy use cases, such as running a single server (in a multi-server deployment you'll need some shared cookie source anyway, and probably also implement some key rotation mechanism).

@ekr
Copy link
Collaborator

ekr commented Nov 9, 2017

The issue with the nonce here is that AES-GCM fails catastrophically if you have nonce reuse. If you use a random nonce, you get a non-trivial probability of reuse with enough connections (the 50% mark is at 2^{48} but the risk is unacceptably high well below that). Unfortunately, just making the nonce bigger doesn't help because AES-GCM hashes a >96 bit nonce down to 96 bits.

If you want to use GCM, you can guarantee a unique key/nonce pair by generating a longer nonce and then using HKDF to turn the master key into a key/nonce. Because the output is longer, the chance of collision is reduced.

@marten-seemann
Copy link
Collaborator Author

I just pushed a fixed. We're now creating a new key (which is HKDF derived using the nonce as a salt) for every nonce.

cookie_source.go Outdated
}

const tokenKeySize = 16
const tokenNonceSize = 16
var _ CookieSource = &DefaultCookieSource{}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am probably not enough of a Golang wizard, but why is this neded.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Strictly speaking this is not needed. It makes the compiler check that DefaultCookieSource actually implements the CookieSource interface. Since it's an assignment to _, it will be removed during compilation.

cookie_source.go Outdated
if err != nil {
return nil, err
}
return aead.Seal(nonce, nonce, data, nil), nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is quite what you want. Rather, you want to do:

r := make([]byte, 32)
rand.Read(r)
key = hkdf(secret, R, []byte("mint cookie key")) // key length
nonce = hkdf(secret, R, []byte("mint cookie nonce")) // 12 bytes

And then you can use nonce the way you have it here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or, if you prefer you can just read the key and the nonce as successive values from the hkdf.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How is that supposed to work? When decoding the cookie, I don't know r any more, and I can't create the AEAD to open the cookie.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The cookie is r || aead.Seal(blah...)

Right now you are just using nonce == r, so the difference I am proposing is you hkdf r to make the nonce.

cookie_source.go Outdated
if err != nil {
return nil, err
}
return cipher.NewGCMWithNonceSize(c, cookieNonceSize)
Copy link
Collaborator

Choose a reason for hiding this comment

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

You want NewGCM()

_, 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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would also be good to have one with a bogus nonce, e.g., by stomping the first byte.

@marten-seemann
Copy link
Collaborator Author

@ekr, I implemented the changes for the CookieSource as you suggested. Please have another look.

@ekr
Copy link
Collaborator

ekr commented Nov 30, 2017

@marten-seemann I think you forgot to push

@marten-seemann
Copy link
Collaborator Author

I don't think so. I rebased and amended the commit.

@ekr
Copy link
Collaborator

ekr commented Nov 30, 2017 via email

cookie_source.go Outdated
aeadNonce := make([]byte, 12)
if _, err := io.ReadFull(h, aeadNonce); err != nil {
return nil, nil, err
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks fine to me. Assuming you didn't change anything else, I think this is good to Go. @bifurcation ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't change anything else. This should be ready to merge now.

@@ -0,0 +1,10 @@
version: 2
Copy link
Owner

Choose a reason for hiding this comment

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

Unless this change is necessary for this functionality, please put it in a separate PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was the first commit of this PR (03b32d6). I openend #147, containing just this one commit.

@marten-seemann
Copy link
Collaborator Author

@bifurcation: Is there anything else you need from me to merge this PR?

Copy link
Owner

@bifurcation bifurcation left a comment

Choose a reason for hiding this comment

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

Just to confirm my understanding, we basically have a three-layer cookie here:

  1. Application content (from CookieHandler)
  2. Mint framing (cookie{})
  3. Protection (CookieSource)

Assuming I'm understanding the structure here, this looks fine to me. Couple of nits and one file name change in the comments, then it's ready to merge.

@@ -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
Owner

Choose a reason for hiding this comment

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

Filed #149

cookie_source.go Outdated
@@ -0,0 +1,86 @@
package mint
Copy link
Owner

Choose a reason for hiding this comment

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

For consistency with the other files, this should be cookie-source.go (reserving underscores for _test).

conn.go Outdated
// 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 endoced using the CookieSource.
Copy link
Owner

Choose a reason for hiding this comment

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

Nit: "encoded"

conn.go Outdated
// 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
Copy link
Owner

Choose a reason for hiding this comment

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

Nit: CookieSource doesn't seem like the right name for something whose job is to protect / unprotect cookie values. CookieProtection or something like that?

@marten-seemann
Copy link
Collaborator Author

marten-seemann commented Dec 6, 2017

@bifurcation: I rebased this PR on top of the current master, fixed the typo, and renamed the CookieSource to CookieProtector.

@bifurcation
Copy link
Owner

@marten-seemann Thanks! Merging and moving on to #145 ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants