Skip to content

Commit ee1b22d

Browse files
cjpattonbwesterb
authored andcommitted
crypto/tls: implement draft-ietf-tls-esni-13
Adds support for draft 13 of the Encrypted ClientHello (ECH) extension for TLS. This requires CIRCL to implement draft 08 or later of the HPKE specification (draft-irtf-cfrg-hpke-08). Adds a CFEvent for reporting when ECH is offered or greased by the client, when ECH is accepted or rejected by the server, and when the outer SNI doesn't match the public name of the ECH config. Missing ECH features: * Record-level padding. * Proper validation of the public name by the client. * Retry after rejection. * PSKs are disabled when ECH is accepted.
1 parent 41259f6 commit ee1b22d

38 files changed

+7336
-37
lines changed

src/crypto/tls/alert.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const (
5858
alertUnknownPSKIdentity alert = 115
5959
alertCertificateRequired alert = 116
6060
alertNoApplicationProtocol alert = 120
61+
alertECHRequired alert = 121
6162
)
6263

6364
var alertText = map[alert]string{
@@ -94,6 +95,7 @@ var alertText = map[alert]string{
9495
alertUnknownPSKIdentity: "unknown PSK identity",
9596
alertCertificateRequired: "certificate required",
9697
alertNoApplicationProtocol: "no application protocol",
98+
alertECHRequired: "ECH required",
9799
}
98100

99101
func (e alert) String() string {

src/crypto/tls/cfevent.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,71 @@ func createTLS13ServerHandshakeTimingInfo(timerFunc func() time.Time) CFEventTLS
9797
}
9898
}
9999

100+
const (
101+
// Constants for ECH status events.
102+
echStatusBypassed = 1 + iota
103+
echStatusInner
104+
echStatusOuter
105+
)
106+
107+
// CFEventECHClientStatus is emitted once it is known whether the client
108+
// bypassed, offered, or greased ECH.
109+
type CFEventECHClientStatus int
110+
111+
// Bypassed returns true if the client bypassed ECH.
112+
func (e CFEventECHClientStatus) Bypassed() bool {
113+
return e == echStatusBypassed
114+
}
115+
116+
// Offered returns true if the client offered ECH.
117+
func (e CFEventECHClientStatus) Offered() bool {
118+
return e == echStatusInner
119+
}
120+
121+
// Greased returns true if the client greased ECH.
122+
func (e CFEventECHClientStatus) Greased() bool {
123+
return e == echStatusOuter
124+
}
125+
126+
// Name is required by the CFEvent interface.
127+
func (e CFEventECHClientStatus) Name() string {
128+
return "ech client status"
129+
}
130+
131+
// CFEventECHServerStatus is emitted once it is known whether the client
132+
// bypassed, offered, or greased ECH.
133+
type CFEventECHServerStatus int
134+
135+
// Bypassed returns true if the client bypassed ECH.
136+
func (e CFEventECHServerStatus) Bypassed() bool {
137+
return e == echStatusBypassed
138+
}
139+
140+
// Accepted returns true if the client offered ECH.
141+
func (e CFEventECHServerStatus) Accepted() bool {
142+
return e == echStatusInner
143+
}
144+
145+
// Rejected returns true if the client greased ECH.
146+
func (e CFEventECHServerStatus) Rejected() bool {
147+
return e == echStatusOuter
148+
}
149+
150+
// Name is required by the CFEvent interface.
151+
func (e CFEventECHServerStatus) Name() string {
152+
return "ech server status"
153+
}
154+
155+
// CFEventECHPublicNameMismatch is emitted if the outer SNI does not match
156+
// match the public name of the ECH configuration. Note that we do not record
157+
// the outer SNI in order to avoid collecting this potentially sensitive data.
158+
type CFEventECHPublicNameMismatch struct{}
159+
160+
// Name is required by the CFEvent interface.
161+
func (e CFEventECHPublicNameMismatch) Name() string {
162+
return "ech public name does not match outer sni"
163+
}
164+
100165
// For backwards compatibility.
101166
type CFEventTLS13NegotiatedKEX = CFEventTLSNegotiatedNamedKEX
102167

src/crypto/tls/common.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ const (
123123
extensionKeyShare uint16 = 51
124124
extensionQUICTransportParameters uint16 = 57
125125
extensionRenegotiationInfo uint16 = 0xff01
126+
extensionECH uint16 = 0xfe0d // draft-ietf-tls-esni-13
127+
extensionECHOuterExtensions uint16 = 0xfd00 // draft-ietf-tls-esni-13
126128
)
127129

