Skip to content

Commit 64889b7

Browse files
Christopher WoodLekensteyn
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 a2df9ec commit 64889b7

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
@@ -736,6 +736,14 @@ type Config struct {
736736
// used for debugging.
737737
KeyLogWriter io.Writer
738738

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

src/crypto/tls/conn.go

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

src/crypto/tls/handshake_client.go

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

157+
hsTimings := createTLS13ClientHandshakeTimingInfo(c.config.Time)
158+
157159
// This may be a renegotiation handshake, in which case some fields
158160
// need to be reset.
159161
c.didResume = false
@@ -183,6 +185,8 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
183185
return err
184186
}
185187

188+
hsTimings.WriteClientHello = hsTimings.elapsedTime()
189+
186190
msg, err := c.readHandshake()
187191
if err != nil {
188192
return err
@@ -220,6 +224,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
220224
session: session,
221225
earlySecret: earlySecret,
222226
binderKey: binderKey,
227+
hsTimings: hsTimings,
223228
}
224229

225230
// 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,
@@ -104,6 +106,7 @@ func (hs *clientHandshakeStateTLS13) handshake() error {
104106
return err
105107
}
106108

109+
c.handleCFEvent(hs.hsTimings)
107110
atomic.StoreUint32(&c.handshakeStatus, 1)
108111

109112
return nil
@@ -291,6 +294,10 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error {
291294
func (hs *clientHandshakeStateTLS13) processServerHello() error {
292295
c := hs.c
293296

297+
defer func() {
298+
hs.hsTimings.ProcessServerHello = hs.hsTimings.elapsedTime()
299+
}()
300+
294301
if bytes.Equal(hs.serverHello.random, helloRetryRequestRandom) {
295302
c.sendAlert(alertUnexpectedMessage)
296303
return errors.New("tls: server sent two HelloRetryRequest messages")
@@ -406,6 +413,8 @@ func (hs *clientHandshakeStateTLS13) readServerParameters() error {
406413
}
407414
c.clientProtocol = encryptedExtensions.alpnProtocol
408415

416+
hs.hsTimings.ReadEncryptedExtensions = hs.hsTimings.elapsedTime()
417+
409418
return nil
410419
}
411420

@@ -455,6 +464,8 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
455464
}
456465
hs.transcript.Write(certMsg.marshal())
457466

467+
hs.hsTimings.ReadCertificate = hs.hsTimings.elapsedTime()
468+
458469
c.scts = certMsg.certificate.SignedCertificateTimestamps
459470
c.ocspResponse = certMsg.certificate.OCSPStaple
460471

@@ -495,6 +506,8 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
495506

496507
hs.transcript.Write(certVerify.marshal())
497508

509+
hs.hsTimings.ReadCertificateVerify = hs.hsTimings.elapsedTime()
510+
498511
return nil
499512
}
500513

@@ -512,6 +525,8 @@ func (hs *clientHandshakeStateTLS13) readServerFinished() error {
512525
return unexpectedMessageError(finished, msg)
513526
}
514527

