Skip to content

Commit 7286b3a

Browse files
Christopher Woodbwesterb
authored andcommitted
crypto/tls: add Config.CFEventHandler to expose intra-handshake timing
Add basic support for handshake metrics: * Adds a `CFEventHandler()` callback to `Config` which can be called at various points during the handshake to respond to various events. * Use this callback to expose client and server intra-handshake state machine durations, respectively. Each event records elapsed timestamps (durations) for relevant events during the course of a connection, such as reading and writing handshake messages of interest. This will be useful for recording intra-stack costs of TLS extensions such as ECH and KEMTLS.
1 parent e7294eb commit 7286b3a

File tree

9 files changed

+290
-1
lines changed

9 files changed

+290
-1
lines changed

src/crypto/tls/common.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,14 @@ type Config struct {
737737
// used for debugging.
738738
KeyLogWriter io.Writer
739739

740+
// CFEventHandler, if set, is called by the client and server at various
741+
// points during the handshake to handle specific events. This is used
742+
// primarily for collecting metrics.
743+
//
744+
// NOTE: This feature is used to implement Cloudflare-internal features.
745+
// This feature is unstable and applications MUST NOT depend on it.
746+
CFEventHandler func(event CFEvent)
747+
740748
// CFControl is used to pass additional TLS configuration information to
741749
// HTTP requests via ConnectionState.
742750
//
@@ -834,6 +842,7 @@ func (c *Config) Clone() *Config {
834842
DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled,
835843
Renegotiation: c.Renegotiation,
836844
KeyLogWriter: c.KeyLogWriter,
845+
CFEventHandler: c.CFEventHandler,
837846
CFControl: c.CFControl,
838847
sessionTicketKeys: c.sessionTicketKeys,
839848
autoSessionTicketKeys: c.autoSessionTicketKeys,

src/crypto/tls/conn.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,3 +1542,9 @@ func (c *Conn) VerifyHostname(host string) error {
15421542
func (c *Conn) handshakeComplete() bool {
15431543
return atomic.LoadUint32(&c.handshakeStatus) == 1
15441544
}
1545+
1546+
func (c *Conn) handleCFEvent(event CFEvent) {
1547+
if c.config.CFEventHandler != nil {
1548+
c.config.CFEventHandler(event)
1549+
}
1550+
}

src/crypto/tls/handshake_client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
149149
c.config = defaultConfig()
150150
}
151151

152+
hsTimings := createTLS13ClientHandshakeTimingInfo(c.config.Time)
153+
152154
// This may be a renegotiation handshake, in which case some fields
153155
// need to be reset.
154156
c.didResume = false
@@ -178,6 +180,8 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
178180
return err
179181
}
180182

183+
hsTimings.WriteClientHello = hsTimings.elapsedTime()
184+
181185
msg, err := c.readHandshake()
182186
if err != nil {
183187
return err
@@ -215,6 +219,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
215219
session: session,
216220
earlySecret: earlySecret,
217221
binderKey: binderKey,
222+
hsTimings: hsTimings,
218223
}
219224

220225
// In TLS 1.3, session tickets are delivered after the handshake.

src/crypto/tls/handshake_client_tls13.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ type clientHandshakeStateTLS13 struct {
3434
transcript hash.Hash
3535
masterSecret []byte
3636
trafficSecret []byte // client_application_traffic_secret_0
37+
38+
hsTimings CFEventTLS13ClientHandshakeTimingInfo
3739
}
3840

3941
// handshake requires hs.c, hs.hello, hs.serverHello, hs.ecdheParams, and,
@@ -100,6 +102,7 @@ func (hs *clientHandshakeStateTLS13) handshake() error {
100102
return err
101103
}
102104

105+
c.handleCFEvent(hs.hsTimings)
103106
atomic.StoreUint32(&c.handshakeStatus, 1)
104107

105108
return nil
@@ -287,6 +290,10 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error {
287290
func (hs *clientHandshakeStateTLS13) processServerHello() error {
288291
c := hs.c
289292

293+
defer func() {
294+
hs.hsTimings.ProcessServerHello = hs.hsTimings.elapsedTime()
295+
}()
296+
290297
if bytes.Equal(hs.serverHello.random, helloRetryRequestRandom) {
291298
c.sendAlert(alertUnexpectedMessage)
292299
return errors.New("tls: server sent two HelloRetryRequest messages")
@@ -402,6 +409,8 @@ func (hs *clientHandshakeStateTLS13) readServerParameters() error {
402409
}
403410
c.clientProtocol = encryptedExtensions.alpnProtocol
404411

412+
hs.hsTimings.ReadEncryptedExtensions = hs.hsTimings.elapsedTime()
413+
405414
return nil
406415
}
407416

@@ -451,6 +460,8 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
451460
}
452461
hs.transcript.Write(certMsg.marshal())
453462

463+
hs.hsTimings.ReadCertificate = hs.hsTimings.elapsedTime()
464+
454465
c.scts = certMsg.certificate.SignedCertificateTimestamps
455466
c.ocspResponse = certMsg.certificate.OCSPStaple
456467

@@ -491,6 +502,8 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
491502

492503
hs.transcript.Write(certVerify.marshal())
493504

505+
hs.hsTimings.ReadCertificateVerify = hs.hsTimings.elapsedTime()
506+
494507
return nil
495508
}
496509

@@ -508,6 +521,8 @@ func (hs *clientHandshakeStateTLS13) readServerFinished() error {
508521
return unexpectedMessageError(finished, msg)
509522
}
510523

524+
hs.hsTimings.ReadServerFinished = hs.hsTimings.elapsedTime()
525+
511526
expectedMAC := hs.suite.finishedHash(c.in.trafficSecret, hs.transcript)
512527
if !hmac.Equal(expectedMAC, finished.verifyData) {
513528
c.sendAlert(alertDecryptError)
@@ -568,6 +583,8 @@ func (hs *clientHandshakeStateTLS13) sendClientCertificate() error {
568583
return err
569584
}
570585

586+
hs.hsTimings.WriteCertificate = hs.hsTimings.elapsedTime()
587+
571588
// If we sent an empty certificate message, skip the CertificateVerify.
572589
if len(cert.Certificate) == 0 {
573590
return nil
@@ -606,6 +623,8 @@ func (hs *clientHandshakeStateTLS13) sendClientCertificate() error {
606623
return err
607624
}
608625

626+
hs.hsTimings.WriteCertificateVerify = hs.hsTimings.elapsedTime()
627+
609628
return nil
610629
}
611630

@@ -621,6 +640,8 @@ func (hs *clientHandshakeStateTLS13) sendClientFinished() error {
621640
return err
622641
}
623642

643+
hs.hsTimings.WriteClientFinished = hs.hsTimings.elapsedTime()
644+
624645
c.out.setTrafficSecret(hs.suite, hs.trafficSecret)
625646

626647
if !c.config.SessionTicketsDisabled && c.config.ClientSessionCache != nil {

src/crypto/tls/handshake_server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (c *Conn) serverHandshake(ctx context.Context) error {
5252
c: c,
5353
ctx: ctx,
5454
clientHello: clientHello,
55+
hsTimings: createTLS13ServerHandshakeTimingInfo(c.config.Time),
5556
}
5657
return hs.handshake()
5758
}

src/crypto/tls/handshake_server_tls13.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ type serverHandshakeStateTLS13 struct {
4040
trafficSecret []byte // client_application_traffic_secret_0
4141
transcript hash.Hash
4242
clientFinished []byte
43+
44+
hsTimings CFEventTLS13ServerHandshakeTimingInfo
4345
}
4446

4547
func (hs *serverHandshakeStateTLS13) handshake() error {
@@ -78,6 +80,7 @@ func (hs *serverHandshakeStateTLS13) handshake() error {
7880
return err
7981
}
8082

83+
c.handleCFEvent(hs.hsTimings)
8184
atomic.StoreUint32(&c.handshakeStatus, 1)
8285

8386
return nil
@@ -219,6 +222,9 @@ GroupSelection:
219222
}
220223

221224
c.serverName = hs.clientHello.serverName
225+
226+
hs.hsTimings.ProcessClientHello = hs.hsTimings.elapsedTime()
227+
222228
return nil
223229
}
224230

@@ -520,6 +526,8 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
520526
return err
521527
}
522528

