mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Manage TLS session tickets manually in QUIC peering (#62896)
* Manage TLS session tickets manually in QUIC peering * Remove static assert on return types
This commit is contained in:
@@ -18,6 +18,7 @@ package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -133,15 +135,17 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
}
|
||||
|
||||
getClientCAs := cfg.GetClientCAs
|
||||
str := new(sessionTicketRefresher)
|
||||
tlsConfig.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
clientCAs, err := getClientCAs(chi)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
utils.RefreshTLSConfigTickets(tlsConfig)
|
||||
c := tlsConfig.Clone()
|
||||
c.GetConfigForClient = nil
|
||||
c.ClientCAs = clientCAs
|
||||
c.SetSessionTicketKeys(str.getSessionTicketKeys())
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -477,3 +481,66 @@ func (r *replayStore) add(nonce uint64, now time.Time) (added bool) {
|
||||
r.currentSet[nonce] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
// sessionTicketRefresherState contains a non-empty slice of randomly generated
|
||||
// session tickets together with a slice of creation times that has the same
|
||||
// length and is in descending order.
|
||||
type sessionTicketRefresherState struct {
|
||||
tickets [][32]byte
|
||||
created []time.Time
|
||||
}
|
||||
|
||||
// sessionTicketRefresher generates and rotates TLS session tickets suitable for
|
||||
// use with [tls.Config.SetSessionTicketKeys].
|
||||
type sessionTicketRefresher struct {
|
||||
// state is either nil or a pointer to a valid (i.e. non-zero) state struct.
|
||||
state atomic.Pointer[sessionTicketRefresherState]
|
||||
// mu should be held before calculating and storing a new state, to avoid
|
||||
// extra work.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// getSessionTicketKeys returns a slice of session tickets, potentially adding a
|
||||
// new one and rotating out expired old ones. A new ticket is created when the
|
||||
// most recent is a day old, and tickets are rotated away after a week or so.
|
||||
func (s *sessionTicketRefresher) getSessionTicketKeys() [][32]byte {
|
||||
// same logic for the fast path as in crypto/tls, including the fact that
|
||||
// the oldest ticket can actually be up to 8 days old since we don't check
|
||||
// for expiry in the fast path
|
||||
if state := s.state.Load(); state != nil && time.Since(state.created[0]) < 24*time.Hour {
|
||||
return state.tickets
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
state := s.state.Load()
|
||||
now := time.Now()
|
||||
if state != nil && now.Sub(state.created[0]) < 24*time.Hour {
|
||||
return state.tickets
|
||||
}
|
||||
|
||||
if state == nil {
|
||||
state = new(sessionTicketRefresherState)
|
||||
}
|
||||
newState := &sessionTicketRefresherState{
|
||||
tickets: make([][32]byte, 0, 1+len(state.tickets)),
|
||||
created: make([]time.Time, 0, 1+len(state.tickets)),
|
||||
}
|
||||
|
||||
var newTicket [32]byte
|
||||
rand.Read(newTicket[:])
|
||||
newState.tickets = append(newState.tickets, newTicket)
|
||||
newState.created = append(newState.created, now)
|
||||
|
||||
for i := range state.tickets {
|
||||
if now.Sub(state.created[i]) >= 7*24*time.Hour {
|
||||
break
|
||||
}
|
||||
newState.tickets = append(newState.tickets, state.tickets[i])
|
||||
newState.created = append(newState.created, state.created[i])
|
||||
}
|
||||
s.state.Store(newState)
|
||||
|
||||
return newState.tickets
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ package quic
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -42,3 +44,53 @@ func TestDialResponseOKEncoding(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dialResponseOK, string(sizedResp))
|
||||
}
|
||||
|
||||
func TestSessionTicketRefresher(t *testing.T) {
|
||||
synctest.Test(t, testSessionTicketRefresher)
|
||||
}
|
||||
func testSessionTicketRefresher(t *testing.T) {
|
||||
str := new(sessionTicketRefresher)
|
||||
|
||||
t1 := time.Now()
|
||||
k1 := str.getSessionTicketKeys()
|
||||
require.Len(t, k1, 1)
|
||||
requireSameSlice(t, k1, str.getSessionTicketKeys())
|
||||
|
||||
time.Sleep(time.Hour)
|
||||
|
||||
// the first key is still valid, no keys are added
|
||||
|
||||
requireSameSlice(t, k1, str.getSessionTicketKeys())
|
||||
|
||||
requireSameSlice(t, k1, str.state.Load().tickets)
|
||||
require.True(t, t1.Equal(str.state.Load().created[0]))
|
||||
|
||||
time.Sleep(24 * time.Hour)
|
||||
|
||||
// the first key is valid but old, a new fresh key is generated
|
||||
|
||||
t2 := time.Now()
|
||||
k2 := str.getSessionTicketKeys()
|
||||
|
||||
require.Len(t, k2, 2)
|
||||
requireSameSlice(t, k2, str.state.Load().tickets)
|
||||
require.True(t, t2.Equal(str.state.Load().created[0]))
|
||||
require.True(t, t1.Equal(str.state.Load().created[1]))
|
||||
|
||||
time.Sleep(6 * 24 * time.Hour)
|
||||
|
||||
// the first key should be rotated out
|
||||
|
||||
t3 := time.Now()
|
||||
k3 := str.getSessionTicketKeys()
|
||||
|
||||
require.Len(t, k3, 2)
|
||||
requireSameSlice(t, k3, str.state.Load().tickets)
|
||||
require.True(t, t3.Equal(str.state.Load().created[0]))
|
||||
require.True(t, t2.Equal(str.state.Load().created[1]))
|
||||
}
|
||||
|
||||
func requireSameSlice[S ~[]T, T any](t *testing.T, expected, actual S) {
|
||||
require.Len(t, actual, len(expected))
|
||||
require.Same(t, &expected[0], &actual[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user