Skip to content

Commit

Permalink
crypto/tls: implement draft-ietf-tls-esni-13
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cjpatton authored and bwesterb committed Sep 6, 2023
1 parent 865f546 commit 9ea1834
Show file tree
Hide file tree
Showing 38 changed files with 7,336 additions and 37 deletions.
2 changes: 2 additions & 0 deletions src/crypto/tls/alert.go
Expand Up @@ -58,6 +58,7 @@ const (
alertUnknownPSKIdentity alert = 115
alertCertificateRequired alert = 116
alertNoApplicationProtocol alert = 120
alertECHRequired alert = 121
)

var alertText = map[alert]string{
Expand Down Expand Up @@ -94,6 +95,7 @@ var alertText = map[alert]string{
alertUnknownPSKIdentity: "unknown PSK identity",
alertCertificateRequired: "certificate required",
alertNoApplicationProtocol: "no application protocol",
alertECHRequired: "ECH required",
}

func (e alert) String() string {
Expand Down
65 changes: 65 additions & 0 deletions src/crypto/tls/cfevent.go
Expand Up @@ -97,6 +97,71 @@ func createTLS13ServerHandshakeTimingInfo(timerFunc func() time.Time) CFEventTLS
}
}

const (
// Constants for ECH status events.
echStatusBypassed = 1 + iota
echStatusInner
echStatusOuter
)

// CFEventECHClientStatus is emitted once it is known whether the client
// bypassed, offered, or greased ECH.
type CFEventECHClientStatus int

// Bypassed returns true if the client bypassed ECH.
func (e CFEventECHClientStatus) Bypassed() bool {
return e == echStatusBypassed
}

// Offered returns true if the client offered ECH.
func (e CFEventECHClientStatus) Offered() bool {
return e == echStatusInner
}

// Greased returns true if the client greased ECH.
func (e CFEventECHClientStatus) Greased() bool {
return e == echStatusOuter
}

// Name is required by the CFEvent interface.
func (e CFEventECHClientStatus) Name() string {
return "ech client status"
}

// CFEventECHServerStatus is emitted once it is known whether the client
// bypassed, offered, or greased ECH.
type CFEventECHServerStatus int

// Bypassed returns true if the client bypassed ECH.
func (e CFEventECHServerStatus) Bypassed() bool {
return e == echStatusBypassed
}

// Accepted returns true if the client offered ECH.
func (e CFEventECHServerStatus) Accepted() bool {
return e == echStatusInner
}

// Rejected returns true if the client greased ECH.
func (e CFEventECHServerStatus) Rejected() bool {
return e == echStatusOuter
}

// Name is required by the CFEvent interface.
func (e CFEventECHServerStatus) Name() string {
return "ech server status"
}

// CFEventECHPublicNameMismatch is emitted if the outer SNI does not match
// match the public name of the ECH configuration. Note that we do not record
// the outer SNI in order to avoid collecting this potentially sensitive data.
type CFEventECHPublicNameMismatch struct{}

// Name is required by the CFEvent interface.
func (e CFEventECHPublicNameMismatch) Name() string {
return "ech public name does not match outer sni"
}

// For backwards compatibility.
type CFEventTLS13NegotiatedKEX = CFEventTLSNegotiatedNamedKEX

Expand Down
83 changes: 82 additions & 1 deletion src/crypto/tls/common.go
Expand Up @@ -123,6 +123,8 @@ const (
extensionKeyShare uint16 = 51
extensionQUICTransportParameters uint16 = 57
extensionRenegotiationInfo uint16 = 0xff01
extensionECH uint16 = 0xfe0d // draft-ietf-tls-esni-13
extensionECHOuterExtensions uint16 = 0xfd00 // draft-ietf-tls-esni-13
)

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

// testingTriggerHRR causes the server to intentionally trigger a
// HelloRetryRequest (HRR). This is useful for testing new TLS features that
// change the HRR codepath.
var testingTriggerHRR bool

// testingECHTriggerBypassAfterHRR causes the client to bypass ECH after HRR.
// If available, the client will offer ECH in the first CH only.
var testingECHTriggerBypassAfterHRR bool

// testingECHTriggerBypassBeforeHRR causes the client to bypass ECH before HRR.
// The client will offer ECH in the second CH only.
var testingECHTriggerBypassBeforeHRR bool

// testingECHIllegalHandleAfterHRR causes the client to illegally change the ECH
// extension after HRR.
var testingECHIllegalHandleAfterHRR bool

// testingECHTriggerPayloadDecryptError causes the client to to send an
// inauthentic payload.
var testingECHTriggerPayloadDecryptError bool

// testingECHOuterExtMany causes a client to incorporate a sequence of
// outer extensions into the ClientHelloInner when it offers the ECH extension.
// The "key_share" extension is the only incorporated extension by default.
var testingECHOuterExtMany bool

// testingECHOuterExtNone causes a client to not use the "outer_extension"
// mechanism for ECH. The "key_shares" extension is incorporated by default.
var testingECHOuterExtNone bool

// testingECHOuterExtIncorrectOrder causes the client to send the
// "outer_extension" extension in the wrong order when offering the ECH
// extension.
var testingECHOuterExtIncorrectOrder bool

// testingECHOuterExtIllegal causes the client to send in its
// "outer_extension" extension the codepoint for the ECH extension.
var testingECHOuterExtIllegal bool