529+
hs.hsTimings.WriteServerHello = hs.hsTimings.elapsedTime()
530+
523531
if err := hs.sendDummyChangeCipherSpec(); err != nil {
524532
return err
525533
}
@@ -564,6 +572,8 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
564572
return err
565573
}
566574

575+
hs.hsTimings.WriteEncryptedExtensions = hs.hsTimings.elapsedTime()
576+
567577
return nil
568578
}
569579

@@ -606,6 +616,8 @@ func (hs *serverHandshakeStateTLS13) sendServerCertificate() error {
606616
return err
607617
}
608618

619+
hs.hsTimings.WriteCertificate = hs.hsTimings.elapsedTime()
620+
609621
certVerifyMsg := new(certificateVerifyMsg)
610622
certVerifyMsg.hasSignatureAlgorithm = true
611623
certVerifyMsg.signatureAlgorithm = hs.sigAlg
@@ -638,6 +650,8 @@ func (hs *serverHandshakeStateTLS13) sendServerCertificate() error {
638650
return err
639651
}
640652

653+
hs.hsTimings.WriteCertificateVerify = hs.hsTimings.elapsedTime()
654+
641655
return nil
642656
}
643657

@@ -653,6 +667,8 @@ func (hs *serverHandshakeStateTLS13) sendServerFinished() error {
653667
return err
654668
}
655669

