From 1f8a47af02b8e14756eabee9ed14151304715738 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Mon, 5 Aug 2019 06:49:50 +0700 Subject: [PATCH] implement a ClientSessionCache that can save application data --- .golangci.yml | 1 + internal/handshake/client_session_cache.go | 83 +++++++++++++++++++ .../handshake/client_session_cache_test.go | 38 +++++++++ internal/handshake/client_session_state.go | 23 +++++ internal/handshake/qtls.go | 45 ++-------- internal/handshake/unsafe.go | 3 + 6 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 internal/handshake/client_session_cache.go create mode 100644 internal/handshake/client_session_cache_test.go create mode 100644 internal/handshake/client_session_state.go diff --git a/.golangci.yml b/.golangci.yml index 52c0a3657..19ac1599f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,6 @@ run: skip-files: + - internal/handshake/client_session_state.go - internal/handshake/unsafe_test.go linters-settings: diff --git a/internal/handshake/client_session_cache.go b/internal/handshake/client_session_cache.go new file mode 100644 index 000000000..3470e7ab0 --- /dev/null +++ b/internal/handshake/client_session_cache.go @@ -0,0 +1,83 @@ +package handshake + +import ( + "crypto/tls" + "encoding/asn1" + "unsafe" + + "github.com/marten-seemann/qtls" +) + +type nonceField struct { + Nonce []byte + AppData []byte +} + +type clientSessionCache struct { + tls.ClientSessionCache + + getAppData func() []byte + setAppData func([]byte) +} + +func newClientSessionCache(cache tls.ClientSessionCache, get func() []byte, set func([]byte)) *clientSessionCache { + return &clientSessionCache{ + ClientSessionCache: cache, + getAppData: get, + setAppData: set, + } +} + +var _ qtls.ClientSessionCache = &clientSessionCache{} + +func (c *clientSessionCache) Get(sessionKey string) (*qtls.ClientSessionState, bool) { + sess, ok := c.ClientSessionCache.Get(sessionKey) + if sess == nil { + return nil, ok + } + // qtls.ClientSessionState is identical to the tls.ClientSessionState. + // In order to allow users of quic-go to use a tls.Config, + // we need this workaround to use the ClientSessionCache. + // In unsafe.go we check that the two structs are actually identical. + tlsSessBytes := (*[unsafe.Sizeof(*sess)]byte)(unsafe.Pointer(sess))[:] + var session clientSessionState + sessBytes := (*[unsafe.Sizeof(session)]byte)(unsafe.Pointer(&session))[:] + copy(sessBytes, tlsSessBytes) + var nf nonceField + if _, err := asn1.Unmarshal(session.nonce, &nf); err != nil { + return nil, false + } + c.setAppData(nf.AppData) + session.nonce = nf.Nonce + var qtlsSession qtls.ClientSessionState + qtlsSessBytes := (*[unsafe.Sizeof(qtlsSession)]byte)(unsafe.Pointer(&qtlsSession))[:] + copy(qtlsSessBytes, sessBytes) + return &qtlsSession, ok +} + +func (c *clientSessionCache) Put(sessionKey string, cs *qtls.ClientSessionState) { + if cs == nil { + c.ClientSessionCache.Put(sessionKey, nil) + return + } + // qtls.ClientSessionState is identical to the tls.ClientSessionState. + // In order to allow users of quic-go to use a tls.Config, + // we need this workaround to use the ClientSessionCache. + // In unsafe.go we check that the two structs are actually identical. + qtlsSessBytes := (*[unsafe.Sizeof(*cs)]byte)(unsafe.Pointer(cs))[:] + var session clientSessionState + sessBytes := (*[unsafe.Sizeof(session)]byte)(unsafe.Pointer(&session))[:] + copy(sessBytes, qtlsSessBytes) + nonce, err := asn1.Marshal(nonceField{ + Nonce: session.nonce, + AppData: c.getAppData(), + }) + if err != nil { // marshaling + panic(err) + } + session.nonce = nonce + var tlsSession tls.ClientSessionState + tlsSessBytes := (*[unsafe.Sizeof(tlsSession)]byte)(unsafe.Pointer(&tlsSession))[:] + copy(tlsSessBytes, sessBytes) + c.ClientSessionCache.Put(sessionKey, &tlsSession) +} diff --git a/internal/handshake/client_session_cache_test.go b/internal/handshake/client_session_cache_test.go new file mode 100644 index 000000000..75761455d --- /dev/null +++ b/internal/handshake/client_session_cache_test.go @@ -0,0 +1,38 @@ +package handshake + +import ( + "crypto/tls" + + "github.com/marten-seemann/qtls" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ClientSessionCache", func() { + var ( + csc *clientSessionCache + get, set chan []byte + ) + + BeforeEach(func() { + get = make(chan []byte, 100) + set = make(chan []byte, 100) + csc = newClientSessionCache( + tls.NewLRUClientSessionCache(100), + func() []byte { return <-get }, + func(b []byte) { set <- b }, + ) + }) + + It("puts and gets", func() { + get <- []byte("foobar") + csc.Put("localhost", &qtls.ClientSessionState{}) + Expect(set).To(BeEmpty()) + + state, ok := csc.Get("localhost") + Expect(ok).To(BeTrue()) + Expect(state).ToNot(BeNil()) + Expect(set).To(Receive(Equal([]byte("foobar")))) + }) +}) diff --git a/internal/handshake/client_session_state.go b/internal/handshake/client_session_state.go new file mode 100644 index 000000000..6424461c8 --- /dev/null +++ b/internal/handshake/client_session_state.go @@ -0,0 +1,23 @@ +package handshake + +import ( + "crypto/x509" + "time" +) + +// copied from crypto/tls +// Needs to be in a separate file so golangci-lint can skip it. +type clientSessionState struct { + sessionTicket []uint8 // Encrypted ticket used for session resumption with server + vers uint16 // SSL/TLS version negotiated for the session + cipherSuite uint16 // Ciphersuite negotiated for the session + masterSecret []byte // Full handshake MasterSecret, or TLS 1.3 resumption_master_secret + serverCertificates []*x509.Certificate // Certificate chain presented by the server + verifiedChains [][]*x509.Certificate // Certificate chains we built for verification + receivedAt time.Time // When the session ticket was received from the server + + // TLS 1.3 fields. + nonce []byte // Ticket nonce sent by the server, to derive PSK + useBy time.Time // Expiration of the ticket lifetime as set by the server + ageAdd uint32 // Random obfuscation factor for sending the ticket age +} diff --git a/internal/handshake/qtls.go b/internal/handshake/qtls.go index 9b8ec31f8..2528bf170 100644 --- a/internal/handshake/qtls.go +++ b/internal/handshake/qtls.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "net" "time" - "unsafe" "github.com/marten-seemann/qtls" ) @@ -28,44 +27,6 @@ func (c *conn) SetReadDeadline(time.Time) error { return nil } func (c *conn) SetWriteDeadline(time.Time) error { return nil } func (c *conn) SetDeadline(time.Time) error { return nil } -type clientSessionCache struct { - tls.ClientSessionCache -} - -var _ qtls.ClientSessionCache = &clientSessionCache{} - -func (c *clientSessionCache) Get(sessionKey string) (*qtls.ClientSessionState, bool) { - sess, ok := c.ClientSessionCache.Get(sessionKey) - if sess == nil { - return nil, ok - } - // qtls.ClientSessionState is identical to the tls.ClientSessionState. - // In order to allow users of quic-go to use a tls.Config, - // we need this workaround to use the ClientSessionCache. - // In unsafe.go we check that the two structs are actually identical. - usess := (*[unsafe.Sizeof(*sess)]byte)(unsafe.Pointer(sess))[:] - var session qtls.ClientSessionState - usession := (*[unsafe.Sizeof(session)]byte)(unsafe.Pointer(&session))[:] - copy(usession, usess) - return &session, ok -} - -func (c *clientSessionCache) Put(sessionKey string, cs *qtls.ClientSessionState) { - if cs == nil { - c.ClientSessionCache.Put(sessionKey, nil) - return - } - // qtls.ClientSessionState is identical to the tls.ClientSessionState. - // In order to allow users of quic-go to use a tls.Config, - // we need this workaround to use the ClientSessionCache. - // In unsafe.go we check that the two structs are actually identical. - usess := (*[unsafe.Sizeof(*cs)]byte)(unsafe.Pointer(cs))[:] - var session tls.ClientSessionState - usession := (*[unsafe.Sizeof(session)]byte)(unsafe.Pointer(&session))[:] - copy(usession, usess) - c.ClientSessionCache.Put(sessionKey, &session) -} - func tlsConfigToQtlsConfig( c *tls.Config, recordLayer qtls.RecordLayer, @@ -103,7 +64,11 @@ func tlsConfigToQtlsConfig( } var csc qtls.ClientSessionCache if c.ClientSessionCache != nil { - csc = &clientSessionCache{c.ClientSessionCache} + csc = newClientSessionCache( + c.ClientSessionCache, + func() []byte { return nil }, + func([]byte) {}, + ) } conf := &qtls.Config{ Rand: c.Rand, diff --git a/internal/handshake/unsafe.go b/internal/handshake/unsafe.go index fb051aebf..8c0d3c41d 100644 --- a/internal/handshake/unsafe.go +++ b/internal/handshake/unsafe.go @@ -19,6 +19,9 @@ func init() { if !structsEqual(&tls.ClientSessionState{}, &qtls.ClientSessionState{}) { panic("qtls.ClientSessionState not compatible with tls.ClientSessionState") } + if !structsEqual(&tls.ClientSessionState{}, &clientSessionState{}) { + panic("clientSessionState not compatible with tls.ClientSessionState") + } } func structsEqual(a, b interface{}) bool {