Skip to content

Commit 61c44b8

Browse files
author
Anonymous
committed
Session resumption with client certificates
Allow mutual authentication (RequireAndVerifyClientCert/ VerifyClientCertIfGiven)to work with session resumption by recording the certificate expiry time in the session struct
1 parent a1db639 commit 61c44b8

File tree

10 files changed

+350
-48
lines changed

10 files changed

+350
-48
lines changed

Diff for: config.go

+4
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ type Config struct {
128128
// SessionStore is the container to store session for resumption.
129129
SessionStore SessionStore
130130

131+
// PeerCertDisablesSessionResumption prevents session resumption if a client certificate
132+
// is provided, regardless of the state of the session store
133+
PeerCertDisablesSessionResumption bool
134+
131135
// List of application protocols the peer supports, for ALPN
132136
SupportedProtocols []string
133137
}

Diff for: conn.go

+21-20
Original file line numberDiff line numberDiff line change
@@ -155,26 +155,27 @@ func createConn(ctx context.Context, nextConn net.Conn, config *Config, isClient
155155
}
156156

157157
hsCfg := &handshakeConfig{
158-
localPSKCallback: config.PSK,
159-
localPSKIdentityHint: config.PSKIdentityHint,
160-
localCipherSuites: cipherSuites,
161-
localSignatureSchemes: signatureSchemes,
162-
extendedMasterSecret: config.ExtendedMasterSecret,
163-
localSRTPProtectionProfiles: config.SRTPProtectionProfiles,
164-
serverName: serverName,
165-
supportedProtocols: config.SupportedProtocols,
166-
clientAuth: config.ClientAuth,
167-
localCertificates: config.Certificates,
168-
insecureSkipVerify: config.InsecureSkipVerify,
169-
verifyPeerCertificate: config.VerifyPeerCertificate,
170-
rootCAs: config.RootCAs,
171-
clientCAs: config.ClientCAs,
172-
customCipherSuites: config.CustomCipherSuites,
173-
retransmitInterval: workerInterval,
174-
log: logger,
175-
initialEpoch: 0,
176-
keyLogWriter: config.KeyLogWriter,
177-
sessionStore: config.SessionStore,
158+
localPSKCallback: config.PSK,
159+
localPSKIdentityHint: config.PSKIdentityHint,
160+
localCipherSuites: cipherSuites,
161+
localSignatureSchemes: signatureSchemes,
162+
extendedMasterSecret: config.ExtendedMasterSecret,
163+
localSRTPProtectionProfiles: config.SRTPProtectionProfiles,
164+
serverName: serverName,
165+
supportedProtocols: config.SupportedProtocols,
166+
clientAuth: config.ClientAuth,
167+
localCertificates: config.Certificates,
168+
insecureSkipVerify: config.InsecureSkipVerify,
169+
verifyPeerCertificate: config.VerifyPeerCertificate,
170+
rootCAs: config.RootCAs,
171+
clientCAs: config.ClientCAs,
172+
customCipherSuites: config.CustomCipherSuites,
173+
retransmitInterval: workerInterval,
174+
log: logger,
175+
initialEpoch: 0,
176+
keyLogWriter: config.KeyLogWriter,
177+
sessionStore: config.SessionStore,
178+
peerCertDisablesSessionResumption: config.PeerCertDisablesSessionResumption,
178179
}
179180

180181
// rfc5246#section-7.4.3

Diff for: conn_test.go

