Skip to content

Commit

Permalink
ECH-12
Browse files Browse the repository at this point in the history
Adds initial support for the Encrypted ClientHello (ECH) extension, as
specified by draft-ietf-tls-esni-08. A few features are not implemented,
including client- and server-side padding.

This commit also adds basic support for handshake metrics:

* Adds a callback `EventHandler()` to `Config`, which can be called at
various points during the handshake to respond to various events. For
example, this callback can be used to record metrics.
* Adds calls to `EventHandler()` just before closing the TLS connection
for resolving ECH usage: whether the client offered, greased, or
bypassed ECH; and whether the server accepted, rejected, or bypassed
ECH.

crypto/tls: Fix the testingTriggerHRR condition

The server sends an HRR if the client does not offer a key share for a
key exchange algorithm the server doesn't support. For testing purposes,
it's useful to trigger this codepath manually. If testingTriggerHRR is
set, then the server only advertises support for algorithms that the
client supports, but did not provide a key share for.

This change fixes a bug in the trigger logic. It seemed to work for the
existing algorithm preferences, but could break if the preferences
change.

crypto/tls: Move draft-ietf-tls-esni-08 to -09

Most significant spec changes include:
* Bump HPKE-05 to -07.
* Derive acceptance confirmation from handshake secret.
* Reuse HPKE context across HRR.
* Use a new codepoint to distinguish between CHI/CHO.
* Bind context handle to AEAD encryption.

Other changes:
* Remove hrrPsk from ECHProvider.Context (breaks API).
* Prune retry configs of unknown version returned by the ECH provider.
* Add EXP_ECHKeySet, a default implementation of the ECH provider. (This
  will be useful for interop testing.)
* Require that the ECH extension not appear in OuterExtensions.
* Add event handler for outer SNI / public name mismatch.
* Remove implementation of HPKE-05

crypto/tls: Move draft-ietf-tls-esni-09 to 10

This change adds support for ECH-10 and removes support for ECH-09. The
primary changes are moving to HPKE-08 and changing the ECHConfig
identifier from a client-computed value to a server-chosen value.
ECHProviders MUST use rejection sampling in choosing the configuration
identifier so as to not introduce conflicts.

crypto/tls: Add cipher suite checks to ECH unit tests

Test that UnmarshalECHConfigs (resp. EXP_UnmarshalECHKeys) parses the
correct number of cipher suites.

crypto/tls: Fix an ECH bug triggered by HRR

Per the spec, the server checks that the config_id matches in
ClientHelloOuter1 and ClientHelloOuter2. We fail to correctly store the
first config_id, resulting in the server enforcing, in effect, that the
second config_id is equal to 0. This bug wasn't exercised by our unit
tests because the test data uses 0 as the config_id.

crypto/tls: Upgrade draft-ietf-tls-esni-10 to 11

Drops support for the previous version of ECH and adds support for the
next one. There's one caveat, however. In draft-ietf-tls-esni-11, the
ECHConfig.version uses the same codepoint as in the previous draft. This
is a bug in draft 11 that will be fixed in a future draft. In the
meantime, we use the codepoint from draft 11.

crypto/tls: Refactor client state machine

Before draft-ietf-tls-esni-11, there was no way for a client that
offered ECH to tell whether a HelloRetryRequest (HRR) was sent by the
client-facing or backend server. Starting in draft-ietf-tls-esni-11
there is an explicit signal of ECH acceptance after HRR. This allows us
to simplify the client state machine.

crypto/tls: Move draft-ietf-tls-esni-11 to 12

Drops support for the previous draft and adds support for the next
one. This change also includes the ClientHelloInner padding scheme
described in Section 6.1.3.

crypto/tls: Reject ECH on invalid encapsulated key

The ECH provider aborts if it gets an encapsulated key it's unable to
parse, causing the ECH server to abort with an "internal_error" alert.
This condition can be triggered by a client that generates a GREASE ECH
extension with a config_id that happens to match a known config, but
uses the incorrect KEM algorithm. This changes the server behavior so
that it rejects instead.
  • Loading branch information