// ConnectionState records basic TLS details about the connection.
type ConnectionState struct {
// Version is the TLS version used by the connection (e.g. VersionTLS12).
Expand Down Expand Up @@ -313,6 +354,14 @@ type ConnectionState struct {
// resumed connections that don't support Extended Master Secret (RFC 7627).
TLSUnique []byte

// ECHAccepted is set if the ECH extension was offered by the client and
// accepted by the server.
ECHAccepted bool

// ECHOffered is set if the ECH extension is present in the ClientHello.
// This means the client has offered ECH or sent GREASE ECH.
ECHOffered bool

// ekm is a closure exposed via ExportKeyingMaterial.
ekm func(label string, context []byte, length int) ([]byte, error)
}
Expand Down Expand Up @@ -723,7 +772,8 @@ type Config struct {

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

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

// ECHEnabled determines whether the ECH extension is enabled for this
// connection.
ECHEnabled bool

// ClientECHConfigs are the parameters used by the client when it offers the
// ECH extension. If ECH is enabled, a suitable configuration is found, and
// the client supports TLS 1.3, then it will offer ECH in this handshake.
// Otherwise, if ECH is enabled, it will send a dummy ECH extension.
ClientECHConfigs []ECHConfig

// ServerECHProvider is the ECH provider used by the client-facing server
// for the ECH extension. If the client offers ECH and TLS 1.3 is
// negotiated, then the provider is used to compute the HPKE context
// (draft-irtf-cfrg-hpke-07), which in turn is used to decrypt the extension
// payload.
ServerECHProvider ECHProvider

// SupportDelegatedCredential is true if the client or server is willing
// to negotiate the delegated credential extension.
// This can only be used with TLS 1.3.
Expand Down Expand Up @@ -912,6 +979,9 @@ func (c *Config) Clone() *Config {
Renegotiation: c.Renegotiation,
KeyLogWriter: c.KeyLogWriter,
SupportDelegatedCredential: c.SupportDelegatedCredential,
ECHEnabled: c.ECHEnabled,
ClientECHConfigs: c.ClientECHConfigs,
ServerECHProvider: c.ServerECHProvider,
sessionTicketKeys: c.sessionTicketKeys,
autoSessionTicketKeys: c.autoSessionTicketKeys,
}
Expand Down Expand Up @@ -1102,6 +1172,17 @@ func (c *Config) supportedVersions(isClient bool) []uint16 {
return versions
}

func (c *Config) supportedVersionsFromMin(isClient bool, minVersion uint16) []uint16 {
versions := c.supportedVersions(isClient)
filteredVersions := versions[:0]
for _, v := range versions {
if v >= minVersion {
filteredVersions = append(filteredVersions, v)
}
}
return filteredVersions
}

func (c *Config) maxSupportedVersion(isClient bool) uint16 {
supportedVersions := c.supportedVersions(isClient)
if len(supportedVersions) == 0 {
Expand Down
47 changes: 47 additions & 0 deletions src/crypto/tls/conn.go
Expand Up @@ -20,6 +20,8 @@ import (
"sync"
"sync/atomic"
"time"

"github.com/cloudflare/circl/hpke"
)

// A Conn represents a secured connection.
Expand Down Expand Up @@ -126,6 +128,20 @@ type Conn struct {
// cfEventHandler is called at several points during the handshake if
// set. See also CFEventHandlerContextKey.
cfEventHandler func(event CFEvent)

// State used for the ECH extension.
ech struct {
sealer hpke.Sealer // The client's HPKE context
opener hpke.Opener // The server's HPKE context

// The state shared by the client and server.
offered bool // Client offered ECH
greased bool // Client greased ECH
accepted bool // Server accepted ECH
retryConfigs []byte // The retry configurations
configId uint8 // The ECH config id
maxNameLen int // maximum_name_len indicated by the ECH config
}
}

// Access to net.Conn methods.
Expand Down Expand Up @@ -727,6 +743,12 @@ func (c *Conn) readRecordOrCCS(expectChangeCipherSpec bool) error {
return c.in.setErrorLocked(io.EOF)
}
if c.vers == VersionTLS13 {
if !c.isClient && c.ech.greased && alert(data[1]) == alertECHRequired {
// This condition indicates that the client intended to offer
// ECH, but did not use a known ECH config.
c.ech.offered = true
c.ech.greased = false
}
return c.in.setErrorLocked(&net.OpError{Op: "remote error", Err: alert(data[1])})
}
switch data[0] {
Expand Down Expand Up @@ -1435,6 +1457,29 @@ func (c *Conn) Close() error {
if err := c.conn.Close(); err != nil {
return err
}

// Resolve ECH status.
if !c.isClient && c.config.MaxVersion < VersionTLS13 {
c.handleCFEvent(CFEventECHServerStatus(echStatusBypassed))
} else if !c.ech.offered {
if !c.ech.greased {
c.handleCFEvent(CFEventECHClientStatus(echStatusBypassed))
} else {
c.handleCFEvent(CFEventECHClientStatus(echStatusOuter))
}
} else {
c.handleCFEvent(CFEventECHClientStatus(echStatusInner))
if !c.ech.accepted {
if len(c.ech.retryConfigs) > 0 {
c.handleCFEvent(CFEventECHServerStatus(echStatusOuter))
} else {
c.handleCFEvent(CFEventECHServerStatus(echStatusBypassed))
}
} else {
c.handleCFEvent(CFEventECHServerStatus(echStatusInner))
}
}

return alertErr
}

Expand Down Expand Up @@ -1626,6 +1671,8 @@ func (c *Conn) connectionStateLocked() ConnectionState {
}
state.SignedCertificateTimestamps = c.scts
state.OCSPResponse = c.ocspResponse
state.ECHAccepted = c.ech.accepted
state.ECHOffered = c.ech.offered || c.ech.greased
if (!c.didResume || c.extMasterSecret) && c.vers != VersionTLS13 {
if c.clientFinishedIsFirst {
state.TLSUnique = c.clientFinished[:]
Expand Down

0 comments on commit 9ea1834

Please sign in to comment.