670+
hs.hsTimings.WriteServerFinished = hs.hsTimings.elapsedTime()
671+
656672
// Derive secrets that take context through the server Finished.
657673

658674
hs.masterSecret = hs.suite.extract(nil,
@@ -803,6 +819,8 @@ func (hs *serverHandshakeStateTLS13) readClientCertificate() error {
803819
}
804820
}
805821

822+
hs.hsTimings.ReadCertificate = hs.hsTimings.elapsedTime()
823+
806824
if len(certMsg.certificate.Certificate) != 0 {
807825
msg, err = c.readHandshake()
808826
if err != nil {
@@ -838,6 +856,8 @@ func (hs *serverHandshakeStateTLS13) readClientCertificate() error {
838856
hs.transcript.Write(certVerify.marshal())
839857
}
840858

859+
hs.hsTimings.ReadCertificateVerify = hs.hsTimings.elapsedTime()
860+
841861
// If we waited until the client certificates to send session tickets, we
842862
// are ready to do it now.
843863
if err := hs.sendSessionTickets(); err != nil {
@@ -866,6 +886,8 @@ func (hs *serverHandshakeStateTLS13) readClientFinished() error {
866886
return errors.New("tls: invalid client finished hash")
867887
}
868888

889+
hs.hsTimings.ReadClientFinished = hs.hsTimings.elapsedTime()
890+
869891
c.in.setTrafficSecret(hs.suite, hs.trafficSecret)
870892

871893
return nil

src/crypto/tls/metrics_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code
2+
// is governed by a BSD-style license that can be found in the LICENSE file.
3+
4+
package tls
5+
6+
import (
7+
"crypto/x509"
8+
"fmt"
9+
"io"
10+
"io/ioutil"
11+
"testing"
12+
"time"
13+
)
14+
15+
type testTimingInfo struct {
16+
serverTimingInfo CFEventTLS13ServerHandshakeTimingInfo
17+
clientTimingInfo CFEventTLS13ClientHandshakeTimingInfo
18+
}
19+
20+
func (t testTimingInfo) isMonotonicallyIncreasing() bool {
21+
serverIsMonotonicallyIncreasing :=
22+
t.serverTimingInfo.ProcessClientHello < t.serverTimingInfo.WriteServerHello &&
23+
t.serverTimingInfo.WriteServerHello < t.serverTimingInfo.WriteEncryptedExtensions &&
24+
t.serverTimingInfo.WriteEncryptedExtensions < t.serverTimingInfo.WriteCertificate &&
25+
t.serverTimingInfo.WriteCertificate < t.serverTimingInfo.WriteCertificateVerify &&
26+
t.serverTimingInfo.WriteCertificateVerify < t.serverTimingInfo.WriteServerFinished &&
27+
t.serverTimingInfo.WriteServerFinished < t.serverTimingInfo.ReadCertificate &&
28+
t.serverTimingInfo.ReadCertificate < t.serverTimingInfo.ReadCertificateVerify &&
29+
t.serverTimingInfo.ReadCertificateVerify < t.serverTimingInfo.ReadClientFinished
30+
31+
clientIsMonotonicallyIncreasing :=
32+
t.clientTimingInfo.WriteClientHello < t.clientTimingInfo.ProcessServerHello &&
33+
t.clientTimingInfo.ProcessServerHello < t.clientTimingInfo.ReadEncryptedExtensions &&
34+
t.clientTimingInfo.ReadEncryptedExtensions < t.clientTimingInfo.ReadCertificate &&
35+
t.clientTimingInfo.ReadCertificate < t.clientTimingInfo.ReadCertificateVerify &&
36+
t.clientTimingInfo.ReadCertificateVerify < t.clientTimingInfo.ReadServerFinished &&
37+
t.clientTimingInfo.ReadServerFinished < t.clientTimingInfo.WriteCertificate &&
38+
t.clientTimingInfo.WriteCertificate < t.clientTimingInfo.WriteCertificateVerify &&
39+
t.clientTimingInfo.WriteCertificateVerify < t.clientTimingInfo.WriteClientFinished
40+
41+
return (serverIsMonotonicallyIncreasing && clientIsMonotonicallyIncreasing)
42+
}
43+
44+
func (r *testTimingInfo) eventHandler(event CFEvent) {
45+
switch e := event.(type) {
46+
case CFEventTLS13ServerHandshakeTimingInfo:
47+
r.serverTimingInfo = e
48+
case CFEventTLS13ClientHandshakeTimingInfo:
49+
r.clientTimingInfo = e
50+
}
51+
}
52+
53+
func runHandshake(t *testing.T, clientConfig, serverConfig *Config) (timingState testTimingInfo, err error) {
54+
const sentinel = "SENTINEL\n"
55+
c, s := localPipe(t)
56+
errChan := make(chan error)
57+
58+
clientConfig.CFEventHandler = timingState.eventHandler
59+
serverConfig.CFEventHandler = timingState.eventHandler
60+
61+
go func() {
62+
cli := Client(c, clientConfig)
63+
err := cli.Handshake()
64+
if err != nil {
65+
errChan <- fmt.Errorf("client: %v", err)
66+
c.Close()
67+
return
68+
}
69+
defer cli.Close()
70+
buf, err := ioutil.ReadAll(cli)
71+
if err != nil {
72+
t.Errorf("failed to call cli.Read: %v", err)
73+
}
74+
if got := string(buf); got != sentinel {
75+
t.Errorf("read %q from TLS connection, but expected %q", got, sentinel)
76+
}
77+
errChan <- nil
78+
}()
79+
80+
server := Server(s, serverConfig)
81+
err = server.Handshake()
82+
if err == nil {
83+
if _, err := io.WriteString(server, sentinel); err != nil {
84+
t.Errorf("failed to call server.Write: %v", err)
85+
}
86+
if err := server.Close(); err != nil {
87+
t.Errorf("failed to call server.Close: %v", err)
88+
}
89+
err = <-errChan
90+
} else {
91+
s.Close()
92+
<-errChan
93+
}
94+
95+
return
96+
}
97+
98+
func TestTLS13HandshakeTiming(t *testing.T) {
99+
issuer, err := x509.ParseCertificate(testRSACertificateIssuer)
100+
if err != nil {
101+
panic(err)
102+
}
103+
rootCAs := x509.NewCertPool()
104+
rootCAs.AddCert(issuer)
105+
106+
const serverName = "example.golang"
107+
108+
baseConfig := &Config{
109+
Time: time.Now,
110+
Rand: zeroSource{},
111+
Certificates: make([]Certificate, 1),
112+
MaxVersion: VersionTLS13,
113+
RootCAs: rootCAs,
114+
ClientCAs: rootCAs,
115+
ClientAuth: RequireAndVerifyClientCert,
116+
ServerName: serverName,
117+
}
118+
baseConfig.Certificates[0].Certificate = [][]byte{testRSACertificate}
119+
baseConfig.Certificates[0].PrivateKey = testRSAPrivateKey
120+
121+
clientConfig := baseConfig.Clone()
122+
serverConfig := baseConfig.Clone()
123+
124+
ts, err := runHandshake(t, clientConfig, serverConfig)
125+
if err != nil {
126+
t.Fatalf("Handshake failed: %v", err)
127+
}
128+
129+
if !ts.isMonotonicallyIncreasing() {
130+
t.Fatalf("Timing information is not monotonic")
131+
}
132+
}

0 commit comments

Comments
 (0)