+274-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"crypto/tls"
1212
"crypto/x509"
1313
"encoding/hex"
14+
"encoding/pem"
1415
"errors"
1516
"fmt"
1617
"io"
@@ -2486,6 +2487,269 @@ func TestSessionResume(t *testing.T) {
24862487
}
24872488
_ = res.c.Close()
24882489
})
2490+
2491+
t.Run("resumed client cert", func(t *testing.T) {
2492+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
2493+
defer cancel()
2494+
2495+
type result struct {
2496+
c *Conn
2497+
err error
2498+
}
2499+
clientRes := make(chan result, 1)
2500+
2501+
commonCert, _ := selfsign.GenerateSelfSignedWithDNS("example.com")
2502+
2503+
certPool := x509.NewCertPool()
2504+
certPool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: commonCert.Certificate[0]}))
2505+
2506+
ss := &memSessStore{}
2507+
2508+
id, _ := hex.DecodeString("9b9fc92255634d9fb109febed42166717bb8ded8c738ba71bc7f2a0d9dae0306")
2509+
secret, _ := hex.DecodeString("2e942a37aca5241deb2295b5fcedac221c7078d2503d2b62aeb48c880d7da73c001238b708559686b9da6e829c05ead7")
2510+
2511+
s := Session{ID: id, Secret: secret}
2512+
2513+
ca, cb := dpipe.Pipe()
2514+
2515+
_ = ss.Set(id, s)
2516+
_ = ss.Set([]byte(ca.RemoteAddr().String()+"_"+commonCert.Leaf.Subject.CommonName), s)
2517+
2518+
go func() {
2519+
config := &Config{
2520+
CipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
2521+
ServerName: commonCert.Leaf.Subject.CommonName,
2522+
SessionStore: ss,
2523+
RootCAs: certPool,
2524+
Certificates: nil, // Client shouldn't need to send a cert to resume a session
2525+
MTU: 100,
2526+
}
2527+
c, err := ClientWithContext(ctx, ca, config)
2528+
clientRes <- result{c, err}
2529+
}()
2530+
2531+
config := &Config{
2532+
CipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
2533+
SessionStore: ss,
2534+
ClientCAs: certPool,
2535+
Certificates: []tls.Certificate{commonCert},
2536+
MTU: 100,
2537+
ClientAuth: RequireAndVerifyClientCert,
2538+
}
2539+
server, err := testServer(ctx, cb, config, true)
2540+
if err != nil {
2541+
t.Fatalf("TestSessionResume: Server failed(%v)", err)
2542+
}
2543+
2544+
actualSessionID := server.ConnectionState().SessionID
2545+
actualMasterSecret := server.ConnectionState().masterSecret
2546+
if !bytes.Equal(actualSessionID, id) {
2547+
t.Errorf("TestSessionResumetion: SessionID Mismatch: expected(%v) actual(%v)", id, actualSessionID)
2548+
}
2549+
if !bytes.Equal(actualMasterSecret, secret) {
2550+
t.Errorf("TestSessionResumetion: masterSecret Mismatch: expected(%v) actual(%v)", secret, actualMasterSecret)
2551+
}
2552+
2553+
defer func() {
2554+
_ = server.Close()
2555+
}()
2556+
2557+
res := <-clientRes
2558+
if res.err != nil {
2559+
t.Fatal(res.err)
2560+
}
2561+
_ = res.c.Close()
2562+
})
2563+
2564+
t.Run("new session client cert", func(t *testing.T) {
2565+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
2566+
defer cancel()
2567+
2568+
type result struct {
2569+
c *Conn
2570+
err error
2571+
}
2572+
clientRes := make(chan result, 1)
2573+
2574+
commonCert, _ := selfsign.GenerateSelfSignedWithDNS("example.com")
2575+
2576+
certPool := x509.NewCertPool()
2577+
certPool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: commonCert.Certificate[0]}))
2578+
2579+
s1 := &memSessStore{}
2580+
s2 := &memSessStore{}
2581+
2582+
ca, cb := dpipe.Pipe()
2583+
go func() {
2584+
config := &Config{
2585+
ServerName: commonCert.Leaf.Subject.CommonName,
2586+
SessionStore: s1,
2587+
RootCAs: certPool,
2588+
Certificates: []tls.Certificate{commonCert},
2589+
}
2590+
c, err := ClientWithContext(ctx, ca, config)
2591+
clientRes <- result{c, err}
2592+
}()
2593+
2594+
config := &Config{
2595+
SessionStore: s2,
2596+
ClientAuth: RequireAndVerifyClientCert,
2597+
ClientCAs: certPool,
2598+
Certificates: []tls.Certificate{commonCert},
2599+
}
2600+
server, err := testServer(ctx, cb, config, false)
2601+
if err != nil {
2602+
t.Fatalf("TestSessionResumetion: Server failed(%v)", err)
2603+
}
2604+
2605+
actualSessionID := server.ConnectionState().SessionID
2606+
actualMasterSecret := server.ConnectionState().masterSecret
2607+
ss, _ := s2.Get(actualSessionID)
2608+
if !bytes.Equal(actualMasterSecret, ss.Secret) {
2609+
t.Errorf("TestSessionResumetion: masterSecret Mismatch: expected/actual:\n(%v)\n(%v)", ss.Secret, actualMasterSecret)
2610+
}
2611+
2612+
if ss.Expiry.Unix() != commonCert.Leaf.NotAfter.Unix() {
2613+
t.Errorf("TestSessionResumption: expected server session store to contain certificate expiry")
2614+
}
2615+
2616+
defer func() {
2617+
_ = server.Close()
2618+
}()
2619+
2620+
res := <-clientRes
2621+
if res.err != nil {
2622+
t.Fatal(res.err)
2623+
}
2624+
cs, _ := s1.Get([]byte(ca.RemoteAddr().String() + "_" + commonCert.Leaf.Subject.CommonName))
2625+
if !bytes.Equal(actualMasterSecret, cs.Secret) {
2626+
t.Errorf("TestSessionResumetion: masterSecret Mismatch: expected/actual\n(%v)\n(%v)", cs.Secret, actualMasterSecret)
2627+
}
2628+
2629+
if cs.Expiry.Unix() != commonCert.Leaf.NotAfter.Unix() {
2630+
t.Errorf("TestSessionResumption: expected client session store to contain certificate expiry")
2631+
}
2632+
2633+
_ = res.c.Close()
2634+
})
2635+
2636+
t.Run("expire client cert session", func(t *testing.T) {
2637+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
2638+
defer cancel()
2639+
2640+
type result struct {
2641+
c *Conn
2642+
err error
2643+
}
2644+
clientRes := make(chan result, 1)
2645+
2646+
commonCert, _ := selfsign.GenerateSelfSignedWithDNS("example.com")
2647+
2648+
certPool := x509.NewCertPool()
2649+
certPool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: commonCert.Certificate[0]}))
2650+
2651+
ss := &memSessStore{}
2652+
2653+
id, _ := hex.DecodeString("9b9fc92255634d9fb109febed42166717bb8ded8c738ba71bc7f2a0d9dae0306")
2654+
secret, _ := hex.DecodeString("2e942a37aca5241deb2295b5fcedac221c7078d2503d2b62aeb48c880d7da73c001238b708559686b9da6e829c05ead7")
2655+
2656+
oldClientSessionTime := time.Now().Add(time.Hour)
2657+
clientSession := Session{
2658+
ID: id,
2659+
Secret: secret,
2660+
Expiry: oldClientSessionTime,
2661+
}
2662+
2663+
expiredServerSession := Session{
2664+
ID: id,
2665+
Secret: secret,
2666+
Expiry: time.Now().Add(-time.Hour), // server should treat this as expired session and force a new cert verification
2667+
}
2668+
2669+
ca, cb := dpipe.Pipe()
2670+
2671+
_ = ss.Set(id, expiredServerSession)
2672+
_ = ss.Set([]byte(ca.RemoteAddr().String()+"_"+commonCert.Leaf.Subject.CommonName), clientSession)
2673+
2674+
go func() {
2675+
config := &Config{
2676+
CipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
2677+
ServerName: commonCert.Leaf.Subject.CommonName,
2678+
SessionStore: ss,
2679+
RootCAs: certPool,
2680+
Certificates: []tls.Certificate{commonCert},
2681+
MTU: 1200, // MTU must be able to fit cert chain in one packet
2682+
}
2683+
c, err := ClientWithContext(ctx, ca, config)
2684+
clientRes <- result{c, err}
2685+
}()
2686+
2687+
config := &Config{
2688+
CipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
2689+
SessionStore: ss,
2690+
ClientCAs: certPool,
2691+
Certificates: []tls.Certificate{commonCert},
2692+
MTU: 1200,
2693+
ClientAuth: RequireAndVerifyClientCert,
2694+
}
2695+
server, err := testServer(ctx, cb, config, false)
2696+
if err != nil {
2697+
t.Fatalf("TestSessionResume: Server failed(%v)", err)
2698+
}
2699+
2700+
actualSessionID := server.ConnectionState().SessionID
2701+
actualMasterSecret := server.ConnectionState().masterSecret
2702+
if bytes.Equal(actualSessionID, id) {
2703+
t.Errorf("TestSessionResumption: SessionID Mismatch: expected new session ID(%v) actual(%v)", id, actualSessionID)
2704+
}
2705+
2706+
if bytes.Equal(actualMasterSecret, secret) {
2707+
t.Errorf("TestSessionResumption: masterSecret Mismatch: expected new master secret (%v) actual(%v)", secret, actualMasterSecret)
2708+
}
2709+
2710+
defer func() {
2711+
_ = server.Close()
2712+
}()
2713+
2714+
res := <-clientRes
2715+
if res.err != nil {
2716+
t.Fatal(res.err)
2717+
}
2718+
2719+
_, ok := ss.Map.Load(hex.EncodeToString(expiredServerSession.ID))
2720+
if ok {
2721+
t.Errorf("expected server to have deleted session")
2722+
}
2723+
2724+
cSess, ok := ss.Map.Load(hex.EncodeToString([]byte(ca.RemoteAddr().String() + "_" + commonCert.Leaf.Subject.CommonName)))
2725+
if !ok {
2726+
t.Errorf("expected client store to have cached new session ID")
2727+
}
2728+
2729+
newClientSession := cSess.(Session)
2730+
if bytes.Equal(secret, newClientSession.Secret) {
2731+
t.Errorf("expected : expected client session store to contain new master secret (%v) actual(%v)", secret, newClientSession.Secret)
2732+
}
2733+
2734+
if newClientSession.Expiry.Unix() == oldClientSessionTime.Unix() {
2735+
t.Errorf("expected new client session to have updated")
2736+
}
2737+
2738+
if newClientSession.Expiry.Unix() != commonCert.Leaf.NotAfter.Unix() {
2739+
t.Errorf("expected new client session to expire with client cert")
2740+
}
2741+
2742+
sSess, ok := ss.Map.Load(hex.EncodeToString(newClientSession.ID))
2743+
if !ok {
2744+
t.Errorf("expected server store to have cached new client session ID")
2745+
}
2746+
newServerSession := sSess.(Session)
2747+
2748+
if !bytes.Equal(newServerSession.Secret, newClientSession.Secret) {
2749+
t.Errorf("expected : expected session store to contain new shared secret (%v) actual(%v)", newServerSession.Secret, newClientSession.Secret)
2750+
}
2751+
_ = res.c.Close()
2752+
})
24892753
}
24902754