528+
hs.hsTimings.ReadServerFinished = hs.hsTimings.elapsedTime()
529+
515530
expectedMAC := hs.suite.finishedHash(c.in.trafficSecret, hs.transcript)
516531
if !hmac.Equal(expectedMAC, finished.verifyData) {
517532
c.sendAlert(alertDecryptError)
@@ -572,6 +587,8 @@ func (hs *clientHandshakeStateTLS13) sendClientCertificate() error {
572587
return err
573588
}
574589

590+
hs.hsTimings.WriteCertificate = hs.hsTimings.elapsedTime()
591+
575592
// If we sent an empty certificate message, skip the CertificateVerify.
576593
if len(cert.Certificate) == 0 {
577594
return nil
@@ -610,6 +627,8 @@ func (hs *clientHandshakeStateTLS13) sendClientCertificate() error {
610627
return err
611628
}
612629

630+
hs.hsTimings.WriteCertificateVerify = hs.hsTimings.elapsedTime()
631+
613632
return nil
614633
}
615634

@@ -625,6 +644,8 @@ func (hs *clientHandshakeStateTLS13) sendClientFinished() error {
625644
return err
626645
}
627646

647+
hs.hsTimings.WriteClientFinished = hs.hsTimings.elapsedTime()
648+
628649
c.out.setTrafficSecret(hs.suite, hs.trafficSecret)
629650

630651
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 {
@@ -82,6 +84,7 @@ func (hs *serverHandshakeStateTLS13) handshake() error {
8284
return err
8385
}
8486

87+
c.handleCFEvent(hs.hsTimings)
8588
atomic.StoreUint32(&c.handshakeStatus, 1)
8689

8790
return nil
@@ -223,6 +226,9 @@ GroupSelection:
223226
}
224227

225228
c.serverName = hs.clientHello.serverName
229+
230+
hs.hsTimings.ProcessClientHello = hs.hsTimings.elapsedTime()
231+
226232
return nil
227233
}
228234

@@ -524,6 +530,8 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
524530
return err
525531
}
526532

533+
hs.hsTimings.WriteServerHello = hs.hsTimings.elapsedTime()
534+
527535
if err := hs.sendDummyChangeCipherSpec(); err != nil {
528536
return err
529537
}
@@ -568,6 +576,8 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
568576
return err
569577
}
570578

579+
hs.hsTimings.WriteEncryptedExtensions = hs.hsTimings.elapsedTime()
580+
571581
return nil
572582
}
573583

@@ -610,6 +620,8 @@ func (hs *serverHandshakeStateTLS13) sendServerCertificate() error {
610620
return err
611621
}
612622

623+
hs.hsTimings.WriteCertificate = hs.hsTimings.elapsedTime()
624+
613625
certVerifyMsg := new(certificateVerifyMsg)
614626
certVerifyMsg.hasSignatureAlgorithm = true
615627
certVerifyMsg.signatureAlgorithm = hs.sigAlg
@@ -642,6 +654,8 @@ func (hs *serverHandshakeStateTLS13) sendServerCertificate() error {
642654
return err
643655
}
644656

657+
hs.hsTimings.WriteCertificateVerify = hs.hsTimings.elapsedTime()
658+
645659
return nil
646660
}
647661

@@ -657,6 +671,8 @@ func (hs *serverHandshakeStateTLS13) sendServerFinished() error {
657671
return err
658672
}
659673

674+
hs.hsTimings.WriteServerFinished = hs.hsTimings.elapsedTime()
675+
660676
// Derive secrets that take context through the server Finished.
661677

662678
hs.masterSecret = hs.suite.extract(nil,
@@ -807,6 +823,8 @@ func (hs *serverHandshakeStateTLS13) readClientCertificate() error {
807823
}
808824
}
809825

826+
hs.hsTimings.ReadCertificate = hs.hsTimings.elapsedTime()
827+
810828
if len(certMsg.certificate.Certificate) != 0 {
811829
msg, err = c.readHandshake()
812830
if err != nil {
@@ -842,6 +860,8 @@ func (hs *serverHandshakeStateTLS13) readClientCertificate() error {
842860
hs.transcript.Write(certVerify.marshal())
843861
}
844862

863+
hs.hsTimings.ReadCertificateVerify = hs.hsTimings.elapsedTime()
864+
845865
// If we waited until the client certificates to send session tickets, we
846866
// are ready to do it now.
847867
if err := hs.sendSessionTickets(); err != nil {
@@ -870,6 +890,8 @@ func (hs *serverHandshakeStateTLS13) readClientFinished() error {
870890
return errors.New("tls: invalid client finished hash")
871891
}
872892

893+
hs.hsTimings.ReadClientFinished = hs.hsTimings.elapsedTime()
894+
873895
c.in.setTrafficSecret(hs.suite, hs.trafficSecret)
874896

875897
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)