cjpatton committed Aug 23, 2021
1 parent 364f753 commit f9e5bfc
Show file tree
Hide file tree
Showing 17 changed files with 3,212 additions and 34 deletions.
2 changes: 2 additions & 0 deletions src/crypto/tls/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const (
alertUnknownPSKIdentity alert = 115
alertCertificateRequired alert = 116
alertNoApplicationProtocol alert = 120
alertECHRequired alert = 121
)

var alertText = map[alert]string{
Expand Down Expand Up @@ -84,6 +85,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
85 changes: 84 additions & 1 deletion src/crypto/tls/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const (
extensionSignatureAlgorithmsCert uint16 = 50
extensionKeyShare uint16 = 51
extensionRenegotiationInfo uint16 = 0xff01
extensionECH uint16 = 0xfe0c // draft-ietf-tls-esni-12
extensionECHOuterExtensions uint16 = 0xfd00 // draft-ietf-tls-esni-12
)

// TLS signaling cipher suite values
Expand Down Expand Up @@ -214,6 +216,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 @@ -277,6 +318,10 @@ type ConnectionState struct {
// RFC 7627, and https://mitls.org/pages/attacks/3SHAKE#channelbindings.
TLSUnique []byte

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

// CFControl is used to pass additional TLS configuration information to
// HTTP requests.
//
Expand Down Expand Up @@ -646,7 +691,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 @@ -696,6 +742,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

// CFEventHandler, if set, is called by the client and server at various
// points during the handshake to handle specific events. This is used
// primarily for collecting metrics.
Expand Down Expand Up @@ -800,6 +863,9 @@ func (c *Config) Clone() *Config {
DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled,
Renegotiation: c.Renegotiation,
KeyLogWriter: c.KeyLogWriter,
ECHEnabled: c.ECHEnabled,
ClientECHConfigs: c.ClientECHConfigs,
ServerECHProvider: c.ServerECHProvider,
CFEventHandler: c.CFEventHandler,
CFControl: c.CFControl,
sessionTicketKeys: c.sessionTicketKeys,
Expand Down Expand Up @@ -978,6 +1044,23 @@ func (c *Config) supportedVersions() []uint16 {
return versions
}

func (c *Config) supportedVersionsFromMin(minVersion uint16) []uint16 {
versions := make([]uint16, 0, len(supportedVersions))
for _, v := range supportedVersions {
if c != nil && c.MinVersion != 0 && v < c.MinVersion {
continue
}
if c != nil && c.MaxVersion != 0 && v > c.MaxVersion {
continue
}
if v < minVersion {
continue
}
versions = append(versions, v)
}
return versions
}

func (c *Config) maxSupportedVersion() uint16 {
supportedVersions := c.supportedVersions()
if len(supportedVersions) == 0 {
Expand Down
44 changes: 44 additions & 0 deletions src/crypto/tls/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package tls

import (
"bytes"
"circl/hpke"
"crypto/cipher"
"crypto/subtle"
"crypto/x509"
Expand Down Expand Up @@ -114,6 +115,20 @@ type Conn struct {
activeCall int32

tmp [16]byte

// 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 @@ -691,6 +706,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 @@ -1336,6 +1357,28 @@ func (c *Conn) Close() error {
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 @@ -1425,6 +1468,7 @@ func (c *Conn) connectionStateLocked() ConnectionState {
state.VerifiedChains = c.verifiedChains
state.SignedCertificateTimestamps = c.scts
state.OCSPResponse = c.ocspResponse
state.ECHAccepted = c.ech.accepted
state.CFControl = c.config.CFControl
if !c.didResume && c.vers != VersionTLS13 {
if c.clientFinishedIsFirst {
Expand Down
Loading

0 comments on commit f9e5bfc

Please sign in to comment.