128130
// TLS signaling cipher suite values
@@ -245,6 +247,45 @@ const (
245247
// include downgrade canaries even if it's using its highers supported version.
246248
var testingOnlyForceDowngradeCanary bool
247249

250+
// testingTriggerHRR causes the server to intentionally trigger a
251+
// HelloRetryRequest (HRR). This is useful for testing new TLS features that
252+
// change the HRR codepath.
253+
var testingTriggerHRR bool
254+
255+
// testingECHTriggerBypassAfterHRR causes the client to bypass ECH after HRR.
256+
// If available, the client will offer ECH in the first CH only.
257+
var testingECHTriggerBypassAfterHRR bool
258+
259+
// testingECHTriggerBypassBeforeHRR causes the client to bypass ECH before HRR.
260+
// The client will offer ECH in the second CH only.
261+
var testingECHTriggerBypassBeforeHRR bool
262+
263+
// testingECHIllegalHandleAfterHRR causes the client to illegally change the ECH
264+
// extension after HRR.
265+
var testingECHIllegalHandleAfterHRR bool
266+
267+
// testingECHTriggerPayloadDecryptError causes the client to to send an
268+
// inauthentic payload.
269+
var testingECHTriggerPayloadDecryptError bool
270+
271+
// testingECHOuterExtMany causes a client to incorporate a sequence of
272+
// outer extensions into the ClientHelloInner when it offers the ECH extension.
273+
// The "key_share" extension is the only incorporated extension by default.
274+
var testingECHOuterExtMany bool
275+
276+
// testingECHOuterExtNone causes a client to not use the "outer_extension"
277+
// mechanism for ECH. The "key_shares" extension is incorporated by default.
278+
var testingECHOuterExtNone bool
279+
280+
// testingECHOuterExtIncorrectOrder causes the client to send the
281+
// "outer_extension" extension in the wrong order when offering the ECH
282+
// extension.
283+
var testingECHOuterExtIncorrectOrder bool
284+
285+
// testingECHOuterExtIllegal causes the client to send in its
286+
// "outer_extension" extension the codepoint for the ECH extension.
287+
var testingECHOuterExtIllegal bool
288+
248289
// ConnectionState records basic TLS details about the connection.
249290
type ConnectionState struct {
250291
// Version is the TLS version used by the connection (e.g. VersionTLS12).
@@ -313,6 +354,14 @@ type ConnectionState struct {
313354
// resumed connections that don't support Extended Master Secret (RFC 7627).
314355
TLSUnique []byte
315356

357+
// ECHAccepted is set if the ECH extension was offered by the client and
358+
// accepted by the server.
359+
ECHAccepted bool
360+
361+
// ECHOffered is set if the ECH extension is present in the ClientHello.
362+
// This means the client has offered ECH or sent GREASE ECH.
363+
ECHOffered bool
364+
316365
// ekm is a closure exposed via ExportKeyingMaterial.
317366
ekm func(label string, context []byte, length int) ([]byte, error)
318367
}
@@ -723,7 +772,8 @@ type Config struct {
723772

724773
// SessionTicketsDisabled may be set to true to disable session ticket and
725774
// PSK (resumption) support. Note that on clients, session ticket support is
726-
// also disabled if ClientSessionCache is nil.
775+
// also disabled if ClientSessionCache is nil. On clients or servers,
776+
// support is disabled if the ECH extension is enabled.
727777
SessionTicketsDisabled bool
728778

729779
// SessionTicketKey is used by TLS servers to provide session resumption.
@@ -816,6 +866,23 @@ type Config struct {
816866
// used for debugging.
817867
KeyLogWriter io.Writer
818868

869+
// ECHEnabled determines whether the ECH extension is enabled for this
870+
// connection.
871+
ECHEnabled bool
872+
873+
// ClientECHConfigs are the parameters used by the client when it offers the
874+
// ECH extension. If ECH is enabled, a suitable configuration is found, and
875+
// the client supports TLS 1.3, then it will offer ECH in this handshake.
876+
// Otherwise, if ECH is enabled, it will send a dummy ECH extension.
877+
ClientECHConfigs []ECHConfig
878+
879+
// ServerECHProvider is the ECH provider used by the client-facing server
880+
// for the ECH extension. If the client offers ECH and TLS 1.3 is
881+
// negotiated, then the provider is used to compute the HPKE context
882+
// (draft-irtf-cfrg-hpke-07), which in turn is used to decrypt the extension
883+
// payload.
884+
ServerECHProvider ECHProvider
885+
819886
// SupportDelegatedCredential is true if the client or server is willing
820887
// to negotiate the delegated credential extension.
821888
// This can only be used with TLS 1.3.
@@ -912,6 +979,9 @@ func (c *Config) Clone() *Config {
912979
Renegotiation: c.Renegotiation,
913980
KeyLogWriter: c.KeyLogWriter,
914981
SupportDelegatedCredential: c.SupportDelegatedCredential,
982+
ECHEnabled: c.ECHEnabled,
983+
ClientECHConfigs: c.ClientECHConfigs,
984+
ServerECHProvider: c.ServerECHProvider,
915985
sessionTicketKeys: c.sessionTicketKeys,
916986
autoSessionTicketKeys: c.autoSessionTicketKeys,
917987
}
@@ -1102,6 +1172,17 @@ func (c *Config) supportedVersions(isClient bool) []uint16 {
11021172
return versions
11031173
}
11041174

1175+
func (c *Config) supportedVersionsFromMin(isClient bool, minVersion uint16) []uint16 {
1176+
versions := c.supportedVersions(isClient)
1177+
filteredVersions := versions[:0]
1178+
for _, v := range versions {
1179+
if v >= minVersion {
1180+
filteredVersions = append(filteredVersions, v)
1181+
}
1182+
}
1183+
return filteredVersions
1184+
}
1185+
11051186
func (c *Config) maxSupportedVersion(isClient bool) uint16 {
11061187
supportedVersions := c.supportedVersions(isClient)
11071188
if len(supportedVersions) == 0 {

src/crypto/tls/conn.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"sync"
2121
"sync/atomic"
2222
"time"
23+
24+
"github.com/cloudflare/circl/hpke"
2325
)
2426

2527
// A Conn represents a secured connection.
@@ -126,6 +128,20 @@ type Conn struct {
126128
// cfEventHandler is called at several points during the handshake if
127129
// set. See also CFEventHandlerContextKey.
128130
cfEventHandler func(event CFEvent)
131+
132+
// State used for the ECH extension.
133+
ech struct {
134+
sealer hpke.Sealer // The client's HPKE context
135+
opener hpke.Opener // The server's HPKE context
136+
137+
// The state shared by the client and server.
138+
offered bool // Client offered ECH
139+
greased bool // Client greased ECH
140+
accepted bool // Server accepted ECH
141+
retryConfigs []byte // The retry configurations
142+
configId uint8 // The ECH config id
143+
maxNameLen int // maximum_name_len indicated by the ECH config
144+
}
129145
}
130146

131147
// Access to net.Conn methods.
@@ -727,6 +743,12 @@ func (c *Conn) readRecordOrCCS(expectChangeCipherSpec bool) error {
727743
return c.in.setErrorLocked(io.EOF)
728744
}
729745
if c.vers == VersionTLS13 {
746+
if !c.isClient && c.ech.greased && alert(data[1]) == alertECHRequired {
747+
// This condition indicates that the client intended to offer
748+
// ECH, but did not use a known ECH config.
749+
c.ech.offered = true
750+
c.ech.greased = false
751+
}
730752
return c.in.setErrorLocked(&net.OpError{Op: "remote error", Err: alert(data[1])})
731753
}
732754
switch data[0] {
@@ -1435,6 +1457,29 @@ func (c *Conn) Close() error {
14351457
if err := c.conn.Close(); err != nil {
14361458
return err
14371459
}
1460+
1461+
// Resolve ECH status.
1462+
if !c.isClient && c.config.MaxVersion < VersionTLS13 {
1463+
c.handleCFEvent(CFEventECHServerStatus(echStatusBypassed))
1464+
} else if !c.ech.offered {
1465+
if !c.ech.greased {
1466+
c.handleCFEvent(CFEventECHClientStatus(echStatusBypassed))
1467+
} else {
1468+
c.handleCFEvent(CFEventECHClientStatus(echStatusOuter))
1469+
}
1470+
} else {
1471+
c.handleCFEvent(CFEventECHClientStatus(echStatusInner))
1472+
if !c.ech.accepted {
1473+
if len(c.ech.retryConfigs) > 0 {
1474+
c.handleCFEvent(CFEventECHServerStatus(echStatusOuter))
1475+
} else {
1476+
c.handleCFEvent(CFEventECHServerStatus(echStatusBypassed))
1477+
}
1478+
} else {
1479+
c.handleCFEvent(CFEventECHServerStatus(echStatusInner))
1480+
}
1481+
}
1482+
14381483
return alertErr
14391484
}
14401485

@@ -1621,6 +1666,8 @@ func (c *Conn) connectionStateLocked() ConnectionState {
16211666
}
16221667
state.SignedCertificateTimestamps = c.scts
16231668
state.OCSPResponse = c.ocspResponse
1669+
state.ECHAccepted = c.ech.accepted
1670+
state.ECHOffered = c.ech.offered || c.ech.greased
16241671
if (!c.didResume || c.extMasterSecret) && c.vers != VersionTLS13 {
16251672
if c.clientFinishedIsFirst {
16261673
state.TLSUnique = c.clientFinished[:]

0 commit comments

Comments
 (0)