From be72a48a17639c54b62251840bed08bddac1d944 Mon Sep 17 00:00:00 2001 From: Christopher Patton Date: Wed, 23 Sep 2020 17:10:26 -0700 Subject: [PATCH] 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. --- src/crypto/tls/alert.go | 2 + src/crypto/tls/common.go | 93 +- src/crypto/tls/conn.go | 46 + src/crypto/tls/ech.go | 1081 ++++++++++++++++++++++ src/crypto/tls/ech_config.go | 164 ++++ src/crypto/tls/ech_provider.go | 302 ++++++ src/crypto/tls/ech_test.go | 949 +++++++++++++++++++ src/crypto/tls/handshake_client.go | 37 +- src/crypto/tls/handshake_client_tls13.go | 211 ++++- src/crypto/tls/handshake_messages.go | 42 + src/crypto/tls/handshake_server.go | 13 +- src/crypto/tls/handshake_server_tls13.go | 108 ++- src/crypto/tls/hpke.go | 41 + src/crypto/tls/tls.go | 27 + src/crypto/tls/tls_cf.go | 65 ++ src/crypto/tls/tls_cf_test.go | 48 + src/crypto/tls/tls_test.go | 6 +- 17 files changed, 3201 insertions(+), 34 deletions(-) create mode 100644 src/crypto/tls/ech.go create mode 100644 src/crypto/tls/ech_config.go create mode 100644 src/crypto/tls/ech_provider.go create mode 100644 src/crypto/tls/ech_test.go create mode 100644 src/crypto/tls/hpke.go diff --git a/src/crypto/tls/alert.go b/src/crypto/tls/alert.go index 4790b737245..755083b866c 100644 --- a/src/crypto/tls/alert.go +++ b/src/crypto/tls/alert.go @@ -48,6 +48,7 @@ const ( alertUnknownPSKIdentity alert = 115 alertCertificateRequired alert = 116 alertNoApplicationProtocol alert = 120 + alertECHRequired alert = 121 ) var alertText = map[alert]string{ @@ -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 { diff --git a/src/crypto/tls/common.go b/src/crypto/tls/common.go index cb00adfcc99..56b951d67d2 100644 --- a/src/crypto/tls/common.go +++ b/src/crypto/tls/common.go @@ -102,6 +102,8 @@ const ( extensionSignatureAlgorithmsCert uint16 = 50 extensionKeyShare uint16 = 51 extensionRenegotiationInfo uint16 = 0xff01 + extensionECH uint16 = 0xfe0d // draft-ietf-tls-esni-13 + extensionECHOuterExtensions uint16 = 0xfd00 // draft-ietf-tls-esni-13 ) // TLS signaling cipher suite values @@ -224,6 +226,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). @@ -292,6 +333,14 @@ 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 + + // 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 + // CFControl is used to pass additional TLS configuration information to // HTTP requests. // @@ -711,7 +760,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. @@ -775,6 +825,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. @@ -888,6 +955,9 @@ func (c *Config) Clone() *Config { Renegotiation: c.Renegotiation, KeyLogWriter: c.KeyLogWriter, SupportDelegatedCredential: c.SupportDelegatedCredential, + ECHEnabled: c.ECHEnabled, + ClientECHConfigs: c.ClientECHConfigs, + ServerECHProvider: c.ServerECHProvider, CFEventHandler: c.CFEventHandler, CFControl: c.CFControl, sessionTicketKeys: c.sessionTicketKeys, @@ -1077,6 +1147,27 @@ func (c *Config) supportedVersions(isClient bool) []uint16 { return versions } +func (c *Config) supportedVersionsFromMin(isClient bool, minVersion uint16) []uint16 { + versions := make([]uint16, 0, len(supportedVersions)) + for _, v := range supportedVersions { + if (c == nil || c.MinVersion == 0) && !debugEnableTLS10 && + isClient && v < VersionTLS12 { + continue + } + 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(isClient bool) uint16 { supportedVersions := c.supportedVersions(isClient) if len(supportedVersions) == 0 { diff --git a/src/crypto/tls/conn.go b/src/crypto/tls/conn.go index cf2f31ab164..5372e1a7cb8 100644 --- a/src/crypto/tls/conn.go +++ b/src/crypto/tls/conn.go @@ -8,6 +8,7 @@ package tls import ( "bytes" + "circl/hpke" "context" "crypto/cipher" "crypto/subtle" @@ -119,6 +120,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. @@ -703,6 +718,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] { @@ -1347,6 +1368,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 } @@ -1502,6 +1546,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 state.CFControl = c.config.CFControl if !c.didResume && c.vers != VersionTLS13 { if c.clientFinishedIsFirst { diff --git a/src/crypto/tls/ech.go b/src/crypto/tls/ech.go new file mode 100644 index 00000000000..86439114b72 --- /dev/null +++ b/src/crypto/tls/ech.go @@ -0,0 +1,1081 @@ +// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "circl/hpke" + "errors" + "fmt" + "io" + + "golang.org/x/crypto/cryptobyte" +) + +const ( + // Constants for TLS operations + echAcceptConfLabel = "ech accept confirmation" + echAcceptConfHRRLabel = "hrr ech accept confirmation" + + // Constants for HPKE operations + echHpkeInfoSetup = "tls ech" + + // When sent in the ClientHello, the first byte of the payload of the ECH + // extension indicates whether the message is the ClientHelloOuter or + // ClientHelloInner. + echClientHelloOuterVariant uint8 = 0 + echClientHelloInnerVariant uint8 = 1 +) + +var ( + zeros = [8]byte{} +) + +// echOfferOrGrease is called by the client after generating its ClientHello +// message to decide if it will offer or GREASE ECH. It does neither if ECH is +// disabled. Returns a pair of ClientHello messages, hello and helloInner. If +// offering ECH, these are the ClienthelloOuter and ClientHelloInner +// respectively. Otherwise, hello is the ClientHello and helloInner == nil. +// +// TODO(cjpatton): "[When offering ECH, the client] MUST NOT offer to resume any +// session for TLS 1.2 and below [in ClientHelloInner]." +func (c *Conn) echOfferOrGrease(helloBase *clientHelloMsg) (hello, helloInner *clientHelloMsg, err error) { + config := c.config + + if !config.ECHEnabled || testingECHTriggerBypassBeforeHRR { + // Bypass ECH. + return helloBase, nil, nil + } + + // Choose the ECHConfig to use for this connection. If none is available, or + // if we're not offering TLS 1.3 or above, then GREASE. + echConfig := config.echSelectConfig() + if echConfig == nil || config.maxSupportedVersion(roleClient) < VersionTLS13 { + var err error + + // Generate a dummy ClientECH. + helloBase.ech, err = echGenerateGreaseExt(config.rand()) + if err != nil { + return nil, nil, fmt.Errorf("tls: ech: failed to generate grease ECH: %s", err) + } + + // GREASE ECH. + c.ech.offered = false + c.ech.greased = true + helloBase.raw = nil + return helloBase, nil, nil + } + + // Store the ECH config parameters that are needed later. + c.ech.configId = echConfig.configId + c.ech.maxNameLen = int(echConfig.maxNameLen) + + // Generate the HPKE context. Store it in case of HRR. + var enc []byte + enc, c.ech.sealer, err = echConfig.setupSealer(config.rand()) + if err != nil { + return nil, nil, fmt.Errorf("tls: ech: %s", err) + } + + // ClientHelloInner is constructed from the base ClientHello. The payload of + // the "encrypted_client_hello" extension is a single 1 byte indicating that + // this is the ClientHelloInner. + helloInner = helloBase + helloInner.ech = []byte{echClientHelloInnerVariant} + + // Ensure that only TLS 1.3 and above are offered in the inner handshake. + if v := helloInner.supportedVersions; len(v) == 0 || v[len(v)-1] < VersionTLS13 { + return nil, nil, errors.New("tls: ech: only TLS 1.3 is allowed in ClientHelloInner") + } + + // ClientHelloOuter is constructed by generating a fresh ClientHello and + // copying "session_id" from ClientHelloInner, setting "server_name" to the + // client-facing server, and adding the "encrypted_client_hello" extension. + // + // In addition, we discard the "key_share" and instead use the one from + // ClientHelloInner. + hello, _, err = c.makeClientHello(config.MinVersion) + if err != nil { + return nil, nil, fmt.Errorf("tls: ech: %s", err) + } + hello.sessionId = helloBase.sessionId + hello.serverName = hostnameInSNI(string(echConfig.rawPublicName)) + if err := c.echUpdateClientHelloOuter(hello, helloInner, enc); err != nil { + return nil, nil, err + } + + // Offer ECH. + c.ech.offered = true + helloInner.raw = nil + hello.raw = nil + return hello, helloInner, nil +} + +// echUpdateClientHelloOuter is called by the client to construct the payload of +// the ECH extension in the outer handshake. +func (c *Conn) echUpdateClientHelloOuter(hello, helloInner *clientHelloMsg, enc []byte) error { + var ( + ech echClientOuter + err error + ) + + // Copy all compressed extensions from ClientHelloInner into + // ClientHelloOuter. + for _, ext := range echOuterExtensions() { + echCopyExtensionFromClientHelloInner(hello, helloInner, ext) + } + + // Always copy the "key_shares" extension from ClientHelloInner, regardless + // of whether it gets compressed. + hello.keyShares = helloInner.keyShares + + _, kdf, aead := c.ech.sealer.Suite().Params() + ech.handle.suite.kdfId = uint16(kdf) + ech.handle.suite.aeadId = uint16(aead) + ech.handle.configId = c.ech.configId + ech.handle.enc = enc + + // EncodedClientHelloInner + helloInner.raw = nil + encodedHelloInner := echEncodeClientHelloInner( + helloInner.marshal(), + len(helloInner.serverName), + c.ech.maxNameLen) + if encodedHelloInner == nil { + return errors.New("tls: ech: encoding of EncodedClientHelloInner failed") + } + + // ClientHelloOuterAAD + hello.raw = nil + hello.ech = ech.marshal() + helloOuterAad := echEncodeClientHelloOuterAAD(hello.marshal(), + aead.CipherLen(uint(len(encodedHelloInner)))) + if helloOuterAad == nil { + return errors.New("tls: ech: encoding of ClientHelloOuterAAD failed") + } + + ech.payload, err = c.ech.sealer.Seal(encodedHelloInner, helloOuterAad) + if err != nil { + return fmt.Errorf("tls: ech: seal failed: %s", err) + } + if testingECHTriggerPayloadDecryptError { + ech.payload[0] ^= 0xff // Inauthentic ciphertext + } + ech.raw = nil + hello.ech = ech.marshal() + + helloInner.raw = nil + hello.raw = nil + return nil +} + +// echAcceptOrReject is called by the client-facing server to determine whether +// ECH was offered by the client, and if so, whether to accept or reject. The +// return value is the ClientHello that will be used for the connection. +// +// This function is called prior to processing the ClientHello. In case of +// HelloRetryRequest, it is also called before processing the second +// ClientHello. This is indicated by the afterHRR flag. +func (c *Conn) echAcceptOrReject(hello *clientHelloMsg, afterHRR bool) (*clientHelloMsg, error) { + config := c.config + p := config.ServerECHProvider + + if !config.echCanAccept() { + // Bypass ECH. + return hello, nil + } + + if len(hello.ech) > 0 { // The ECH extension is present + switch hello.ech[0] { + case echClientHelloInnerVariant: // inner handshake + if len(hello.ech) > 1 { + c.sendAlert(alertIllegalParameter) + return nil, errors.New("ech: inner handshake has non-empty payload") + } + + // Continue as the backend server. + return hello, nil + case echClientHelloOuterVariant: // outer handshake + default: + c.sendAlert(alertIllegalParameter) + return nil, errors.New("ech: inner handshake has non-empty payload") + } + } else { + if c.ech.offered { + // This occurs if the server accepted prior to HRR, but the client + // failed to send the ECH extension in the second ClientHelloOuter. This + // would cause ClientHelloOuter to be used after ClientHelloInner, which + // is illegal. + c.sendAlert(alertMissingExtension) + return nil, errors.New("ech: hrr: bypass after offer") + } + + // Bypass ECH. + return hello, nil + } + + if afterHRR && !c.ech.offered && !c.ech.greased { + // The client bypassed ECH prior to HRR, but not after. This could + // cause ClientHelloInner to be used after ClientHelloOuter, which is + // illegal. + c.sendAlert(alertIllegalParameter) + return nil, errors.New("ech: hrr: offer or grease after bypass") + } + + // Parse ClientECH. + ech, err := echUnmarshalClientOuter(hello.ech) + if err != nil { + c.sendAlert(alertIllegalParameter) + return nil, fmt.Errorf("ech: failed to parse extension: %s", err) + } + + // Make sure that the HPKE suite and config id don't change across HRR and + // that the encapsulated key is not present after HRR. + if afterHRR && c.ech.offered { + _, kdf, aead := c.ech.opener.Suite().Params() + if ech.handle.suite.kdfId != uint16(kdf) || + ech.handle.suite.aeadId != uint16(aead) || + ech.handle.configId != c.ech.configId || + len(ech.handle.enc) > 0 { + c.sendAlert(alertIllegalParameter) + return nil, errors.New("ech: hrr: illegal handle in second hello") + } + } + + // Store the config id in case of HRR. + c.ech.configId = ech.handle.configId + + // Ask the ECH provider for the HPKE context. + if c.ech.opener == nil { + res := p.GetDecryptionContext(ech.handle.marshal(), extensionECH) + + // Compute retry configurations, skipping those indicating an + // unsupported version. + if len(res.RetryConfigs) > 0 { + configs, err := UnmarshalECHConfigs(res.RetryConfigs) // skips unrecognized versions + if err != nil { + c.sendAlert(alertInternalError) + return nil, fmt.Errorf("ech: %s", err) + } + + if len(configs) > 0 { + c.ech.retryConfigs, err = echMarshalConfigs(configs) + if err != nil { + c.sendAlert(alertInternalError) + return nil, fmt.Errorf("ech: %s", err) + } + } + + // Check if the outer SNI matches the public name of any ECH config + // advertised by the client-facing server. As of + // draft-ietf-tls-esni-10, the client is required to use the ECH + // config's public name as the outer SNI. Although there's no real + // reason for the server to enforce this, it's worth noting it when + // it happens. + pubNameMatches := false + for _, config := range configs { + if hello.serverName == string(config.rawPublicName) { + pubNameMatches = true + } + } + if !pubNameMatches { + c.handleCFEvent(CFEventECHPublicNameMismatch{}) + } + } + + switch res.Status { + case ECHProviderSuccess: + c.ech.opener, err = hpke.UnmarshalOpener(res.Context) + if err != nil { + c.sendAlert(alertInternalError) + return nil, fmt.Errorf("ech: %s", err) + } + case ECHProviderReject: + // Reject ECH. We do not know at this point whether the client + // intended to offer or grease ECH, so we presume grease until the + // client indicates rejection by sending an "ech_required" alert. + c.ech.greased = true + return hello, nil + case ECHProviderAbort: + c.sendAlert(alert(res.Alert)) + return nil, fmt.Errorf("ech: provider aborted: %s", res.Error) + default: + c.sendAlert(alertInternalError) + return nil, errors.New("ech: unexpected provider status") + } + } + + // ClientHelloOuterAAD + rawHelloOuterAad := echEncodeClientHelloOuterAAD(hello.marshal(), uint(len(ech.payload))) + if rawHelloOuterAad == nil { + // This occurs if the ClientHelloOuter is malformed. This values was + // already parsed into `hello`, so this should not happen. + c.sendAlert(alertInternalError) + return nil, fmt.Errorf("ech: failed to encode ClientHelloOuterAAD") + } + + // EncodedClientHelloInner + rawEncodedHelloInner, err := c.ech.opener.Open(ech.payload, rawHelloOuterAad) + if err != nil { + if afterHRR && c.ech.accepted { + // Don't reject after accept, as this would result in processing the + // ClientHelloOuter after processing the ClientHelloInner. + c.sendAlert(alertDecryptError) + return nil, fmt.Errorf("ech: hrr: reject after accept: %s", err) + } + + // Reject ECH. We do not know at this point whether the client + // intended to offer or grease ECH, so we presume grease until the + // client indicates rejection by sending an "ech_required" alert. + c.ech.greased = true + return hello, nil + } + + // ClientHelloInner + rawHelloInner := echDecodeClientHelloInner(rawEncodedHelloInner, hello.marshal(), hello.sessionId) + if rawHelloInner == nil { + c.sendAlert(alertIllegalParameter) + return nil, fmt.Errorf("ech: failed to decode EncodedClientHelloInner") + } + helloInner := new(clientHelloMsg) + if !helloInner.unmarshal(rawHelloInner) { + c.sendAlert(alertIllegalParameter) + return nil, fmt.Errorf("ech: failed to parse ClientHelloInner") + } + + // Check for a well-formed ECH extension. + if len(helloInner.ech) != 1 || + helloInner.ech[0] != echClientHelloInnerVariant { + c.sendAlert(alertIllegalParameter) + return nil, fmt.Errorf("ech: ClientHelloInner does not have a well-formed ECH extension") + } + + // Check that the client did not offer TLS 1.2 or below in the inner + // handshake. + helloInnerSupportsTLS12OrBelow := len(helloInner.supportedVersions) == 0 + for _, v := range helloInner.supportedVersions { + if v < VersionTLS13 { + helloInnerSupportsTLS12OrBelow = true + } + } + if helloInnerSupportsTLS12OrBelow { + c.sendAlert(alertIllegalParameter) + return nil, errors.New("ech: ClientHelloInner offers TLS 1.2 or below") + } + + // Accept ECH. + c.ech.offered = true + c.ech.accepted = true + return helloInner, nil +} + +// echClientOuter represents a ClientECH structure, the payload of the client's +// "encrypted_client_hello" extension that appears in the outer handshake. +type echClientOuter struct { + raw []byte + + // Parsed from raw + handle echContextHandle + payload []byte +} + +// echUnmarshalClientOuter parses a ClientECH structure. The caller provides the +// ECH version indicated by the client. +func echUnmarshalClientOuter(raw []byte) (*echClientOuter, error) { + s := cryptobyte.String(raw) + ech := new(echClientOuter) + ech.raw = raw + + // Make sure this is the outer handshake. + var variant uint8 + if !s.ReadUint8(&variant) { + return nil, fmt.Errorf("error parsing ClientECH.type") + } + if variant != echClientHelloOuterVariant { + return nil, fmt.Errorf("unexpected ClientECH.type (want outer (0))") + } + + // Parse the context handle. + if !echReadContextHandle(&s, &ech.handle) { + return nil, fmt.Errorf("error parsing context handle") + } + endOfContextHandle := len(raw) - len(s) + ech.handle.raw = raw[1:endOfContextHandle] + + // Parse the payload. + var t cryptobyte.String + if !s.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&ech.payload, len(t)) || !s.Empty() { + return nil, fmt.Errorf("error parsing payload") + } + + return ech, nil +} + +func (ech *echClientOuter) marshal() []byte { + if ech.raw != nil { + return ech.raw + } + var b cryptobyte.Builder + b.AddUint8(echClientHelloOuterVariant) + b.AddBytes(ech.handle.marshal()) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(ech.payload) + }) + return b.BytesOrPanic() +} + +// echContextHandle represents the prefix of a ClientECH structure used by +// the server to compute the HPKE context. +type echContextHandle struct { + raw []byte + + // Parsed from raw + suite hpkeSymmetricCipherSuite + configId uint8 + enc []byte +} + +func (handle *echContextHandle) marshal() []byte { + if handle.raw != nil { + return handle.raw + } + var b cryptobyte.Builder + b.AddUint16(handle.suite.kdfId) + b.AddUint16(handle.suite.aeadId) + b.AddUint8(handle.configId) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(handle.enc) + }) + return b.BytesOrPanic() +} + +func echReadContextHandle(s *cryptobyte.String, handle *echContextHandle) bool { + var t cryptobyte.String + if !s.ReadUint16(&handle.suite.kdfId) || // cipher_suite.kdf_id + !s.ReadUint16(&handle.suite.aeadId) || // cipher_suite.aead_id + !s.ReadUint8(&handle.configId) || // config_id + !s.ReadUint16LengthPrefixed(&t) || // enc + !t.ReadBytes(&handle.enc, len(t)) { + return false + } + return true +} + +// hpkeSymmetricCipherSuite represents an ECH ciphersuite, a KDF/AEAD algorithm pair. This +// is different from an HPKE ciphersuite, which represents a KEM/KDF/AEAD +// triple. +type hpkeSymmetricCipherSuite struct { + kdfId, aeadId uint16 +} + +// Generates a grease ECH extension using a hard-coded KEM public key. +func echGenerateGreaseExt(rand io.Reader) ([]byte, error) { + var err error + var dummyX25519PublicKey = []byte{ + 143, 38, 37, 36, 12, 6, 229, 30, 140, 27, 167, 73, 26, 100, 203, 107, 216, + 81, 163, 222, 52, 211, 54, 210, 46, 37, 78, 216, 157, 97, 241, 244, + } + dummyEncodedHelloInnerLen := 100 // TODO(cjpatton): Compute this correctly. + kem, kdf, aead := defaultHPKESuite.Params() + + pk, err := kem.Scheme().UnmarshalBinaryPublicKey(dummyX25519PublicKey) + if err != nil { + return nil, fmt.Errorf("tls: grease ech: failed to parse dummy public key: %s", err) + } + sender, err := defaultHPKESuite.NewSender(pk, nil) + if err != nil { + return nil, fmt.Errorf("tls: grease ech: failed to create sender: %s", err) + } + + var ech echClientOuter + ech.handle.suite.kdfId = uint16(kdf) + ech.handle.suite.aeadId = uint16(aead) + randomByte := make([]byte, 1) + _, err = io.ReadFull(rand, randomByte) + if err != nil { + return nil, fmt.Errorf("tls: grease ech: %s", err) + } + ech.handle.configId = randomByte[0] + ech.handle.enc, _, err = sender.Setup(rand) + if err != nil { + return nil, fmt.Errorf("tls: grease ech: %s", err) + } + ech.payload = make([]byte, + int(aead.CipherLen(uint(dummyEncodedHelloInnerLen)))) + if _, err = io.ReadFull(rand, ech.payload); err != nil { + return nil, fmt.Errorf("tls: grease ech: %s", err) + } + return ech.marshal(), nil +} + +// echEncodeClientHelloInner interprets innerData as a ClientHelloInner message +// and transforms it into an EncodedClientHelloInner. Returns nil if parsing +// innerData fails. +func echEncodeClientHelloInner(innerData []byte, serverNameLen, maxNameLen int) []byte { + var ( + errIllegalParameter = errors.New("illegal parameter") + outerExtensions = echOuterExtensions() + msgType uint8 + legacyVersion uint16 + random []byte + legacySessionId cryptobyte.String + cipherSuites cryptobyte.String + legacyCompressionMethods cryptobyte.String + extensions cryptobyte.String + s cryptobyte.String + b cryptobyte.Builder + ) + + u := cryptobyte.String(innerData) + if !u.ReadUint8(&msgType) || + !u.ReadUint24LengthPrefixed(&s) || !u.Empty() { + return nil + } + + if !s.ReadUint16(&legacyVersion) || + !s.ReadBytes(&random, 32) || + !s.ReadUint8LengthPrefixed(&legacySessionId) || + !s.ReadUint16LengthPrefixed(&cipherSuites) || + !s.ReadUint8LengthPrefixed(&legacyCompressionMethods) { + return nil + } + + if s.Empty() { + // Extensions field must be present in TLS 1.3. + return nil + } + + if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { + return nil + } + + b.AddUint16(legacyVersion) + b.AddBytes(random) + b.AddUint8(0) // 0-length legacy_session_id + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(cipherSuites) + }) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(legacyCompressionMethods) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + if testingECHOuterExtIncorrectOrder { + // Replace outer extensions with "outer_extension" extension, but in + // the incorrect order. + echAddOuterExtensions(b, outerExtensions) + } + + for !extensions.Empty() { + var ext uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&ext) || + !extensions.ReadUint16LengthPrefixed(&extData) { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + + if len(outerExtensions) > 0 && ext == outerExtensions[0] { + if !testingECHOuterExtIncorrectOrder { + // Replace outer extensions with "outer_extension" extension. + echAddOuterExtensions(b, outerExtensions) + } + + // Consume the remaining outer extensions. + for _, outerExt := range outerExtensions[1:] { + if !extensions.ReadUint16(&ext) || + !extensions.ReadUint16LengthPrefixed(&extData) { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + if ext != outerExt { + panic("internal error: malformed ClientHelloInner") + } + } + + } else { + b.AddUint16(ext) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(extData) + }) + } + } + }) + + encodedData, err := b.Bytes() + if err == errIllegalParameter { + return nil // Input malformed + } else if err != nil { + panic(err) // Host encountered internal error + } + + // Add padding. + paddingLen := 0 + if serverNameLen > 0 { + // draft-ietf-tls-esni-13, Section 6.1.3: + // + // If the ClientHelloInner contained a "server_name" extension with a + // name of length D, add max(0, L - D) bytes of padding. + if n := maxNameLen - serverNameLen; n > 0 { + paddingLen += n + } + } else { + // draft-ietf-tls-esni-13, Section 6.1.3: + // + // If the ClientHelloInner did not contain a "server_name" extension + // (e.g., if the client is connecting to an IP address), add L + 9 bytes + // of padding. This is the length of a "server_name" extension with an + // L-byte name. + const sniPaddingLen = 9 + paddingLen += sniPaddingLen + maxNameLen + } + paddingLen = 31 - ((len(encodedData) + paddingLen - 1) % 32) + for i := 0; i < paddingLen; i++ { + encodedData = append(encodedData, 0) + } + + return encodedData +} + +func echAddOuterExtensions(b *cryptobyte.Builder, outerExtensions []uint16) { + b.AddUint16(extensionECHOuterExtensions) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + for _, outerExt := range outerExtensions { + b.AddUint16(outerExt) + } + if testingECHOuterExtIllegal { + // This is not allowed. + b.AddUint16(extensionECH) + } + }) + }) +} + +// echDecodeClientHelloInner interprets encodedData as an EncodedClientHelloInner +// message and substitutes the "outer_extension" extension with extensions from +// outerData, interpreted as the ClientHelloOuter message. Returns nil if +// parsing encodedData fails. +func echDecodeClientHelloInner(encodedData, outerData, outerSessionId []byte) []byte { + var ( + errIllegalParameter = errors.New("illegal parameter") + legacyVersion uint16 + random []byte + legacySessionId cryptobyte.String + cipherSuites cryptobyte.String + legacyCompressionMethods cryptobyte.String + extensions cryptobyte.String + b cryptobyte.Builder + ) + + s := cryptobyte.String(encodedData) + if !s.ReadUint16(&legacyVersion) || + !s.ReadBytes(&random, 32) || + !s.ReadUint8LengthPrefixed(&legacySessionId) || + !s.ReadUint16LengthPrefixed(&cipherSuites) || + !s.ReadUint8LengthPrefixed(&legacyCompressionMethods) { + return nil + } + + if len(legacySessionId) > 0 { + return nil + } + + if s.Empty() { + // Extensions field must be present in TLS 1.3. + return nil + } + + if !s.ReadUint16LengthPrefixed(&extensions) { + return nil + } + + b.AddUint8(typeClientHello) + b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddUint16(legacyVersion) + b.AddBytes(random) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(outerSessionId) // ClientHelloOuter.legacy_session_id + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(cipherSuites) + }) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(legacyCompressionMethods) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + var handledOuterExtensions bool + for !extensions.Empty() { + var ext uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&ext) || + !extensions.ReadUint16LengthPrefixed(&extData) { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + + if ext == extensionECHOuterExtensions { + if handledOuterExtensions { + // It is an error to send any extension more than once in a + // single message. + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + handledOuterExtensions = true + + // Read the referenced outer extensions. + referencedExts := make([]uint16, 0, 10) + var outerExtData cryptobyte.String + if !extData.ReadUint8LengthPrefixed(&outerExtData) || + len(outerExtData)%2 != 0 || + !extData.Empty() { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + for !outerExtData.Empty() { + if !outerExtData.ReadUint16(&ext) || + ext == extensionECH { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + referencedExts = append(referencedExts, ext) + } + + // Add the outer extensions from the ClientHelloOuter into the + // ClientHelloInner. + outerCt := 0 + r := processClientHelloExtensions(outerData, func(ext uint16, extData cryptobyte.String) bool { + if outerCt < len(referencedExts) && ext == referencedExts[outerCt] { + outerCt++ + b.AddUint16(ext) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(extData) + }) + } + return true + }) + + // Ensure that all outer extensions have been incorporated + // exactly once, and in the correct order. + if !r || outerCt != len(referencedExts) { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + } else { + b.AddUint16(ext) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(extData) + }) + } + } + }) + }) + + innerData, err := b.Bytes() + if err == errIllegalParameter { + return nil // Input malformed + } else if err != nil { + panic(err) // Host encountered internal error + } + + // Read the padding. + for !s.Empty() { + var zero uint8 + if !s.ReadUint8(&zero) || zero != 0 { + return nil + } + } + + return innerData +} + +// echEncodeClientHelloOuterAAD interprets outerData as ClientHelloOuter and +// constructs a ClientHelloOuterAAD. The output doesn't have the 4-byte prefix +// that indicates the handshake message type and its length. +func echEncodeClientHelloOuterAAD(outerData []byte, payloadLen uint) []byte { + var ( + errIllegalParameter = errors.New("illegal parameter") + msgType uint8 + legacyVersion uint16 + random []byte + legacySessionId cryptobyte.String + cipherSuites cryptobyte.String + legacyCompressionMethods cryptobyte.String + extensions cryptobyte.String + s cryptobyte.String + b cryptobyte.Builder + ) + + u := cryptobyte.String(outerData) + if !u.ReadUint8(&msgType) || + !u.ReadUint24LengthPrefixed(&s) || !u.Empty() { + return nil + } + + if !s.ReadUint16(&legacyVersion) || + !s.ReadBytes(&random, 32) || + !s.ReadUint8LengthPrefixed(&legacySessionId) || + !s.ReadUint16LengthPrefixed(&cipherSuites) || + !s.ReadUint8LengthPrefixed(&legacyCompressionMethods) { + return nil + } + + if s.Empty() { + // Extensions field must be present in TLS 1.3. + return nil + } + + if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { + return nil + } + + b.AddUint16(legacyVersion) + b.AddBytes(random) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(legacySessionId) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(cipherSuites) + }) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(legacyCompressionMethods) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for !extensions.Empty() { + var ext uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&ext) || + !extensions.ReadUint16LengthPrefixed(&extData) { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + + // If this is the ECH extension and the payload is the outer variant + // of ClientECH, then replace the payloadLen 0 bytes. + if ext == extensionECH { + ech, err := echUnmarshalClientOuter(extData) + if err != nil { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + ech.payload = make([]byte, payloadLen) + ech.raw = nil + extData = ech.marshal() + } + + b.AddUint16(ext) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(extData) + }) + } + }) + + outerAadData, err := b.Bytes() + if err == errIllegalParameter { + return nil // Input malformed + } else if err != nil { + panic(err) // Host encountered internal error + } + + return outerAadData +} + +// echEncodeAcceptConfHelloRetryRequest interprets data as a ServerHello message +// and replaces the payload of the ECH extension with 8 zero bytes. The output +// includes the 4-byte prefix that indicates the message type and its length. +func echEncodeAcceptConfHelloRetryRequest(data []byte) []byte { + var ( + errIllegalParameter = errors.New("illegal parameter") + vers uint16 + random []byte + sessionId []byte + cipherSuite uint16 + compressionMethod uint8 + s cryptobyte.String + b cryptobyte.Builder + ) + + s = cryptobyte.String(data) + if !s.Skip(4) || // message type and uint24 length field + !s.ReadUint16(&vers) || !s.ReadBytes(&random, 32) || + !readUint8LengthPrefixed(&s, &sessionId) || + !s.ReadUint16(&cipherSuite) || + !s.ReadUint8(&compressionMethod) { + return nil + } + + if s.Empty() { + // ServerHello is optionally followed by extension data + return nil + } + + var extensions cryptobyte.String + if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { + return nil + } + + b.AddUint8(typeServerHello) + b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddUint16(vers) + b.AddBytes(random) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(sessionId) + }) + b.AddUint16(cipherSuite) + b.AddUint8(compressionMethod) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for !extensions.Empty() { + var extension uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&extension) || + !extensions.ReadUint16LengthPrefixed(&extData) { + panic(cryptobyte.BuildError{Err: errIllegalParameter}) + } + + b.AddUint16(extension) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + if extension == extensionECH { + b.AddBytes(zeros[:8]) + } else { + b.AddBytes(extData) + } + }) + } + }) + }) + + encodedData, err := b.Bytes() + if err == errIllegalParameter { + return nil // Input malformed + } else if err != nil { + panic(err) // Host encountered internal error + } + + return encodedData +} + +// processClientHelloExtensions interprets data as a ClientHello and applies a +// function proc to each extension. Returns a bool indicating whether parsing +// succeeded. +func processClientHelloExtensions(data []byte, proc func(ext uint16, extData cryptobyte.String) bool) bool { + _, extensionsData := splitClientHelloExtensions(data) + if extensionsData == nil { + return false + } + + s := cryptobyte.String(extensionsData) + if s.Empty() { + // Extensions field not present. + return true + } + + var extensions cryptobyte.String + if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { + return false + } + + for !extensions.Empty() { + var ext uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&ext) || + !extensions.ReadUint16LengthPrefixed(&extData) { + return false + } + if ok := proc(ext, extData); !ok { + return false + } + } + return true +} + +// splitClientHelloExtensions interprets data as a ClientHello message and +// returns two strings: the first contains the start of the ClientHello up to +// the start of the extensions; and the second is the length-prefixed +// extensions. Returns (nil, nil) if parsing of data fails. +func splitClientHelloExtensions(data []byte) ([]byte, []byte) { + s := cryptobyte.String(data) + + var ignored uint16 + var t cryptobyte.String + if !s.Skip(4) || // message type and uint24 length field + !s.ReadUint16(&ignored) || !s.Skip(32) || // vers, random + !s.ReadUint8LengthPrefixed(&t) { // session_id + return nil, nil + } + + if !s.ReadUint16LengthPrefixed(&t) { // cipher_suites + return nil, nil + } + + if !s.ReadUint8LengthPrefixed(&t) { // compression_methods + return nil, nil + } + + return data[:len(data)-len(s)], s +} + +// TODO(cjpatton): Handle public name as described in draft-ietf-tls-esni-13, +// Section 4. +// +// TODO(cjpatton): Implement ECH config extensions as described in +// draft-ietf-tls-esni-13, Section 4.1. +func (c *Config) echSelectConfig() *ECHConfig { + for _, echConfig := range c.ClientECHConfigs { + if _, err := echConfig.selectSuite(); err == nil && + echConfig.version == extensionECH { + return &echConfig + } + } + return nil +} + +func (c *Config) echCanOffer() bool { + if c == nil { + return false + } + return c.ECHEnabled && + c.echSelectConfig() != nil && + c.maxSupportedVersion(roleClient) >= VersionTLS13 +} + +func (c *Config) echCanAccept() bool { + if c == nil { + return false + } + return c.ECHEnabled && + c.ServerECHProvider != nil && + c.maxSupportedVersion(roleServer) >= VersionTLS13 +} + +// echOuterExtensions returns the list of extensions of the ClientHelloOuter +// that will be incorporated into the CleintHelloInner. +func echOuterExtensions() []uint16 { + // NOTE(cjpatton): It would be nice to incorporate more extensions, but + // "key_share" is the last extension to appear in the ClientHello before + // "pre_shared_key". As a result, the only contiguous sequence of outer + // extensions that contains "key_share" is "key_share" itself. Note that + // we cannot change the order of extensions in the ClientHello, as the + // unit tests expect "key_share" to be the second to last extension. + outerExtensions := []uint16{extensionKeyShare} + if testingECHOuterExtMany { + // NOTE(cjpatton): Incorporating this particular sequence does not + // yield significant savings. However, it's useful to test that our + // server correctly handles a sequence of compressed extensions and + // not just one. + outerExtensions = []uint16{ + extensionStatusRequest, + extensionSupportedCurves, + extensionSupportedPoints, + } + } else if testingECHOuterExtNone { + outerExtensions = []uint16{} + } + + return outerExtensions +} + +func echCopyExtensionFromClientHelloInner(hello, helloInner *clientHelloMsg, ext uint16) { + switch ext { + case extensionStatusRequest: + hello.ocspStapling = helloInner.ocspStapling + case extensionSupportedCurves: + hello.supportedCurves = helloInner.supportedCurves + case extensionSupportedPoints: + hello.supportedPoints = helloInner.supportedPoints + case extensionKeyShare: + hello.keyShares = helloInner.keyShares + default: + panic(fmt.Errorf("tried to copy unrecognized extension: %04x", ext)) + } +} diff --git a/src/crypto/tls/ech_config.go b/src/crypto/tls/ech_config.go new file mode 100644 index 00000000000..12da4c44c78 --- /dev/null +++ b/src/crypto/tls/ech_config.go @@ -0,0 +1,164 @@ +// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "circl/hpke" + "circl/kem" + "errors" + "fmt" + "io" + + "golang.org/x/crypto/cryptobyte" +) + +// ECHConfig represents an ECH configuration. +type ECHConfig struct { + pk kem.PublicKey + raw []byte + + // Parsed from raw + version uint16 + configId uint8 + rawPublicName []byte + rawPublicKey []byte + kemId uint16 + suites []hpkeSymmetricCipherSuite + maxNameLen uint8 + ignoredExtensions []byte +} + +// UnmarshalECHConfigs parses a sequence of ECH configurations. +func UnmarshalECHConfigs(raw []byte) ([]ECHConfig, error) { + var ( + err error + config ECHConfig + t, contents cryptobyte.String + ) + configs := make([]ECHConfig, 0) + s := cryptobyte.String(raw) + if !s.ReadUint16LengthPrefixed(&t) || !s.Empty() { + return configs, errors.New("error parsing configs") + } + raw = raw[2:] +ConfigsLoop: + for !t.Empty() { + l := len(t) + if !t.ReadUint16(&config.version) || + !t.ReadUint16LengthPrefixed(&contents) { + return nil, errors.New("error parsing config") + } + n := l - len(t) + config.raw = raw[:n] + raw = raw[n:] + + if config.version != extensionECH { + continue ConfigsLoop + } + if !readConfigContents(&contents, &config) { + return nil, errors.New("error parsing config contents") + } + + kem := hpke.KEM(config.kemId) + if !kem.IsValid() { + continue ConfigsLoop + } + config.pk, err = kem.Scheme().UnmarshalBinaryPublicKey(config.rawPublicKey) + if err != nil { + return nil, fmt.Errorf("error parsing public key: %s", err) + } + configs = append(configs, config) + } + return configs, nil +} + +func echMarshalConfigs(configs []ECHConfig) ([]byte, error) { + var b cryptobyte.Builder + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, config := range configs { + if config.raw == nil { + panic("config.raw not set") + } + b.AddBytes(config.raw) + } + }) + return b.Bytes() +} + +func readConfigContents(contents *cryptobyte.String, config *ECHConfig) bool { + var t cryptobyte.String + if !contents.ReadUint8(&config.configId) || + !contents.ReadUint16(&config.kemId) || + !contents.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&config.rawPublicKey, len(t)) || + !contents.ReadUint16LengthPrefixed(&t) || + len(t)%4 != 0 { + return false + } + + config.suites = nil + for !t.Empty() { + var kdfId, aeadId uint16 + if !t.ReadUint16(&kdfId) || !t.ReadUint16(&aeadId) { + // This indicates an internal bug. + panic("internal error while parsing contents.cipher_suites") + } + config.suites = append(config.suites, hpkeSymmetricCipherSuite{kdfId, aeadId}) + } + + if !contents.ReadUint8(&config.maxNameLen) || + !contents.ReadUint8LengthPrefixed(&t) || + !t.ReadBytes(&config.rawPublicName, len(t)) || + !contents.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&config.ignoredExtensions, len(t)) || + !contents.Empty() { + return false + } + return true +} + +// setupSealer generates the client's HPKE context for use with the ECH +// extension. It returns the context and corresponding encapsulated key. +func (config *ECHConfig) setupSealer(rand io.Reader) (enc []byte, sealer hpke.Sealer, err error) { + if config.raw == nil { + panic("config.raw not set") + } + hpkeSuite, err := config.selectSuite() + if err != nil { + return nil, nil, err + } + info := append(append([]byte(echHpkeInfoSetup), 0), config.raw...) + sender, err := hpkeSuite.NewSender(config.pk, info) + if err != nil { + return nil, nil, err + } + return sender.Setup(rand) +} + +// isPeerCipherSuiteSupported returns true if this configuration indicates +// support for the given ciphersuite. +func (config *ECHConfig) isPeerCipherSuiteSupported(suite hpkeSymmetricCipherSuite) bool { + for _, configSuite := range config.suites { + if suite == configSuite { + return true + } + } + return false +} + +// selectSuite returns the first ciphersuite indicated by this +// configuration that is supported by the caller. +func (config *ECHConfig) selectSuite() (hpke.Suite, error) { + for _, suite := range config.suites { + hpkeSuite, err := hpkeAssembleSuite( + config.kemId, + suite.kdfId, + suite.aeadId, + ) + if err == nil { + return hpkeSuite, nil + } + } + return hpke.Suite{}, errors.New("could not negotiate a ciphersuite") +} diff --git a/src/crypto/tls/ech_provider.go b/src/crypto/tls/ech_provider.go new file mode 100644 index 00000000000..4c8096a7941 --- /dev/null +++ b/src/crypto/tls/ech_provider.go @@ -0,0 +1,302 @@ +// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "circl/hpke" + "circl/kem" + "errors" + "fmt" + + "golang.org/x/crypto/cryptobyte" +) + +// ECHProvider specifies the interface of an ECH service provider that decrypts +// the ECH payload on behalf of the client-facing server. It also defines the +// set of acceptable ECH configurations. +type ECHProvider interface { + // GetDecryptionContext attempts to construct the HPKE context used by the + // client-facing server for decryption. (See draft-irtf-cfrg-hpke-07, + // Section 5.2.) + // + // handle encodes the parameters of the client's "encrypted_client_hello" + // extension that are needed to construct the context. Since + // draft-ietf-tls-esni-10 these are the ECH cipher suite, the identity of + // the ECH configuration, and the encapsulated key. + // + // version is the version of ECH indicated by the client. + // + // res.Status == ECHProviderStatusSuccess indicates the call was successful + // and the caller may proceed. res.Context is set. + // + // res.Status == ECHProviderStatusReject indicates the caller must reject + // ECH. res.RetryConfigs may be set. + // + // res.Status == ECHProviderStatusAbort indicates the caller should abort + // the handshake. Note that, in some cases, it's appropriate to reject + // rather than abort. In particular, aborting with "illegal_parameter" might + // "stick out". res.Alert and res.Error are set. + GetDecryptionContext(handle []byte, version uint16) (res ECHProviderResult) +} + +// ECHProviderStatus is the status of the ECH provider's response. +type ECHProviderStatus uint + +const ( + ECHProviderSuccess ECHProviderStatus = 0 + ECHProviderReject = 1 + ECHProviderAbort = 2 + + errHPKEInvalidPublicKey = "hpke: invalid KEM public key" +) + +// ECHProviderResult represents the result of invoking the ECH provider. +type ECHProviderResult struct { + Status ECHProviderStatus + + // Alert is the TLS alert sent by the caller when aborting the handshake. + Alert uint8 + + // Error is the error propagated by the caller when aborting the handshake. + Error error + + // RetryConfigs is the sequence of ECH configs to offer to the client for + // retrying the handshake. This may be set in case of success or rejection. + RetryConfigs []byte + + // Context is the server's HPKE context. This is set if ECH is not rejected + // by the provider and no error was reported. The data has the following + // format (in TLS syntax): + // + // enum { sealer(0), opener(1) } HpkeRole; + // + // struct { + // HpkeRole role; + // HpkeKemId kem_id; // as defined in draft-irtf-cfrg-hpke-07 + // HpkeKdfId kdf_id; // as defined in draft-irtf-cfrg-hpke-07 + // HpkeAeadId aead_id; // as defined in draft-irtf-cfrg-hpke-07 + // opaque exporter_secret<0..255>; + // opaque key<0..255>; + // opaque base_nonce<0..255>; + // opaque seq<0..255>; + // } HpkeContext; + Context []byte +} + +// EXP_ECHKeySet implements the ECHProvider interface for a sequence of ECH keys. +// +// NOTE: This API is EXPERIMENTAL and subject to change. +type EXP_ECHKeySet struct { + // The serialized ECHConfigs, in order of the server's preference. + configs []byte + + // Maps a configuration identifier to its secret key. + sk map[uint8]EXP_ECHKey +} + +// EXP_NewECHKeySet constructs an EXP_ECHKeySet. +func EXP_NewECHKeySet(keys []EXP_ECHKey) (*EXP_ECHKeySet, error) { + if len(keys) > 255 { + return nil, fmt.Errorf("tls: ech provider: unable to support more than 255 ECH configurations at once") + } + + keySet := new(EXP_ECHKeySet) + keySet.sk = make(map[uint8]EXP_ECHKey) + configs := make([]byte, 0) + for _, key := range keys { + if _, ok := keySet.sk[key.config.configId]; ok { + return nil, fmt.Errorf("tls: ech provider: ECH config conflict for configId %d", key.config.configId) + } + + keySet.sk[key.config.configId] = key + configs = append(configs, key.config.raw...) + } + + var b cryptobyte.Builder + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(configs) + }) + keySet.configs = b.BytesOrPanic() + + return keySet, nil +} + +// GetDecryptionContext is required by the ECHProvider interface. +func (keySet *EXP_ECHKeySet) GetDecryptionContext(rawHandle []byte, version uint16) (res ECHProviderResult) { + // Propagate retry configurations regardless of the result. The caller sends + // these to the clients only if it rejects. + res.RetryConfigs = keySet.configs + + // Ensure we know how to proceed, i.e., the caller has indicated a supported + // version of ECH. Currently only draft-ietf-tls-esni-13 is supported. + if version != extensionECH { + res.Status = ECHProviderAbort + res.Alert = uint8(alertInternalError) + res.Error = errors.New("version not supported") + return // Abort + } + + // Parse the handle. + s := cryptobyte.String(rawHandle) + handle := new(echContextHandle) + if !echReadContextHandle(&s, handle) || !s.Empty() { + // This is the result of a client-side error. However, aborting with + // "illegal_parameter" would stick out, so we reject instead. + res.Status = ECHProviderReject + res.RetryConfigs = keySet.configs + return // Reject + } + handle.raw = rawHandle + + // Look up the secret key for the configuration indicated by the client. + key, ok := keySet.sk[handle.configId] + if !ok { + res.Status = ECHProviderReject + res.RetryConfigs = keySet.configs + return // Reject + } + + // Ensure that support for the selected ciphersuite is indicated by the + // configuration. + suite := handle.suite + if !key.config.isPeerCipherSuiteSupported(suite) { + // This is the result of a client-side error. However, aborting with + // "illegal_parameter" would stick out, so we reject instead. + res.Status = ECHProviderReject + res.RetryConfigs = keySet.configs + return // Reject + } + + // Ensure the version indicated by the client matches the version supported + // by the configuration. + if version != key.config.version { + // This is the result of a client-side error. However, aborting with + // "illegal_parameter" would stick out, so we reject instead. + res.Status = ECHProviderReject + res.RetryConfigs = keySet.configs + return // Reject + } + + // Compute the decryption context. + opener, err := key.setupOpener(handle.enc, suite) + if err != nil { + if err.Error() == errHPKEInvalidPublicKey { + // This occurs if the KEM algorithm used to generate handle.enc is + // not the same as the KEM algorithm of the key. One way this can + // happen is if the client sent a GREASE ECH extension with a + // config_id that happens to match a known config, but which uses a + // different KEM algorithm. + res.Status = ECHProviderReject + res.RetryConfigs = keySet.configs + return // Reject + } + + res.Status = ECHProviderAbort + res.Alert = uint8(alertInternalError) + res.Error = err + return // Abort + } + + // Serialize the decryption context. + res.Context, err = opener.MarshalBinary() + if err != nil { + res.Status = ECHProviderAbort + res.Alert = uint8(alertInternalError) + res.Error = err + return // Abort + } + + res.Status = ECHProviderSuccess + return // Success +} + +// EXP_ECHKey represents an ECH key and its corresponding configuration. The +// encoding of an ECH Key has the format defined below (in TLS syntax). Note +// that the ECH standard does not specify this format. +// +// struct { +// opaque sk<0..2^16-1>; +// ECHConfig config<0..2^16>; // draft-ietf-tls-esni-13 +// } ECHKey; +type EXP_ECHKey struct { + sk kem.PrivateKey + config ECHConfig +} + +// EXP_UnmarshalECHKeys parses a sequence of ECH keys. +func EXP_UnmarshalECHKeys(raw []byte) ([]EXP_ECHKey, error) { + var ( + err error + key EXP_ECHKey + sk, config, contents cryptobyte.String + ) + s := cryptobyte.String(raw) + keys := make([]EXP_ECHKey, 0) +KeysLoop: + for !s.Empty() { + if !s.ReadUint16LengthPrefixed(&sk) || + !s.ReadUint16LengthPrefixed(&config) { + return nil, errors.New("error parsing key") + } + + key.config.raw = config + if !config.ReadUint16(&key.config.version) || + !config.ReadUint16LengthPrefixed(&contents) || + !config.Empty() { + return nil, errors.New("error parsing config") + } + + if key.config.version != extensionECH { + continue KeysLoop + } + if !readConfigContents(&contents, &key.config) { + return nil, errors.New("error parsing config contents") + } + + for _, suite := range key.config.suites { + if !hpke.KDF(suite.kdfId).IsValid() || + !hpke.AEAD(suite.aeadId).IsValid() { + continue KeysLoop + } + } + + kem := hpke.KEM(key.config.kemId) + if !kem.IsValid() { + continue KeysLoop + } + key.config.pk, err = kem.Scheme().UnmarshalBinaryPublicKey(key.config.rawPublicKey) + if err != nil { + return nil, fmt.Errorf("error parsing public key: %s", err) + } + key.sk, err = kem.Scheme().UnmarshalBinaryPrivateKey(sk) + if err != nil { + return nil, fmt.Errorf("error parsing secret key: %s", err) + } + + keys = append(keys, key) + } + return keys, nil +} + +// setupOpener computes the HPKE context used by the server in the ECH +// extension.i +func (key *EXP_ECHKey) setupOpener(enc []byte, suite hpkeSymmetricCipherSuite) (hpke.Opener, error) { + if key.config.raw == nil { + panic("raw config not set") + } + hpkeSuite, err := hpkeAssembleSuite( + key.config.kemId, + suite.kdfId, + suite.aeadId, + ) + if err != nil { + return nil, err + } + info := append(append([]byte(echHpkeInfoSetup), 0), key.config.raw...) + receiver, err := hpkeSuite.NewReceiver(key.sk, info) + if err != nil { + return nil, err + } + return receiver.Setup(enc) +} diff --git a/src/crypto/tls/ech_test.go b/src/crypto/tls/ech_test.go new file mode 100644 index 00000000000..ee908b92aa1 --- /dev/null +++ b/src/crypto/tls/ech_test.go @@ -0,0 +1,949 @@ +// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "testing" + "time" +) + +const ( + echTestBackendServerName = "example.com" + echTestClientFacingServerName = "cloudflare-esni.com" +) + +// The client's root CA certificate. +const echTestCertRootPEM = ` +-----BEGIN CERTIFICATE----- +MIICQTCCAeigAwIBAgIUYGSqOFcpxSleCzSCaveKL8lV4N0wCgYIKoZIzj0EAwIw +fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xHzAdBgNVBAoTFkludGVybmV0IFdpZGdldHMsIEluYy4xDDAK +BgNVBAsTA1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjAwOTIyMTcwNjAw +WhcNMjUwOTIxMTcwNjAwWjB/MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv +cm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEfMB0GA1UEChMWSW50ZXJuZXQg +V2lkZ2V0cywgSW5jLjEMMAoGA1UECxMDV1dXMRQwEgYDVQQDEwtleGFtcGxlLmNv +bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNcFaBtPRgekRBKTBvuKdTy3raqs +4IizMLFup434MfQ5oH71mYpKndfBzxcZDTMYeocKlt1pVYwvZ3ZdpRsW6yWjQjBA +MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ2GJIW ++4m3/qpkage5tEvMg3NwPTAKBggqhkjOPQQDAgNHADBEAiB6J8UqRvdhLOiaDYqH +KG+TuveHOqlfQqQgXo4/hNKMiAIgV79TTPHu+Ymn/tcCy9LVWZcpgnCEjrZi0ou5 +et8BX9s= +-----END CERTIFICATE-----` + +// Certificate of the client-facing server. The server name is +// "cloudflare-esni.com". +const echTestCertClientFacingPEM = ` +-----BEGIN CERTIFICATE----- +MIICIjCCAcigAwIBAgIUCXySp2MadlDlcvFrSm4BtLUY70owCgYIKoZIzj0EAwIw +fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xHzAdBgNVBAoTFkludGVybmV0IFdpZGdldHMsIEluYy4xDDAK +BgNVBAsTA1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjAwOTIyMTcxMDAw +WhcNMjEwOTIyMTcxMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7nP/ +Txinb0JPE/xdjv5d3zrWJqXo7qwP67oVaMKJp5ausJ+0IZfiMWz8pa6T7pyyLrC5 +xvQNkfVkpP9/FxmNFaOBoDCBnTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNN7Afv+ +CgPAxRr4QdZn8JFvQ9nTMB8GA1UdIwQYMBaAFDYYkhb7ibf+qmRqB7m0S8yDc3A9 +MB4GA1UdEQQXMBWCE2Nsb3VkZmxhcmUtZXNuaS5jb20wCgYIKoZIzj0EAwIDSAAw +RQIgZ4VlBtjTRludP/JwfaNQyGKZFWFqRsECvGPbk+ZHLZwCIQCTjuMAFrnjf/j5 +3RNw67l7+QQPrmurSO86l1IlDWNtcA== +-----END CERTIFICATE-----` + +// Signing key of the client-facing server. +const echTestKeyClientFacingPEM = ` +-----BEGIN PRIVATE KEY----- +MHcCAQEEIPpCcU8mu+h4xHAm18NJvn73Ko9fjH9QxDCpRt7kCIq9oAoGCCqGSM49 +AwEHoUQDQgAE7nP/Txinb0JPE/xdjv5d3zrWJqXo7qwP67oVaMKJp5ausJ+0IZfi +MWz8pa6T7pyyLrC5xvQNkfVkpP9/FxmNFQ== +-----END PRIVATE KEY-----` + +// Certificate of the backend server. The server name is "example.com". +const echTestCertBackendPEM = ` +-----BEGIN CERTIFICATE----- +MIICGTCCAcCgAwIBAgIUQJSSdOZs9wag1Toanlt9lol0uegwCgYIKoZIzj0EAwIw +fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xHzAdBgNVBAoTFkludGVybmV0IFdpZGdldHMsIEluYy4xDDAK +BgNVBAsTA1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjAwOTIyMTcwOTAw +WhcNMjEwOTIyMTcwOTAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElq+q +E01Z87KIPHWdEAk0cWssHkRnS4aQCDfstoxDIWQ4rMwHvrWGFy/vytRwyjhHuX9n +tc5ArCpwbAmY+oW/46OBmDCBlTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPz9Ct9U +EIjBEcUpv/yxHYccUDo1MB8GA1UdIwQYMBaAFDYYkhb7ibf+qmRqB7m0S8yDc3A9 +MBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMAoGCCqGSM49BAMCA0cAMEQCICDBEzzE +DF529x9Z4BkOKVxNDicfWSjxrcMohevjeCWDAiBaxXS5+6I2fcred0JGMsJgo7ts +S8GYhuKE99mQA0/mug== +-----END CERTIFICATE-----` + +// Signing key of the backend server. +const echTestKeyBackendPEM = ` +-----BEGIN PRIVATE KEY----- +MHcCAQEEIIJsLXmfzw6FDlqyRRLhY6lVB6ws5ewjUQjnS4DXsQ60oAoGCCqGSM49 +AwEHoUQDQgAElq+qE01Z87KIPHWdEAk0cWssHkRnS4aQCDfstoxDIWQ4rMwHvrWG +Fy/vytRwyjhHuX9ntc5ArCpwbAmY+oW/4w== +-----END PRIVATE KEY-----` + +// The ECH keys used by the client-facing server. +const echTestKeys = `-----BEGIN ECH KEYS----- +ACBpvnEYyFK6Ey4Pajbm6VaEsQp4bgRxoPVOs2wOiMuD+QBG/g0AQsMAIAAgCfU+ +VOBXjOut9a9m7wLhrZhHfM0GqE5BQLQK03DJf10ABAABAAElE2Nsb3VkZmxhcmUt +ZXNuaS5jb20AAAAguffuF8tjWUORwFbQ3+cDDqkMQuuMV7py7p1EJfM9S3IAZ/4N +AGMDABAAQQRhm1JRi7hkaK1HhcJq4ByJpK4fbsaD65xSqUuW0L53OYK3zEtz78pk +NhWC9NlkItWc2SYOTrGGHc5WhmJxKCTbAAQAAQABKhNjbG91ZGZsYXJlLWVzbmku +Y29tAAA= +-----END ECH KEYS-----` + +// A sequence of ECH keys with unsupported versions. +const echTestInvalidVersionKeys = `-----BEGIN ECH KEYS----- +ACDhS0q2cTU1Qzi6hPM4BQ/HLnbEUZyWdY2GbmS0DVkumgBIAfUARAAAIAAgi1Tu +jWJ236k1VAMeRnysKbDigxLpDs/AGdEowK8KiBkABAABAAEAAAATY2xvdWRmbGFy +ZS1lc25pLmNvbQAAACBmNj/zQe6OT/MR/MM39G6kwMJCJEXpdvTAkbdHErlgXwBI +AfUARAEAIAAgZ1Ru1uyGX6N9HYs5/pAE3KwUXRDBHD0Bdna8oP4uVEwABAABAAEA +AAATY2xvdWRmbGFyZS1lc25pLmNvbQAA +-----END ECH KEYS-----` + +// The sequence of ECH configurations corresponding to echTestKeys. +const echTestConfigs = `-----BEGIN ECH CONFIGS----- +AK3+DQBCwwAgACAJ9T5U4FeM6631r2bvAuGtmEd8zQaoTkFAtArTcMl/XQAEAAEA +ASUTY2xvdWRmbGFyZS1lc25pLmNvbQAA/g0AYwMAEABBBGGbUlGLuGRorUeFwmrg +HImkrh9uxoPrnFKpS5bQvnc5grfMS3PvymQ2FYL02WQi1ZzZJg5OsYYdzlaGYnEo +JNsABAABAAEqE2Nsb3VkZmxhcmUtZXNuaS5jb20AAA== +-----END ECH CONFIGS-----` + +// An invalid sequence of ECH configurations. +const echTestStaleConfigs = `-----BEGIN ECH CONFIGS----- +AK3+DQBCfgAgACA02DWuCoykTn5CZ/t+h3dXN2JLS5r5RlJPaOzH1UdnRgAEAAEA +ASUTY2xvdWRmbGFyZS1lc25pLmNvbQAA/g0AY8YAEABBBIpQ8lWXbmjAgaFg6TDf +si7tgaTV7fUMbrOZzCyKyIfv/cO872MYb9dvEH1Izu6LtKdGAlmKmu2pxtdpbsSW +CX0ABAABAAEqE2Nsb3VkZmxhcmUtZXNuaS5jb20AAA== +-----END ECH CONFIGS-----` + +// echTestProviderAlwaysAbort mocks an ECHProvider that, in response to any +// request, sets an alert and returns an error. The client-facing server must +// abort the handshake. +type echTestProviderAlwaysAbort struct{} + +// Required by the ECHProvider interface. +func (p echTestProviderAlwaysAbort) GetDecryptionContext(_ []byte, _ uint16) (res ECHProviderResult) { + res.Status = ECHProviderAbort + res.Alert = uint8(alertInternalError) + res.Error = errors.New("provider failed") + return // Abort +} + +// echTestProviderAlwaysReject simulates fallover of the ECH provider. In +// response to any query, it rejects without sending retry configurations., in response to any +type echTestProviderAlwaysReject struct{} + +// Required by the ECHProvider interface. +func (p echTestProviderAlwaysReject) GetDecryptionContext(_ []byte, _ uint16) (res ECHProviderResult) { + res.Status = ECHProviderReject + return // Reject without retry configs +} + +func echTestLoadConfigs(pemData string) []ECHConfig { + block, rest := pem.Decode([]byte(pemData)) + if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { + panic("pem decoding fails") + } + + configs, err := UnmarshalECHConfigs(block.Bytes) + if err != nil { + panic(err) + } + + return configs +} + +func echTestLoadKeySet(pemData string) *EXP_ECHKeySet { + block, rest := pem.Decode([]byte(pemData)) + if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { + panic("pem decoding fails") + } + + keys, err := EXP_UnmarshalECHKeys(block.Bytes) + if err != nil { + panic(err) + } + + keySet, err := EXP_NewECHKeySet(keys) + if err != nil { + panic(err) + } + + return keySet +} + +type echTestCase struct { + name string + + // expected outcomes + expectClientAbort bool // client aborts + expectServerAbort bool // server aborts + expectOffered bool // server indicates that ECH was offered + expectClientBypassed bool // server bypasses ECH + expectServerBypassed bool // server indicates that ECH was bypassed by client + expectAccepted bool // server indicates ECH acceptance + expectRejected bool // server indicates ECH rejection + expectGrease bool // server indicates dummy ECH was detected + expectBackendServerName bool // client verified backend server name + + // client config + clientEnabled bool // client enables ECH + clientStaleConfigs bool // client offers ECH with invalid config + clientNoConfigs bool // client sends dummy ECH if ECH enabled + clientInvalidTLSVersion bool // client does not offer 1.3 + + // server config + serverEnabled bool // server enables ECH + serverProviderAlwaysAbort bool // ECH provider always aborts + serverProviderAlwaysReject bool // ECH provider always rejects + serverProviderInvalidVersion bool // ECH provider uses configs with unsupported version + serverInvalidTLSVersion bool // server does not offer 1.3 + + // code path triggers + triggerHRR bool // server triggers HRR + triggerECHBypassAfterHRR bool // client bypasses after HRR + triggerECHBypassBeforeHRR bool // client bypasses before HRR + triggerIllegalHandleAfterHRR bool // client sends illegal ECH extension after HRR + triggerOuterExtMany bool // client sends many (not just one) outer extensions + triggerOuterExtIncorrectOrder bool // client sends malformed outer extensions + triggerOuterExtIllegal bool // client sends malformed outer extensions + triggerOuterExtNone bool // client does not incorporate outer extensions + triggerOuterIsInner bool // client sends "ech_is_inner" in ClientHelloOuter + triggerPayloadDecryptError bool // client sends inauthentic ciphertext +} + +// TODO(cjpatton): Add test cases for PSK interactions: +// - ECH bypassed, backend server consumes early data (baseline test config) +// - ECH accepted, backend server consumes early data +// - ECH rejected, client-facing server ignores early data intended for backend +var echTestCases = []echTestCase{ + { + // The client offers ECH and it is accepted by the server + name: "success / accepted", + expectOffered: true, + expectAccepted: true, + expectBackendServerName: true, + clientEnabled: true, + serverEnabled: true, + }, + { + // The client bypasses ECH, i.e., it neither offers ECH nor sends a + // dummy ECH extension. + name: "success / bypassed: not offered", + expectClientBypassed: true, + expectBackendServerName: true, + serverEnabled: true, + }, + { + // The client sends dummy (i.e., "GREASEd") ECH. The server sends retry + // configs in case the client meant to offer ECH. The client does not + // signal rejection, so the server concludes ECH was not offered. + name: "success / bypassed: grease", + expectGrease: true, + expectBackendServerName: true, + clientEnabled: true, + clientNoConfigs: true, + serverEnabled: true, + }, + { + // The client sends dummy ECH because it has enabled ECH but not TLS + // 1.3. The server sends retry configs in case the client meant to offer + // ECH. The client does not signal rejection, so the server concludes + // ECH was not offered. + name: "success / bypassed: client invalid version", + expectGrease: true, + expectBackendServerName: true, + clientInvalidTLSVersion: true, + clientEnabled: true, + serverEnabled: true, + }, + { + // The client offers ECH with an invalid (e.g., stale) config. The + // server sends retry configs. The client signals rejection by sending + // an "ech_required" alert. + name: "success / rejected: invalid config", + expectOffered: true, + expectRejected: true, + expectClientAbort: true, + expectServerAbort: true, + clientStaleConfigs: true, + clientEnabled: true, + serverEnabled: true, + }, + { + // The client offers ECH, but the payload is mangled in transit. The + // server sends retry configurations. The client signals rejection by + // sending an "ech_required" alert. + name: "success / rejected: inauthentic ciphertext", + expectOffered: true, + expectRejected: true, + expectClientAbort: true, + expectServerAbort: true, + clientEnabled: true, + serverEnabled: true, + triggerPayloadDecryptError: true, + }, + { + // The client offered ECH, but client-facing server terminates the + // connection without sending retry configurations. The client aborts + // with "ech_required" and regards ECH as securely disabled by the + // server. + name: "success / rejected: not supported by client-facing server", + expectServerBypassed: true, + expectClientAbort: true, + expectServerAbort: true, + clientEnabled: true, + }, + { + // The client offers ECH. The server ECH rejects without sending retry + // configurations, simulating fallover of the ECH provider. The client + // signals rejection. + name: "success / rejected: provider falls over", + expectServerAbort: true, + expectOffered: true, + expectServerBypassed: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + serverProviderAlwaysReject: true, + }, + { + // The client offers ECH. The server ECH rejects without sending retry + // configurations because the ECH provider returns configurations with + // unsupported versions only. + name: "success / rejected: provider invalid version", + expectServerAbort: true, + expectOffered: true, + expectServerBypassed: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + serverProviderInvalidVersion: true, + }, + { + // The client offers ECH. The server does not support TLS 1.3, so it + // ignores the extension and continues as usual. The client does not + // signal rejection because TLS 1.2 has been negotiated. + name: "success / bypassed: client-facing invalid version", + expectServerBypassed: true, + clientEnabled: true, + serverEnabled: true, + serverInvalidTLSVersion: true, + }, + { + // The client offers ECH. The ECH provider encounters an unrecoverable + // error, causing the server to abort. + name: "server abort: provider hard fails", + expectServerAbort: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + serverProviderAlwaysAbort: true, + }, + { + // The client offers ECH and it is accepted by the server. The HRR code + // path is triggered. + name: "hrr / accepted", + expectOffered: true, + expectAccepted: true, + expectBackendServerName: true, + triggerHRR: true, + clientEnabled: true, + serverEnabled: true, + }, + { + // The client sends a dummy ECH extension. The server sends retry + // configs in case the client meant to offer ECH. The client does not + // signal rejection, so the server concludes ECH was not offered. The + // HRR code path is triggered. + name: "hrr / bypassed: grease", + expectGrease: true, + expectBackendServerName: true, + clientEnabled: true, + clientNoConfigs: true, + serverEnabled: true, + triggerHRR: true, + }, + { + // The client offers ECH with an invalid (e.g., stale) config. The + // server sends retry configs. The client signals rejection. The HRR + // code path is triggered. + name: "hrr / rejected: invalid config", + expectOffered: true, + expectRejected: true, + expectClientAbort: true, + expectServerAbort: true, + clientEnabled: true, + clientStaleConfigs: true, + serverEnabled: true, + triggerHRR: true, + }, + { + // The HRR code path is triggered. The client offered ECH in the second + // CH but not the first. + name: "hrr / server abort: offer after bypass", + expectServerAbort: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + triggerHRR: true, + triggerECHBypassBeforeHRR: true, + }, + { + // The HRR code path is triggered. The client offered ECH in the first + // CH but not the second. + name: "hrr / server abort: bypass after offer", + expectOffered: true, + expectAccepted: true, + expectServerAbort: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + triggerHRR: true, + triggerECHBypassAfterHRR: true, + }, + { + // The HRR code path is triggered. In the second CH, the value of the + // context handle changes illegally. Specifically, the client sends a + // non-empty "config_id" and "enc". + name: "hrr / server abort: illegal handle", + expectOffered: true, + expectAccepted: true, + expectServerAbort: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + triggerHRR: true, + triggerIllegalHandleAfterHRR: true, + }, + { + // The client offers ECH and it is accepted by the server. The client + // incorporates many outer extensions instead of just one (the default + // behavior). + name: "outer extensions, many / accepted", + expectBackendServerName: true, + expectOffered: true, + expectAccepted: true, + clientEnabled: true, + serverEnabled: true, + triggerOuterExtMany: true, + }, + { + // The client offers ECH and it is accepted by the server. The client + // incorporates no outer extensions. + name: "outer extensions, none / accepted", + expectBackendServerName: true, + expectOffered: true, + expectAccepted: true, + clientEnabled: true, + serverEnabled: true, + triggerOuterExtNone: true, + }, + { + // The client offers ECH but does not implement the "outer_extension" + // mechanism correctly. Specifically, it sends them in the wrong order, + // causing the client and server to compute different transcripts. + name: "outer extensions, incorrect order / server abort: incorrect transcript", + expectOffered: true, + expectAccepted: true, + expectServerAbort: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + triggerOuterExtIncorrectOrder: true, + }, + { + // The client offers ECH but does not implement the "outer_extension" + // mechanism correctly. Specifically, the "outer extensions" contains + // the codepoint for the ECH extension itself. + name: "outer extensions, illegal: illegal parameter", + expectServerAbort: true, + expectClientAbort: true, + clientEnabled: true, + serverEnabled: true, + triggerOuterExtIllegal: true, + }, +} + +// Returns the base configurations for the client and client-facing server, +func echSetupConnTest() (clientConfig, serverConfig *Config) { + echTestNow := time.Date(2020, time.September, 23, 0, 0, 0, 0, time.UTC) + echTestConfig := &Config{ + Time: func() time.Time { + return echTestNow + }, + Rand: rand.Reader, + CipherSuites: allCipherSuites(), + InsecureSkipVerify: false, + } + + clientFacingCert, err := X509KeyPair([]byte(echTestCertClientFacingPEM), []byte(echTestKeyClientFacingPEM)) + if err != nil { + panic(err) + } + + backendCert, err := X509KeyPair([]byte(echTestCertBackendPEM), []byte(echTestKeyBackendPEM)) + if err != nil { + panic(err) + } + + block, rest := pem.Decode([]byte(echTestCertRootPEM)) + if block == nil || block.Type != "CERTIFICATE" || len(rest) > 0 { + panic("pem decoding fails") + } + + rootCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic(err) + } + + clientConfig = echTestConfig.Clone() + clientConfig.ServerName = echTestBackendServerName + clientConfig.RootCAs = x509.NewCertPool() + clientConfig.RootCAs.AddCert(rootCert) + + serverConfig = echTestConfig.Clone() + serverConfig.GetCertificate = func(info *ClientHelloInfo) (*Certificate, error) { + if info.ServerName == echTestBackendServerName { + return &backendCert, nil + } else if info.ServerName == echTestClientFacingServerName { + return &clientFacingCert, nil + } + return nil, nil + } + return +} + +// echTestResult represents the ECH status and error status of a connection. +type echTestResult struct { + // Operational parameters + clientDone, serverDone bool + // Results + clientStatus CFEventECHClientStatus + serverStatus CFEventECHServerStatus + connState ConnectionState + err error +} + +func (r *echTestResult) eventHandler(event CFEvent) { + switch e := event.(type) { + case CFEventECHClientStatus: + if r.clientDone { + panic("expected at most one client ECH status event") + } + r.clientStatus = e + r.clientDone = true + case CFEventECHServerStatus: + if r.serverDone { + panic("expected at most one server ECH status event") + } + r.serverStatus = e + r.clientDone = true + } +} + +// echTestConn runs the handshake and returns the ECH and error status of the +// client and server. It also returns the server name verified by the client. +func echTestConn(t *testing.T, clientConfig, serverConfig *Config) (clientRes, serverRes echTestResult) { + testMessage := []byte("hey bud") + buf := make([]byte, len(testMessage)) + ln := newLocalListener(t) + defer ln.Close() + + serverCh := make(chan echTestResult, 1) + go func() { + var res echTestResult + serverConfig.CFEventHandler = res.eventHandler + serverConn, err := ln.Accept() + if err != nil { + res.err = err + serverCh <- res + return + } + + server := Server(serverConn, serverConfig) + defer func() { + server.Close() + serverCh <- res + }() + + if err := server.Handshake(); err != nil { + res.err = err + return + } + + if _, err = server.Read(buf); err != nil { + res.err = err + } + + res.connState = server.ConnectionState() + }() + + clientConfig.CFEventHandler = clientRes.eventHandler + client, err := Dial("tcp", ln.Addr().String(), clientConfig) + if err != nil { + serverRes = <-serverCh + clientRes.err = err + return + } + defer client.Close() + + _, err = client.Write(testMessage) + if err != nil { + serverRes = <-serverCh + clientRes.err = err + return + } + + clientRes.connState = client.ConnectionState() + serverRes = <-serverCh + return +} + +func TestECHHandshake(t *testing.T) { + defer func() { + // Reset testing triggers after the test completes. + testingTriggerHRR = false + testingECHTriggerBypassAfterHRR = false + testingECHTriggerBypassBeforeHRR = false + testingECHIllegalHandleAfterHRR = false + testingECHOuterExtMany = false + testingECHOuterExtNone = false + testingECHOuterExtIncorrectOrder = false + testingECHOuterExtIllegal = false + testingECHTriggerPayloadDecryptError = false + }() + + staleConfigs := echTestLoadConfigs(echTestStaleConfigs) + configs := echTestLoadConfigs(echTestConfigs) + keySet := echTestLoadKeySet(echTestKeys) + invalidVersionKeySet := echTestLoadKeySet(echTestInvalidVersionKeys) + + clientConfig, serverConfig := echSetupConnTest() + for i, test := range echTestCases { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + // Configure the client. + n := 0 + if test.clientNoConfigs { + clientConfig.ClientECHConfigs = nil + n++ + } + if test.clientStaleConfigs { + clientConfig.ClientECHConfigs = staleConfigs + n++ + } + if n == 0 { + clientConfig.ClientECHConfigs = configs + } else if n > 1 { + panic("invalid test configuration") + } + + if test.clientEnabled { + clientConfig.ECHEnabled = true + } else { + clientConfig.ECHEnabled = false + } + + if test.clientInvalidTLSVersion { + clientConfig.MinVersion = VersionTLS10 + clientConfig.MaxVersion = VersionTLS12 + } else { + clientConfig.MinVersion = VersionTLS10 + clientConfig.MaxVersion = VersionTLS13 + } + + // Configure the client-facing server. + if test.serverEnabled { + serverConfig.ECHEnabled = true + } else { + serverConfig.ECHEnabled = false + } + + n = 0 + if test.serverProviderAlwaysAbort { + serverConfig.ServerECHProvider = &echTestProviderAlwaysAbort{} + n++ + } + if test.serverProviderAlwaysReject { + serverConfig.ServerECHProvider = &echTestProviderAlwaysReject{} + n++ + } + if test.serverProviderInvalidVersion { + serverConfig.ServerECHProvider = invalidVersionKeySet + n++ + } + if n == 0 { + serverConfig.ServerECHProvider = keySet + } else if n > 1 { + panic("invalid test configuration") + } + + if test.serverInvalidTLSVersion { + serverConfig.MinVersion = VersionTLS10 + serverConfig.MaxVersion = VersionTLS12 + } else { + serverConfig.MinVersion = VersionTLS10 + serverConfig.MaxVersion = VersionTLS13 + } + + testingTriggerHRR = false + if test.triggerHRR { + testingTriggerHRR = true + } + + testingECHTriggerBypassAfterHRR = false + if test.triggerECHBypassAfterHRR { + testingECHTriggerBypassAfterHRR = true + } + + testingECHTriggerBypassBeforeHRR = false + if test.triggerECHBypassBeforeHRR { + testingECHTriggerBypassBeforeHRR = true + } + + testingECHTriggerPayloadDecryptError = false + if test.triggerPayloadDecryptError { + testingECHTriggerPayloadDecryptError = true + } + + n = 0 + testingECHOuterExtMany = false + if test.triggerOuterExtMany { + testingECHOuterExtMany = true + n++ + } + testingECHOuterExtNone = false + if test.triggerOuterExtNone { + testingECHOuterExtNone = true + n++ + } + testingECHOuterExtIncorrectOrder = false + if test.triggerOuterExtIncorrectOrder { + testingECHOuterExtIncorrectOrder = true + n++ + } + testingECHOuterExtIllegal = false + if test.triggerOuterExtIllegal { + testingECHOuterExtIllegal = true + n++ + } + testingECHIllegalHandleAfterHRR = false + if test.triggerIllegalHandleAfterHRR { + testingECHIllegalHandleAfterHRR = true + n++ + } + if n > 1 { + panic("invalid test configuration") + } + + t.Logf("%s", test.name) + + // Run the handshake. + client, server := echTestConn(t, clientConfig, serverConfig) + if !test.expectClientAbort && client.err != nil { + t.Error("client aborts; want success") + } + + if !test.expectServerAbort && server.err != nil { + t.Error("server aborts; want success") + } + + if test.expectClientAbort && client.err == nil { + t.Error("client succeeds; want abort") + } else if client.err != nil { + t.Logf("client err: %s", client.err) + } + + if test.expectServerAbort && server.err == nil { + t.Errorf("server succeeds; want abort") + } else if server.err != nil { + t.Logf("server err: %s", server.err) + } + + if got := server.clientStatus.Offered(); got != test.expectOffered { + t.Errorf("got offered=%v; want %v", got, test.expectOffered) + } + + if got := server.clientStatus.Greased(); got != test.expectGrease { + t.Errorf("got grease=%v; want %v", got, test.expectGrease) + } + + if got := server.clientStatus.Bypassed(); got != test.expectClientBypassed && server.err == nil { + t.Errorf("got clientBypassed=%v; want %v", got, test.expectClientBypassed) + } + + if got := server.serverStatus.Bypassed(); got != test.expectServerBypassed && server.err == nil { + t.Errorf("got serverBypassed=%v; want %v", got, test.expectServerBypassed) + } + + if got := server.serverStatus.Accepted(); got != test.expectAccepted { + t.Errorf("got accepted=%v; want %v", got, test.expectAccepted) + } + + if got := server.serverStatus.Rejected(); got != test.expectRejected { + t.Errorf("got rejected=%v; want %v", got, test.expectRejected) + } + + if client.err != nil { + return + } + + if name := client.connState.ServerName; test.expectBackendServerName != (name == echTestBackendServerName) { + t.Errorf("got backend server name=%v; want %v", name == echTestBackendServerName, test.expectBackendServerName) + } + + if client.clientStatus.Greased() != server.clientStatus.Greased() || + client.clientStatus.Bypassed() != server.clientStatus.Bypassed() || + client.serverStatus.Bypassed() != server.serverStatus.Bypassed() || + client.serverStatus.Accepted() != server.serverStatus.Accepted() || + client.serverStatus.Rejected() != server.serverStatus.Rejected() { + t.Error("client and server disagree on ech usage") + t.Errorf("client=%+v", client) + t.Errorf("server=%+v", server) + } + + if accepted := client.connState.ECHAccepted; accepted != client.serverStatus.Accepted() { + t.Errorf("client got ECHAccepted=%v; want %v", accepted, client.serverStatus.Accepted()) + } + + if accepted := server.connState.ECHAccepted; accepted != server.serverStatus.Accepted() { + t.Errorf("server got ECHAccepted=%v; want %v", accepted, server.serverStatus.Accepted()) + } + }) + } +} + +func TestUnmarshalConfigs(t *testing.T) { + block, rest := pem.Decode([]byte(echTestConfigs)) + if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { + t.Fatal("pem decoding fails") + } + + configs, err := UnmarshalECHConfigs(block.Bytes) + if err != nil { + t.Fatal(err) + } + + if len(configs) != 2 { + t.Errorf("wrong number of configs: got %d; want %d", len(configs), 2) + } + + for i, config := range configs { + if len(config.suites) != 1 { + t.Errorf("wrong number of cipher suites in config #%d: got %d; want %d", i, len(config.suites), 1) + } + } + + for _, config := range configs { + if len(config.raw) == 0 { + t.Error("raw config not set") + } + } +} + +func TestUnmarshalKeys(t *testing.T) { + block, rest := pem.Decode([]byte(echTestKeys)) + if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 { + t.Fatal("pem decoding fails") + } + + keys, err := EXP_UnmarshalECHKeys(block.Bytes) + if err != nil { + t.Fatal(err) + } + + if len(keys) != 2 { + t.Errorf("wrong number of configs: got %d; want %d", len(keys), 2) + } + + for i, key := range keys { + if len(key.config.raw) == 0 { + t.Error("raw config not set") + } + + if len(key.config.suites) != 1 { + t.Errorf("wrong number of cipher suites in config #%d: got %d; want %d", i, len(key.config.suites), 1) + } + } +} + +func testECHProvider(t *testing.T, p ECHProvider, handle []byte, version uint16, want ECHProviderResult) { + got := p.GetDecryptionContext(handle, version) + if got.Status != want.Status { + t.Errorf("incorrect status: got %+v; want %+v", got.Status, want.Status) + } + if got.Alert != want.Alert { + t.Errorf("incorrect alert: got %+v; want %+v", got.Alert, want.Alert) + } + if got.Error != want.Error { + t.Errorf("incorrect error: got %+v; want %+v", got.Error, want.Error) + } + if !bytes.Equal(got.RetryConfigs, want.RetryConfigs) { + t.Errorf("incorrect retry configs: got %+v; want %+v", got.RetryConfigs, want.RetryConfigs) + } + if !bytes.Equal(got.Context, want.Context) { + t.Errorf("incorrect context: got %+v; want %+v", got.Context, want.Context) + } +} + +func TestECHProvider(t *testing.T) { + p := echTestLoadKeySet(echTestKeys) + t.Run("ok", func(t *testing.T) { + handle := []byte{ + 0, 1, 0, 1, 195, 0, 32, 49, 215, 32, 55, 8, 132, 98, 118, 166, 113, + 184, 40, 196, 151, 103, 20, 221, 148, 22, 72, 112, 152, 18, 20, 107, + 15, 109, 178, 15, 98, 104, 66, + } + context := []byte{ + 1, 0, 32, 0, 1, 0, 1, 32, 111, 237, 227, 138, 43, 202, 113, 109, + 127, 174, 36, 48, 232, 103, 97, 52, 76, 112, 136, 36, 220, 91, 12, + 21, 63, 194, 77, 110, 112, 25, 241, 135, 16, 214, 55, 95, 236, 101, + 6, 49, 56, 18, 215, 166, 137, 136, 225, 58, 54, 12, 229, 100, 254, + 43, 179, 2, 188, 179, 6, 166, 138, 138, 12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + } + testECHProvider(t, p, handle, extensionECH, ECHProviderResult{ + Status: ECHProviderSuccess, + RetryConfigs: p.configs, + Context: context, + }) + }) + t.Run("invalid config id", func(t *testing.T) { + handle := []byte{ + 0, 1, 0, 1, 255, 202, 62, 220, 1, 243, 58, 0, 32, 40, 52, 167, 167, + 21, 125, 151, 32, 250, 255, 1, 125, 206, 103, 62, 96, 189, 112, 126, + 48, 221, 41, 198, 146, 100, 149, 29, 133, 103, 87, 87, 78, + } + testECHProvider(t, p, handle, extensionECH, ECHProviderResult{ + Status: ECHProviderReject, + RetryConfigs: p.configs, + }) + }) + t.Run("invalid cipher suite", func(t *testing.T) { + handle := []byte{ + 99, 99, 0, 1, 8, 202, 62, 220, 1, 243, 58, 247, 102, 0, 32, 40, 52, + 167, 167, 21, 125, 151, 32, 250, 255, 1, 125, 206, 103, 62, 96, 189, + 112, 126, 48, 221, 41, 198, 146, 100, 149, 29, 133, 103, 87, 87, 78, + } + testECHProvider(t, p, handle, extensionECH, ECHProviderResult{ + Status: ECHProviderReject, + RetryConfigs: p.configs, + }) + }) + t.Run("malformed", func(t *testing.T) { + handle := []byte{ + 0, 1, 0, 1, 8, 202, 62, 220, 1, + } + testECHProvider(t, p, handle, extensionECH, ECHProviderResult{ + Status: ECHProviderReject, + RetryConfigs: p.configs, + }) + }) +} diff --git a/src/crypto/tls/handshake_client.go b/src/crypto/tls/handshake_client.go index 4f442401b83..fa75eefa194 100644 --- a/src/crypto/tls/handshake_client.go +++ b/src/crypto/tls/handshake_client.go @@ -36,7 +36,7 @@ type clientHandshakeState struct { session *ClientSessionState } -func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) { +func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, ecdheParameters, error) { config := c.config if len(config.ServerName) == 0 && !config.InsecureSkipVerify { return nil, nil, errors.New("tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config") @@ -54,7 +54,7 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) { return nil, nil, errors.New("tls: NextProtos values too large") } - supportedVersions := config.supportedVersions(roleClient) + supportedVersions := config.supportedVersionsFromMin(roleClient, minVersion) if len(supportedVersions) == 0 { return nil, nil, errors.New("tls: no supported versions satisfy MinVersion and MaxVersion") } @@ -157,13 +157,30 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { // need to be reset. c.didResume = false - hello, ecdheParams, err := c.makeClientHello() + // Determine the minimum required version for this handshake. + minVersion := c.config.MinVersion + if c.config.echCanOffer() { + // If the ECH extension will be offered in this handshake, then the + // ClientHelloInner must not offer TLS 1.2 or below. + minVersion = VersionTLS13 + } + + helloBase, ecdheParams, err := c.makeClientHello(minVersion) if err != nil { return err } - c.serverName = hello.serverName - cacheKey, session, earlySecret, binderKey := c.loadSession(hello) + hello, helloInner, err := c.echOfferOrGrease(helloBase) + if err != nil { + return err + } + + helloResumed := hello + if c.ech.offered { + helloResumed = helloInner + } + + cacheKey, session, earlySecret, binderKey := c.loadSession(helloResumed) if cacheKey != "" && session != nil { defer func() { // If we got a handshake failure when resuming a session, throw away @@ -217,6 +234,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { ctx: ctx, serverHello: serverHello, hello: hello, + helloInner: helloInner, ecdheParams: ecdheParams, session: session, earlySecret: earlySecret, @@ -228,6 +246,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { return hs.handshake() } + c.serverName = hello.serverName hs := &clientHandshakeState{ c: c, ctx: ctx, @@ -251,7 +270,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { func (c *Conn) loadSession(hello *clientHelloMsg) (cacheKey string, session *ClientSessionState, earlySecret, binderKey []byte) { - if c.config.SessionTicketsDisabled || c.config.ClientSessionCache == nil { + if c.config.SessionTicketsDisabled || c.config.ClientSessionCache == nil || c.config.ECHEnabled { return "", nil, nil, nil } @@ -864,10 +883,14 @@ func (c *Conn) verifyServerCertificate(certificates [][]byte) error { } if !c.config.InsecureSkipVerify { + dnsName := c.config.ServerName + if c.ech.offered && !c.ech.accepted { + dnsName = c.serverName + } opts := x509.VerifyOptions{ Roots: c.config.RootCAs, CurrentTime: c.config.time(), - DNSName: c.config.ServerName, + DNSName: dnsName, Intermediates: x509.NewCertPool(), } for _, cert := range certs[1:] { diff --git a/src/crypto/tls/handshake_client_tls13.go b/src/crypto/tls/handshake_client_tls13.go index 8f3f327b06d..b5ae137f206 100644 --- a/src/crypto/tls/handshake_client_tls13.go +++ b/src/crypto/tls/handshake_client_tls13.go @@ -10,6 +10,7 @@ import ( "crypto" "crypto/hmac" "crypto/rsa" + "crypto/subtle" "errors" "fmt" "hash" @@ -22,19 +23,21 @@ type clientHandshakeStateTLS13 struct { ctx context.Context serverHello *serverHelloMsg hello *clientHelloMsg + helloInner *clientHelloMsg ecdheParams ecdheParameters session *ClientSessionState earlySecret []byte binderKey []byte - certReq *certificateRequestMsgTLS13 - usingPSK bool - sentDummyCCS bool - suite *cipherSuiteTLS13 - transcript hash.Hash - masterSecret []byte - trafficSecret []byte // client_application_traffic_secret_0 + certReq *certificateRequestMsgTLS13 + usingPSK bool + sentDummyCCS bool + suite *cipherSuiteTLS13 + transcript hash.Hash + transcriptInner hash.Hash + masterSecret []byte + trafficSecret []byte // client_application_traffic_secret_0 hsTimings CFEventTLS13ClientHandshakeTimingInfo } @@ -106,6 +109,14 @@ func (hs *clientHandshakeStateTLS13) handshake() error { hs.transcript = hs.suite.hash.New() hs.transcript.Write(hs.hello.marshal()) + // When offering ECH, we don't know whether ECH was accepted or rejected + // until we get the server's response. Compute the transcript of both the + // inner and outer handshake until we know. + if c.ech.offered { + hs.transcriptInner = hs.suite.hash.New() + hs.transcriptInner.Write(hs.helloInner.marshal()) + } + if bytes.Equal(hs.serverHello.random, helloRetryRequestRandom) { if err := hs.sendDummyChangeCipherSpec(); err != nil { return err @@ -115,8 +126,40 @@ func (hs *clientHandshakeStateTLS13) handshake() error { } } + // Check for ECH acceptance confirmation. + if c.ech.offered { + echAcceptConfTranscript := cloneHash(hs.transcriptInner, hs.suite.hash) + if echAcceptConfTranscript == nil { + c.sendAlert(alertInternalError) + return errors.New("tls: internal error: failed to clone hash") + } + + sh := hs.serverHello.marshal() + echAcceptConfTranscript.Write(sh[:30]) + echAcceptConfTranscript.Write(zeros[:8]) + echAcceptConfTranscript.Write(sh[38:]) + echAcceptConf := hs.suite.expandLabel( + hs.suite.extract(hs.helloInner.random, nil), + echAcceptConfLabel, + echAcceptConfTranscript.Sum(nil), + 8) + + if subtle.ConstantTimeCompare(hs.serverHello.random[24:], echAcceptConf) == 1 { + c.ech.accepted = true + hs.hello = hs.helloInner + hs.transcript = hs.transcriptInner + } + } + hs.transcript.Write(hs.serverHello.marshal()) + // Resolve the server name now that ECH acceptance has been determined. + // + // NOTE(cjpatton): Currently the client sends the same ALPN extension in the + // ClientHelloInner and ClientHelloOuter. If that changes, then we'll need + // to resolve ALPN here as well. + c.serverName = hs.hello.serverName + c.buffering = true if err := hs.processServerHello(); err != nil { return err @@ -142,6 +185,9 @@ func (hs *clientHandshakeStateTLS13) handshake() error { if err := hs.sendClientFinished(); err != nil { return err } + if err := hs.abortIfRequired(); err != nil { + return err + } if _, err := c.flush(); err != nil { return err } @@ -233,6 +279,46 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { hs.transcript.Write(chHash) hs.transcript.Write(hs.serverHello.marshal()) + // Determine which ClientHello message was consumed by the server. If ECH + // was offered, this may be the ClientHelloInner or ClientHelloOuter. + hello := hs.hello + isInner := false + if c.ech.offered { + chHash = hs.transcriptInner.Sum(nil) + hs.transcriptInner.Reset() + hs.transcriptInner.Write([]byte{typeMessageHash, 0, 0, uint8(len(chHash))}) + hs.transcriptInner.Write(chHash) + + // Check for ECH acceptance confirmation. + if hs.serverHello.ech != nil { + if len(hs.serverHello.ech) != 8 { + c.sendAlert(alertDecodeError) + return errors.New("tls: ech: hrr: malformed acceptance signal") + } + + echAcceptConfHRRTranscript := cloneHash(hs.transcriptInner, hs.suite.hash) + if echAcceptConfHRRTranscript == nil { + c.sendAlert(alertInternalError) + return errors.New("tls: internal error: failed to clone hash") + } + + echAcceptConfHRR := echEncodeAcceptConfHelloRetryRequest(hs.serverHello.marshal()) + echAcceptConfHRRTranscript.Write(echAcceptConfHRR) + echAcceptConfHRRSignal := hs.suite.expandLabel( + hs.suite.extract(hs.helloInner.random, nil), + echAcceptConfHRRLabel, + echAcceptConfHRRTranscript.Sum(nil), + 8) + + if subtle.ConstantTimeCompare(hs.serverHello.ech, echAcceptConfHRRSignal) == 1 { + hello = hs.helloInner + isInner = true + } + } + + hs.transcriptInner.Write(hs.serverHello.marshal()) + } + // The only HelloRetryRequest extensions we support are key_share and // cookie, and clients must abort the handshake if the HRR would not result // in any change in the ClientHello. @@ -242,7 +328,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { } if hs.serverHello.cookie != nil { - hs.hello.cookie = hs.serverHello.cookie + hello.cookie = hs.serverHello.cookie } if hs.serverHello.serverShare.group != 0 { @@ -255,7 +341,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { // share for it this time. if curveID := hs.serverHello.selectedGroup; curveID != 0 { curveOK := false - for _, id := range hs.hello.supportedCurves { + for _, id := range hello.supportedCurves { if id == curveID { curveOK = true break @@ -279,11 +365,11 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { return err } hs.ecdheParams = params - hs.hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}} + hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}} } - hs.hello.raw = nil - if len(hs.hello.pskIdentities) > 0 { + hello.raw = nil + if len(hello.pskIdentities) > 0 { pskSuite := cipherSuiteTLS13ByID(hs.session.cipherSuite) if pskSuite == nil { return c.sendAlert(alertInternalError) @@ -291,23 +377,70 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { if pskSuite.hash == hs.suite.hash { // Update binders and obfuscated_ticket_age. ticketAge := uint32(c.config.time().Sub(hs.session.receivedAt) / time.Millisecond) - hs.hello.pskIdentities[0].obfuscatedTicketAge = ticketAge + hs.session.ageAdd + hello.pskIdentities[0].obfuscatedTicketAge = ticketAge + hs.session.ageAdd transcript := hs.suite.hash.New() transcript.Write([]byte{typeMessageHash, 0, 0, uint8(len(chHash))}) transcript.Write(chHash) transcript.Write(hs.serverHello.marshal()) - transcript.Write(hs.hello.marshalWithoutBinders()) + transcript.Write(hello.marshalWithoutBinders()) pskBinders := [][]byte{hs.suite.finishedHash(hs.binderKey, transcript)} - hs.hello.updateBinders(pskBinders) + hello.updateBinders(pskBinders) } else { // Server selected a cipher suite incompatible with the PSK. - hs.hello.pskIdentities = nil - hs.hello.pskBinders = nil + hello.pskIdentities = nil + hello.pskBinders = nil + } + } + + if isInner { + hs.helloInner = hello + hs.transcriptInner.Write(hs.helloInner.marshal()) + if err := c.echUpdateClientHelloOuter(hs.hello, hs.helloInner, nil); err != nil { + return err + } + } else { + hs.hello = hello + } + + if c.ech.offered && testingECHIllegalHandleAfterHRR { + hs.hello.raw = nil + + // Change the cipher suite and config id and set an encapsulated key in + // the updated ClientHello. This will trigger a server abort because the + // cipher suite and config id are supposed to match the previous + // ClientHello and the encapsulated key is supposed to be empty. + var ech echClientOuter + _, kdf, aead := c.ech.sealer.Suite().Params() + ech.handle.suite.kdfId = uint16(kdf) ^ 0xff + ech.handle.suite.aeadId = uint16(aead) ^ 0xff + ech.handle.configId = c.ech.configId ^ 0xff + ech.handle.enc = []byte{1, 2, 3, 4, 5} + ech.payload = []byte{1, 2, 3, 4, 5} + hs.hello.ech = ech.marshal() + } + + if testingECHTriggerBypassAfterHRR { + hs.hello.raw = nil + + // Don't send the ECH extension in the updated ClientHello. This will + // trigger a server abort, since this is illegal. + hs.hello.ech = nil + } + + if testingECHTriggerBypassBeforeHRR { + hs.hello.raw = nil + + // Send a dummy ECH extension in the updated ClientHello. This will + // trigger a server abort, since no ECH extension was sent in the + // previous ClientHello. + var err error + hs.hello.ech, err = echGenerateGreaseExt(c.config.rand()) + if err != nil { + return fmt.Errorf("tls: ech: failed to generate grease ECH: %s", err) } } - hs.transcript.Write(hs.hello.marshal()) if _, err := c.writeRecord(recordTypeHandshake, hs.hello.marshal()); err != nil { return err } @@ -328,6 +461,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { return err } + hs.transcript.Write(hs.hello.marshal()) return nil } @@ -366,6 +500,16 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error { return nil } + // Per the rules of draft-ietf-tls-esni-13, Section 6.1, the server is not + // permitted to resume a connection connection in the outer handshake. If + // ECH is rejected and the client-facing server replies with a + // "pre_shared_key" extension in its ServerHello, then the client MUST abort + // the handshake with an "illegal_parameter" alert. + if c.ech.offered && !c.ech.accepted { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: ech: client-facing server offered PSK after ECH rejection") + } + if int(hs.serverHello.selectedIdentity) >= len(hs.hello.pskIdentities) { c.sendAlert(alertIllegalParameter) return errors.New("tls: server selected an invalid PSK") @@ -453,6 +597,23 @@ func (hs *clientHandshakeStateTLS13) readServerParameters() error { } c.clientProtocol = encryptedExtensions.alpnProtocol + if c.ech.offered && len(encryptedExtensions.ech) > 0 { + if !c.ech.accepted { + // If the server rejects ECH, then it may send retry configurations. + // If present, we must check them for syntactic correctness and + // abort if they are not correct. + c.ech.retryConfigs = encryptedExtensions.ech + if _, err = UnmarshalECHConfigs(c.ech.retryConfigs); err != nil { + c.sendAlert(alertDecodeError) + return fmt.Errorf("tls: ech: failed to parse retry configs: %s", err) + } + } else { + // Retry configs must not be sent in the inner handshake. + c.sendAlert(alertUnsupportedExtension) + return errors.New("tls: ech: got retry configs after ECH acceptance") + } + } + hs.hsTimings.ReadEncryptedExtensions = hs.hsTimings.elapsedTime() return nil @@ -765,7 +926,7 @@ func (hs *clientHandshakeStateTLS13) sendClientFinished() error { c.out.setTrafficSecret(hs.suite, hs.trafficSecret) - if !c.config.SessionTicketsDisabled && c.config.ClientSessionCache != nil { + if !c.config.SessionTicketsDisabled && c.config.ClientSessionCache != nil && !c.config.ECHEnabled { c.resumptionSecret = hs.suite.deriveSecret(hs.masterSecret, resumptionLabel, hs.transcript) } @@ -779,7 +940,7 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error { return errors.New("tls: received new session ticket from a client") } - if c.config.SessionTicketsDisabled || c.config.ClientSessionCache == nil { + if c.config.SessionTicketsDisabled || c.config.ClientSessionCache == nil || c.config.ECHEnabled { return nil } @@ -822,3 +983,13 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error { return nil } + +func (hs *clientHandshakeStateTLS13) abortIfRequired() error { + c := hs.c + if c.ech.offered && !c.ech.accepted { + // If ECH was rejected, then abort the handshake. + c.sendAlert(alertECHRequired) + return errors.New("tls: ech: rejected") + } + return nil +} diff --git a/src/crypto/tls/handshake_messages.go b/src/crypto/tls/handshake_messages.go index 2f501bfc0a7..a58e684145b 100644 --- a/src/crypto/tls/handshake_messages.go +++ b/src/crypto/tls/handshake_messages.go @@ -94,6 +94,7 @@ type clientHelloMsg struct { pskModes []uint8 pskIdentities []pskIdentity pskBinders [][]byte + ech []byte } func (m *clientHelloMsg) marshal() []byte { @@ -123,6 +124,13 @@ func (m *clientHelloMsg) marshal() []byte { bWithoutExtensions := *b b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + if len(m.ech) > 0 { + // draft-ietf-tls-esni-13, "encrypted_client_hello" + b.AddUint16(extensionECH) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(m.ech) + }) + } if len(m.serverName) > 0 { // RFC 6066, Section 3 b.AddUint16(extensionServerName) @@ -408,6 +416,12 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool { } switch extension { + case extensionECH: + // draft-ietf-tls-esni-13, "encrypted_client_hello" + if len(extData) == 0 || + !extData.ReadBytes(&m.ech, len(extData)) { + return false + } case extensionServerName: // RFC 6066, Section 3 var nameList cryptobyte.String @@ -639,6 +653,7 @@ type serverHelloMsg struct { // HelloRetryRequest extensions cookie []byte selectedGroup CurveID + ech []byte } func (m *serverHelloMsg) marshal() []byte { @@ -744,6 +759,13 @@ func (m *serverHelloMsg) marshal() []byte { }) }) } + if len(m.ech) > 0 { + // draft-ietf-tls-esni-13, "encrypted_client_hello" + b.AddUint16(extensionECH) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(m.ech) + }) + } extensionsPresent = len(b.BytesOrPanic()) > 2 }) @@ -854,6 +876,11 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool { len(m.supportedPoints) == 0 { return false } + case extensionECH: + // draft-ietf-tls-esni-13, "encrypted_client_hello" + if !extData.ReadBytes(&m.ech, len(extData)) { + return false + } default: // Ignore unknown extensions. continue @@ -870,6 +897,7 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool { type encryptedExtensionsMsg struct { raw []byte alpnProtocol string + ech []byte } func (m *encryptedExtensionsMsg) marshal() []byte { @@ -891,6 +919,15 @@ func (m *encryptedExtensionsMsg) marshal() []byte { }) }) } + if len(m.ech) > 0 { + // draft-ietf-tls-esni-13, "encrypted_client_hello" + b.AddUint16(extensionECH) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + // If the client-facing server rejects ECH, then it may + // sends retry configurations here. + b.AddBytes(m.ech) + }) + } }) }) @@ -928,6 +965,11 @@ func (m *encryptedExtensionsMsg) unmarshal(data []byte) bool { return false } m.alpnProtocol = string(proto) + case extensionECH: + // draft-ietf-tls-esni-13 + if !extData.ReadBytes(&m.ech, len(extData)) { + return false + } default: // Ignore unknown extensions. continue diff --git a/src/crypto/tls/handshake_server.go b/src/crypto/tls/handshake_server.go index 22836842966..e4f75b1cb92 100644 --- a/src/crypto/tls/handshake_server.go +++ b/src/crypto/tls/handshake_server.go @@ -142,6 +142,15 @@ func (c *Conn) readClientHello(ctx context.Context) (*clientHelloMsg, error) { return nil, unexpectedMessageError(clientHello, msg) } + // NOTE(cjpatton): ECH usage is resolved before calling GetConfigForClient() + // or GetCertifciate(). Hence, it is not currently possible to reject ECH if + // we don't recognize the inner SNI. This may or may not be desirable in the + // future. + clientHello, err = c.echAcceptOrReject(clientHello, false) // afterHRR == false + if err != nil { + return nil, fmt.Errorf("tls: %s", err) // Alert sent. + } + var configForClient *Config originalConfig := c.config if c.config.GetConfigForClient != nil { @@ -400,7 +409,7 @@ func (hs *serverHandshakeState) cipherSuiteOk(c *cipherSuite) bool { func (hs *serverHandshakeState) checkForResumption() bool { c := hs.c - if c.config.SessionTicketsDisabled { + if c.config.SessionTicketsDisabled || c.config.ECHEnabled { return false } @@ -497,7 +506,7 @@ func (hs *serverHandshakeState) doFullHandshake() error { hs.hello.ocspStapling = true } - hs.hello.ticketSupported = hs.clientHello.ticketSupported && !c.config.SessionTicketsDisabled + hs.hello.ticketSupported = hs.clientHello.ticketSupported && !c.config.SessionTicketsDisabled && !c.config.ECHEnabled hs.hello.cipherSuite = hs.suite.id hs.finishedHash = newFinishedHash(hs.c.vers, hs.suite) diff --git a/src/crypto/tls/handshake_server_tls13.go b/src/crypto/tls/handshake_server_tls13.go index 82b3479afce..378bb17c9a0 100644 --- a/src/crypto/tls/handshake_server_tls13.go +++ b/src/crypto/tls/handshake_server_tls13.go @@ -46,6 +46,10 @@ type serverHandshakeStateTLS13 struct { hsTimings CFEventTLS13ServerHandshakeTimingInfo } +func (hs *serverHandshakeStateTLS13) echIsInner() bool { + return len(hs.clientHello.ech) == 1 && hs.clientHello.ech[0] == echClientHelloInnerVariant +} + // processDelegatedCredentialFromClient unmarshals the DelegatedCredential // offered by the client (if present) and validates it using the peer's // certificate. @@ -212,12 +216,42 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error { hs.hello.cipherSuite = hs.suite.id hs.transcript = hs.suite.hash.New() + // Resolve the server's preference for the ECDHE group. + supportedCurves := c.config.curvePreferences() + if testingTriggerHRR { + // A HelloRetryRequest (HRR) is sent if the client does not offer a key + // share for a curve supported by the server. To trigger this condition + // intentionally, we compute the set of ECDHE groups supported by both + // the client and server but for which the client did not offer a key + // share. + m := make(map[CurveID]bool) + for _, serverGroup := range c.config.curvePreferences() { + for _, clientGroup := range hs.clientHello.supportedCurves { + if clientGroup == serverGroup { + m[clientGroup] = true + } + } + } + for _, ks := range hs.clientHello.keyShares { + delete(m, ks.group) + } + supportedCurves = nil + for group := range m { + supportedCurves = append(supportedCurves, group) + } + if len(supportedCurves) == 0 { + // This occurs if the client offered a key share for each mutually + // supported group. + panic("failed to trigger HelloRetryRequest") + } + } + // Pick the ECDHE group in server preference order, but give priority to // groups with a key share, to avoid a HelloRetryRequest round-trip. var selectedGroup CurveID var clientKeyShare *keyShare GroupSelection: - for _, preferredGroup := range c.config.curvePreferences() { + for _, preferredGroup := range supportedCurves { for _, ks := range hs.clientHello.keyShares { if ks.group == preferredGroup { selectedGroup = ks.group @@ -272,7 +306,7 @@ GroupSelection: func (hs *serverHandshakeStateTLS13) checkForResumption() error { c := hs.c - if c.config.SessionTicketsDisabled { + if c.config.SessionTicketsDisabled || c.config.ECHEnabled { return nil } @@ -508,6 +542,39 @@ func (hs *serverHandshakeStateTLS13) doHelloRetryRequest(selectedGroup CurveID) selectedGroup: selectedGroup, } + // Decide whether to send "encrypted_client_hello" extension. + if hs.echIsInner() { + // Confirm ECH acceptance if this is the inner handshake. + echAcceptConfHRRTranscript := cloneHash(hs.transcript, hs.suite.hash) + if echAcceptConfHRRTranscript == nil { + c.sendAlert(alertInternalError) + return errors.New("tls: internal error: failed to clone hash") + } + + helloRetryRequest.ech = zeros[:8] + echAcceptConfHRR := helloRetryRequest.marshal() + echAcceptConfHRRTranscript.Write(echAcceptConfHRR) + echAcceptConfHRRSignal := hs.suite.expandLabel( + hs.suite.extract(hs.clientHello.random, nil), + echAcceptConfHRRLabel, + echAcceptConfHRRTranscript.Sum(nil), + 8) + + helloRetryRequest.ech = echAcceptConfHRRSignal + helloRetryRequest.raw = nil + } else if c.ech.greased { + // draft-ietf-tls-esni-13, Section 7.1: + // + // If sending a HelloRetryRequest, the server MAY include an + // "encrypted_client_hello" extension with a payload of 8 random bytes; + // see Section 10.9.4 for details. + helloRetryRequest.ech = make([]byte, 8) + if _, err := io.ReadFull(c.config.rand(), helloRetryRequest.ech); err != nil { + c.sendAlert(alertInternalError) + return fmt.Errorf("tls: internal error: rng failure: %s", err) + } + } + hs.transcript.Write(helloRetryRequest.marshal()) if _, err := c.writeRecord(recordTypeHandshake, helloRetryRequest.marshal()); err != nil { return err @@ -528,6 +595,11 @@ func (hs *serverHandshakeStateTLS13) doHelloRetryRequest(selectedGroup CurveID) return unexpectedMessageError(clientHello, msg) } + clientHello, err = c.echAcceptOrReject(clientHello, true) // afterHRR == true + if err != nil { + return fmt.Errorf("tls: %s", err) // Alert sent + } + if len(clientHello.keyShares) != 1 || clientHello.keyShares[0].group != selectedGroup { c.sendAlert(alertIllegalParameter) return errors.New("tls: client sent invalid key share in second ClientHello") @@ -615,6 +687,32 @@ func illegalClientHelloChange(ch, ch1 *clientHelloMsg) bool { func (hs *serverHandshakeStateTLS13) sendServerParameters() error { c := hs.c + // Confirm ECH acceptance. + if hs.echIsInner() { + // Clear the last 8 bytes of the ServerHello.random in preparation for + // computing the confirmation hint. + copy(hs.hello.random[24:], zeros[:8]) + + // Set the last 8 bytes of ServerHello.random to a string derived from + // the inner handshake. + echAcceptConfTranscript := cloneHash(hs.transcript, hs.suite.hash) + if echAcceptConfTranscript == nil { + c.sendAlert(alertInternalError) + return errors.New("tls: internal error: failed to clone hash") + } + echAcceptConfTranscript.Write(hs.clientHello.marshal()) + echAcceptConfTranscript.Write(hs.hello.marshal()) + + echAcceptConf := hs.suite.expandLabel( + hs.suite.extract(hs.clientHello.random, nil), + echAcceptConfLabel, + echAcceptConfTranscript.Sum(nil), + 8) + + copy(hs.hello.random[24:], echAcceptConf) + hs.hello.raw = nil + } + hs.transcript.Write(hs.clientHello.marshal()) hs.transcript.Write(hs.hello.marshal()) if _, err := c.writeRecord(recordTypeHandshake, hs.hello.marshal()); err != nil { @@ -662,6 +760,10 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error { encryptedExtensions.alpnProtocol = selectedProto c.clientProtocol = selectedProto + if !c.ech.accepted && len(c.ech.retryConfigs) > 0 { + encryptedExtensions.ech = c.ech.retryConfigs + } + hs.transcript.Write(encryptedExtensions.marshal()) if _, err := c.writeRecord(recordTypeHandshake, encryptedExtensions.marshal()); err != nil { return err @@ -804,7 +906,7 @@ func (hs *serverHandshakeStateTLS13) sendServerFinished() error { } func (hs *serverHandshakeStateTLS13) shouldSendSessionTickets() bool { - if hs.c.config.SessionTicketsDisabled { + if hs.c.config.SessionTicketsDisabled || hs.c.config.ECHEnabled { return false } diff --git a/src/crypto/tls/hpke.go b/src/crypto/tls/hpke.go new file mode 100644 index 00000000000..26c29e59a50 --- /dev/null +++ b/src/crypto/tls/hpke.go @@ -0,0 +1,41 @@ +// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "circl/hpke" + "errors" + "fmt" +) + +// The mandatory-to-implement HPKE cipher suite for use with the ECH extension. +var defaultHPKESuite hpke.Suite + +func init() { + var err error + defaultHPKESuite, err = hpkeAssembleSuite( + uint16(hpke.KEM_X25519_HKDF_SHA256), + uint16(hpke.KDF_HKDF_SHA256), + uint16(hpke.AEAD_AES128GCM), + ) + if err != nil { + panic(fmt.Sprintf("hpke: mandatory-to-implement cipher suite not supported: %s", err)) + } +} + +func hpkeAssembleSuite(kemId, kdfId, aeadId uint16) (hpke.Suite, error) { + kem := hpke.KEM(kemId) + if !kem.IsValid() { + return hpke.Suite{}, errors.New("KEM is not supported") + } + kdf := hpke.KDF(kdfId) + if !kdf.IsValid() { + return hpke.Suite{}, errors.New("KDF is not supported") + } + aead := hpke.AEAD(aeadId) + if !aead.IsValid() { + return hpke.Suite{}, errors.New("AEAD is not supported") + } + return hpke.NewSuite(kem, kdf, aead), nil +} diff --git a/src/crypto/tls/tls.go b/src/crypto/tls/tls.go index 37c961cd188..823deba20eb 100644 --- a/src/crypto/tls/tls.go +++ b/src/crypto/tls/tls.go @@ -5,6 +5,14 @@ // Package tls partially implements TLS 1.2, as specified in RFC 5246, // and TLS 1.3, as specified in RFC 8446. // +// This package implements the "Encrypted ClientHello (ECH)" extension, as +// specified by draft-ietf-tls-esni-13. This extension allows the client to +// encrypt its ClientHello to the public key of an ECH-service provider, known +// as the client-facing server. If successful, then the client-facing server +// forwards the decrypted ClientHello to the intended recipient, known as the +// backend server. The goal of this mechanism is to ensure that connections made +// to backend servers are indistinguishable from one another. +// // This package implements the "Delegated Credentials" extension, as // specified by draft-ietf-tls-subcerts-10. This extension allows the usage // of a limited delegation mechanism that allows a TLS peer to issue its own @@ -20,6 +28,25 @@ // valid for a short time (days, hours, or even minutes). package tls +// BUG(cjpatton): In order to achieve its security goal, the ECH extension +// requires padding in order to ensure that the length of handshake messages +// doesn't depend on who terminates the connection. This package does not yet +// implement server-side padding: see +// https://github.com/tlswg/draft-ietf-tls-esni/issues/264. + +// BUG(cjpatton): The interaction of the ECH extension with PSK has not yet been +// fully vetted. For now, the server disables session tickets if ECH is enabled. + +// BUG(cjpatton): Upon ECH rejection, if retry configurations are provided, then +// the client is expected to retry the connection. Otherwise, it may regard ECH +// as being securely disabled by the client-facing server. The client in this +// package does not attempt to retry the handshake. + +// BUG(cjpatton): If the client offers the ECH extension and the client-facing +// server rejects it, then only the client-facing server is authenticated. In +// particular, the client is expected to respond to a CertificateRequest with an +// empty certificate. This package does not yet implement this behavior. + // BUG(agl): The crypto/tls package only implements some countermeasures // against Lucky13 attacks on CBC-mode encryption, and only on SHA1 // variants. See http://www.isg.rhul.ac.uk/tls/TLStiming.pdf and diff --git a/src/crypto/tls/tls_cf.go b/src/crypto/tls/tls_cf.go index ab0447da005..b7e36dc8e1e 100644 --- a/src/crypto/tls/tls_cf.go +++ b/src/crypto/tls/tls_cf.go @@ -11,6 +11,13 @@ import ( "time" ) +const ( + // Constants for ECH status events. + echStatusBypassed = 1 + iota + echStatusInner + echStatusOuter +) + // To add a signature scheme from Circl // // 1. make sure it implements TLSScheme and CertificateScheme, @@ -153,3 +160,61 @@ func createTLS13ServerHandshakeTimingInfo(timerFunc func() time.Time) CFEventTLS start: timer(), } } + +// 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" +} diff --git a/src/crypto/tls/tls_cf_test.go b/src/crypto/tls/tls_cf_test.go index 53c3becf3df..69a7daba3ef 100644 --- a/src/crypto/tls/tls_cf_test.go +++ b/src/crypto/tls/tls_cf_test.go @@ -1,6 +1,8 @@ package tls import ( + "circl/hpke" + "crypto/rand" "testing" ) @@ -17,3 +19,49 @@ func TestPropagateCFControl(t *testing.T) { t.Errorf("failed to propagate CFControl: got %v; want %v", got, want) } } + +// If the client uses the wrong KEM algorithm to offer ECH, the ECH provider +// should reject rather than abort. We check for this condition by looking at +// the error returned by hpke.Receiver.Setup(). This test asserts that the +// CIRCL's HPKE implementation returns the error we expect. +func TestCirclHpkeKemAlgorithmMismatchError(t *testing.T) { + kem := hpke.KEM_P256_HKDF_SHA256 + kdf := hpke.KDF_HKDF_SHA256 + aead := hpke.AEAD_AES128GCM + suite := hpke.NewSuite(kem, kdf, aead) + _, sk, err := kem.Scheme().GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + + incorrectKEM := hpke.KEM_X25519_HKDF_SHA256 + incorrectSuite := hpke.NewSuite(incorrectKEM, kdf, aead) + incorrectPK, _, err := incorrectKEM.Scheme().GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + + // Generate an encapsulated key share with the incorrect KEM algorithm. + incorrectSender, err := incorrectSuite.NewSender(incorrectPK, []byte("some info string")) + if err != nil { + t.Fatal(err) + } + incorrectEnc, _, err := incorrectSender.Setup(rand.Reader) + if err != nil { + t.Fatal(err) + } + + // Attempt to parse an encapsulated key generated using the incorrect KEM + // algorithm. + receiver, err := suite.NewReceiver(sk, []byte("some info string")) + if err != nil { + t.Fatal(err) + } + + expectedErrorString := "hpke: invalid KEM public key" + if _, err := receiver.Setup(incorrectEnc); err == nil { + t.Errorf("expected error; got success") + } else if err.Error() != expectedErrorString { + t.Errorf("incorrect error string: got '%s'; want '%s'", err, expectedErrorString) + } +} diff --git a/src/crypto/tls/tls_test.go b/src/crypto/tls/tls_test.go index 3ce93846c1d..71295f972fe 100644 --- a/src/crypto/tls/tls_test.go +++ b/src/crypto/tls/tls_test.go @@ -790,7 +790,7 @@ func TestCloneNonFuncFields(t *testing.T) { switch fn := typ.Field(i).Name; fn { case "Rand": f.Set(reflect.ValueOf(io.Reader(os.Stdin))) - case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "CFEventHandler": + case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "ServerECHProvider", "CFEventHandler": // DeepEqual can't compare functions. If you add a // function field to this list, you must also change // TestCloneFuncFields to ensure that the func field is @@ -831,6 +831,10 @@ func TestCloneNonFuncFields(t *testing.T) { f.Set(reflect.ValueOf(RenegotiateOnceAsClient)) case "mutex", "autoSessionTicketKeys", "sessionTicketKeys": continue // these are unexported fields that are handled separately + case "ClientECHConfigs": + f.Set(reflect.ValueOf([]ECHConfig{ECHConfig{}})) + case "ECHEnabled": + f.Set(reflect.ValueOf(true)) case "CFControl": f.Set(reflect.ValueOf(&testCFControl{23})) default: