Send ECH acceptance signal from backend server.
This CL implements the backend server behavior described in Section 7.2
of draft-ietf-tls-esni-09.
Bug: 275
Change-Id: I2e162673ce564db0cb75fc9b71ef11ed15037f4b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/43924
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/ssl/handshake.cc b/ssl/handshake.cc
index eb630a5..b38f96a 100644
--- a/ssl/handshake.cc
+++ b/ssl/handshake.cc
@@ -126,6 +126,8 @@
SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg)
: ssl(ssl_arg),
+ ech_present(false),
+ ech_is_inner_present(false),
scts_requested(false),
needs_psk_binder(false),
handshake_finalized(false),
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index 22fa6a1..bc0a0d1 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -644,6 +644,12 @@
return ssl_hs_error;
}
+ if (hs->ech_present && hs->ech_is_inner_present) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+ return ssl_hs_error;
+ }
+
hs->state = state12_select_certificate;
return ssl_hs_ok;
}
diff --git a/ssl/internal.h b/ssl/internal.h
index 5545bec..58a3769 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1419,6 +1419,15 @@
const SSLMessage &msg, CBS *binders);
+// Encrypted Client Hello.
+
+// tls13_ech_accept_confirmation computes the server's ECH acceptance signal,
+// writing it to |out|. It returns true on success, and false on failure.
+bool tls13_ech_accept_confirmation(
+ SSL_HANDSHAKE *hs, bssl::Span<uint8_t> out,
+ bssl::Span<const uint8_t> server_hello_ech_conf);
+
+
// Handshake functions.
enum ssl_hs_wait_t {
@@ -1720,6 +1729,14 @@
// key_block is the record-layer key block for TLS 1.2 and earlier.
Array<uint8_t> key_block;
+ // ech_present, on the server, indicates whether the ClientHello contained an
+ // encrypted_client_hello extension.
+ bool ech_present : 1;
+
+ // ech_is_inner_present, on the server, indicates whether the ClientHello
+ // contained an ech_is_inner extension.
+ bool ech_is_inner_present : 1;
+
// scts_requested is true if the SCT extension is in the ClientHello.
bool scts_requested : 1;
@@ -1886,7 +1903,8 @@
bool ssl_ext_key_share_parse_clienthello(SSL_HANDSHAKE *hs, bool *out_found,
Array<uint8_t> *out_secret,
uint8_t *out_alert, CBS *contents);
-bool ssl_ext_key_share_add_serverhello(SSL_HANDSHAKE *hs, CBB *out);
+bool ssl_ext_key_share_add_serverhello(SSL_HANDSHAKE *hs, CBB *out,
+ bool dry_run);
bool ssl_ext_pre_shared_key_parse_serverhello(SSL_HANDSHAKE *hs,
uint8_t *out_alert,
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 7ec61fd..ce1a7a6 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -739,6 +739,33 @@
return true;
}
+static bool ext_ech_parse_clienthello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+ CBS *contents) {
+ if (contents != nullptr) {
+ hs->ech_present = true;
+ return true;
+ }
+ return true;
+}
+
+static bool ext_ech_is_inner_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
+ return true;
+}
+
+static bool ext_ech_is_inner_parse_clienthello(SSL_HANDSHAKE *hs,
+ uint8_t *out_alert,
+ CBS *contents) {
+ if (contents == nullptr) {
+ return true;
+ }
+ if (CBS_len(contents) > 0) {
+ *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+ return false;
+ }
+ hs->ech_is_inner_present = true;
+ return true;
+}
+
// Renegotiation indication.
//
@@ -2435,7 +2462,8 @@
return true;
}
-bool ssl_ext_key_share_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+bool ssl_ext_key_share_add_serverhello(SSL_HANDSHAKE *hs, CBB *out,
+ bool dry_run) {
uint16_t group_id;
CBB kse_bytes, public_key;
if (!tls1_get_shared_group(hs, &group_id) ||
@@ -2448,10 +2476,10 @@
!CBB_flush(out)) {
return false;
}
-
- hs->ecdh_public_key.Reset();
-
- hs->new_session->group_id = group_id;
+ if (!dry_run) {
+ hs->ecdh_public_key.Reset();
+ hs->new_session->group_id = group_id;
+ }
return true;
}
@@ -3128,7 +3156,15 @@
NULL,
ext_ech_add_clienthello,
ext_ech_parse_serverhello,
- ignore_parse_clienthello,
+ ext_ech_parse_clienthello,
+ dont_add_serverhello,
+ },
+ {
+ TLSEXT_TYPE_ech_is_inner,
+ NULL,
+ ext_ech_is_inner_add_clienthello,
+ forbid_parse_serverhello,
+ ext_ech_is_inner_parse_clienthello,
dont_add_serverhello,
},
{
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index b8e0eae..f1d4bfa 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -128,6 +128,7 @@
extensionDelegatedCredentials uint16 = 0x22 // draft-ietf-tls-subcerts-06
extensionDuplicate uint16 = 0xffff // not IANA assigned
extensionEncryptedClientHello uint16 = 0xfe09 // not IANA assigned
+ extensionECHIsInner uint16 = 0xda09 // not IANA assigned
)
// TLS signaling cipher suite values
@@ -799,10 +800,24 @@
// encrypted_client_hello extension containing a ClientECH structure.
ExpectClientECH bool
+ // ExpectServerAcceptECH causes the client to expect that the server will
+ // indicate ECH acceptance in the ServerHello.
+ ExpectServerAcceptECH bool
+
// SendECHRetryConfigs, if not empty, contains the ECH server's serialized
// retry configs.
SendECHRetryConfigs []byte
+ // SendEncryptedClientHello, when true, causes the client to send a
+ // placeholder encrypted_client_hello extension on the ClientHelloOuter
+ // message.
+ SendPlaceholderEncryptedClientHello bool
+
+ // SendECHIsInner, when non-nil, causes the client to send an ech_is_inner
+ // extension on the ClientHelloOuter message. When nil, the extension will
+ // be omitted.
+ SendECHIsInner []byte
+
// SwapNPNAndALPN switches the relative order between NPN and ALPN in
// both ClientHello and ServerHello.
SwapNPNAndALPN bool
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 3ebc070..bf89c01 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -19,6 +19,8 @@
"math/big"
"net"
"time"
+
+ "boringssl.googlesource.com/boringssl/ssl/test/runner/hpke"
)
type clientHandshakeState struct {
@@ -122,6 +124,7 @@
ocspStapling: !c.config.Bugs.NoOCSPStapling,
sctListSupported: !c.config.Bugs.NoSignedCertificateTimestamps,
serverName: c.config.ServerName,
+ echIsInner: c.config.Bugs.SendECHIsInner,
supportedCurves: c.config.curvePreferences(),
supportedPoints: []uint8{pointFormatUncompressed},
nextProtoNeg: len(c.config.NextProtos) > 0,
@@ -211,6 +214,16 @@
hello.alpsProtocols = append(hello.alpsProtocols, protocol)
}
+ if c.config.Bugs.SendPlaceholderEncryptedClientHello {
+ hello.clientECH = &clientECH{
+ hpkeKDF: hpke.HKDFSHA256,
+ hpkeAEAD: hpke.AES128GCM,
+ configID: []byte{0x02, 0x0d, 0xe8, 0xae, 0xf5, 0x8b, 0x59, 0xb5},
+ enc: []byte{0xe2, 0xf0, 0x96, 0x64, 0x18, 0x35, 0x10, 0xb3},
+ payload: []byte{0xa8, 0x53, 0x3a, 0x8d, 0xe8, 0x5f, 0x7c, 0xd7},
+ }
+ }
+
var keyShares map[CurveID]ecdhCurve
if maxVersion >= VersionTLS13 {
keyShares = make(map[CurveID]ecdhCurve)
@@ -740,13 +753,13 @@
hs.writeServerHash(helloRetryRequest.marshal())
hs.writeClientHash(secondHelloBytes)
}
- hs.writeServerHash(hs.serverHello.marshal())
if c.vers >= VersionTLS13 {
if err := hs.doTLS13Handshake(); err != nil {
return err
}
} else {
+ hs.writeServerHash(hs.serverHello.marshal())
if c.config.Bugs.EarlyChangeCipherSpec > 0 {
hs.establishKeys()
c.writeRecord(recordTypeChangeCipherSpec, []byte{1})
@@ -839,10 +852,6 @@
return errors.New("tls: session IDs did not match.")
}
- // Once the PRF hash is known, TLS 1.3 does not require a handshake
- // buffer.
- hs.finishedHash.discardHandshakeBuffer()
-
zeroSecret := hs.finishedHash.zeroSecret()
// Resolve PSK and compute the early secret.
@@ -891,6 +900,25 @@
hs.finishedHash.addEntropy(zeroSecret)
}
+ // Determine whether the server indicated ECH acceptance.
+
+ // Generate ServerHelloECHConf, which is identical to the ServerHello except
+ // that the last 8 bytes of the random value are zeroes.
+ echAcceptConfirmation := hs.finishedHash.deriveSecretPeek([]byte("ech accept confirmation"), hs.serverHello.marshalForECHConf())
+ serverAcceptedECH := bytes.Equal(echAcceptConfirmation[:8], hs.serverHello.random[24:])
+ if c.config.Bugs.ExpectServerAcceptECH && !serverAcceptedECH {
+ return errors.New("tls: server did not indicate ECH acceptance")
+ }
+ if !c.config.Bugs.ExpectServerAcceptECH && serverAcceptedECH {
+ return errors.New("tls: server indicated ECH acceptance")
+ }
+
+ // Once the PRF hash is known, TLS 1.3 does not require a handshake
+ // buffer.
+ hs.finishedHash.discardHandshakeBuffer()
+
+ hs.writeServerHash(hs.serverHello.marshal())
+
// Derive handshake traffic keys and switch read key to handshake
// traffic key.
clientHandshakeTrafficSecret := hs.finishedHash.deriveSecret(clientHandshakeTrafficLabel)
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 42b5eb5..a8ea67d 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -303,6 +303,7 @@
nextProtoNeg bool
serverName string
clientECH *clientECH
+ echIsInner []byte
ocspStapling bool
supportedCurves []CurveID
supportedPoints []uint8
@@ -422,7 +423,6 @@
})
}
if m.clientECH != nil {
- // https://tools.ietf.org/html/draft-ietf-tls-esni-09
body := newByteBuilder()
body.addU16(m.clientECH.hpkeKDF)
body.addU16(m.clientECH.hpkeAEAD)
@@ -435,6 +435,12 @@
body: body.finish(),
})
}
+ if m.echIsInner != nil {
+ extensions = append(extensions, extension{
+ id: extensionECHIsInner,
+ body: m.echIsInner,
+ })
+ }
if m.ocspStapling {
certificateStatusRequest := newByteBuilder()
// RFC 4366, section 3.6
@@ -1269,6 +1275,16 @@
return true
}
+// marshalForECHConf marshals |m|, but zeroes out the last 8 bytes of the
+// ServerHello.random.
+func (m *serverHelloMsg) marshalForECHConf() []byte {
+ serverHelloECHConf := *m
+ serverHelloECHConf.raw = nil
+ serverHelloECHConf.random = make([]byte, 32)
+ copy(serverHelloECHConf.random[:24], m.random)
+ return serverHelloECHConf.marshal()
+}
+
type encryptedExtensionsMsg struct {
raw []byte
extensions serverExtensions
diff --git a/ssl/test/runner/prf.go b/ssl/test/runner/prf.go
index 73251c6..b27f038 100644
--- a/ssl/test/runner/prf.go
+++ b/ssl/test/runner/prf.go
@@ -457,6 +457,16 @@
return hkdfExpandLabel(h.hash, h.secret, label, h.appendContextHashes(nil), h.hash.Size())
}
+// deriveSecretPeek is the same as deriveSecret, but it enables the caller to
+// tentatively append messages to the transcript. The |extraMessages| parameter
+// contains the bytes of these tentative messages.
+func (h *finishedHash) deriveSecretPeek(label []byte, extraMessages []byte) []byte {
+ hashPeek := h.hash.New()
+ hashPeek.Write(h.buffer)
+ hashPeek.Write(extraMessages)
+ return hkdfExpandLabel(h.hash, h.secret, label, hashPeek.Sum(nil), h.hash.Size())
+}
+
// The following are context strings for CertificateVerify in TLS 1.3.
var (
clientCertificateVerifyContextTLS13 = []byte("TLS 1.3, client CertificateVerify")
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 3b0bd85..6f1f0ab 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -16246,6 +16246,95 @@
expectedLocalError: "remote error: error decoding message",
expectedError: ":ERROR_PARSING_EXTENSION:",
})
+
+ // Test that the server responds to an empty ECH extension with the acceptance
+ // confirmation.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ECH-Server-ECHIsInner",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ SendECHIsInner: []byte{},
+ ExpectServerAcceptECH: true,
+ },
+ },
+ })
+
+ // Test that server fails the handshake when it sees a nonempty ech_is_inner
+ // extension.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ECH-Server-ECHIsInner-NotEmpty",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ SendECHIsInner: []byte{42, 42, 42},
+ },
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":ERROR_PARSING_EXTENSION:",
+ })
+
+ // When ech_is_inner extension is absent, the server should not accept ECH.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ECH-Server-ECHIsInner-Absent",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectServerAcceptECH: false,
+ },
+ },
+ })
+
+ // Test that a TLS 1.3 server that receives an ech_is_inner extension can
+ // negotiate TLS 1.2 without clobbering the downgrade signal.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ECH-Server-ECHIsInner-Absent-TLS12",
+ config: Config{
+ MinVersion: VersionTLS12,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ // Omit supported_versions extension so the server negotiates
+ // TLS 1.2.
+ OmitSupportedVersions: true,
+ SendECHIsInner: []byte{},
+ },
+ },
+ // Check that the client sees the TLS 1.3 downgrade signal in
+ // ServerHello.random.
+ shouldFail: true,
+ expectedLocalError: "tls: downgrade from TLS 1.3 detected",
+ })
+
+ // Test that the handshake fails when the client sends both
+ // encrypted_client_hello and ech_is_inner extensions.
+ //
+ // TODO(dmcardle) Replace this test once runner is capable of sending real
+ // ECH extensions. The equivalent test will send ech_is_inner and a real
+ // encrypted_client_hello extension derived from a key unknown to the
+ // server.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ECH-Server-EncryptedClientHello-ECHIsInner",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ SendECHIsInner: []byte{},
+ SendPlaceholderEncryptedClientHello: true,
+ },
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":UNEXPECTED_EXTENSION:",
+ })
}
func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
diff --git a/ssl/tls13_enc.cc b/ssl/tls13_enc.cc
index 198adb6..cda53ec 100644
--- a/ssl/tls13_enc.cc
+++ b/ssl/tls13_enc.cc
@@ -507,4 +507,40 @@
return true;
}
+bool tls13_ech_accept_confirmation(
+ SSL_HANDSHAKE *hs, bssl::Span<uint8_t> out,
+ bssl::Span<const uint8_t> server_hello_ech_conf) {
+ // Compute the hash of the transcript concatenated with
+ // |server_hello_ech_conf| without modifying |hs->transcript|.
+ uint8_t context_hash[EVP_MAX_MD_SIZE];
+ unsigned context_hash_len;
+ ScopedEVP_MD_CTX ctx;
+ if (!hs->transcript.CopyToHashContext(ctx.get(), hs->transcript.Digest()) ||
+ !EVP_DigestUpdate(ctx.get(), server_hello_ech_conf.data(),
+ server_hello_ech_conf.size()) ||
+ !EVP_DigestFinal_ex(ctx.get(), context_hash, &context_hash_len)) {
+ return false;
+ }
+
+ // Per draft-ietf-tls-esni-09, accept_confirmation is computed with
+ // Derive-Secret, which derives a secret of size Hash.length. That value is
+ // then truncated to the first 8 bytes. Note this differs from deriving an
+ // 8-byte secret because the target length is included in the derivation.
+ uint8_t accept_confirmation_buf[EVP_MAX_MD_SIZE];
+ bssl::Span<uint8_t> accept_confirmation =
+ MakeSpan(accept_confirmation_buf, hs->transcript.DigestLen());
+ if (!hkdf_expand_label(accept_confirmation, hs->transcript.Digest(),
+ hs->secret(), label_to_span("ech accept confirmation"),
+ MakeConstSpan(context_hash, context_hash_len))) {
+ return false;
+ }
+
+ if (out.size() > accept_confirmation.size()) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+ OPENSSL_memcpy(out.data(), accept_confirmation.data(), out.size());
+ return true;
+}
+
BSSL_NAMESPACE_END
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 9dfe325..c072a6f 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -636,20 +636,58 @@
static enum ssl_hs_wait_t do_send_server_hello(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl;
+ Span<uint8_t> random(ssl->s3->server_random);
+ RAND_bytes(random.data(), random.size());
+
+ // If the ClientHello has an ech_is_inner extension, we must be the ECH
+ // backend server. In response to ech_is_inner, we will overwrite part of the
+ // ServerHello.random with the ECH acceptance confirmation.
+ if (hs->ech_is_inner_present) {
+ // Construct the ServerHelloECHConf message, which is the same as
+ // ServerHello, except the last 8 bytes of its random field are zeroed out.
+ Span<uint8_t> random_suffix = random.subspan(24);
+ OPENSSL_memset(random_suffix.data(), 0, random_suffix.size());
+
+ ScopedCBB cbb;
+ CBB body, extensions, session_id;
+ if (!ssl->method->init_message(ssl, cbb.get(), &body,
+ SSL3_MT_SERVER_HELLO) ||
+ !CBB_add_u16(&body, TLS1_2_VERSION) ||
+ !CBB_add_bytes(&body, random.data(), random.size()) ||
+ !CBB_add_u8_length_prefixed(&body, &session_id) ||
+ !CBB_add_bytes(&session_id, hs->session_id, hs->session_id_len) ||
+ !CBB_add_u16(&body, SSL_CIPHER_get_protocol_id(hs->new_cipher)) ||
+ !CBB_add_u8(&body, 0) ||
+ !CBB_add_u16_length_prefixed(&body, &extensions) ||
+ !ssl_ext_pre_shared_key_add_serverhello(hs, &extensions) ||
+ !ssl_ext_key_share_add_serverhello(hs, &extensions, /*dry_run=*/true) ||
+ !ssl_ext_supported_versions_add_serverhello(hs, &extensions) ||
+ !CBB_flush(cbb.get())) {
+ return ssl_hs_error;
+ }
+
+ // Note that |cbb| includes the message type and length fields, but not the
+ // record layer header.
+ if (!tls13_ech_accept_confirmation(
+ hs, random_suffix,
+ bssl::MakeConstSpan(CBB_data(cbb.get()), CBB_len(cbb.get())))) {
+ return ssl_hs_error;
+ }
+ }
+
// Send a ServerHello.
ScopedCBB cbb;
CBB body, extensions, session_id;
if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_SERVER_HELLO) ||
!CBB_add_u16(&body, TLS1_2_VERSION) ||
- !RAND_bytes(ssl->s3->server_random, sizeof(ssl->s3->server_random)) ||
- !CBB_add_bytes(&body, ssl->s3->server_random, SSL3_RANDOM_SIZE) ||
+ !CBB_add_bytes(&body, random.data(), random.size()) ||
!CBB_add_u8_length_prefixed(&body, &session_id) ||
!CBB_add_bytes(&session_id, hs->session_id, hs->session_id_len) ||
!CBB_add_u16(&body, SSL_CIPHER_get_protocol_id(hs->new_cipher)) ||
!CBB_add_u8(&body, 0) ||
!CBB_add_u16_length_prefixed(&body, &extensions) ||
!ssl_ext_pre_shared_key_add_serverhello(hs, &extensions) ||
- !ssl_ext_key_share_add_serverhello(hs, &extensions) ||
+ !ssl_ext_key_share_add_serverhello(hs, &extensions, /*dry_run=*/false) ||
!ssl_ext_supported_versions_add_serverhello(hs, &extensions) ||
!ssl_add_message_cbb(ssl, cbb.get())) {
return ssl_hs_error;