24912755
type memSessStore struct {
@@ -2507,7 +2771,16 @@ func (ms *memSessStore) Get(key []byte) (Session, error) {
25072771
return Session{}, nil
25082772
}
25092773

2510-
return v.(Session), nil
2774+
session := v.(Session)
2775+
if session.Expiry.IsZero() {
2776+
return session, nil
2777+
}
2778+
2779+
if time.Now().After(session.Expiry) {
2780+
_ = ms.Del(key)
2781+
return Session{}, nil
2782+
}
2783+
return session, nil
25112784
}
25122785

25132786
func (ms *memSessStore) Del(key []byte) error {

Diff for: crypto.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -184,28 +184,30 @@ func loadCerts(rawCertificates [][]byte) ([]*x509.Certificate, error) {
184184
return certs, nil
185185
}
186186

187-
func verifyClientCert(rawCertificates [][]byte, roots *x509.CertPool) (chains [][]*x509.Certificate, err error) {
187+
func verifyClientCert(rawCertificates [][]byte, roots *x509.CertPool) (chains [][]*x509.Certificate, expiry time.Time, err error) {
188188
certificate, err := loadCerts(rawCertificates)
189189
if err != nil {
190-
return nil, err
190+
return nil, time.Time{}, err
191191
}
192192
intermediateCAPool := x509.NewCertPool()
193193
for _, cert := range certificate[1:] {
194194
intermediateCAPool.AddCert(cert)
195195
}
196+
196197
opts := x509.VerifyOptions{
197198
Roots: roots,
198199
CurrentTime: time.Now(),
199200
Intermediates: intermediateCAPool,
200201
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
201202
}
202-
return certificate[0].Verify(opts)
203+
chains, err = certificate[0].Verify(opts)
204+
return chains, certificate[0].NotAfter, err
203205
}
204206

205-
func verifyServerCert(rawCertificates [][]byte, roots *x509.CertPool, serverName string) (chains [][]*x509.Certificate, err error) {
207+
func verifyServerCert(rawCertificates [][]byte, roots *x509.CertPool, serverName string) (chains [][]*x509.Certificate, expiry time.Time, err error) {
206208
certificate, err := loadCerts(rawCertificates)
207209
if err != nil {
208-
return nil, err
210+
return nil, time.Time{}, err
209211
}
210212
intermediateCAPool := x509.NewCertPool()
211213
for _, cert := range certificate[1:] {
@@ -217,5 +219,6 @@ func verifyServerCert(rawCertificates [][]byte, roots *x509.CertPool, serverName
217219
DNSName: serverName,
218220
Intermediates: intermediateCAPool,
219221
}
220-
return certificate[0].Verify(opts)
222+
chains, err = certificate[0].Verify(opts)
223+
return chains, certificate[0].NotAfter, err
221224
}

0 commit comments

Comments
 (0)