crypto/tls: improved 0-RTT QUIC API

Add synchronous management of stored sessions to QUICConn.

This adds QUICStoreSession and QUICResumeSession events,
permitting a QUIC implementation to handle session resumption
as part of its regular event loop processing.

Fixes #63691

Change-Id: I9fe16207cc1986eac084869675bc36e227cbf3f0
Reviewed-on: https://go-review.googlesource.com/c/go/+/536935
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Marten Seemann <martenseemann@gmail.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
Damien Neil 2023-10-22 16:31:59 -04:00
parent a81de4f2e0
commit 833bba2d07
7 changed files with 254 additions and 30 deletions

View file

@ -5,6 +5,7 @@
package tls
import (
"bytes"
"context"
"errors"
"reflect"
@ -12,12 +13,15 @@ import (
)
type testQUICConn struct {
t *testing.T
conn *QUICConn
readSecret map[QUICEncryptionLevel]suiteSecret
writeSecret map[QUICEncryptionLevel]suiteSecret
gotParams []byte
complete bool
t *testing.T
conn *QUICConn
readSecret map[QUICEncryptionLevel]suiteSecret
writeSecret map[QUICEncryptionLevel]suiteSecret
ticketOpts QUICSessionTicketOptions
onResumeSession func(*SessionState)
gotParams []byte
earlyDataRejected bool
complete bool
}
func newTestQUICClient(t *testing.T, config *Config) *testQUICConn {
@ -48,7 +52,7 @@ type suiteSecret struct {
}
func (q *testQUICConn) setReadSecret(level QUICEncryptionLevel, suite uint16, secret []byte) {
if _, ok := q.writeSecret[level]; !ok {
if _, ok := q.writeSecret[level]; !ok && level != QUICEncryptionLevelEarly {
q.t.Errorf("SetReadSecret for level %v called before SetWriteSecret", level)
}
if level == QUICEncryptionLevelApplication && !q.complete {
@ -61,7 +65,9 @@ func (q *testQUICConn) setReadSecret(level QUICEncryptionLevel, suite uint16, se
q.readSecret = map[QUICEncryptionLevel]suiteSecret{}
}
switch level {
case QUICEncryptionLevelHandshake, QUICEncryptionLevelApplication:
case QUICEncryptionLevelHandshake,
QUICEncryptionLevelEarly,
QUICEncryptionLevelApplication:
q.readSecret[level] = suiteSecret{suite, secret}
default:
q.t.Errorf("SetReadSecret for unexpected level %v", level)
@ -76,7 +82,9 @@ func (q *testQUICConn) setWriteSecret(level QUICEncryptionLevel, suite uint16, s
q.writeSecret = map[QUICEncryptionLevel]suiteSecret{}
}
switch level {
case QUICEncryptionLevelHandshake, QUICEncryptionLevelApplication:
case QUICEncryptionLevelHandshake,
QUICEncryptionLevelEarly,
QUICEncryptionLevelApplication:
q.writeSecret[level] = suiteSecret{suite, secret}
default:
q.t.Errorf("SetWriteSecret for unexpected level %v", level)
@ -128,11 +136,16 @@ func runTestQUICConnection(ctx context.Context, cli, srv *testQUICConn, onEvent
case QUICHandshakeDone:
a.complete = true
if a == srv {
opts := QUICSessionTicketOptions{}
if err := srv.conn.SendSessionTicket(opts); err != nil {
if err := srv.conn.SendSessionTicket(srv.ticketOpts); err != nil {
return err
}
}
case QUICResumeSession:
if a.onResumeSession != nil {
a.onResumeSession(e.SessionState)
}
case QUICRejectedEarlyData:
a.earlyDataRejected = true
}
if e.Kind != QUICNoEvent {
idleCount = 0
@ -487,3 +500,113 @@ func TestQUICCanceledWaitingForTransportParams(t *testing.T) {
t.Errorf("conn.Close() = %v, want alertCloseNotify", err)
}
}
func TestQUICEarlyData(t *testing.T) {
clientConfig := testConfig.Clone()
clientConfig.MinVersion = VersionTLS13
clientConfig.ClientSessionCache = NewLRUClientSessionCache(1)
clientConfig.ServerName = "example.go.dev"
clientConfig.NextProtos = []string{"h3"}
serverConfig := testConfig.Clone()
serverConfig.MinVersion = VersionTLS13
serverConfig.NextProtos = []string{"h3"}
cli := newTestQUICClient(t, clientConfig)
cli.conn.SetTransportParameters(nil)
srv := newTestQUICServer(t, serverConfig)
srv.conn.SetTransportParameters(nil)
srv.ticketOpts.EarlyData = true
if err := runTestQUICConnection(context.Background(), cli, srv, nil); err != nil {
t.Fatalf("error during first connection handshake: %v", err)
}
if cli.conn.ConnectionState().DidResume {
t.Errorf("first connection unexpectedly used session resumption")
}
cli2 := newTestQUICClient(t, clientConfig)
cli2.conn.SetTransportParameters(nil)
srv2 := newTestQUICServer(t, serverConfig)
srv2.conn.SetTransportParameters(nil)
if err := runTestQUICConnection(context.Background(), cli2, srv2, nil); err != nil {
t.Fatalf("error during second connection handshake: %v", err)
}
if !cli2.conn.ConnectionState().DidResume {
t.Errorf("second connection did not use session resumption")
}
cliSecret := cli2.writeSecret[QUICEncryptionLevelEarly]
if cliSecret.secret == nil {
t.Errorf("client did not receive early data write secret")
}
srvSecret := srv2.readSecret[QUICEncryptionLevelEarly]
if srvSecret.secret == nil {
t.Errorf("server did not receive early data read secret")
}
if cliSecret.suite != srvSecret.suite || !bytes.Equal(cliSecret.secret, srvSecret.secret) {
t.Errorf("client early data secret does not match server")
}
}
func TestQUICEarlyDataDeclined(t *testing.T) {
t.Run("server", func(t *testing.T) {
testQUICEarlyDataDeclined(t, true)
})
t.Run("client", func(t *testing.T) {
testQUICEarlyDataDeclined(t, false)
})
}
func testQUICEarlyDataDeclined(t *testing.T, server bool) {
clientConfig := testConfig.Clone()
clientConfig.MinVersion = VersionTLS13
clientConfig.ClientSessionCache = NewLRUClientSessionCache(1)
clientConfig.ServerName = "example.go.dev"
clientConfig.NextProtos = []string{"h3"}
serverConfig := testConfig.Clone()
serverConfig.MinVersion = VersionTLS13
serverConfig.NextProtos = []string{"h3"}
cli := newTestQUICClient(t, clientConfig)
cli.conn.SetTransportParameters(nil)
srv := newTestQUICServer(t, serverConfig)
srv.conn.SetTransportParameters(nil)
srv.ticketOpts.EarlyData = true
if err := runTestQUICConnection(context.Background(), cli, srv, nil); err != nil {
t.Fatalf("error during first connection handshake: %v", err)
}
if cli.conn.ConnectionState().DidResume {
t.Errorf("first connection unexpectedly used session resumption")
}
cli2 := newTestQUICClient(t, clientConfig)
cli2.conn.SetTransportParameters(nil)
srv2 := newTestQUICServer(t, serverConfig)
srv2.conn.SetTransportParameters(nil)
declineEarlyData := func(state *SessionState) {
state.EarlyData = false
}
if server {
srv2.onResumeSession = declineEarlyData
} else {
cli2.onResumeSession = declineEarlyData
}
if err := runTestQUICConnection(context.Background(), cli2, srv2, nil); err != nil {
t.Fatalf("error during second connection handshake: %v", err)
}
if !cli2.conn.ConnectionState().DidResume {
t.Errorf("second connection did not use session resumption")
}
_, cliEarlyData := cli2.writeSecret[QUICEncryptionLevelEarly]
if server {
if !cliEarlyData {
t.Errorf("client did not receive early data write secret")
}
if !cli2.earlyDataRejected {
t.Errorf("client did not receive QUICEarlyDataRejected")
}
}
if _, srvEarlyData := srv2.readSecret[QUICEncryptionLevelEarly]; srvEarlyData {
t.Errorf("server received early data read secret")
}
}