Add ECH server (draft-ietf-tls-esni-09).
This CL adds an initial implementation of the ECH server, with pieces of
the client in BoGo as necessary for testing. In particular, the server
supports ClientHelloInner compression with ech_outer_extensions. When
ECH decryption fails, it can send retry_configs back to the client.
This server passes the "ech-accept" and "ech-reject" test cases in
tls-interop-runner[0] when tested against both the cloudflare-go and nss
clients. For reproducibility, I started with the main branch at commit
707604c262d8bcf3e944ed1d5a675077304732ce and updated the endpoint's
script to pass the server's ECHConfig and private key to the boringssl
tool.
Follow-up CLs will update HPKE to the latest draft and catch us up to
draft-10.
[0]: https://github.com/xvzcf/tls-interop-runner
Bug: 275
Change-Id: I49be35af46d1fd5dd9c62252f07d0bae179381ab
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/45285
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index 44c3904..da85b1f 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -58,6 +58,9 @@
SSL,296,DUPLICATE_SIGNATURE_ALGORITHM
SSL,283,EARLY_DATA_NOT_IN_USE
SSL,144,ECC_CERT_NOT_FOR_SIGNING
+SSL,310,ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH
+SSL,311,ECH_SERVER_CONFIG_UNSUPPORTED_EXTENSION
+SSL,313,ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS
SSL,282,EMPTY_HELLO_RETRY_REQUEST
SSL,145,EMS_STATE_INCONSISTENT
SSL,146,ENCRYPTED_LENGTH_TOO_LONG
@@ -76,6 +79,7 @@
SSL,157,INAPPROPRIATE_FALLBACK
SSL,303,INCONSISTENT_CLIENT_HELLO
SSL,259,INVALID_ALPN_PROTOCOL
+SSL,314,INVALID_CLIENT_HELLO_INNER
SSL,158,INVALID_COMMAND
SSL,256,INVALID_COMPRESSION_LIST
SSL,301,INVALID_DELEGATED_CREDENTIAL
@@ -226,6 +230,7 @@
SSL,236,UNSAFE_LEGACY_RENEGOTIATION_DISABLED
SSL,237,UNSUPPORTED_CIPHER
SSL,238,UNSUPPORTED_COMPRESSION_ALGORITHM
+SSL,312,UNSUPPORTED_ECH_SERVER_CONFIG
SSL,239,UNSUPPORTED_ELLIPTIC_CURVE
SSL,240,UNSUPPORTED_PROTOCOL
SSL,252,UNSUPPORTED_PROTOCOL_FOR_CUSTOM_KEY
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index fa44d8e..9f9cf7b 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -32,9 +32,6 @@
#define KEM_CONTEXT_LEN (2 * X25519_PUBLIC_VALUE_LEN)
-// HPKE KEM scheme IDs.
-#define HPKE_DHKEM_X25519_HKDF_SHA256 0x0020
-
// This is strlen("HPKE") + 3 * sizeof(uint16_t).
#define HPKE_SUITE_ID_LEN 10
@@ -51,8 +48,8 @@
// that the suite_id used outside of the KEM also includes the kdf_id and
// aead_id.
static const uint8_t kX25519SuiteID[] = {
- 'K', 'E', 'M', HPKE_DHKEM_X25519_HKDF_SHA256 >> 8,
- HPKE_DHKEM_X25519_HKDF_SHA256 & 0x00ff};
+ 'K', 'E', 'M', EVP_HPKE_DHKEM_X25519_HKDF_SHA256 >> 8,
+ EVP_HPKE_DHKEM_X25519_HKDF_SHA256 & 0x00ff};
// The suite_id for non-KEM pieces of HPKE is defined as concat("HPKE",
// I2OSP(kem_id, 2), I2OSP(kdf_id, 2), I2OSP(aead_id, 2)).
@@ -61,7 +58,7 @@
CBB cbb;
int ret = CBB_init_fixed(&cbb, out, HPKE_SUITE_ID_LEN) &&
add_label_string(&cbb, "HPKE") &&
- CBB_add_u16(&cbb, HPKE_DHKEM_X25519_HKDF_SHA256) &&
+ CBB_add_u16(&cbb, EVP_HPKE_DHKEM_X25519_HKDF_SHA256) &&
CBB_add_u16(&cbb, kdf_id) &&
CBB_add_u16(&cbb, aead_id);
CBB_cleanup(&cbb);
@@ -126,6 +123,14 @@
return 1;
}
+uint16_t EVP_HPKE_CTX_get_aead_id(const EVP_HPKE_CTX *hpke) {
+ return hpke->aead_id;
+}
+
+uint16_t EVP_HPKE_CTX_get_kdf_id(const EVP_HPKE_CTX *hpke) {
+ return hpke->kdf_id;
+}
+
const EVP_AEAD *EVP_HPKE_get_aead(uint16_t aead_id) {
switch (aead_id) {
case EVP_HPKE_AEAD_AES_128_GCM:
diff --git a/crypto/hpke/internal.h b/crypto/hpke/internal.h
index 5bd6508..bf705b9 100644
--- a/crypto/hpke/internal.h
+++ b/crypto/hpke/internal.h
@@ -33,6 +33,9 @@
//
// See https://tools.ietf.org/html/draft-irtf-cfrg-hpke-07.
+// EVP_HPKE_DHKEM_* are KEM identifiers.
+#define EVP_HPKE_DHKEM_X25519_HKDF_SHA256 0x0020
+
// EVP_HPKE_AEAD_* are AEAD identifiers.
#define EVP_HPKE_AEAD_AES_128_GCM 0x0001
#define EVP_HPKE_AEAD_AES_256_GCM 0x0002
@@ -224,6 +227,16 @@
// set up as a sender.
OPENSSL_EXPORT size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *hpke);
+// EVP_HPKE_CTX_get_aead_id returns |hpke|'s configured AEAD. The returned value
+// is one of the |EVP_HPKE_AEAD_*| constants, or zero if the context has not
+// been set up.
+OPENSSL_EXPORT uint16_t EVP_HPKE_CTX_get_aead_id(const EVP_HPKE_CTX *hpke);
+
+// EVP_HPKE_CTX_get_aead_id returns |hpke|'s configured KDF. The returned value
+// is one of the |EVP_HPKE_HKDF_*| constants, or zero if the context has not
+// been set up.
+OPENSSL_EXPORT uint16_t EVP_HPKE_CTX_get_kdf_id(const EVP_HPKE_CTX *hpke);
+
// EVP_HPKE_get_aead returns the AEAD corresponding to |aead_id|, or NULL if
// |aead_id| is not a known AEAD identifier.
OPENSSL_EXPORT const EVP_AEAD *EVP_HPKE_get_aead(uint16_t aead_id);
diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt
index 98a959a..62652cb 100644
--- a/fuzz/CMakeLists.txt
+++ b/fuzz/CMakeLists.txt
@@ -29,3 +29,4 @@
fuzzer(dtls_client ssl)
fuzzer(ssl_ctx_api ssl)
fuzzer(session ssl)
+fuzzer(decode_client_hello_inner ssl)
diff --git a/fuzz/decode_client_hello_inner.cc b/fuzz/decode_client_hello_inner.cc
new file mode 100644
index 0000000..db090c5
--- /dev/null
+++ b/fuzz/decode_client_hello_inner.cc
@@ -0,0 +1,52 @@
+/* Copyright (c) 2021, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include <openssl/bytestring.h>
+#include <openssl/ssl.h>
+#include <openssl/span.h>
+
+#include "../ssl/internal.h"
+
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) {
+ static bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+ static bssl::UniquePtr<SSL> ssl(SSL_new(ctx.get()));
+
+ CBS reader(bssl::MakeConstSpan(buf, len));
+ CBS encoded_client_hello_inner_cbs;
+
+ if (!CBS_get_u24_length_prefixed(&reader, &encoded_client_hello_inner_cbs)) {
+ return 0;
+ }
+
+ bssl::Array<uint8_t> encoded_client_hello_inner;
+ if (!encoded_client_hello_inner.CopyFrom(encoded_client_hello_inner_cbs)) {
+ return 0;
+ }
+
+ // Use the remaining bytes in |reader| as the ClientHelloOuter.
+ SSL_CLIENT_HELLO client_hello_outer;
+ if (!bssl::ssl_client_hello_init(ssl.get(), &client_hello_outer, reader)) {
+ return 0;
+ }
+
+ // Recover the ClientHelloInner from the EncodedClientHelloInner and
+ // ClientHelloOuter.
+ uint8_t alert_unused;
+ bssl::Array<uint8_t> client_hello_inner;
+ bssl::ssl_decode_client_hello_inner(
+ ssl.get(), &alert_unused, &client_hello_inner, encoded_client_hello_inner,
+ &client_hello_outer);
+ return 0;
+}
diff --git a/include/openssl/base.h b/include/openssl/base.h
index 61a8886..e5fe146 100644
--- a/include/openssl/base.h
+++ b/include/openssl/base.h
@@ -431,6 +431,7 @@
typedef struct srtp_protection_profile_st SRTP_PROTECTION_PROFILE;
typedef struct ssl_cipher_st SSL_CIPHER;
typedef struct ssl_ctx_st SSL_CTX;
+typedef struct ssl_ech_server_config_list_st SSL_ECH_SERVER_CONFIG_LIST;
typedef struct ssl_method_st SSL_METHOD;
typedef struct ssl_private_key_method_st SSL_PRIVATE_KEY_METHOD;
typedef struct ssl_quic_method_st SSL_QUIC_METHOD;
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index f420fda..16b64c1 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3575,7 +3575,7 @@
enum ssl_early_data_reason_t reason);
-// Encrypted Client Hello.
+// Encrypted ClientHello.
//
// ECH is a mechanism for encrypting the entire ClientHello message in TLS 1.3.
// This can prevent observers from seeing cleartext information about the
@@ -3589,6 +3589,72 @@
// as part of this connection.
OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
+// SSL_ECH_SERVER_CONFIG_LIST_new returns a newly-allocated
+// |SSL_ECH_SERVER_CONFIG_LIST| or NULL on error.
+OPENSSL_EXPORT SSL_ECH_SERVER_CONFIG_LIST *SSL_ECH_SERVER_CONFIG_LIST_new(void);
+
+// SSL_ECH_SERVER_CONFIG_LIST_up_ref increments the reference count of |list|.
+OPENSSL_EXPORT void SSL_ECH_SERVER_CONFIG_LIST_up_ref(
+ SSL_ECH_SERVER_CONFIG_LIST *list);
+
+// SSL_ECH_SERVER_CONFIG_LIST_free releases memory associated with |list|.
+OPENSSL_EXPORT void SSL_ECH_SERVER_CONFIG_LIST_free(
+ SSL_ECH_SERVER_CONFIG_LIST *list);
+
+// SSL_ECH_SERVER_CONFIG_LIST_add appends an ECHConfig in |ech_config| and its
+// corresponding private key in |private_key| to |list|. When |is_retry_config|
+// is non-zero, this config will be returned to the client on configuration
+// mismatch. It returns one on success and zero on error. See also
+// |SSL_CTX_set1_ech_server_config_list|.
+//
+// This function should be called successively to register each ECHConfig in
+// decreasing order of preference. This configuration must be completed before
+// setting |list| on an |SSL_CTX| with |SSL_CTX_set1_ech_server_config_list|.
+// After that point, |list| is immutable; no more ECHConfig values may be added.
+OPENSSL_EXPORT int SSL_ECH_SERVER_CONFIG_LIST_add(
+ SSL_ECH_SERVER_CONFIG_LIST *list, int is_retry_config,
+ const uint8_t *ech_config, size_t ech_config_len,
+ const uint8_t *private_key, size_t private_key_len);
+
+// SSL_CTX_set1_ech_server_config_list atomically sets the refcounted |list|
+// onto |ctx|, releasing the old list. |SSL| objects associated with |ctx|, as
+// servers, will use |list| to decrypt incoming encrypted ClientHello messages.
+// It returns one on success, and zero on failure.
+//
+// If |list| does not contain any retry configs, this function will fail. Retry
+// configs are marked as such when they are added to |list| with
+// |SSL_ECH_SERVER_CONFIG_LIST_add|.
+//
+// Once |list| has been passed to this function, it is immutable. Unlike most
+// |SSL_CTX| configuration functions, this function may be called even if |ctx|
+// already has associated connections on multiple threads. This may be used to
+// rotate keys in a long-lived server process.
+//
+// The configured ECHConfig values should also be advertised out-of-band via DNS
+// (see draft-ietf-dnsop-svcb-https). Before advertising an ECHConfig in DNS,
+// deployments should ensure all instances of the service are configured with
+// the ECHConfig and corresponding private key.
+//
+// Only the most recent fully-deployed ECHConfigs should be advertised in DNS.
+// |list| may contain a newer set if those ECHConfigs are mid-deployment. It
+// should also contain older sets, until the DNS change has rolled out and the
+// old records have expired from caches.
+//
+// If there is a mismatch, |SSL| objects associated with |ctx| will complete the
+// handshake using the cleartext ClientHello and send updated ECHConfig values
+// to the client. The client will then retry to recover, but with a latency
+// penalty. This recovery flow depends on the public name in the ECHConfig.
+// Before advertising an ECHConfig in DNS, deployments must ensure all instances
+// of the service can present a valid certificate for the public name.
+//
+// BoringSSL negotiates ECH before certificate selection callbacks are called,
+// including |SSL_CTX_set_select_certificate_cb|. If ECH is negotiated, the
+// reported |SSL_CLIENT_HELLO| structure and |SSL_get_servername| function will
+// transparently reflect the inner ClientHello. Callers should select parameters
+// based on these values to correctly handle ECH as well as the recovery flow.
+OPENSSL_EXPORT int SSL_CTX_set1_ech_server_config_list(
+ SSL_CTX *ctx, SSL_ECH_SERVER_CONFIG_LIST *list);
+
// Alerts.
//
@@ -4960,6 +5026,10 @@
BORINGSSL_MAKE_DELETER(SSL, SSL_free)
BORINGSSL_MAKE_DELETER(SSL_CTX, SSL_CTX_free)
BORINGSSL_MAKE_UP_REF(SSL_CTX, SSL_CTX_up_ref)
+BORINGSSL_MAKE_DELETER(SSL_ECH_SERVER_CONFIG_LIST,
+ SSL_ECH_SERVER_CONFIG_LIST_free)
+BORINGSSL_MAKE_UP_REF(SSL_ECH_SERVER_CONFIG_LIST,
+ SSL_ECH_SERVER_CONFIG_LIST_up_ref)
BORINGSSL_MAKE_DELETER(SSL_SESSION, SSL_SESSION_free)
BORINGSSL_MAKE_UP_REF(SSL_SESSION, SSL_SESSION_up_ref)
@@ -5293,6 +5363,11 @@
#define SSL_R_NO_APPLICATION_PROTOCOL 307
#define SSL_R_NEGOTIATED_ALPS_WITHOUT_ALPN 308
#define SSL_R_ALPS_MISMATCH_ON_EARLY_DATA 309
+#define SSL_R_ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH 310
+#define SSL_R_ECH_SERVER_CONFIG_UNSUPPORTED_EXTENSION 311
+#define SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG 312
+#define SSL_R_ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS 313
+#define SSL_R_INVALID_CLIENT_HELLO_INNER 314
#define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
#define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
#define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index da79a08..f1242b3 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -257,6 +257,7 @@
// extension number.
#define TLSEXT_TYPE_encrypted_client_hello 0xfe09
#define TLSEXT_TYPE_ech_is_inner 0xda09
+#define TLSEXT_TYPE_ech_outer_extensions 0xfd00
// ExtensionType value from RFC6962
#define TLSEXT_TYPE_certificate_timestamp 18
diff --git a/ssl/CMakeLists.txt b/ssl/CMakeLists.txt
index 0fb532e..38c686b 100644
--- a/ssl/CMakeLists.txt
+++ b/ssl/CMakeLists.txt
@@ -10,6 +10,7 @@
d1_srtp.cc
dtls_method.cc
dtls_record.cc
+ encrypted_client_hello.cc
handoff.cc
handshake.cc
handshake_client.cc
diff --git a/ssl/encrypted_client_hello.cc b/ssl/encrypted_client_hello.cc
new file mode 100644
index 0000000..f10cd3a
--- /dev/null
+++ b/ssl/encrypted_client_hello.cc
@@ -0,0 +1,444 @@
+/* Copyright (c) 2021, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include <openssl/bytestring.h>
+#include <openssl/curve25519.h>
+#include <openssl/err.h>
+#include <openssl/hkdf.h>
+#include <openssl/ssl.h>
+
+#include "internal.h"
+
+
+#if defined(OPENSSL_MSAN)
+#define NO_SANITIZE_MEMORY __attribute__((no_sanitize("memory")))
+#else
+#define NO_SANITIZE_MEMORY
+#endif
+
+BSSL_NAMESPACE_BEGIN
+
+// ssl_client_hello_write_without_extensions serializes |client_hello| into
+// |out|, omitting the length-prefixed extensions. It serializes individual
+// fields, starting with |client_hello->version|, and ignores the
+// |client_hello->client_hello| field. It returns true on success and false on
+// failure.
+static bool ssl_client_hello_write_without_extensions(
+ const SSL_CLIENT_HELLO *client_hello, CBB *out) {
+ CBB cbb;
+ if (!CBB_add_u16(out, client_hello->version) ||
+ !CBB_add_bytes(out, client_hello->random, client_hello->random_len) ||
+ !CBB_add_u8_length_prefixed(out, &cbb) ||
+ !CBB_add_bytes(&cbb, client_hello->session_id,
+ client_hello->session_id_len) ||
+ !CBB_add_u16_length_prefixed(out, &cbb) ||
+ !CBB_add_bytes(&cbb, client_hello->cipher_suites,
+ client_hello->cipher_suites_len) ||
+ !CBB_add_u8_length_prefixed(out, &cbb) ||
+ !CBB_add_bytes(&cbb, client_hello->compression_methods,
+ client_hello->compression_methods_len) ||
+ !CBB_flush(out)) {
+ return false;
+ }
+ return true;
+}
+
+bool ssl_decode_client_hello_inner(
+ SSL *ssl, uint8_t *out_alert, Array<uint8_t> *out_client_hello_inner,
+ Span<const uint8_t> encoded_client_hello_inner,
+ const SSL_CLIENT_HELLO *client_hello_outer) {
+ SSL_CLIENT_HELLO client_hello_inner;
+ if (!ssl_client_hello_init(ssl, &client_hello_inner,
+ encoded_client_hello_inner)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ // TLS 1.3 ClientHellos must have extensions, and EncodedClientHelloInners use
+ // ClientHelloOuter's session_id.
+ if (client_hello_inner.extensions_len == 0 ||
+ client_hello_inner.session_id_len != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ client_hello_inner.session_id = client_hello_outer->session_id;
+ client_hello_inner.session_id_len = client_hello_outer->session_id_len;
+
+ // Begin serializing a message containing the ClientHelloInner in |cbb|.
+ ScopedCBB cbb;
+ CBB body, extensions;
+ if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_CLIENT_HELLO) ||
+ !ssl_client_hello_write_without_extensions(&client_hello_inner, &body) ||
+ !CBB_add_u16_length_prefixed(&body, &extensions)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+
+ // Sort the extensions in ClientHelloOuter, so ech_outer_extensions may be
+ // processed in O(n*log(n)) time, rather than O(n^2).
+ struct Extension {
+ uint16_t extension = 0;
+ Span<const uint8_t> body;
+ bool copied = false;
+ };
+
+ // MSan's libc interceptors do not handle |bsearch|. See b/182583130.
+ auto compare_extension = [](const void *a, const void *b)
+ NO_SANITIZE_MEMORY -> int {
+ const Extension *extension_a = reinterpret_cast<const Extension *>(a);
+ const Extension *extension_b = reinterpret_cast<const Extension *>(b);
+ if (extension_a->extension < extension_b->extension) {
+ return -1;
+ } else if (extension_a->extension > extension_b->extension) {
+ return 1;
+ }
+ return 0;
+ };
+ GrowableArray<Extension> sorted_extensions;
+ CBS unsorted_extensions(MakeConstSpan(client_hello_outer->extensions,
+ client_hello_outer->extensions_len));
+ while (CBS_len(&unsorted_extensions) > 0) {
+ Extension extension;
+ CBS extension_body;
+ if (!CBS_get_u16(&unsorted_extensions, &extension.extension) ||
+ !CBS_get_u16_length_prefixed(&unsorted_extensions, &extension_body)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+ extension.body = extension_body;
+ if (!sorted_extensions.Push(extension)) {
+ return false;
+ }
+ }
+ qsort(sorted_extensions.data(), sorted_extensions.size(), sizeof(Extension),
+ compare_extension);
+
+ // Copy extensions from |client_hello_inner|, expanding ech_outer_extensions.
+ CBS inner_extensions(MakeConstSpan(client_hello_inner.extensions,
+ client_hello_inner.extensions_len));
+ while (CBS_len(&inner_extensions) > 0) {
+ uint16_t extension_id;
+ CBS extension_body;
+ if (!CBS_get_u16(&inner_extensions, &extension_id) ||
+ !CBS_get_u16_length_prefixed(&inner_extensions, &extension_body)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ if (extension_id != TLSEXT_TYPE_ech_outer_extensions) {
+ if (!CBB_add_u16(&extensions, extension_id) ||
+ !CBB_add_u16(&extensions, CBS_len(&extension_body)) ||
+ !CBB_add_bytes(&extensions, CBS_data(&extension_body),
+ CBS_len(&extension_body))) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+ continue;
+ }
+
+ // Replace ech_outer_extensions with the corresponding outer extensions.
+ CBS outer_extensions;
+ if (!CBS_get_u8_length_prefixed(&extension_body, &outer_extensions) ||
+ CBS_len(&extension_body) != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ while (CBS_len(&outer_extensions) > 0) {
+ uint16_t extension_needed;
+ if (!CBS_get_u16(&outer_extensions, &extension_needed)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ if (extension_needed == TLSEXT_TYPE_encrypted_client_hello) {
+ *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ // Find the referenced extension.
+ Extension key;
+ key.extension = extension_needed;
+ Extension *result = reinterpret_cast<Extension *>(
+ bsearch(&key, sorted_extensions.data(), sorted_extensions.size(),
+ sizeof(Extension), compare_extension));
+ if (result == nullptr) {
+ *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+
+ // Extensions may be referenced at most once, to bound the result size.
+ if (result->copied) {
+ *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DUPLICATE_EXTENSION);
+ return false;
+ }
+ result->copied = true;
+
+ if (!CBB_add_u16(&extensions, extension_needed) ||
+ !CBB_add_u16(&extensions, result->body.size()) ||
+ !CBB_add_bytes(&extensions, result->body.data(),
+ result->body.size())) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return false;
+ }
+ }
+ }
+ if (!CBB_flush(&body)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+
+ // See https://github.com/tlswg/draft-ietf-tls-esni/pull/411
+ CBS extension;
+ if (!ssl_client_hello_init(ssl, &client_hello_inner,
+ MakeConstSpan(CBB_data(&body), CBB_len(&body))) ||
+ !ssl_client_hello_get_extension(&client_hello_inner, &extension,
+ TLSEXT_TYPE_ech_is_inner) ||
+ CBS_len(&extension) != 0 ||
+ ssl_client_hello_get_extension(&client_hello_inner, &extension,
+ TLSEXT_TYPE_encrypted_client_hello) ||
+ !ssl_client_hello_get_extension(&client_hello_inner, &extension,
+ TLSEXT_TYPE_supported_versions)) {
+ *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_CLIENT_HELLO_INNER);
+ return false;
+ }
+ // Parse supported_versions and reject TLS versions prior to TLS 1.3. Older
+ // versions are incompatible with ECH.
+ CBS versions;
+ if (!CBS_get_u8_length_prefixed(&extension, &versions) ||
+ CBS_len(&extension) != 0 || //
+ CBS_len(&versions) == 0) {
+ *out_alert = SSL_AD_DECODE_ERROR;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ while (CBS_len(&versions) != 0) {
+ uint16_t version;
+ if (!CBS_get_u16(&versions, &version)) {
+ *out_alert = SSL_AD_DECODE_ERROR;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ if (version == SSL3_VERSION || version == TLS1_VERSION ||
+ version == TLS1_1_VERSION || version == TLS1_2_VERSION ||
+ version == DTLS1_VERSION || version == DTLS1_2_VERSION) {
+ *out_alert = SSL_AD_ILLEGAL_PARAMETER;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_CLIENT_HELLO_INNER);
+ return false;
+ }
+ }
+
+ if (!ssl->method->finish_message(ssl, cbb.get(), out_client_hello_inner)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+ return true;
+}
+
+bool ssl_client_hello_decrypt(
+ EVP_HPKE_CTX *hpke_ctx, Array<uint8_t> *out_encoded_client_hello_inner,
+ bool *out_is_decrypt_error, const SSL_CLIENT_HELLO *client_hello_outer,
+ uint16_t kdf_id, uint16_t aead_id, Span<const uint8_t> config_id,
+ Span<const uint8_t> enc, Span<const uint8_t> payload) {
+ *out_is_decrypt_error = false;
+
+ // Compute the ClientHello portion of the ClientHelloOuterAAD value. See
+ // draft-ietf-tls-esni-09, section 5.2.
+ ScopedCBB ch_outer_aad_cbb;
+ CBB config_id_cbb, enc_cbb, outer_hello_cbb, extensions_cbb;
+ if (!CBB_init(ch_outer_aad_cbb.get(), 0) ||
+ !CBB_add_u16(ch_outer_aad_cbb.get(), kdf_id) ||
+ !CBB_add_u16(ch_outer_aad_cbb.get(), aead_id) ||
+ !CBB_add_u8_length_prefixed(ch_outer_aad_cbb.get(), &config_id_cbb) ||
+ !CBB_add_bytes(&config_id_cbb, config_id.data(), config_id.size()) ||
+ !CBB_add_u16_length_prefixed(ch_outer_aad_cbb.get(), &enc_cbb) ||
+ !CBB_add_bytes(&enc_cbb, enc.data(), enc.size()) ||
+ !CBB_add_u24_length_prefixed(ch_outer_aad_cbb.get(), &outer_hello_cbb) ||
+ !ssl_client_hello_write_without_extensions(client_hello_outer,
+ &outer_hello_cbb) ||
+ !CBB_add_u16_length_prefixed(&outer_hello_cbb, &extensions_cbb)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return false;
+ }
+
+ CBS extensions(MakeConstSpan(client_hello_outer->extensions,
+ client_hello_outer->extensions_len));
+ while (CBS_len(&extensions) > 0) {
+ uint16_t extension_id;
+ CBS extension_body;
+ if (!CBS_get_u16(&extensions, &extension_id) ||
+ !CBS_get_u16_length_prefixed(&extensions, &extension_body)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ if (extension_id == TLSEXT_TYPE_encrypted_client_hello) {
+ continue;
+ }
+ if (!CBB_add_u16(&extensions_cbb, extension_id) ||
+ !CBB_add_u16(&extensions_cbb, CBS_len(&extension_body)) ||
+ !CBB_add_bytes(&extensions_cbb, CBS_data(&extension_body),
+ CBS_len(&extension_body))) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return false;
+ }
+ }
+ if (!CBB_flush(ch_outer_aad_cbb.get())) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return false;
+ }
+
+ // Attempt to decrypt into |out_encoded_client_hello_inner|.
+ if (!out_encoded_client_hello_inner->Init(payload.size())) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return false;
+ }
+ size_t encoded_client_hello_inner_len;
+ if (!EVP_HPKE_CTX_open(hpke_ctx, out_encoded_client_hello_inner->data(),
+ &encoded_client_hello_inner_len,
+ out_encoded_client_hello_inner->size(), payload.data(),
+ payload.size(), CBB_data(ch_outer_aad_cbb.get()),
+ CBB_len(ch_outer_aad_cbb.get()))) {
+ *out_is_decrypt_error = true;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECRYPTION_FAILED);
+ return false;
+ }
+ out_encoded_client_hello_inner->Shrink(encoded_client_hello_inner_len);
+ return true;
+}
+
+
+bool ECHServerConfig::Init(Span<const uint8_t> raw,
+ Span<const uint8_t> private_key,
+ bool is_retry_config) {
+ assert(!initialized_);
+ is_retry_config_ = is_retry_config;
+
+ if (!raw_.CopyFrom(raw)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return false;
+ }
+ // Read from |raw_| so we can save Spans with the same lifetime as |this|.
+ CBS reader(raw_);
+
+ uint16_t version;
+ if (!CBS_get_u16(&reader, &version)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ // Parse the ECHConfig, rejecting all unsupported parameters and extensions.
+ // Unlike most server options, ECH's server configuration is serialized and
+ // configured in both the server and DNS. If the caller configures an
+ // unsupported parameter, this is a deployment error. To catch these errors,
+ // we fail early.
+ if (version != TLSEXT_TYPE_encrypted_client_hello) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG);
+ return false;
+ }
+
+ CBS ech_config_contents, public_name, public_key, cipher_suites, extensions;
+ uint16_t kem_id, max_name_len;
+ if (!CBS_get_u16_length_prefixed(&reader, &ech_config_contents) ||
+ !CBS_get_u16_length_prefixed(&ech_config_contents, &public_name) ||
+ CBS_len(&public_name) == 0 ||
+ !CBS_get_u16_length_prefixed(&ech_config_contents, &public_key) ||
+ CBS_len(&public_key) == 0 ||
+ !CBS_get_u16(&ech_config_contents, &kem_id) ||
+ !CBS_get_u16_length_prefixed(&ech_config_contents, &cipher_suites) ||
+ CBS_len(&cipher_suites) == 0 ||
+ !CBS_get_u16(&ech_config_contents, &max_name_len) ||
+ !CBS_get_u16_length_prefixed(&ech_config_contents, &extensions) ||
+ CBS_len(&ech_config_contents) != 0 || //
+ CBS_len(&reader) != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ // We only support one KEM, and the KEM decides the length of |public_key|.
+ if (CBS_len(&public_key) != X25519_PUBLIC_VALUE_LEN ||
+ kem_id != EVP_HPKE_DHKEM_X25519_HKDF_SHA256) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG);
+ return false;
+ }
+ public_key_ = public_key;
+
+ // We do not support any ECHConfig extensions, so |extensions| must be empty.
+ if (CBS_len(&extensions) != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_ECH_SERVER_CONFIG_UNSUPPORTED_EXTENSION);
+ return false;
+ }
+
+ cipher_suites_ = cipher_suites;
+ while (CBS_len(&cipher_suites) > 0) {
+ uint16_t kdf_id, aead_id;
+ if (!CBS_get_u16(&cipher_suites, &kdf_id) ||
+ !CBS_get_u16(&cipher_suites, &aead_id)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ // This parser fails when it encounters any bytes it does not understand. If
+ // the config lists any unsupported cipher suites, that is a parse error.
+ if (kdf_id != EVP_HPKE_HKDF_SHA256 ||
+ (aead_id != EVP_HPKE_AEAD_AES_128_GCM &&
+ aead_id != EVP_HPKE_AEAD_AES_256_GCM &&
+ aead_id != EVP_HPKE_AEAD_CHACHA20POLY1305)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG);
+ return false;
+ }
+ }
+
+ // Precompute the config_id.
+ uint8_t key[EVP_MAX_KEY_LENGTH];
+ size_t key_len;
+ static const uint8_t kInfo[] = "tls ech config id";
+ if (!HKDF_extract(key, &key_len, EVP_sha256(), raw_.data(), raw_.size(),
+ nullptr, 0) ||
+ !HKDF_expand(config_id_sha256_, sizeof(config_id_sha256_), EVP_sha256(),
+ key, key_len, kInfo, OPENSSL_ARRAY_SIZE(kInfo) - 1)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+
+ if (private_key.size() != X25519_PRIVATE_KEY_LEN) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return false;
+ }
+ uint8_t expected_public_key[X25519_PUBLIC_VALUE_LEN];
+ X25519_public_from_private(expected_public_key, private_key.data());
+ if (public_key_ != expected_public_key) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH);
+ return false;
+ }
+ assert(sizeof(private_key_) == private_key.size());
+ OPENSSL_memcpy(private_key_, private_key.data(), private_key.size());
+
+ initialized_ = true;
+ return true;
+}
+
+bool ECHServerConfig::SupportsCipherSuite(uint16_t kdf_id,
+ uint16_t aead_id) const {
+ assert(initialized_);
+ CBS cbs(cipher_suites_);
+ while (CBS_len(&cbs) != 0) {
+ uint16_t supported_kdf_id, supported_aead_id;
+ if (!CBS_get_u16(&cbs, &supported_kdf_id) ||
+ !CBS_get_u16(&cbs, &supported_aead_id)) {
+ return false;
+ }
+ if (kdf_id == supported_kdf_id && aead_id == supported_aead_id) {
+ return true;
+ }
+ }
+ return false;
+}
+
+BSSL_NAMESPACE_END
diff --git a/ssl/handoff.cc b/ssl/handoff.cc
index 16cbdf7..6cf0a44 100644
--- a/ssl/handoff.cc
+++ b/ssl/handoff.cc
@@ -93,7 +93,7 @@
!serialize_features(&seq) ||
!CBB_flush(out) ||
!ssl->method->get_message(ssl, &msg) ||
- !ssl_client_hello_init(ssl, out_hello, msg)) {
+ !ssl_client_hello_init(ssl, out_hello, msg.body)) {
return false;
}
diff --git a/ssl/handshake.cc b/ssl/handshake.cc
index d889372..f5b6ca0 100644
--- a/ssl/handshake.cc
+++ b/ssl/handshake.cc
@@ -126,6 +126,7 @@
SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg)
: ssl(ssl_arg),
+ ech_accept(false),
ech_present(false),
ech_is_inner_present(false),
scts_requested(false),
@@ -164,6 +165,28 @@
hash_len_ = hash_len;
}
+bool SSL_HANDSHAKE::GetClientHello(SSLMessage *out_msg,
+ SSL_CLIENT_HELLO *out_client_hello) {
+ if (!ech_client_hello_buf.empty()) {
+ // If the backing buffer is non-empty, the ClientHelloInner has been set.
+ out_msg->is_v2_hello = false;
+ out_msg->type = SSL3_MT_CLIENT_HELLO;
+ out_msg->raw = CBS(ech_client_hello_buf);
+ out_msg->body = MakeConstSpan(ech_client_hello_buf).subspan(4);
+ } else if (!ssl->method->get_message(ssl, out_msg)) {
+ // The message has already been read, so this cannot fail.
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return false;
+ }
+
+ if (!ssl_client_hello_init(ssl, out_client_hello, out_msg->body)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_CLIENTHELLO_PARSE_FAILED);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+ return false;
+ }
+ return true;
+}
+
UniquePtr<SSL_HANDSHAKE> ssl_handshake_new(SSL *ssl) {
UniquePtr<SSL_HANDSHAKE> hs = MakeUnique<SSL_HANDSHAKE>(ssl);
if (!hs || !hs->transcript.Init()) {
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index bc0a0d1..1c5f0cf 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -154,6 +154,8 @@
#include <openssl/bn.h>
#include <openssl/bytestring.h>
#include <openssl/cipher.h>
+#include <openssl/curve25519.h>
+#include <openssl/digest.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/err.h>
@@ -167,6 +169,7 @@
#include "internal.h"
#include "../crypto/internal.h"
+#include "../crypto/hpke/internal.h"
BSSL_NAMESPACE_BEGIN
@@ -563,7 +566,7 @@
}
SSL_CLIENT_HELLO client_hello;
- if (!ssl_client_hello_init(ssl, &client_hello, msg)) {
+ if (!ssl_client_hello_init(ssl, &client_hello, msg.body)) {
OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
return ssl_hs_error;
@@ -581,12 +584,137 @@
return ssl_hs_handoff;
}
+ // If the ClientHello contains an encrypted_client_hello extension (and no
+ // ech_is_inner extension), act as a client-facing server and attempt to
+ // decrypt the ClientHelloInner.
+ CBS ech_body;
+ if (ssl_client_hello_get_extension(&client_hello, &ech_body,
+ TLSEXT_TYPE_encrypted_client_hello)) {
+ CBS unused;
+ if (ssl_client_hello_get_extension(&client_hello, &unused,
+ TLSEXT_TYPE_ech_is_inner)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+ return ssl_hs_error;
+ }
+
+ // Parse a ClientECH out of the extension body.
+ uint16_t kdf_id, aead_id;
+ CBS config_id, enc, payload;
+ if (!CBS_get_u16(&ech_body, &kdf_id) || //
+ !CBS_get_u16(&ech_body, &aead_id) ||
+ !CBS_get_u8_length_prefixed(&ech_body, &config_id) ||
+ !CBS_get_u16_length_prefixed(&ech_body, &enc) ||
+ !CBS_get_u16_length_prefixed(&ech_body, &payload) ||
+ CBS_len(&ech_body) != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+ return ssl_hs_error;
+ }
+
+ {
+ MutexReadLock lock(&ssl->ctx->lock);
+ hs->ech_server_config_list = UpRef(ssl->ctx->ech_server_config_list);
+ }
+
+ if (hs->ech_server_config_list) {
+ for (const ECHServerConfig &ech_config :
+ hs->ech_server_config_list->configs) {
+ // Skip this config if the client-provided config_id does not match or
+ // if the client indicated an unsupported HPKE ciphersuite.
+ if (config_id != ech_config.config_id_sha256() ||
+ !ech_config.SupportsCipherSuite(kdf_id, aead_id)) {
+ continue;
+ }
+
+ static const uint8_t kInfoLabel[] = "tls ech";
+ ScopedCBB info_cbb;
+ if (!CBB_init(info_cbb.get(),
+ sizeof(kInfoLabel) + ech_config.raw().size()) ||
+ !CBB_add_bytes(info_cbb.get(), kInfoLabel,
+ sizeof(kInfoLabel) /* includes trailing NUL */) ||
+ !CBB_add_bytes(info_cbb.get(), ech_config.raw().data(),
+ ech_config.raw().size())) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return ssl_hs_error;
+ }
+
+ // Set up a fresh HPKE context for each decryption attempt.
+ hs->ech_hpke_ctx.Reset();
+
+ if (CBS_len(&enc) != X25519_PUBLIC_VALUE_LEN ||
+ !EVP_HPKE_CTX_setup_base_r_x25519(
+ hs->ech_hpke_ctx.get(), kdf_id, aead_id, CBS_data(&enc),
+ CBS_len(&enc), ech_config.public_key().data(),
+ ech_config.public_key().size(), ech_config.private_key().data(),
+ ech_config.private_key().size(), CBB_data(info_cbb.get()),
+ CBB_len(info_cbb.get()))) {
+ // Ignore the error and try another ECHConfig.
+ ERR_clear_error();
+ continue;
+ }
+ Array<uint8_t> encoded_client_hello_inner;
+ bool is_decrypt_error;
+ if (!ssl_client_hello_decrypt(hs->ech_hpke_ctx.get(),
+ &encoded_client_hello_inner,
+ &is_decrypt_error, &client_hello, kdf_id,
+ aead_id, config_id, enc, payload)) {
+ if (is_decrypt_error) {
+ // Ignore the error and try another ECHConfig.
+ ERR_clear_error();
+ continue;
+ }
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECRYPTION_FAILED);
+ return ssl_hs_error;
+ }
+
+ // Recover the ClientHelloInner from the EncodedClientHelloInner.
+ uint8_t alert = SSL_AD_DECODE_ERROR;
+ bssl::Array<uint8_t> client_hello_inner;
+ if (!ssl_decode_client_hello_inner(ssl, &alert, &client_hello_inner,
+ encoded_client_hello_inner,
+ &client_hello)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return ssl_hs_error;
+ }
+ hs->ech_client_hello_buf = std::move(client_hello_inner);
+
+ // Load the ClientHelloInner into |client_hello|.
+ if (!hs->GetClientHello(&msg, &client_hello)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return ssl_hs_error;
+ }
+
+ hs->ech_accept = true;
+ break;
+ }
+ }
+
+ // If we did not set |hs->ech_accept| to true, we will send the current
+ // ECHConfigs as retry_configs in the ServerHello's encrypted extensions.
+ // Proceed with the ClientHelloOuter.
+ }
+
uint8_t alert = SSL_AD_DECODE_ERROR;
if (!extract_sni(hs, &alert, &client_hello)) {
ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
return ssl_hs_error;
}
+ hs->state = state12_read_client_hello_after_ech;
+ return ssl_hs_ok;
+}
+
+static enum ssl_hs_wait_t do_read_client_hello_after_ech(SSL_HANDSHAKE *hs) {
+ SSL *const ssl = hs->ssl;
+
+ SSLMessage msg_unused;
+ SSL_CLIENT_HELLO client_hello;
+ if (!hs->GetClientHello(&msg_unused, &client_hello)) {
+ return ssl_hs_error;
+ }
+
// Run the early callback.
if (ssl->ctx->select_certificate_cb != NULL) {
switch (ssl->ctx->select_certificate_cb(&client_hello)) {
@@ -614,6 +742,7 @@
hs->apply_jdk11_workaround = true;
}
+ uint8_t alert = SSL_AD_DECODE_ERROR;
if (!negotiate_version(hs, &alert, &client_hello)) {
ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
return ssl_hs_error;
@@ -657,11 +786,6 @@
static enum ssl_hs_wait_t do_select_certificate(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl;
- SSLMessage msg;
- if (!ssl->method->get_message(ssl, &msg)) {
- return ssl_hs_read_message;
- }
-
// Call |cert_cb| to update server certificates if required.
if (hs->config->cert->cert_cb != NULL) {
int rv = hs->config->cert->cert_cb(ssl, hs->config->cert->cert_cb_arg);
@@ -701,10 +825,16 @@
return ssl_hs_ok;
}
+ // It should not be possible to negotiate TLS 1.2 with ECH. The
+ // ClientHelloInner decoding function rejects ClientHellos which offer TLS 1.2
+ // or below.
+ assert(!hs->ech_accept);
+
ssl->s3->early_data_reason = ssl_early_data_protocol_version;
+ SSLMessage msg_unused;
SSL_CLIENT_HELLO client_hello;
- if (!ssl_client_hello_init(ssl, &client_hello, msg)) {
+ if (!hs->GetClientHello(&msg_unused, &client_hello)) {
return ssl_hs_error;
}
@@ -743,7 +873,7 @@
}
SSL_CLIENT_HELLO client_hello;
- if (!ssl_client_hello_init(ssl, &client_hello, msg)) {
+ if (!ssl_client_hello_init(ssl, &client_hello, msg.body)) {
return ssl_hs_error;
}
@@ -1693,6 +1823,9 @@
case state12_read_client_hello:
ret = do_read_client_hello(hs);
break;
+ case state12_read_client_hello_after_ech:
+ ret = do_read_client_hello_after_ech(hs);
+ break;
case state12_select_certificate:
ret = do_select_certificate(hs);
break;
@@ -1773,6 +1906,8 @@
return "TLS server start_accept";
case state12_read_client_hello:
return "TLS server read_client_hello";
+ case state12_read_client_hello_after_ech:
+ return "TLS server read_client_hello_after_ech";
case state12_select_certificate:
return "TLS server select_certificate";
case state12_tls13:
diff --git a/ssl/internal.h b/ssl/internal.h
index e733e67..9c048bb 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -152,6 +152,7 @@
#include <utility>
#include <openssl/aead.h>
+#include <openssl/curve25519.h>
#include <openssl/err.h>
#include <openssl/lhash.h>
#include <openssl/mem.h>
@@ -161,6 +162,7 @@
#include "../crypto/err/internal.h"
#include "../crypto/internal.h"
+#include "../crypto/hpke/internal.h"
#if defined(OPENSSL_WINDOWS)
@@ -378,6 +380,8 @@
return *this;
}
+ const T *data() const { return array_.data(); }
+ T *data() { return array_.data(); }
size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
@@ -1423,7 +1427,88 @@
const SSLMessage &msg, CBS *binders);
-// Encrypted Client Hello.
+// Encrypted ClientHello.
+
+class ECHServerConfig {
+ public:
+ ECHServerConfig() : is_retry_config_(false), initialized_(false) {}
+ ECHServerConfig(ECHServerConfig &&other) = default;
+ ~ECHServerConfig() = default;
+ ECHServerConfig &operator=(ECHServerConfig &&) = default;
+
+ // Init parses |ech_config| as an ECHConfig and saves a copy of |private_key|.
+ // It returns true on success and false on error. It will also error if
+ // |private_key| is not a valid X25519 private key or it does not correspond
+ // to the parsed public key.
+ bool Init(Span<const uint8_t> ech_config, Span<const uint8_t> private_key,
+ bool is_retry_config);
+
+ // SupportsCipherSuite returns true when this ECHConfig supports the HPKE
+ // ciphersuite composed of |kdf_id| and |aead_id|. This function must only be
+ // called on an initialized object.
+ bool SupportsCipherSuite(uint16_t kdf_id, uint16_t aead_id) const;
+
+ Span<const uint8_t> raw() const {
+ assert(initialized_);
+ return raw_;
+ }
+ Span<const uint8_t> public_key() const {
+ assert(initialized_);
+ return public_key_;
+ }
+ Span<const uint8_t> private_key() const {
+ assert(initialized_);
+ return MakeConstSpan(private_key_, sizeof(private_key_));
+ }
+ Span<const uint8_t> config_id_sha256() const {
+ assert(initialized_);
+ return MakeConstSpan(config_id_sha256_, sizeof(config_id_sha256_));
+ }
+ bool is_retry_config() const {
+ assert(initialized_);
+ return is_retry_config_;
+ }
+
+ private:
+ Array<uint8_t> raw_;
+ Span<const uint8_t> public_key_;
+ Span<const uint8_t> cipher_suites_;
+
+ // private_key_ is the key corresponding to |public_key|. For clients, it must
+ // be empty (|private_key_present_ == false|). For servers, it must be a valid
+ // X25519 private key.
+ uint8_t private_key_[X25519_PRIVATE_KEY_LEN];
+
+ // config_id_ stores the precomputed result of |ConfigID| for
+ // |EVP_HPKE_HKDF_SHA256|.
+ uint8_t config_id_sha256_[8];
+
+ bool is_retry_config_ : 1;
+ bool initialized_ : 1;
+};
+
+// ssl_decode_client_hello_inner recovers the full ClientHelloInner from the
+// EncodedClientHelloInner |encoded_client_hello_inner| by replacing its
+// outer_extensions extension with the referenced extensions from the
+// ClientHelloOuter |client_hello_outer|. If successful, it writes the recovered
+// ClientHelloInner to |out_client_hello_inner|. It returns true on success and
+// false on failure.
+OPENSSL_EXPORT bool ssl_decode_client_hello_inner(
+ SSL *ssl, uint8_t *out_alert, Array<uint8_t> *out_client_hello_inner,
+ Span<const uint8_t> encoded_client_hello_inner,
+ const SSL_CLIENT_HELLO *client_hello_outer);
+
+// ssl_client_hello_decrypt attempts to decrypt the given |payload| into
+// |out_encoded_client_hello_inner|. The decrypted value should be an
+// EncodedClientHelloInner. It returns false if any fatal errors occur and true
+// otherwise, regardless of whether the decrypt was successful. It sets
+// |out_encoded_client_hello_inner| to true if the decryption fails, and false
+// otherwise.
+bool ssl_client_hello_decrypt(
+ EVP_HPKE_CTX *hpke_ctx, Array<uint8_t> *out_encoded_client_hello_inner,
+ bool *out_is_decrypt_error, const SSL_CLIENT_HELLO *client_hello_outer,
+ uint16_t kdf_id, uint16_t aead_id, Span<const uint8_t> config_id,
+ Span<const uint8_t> enc, Span<const uint8_t> payload);
// tls13_ech_accept_confirmation computes the server's ECH acceptance signal,
// writing it to |out|. It returns true on success, and false on failure.
@@ -1507,6 +1592,7 @@
enum tls12_server_hs_state_t {
state12_start_accept = 0,
state12_read_client_hello,
+ state12_read_client_hello_after_ech,
state12_select_certificate,
state12_tls13,
state12_select_parameters,
@@ -1602,6 +1688,17 @@
public:
void ResizeSecrets(size_t hash_len);
+ // GetClientHello, on the server, returns either the normal ClientHello
+ // message or the ClientHelloInner if it has been serialized to
+ // |ech_client_hello_buf|. This function should only be called when the
+ // current message is a ClientHello. It returns true on success and false on
+ // error.
+ //
+ // Note that fields of the returned |out_msg| and |out_client_hello| point
+ // into a handshake-owned buffer, so their lifetimes should not exceed this
+ // SSL_HANDSHAKE.
+ bool GetClientHello(SSLMessage *out_msg, SSL_CLIENT_HELLO *out_client_hello);
+
Span<uint8_t> secret() { return MakeSpan(secret_, hash_len_); }
Span<uint8_t> early_traffic_secret() {
return MakeSpan(early_traffic_secret_, hash_len_);
@@ -1654,6 +1751,10 @@
// the first ClientHello.
Array<uint8_t> ech_grease;
+ // ech_client_hello_buf, on the server, contains the bytes of the
+ // reconstructed ClientHelloInner message.
+ Array<uint8_t> ech_client_hello_buf;
+
// key_share_bytes is the value of the previously sent KeyShare extension by
// the client in TLS 1.3.
Array<uint8_t> key_share_bytes;
@@ -1690,6 +1791,10 @@
// |cert_compression_negotiated| is true.
uint16_t cert_compression_alg_id;
+ // ech_hpke_ctx, on the server, is the HPKE context used to decrypt the
+ // client's ECH payloads.
+ ScopedEVP_HPKE_CTX ech_hpke_ctx;
+
// server_params, in a TLS 1.2 server, stores the ServerKeyExchange
// parameters. It has client and server randoms prepended for signing
// convenience.
@@ -1726,12 +1831,21 @@
// the client if |in_early_data| is true.
UniquePtr<SSL_SESSION> early_session;
+ // ech_server_config_list, for servers, is the list of ECHConfig values that
+ // were valid when the server received the first ClientHello. Its value will
+ // not change when the config list on |SSL_CTX| is updated.
+ UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> ech_server_config_list;
+
// new_cipher is the cipher being negotiated in this handshake.
const SSL_CIPHER *new_cipher = nullptr;
// key_block is the record-layer key block for TLS 1.2 and earlier.
Array<uint8_t> key_block;
+ // ech_accept, on the server, indicates whether the server should overwrite
+ // part of ServerHello.random with the ECH accept_confirmation value.
+ bool ech_accept : 1;
+
// ech_present, on the server, indicates whether the ClientHello contained an
// encrypted_client_hello extension.
bool ech_present : 1;
@@ -1997,7 +2111,7 @@
// ClientHello functions.
bool ssl_client_hello_init(const SSL *ssl, SSL_CLIENT_HELLO *out,
- const SSLMessage &msg);
+ Span<const uint8_t> body);
bool ssl_client_hello_get_extension(const SSL_CLIENT_HELLO *client_hello,
CBS *out, uint16_t extension_type);
@@ -3321,6 +3435,11 @@
// The client's Channel ID private key.
bssl::UniquePtr<EVP_PKEY> channel_id_private;
+ // ech_server_config_list contains the server's list of ECHConfig values and
+ // associated private keys. This list may be swapped out at any time, so all
+ // access must be synchronized through |lock|.
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> ech_server_config_list;
+
// keylog_callback, if not NULL, is the key logging callback. See
// |SSL_CTX_set_keylog_callback|.
void (*keylog_callback)(const SSL *ssl, const char *line) = nullptr;
@@ -3634,5 +3753,18 @@
friend void SSL_SESSION_free(SSL_SESSION *);
};
+struct ssl_ech_server_config_list_st {
+ ssl_ech_server_config_list_st() = default;
+ ssl_ech_server_config_list_st(const ssl_ech_server_config_list_st &) = delete;
+ ssl_ech_server_config_list_st &operator=(
+ const ssl_ech_server_config_list_st &) = delete;
+
+ bssl::GrowableArray<bssl::ECHServerConfig> configs;
+ CRYPTO_refcount_t references = 1;
+
+ private:
+ ~ssl_ech_server_config_list_st() = default;
+ friend void SSL_ECH_SERVER_CONFIG_LIST_free(SSL_ECH_SERVER_CONFIG_LIST *);
+};
#endif // OPENSSL_HEADER_SSL_INTERNAL_H
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 0990d3c..4e820f0 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -2186,6 +2186,63 @@
return 1;
}
+SSL_ECH_SERVER_CONFIG_LIST *SSL_ECH_SERVER_CONFIG_LIST_new() {
+ return New<SSL_ECH_SERVER_CONFIG_LIST>();
+}
+
+void SSL_ECH_SERVER_CONFIG_LIST_up_ref(SSL_ECH_SERVER_CONFIG_LIST *configs) {
+ CRYPTO_refcount_inc(&configs->references);
+}
+
+void SSL_ECH_SERVER_CONFIG_LIST_free(SSL_ECH_SERVER_CONFIG_LIST *configs) {
+ if (configs == nullptr ||
+ !CRYPTO_refcount_dec_and_test_zero(&configs->references)) {
+ return;
+ }
+
+ configs->~ssl_ech_server_config_list_st();
+ OPENSSL_free(configs);
+}
+
+int SSL_ECH_SERVER_CONFIG_LIST_add(SSL_ECH_SERVER_CONFIG_LIST *configs,
+ int is_retry_config,
+ const uint8_t *ech_config,
+ size_t ech_config_len,
+ const uint8_t *private_key,
+ size_t private_key_len) {
+ ECHServerConfig parsed_config;
+ if (!parsed_config.Init(MakeConstSpan(ech_config, ech_config_len),
+ MakeConstSpan(private_key, private_key_len),
+ !!is_retry_config)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ return 0;
+ }
+ if (!configs->configs.Push(std::move(parsed_config))) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return 0;
+ }
+ return 1;
+}
+
+int SSL_CTX_set1_ech_server_config_list(SSL_CTX *ctx,
+ SSL_ECH_SERVER_CONFIG_LIST *list) {
+ bool has_retry_config = false;
+ for (const bssl::ECHServerConfig &config : list->configs) {
+ if (config.is_retry_config()) {
+ has_retry_config = true;
+ break;
+ }
+ }
+ if (!has_retry_config) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS);
+ return 0;
+ }
+ UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> owned_list = UpRef(list);
+ MutexWriteLock lock(&ctx->lock);
+ ctx->ech_server_config_list.swap(owned_list);
+ return 1;
+}
+
int SSL_select_next_proto(uint8_t **out, uint8_t *out_len, const uint8_t *peer,
unsigned peer_len, const uint8_t *supported,
unsigned supported_len) {
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 637f4d5..0c6bf50 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -29,6 +29,7 @@
#include <openssl/bio.h>
#include <openssl/cipher.h>
#include <openssl/crypto.h>
+#include <openssl/curve25519.h>
#include <openssl/err.h>
#include <openssl/hmac.h>
#include <openssl/pem.h>
@@ -1440,6 +1441,230 @@
EXPECT_EQ(0, X509_NAME_cmp(sk_X509_NAME_value(list, 2), name1));
}
+// kECHConfig contains a serialized ECHConfig value.
+static const uint8_t kECHConfig[] = {
+ // version
+ 0xfe, 0x09,
+ // length
+ 0x00, 0x42,
+ // contents.public_name
+ 0x00, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65,
+ // contents.public_key
+ 0x00, 0x20, 0xa6, 0x9a, 0x41, 0x48, 0x5d, 0x32, 0x96, 0xa4, 0xe0, 0xc3,
+ 0x6a, 0xee, 0xf6, 0x63, 0x0f, 0x59, 0x32, 0x6f, 0xdc, 0xff, 0x81, 0x29,
+ 0x59, 0xa5, 0x85, 0xd3, 0x9b, 0x3b, 0xde, 0x98, 0x55, 0x5c,
+ // contents.kem_id
+ 0x00, 0x20,
+ // contents.cipher_suites
+ 0x00, 0x08, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03,
+ // contents.maximum_name_length
+ 0x00, 0x10,
+ // contents.extensions
+ 0x00, 0x00};
+
+// kECHConfigPublicKey is the public key encoded in |kECHConfig|.
+static const uint8_t kECHConfigPublicKey[X25519_PUBLIC_VALUE_LEN] = {
+ 0xa6, 0x9a, 0x41, 0x48, 0x5d, 0x32, 0x96, 0xa4, 0xe0, 0xc3, 0x6a,
+ 0xee, 0xf6, 0x63, 0x0f, 0x59, 0x32, 0x6f, 0xdc, 0xff, 0x81, 0x29,
+ 0x59, 0xa5, 0x85, 0xd3, 0x9b, 0x3b, 0xde, 0x98, 0x55, 0x5c};
+
+// kECHConfigPrivateKey is the X25519 private key corresponding to
+// |kECHConfigPublicKey|.
+static const uint8_t kECHConfigPrivateKey[X25519_PRIVATE_KEY_LEN] = {
+ 0xbc, 0xb5, 0x51, 0x29, 0x31, 0x10, 0x30, 0xc9, 0xed, 0x26, 0xde,
+ 0xd4, 0xb3, 0xdf, 0x3a, 0xce, 0x06, 0x8a, 0xee, 0x17, 0xab, 0xce,
+ 0xd7, 0xdb, 0xf3, 0x11, 0xe5, 0xa8, 0xf3, 0xb1, 0x8e, 0x24};
+
+// MakeECHConfig serializes an ECHConfig and writes it to |*out| with the
+// specified parameters. |cipher_suites| is a list of code points which should
+// contain pairs of KDF and AEAD IDs.
+bool MakeECHConfig(std::vector<uint8_t> *out, uint16_t kem_id,
+ Span<const uint8_t> public_key,
+ Span<const uint16_t> cipher_suites,
+ Span<const uint8_t> extensions) {
+ bssl::ScopedCBB cbb;
+ CBB contents, child;
+ static const char kPublicName[] = "example.com";
+ if (!CBB_init(cbb.get(), 64) ||
+ !CBB_add_u16(cbb.get(), TLSEXT_TYPE_encrypted_client_hello) ||
+ !CBB_add_u16_length_prefixed(cbb.get(), &contents) ||
+ !CBB_add_u16_length_prefixed(&contents, &child) ||
+ !CBB_add_bytes(&child, reinterpret_cast<const uint8_t *>(kPublicName),
+ strlen(kPublicName)) ||
+ !CBB_add_u16_length_prefixed(&contents, &child) ||
+ !CBB_add_bytes(&child, public_key.data(), public_key.size()) ||
+ !CBB_add_u16(&contents, kem_id) ||
+ !CBB_add_u16_length_prefixed(&contents, &child)) {
+ return false;
+ }
+ for (uint16_t cipher_suite : cipher_suites) {
+ if (!CBB_add_u16(&child, cipher_suite)) {
+ return false;
+ }
+ }
+ if (!CBB_add_u16(&contents, strlen(kPublicName)) || // maximum_name_length
+ !CBB_add_u16_length_prefixed(&contents, &child) ||
+ !CBB_add_bytes(&child, extensions.data(), extensions.size()) ||
+ !CBB_flush(cbb.get())) {
+ return false;
+ }
+
+ out->assign(CBB_data(cbb.get()), CBB_data(cbb.get()) + CBB_len(cbb.get()));
+ return true;
+}
+
+TEST(SSLTest, ECHServerConfigList) {
+ // kWrongPrivateKey is an unrelated, but valid X25519 private key.
+ const uint8_t kWrongPrivateKey[X25519_PRIVATE_KEY_LEN] = {
+ 0xbb, 0xfe, 0x08, 0xf7, 0x31, 0xde, 0x9c, 0x8a, 0xf2, 0x06, 0x4a,
+ 0x18, 0xd7, 0x8b, 0x79, 0x31, 0xe2, 0x53, 0xdd, 0x63, 0x8f, 0x58,
+ 0x42, 0xda, 0x21, 0x0e, 0x61, 0x97, 0x29, 0xcc, 0x17, 0x71};
+
+ bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+ ASSERT_TRUE(ctx);
+
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> config_list(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ ASSERT_TRUE(config_list);
+
+ // Adding an ECHConfig with the wrong private key is an error.
+ ASSERT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, kECHConfig, sizeof(kECHConfig),
+ kWrongPrivateKey, sizeof(kWrongPrivateKey)));
+ uint32_t err = ERR_get_error();
+ EXPECT_EQ(ERR_LIB_SSL, ERR_GET_LIB(err));
+ EXPECT_EQ(SSL_R_ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH,
+ ERR_GET_REASON(err));
+ ERR_clear_error();
+
+ // Adding an ECHConfig with the matching private key succeeds.
+ ASSERT_TRUE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, kECHConfig, sizeof(kECHConfig),
+ kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+
+ ASSERT_TRUE(
+ SSL_CTX_set1_ech_server_config_list(ctx.get(), config_list.get()));
+
+ // Build a new config list and replace the old one on |ctx|.
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> next_config_list(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ ASSERT_TRUE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ next_config_list.get(), /*is_retry_config=*/1, kECHConfig,
+ sizeof(kECHConfig), kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+ ASSERT_TRUE(
+ SSL_CTX_set1_ech_server_config_list(ctx.get(), next_config_list.get()));
+}
+
+TEST(SSLTest, ECHServerConfigListTruncatedPublicKey) {
+ std::vector<uint8_t> ech_config;
+ ASSERT_TRUE(MakeECHConfig(
+ &ech_config, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+ MakeConstSpan(kECHConfigPublicKey, sizeof(kECHConfigPublicKey) - 1),
+ std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM},
+ /*extensions=*/{}));
+
+ bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+ ASSERT_TRUE(ctx);
+
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> config_list(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ ASSERT_TRUE(config_list);
+ ASSERT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, ech_config.data(),
+ ech_config.size(), kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+
+ uint32_t err = ERR_peek_error();
+ EXPECT_EQ(ERR_LIB_SSL, ERR_GET_LIB(err));
+ EXPECT_EQ(SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG, ERR_GET_REASON(err));
+ ERR_clear_error();
+}
+
+// Test that |SSL_CTX_set1_ech_server_config_list| fails when the config list
+// has no retry configs.
+TEST(SSLTest, ECHServerConfigsWithoutRetryConfigs) {
+ bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+ ASSERT_TRUE(ctx);
+
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> config_list(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ ASSERT_TRUE(config_list);
+
+ // Adding an ECHConfig with the matching private key succeeds.
+ ASSERT_TRUE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/0, kECHConfig, sizeof(kECHConfig),
+ kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+
+ ASSERT_FALSE(
+ SSL_CTX_set1_ech_server_config_list(ctx.get(), config_list.get()));
+ uint32_t err = ERR_peek_error();
+ EXPECT_EQ(ERR_LIB_SSL, ERR_GET_LIB(err));
+ EXPECT_EQ(SSL_R_ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS, ERR_GET_REASON(err));
+ ERR_clear_error();
+
+ // Add the same ECHConfig to the list, but this time mark it as a retry
+ // config.
+ ASSERT_TRUE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, kECHConfig, sizeof(kECHConfig),
+ kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+ ASSERT_TRUE(
+ SSL_CTX_set1_ech_server_config_list(ctx.get(), config_list.get()));
+}
+
+// Test that the server APIs reject ECHConfigs with unsupported features.
+TEST(SSLTest, UnsupportedECHConfig) {
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> config_list(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ ASSERT_TRUE(config_list);
+
+ // Unsupported versions are rejected.
+ static const uint8_t kUnsupportedVersion[] = {0xff, 0xff, 0x00, 0x00};
+ EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, kUnsupportedVersion,
+ sizeof(kUnsupportedVersion), kECHConfigPrivateKey,
+ sizeof(kECHConfigPrivateKey)));
+
+ // Unsupported cipher suites are rejected. (We only support HKDF-SHA256.)
+ std::vector<uint8_t> ech_config;
+ ASSERT_TRUE(MakeECHConfig(
+ &ech_config, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, kECHConfigPublicKey,
+ std::vector<uint16_t>{EVP_HPKE_HKDF_SHA384, EVP_HPKE_AEAD_AES_128_GCM},
+ /*extensions=*/{}));
+ EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, ech_config.data(),
+ ech_config.size(), kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+
+ // Unsupported KEMs are rejected.
+ static const uint8_t kP256PublicKey[] = {
+ 0x04, 0xe6, 0x2b, 0x69, 0xe2, 0xbf, 0x65, 0x9f, 0x97, 0xbe, 0x2f,
+ 0x1e, 0x0d, 0x94, 0x8a, 0x4c, 0xd5, 0x97, 0x6b, 0xb7, 0xa9, 0x1e,
+ 0x0d, 0x46, 0xfb, 0xdd, 0xa9, 0xa9, 0x1e, 0x9d, 0xdc, 0xba, 0x5a,
+ 0x01, 0xe7, 0xd6, 0x97, 0xa8, 0x0a, 0x18, 0xf9, 0xc3, 0xc4, 0xa3,
+ 0x1e, 0x56, 0xe2, 0x7c, 0x83, 0x48, 0xdb, 0x16, 0x1a, 0x1c, 0xf5,
+ 0x1d, 0x7e, 0xf1, 0x94, 0x2d, 0x4b, 0xcf, 0x72, 0x22, 0xc1};
+ static const uint8_t kP256PrivateKey[] = {
+ 0x07, 0x0f, 0x08, 0x72, 0x7a, 0xd4, 0xa0, 0x4a, 0x9c, 0xdd, 0x59,
+ 0xc9, 0x4d, 0x89, 0x68, 0x77, 0x08, 0xb5, 0x6f, 0xc9, 0x5d, 0x30,
+ 0x77, 0x0e, 0xe8, 0xd1, 0xc9, 0xce, 0x0a, 0x8b, 0xb4, 0x6a};
+ ASSERT_TRUE(MakeECHConfig(
+ &ech_config, 0x0010 /* DHKEM(P-256, HKDF-SHA256) */, kP256PublicKey,
+ std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM},
+ /*extensions=*/{}));
+ EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, ech_config.data(),
+ ech_config.size(), kP256PrivateKey, sizeof(kP256PrivateKey)));
+
+ // Unsupported extensions are rejected.
+ static const uint8_t kExtensions[] = {0x00, 0x01, 0x00, 0x00};
+ ASSERT_TRUE(MakeECHConfig(
+ &ech_config, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, kECHConfigPublicKey,
+ std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM},
+ kExtensions));
+ EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), /*is_retry_config=*/1, ech_config.data(),
+ ech_config.size(), kECHConfigPrivateKey, sizeof(kECHConfigPrivateKey)));
+}
+
static void AppendSession(SSL_SESSION *session, void *arg) {
std::vector<SSL_SESSION*> *out =
reinterpret_cast<std::vector<SSL_SESSION*>*>(arg);
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 155c713..4216fcd 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -209,11 +209,11 @@
}
bool ssl_client_hello_init(const SSL *ssl, SSL_CLIENT_HELLO *out,
- const SSLMessage &msg) {
+ Span<const uint8_t> body) {
OPENSSL_memset(out, 0, sizeof(*out));
out->ssl = const_cast<SSL *>(ssl);
- out->client_hello = CBS_data(&msg.body);
- out->client_hello_len = CBS_len(&msg.body);
+ out->client_hello = body.data();
+ out->client_hello_len = body.size();
CBS client_hello, random, session_id;
CBS_init(&client_hello, out->client_hello, out->client_hello_len);
@@ -591,7 +591,7 @@
}
-// Encrypted Client Hello (ECH)
+// Encrypted ClientHello (ECH)
//
// https://tools.ietf.org/html/draft-ietf-tls-esni-09
@@ -748,6 +748,35 @@
return true;
}
+static bool ext_ech_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+ SSL *const ssl = hs->ssl;
+ if (ssl_protocol_version(ssl) < TLS1_3_VERSION || //
+ hs->ech_accept || //
+ hs->ech_server_config_list == nullptr) {
+ return true;
+ }
+
+ // Write the list of retry configs to |out|. Note
+ // |SSL_CTX_set1_ech_server_config_list| ensures |ech_server_config_list|
+ // contains at least one retry config.
+ CBB body, retry_configs;
+ if (!CBB_add_u16(out, TLSEXT_TYPE_encrypted_client_hello) ||
+ !CBB_add_u16_length_prefixed(out, &body) ||
+ !CBB_add_u16_length_prefixed(&body, &retry_configs)) {
+ return false;
+ }
+ for (const ECHServerConfig &config : hs->ech_server_config_list->configs) {
+ if (!config.is_retry_config()) {
+ continue;
+ }
+ if (!CBB_add_bytes(&retry_configs, config.raw().data(),
+ config.raw().size())) {
+ return false;
+ }
+ }
+ return CBB_flush(out);
+}
+
static bool ext_ech_is_inner_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
return true;
}
@@ -3264,7 +3293,7 @@
ext_ech_add_clienthello,
ext_ech_parse_serverhello,
ext_ech_parse_clienthello,
- dont_add_serverhello,
+ ext_ech_add_serverhello,
},
{
TLSEXT_TYPE_ech_is_inner,
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 83439a0..50d38d1 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -16,6 +16,8 @@
"strings"
"sync"
"time"
+
+ "boringssl.googlesource.com/boringssl/ssl/test/runner/hpke"
)
const (
@@ -129,6 +131,7 @@
extensionDuplicate uint16 = 0xffff // not IANA assigned
extensionEncryptedClientHello uint16 = 0xfe09 // not IANA assigned
extensionECHIsInner uint16 = 0xda09 // not IANA assigned
+ extensionECHOuterExtensions uint16 = 0xfd00 // not IANA assigned
)
// TLS signaling cipher suite values
@@ -266,6 +269,7 @@
QUICTransportParamsLegacy []byte // the legacy QUIC transport params received from the peer
HasApplicationSettings bool // whether ALPS was negotiated
PeerApplicationSettings []byte // application settings received from the peer
+ ECHAccepted bool // whether ECH was accepted on this connection
}
// ClientAuthType declares the policy the server will follow for
@@ -422,6 +426,19 @@
// in the client's handshake to support virtual hosting.
ServerName string
+ // ClientECHConfig, when non-nil, is the ECHConfig the client will use to
+ // attempt ECH.
+ ClientECHConfig *ECHConfig
+
+ // ECHCipherSuites, for the client, is the list of HPKE cipher suites in
+ // decreasing order of preference. If empty, the default will be used.
+ ECHCipherSuites []HPKECipherSuite
+
+ // ECHOuterExtensions is the list of extensions that the client will
+ // compress with the ech_outer_extensions extension. If empty, no extensions
+ // will be compressed.
+ ECHOuterExtensions []uint16
+
// ClientAuth determines the server's policy for
// TLS Client Authentication. The default is NoClientCert.
ClientAuth ClientAuthType
@@ -846,23 +863,74 @@
// 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
+ // IgnoreECHConfigCipherPreferences, when true, causes the client to ignore
+ // the cipher preferences in the ECHConfig and select the most preferred ECH
+ // cipher suite unconditionally.
+ IgnoreECHConfigCipherPreferences bool
+
+ // ExpectECHRetryConfigs, when non-nil, contains the expected bytes of the
+ // server's retry configs.
+ ExpectECHRetryConfigs []byte
// 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
+ // SendInvalidECHIsInner, if not empty, causes the client to send the
+ // specified byte string in the ech_is_inner extension.
+ SendInvalidECHIsInner []byte
- // 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
+ // OmitECHIsInner, if true, causes the client to omit the ech_is_inner
+ // extension on the ClientHelloInner message.
+ OmitECHIsInner bool
+
+ // OmitSecondECHIsInner, if true, causes the client to omit the ech_is_inner
+ // extension on the second ClientHelloInner message.
+ OmitSecondECHIsInner bool
+
+ // AlwaysSendECHIsInner, if true, causes the client to send the
+ // ech_is_inner extension on all ClientHello messages. The server is then
+ // expected to unconditionally confirm the extension when negotiating
+ // TLS 1.3 or later.
+ AlwaysSendECHIsInner bool
+
+ // TruncateClientECHEnc, if true, causes the client to send a shortened
+ // ClientECH.enc value in its encrypted_client_hello extension.
+ TruncateClientECHEnc bool
+
+ // OfferSessionInClientHelloOuter, if true, causes the client to offer
+ // sessions in ClientHelloOuter.
+ OfferSessionInClientHelloOuter bool
+
+ // FirstExtensionInClientHelloOuter, if non-zero, causes the client to place
+ // the specified extension first in ClientHelloOuter.
+ FirstExtensionInClientHelloOuter uint16
+
+ // OnlyCompressSecondClientHelloInner, if true, causes the client to
+ // only apply outer_extensions to the second ClientHello.
+ OnlyCompressSecondClientHelloInner bool
+
+ // OmitSecondEncryptedClientHello, if true, causes the client to omit the
+ // second encrypted_client_hello extension.
+ OmitSecondEncryptedClientHello bool
+
+ // CorruptEncryptedClientHello, if true, causes the client to incorrectly
+ // encrypt the encrypted_client_hello extension.
+ CorruptEncryptedClientHello bool
+
+ // CorruptSecondEncryptedClientHello, if true, causes the client to
+ // incorrectly encrypt the second encrypted_client_hello extension.
+ CorruptSecondEncryptedClientHello bool
+
+ // AllowTLS12InClientHelloInner, if true, causes the client to include
+ // TLS 1.2 and earlier in ClientHelloInner.
+ AllowTLS12InClientHelloInner bool
+
+ // MinimalClientHelloOuter, if true, causes the client to omit most fields
+ // in ClientHelloOuter. Note this will make handshake attempts with the
+ // ClientHelloOuter fail and should only be used in tests that expect
+ // success.
+ MinimalClientHelloOuter bool
// SwapNPNAndALPN switches the relative order between NPN and ALPN in
// both ClientHello and ServerHello.
@@ -1875,6 +1943,19 @@
return defaultCurves
}
+var defaultECHCipherSuitePreferences = []HPKECipherSuite{
+ {KDF: hpke.HKDFSHA256, AEAD: hpke.AES128GCM},
+ {KDF: hpke.HKDFSHA256, AEAD: hpke.AES256GCM},
+ {KDF: hpke.HKDFSHA256, AEAD: hpke.ChaCha20Poly1305},
+}
+
+func (c *Config) echCipherSuitePreferences() []HPKECipherSuite {
+ if c == nil || len(c.ECHCipherSuites) == 0 {
+ return defaultECHCipherSuitePreferences
+ }
+ return c.ECHCipherSuites
+}
+
func wireToVersion(vers uint16, isDTLS bool) (uint16, bool) {
if isDTLS {
switch vers {
@@ -1904,16 +1985,21 @@
return vers, true
}
-func (c *Config) supportedVersions(isDTLS bool) []uint16 {
+func (c *Config) supportedVersions(isDTLS, requireTLS13 bool) []uint16 {
versions := allTLSWireVersions
if isDTLS {
versions = allDTLSWireVersions
}
var ret []uint16
- for _, vers := range versions {
- if _, ok := c.isSupportedVersion(vers, isDTLS); ok {
- ret = append(ret, vers)
+ for _, wireVers := range versions {
+ vers, ok := c.isSupportedVersion(wireVers, isDTLS)
+ if !ok {
+ continue
}
+ if requireTLS13 && vers < VersionTLS13 {
+ continue
+ }
+ ret = append(ret, wireVers)
}
return ret
}
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 6c26e94..4932a0b 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -119,6 +119,9 @@
// handshake data may be received until the next flight or epoch change.
seenHandshakePackEnd bool
+ // echAccepted indicates whether ECH was accepted for this connection.
+ echAccepted bool
+
tmp [16]byte
}
@@ -1860,6 +1863,7 @@
state.QUICTransportParamsLegacy = c.quicTransportParamsLegacy
state.HasApplicationSettings = c.hasApplicationSettings
state.PeerApplicationSettings = c.peerApplicationSettings
+ state.ECHAccepted = c.echAccepted
}
return state
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 774b093..74e9407 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -24,16 +24,19 @@
)
type clientHandshakeState struct {
- c *Conn
- serverHello *serverHelloMsg
- hello *clientHelloMsg
- suite *cipherSuite
- finishedHash finishedHash
- keyShares map[CurveID]ecdhCurve
- masterSecret []byte
- session *ClientSessionState
- finishedBytes []byte
- peerPublicKey crypto.PublicKey
+ c *Conn
+ serverHello *serverHelloMsg
+ hello *clientHelloMsg
+ innerHello *clientHelloMsg
+ echHPKEContext *hpke.Context
+ suite *cipherSuite
+ finishedHash finishedHash
+ innerFinishedHash finishedHash
+ keyShares map[CurveID]ecdhCurve
+ masterSecret []byte
+ session *ClientSessionState
+ finishedBytes []byte
+ peerPublicKey crypto.PublicKey
}
func mapClientHelloVersion(vers uint16, isDTLS bool) uint16 {
@@ -92,240 +95,16 @@
c.sendHandshakeSeq = 0
c.recvHandshakeSeq = 0
- nextProtosLength := 0
- for _, proto := range c.config.NextProtos {
- if l := len(proto); l > 255 {
- return errors.New("tls: invalid NextProtos value")
- } else {
- nextProtosLength += 1 + l
- }
- }
- if nextProtosLength > 0xffff {
- return errors.New("tls: NextProtos values too large")
+ hs := &clientHandshakeState{
+ c: c,
+ keyShares: make(map[CurveID]ecdhCurve),
}
- // Translate the bugs that modify ClientHello extension order into a
- // list of prefix extensions. The marshal function will try these
- // extensions before any others, followed by any remaining extensions in
- // the default order.
- var prefixExtensions []uint16
- if c.config.Bugs.PSKBinderFirst && !c.config.Bugs.OnlyCorruptSecondPSKBinder {
- prefixExtensions = append(prefixExtensions, extensionPreSharedKey)
- }
- if c.config.Bugs.SwapNPNAndALPN {
- prefixExtensions = append(prefixExtensions, extensionALPN)
- prefixExtensions = append(prefixExtensions, extensionNextProtoNeg)
- }
-
- quicTransportParams := c.config.QUICTransportParams
- quicTransportParamsLegacy := c.config.QUICTransportParams
- if !c.config.QUICTransportParamsUseLegacyCodepoint.IncludeStandard() {
- quicTransportParams = nil
- }
- if !c.config.QUICTransportParamsUseLegacyCodepoint.IncludeLegacy() {
- quicTransportParamsLegacy = nil
- }
-
- minVersion := c.config.minVersion(c.isDTLS)
- maxVersion := c.config.maxVersion(c.isDTLS)
- hello := &clientHelloMsg{
- isDTLS: c.isDTLS,
- compressionMethods: []uint8{compressionNone},
- random: make([]byte, 32),
- 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,
- secureRenegotiation: []byte{},
- alpnProtocols: c.config.NextProtos,
- quicTransportParams: quicTransportParams,
- quicTransportParamsLegacy: quicTransportParamsLegacy,
- duplicateExtension: c.config.Bugs.DuplicateExtension,
- channelIDSupported: c.config.ChannelID != nil,
- tokenBindingParams: c.config.TokenBindingParams,
- tokenBindingVersion: c.config.TokenBindingVersion,
- extendedMasterSecret: maxVersion >= VersionTLS10,
- srtpProtectionProfiles: c.config.SRTPProtectionProfiles,
- srtpMasterKeyIdentifier: c.config.Bugs.SRTPMasterKeyIdentifer,
- customExtension: c.config.Bugs.CustomExtension,
- omitExtensions: c.config.Bugs.OmitExtensions,
- emptyExtensions: c.config.Bugs.EmptyExtensions,
- delegatedCredentials: !c.config.Bugs.DisableDelegatedCredentials,
- prefixExtensions: prefixExtensions,
- }
-
- if maxVersion >= VersionTLS13 {
- hello.vers = mapClientHelloVersion(VersionTLS12, c.isDTLS)
- if !c.config.Bugs.OmitSupportedVersions {
- hello.supportedVersions = c.config.supportedVersions(c.isDTLS)
- }
- hello.pskKEModes = []byte{pskDHEKEMode}
- } else {
- hello.vers = mapClientHelloVersion(maxVersion, c.isDTLS)
- }
-
- if c.config.Bugs.SendClientVersion != 0 {
- hello.vers = c.config.Bugs.SendClientVersion
- }
-
- if len(c.config.Bugs.SendSupportedVersions) > 0 {
- hello.supportedVersions = c.config.Bugs.SendSupportedVersions
- }
-
- disableEMS := c.config.Bugs.NoExtendedMasterSecret
- if c.cipherSuite != nil {
- disableEMS = c.config.Bugs.NoExtendedMasterSecretOnRenegotiation
- }
-
- if disableEMS {
- hello.extendedMasterSecret = false
- }
-
- if c.config.Bugs.NoSupportedCurves {
- hello.supportedCurves = nil
- }
-
- if c.config.Bugs.SendPSKKeyExchangeModes != nil {
- hello.pskKEModes = c.config.Bugs.SendPSKKeyExchangeModes
- }
-
- if c.config.Bugs.SendCompressionMethods != nil {
- hello.compressionMethods = c.config.Bugs.SendCompressionMethods
- }
-
- if c.config.Bugs.SendSupportedPointFormats != nil {
- hello.supportedPoints = c.config.Bugs.SendSupportedPointFormats
- }
-
- if len(c.clientVerify) > 0 && !c.config.Bugs.EmptyRenegotiationInfo {
- if c.config.Bugs.BadRenegotiationInfo {
- hello.secureRenegotiation = append(hello.secureRenegotiation, c.clientVerify...)
- hello.secureRenegotiation[0] ^= 0x80
- } else {
- hello.secureRenegotiation = c.clientVerify
- }
- }
-
- if c.config.Bugs.DuplicateCompressedCertAlgs {
- hello.compressedCertAlgs = []uint16{1, 1}
- } else if len(c.config.CertCompressionAlgs) > 0 {
- hello.compressedCertAlgs = make([]uint16, 0, len(c.config.CertCompressionAlgs))
- for id := range c.config.CertCompressionAlgs {
- hello.compressedCertAlgs = append(hello.compressedCertAlgs, uint16(id))
- }
- }
-
- if c.noRenegotiationInfo() {
- hello.secureRenegotiation = nil
- }
-
- for protocol := range c.config.ApplicationSettings {
- 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)
- hello.hasKeyShares = true
- hello.trailingKeyShareData = c.config.Bugs.TrailingKeyShareData
- curvesToSend := c.config.defaultCurves()
- for _, curveID := range hello.supportedCurves {
- if !curvesToSend[curveID] {
- continue
- }
- curve, ok := curveForCurveID(curveID, c.config)
- if !ok {
- continue
- }
- publicKey, err := curve.offer(c.config.rand())
- if err != nil {
- return err
- }
-
- if c.config.Bugs.SendCurve != 0 {
- curveID = c.config.Bugs.SendCurve
- }
- if c.config.Bugs.InvalidECDHPoint {
- publicKey[0] ^= 0xff
- }
-
- hello.keyShares = append(hello.keyShares, keyShareEntry{
- group: curveID,
- keyExchange: publicKey,
- })
- keyShares[curveID] = curve
-
- if c.config.Bugs.DuplicateKeyShares {
- hello.keyShares = append(hello.keyShares, hello.keyShares[len(hello.keyShares)-1])
- }
- }
-
- if c.config.Bugs.MissingKeyShare {
- hello.hasKeyShares = false
- }
- }
-
- possibleCipherSuites := c.config.cipherSuites()
- hello.cipherSuites = make([]uint16, 0, len(possibleCipherSuites))
-
-NextCipherSuite:
- for _, suiteID := range possibleCipherSuites {
- for _, suite := range cipherSuites {
- if suite.id != suiteID {
- continue
- }
- // Don't advertise TLS 1.2-only cipher suites unless
- // we're attempting TLS 1.2.
- if maxVersion < VersionTLS12 && suite.flags&suiteTLS12 != 0 {
- continue
- }
- hello.cipherSuites = append(hello.cipherSuites, suiteID)
- continue NextCipherSuite
- }
- }
-
- if c.config.Bugs.AdvertiseAllConfiguredCiphers {
- hello.cipherSuites = possibleCipherSuites
- }
-
- if c.config.Bugs.SendRenegotiationSCSV {
- hello.cipherSuites = append(hello.cipherSuites, renegotiationSCSV)
- }
-
- if c.config.Bugs.SendFallbackSCSV {
- hello.cipherSuites = append(hello.cipherSuites, fallbackSCSV)
- }
-
- _, err := io.ReadFull(c.config.rand(), hello.random)
- if err != nil {
- c.sendAlert(alertInternalError)
- return errors.New("tls: short read from Rand: " + err.Error())
- }
-
- if maxVersion >= VersionTLS12 && !c.config.Bugs.NoSignatureAlgorithms {
- hello.signatureAlgorithms = c.config.verifySignatureAlgorithms()
- }
-
+ // Pick a session to resume.
var session *ClientSessionState
var cacheKey string
sessionCache := c.config.ClientSessionCache
-
if sessionCache != nil {
- hello.ticketSupported = !c.config.SessionTicketsDisabled
-
// Try to resume a previously negotiated TLS session, if
// available.
cacheKey = clientSessionCacheKey(c.conn.RemoteAddr(), c.config)
@@ -339,7 +118,7 @@
// previous session are still valid.
cipherSuiteOk := false
if candidateSession.vers <= VersionTLS12 {
- for _, id := range hello.cipherSuites {
+ for _, id := range c.config.cipherSuites() {
if id == candidateSession.cipherSuite.id {
cipherSuiteOk = true
break
@@ -351,149 +130,84 @@
cipherSuiteOk = true
}
- versOk := candidateSession.vers >= minVersion &&
- candidateSession.vers <= maxVersion
+ _, versOk := c.config.isSupportedVersion(candidateSession.wireVersion, c.isDTLS)
if ticketOk && versOk && cipherSuiteOk {
session = candidateSession
+ hs.session = session
}
}
}
- if session != nil && c.config.time().Before(session.ticketExpiration) {
- ticket := session.sessionTicket
- if c.config.Bugs.FilterTicket != nil && len(ticket) > 0 {
- // Copy the ticket so FilterTicket may act in-place.
- ticket = make([]byte, len(session.sessionTicket))
- copy(ticket, session.sessionTicket)
-
- ticket, err = c.config.Bugs.FilterTicket(ticket)
- if err != nil {
- return err
- }
+ // Set up ECH parameters.
+ var err error
+ var earlyHello *clientHelloMsg
+ if c.config.ClientECHConfig != nil {
+ if c.config.ClientECHConfig.KEM != hpke.X25519WithHKDFSHA256 {
+ return errors.New("tls: unsupported KEM type in ECHConfig")
}
- if session.vers >= VersionTLS13 || c.config.Bugs.SendBothTickets {
- // TODO(nharper): Support sending more
- // than one PSK identity.
- ticketAge := uint32(c.config.time().Sub(session.ticketCreationTime) / time.Millisecond)
- if c.config.Bugs.SendTicketAge != 0 {
- ticketAge = uint32(c.config.Bugs.SendTicketAge / time.Millisecond)
- }
- psk := pskIdentity{
- ticket: ticket,
- obfuscatedTicketAge: session.ticketAgeAdd + ticketAge,
- }
- hello.pskIdentities = []pskIdentity{psk}
-
- if c.config.Bugs.ExtraPSKIdentity {
- hello.pskIdentities = append(hello.pskIdentities, psk)
- }
+ echCipherSuite, ok := chooseECHCipherSuite(c.config.ClientECHConfig, c.config)
+ if !ok {
+ return errors.New("tls: did not find compatible cipher suite in ECHConfig")
}
- if session.vers < VersionTLS13 || c.config.Bugs.SendBothTickets {
- if ticket != nil {
- hello.sessionTicket = ticket
- // A random session ID is used to detect when the
- // server accepted the ticket and is resuming a session
- // (see RFC 5077).
- sessionIDLen := 16
- if c.config.Bugs.TicketSessionIDLength != 0 {
- sessionIDLen = c.config.Bugs.TicketSessionIDLength
- }
- if c.config.Bugs.EmptyTicketSessionID {
- sessionIDLen = 0
- }
- hello.sessionID = make([]byte, sessionIDLen)
- if _, err := io.ReadFull(c.config.rand(), hello.sessionID); err != nil {
- c.sendAlert(alertInternalError)
- return errors.New("tls: short read from Rand: " + err.Error())
- }
- } else {
- hello.sessionID = session.sessionID
- }
+ info := []byte("tls ech\x00")
+ info = append(info, MarshalECHConfig(c.config.ClientECHConfig)...)
+
+ var echEnc []byte
+ hs.echHPKEContext, echEnc, err = hpke.SetupBaseSenderX25519(echCipherSuite.KDF, echCipherSuite.AEAD, c.config.ClientECHConfig.PublicKey, info, nil)
+ if err != nil {
+ return errors.New("tls: ech: failed to set up client's HPKE sender context")
}
- }
- // Request compatibility mode from the client by sending a fake session
- // ID. Although BoringSSL always enables compatibility mode, other
- // implementations make it conditional on the ClientHello. We test
- // BoringSSL's expected behavior with SendClientHelloSessionID.
- if len(hello.sessionID) == 0 && maxVersion >= VersionTLS13 {
- hello.sessionID = make([]byte, 32)
- if _, err := io.ReadFull(c.config.rand(), hello.sessionID); err != nil {
- c.sendAlert(alertInternalError)
- return errors.New("tls: short read from Rand: " + err.Error())
- }
- }
- if c.config.Bugs.MockQUICTransport != nil && !c.config.Bugs.CompatModeWithQUIC {
- hello.sessionID = []byte{}
- }
-
- if c.config.Bugs.SendCipherSuites != nil {
- hello.cipherSuites = c.config.Bugs.SendCipherSuites
- }
-
- var sendEarlyData bool
- if len(hello.pskIdentities) > 0 && c.config.Bugs.SendEarlyData != nil {
- hello.hasEarlyData = true
- sendEarlyData = true
- }
- if c.config.Bugs.SendFakeEarlyDataLength > 0 {
- hello.hasEarlyData = true
- }
- if c.config.Bugs.OmitEarlyDataExtension {
- hello.hasEarlyData = false
- }
- if c.config.Bugs.SendClientHelloSessionID != nil {
- hello.sessionID = c.config.Bugs.SendClientHelloSessionID
- }
-
- // PSK binders must be computed after the rest of the ClientHello is
- // constructed.
- if len(hello.pskIdentities) > 0 {
- version := session.wireVersion
- // We may have a pre-1.3 session if SendBothTickets is
- // set.
- if session.vers < VersionTLS13 {
- version = VersionTLS13
- }
- generatePSKBinders(version, hello, session, nil, nil, c.config)
- }
-
- if c.config.Bugs.SendClientHelloWithFixes != nil {
- hello, err = replaceClientHello(hello, c.config.Bugs.SendClientHelloWithFixes)
+ hs.innerHello, err = hs.createClientHello(nil, nil)
if err != nil {
return err
}
+ hs.hello, err = hs.createClientHello(hs.innerHello, echEnc)
+ if err != nil {
+ return err
+ }
+ earlyHello = hs.innerHello
+ } else {
+ hs.hello, err = hs.createClientHello(nil, nil)
+ if err != nil {
+ return err
+ }
+ earlyHello = hs.hello
+ }
+
+ if len(earlyHello.pskIdentities) == 0 || c.config.Bugs.SendEarlyData == nil {
+ earlyHello = nil
}
if c.config.Bugs.SendV2ClientHello {
- hello.isV2ClientHello = true
+ hs.hello.isV2ClientHello = true
// The V2ClientHello "challenge" field is variable-length and is
// left-padded or truncated to become the SSL3/TLS random.
challengeLength := c.config.Bugs.V2ClientHelloChallengeLength
if challengeLength == 0 {
- challengeLength = len(hello.random)
+ challengeLength = len(hs.hello.random)
}
- if challengeLength <= len(hello.random) {
- skip := len(hello.random) - challengeLength
+ if challengeLength <= len(hs.hello.random) {
+ skip := len(hs.hello.random) - challengeLength
for i := 0; i < skip; i++ {
- hello.random[i] = 0
+ hs.hello.random[i] = 0
}
- hello.v2Challenge = hello.random[skip:]
+ hs.hello.v2Challenge = hs.hello.random[skip:]
} else {
- hello.v2Challenge = make([]byte, challengeLength)
- copy(hello.v2Challenge, hello.random)
- if _, err := io.ReadFull(c.config.rand(), hello.v2Challenge[len(hello.random):]); err != nil {
+ hs.hello.v2Challenge = make([]byte, challengeLength)
+ copy(hs.hello.v2Challenge, hs.hello.random)
+ if _, err := io.ReadFull(c.config.rand(), hs.hello.v2Challenge[len(hs.hello.random):]); err != nil {
c.sendAlert(alertInternalError)
return fmt.Errorf("tls: short read from Rand: %s", err)
}
}
- c.writeV2Record(hello.marshal())
+ c.writeV2Record(hs.hello.marshal())
} else {
- helloBytes := hello.marshal()
+ helloBytes := hs.hello.marshal()
var appendToHello byte
if c.config.Bugs.PartialClientFinishedWithClientHello {
appendToHello = typeFinished
@@ -523,10 +237,10 @@
}
// Derive early write keys and set Conn state to allow early writes.
- if sendEarlyData {
+ if earlyHello != nil {
finishedHash := newFinishedHash(session.wireVersion, c.isDTLS, session.cipherSuite)
finishedHash.addEntropy(session.secret)
- finishedHash.Write(hello.marshal())
+ finishedHash.Write(earlyHello.marshal())
if !c.config.Bugs.SkipChangeCipherSpec {
c.wireVersion = session.wireVersion
@@ -562,9 +276,9 @@
return errors.New("dtls: bad HelloVerifyRequest version")
}
- hello.raw = nil
- hello.cookie = helloVerifyRequest.cookie
- c.writeRecord(recordTypeHandshake, hello.marshal())
+ hs.hello.raw = nil
+ hs.hello.cookie = helloVerifyRequest.cookie
+ c.writeRecord(recordTypeHandshake, hs.hello.marshal())
c.flushHandshake()
if err := c.simulatePacketLoss(nil); err != nil {
@@ -608,21 +322,26 @@
return errors.New("tls: server selected SSL 3.0")
}
- suite := mutualCipherSuite(hello.cipherSuites, suiteID)
- if suite == nil {
+ cipherSuites := hs.hello.cipherSuites
+ if hs.innerHello != nil && c.config.Bugs.MinimalClientHelloOuter {
+ // hs.hello has a placeholder list of ciphers if testing with
+ // MinimalClientHelloOuter, so we use hs.innerHello instead. (We do not
+ // attempt to support actual different cipher suite preferences between
+ // the two.)
+ cipherSuites = hs.innerHello.cipherSuites
+ }
+ hs.suite = mutualCipherSuite(cipherSuites, suiteID)
+ if hs.suite == nil {
c.sendAlert(alertHandshakeFailure)
return fmt.Errorf("tls: server selected an unsupported cipher suite")
}
- hs := &clientHandshakeState{
- c: c,
- hello: hello,
- suite: suite,
- finishedHash: newFinishedHash(c.wireVersion, c.isDTLS, suite),
- keyShares: keyShares,
- session: session,
+ hs.finishedHash = newFinishedHash(c.wireVersion, c.isDTLS, hs.suite)
+ hs.finishedHash.WriteHandshake(hs.hello.marshal(), hs.c.sendHandshakeSeq-1)
+ if hs.innerHello != nil {
+ hs.innerFinishedHash = newFinishedHash(c.wireVersion, c.isDTLS, hs.suite)
+ hs.innerFinishedHash.WriteHandshake(hs.innerHello.marshal(), hs.c.sendHandshakeSeq-1)
}
- hs.finishedHash.WriteHandshake(hello.marshal(), hs.c.sendHandshakeSeq-1)
if c.vers >= VersionTLS13 {
if err := hs.doTLS13Handshake(msg); err != nil {
@@ -714,13 +433,469 @@
}
c.handshakeComplete = true
- c.cipherSuite = suite
+ c.cipherSuite = hs.suite
copy(c.clientRandom[:], hs.hello.random)
copy(c.serverRandom[:], hs.serverHello.random)
return nil
}
+func chooseECHCipherSuite(echConfig *ECHConfig, config *Config) (HPKECipherSuite, bool) {
+ if echConfig.KEM != hpke.X25519WithHKDFSHA256 {
+ return HPKECipherSuite{}, false
+ }
+
+ for _, wantSuite := range config.echCipherSuitePreferences() {
+ if config.Bugs.IgnoreECHConfigCipherPreferences {
+ return wantSuite, true
+ }
+ for _, cipherSuite := range echConfig.CipherSuites {
+ if cipherSuite == wantSuite {
+ return cipherSuite, true
+ }
+ }
+ }
+ return HPKECipherSuite{}, false
+}
+
+// createClientHello creates a new ClientHello message. If |innerHello| is not
+// nil, this is a ClientHelloOuter that should contain an encrypted |innerHello|
+// with |echEnc| as the encapsulated public key. Otherwise, the ClientHello
+// should reflect the connection's true preferences.
+func (hs *clientHandshakeState) createClientHello(innerHello *clientHelloMsg, echEnc []byte) (*clientHelloMsg, error) {
+ c := hs.c
+ nextProtosLength := 0
+ for _, proto := range c.config.NextProtos {
+ if l := len(proto); l > 255 {
+ return nil, errors.New("tls: invalid NextProtos value")
+ } else {
+ nextProtosLength += 1 + l
+ }
+ }
+ if nextProtosLength > 0xffff {
+ return nil, errors.New("tls: NextProtos values too large")
+ }
+
+ quicTransportParams := c.config.QUICTransportParams
+ quicTransportParamsLegacy := c.config.QUICTransportParams
+ if !c.config.QUICTransportParamsUseLegacyCodepoint.IncludeStandard() {
+ quicTransportParams = nil
+ }
+ if !c.config.QUICTransportParamsUseLegacyCodepoint.IncludeLegacy() {
+ quicTransportParamsLegacy = nil
+ }
+
+ isInner := innerHello == nil && hs.echHPKEContext != nil
+
+ minVersion := c.config.minVersion(c.isDTLS)
+ maxVersion := c.config.maxVersion(c.isDTLS)
+ // The ClientHelloInner may not offer TLS 1.2 or below.
+ requireTLS13 := isInner && !c.config.Bugs.AllowTLS12InClientHelloInner
+ if requireTLS13 && minVersion < VersionTLS13 {
+ minVersion = VersionTLS13
+ if minVersion > maxVersion {
+ return nil, errors.New("tls: ECH requires TLS 1.3")
+ }
+ }
+
+ hello := &clientHelloMsg{
+ isDTLS: c.isDTLS,
+ compressionMethods: []uint8{compressionNone},
+ random: make([]byte, 32),
+ ocspStapling: !c.config.Bugs.NoOCSPStapling,
+ sctListSupported: !c.config.Bugs.NoSignedCertificateTimestamps,
+ supportedCurves: c.config.curvePreferences(),
+ supportedPoints: []uint8{pointFormatUncompressed},
+ nextProtoNeg: len(c.config.NextProtos) > 0,
+ secureRenegotiation: []byte{},
+ alpnProtocols: c.config.NextProtos,
+ quicTransportParams: quicTransportParams,
+ quicTransportParamsLegacy: quicTransportParamsLegacy,
+ duplicateExtension: c.config.Bugs.DuplicateExtension,
+ channelIDSupported: c.config.ChannelID != nil,
+ tokenBindingParams: c.config.TokenBindingParams,
+ tokenBindingVersion: c.config.TokenBindingVersion,
+ extendedMasterSecret: maxVersion >= VersionTLS10,
+ srtpProtectionProfiles: c.config.SRTPProtectionProfiles,
+ srtpMasterKeyIdentifier: c.config.Bugs.SRTPMasterKeyIdentifer,
+ customExtension: c.config.Bugs.CustomExtension,
+ omitExtensions: c.config.Bugs.OmitExtensions,
+ emptyExtensions: c.config.Bugs.EmptyExtensions,
+ delegatedCredentials: !c.config.Bugs.DisableDelegatedCredentials,
+ }
+
+ // Translate the bugs that modify ClientHello extension order into a
+ // list of prefix extensions. The marshal function will try these
+ // extensions before any others, followed by any remaining extensions in
+ // the default order.
+ if innerHello != nil && c.config.Bugs.FirstExtensionInClientHelloOuter != 0 {
+ hello.prefixExtensions = append(hello.prefixExtensions, c.config.Bugs.FirstExtensionInClientHelloOuter)
+ }
+ if c.config.Bugs.PSKBinderFirst && !c.config.Bugs.OnlyCorruptSecondPSKBinder {
+ hello.prefixExtensions = append(hello.prefixExtensions, extensionPreSharedKey)
+ }
+ if c.config.Bugs.SwapNPNAndALPN {
+ hello.prefixExtensions = append(hello.prefixExtensions, extensionALPN)
+ hello.prefixExtensions = append(hello.prefixExtensions, extensionNextProtoNeg)
+ }
+ if isInner && len(c.config.ECHOuterExtensions) > 0 && !c.config.Bugs.OnlyCompressSecondClientHelloInner {
+ applyECHOuterExtensions(hello, c.config.ECHOuterExtensions)
+ }
+
+ if maxVersion >= VersionTLS13 {
+ hello.vers = mapClientHelloVersion(VersionTLS12, c.isDTLS)
+ if !c.config.Bugs.OmitSupportedVersions {
+ hello.supportedVersions = c.config.supportedVersions(c.isDTLS, requireTLS13)
+ }
+ hello.pskKEModes = []byte{pskDHEKEMode}
+ } else {
+ hello.vers = mapClientHelloVersion(maxVersion, c.isDTLS)
+ }
+
+ if c.config.Bugs.SendClientVersion != 0 {
+ hello.vers = c.config.Bugs.SendClientVersion
+ }
+
+ if len(c.config.Bugs.SendSupportedVersions) > 0 {
+ hello.supportedVersions = c.config.Bugs.SendSupportedVersions
+ }
+
+ if innerHello != nil {
+ hello.serverName = c.config.ClientECHConfig.PublicName
+ } else {
+ hello.serverName = c.config.ServerName
+ }
+
+ disableEMS := c.config.Bugs.NoExtendedMasterSecret
+ if c.cipherSuite != nil {
+ disableEMS = c.config.Bugs.NoExtendedMasterSecretOnRenegotiation
+ }
+
+ if disableEMS {
+ hello.extendedMasterSecret = false
+ }
+
+ if c.config.Bugs.NoSupportedCurves {
+ hello.supportedCurves = nil
+ }
+
+ if c.config.Bugs.SendPSKKeyExchangeModes != nil {
+ hello.pskKEModes = c.config.Bugs.SendPSKKeyExchangeModes
+ }
+
+ if c.config.Bugs.SendCompressionMethods != nil {
+ hello.compressionMethods = c.config.Bugs.SendCompressionMethods
+ }
+
+ if c.config.Bugs.SendSupportedPointFormats != nil {
+ hello.supportedPoints = c.config.Bugs.SendSupportedPointFormats
+ }
+
+ if len(c.clientVerify) > 0 && !c.config.Bugs.EmptyRenegotiationInfo {
+ if c.config.Bugs.BadRenegotiationInfo {
+ hello.secureRenegotiation = append(hello.secureRenegotiation, c.clientVerify...)
+ hello.secureRenegotiation[0] ^= 0x80
+ } else {
+ hello.secureRenegotiation = c.clientVerify
+ }
+ }
+
+ if c.config.Bugs.DuplicateCompressedCertAlgs {
+ hello.compressedCertAlgs = []uint16{1, 1}
+ } else if len(c.config.CertCompressionAlgs) > 0 {
+ hello.compressedCertAlgs = make([]uint16, 0, len(c.config.CertCompressionAlgs))
+ for id := range c.config.CertCompressionAlgs {
+ hello.compressedCertAlgs = append(hello.compressedCertAlgs, uint16(id))
+ }
+ }
+
+ if c.noRenegotiationInfo() {
+ hello.secureRenegotiation = nil
+ }
+
+ for protocol := range c.config.ApplicationSettings {
+ hello.alpsProtocols = append(hello.alpsProtocols, protocol)
+ }
+
+ if maxVersion >= VersionTLS13 {
+ // Use the same key shares between ClientHelloInner and ClientHelloOuter.
+ if innerHello != nil {
+ hello.hasKeyShares = innerHello.hasKeyShares
+ hello.keyShares = innerHello.keyShares
+ } else {
+ hello.hasKeyShares = true
+ hello.trailingKeyShareData = c.config.Bugs.TrailingKeyShareData
+ curvesToSend := c.config.defaultCurves()
+ for _, curveID := range hello.supportedCurves {
+ if !curvesToSend[curveID] {
+ continue
+ }
+ curve, ok := curveForCurveID(curveID, c.config)
+ if !ok {
+ continue
+ }
+ publicKey, err := curve.offer(c.config.rand())
+ if err != nil {
+ return nil, err
+ }
+
+ if c.config.Bugs.SendCurve != 0 {
+ curveID = c.config.Bugs.SendCurve
+ }
+ if c.config.Bugs.InvalidECDHPoint {
+ publicKey[0] ^= 0xff
+ }
+
+ hello.keyShares = append(hello.keyShares, keyShareEntry{
+ group: curveID,
+ keyExchange: publicKey,
+ })
+ hs.keyShares[curveID] = curve
+
+ if c.config.Bugs.DuplicateKeyShares {
+ hello.keyShares = append(hello.keyShares, hello.keyShares[len(hello.keyShares)-1])
+ }
+ }
+
+ if c.config.Bugs.MissingKeyShare {
+ hello.hasKeyShares = false
+ }
+ }
+ }
+
+ possibleCipherSuites := c.config.cipherSuites()
+ hello.cipherSuites = make([]uint16, 0, len(possibleCipherSuites))
+
+NextCipherSuite:
+ for _, suiteID := range possibleCipherSuites {
+ for _, suite := range cipherSuites {
+ if suite.id != suiteID {
+ continue
+ }
+ // Don't advertise TLS 1.2-only cipher suites unless
+ // we're attempting TLS 1.2.
+ if maxVersion < VersionTLS12 && suite.flags&suiteTLS12 != 0 {
+ continue
+ }
+ hello.cipherSuites = append(hello.cipherSuites, suiteID)
+ continue NextCipherSuite
+ }
+ }
+
+ if c.config.Bugs.AdvertiseAllConfiguredCiphers {
+ hello.cipherSuites = possibleCipherSuites
+ }
+
+ if c.config.Bugs.SendRenegotiationSCSV {
+ hello.cipherSuites = append(hello.cipherSuites, renegotiationSCSV)
+ }
+
+ if c.config.Bugs.SendFallbackSCSV {
+ hello.cipherSuites = append(hello.cipherSuites, fallbackSCSV)
+ }
+
+ _, err := io.ReadFull(c.config.rand(), hello.random)
+ if err != nil {
+ c.sendAlert(alertInternalError)
+ return nil, errors.New("tls: short read from Rand: " + err.Error())
+ }
+
+ if maxVersion >= VersionTLS12 && !c.config.Bugs.NoSignatureAlgorithms {
+ hello.signatureAlgorithms = c.config.verifySignatureAlgorithms()
+ }
+
+ if c.config.ClientSessionCache != nil {
+ hello.ticketSupported = !c.config.SessionTicketsDisabled
+ }
+
+ session := hs.session
+
+ // ClientHelloOuter cannot offer sessions.
+ if innerHello != nil && !c.config.Bugs.OfferSessionInClientHelloOuter {
+ session = nil
+ }
+
+ if session != nil && c.config.time().Before(session.ticketExpiration) {
+ ticket := session.sessionTicket
+ if c.config.Bugs.FilterTicket != nil && len(ticket) > 0 {
+ // Copy the ticket so FilterTicket may act in-place.
+ ticket = make([]byte, len(session.sessionTicket))
+ copy(ticket, session.sessionTicket)
+
+ ticket, err = c.config.Bugs.FilterTicket(ticket)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if session.vers >= VersionTLS13 || c.config.Bugs.SendBothTickets {
+ // TODO(nharper): Support sending more
+ // than one PSK identity.
+ ticketAge := uint32(c.config.time().Sub(session.ticketCreationTime) / time.Millisecond)
+ if c.config.Bugs.SendTicketAge != 0 {
+ ticketAge = uint32(c.config.Bugs.SendTicketAge / time.Millisecond)
+ }
+ psk := pskIdentity{
+ ticket: ticket,
+ obfuscatedTicketAge: session.ticketAgeAdd + ticketAge,
+ }
+ hello.pskIdentities = []pskIdentity{psk}
+
+ if c.config.Bugs.ExtraPSKIdentity {
+ hello.pskIdentities = append(hello.pskIdentities, psk)
+ }
+ }
+
+ if session.vers < VersionTLS13 || c.config.Bugs.SendBothTickets {
+ if ticket != nil {
+ hello.sessionTicket = ticket
+ // A random session ID is used to detect when the
+ // server accepted the ticket and is resuming a session
+ // (see RFC 5077).
+ sessionIDLen := 16
+ if c.config.Bugs.TicketSessionIDLength != 0 {
+ sessionIDLen = c.config.Bugs.TicketSessionIDLength
+ }
+ if c.config.Bugs.EmptyTicketSessionID {
+ sessionIDLen = 0
+ }
+ hello.sessionID = make([]byte, sessionIDLen)
+ if _, err := io.ReadFull(c.config.rand(), hello.sessionID); err != nil {
+ c.sendAlert(alertInternalError)
+ return nil, errors.New("tls: short read from Rand: " + err.Error())
+ }
+ } else {
+ hello.sessionID = session.sessionID
+ }
+ }
+ }
+
+ if innerHello == nil {
+ // Request compatibility mode from the client by sending a fake session
+ // ID. Although BoringSSL always enables compatibility mode, other
+ // implementations make it conditional on the ClientHello. We test
+ // BoringSSL's expected behavior with SendClientHelloSessionID.
+ if len(hello.sessionID) == 0 && maxVersion >= VersionTLS13 {
+ hello.sessionID = make([]byte, 32)
+ if _, err := io.ReadFull(c.config.rand(), hello.sessionID); err != nil {
+ c.sendAlert(alertInternalError)
+ return nil, errors.New("tls: short read from Rand: " + err.Error())
+ }
+ }
+ if c.config.Bugs.MockQUICTransport != nil && !c.config.Bugs.CompatModeWithQUIC {
+ hello.sessionID = []byte{}
+ }
+ if c.config.Bugs.SendClientHelloSessionID != nil {
+ hello.sessionID = c.config.Bugs.SendClientHelloSessionID
+ }
+ } else {
+ // ClientHelloOuter's session ID is copied from ClientHelloINnner.
+ hello.sessionID = innerHello.sessionID
+ }
+
+ if c.config.Bugs.SendCipherSuites != nil {
+ hello.cipherSuites = c.config.Bugs.SendCipherSuites
+ }
+
+ if innerHello == nil {
+ if len(hello.pskIdentities) > 0 && c.config.Bugs.SendEarlyData != nil {
+ hello.hasEarlyData = true
+ }
+ if c.config.Bugs.SendFakeEarlyDataLength > 0 {
+ hello.hasEarlyData = true
+ }
+ if c.config.Bugs.OmitEarlyDataExtension {
+ hello.hasEarlyData = false
+ }
+ } else {
+ hello.hasEarlyData = innerHello.hasEarlyData
+ }
+
+ if (isInner && !c.config.Bugs.OmitECHIsInner) || c.config.Bugs.AlwaysSendECHIsInner {
+ hello.echIsInner = []byte{}
+ if len(c.config.Bugs.SendInvalidECHIsInner) != 0 {
+ hello.echIsInner = c.config.Bugs.SendInvalidECHIsInner
+ }
+ }
+
+ if innerHello != nil {
+ hash, err := hpke.GetHKDFHash(hs.echHPKEContext.KDF())
+ if err != nil {
+ return nil, err
+ }
+ if err := hs.encryptClientHello(hello, innerHello, c.config.ClientECHConfig.configID(hash), echEnc); err != nil {
+ return nil, err
+ }
+ if c.config.Bugs.CorruptEncryptedClientHello {
+ hello.clientECH.payload[0] ^= 1
+ }
+ }
+
+ // PSK binders and ECH both must be computed last because they incorporate
+ // the rest of the ClientHello and conflict. ECH resolves this by forbidding
+ // clients from offering PSKs on ClientHelloOuter, but we still need to test
+ // servers handle it correctly so they tolerate GREASE. In other cases, we
+ // expect the server to reject ECH, so we put PSK last. Note this renders
+ // ECH undecryptable.
+ if len(hello.pskIdentities) > 0 {
+ version := session.wireVersion
+ // We may have a pre-1.3 session if SendBothTickets is set.
+ if session.vers < VersionTLS13 {
+ version = VersionTLS13
+ }
+ generatePSKBinders(version, hello, session, nil, nil, c.config)
+ }
+
+ if c.config.Bugs.SendClientHelloWithFixes != nil {
+ hello, err = replaceClientHello(hello, c.config.Bugs.SendClientHelloWithFixes)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return hello, nil
+}
+
+// encryptClientHello encrypts |innerHello| using the specified HPKE context and
+// adds the extension to |hello|.
+func (hs *clientHandshakeState) encryptClientHello(hello, innerHello *clientHelloMsg, configID, enc []byte) error {
+ c := hs.c
+
+ if c.config.Bugs.MinimalClientHelloOuter {
+ *hello = clientHelloMsg{
+ vers: VersionTLS12,
+ random: hello.random,
+ sessionID: hello.sessionID,
+ cipherSuites: []uint16{0x0a0a},
+ compressionMethods: hello.compressionMethods,
+ }
+ }
+
+ if c.config.Bugs.TruncateClientECHEnc {
+ enc = enc[:1]
+ }
+
+ aad := newByteBuilder()
+ aad.addU16(hs.echHPKEContext.KDF())
+ aad.addU16(hs.echHPKEContext.AEAD())
+ aad.addU8LengthPrefixed().addBytes(configID)
+ aad.addU16LengthPrefixed().addBytes(enc)
+ hello.marshalForOuterAAD(aad.addU24LengthPrefixed())
+
+ payload := hs.echHPKEContext.Seal(innerHello.marshalForEncodedInner(), aad.finish())
+
+ // Place the ECH extension in the outer CH.
+ hello.clientECH = &clientECH{
+ hpkeKDF: hs.echHPKEContext.KDF(),
+ hpkeAEAD: hs.echHPKEContext.AEAD(),
+ configID: configID,
+ enc: enc,
+ payload: payload,
+ }
+
+ return nil
+}
+
func (hs *clientHandshakeState) doTLS13Handshake(msg interface{}) error {
c := hs.c
@@ -735,6 +910,10 @@
if haveHelloRetryRequest {
hs.finishedHash.UpdateForHelloRetryRequest()
hs.writeServerHash(helloRetryRequest.marshal())
+ if hs.innerHello != nil {
+ hs.innerFinishedHash.UpdateForHelloRetryRequest()
+ hs.innerFinishedHash.WriteHandshake(helloRetryRequest.marshal(), c.recvHandshakeSeq-1)
+ }
if c.config.Bugs.FailIfHelloRetryRequested {
return errors.New("tls: unexpected HelloRetryRequest")
@@ -748,60 +927,18 @@
// Reset the encryption state, in case we sent 0-RTT data.
c.out.resetCipher()
- firstHelloBytes := hs.hello.marshal()
- if len(helloRetryRequest.cookie) > 0 {
- hs.hello.tls13Cookie = helloRetryRequest.cookie
- }
-
- if c.config.Bugs.MisinterpretHelloRetryRequestCurve != 0 {
- helloRetryRequest.hasSelectedGroup = true
- helloRetryRequest.selectedGroup = c.config.Bugs.MisinterpretHelloRetryRequestCurve
- }
- if helloRetryRequest.hasSelectedGroup {
- var hrrCurveFound bool
- group := helloRetryRequest.selectedGroup
- for _, curveID := range hs.hello.supportedCurves {
- if group == curveID {
- hrrCurveFound = true
- break
- }
- }
- if !hrrCurveFound || hs.keyShares[group] != nil {
- c.sendAlert(alertHandshakeFailure)
- return errors.New("tls: received invalid HelloRetryRequest")
- }
- curve, ok := curveForCurveID(group, c.config)
- if !ok {
- return errors.New("tls: Unable to get curve requested in HelloRetryRequest")
- }
- publicKey, err := curve.offer(c.config.rand())
- if err != nil {
+ if hs.innerHello != nil {
+ if err := hs.applyHelloRetryRequest(helloRetryRequest, hs.innerHello, nil); err != nil {
return err
}
- hs.keyShares[group] = curve
- hs.hello.keyShares = []keyShareEntry{{
- group: group,
- keyExchange: publicKey,
- }}
- }
-
- if c.config.Bugs.SecondClientHelloMissingKeyShare {
- hs.hello.hasKeyShares = false
- }
-
- hs.hello.hasEarlyData = c.config.Bugs.SendEarlyDataOnSecondClientHello
- // The first ClientHello may have skipped this due to OnlyCorruptSecondPSKBinder.
- if c.config.Bugs.PSKBinderFirst && c.config.Bugs.OnlyCorruptSecondPSKBinder {
- hs.hello.prefixExtensions = append(hs.hello.prefixExtensions, extensionPreSharedKey)
- }
- if c.config.Bugs.OmitPSKsOnSecondClientHello {
- hs.hello.pskIdentities = nil
- hs.hello.pskBinders = nil
- }
- hs.hello.raw = nil
-
- if len(hs.hello.pskIdentities) > 0 {
- generatePSKBinders(c.wireVersion, hs.hello, hs.session, firstHelloBytes, helloRetryRequest.marshal(), c.config)
+ if err := hs.applyHelloRetryRequest(helloRetryRequest, hs.hello, hs.innerHello); err != nil {
+ return err
+ }
+ hs.innerFinishedHash.WriteHandshake(hs.innerHello.marshal(), c.sendHandshakeSeq)
+ } else {
+ if err := hs.applyHelloRetryRequest(helloRetryRequest, hs.hello, nil); err != nil {
+ return err
+ }
}
hs.writeClientHash(hs.hello.marshal())
toWrite := hs.hello.marshal()
@@ -862,9 +999,9 @@
return errors.New("tls: session IDs did not match.")
}
- zeroSecret := hs.finishedHash.zeroSecret()
-
// Resolve PSK and compute the early secret.
+ zeroSecret := hs.finishedHash.zeroSecret()
+ pskSecret := zeroSecret
if hs.serverHello.hasPSKIdentity {
// We send at most one PSK identity.
if hs.session == nil || hs.serverHello.pskIdentity != 0 {
@@ -875,10 +1012,12 @@
c.sendAlert(alertHandshakeFailure)
return errors.New("tls: server resumed an invalid session for the cipher suite")
}
- hs.finishedHash.addEntropy(hs.session.secret)
+ pskSecret = hs.session.secret
c.didResume = true
- } else {
- hs.finishedHash.addEntropy(zeroSecret)
+ }
+ hs.finishedHash.addEntropy(pskSecret)
+ if hs.innerHello != nil {
+ hs.innerFinishedHash.addEntropy(pskSecret)
}
if !hs.serverHello.hasKeyShare {
@@ -887,6 +1026,7 @@
}
// Resolve ECDHE and compute the handshake secret.
+ ecdheSecret := zeroSecret
if !c.config.Bugs.MissingKeyShare && !c.config.Bugs.SecondClientHelloMissingKeyShare {
curve, ok := hs.keyShares[hs.serverHello.keyShare.group]
if !ok {
@@ -895,28 +1035,45 @@
}
c.curveID = hs.serverHello.keyShare.group
- ecdheSecret, err := curve.finish(hs.serverHello.keyShare.keyExchange)
+ var err error
+ ecdheSecret, err = curve.finish(hs.serverHello.keyShare.keyExchange)
if err != nil {
return err
}
- hs.finishedHash.nextSecret()
- hs.finishedHash.addEntropy(ecdheSecret)
+ }
+ hs.finishedHash.nextSecret()
+ hs.finishedHash.addEntropy(ecdheSecret)
+ if hs.innerHello != nil {
+ hs.innerFinishedHash.nextSecret()
+ hs.innerFinishedHash.addEntropy(ecdheSecret)
+ }
+
+ // Determine whether the server accepted ECH.
+ confirmHash := &hs.finishedHash
+ if hs.innerHello != nil {
+ confirmHash = &hs.innerFinishedHash
+ }
+ echConfirmed := bytes.Equal(hs.serverHello.random[24:], confirmHash.deriveSecretPeek([]byte("ech accept confirmation"), hs.serverHello.marshalForECHConf())[:8])
+ if hs.innerHello != nil {
+ c.echAccepted = echConfirmed
+ if c.echAccepted {
+ hs.hello = hs.innerHello
+ hs.finishedHash = hs.innerFinishedHash
+ }
+ hs.innerHello = nil
+ hs.innerFinishedHash = finishedHash{}
} else {
- hs.finishedHash.nextSecret()
- 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")
+ // When not offering ECH, we may still expect a confirmation signal to
+ // test the backend server behavior.
+ if hs.hello.echIsInner != nil {
+ if !echConfirmed {
+ return errors.New("tls: server did not send ECH confirmation when requested")
+ }
+ } else {
+ if echConfirmed {
+ return errors.New("tls: server did sent ECH confirmation when not requested")
+ }
+ }
}
hs.writeServerHash(hs.serverHello.marshal())
@@ -941,6 +1098,10 @@
}
hs.writeServerHash(encryptedExtensions.marshal())
+ if !bytes.Equal(encryptedExtensions.extensions.echRetryConfigs, c.config.Bugs.ExpectECHRetryConfigs) {
+ return errors.New("tls: server sent ECH retry_configs with unexpected contents")
+ }
+
err = hs.processServerExtensions(&encryptedExtensions.extensions)
if err != nil {
return err
@@ -1276,6 +1437,108 @@
return nil
}
+// applyHelloRetryRequest updates |hello| in-place based on |helloRetryRequest|.
+// If |innerHello| is not nil, this is the second ClientHelloOuter and should
+// contain an encrypted copy of |innerHello|
+func (hs *clientHandshakeState) applyHelloRetryRequest(helloRetryRequest *helloRetryRequestMsg, hello, innerHello *clientHelloMsg) error {
+ c := hs.c
+ firstHelloBytes := hello.marshal()
+ isInner := innerHello == nil && hs.echHPKEContext != nil
+ if len(helloRetryRequest.cookie) > 0 {
+ hello.tls13Cookie = helloRetryRequest.cookie
+ }
+
+ if innerHello != nil {
+ hello.keyShares = innerHello.keyShares
+ } else {
+ if c.config.Bugs.MisinterpretHelloRetryRequestCurve != 0 {
+ helloRetryRequest.hasSelectedGroup = true
+ helloRetryRequest.selectedGroup = c.config.Bugs.MisinterpretHelloRetryRequestCurve
+ }
+ if helloRetryRequest.hasSelectedGroup {
+ var hrrCurveFound bool
+ group := helloRetryRequest.selectedGroup
+ for _, curveID := range hello.supportedCurves {
+ if group == curveID {
+ hrrCurveFound = true
+ break
+ }
+ }
+ if !hrrCurveFound || hs.keyShares[group] != nil {
+ c.sendAlert(alertHandshakeFailure)
+ return errors.New("tls: received invalid HelloRetryRequest")
+ }
+ curve, ok := curveForCurveID(group, c.config)
+ if !ok {
+ return errors.New("tls: Unable to get curve requested in HelloRetryRequest")
+ }
+ publicKey, err := curve.offer(c.config.rand())
+ if err != nil {
+ return err
+ }
+ hs.keyShares[group] = curve
+ hello.keyShares = []keyShareEntry{{
+ group: group,
+ keyExchange: publicKey,
+ }}
+ }
+
+ if c.config.Bugs.SecondClientHelloMissingKeyShare {
+ hello.hasKeyShares = false
+ }
+ }
+
+ if isInner && c.config.Bugs.OmitSecondECHIsInner {
+ hello.echIsInner = nil
+ }
+
+ hello.hasEarlyData = c.config.Bugs.SendEarlyDataOnSecondClientHello
+ // The first ClientHello may have skipped this due to OnlyCorruptSecondPSKBinder.
+ if c.config.Bugs.PSKBinderFirst && c.config.Bugs.OnlyCorruptSecondPSKBinder {
+ hello.prefixExtensions = append(hello.prefixExtensions, extensionPreSharedKey)
+ }
+ // The first ClientHello may have skipped this due to OnlyCompressSecondClientHelloInner.
+ if isInner && len(c.config.ECHOuterExtensions) > 0 && c.config.Bugs.OnlyCompressSecondClientHelloInner {
+ applyECHOuterExtensions(hello, c.config.ECHOuterExtensions)
+ }
+ if c.config.Bugs.OmitPSKsOnSecondClientHello {
+ hello.pskIdentities = nil
+ hello.pskBinders = nil
+ }
+ hello.raw = nil
+
+ if innerHello != nil {
+ if c.config.Bugs.OmitSecondEncryptedClientHello {
+ hello.clientECH = nil
+ } else {
+ if err := hs.encryptClientHello(hello, innerHello, nil, nil); err != nil {
+ return err
+ }
+ if c.config.Bugs.CorruptSecondEncryptedClientHello {
+ hello.clientECH.payload[0] ^= 1
+ }
+ }
+ }
+
+ // PSK binders and ECH both must be inserted last because they incorporate
+ // the rest of the ClientHello and conflict. See corresponding comment in
+ // |createClientHello|.
+ if len(hello.pskIdentities) > 0 {
+ generatePSKBinders(c.wireVersion, hello, hs.session, firstHelloBytes, helloRetryRequest.marshal(), c.config)
+ }
+ return nil
+}
+
+// applyECHOuterExtensions updates |hello| to compress |outerExtensions| with
+// the ech_outer_extensions mechanism.
+func applyECHOuterExtensions(hello *clientHelloMsg, outerExtensions []uint16) {
+ // Ensure that the ech_outer_extensions extension and each of the
+ // extensions it names are serialized consecutively.
+ hello.prefixExtensions = append(hello.prefixExtensions, extensionECHOuterExtensions)
+ hello.prefixExtensions = append(hello.prefixExtensions, outerExtensions...)
+ hello.outerExtensions = outerExtensions
+}
+
func (hs *clientHandshakeState) doFullHandshake() error {
c := hs.c
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 158f7c7..aee47a2 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -5,8 +5,11 @@
package runner
import (
+ "crypto"
"encoding/binary"
"fmt"
+
+ "golang.org/x/crypto/hkdf"
)
func writeLen(buf []byte, v, size int) {
@@ -262,8 +265,7 @@
MaxNameLen uint16
}
-func MarshalECHConfig(e *ECHConfig) []byte {
- bb := newByteBuilder()
+func (e *ECHConfig) marshal(bb *byteBuilder) {
// ECHConfig's wire format reuses the encrypted_client_hello extension
// codepoint as a version identifier.
bb.addU16(extensionEncryptedClientHello)
@@ -278,9 +280,33 @@
}
contents.addU16(e.MaxNameLen)
contents.addU16(0) // Empty extensions field
+}
+
+func MarshalECHConfig(e *ECHConfig) []byte {
+ bb := newByteBuilder()
+ e.marshal(bb)
return bb.finish()
}
+func MarshalECHConfigList(configs ...*ECHConfig) []byte {
+ bb := newByteBuilder()
+ list := bb.addU16LengthPrefixed()
+ for _, config := range configs {
+ config.marshal(list)
+ }
+ return bb.finish()
+}
+
+func (e *ECHConfig) configID(h crypto.Hash) []byte {
+ configIDLength := 8
+ idReader := hkdf.Expand(h.New, hkdf.Extract(h.New, MarshalECHConfig(e), nil), []byte("tls ech config id"))
+ idBytes := make([]byte, configIDLength)
+ if n, err := idReader.Read(idBytes); err != nil || n != configIDLength {
+ panic("failed to compute configID for ECHConfig")
+ }
+ return idBytes
+}
+
// The contents of a CH "encrypted_client_hello" extension.
// https://tools.ietf.org/html/draft-ietf-tls-esni-09
type clientECH struct {
@@ -343,6 +369,7 @@
compressedCertAlgs []uint16
delegatedCredentials bool
alpsProtocols []string
+ outerExtensions []uint16
prefixExtensions []uint16
}
@@ -358,34 +385,21 @@
}
}
-func (m *clientHelloMsg) marshal() []byte {
- if m.raw != nil {
- return m.raw
- }
+type clientHelloType int
- if m.isV2ClientHello {
- v2Msg := newByteBuilder()
- v2Msg.addU8(1)
- v2Msg.addU16(m.vers)
- v2Msg.addU16(uint16(len(m.cipherSuites) * 3))
- v2Msg.addU16(uint16(len(m.sessionID)))
- v2Msg.addU16(uint16(len(m.v2Challenge)))
- for _, spec := range m.cipherSuites {
- v2Msg.addU24(int(spec))
- }
- v2Msg.addBytes(m.sessionID)
- v2Msg.addBytes(m.v2Challenge)
- m.raw = v2Msg.finish()
- return m.raw
- }
+const (
+ clientHelloNormal clientHelloType = iota
+ clientHelloOuterAAD
+ clientHelloEncodedInner
+)
- handshakeMsg := newByteBuilder()
- handshakeMsg.addU8(typeClientHello)
- hello := handshakeMsg.addU24LengthPrefixed()
+func (m *clientHelloMsg) marshalBody(hello *byteBuilder, typ clientHelloType) {
hello.addU16(m.vers)
hello.addBytes(m.random)
sessionID := hello.addU8LengthPrefixed()
- sessionID.addBytes(m.sessionID)
+ if typ != clientHelloEncodedInner {
+ sessionID.addBytes(m.sessionID)
+ }
if m.isDTLS {
cookie := hello.addU8LengthPrefixed()
cookie.addBytes(m.cookie)
@@ -441,7 +455,7 @@
body: serverNameList.finish(),
})
}
- if m.clientECH != nil {
+ if m.clientECH != nil && typ != clientHelloOuterAAD {
body := newByteBuilder()
body.addU16(m.clientECH.hpkeKDF)
body.addU16(m.clientECH.hpkeAEAD)
@@ -460,6 +474,17 @@
body: m.echIsInner,
})
}
+ if m.outerExtensions != nil && typ == clientHelloEncodedInner {
+ body := newByteBuilder()
+ extensionsList := body.addU8LengthPrefixed()
+ for _, extID := range m.outerExtensions {
+ extensionsList.addU16(extID)
+ }
+ extensions = append(extensions, extension{
+ id: extensionECHOuterExtensions,
+ body: body.finish(),
+ })
+ }
if m.ocspStapling {
certificateStatusRequest := newByteBuilder()
// RFC 4366, section 3.6
@@ -700,6 +725,12 @@
for _, ext := range extensions {
extMap[ext.id] = ext.body
}
+ // Elide each of the extensions named by |m.outerExtensions|.
+ if m.outerExtensions != nil && typ == clientHelloEncodedInner {
+ for _, extID := range m.outerExtensions {
+ delete(extMap, extID)
+ }
+ }
// Write each of the prefix extensions, if we have it.
for _, extID := range m.prefixExtensions {
if body, ok := extMap[extID]; ok {
@@ -738,7 +769,43 @@
hello.addU16(0)
}
}
+}
+func (m *clientHelloMsg) marshalForOuterAAD(bb *byteBuilder) {
+ m.marshalBody(bb, clientHelloOuterAAD)
+}
+
+func (m *clientHelloMsg) marshalForEncodedInner() []byte {
+ hello := newByteBuilder()
+ m.marshalBody(hello, clientHelloEncodedInner)
+ return hello.finish()
+}
+
+func (m *clientHelloMsg) marshal() []byte {
+ if m.raw != nil {
+ return m.raw
+ }
+
+ if m.isV2ClientHello {
+ v2Msg := newByteBuilder()
+ v2Msg.addU8(1)
+ v2Msg.addU16(m.vers)
+ v2Msg.addU16(uint16(len(m.cipherSuites) * 3))
+ v2Msg.addU16(uint16(len(m.sessionID)))
+ v2Msg.addU16(uint16(len(m.v2Challenge)))
+ for _, spec := range m.cipherSuites {
+ v2Msg.addU24(int(spec))
+ }
+ v2Msg.addBytes(m.sessionID)
+ v2Msg.addBytes(m.v2Challenge)
+ m.raw = v2Msg.finish()
+ return m.raw
+ }
+
+ handshakeMsg := newByteBuilder()
+ handshakeMsg.addU8(typeClientHello)
+ hello := handshakeMsg.addU24LengthPrefixed()
+ m.marshalBody(hello, clientHelloNormal)
m.raw = handshakeMsg.finish()
// Sanity-check padding.
if m.pad != 0 && (len(m.raw)-4)%m.pad != 0 {
@@ -1529,9 +1596,7 @@
}
if len(m.echRetryConfigs) > 0 {
extensions.addU16(extensionEncryptedClientHello)
- body := extensions.addU16LengthPrefixed()
- echConfigs := body.addU16LengthPrefixed()
- echConfigs.addBytes(m.echRetryConfigs)
+ extensions.addU16LengthPrefixed().addBytes(m.echRetryConfigs)
}
}
@@ -1647,21 +1712,23 @@
m.hasApplicationSettings = true
m.applicationSettings = body
case extensionEncryptedClientHello:
+ if version < VersionTLS13 {
+ return false
+ }
+ m.echRetryConfigs = body
+
+ // Validate the ECHConfig with a top-level parse.
var echConfigs byteReader
if !body.readU16LengthPrefixed(&echConfigs) {
return false
}
for len(echConfigs) > 0 {
- // Validate the ECHConfig with a top-level parse.
- echConfigReader := echConfigs
var version uint16
var contents byteReader
- if !echConfigReader.readU16(&version) ||
- !echConfigReader.readU16LengthPrefixed(&contents) {
+ if !echConfigs.readU16(&version) ||
+ !echConfigs.readU16LengthPrefixed(&contents) {
return false
}
-
- m.echRetryConfigs = contents
}
if len(body) > 0 {
return false
diff --git a/ssl/test/runner/hpke/hpke.go b/ssl/test/runner/hpke/hpke.go
index 71cf1e7..548c98e 100644
--- a/ssl/test/runner/hpke/hpke.go
+++ b/ssl/test/runner/hpke/hpke.go
@@ -18,10 +18,12 @@
package hpke
import (
+ "crypto"
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"errors"
+ "fmt"
"golang.org/x/crypto/chacha20poly1305"
)
@@ -51,6 +53,20 @@
hpkeModePSK uint8 = 1
)
+// GetHKDFHash returns the crypto.Hash that corresponds to kdf. If kdf is not
+// one the supported KDF IDs, returns an error.
+func GetHKDFHash(kdf uint16) (crypto.Hash, error) {
+ switch kdf {
+ case HKDFSHA256:
+ return crypto.SHA256, nil
+ case HKDFSHA384:
+ return crypto.SHA384, nil
+ case HKDFSHA512:
+ return crypto.SHA512, nil
+ }
+ return 0, fmt.Errorf("unknown KDF: %d", kdf)
+}
+
type GenerateKeyPairFunc func() (public []byte, secret []byte, e error)
// Context holds the HPKE state for a sender or a receiver.
@@ -113,6 +129,12 @@
return context, nil
}
+func (c *Context) KEM() uint16 { return c.kemID }
+
+func (c *Context) KDF() uint16 { return c.kdfID }
+
+func (c *Context) AEAD() uint16 { return c.aeadID }
+
func (c *Context) Seal(plaintext, additionalData []byte) []byte {
ciphertext := c.aead.Seal(nil, c.computeNonce(), plaintext, additionalData)
c.incrementSeq()
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 0e250c2..3c8c2b2 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -554,6 +554,8 @@
// peerApplicationSettings are the expected application settings for the
// connection. If nil, no application settings are expected.
peerApplicationSettings []byte
+ // echAccepted is whether ECH should have been accepted on this connection.
+ echAccepted bool
}
type testCase struct {
@@ -969,6 +971,16 @@
}
}
+ if expectations.echAccepted {
+ if !connState.ECHAccepted {
+ return errors.New("tls: server did not accept ECH")
+ }
+ } else {
+ if connState.ECHAccepted {
+ return errors.New("tls: server unexpectedly accepted ECH")
+ }
+ }
+
if test.exportKeyingMaterial > 0 {
actual := make([]byte, test.exportKeyingMaterial)
if _, err := io.ReadFull(tlsConn, actual); err != nil {
@@ -16440,203 +16452,795 @@
})
}
+type echCipher struct {
+ name string
+ cipher HPKECipherSuite
+}
+
+var echCiphers = []echCipher{
+ {
+ name: "HKDF-SHA256-AES-128-GCM",
+ cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES128GCM},
+ },
+ {
+ name: "HKDF-SHA256-AES-256-GCM",
+ cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES256GCM},
+ }, {
+ name: "HKDF-SHA256-ChaCha20-Poly1305",
+ cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.ChaCha20Poly1305},
+ },
+}
+
+// generateECHConfigWithSecretKey constructs a valid ECHConfig and corresponding
+// private key for the server. If the cipher list is empty, all ciphers are
+// included.
+func generateECHConfigWithSecretKey(publicName string, ciphers []HPKECipherSuite) (*ECHConfig, []byte, error) {
+ publicKeyR, secretKeyR, err := hpke.GenerateKeyPair()
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(ciphers) == 0 {
+ ciphers = make([]HPKECipherSuite, 0, len(echCiphers))
+ for _, cipher := range echCiphers {
+ ciphers = append(ciphers, cipher.cipher)
+ }
+ }
+ result := ECHConfig{
+ PublicName: publicName,
+ PublicKey: publicKeyR,
+ KEM: hpke.X25519WithHKDFSHA256,
+ CipherSuites: ciphers,
+ // For real-life purposes, the maxNameLen should be
+ // based on the set of domain names that the server
+ // represents.
+ MaxNameLen: 16,
+ }
+ return &result, secretKeyR, nil
+}
+
func addEncryptedClientHelloTests() {
- // Test ECH GREASE.
-
- // Test the client's behavior when the server ignores ECH GREASE.
- testCases = append(testCases, testCase{
- testType: clientTest,
- name: "ECH-GREASE-Client-TLS13",
- config: Config{
- MinVersion: VersionTLS13,
- MaxVersion: VersionTLS13,
- Bugs: ProtocolBugs{
- ExpectClientECH: true,
- },
- },
- flags: []string{"-enable-ech-grease"},
- })
-
- // Test the client's ECH GREASE behavior when responding to server's
- // HelloRetryRequest. This test implicitly checks that the first and second
- // ClientHello messages have identical ECH extensions.
- testCases = append(testCases, testCase{
- testType: clientTest,
- name: "ECH-GREASE-Client-TLS13-HelloRetryRequest",
- config: Config{
- MaxVersion: VersionTLS13,
- MinVersion: VersionTLS13,
- // P-384 requires a HelloRetryRequest against BoringSSL's default
- // configuration. Assert this with ExpectMissingKeyShare.
- CurvePreferences: []CurveID{CurveP384},
- Bugs: ProtocolBugs{
- ExpectMissingKeyShare: true,
- ExpectClientECH: true,
- },
- },
- flags: []string{"-enable-ech-grease", "-expect-hrr"},
- })
-
- retryConfigValid := ECHConfig{
- PublicName: "example.com",
- // A real X25519 public key obtained from hpke.GenerateKeyPair().
- PublicKey: []byte{
- 0x23, 0x1a, 0x96, 0x53, 0x52, 0x81, 0x1d, 0x7a,
- 0x36, 0x76, 0xaa, 0x5e, 0xad, 0xdb, 0x66, 0x1c,
- 0x92, 0x45, 0x8a, 0x60, 0xc7, 0x81, 0x93, 0xb0,
- 0x47, 0x7b, 0x54, 0x18, 0x6b, 0x9a, 0x1d, 0x6d},
- KEM: hpke.X25519WithHKDFSHA256,
- CipherSuites: []HPKECipherSuite{
- {
- KDF: hpke.HKDFSHA256,
- AEAD: hpke.AES256GCM,
- },
- },
- MaxNameLen: 42,
+ publicECHConfig, secretKey, err := generateECHConfigWithSecretKey("public.example", nil)
+ if err != nil {
+ panic(err)
+ }
+ publicECHConfig1, secretKey1, err := generateECHConfigWithSecretKey("public.example", nil)
+ if err != nil {
+ panic(err)
+ }
+ publicECHConfig2, secretKey2, err := generateECHConfigWithSecretKey("public.example", nil)
+ if err != nil {
+ panic(err)
+ }
+ publicECHConfig3, secretKey3, err := generateECHConfigWithSecretKey("public.example", nil)
+ if err != nil {
+ panic(err)
}
- retryConfigUnsupportedVersion := []byte{
- // version
- 0xba, 0xdd,
- // length
- 0x00, 0x05,
- // contents
- 0x05, 0x04, 0x03, 0x02, 0x01,
+ for _, protocol := range []protocol{tls, quic} {
+ prefix := protocol.String() + "-"
+
+ // There are two ClientHellos, so many of our tests have
+ // HelloRetryRequest variations.
+ for _, hrr := range []bool{false, true} {
+ var suffix string
+ var defaultCurves []CurveID
+ if hrr {
+ suffix = "-HelloRetryRequest"
+ // Require a HelloRetryRequest for every curve.
+ defaultCurves = []CurveID{}
+ }
+
+ // Test the server can accept ECH.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ DefaultCurves: defaultCurves,
+ },
+ resumeSession: true,
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test the server can accept ECH with a minimal ClientHelloOuter.
+ // This confirms that the server does not unexpectedly pick up
+ // fields from the wrong ClientHello.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-MinimalClientHelloOuter" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ DefaultCurves: defaultCurves,
+ Bugs: ProtocolBugs{
+ MinimalClientHelloOuter: true,
+ },
+ },
+ resumeSession: true,
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test that the server can decline ECH. In particular, it must send
+ // retry configs.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-Decline" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ // The client uses an ECHConfig that the server does not understand
+ // so we can observe which retry configs the server sends back.
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ OfferSessionInClientHelloOuter: true,
+ ExpectECHRetryConfigs: MarshalECHConfigList(publicECHConfig2, publicECHConfig3),
+ },
+ },
+ resumeSession: true,
+ flags: []string{
+ // Configure three ECHConfigs on the shim, only two of which
+ // should be sent in retry configs.
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig1)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey1),
+ "-ech-is-retry-config", "0",
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig2)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey2),
+ "-ech-is-retry-config", "1",
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig3)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey3),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "public.example",
+ },
+ })
+
+ // Test that the server considers a ClientHelloInner indicating TLS
+ // 1.2 to be a fatal error.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-TLS12InInner" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ AllowTLS12InClientHelloInner: true,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1"},
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":INVALID_CLIENT_HELLO_INNER:",
+ })
+
+ // When ech_is_inner extension is absent from the ClientHelloInner, the
+ // server should fail the connection.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-MissingECHIsInner" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ OmitECHIsInner: !hrr,
+ OmitSecondECHIsInner: hrr,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":INVALID_CLIENT_HELLO_INNER:",
+ })
+
+ // Test that the server can decode ech_outer_extensions.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-OuterExtensions" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ ClientECHConfig: publicECHConfig,
+ ECHOuterExtensions: []uint16{
+ extensionKeyShare,
+ extensionSupportedCurves,
+ // Include a custom extension, to test that unrecognized
+ // extensions are also decoded.
+ extensionCustom,
+ },
+ Bugs: ProtocolBugs{
+ CustomExtension: "test",
+ // Ensure ClientHelloOuter's extension order is different
+ // from ClientHelloInner. This tests that the server
+ // correctly reconstructs the extension order.
+ FirstExtensionInClientHelloOuter: extensionSupportedCurves,
+ OnlyCompressSecondClientHelloInner: hrr,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test that the server rejects duplicated values in ech_outer_extensions.
+ // Besides causing the server to reconstruct an invalid ClientHelloInner
+ // with duplicated extensions, this behavior would be vulnerable to DoS
+ // attacks.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-OuterExtensions-Duplicate" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ ClientECHConfig: publicECHConfig,
+ ECHOuterExtensions: []uint16{
+ extensionSupportedCurves,
+ extensionSupportedCurves,
+ },
+ Bugs: ProtocolBugs{
+ OnlyCompressSecondClientHelloInner: hrr,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":DUPLICATE_EXTENSION:",
+ })
+
+ // Test that the server rejects references to missing extensions in
+ // ech_outer_extensions.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-OuterExtensions-Missing" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ ClientECHConfig: publicECHConfig,
+ ECHOuterExtensions: []uint16{
+ extensionCustom,
+ },
+ Bugs: ProtocolBugs{
+ OnlyCompressSecondClientHelloInner: hrr,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":DECODE_ERROR:",
+ })
+
+ // Test that the server rejects a references to the ECH extension in
+ // ech_outer_extensions. The ECH extension is not authenticated in the
+ // AAD and would result in an invalid ClientHelloInner.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-OuterExtensions-SelfReference" + suffix,
+ config: Config{
+ ServerName: "secret.example",
+ DefaultCurves: defaultCurves,
+ ClientECHConfig: publicECHConfig,
+ ECHOuterExtensions: []uint16{
+ extensionEncryptedClientHello,
+ },
+ Bugs: ProtocolBugs{
+ OnlyCompressSecondClientHelloInner: hrr,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":DECODE_ERROR:",
+ })
+ }
+
+ // Test that ECH, which runs before an async early callback, interacts
+ // correctly in the state machine.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-AsyncEarlyCallback",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ },
+ flags: []string{
+ "-async",
+ "-use-early-callback",
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test ECH-enabled server with two ECHConfigs can decrypt client's ECH when
+ // it uses the second ECHConfig.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-SecondECHConfig",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig1,
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig1)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey1),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test all supported ECH cipher suites.
+ for i, cipher := range echCiphers {
+ otherCipher := echCiphers[0]
+ if i == 0 {
+ otherCipher = echCiphers[1]
+ }
+
+ // Test the ECH server can handle the specified cipher.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-Cipher-" + cipher.name,
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "secret.example",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test that the ECH server rejects the specified cipher if not
+ // listed in its ECHConfig.
+ config, key, err := generateECHConfigWithSecretKey("public.example", []HPKECipherSuite{otherCipher.cipher})
+ if err != nil {
+ panic(err)
+ }
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-DisabledCipher-" + cipher.name,
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
+ Bugs: ProtocolBugs{
+ ExpectECHRetryConfigs: MarshalECHConfigList(config),
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(config)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(key),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "public.example",
+ },
+ })
+ }
+
+ // Test that the ECH server handles a short ClientECH.enc value by
+ // falling back to ClientHelloOuter.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-ShortClientECHEnc",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ ExpectECHRetryConfigs: MarshalECHConfigList(publicECHConfig),
+ TruncateClientECHEnc: true,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ "-expect-server-name", "public.example",
+ },
+ })
+
+ // Test that the server handles decryption failure by falling back to
+ // ClientHelloOuter.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-CorruptEncryptedClientHello",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ ExpectECHRetryConfigs: MarshalECHConfigList(publicECHConfig),
+ CorruptEncryptedClientHello: true,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ },
+ })
+
+ // Test that the server treats decryption failure in the second
+ // ClientHello as fatal.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-CorruptSecondEncryptedClientHello",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ // Force a HelloRetryRequest.
+ DefaultCurves: []CurveID{},
+ Bugs: ProtocolBugs{
+ CorruptSecondEncryptedClientHello: true,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ },
+ shouldFail: true,
+ expectedError: ":DECRYPTION_FAILED:",
+ expectedLocalError: "remote error: error decrypting message",
+ })
+
+ // Test that the server treats a missing second ECH extension as fatal.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-OmitSecondEncryptedClientHello",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ // Force a HelloRetryRequest.
+ DefaultCurves: []CurveID{},
+ Bugs: ProtocolBugs{
+ OmitSecondEncryptedClientHello: true,
+ },
+ },
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ },
+ shouldFail: true,
+ expectedError: ":MISSING_EXTENSION:",
+ expectedLocalError: "remote error: missing extension",
+ })
+
+ // Test early data works with ECH, in both accept and reject cases.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-EarlyData",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ },
+ resumeSession: true,
+ earlyData: true,
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-EarlyDataRejected",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ // Cause the server to reject 0-RTT with a bad ticket age.
+ SendTicketAge: 1 * time.Hour,
+ },
+ },
+ resumeSession: true,
+ earlyData: true,
+ expectEarlyDataRejected: true,
+ flags: []string{
+ "-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(publicECHConfig)),
+ "-ech-server-key", base64.StdEncoding.EncodeToString(secretKey),
+ "-ech-is-retry-config", "1",
+ },
+ expectations: connectionExpectations{
+ echAccepted: true,
+ },
+ })
+
+ // Test servers with ECH disabled correctly ignore the extension and
+ // handshake with the ClientHelloOuter.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-Disabled",
+ config: Config{
+ ServerName: "secret.example",
+ ClientECHConfig: publicECHConfig,
+ },
+ flags: []string{
+ "-expect-server-name", "public.example",
+ },
+ })
+
+ // Test the client's behavior when the server ignores ECH GREASE.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ protocol: protocol,
+ name: prefix + "ECH-GREASE-Client-TLS13",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectClientECH: true,
+ },
+ },
+ flags: []string{"-enable-ech-grease"},
+ })
+
+ // Test the client's ECH GREASE behavior when responding to server's
+ // HelloRetryRequest. This test implicitly checks that the first and second
+ // ClientHello messages have identical ECH extensions.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ protocol: protocol,
+ name: prefix + "ECH-GREASE-Client-TLS13-HelloRetryRequest",
+ config: Config{
+ MaxVersion: VersionTLS13,
+ MinVersion: VersionTLS13,
+ // P-384 requires a HelloRetryRequest against BoringSSL's default
+ // configuration. Assert this with ExpectMissingKeyShare.
+ CurvePreferences: []CurveID{CurveP384},
+ Bugs: ProtocolBugs{
+ ExpectMissingKeyShare: true,
+ ExpectClientECH: true,
+ },
+ },
+ flags: []string{"-enable-ech-grease", "-expect-hrr"},
+ })
+
+ retryConfigValid := ECHConfig{
+ PublicName: "example.com",
+ // A real X25519 public key obtained from hpke.GenerateKeyPair().
+ PublicKey: []byte{
+ 0x23, 0x1a, 0x96, 0x53, 0x52, 0x81, 0x1d, 0x7a,
+ 0x36, 0x76, 0xaa, 0x5e, 0xad, 0xdb, 0x66, 0x1c,
+ 0x92, 0x45, 0x8a, 0x60, 0xc7, 0x81, 0x93, 0xb0,
+ 0x47, 0x7b, 0x54, 0x18, 0x6b, 0x9a, 0x1d, 0x6d},
+ KEM: hpke.X25519WithHKDFSHA256,
+ CipherSuites: []HPKECipherSuite{
+ {
+ KDF: hpke.HKDFSHA256,
+ AEAD: hpke.AES256GCM,
+ },
+ },
+ MaxNameLen: 42,
+ }
+
+ retryConfigUnsupportedVersion := []byte{
+ // version
+ 0xba, 0xdd,
+ // length
+ 0x00, 0x05,
+ // contents
+ 0x05, 0x04, 0x03, 0x02, 0x01,
+ }
+
+ validAndInvalidConfigsBuilder := newByteBuilder()
+ validAndInvalidConfigsBody := validAndInvalidConfigsBuilder.addU16LengthPrefixed()
+ validAndInvalidConfigsBody.addBytes(MarshalECHConfig(&retryConfigValid))
+ validAndInvalidConfigsBody.addBytes(retryConfigUnsupportedVersion)
+ validAndInvalidConfigs := validAndInvalidConfigsBuilder.finish()
+
+ // Test that the client accepts a well-formed encrypted_client_hello
+ // extension in response to ECH GREASE. The response includes one ECHConfig
+ // with a supported version and one with an unsupported version.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ protocol: protocol,
+ name: prefix + "ECH-GREASE-Client-TLS13-Retry-Configs",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectClientECH: true,
+ // Include an additional well-formed ECHConfig with an invalid
+ // version. This ensures the client can iterate over the retry
+ // configs.
+ SendECHRetryConfigs: validAndInvalidConfigs,
+ },
+ },
+ flags: []string{"-enable-ech-grease"},
+ })
+
+ // Test that the client aborts with a decode_error alert when it receives a
+ // syntactically-invalid encrypted_client_hello extension from the server.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ protocol: protocol,
+ name: prefix + "ECH-GREASE-Client-TLS13-Invalid-Retry-Configs",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectClientECH: true,
+ SendECHRetryConfigs: []byte{0xba, 0xdd, 0xec, 0xcc},
+ },
+ },
+ flags: []string{"-enable-ech-grease"},
+ shouldFail: true,
+ expectedLocalError: "remote error: error decoding message",
+ expectedError: ":ERROR_PARSING_EXTENSION:",
+ })
+
+ // Test that the server responds to an empty ech_is_inner extension with the
+ // acceptance confirmation.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-ECHIsInner",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ AlwaysSendECHIsInner: true,
+ },
+ },
+ resumeSession: true,
+ })
+
+ // Test that server fails the handshake when it sees a non-empty
+ // ech_is_inner extension.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-ECHIsInner-NotEmpty",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ AlwaysSendECHIsInner: true,
+ SendInvalidECHIsInner: []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,
+ protocol: protocol,
+ name: prefix + "ECH-Server-ECHIsInner-Absent",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ },
+ resumeSession: true,
+ })
+
+ // Test that a TLS 1.3 server that receives an ech_is_inner extension can
+ // negotiate TLS 1.2 without clobbering the downgrade signal.
+ if protocol != quic {
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "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,
+ AlwaysSendECHIsInner: true,
+ },
+ },
+ // 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 server has no ECHConfigs and the
+ // ClientHello contains both encrypted_client_hello and ech_is_inner
+ // extensions.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ protocol: protocol,
+ name: prefix + "ECH-Server-Disabled-EncryptedClientHello-ECHIsInner",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ ClientECHConfig: publicECHConfig,
+ Bugs: ProtocolBugs{
+ AlwaysSendECHIsInner: true,
+ },
+ },
+ shouldFail: true,
+ expectedLocalError: "remote error: illegal parameter",
+ expectedError: ":UNEXPECTED_EXTENSION:",
+ })
}
-
- var validAndInvalidConfigs []byte
- validAndInvalidConfigs = append(validAndInvalidConfigs, MarshalECHConfig(&retryConfigValid)...)
- validAndInvalidConfigs = append(validAndInvalidConfigs, retryConfigUnsupportedVersion...)
-
- // Test that the client accepts a well-formed encrypted_client_hello
- // extension in response to ECH GREASE. The response includes one ECHConfig
- // with a supported version and one with an unsupported version.
- testCases = append(testCases, testCase{
- testType: clientTest,
- name: "ECH-GREASE-Client-TLS13-Retry-Configs",
- config: Config{
- MinVersion: VersionTLS13,
- MaxVersion: VersionTLS13,
- Bugs: ProtocolBugs{
- ExpectClientECH: true,
- // Include an additional well-formed ECHConfig with an invalid
- // version. This ensures the client can iterate over the retry
- // configs.
- SendECHRetryConfigs: validAndInvalidConfigs,
- },
- },
- flags: []string{"-enable-ech-grease"},
- })
-
- // Test that the client aborts with a decode_error alert when it receives a
- // syntactically-invalid encrypted_client_hello extension from the server.
- testCases = append(testCases, testCase{
- testType: clientTest,
- name: "ECH-GREASE-Client-TLS13-Invalid-Retry-Configs",
- config: Config{
- MinVersion: VersionTLS13,
- MaxVersion: VersionTLS13,
- Bugs: ProtocolBugs{
- ExpectClientECH: true,
- SendECHRetryConfigs: []byte{0xba, 0xdd, 0xec, 0xcc},
- },
- },
- flags: []string{"-enable-ech-grease"},
- shouldFail: true,
- 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,
- },
- },
- resumeSession: 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,
- },
- },
- resumeSession: true,
- })
-
- // 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/test/test_config.cc b/ssl/test/test_config.cc
index d37769e..7d66d23 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -238,6 +238,12 @@
{"-verify-prefs", &TestConfig::verify_prefs},
{"-expect-peer-verify-pref", &TestConfig::expect_peer_verify_prefs},
{"-curves", &TestConfig::curves},
+ {"-ech-is-retry-config", &TestConfig::ech_is_retry_config},
+};
+
+const Flag<std::vector<std::string>> kBase64VectorFlags[] = {
+ {"-ech-server-config", &TestConfig::ech_server_configs},
+ {"-ech-server-key", &TestConfig::ech_server_keys},
};
const Flag<std::vector<std::pair<std::string, std::string>>>
@@ -245,6 +251,23 @@
{"-application-settings", &TestConfig::application_settings},
};
+bool DecodeBase64(std::string *out, const std::string &in) {
+ size_t len;
+ if (!EVP_DecodedLength(&len, in.size())) {
+ fprintf(stderr, "Invalid base64: %s.\n", in.c_str());
+ return false;
+ }
+ std::vector<uint8_t> buf(len);
+ if (!EVP_DecodeBase64(buf.data(), &len, buf.size(),
+ reinterpret_cast<const uint8_t *>(in.data()),
+ in.size())) {
+ fprintf(stderr, "Invalid base64: %s.\n", in.c_str());
+ return false;
+ }
+ out->assign(reinterpret_cast<const char *>(buf.data()), len);
+ return true;
+}
+
bool ParseFlag(char *flag, int argc, char **argv, int *i,
bool skip, TestConfig *out_config) {
bool *bool_field = FindField(out_config, kBoolFlags, flag);
@@ -289,21 +312,12 @@
fprintf(stderr, "Missing parameter.\n");
return false;
}
- size_t len;
- if (!EVP_DecodedLength(&len, strlen(argv[*i]))) {
- fprintf(stderr, "Invalid base64: %s.\n", argv[*i]);
- return false;
- }
- std::unique_ptr<uint8_t[]> decoded(new uint8_t[len]);
- if (!EVP_DecodeBase64(decoded.get(), &len, len,
- reinterpret_cast<const uint8_t *>(argv[*i]),
- strlen(argv[*i]))) {
- fprintf(stderr, "Invalid base64: %s.\n", argv[*i]);
+ std::string value;
+ if (!DecodeBase64(&value, argv[*i])) {
return false;
}
if (!skip) {
- base64_field->assign(reinterpret_cast<const char *>(decoded.get()),
- len);
+ *base64_field = std::move(value);
}
return true;
}
@@ -337,6 +351,25 @@
return true;
}
+ std::vector<std::string> *base64_vector_field =
+ FindField(out_config, kBase64VectorFlags, flag);
+ if (base64_vector_field) {
+ *i = *i + 1;
+ if (*i >= argc) {
+ fprintf(stderr, "Missing parameter.\n");
+ return false;
+ }
+ std::string value;
+ if (!DecodeBase64(&value, argv[*i])) {
+ return false;
+ }
+ // Each instance of the flag adds to the list.
+ if (!skip) {
+ base64_vector_field->push_back(std::move(value));
+ }
+ return true;
+ }
+
std::vector<std::pair<std::string, std::string>> *string_pair_vector_field =
FindField(out_config, kStringPairVectorFlags, flag);
if (string_pair_vector_field) {
@@ -347,8 +380,10 @@
}
const char *comma = strchr(argv[*i], ',');
if (!comma) {
- fprintf(stderr,
- "Parameter should be a pair of comma-separated strings.\n");
+ fprintf(
+ stderr,
+ "Parameter should be a comma-separated triple composed of two base64 "
+ "strings followed by \"true\" or \"false\".\n");
return false;
}
// Each instance of the flag adds to the list.
@@ -1595,6 +1630,36 @@
if (enable_ech_grease) {
SSL_set_enable_ech_grease(ssl.get(), 1);
}
+ if (ech_server_configs.size() != ech_server_keys.size() ||
+ ech_server_configs.size() != ech_is_retry_config.size()) {
+ fprintf(stderr,
+ "-ech-server-config, -ech-server-key, and -ech-is-retry-config "
+ "flags must match.\n");
+ return nullptr;
+ }
+ if (!ech_server_configs.empty()) {
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> config_list(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ if (!config_list) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < ech_server_configs.size(); i++) {
+ const std::string &ech_config = ech_server_configs[i];
+ const std::string &ech_private_key = ech_server_keys[i];
+ const int is_retry_config = ech_is_retry_config[i];
+ if (!SSL_ECH_SERVER_CONFIG_LIST_add(
+ config_list.get(), is_retry_config,
+ reinterpret_cast<const uint8_t *>(ech_config.data()),
+ ech_config.size(),
+ reinterpret_cast<const uint8_t *>(ech_private_key.data()),
+ ech_private_key.size())) {
+ return nullptr;
+ }
+ }
+ if (!SSL_CTX_set1_ech_server_config_list(ssl_ctx, config_list.get())) {
+ return nullptr;
+ }
+ }
if (!send_channel_id.empty()) {
SSL_set_tls_channel_id_enabled(ssl.get(), 1);
if (!async) {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index babf1d5..4946bc7 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -40,6 +40,9 @@
std::string cert_file;
std::string expect_server_name;
bool enable_ech_grease = false;
+ std::vector<std::string> ech_server_configs;
+ std::vector<std::string> ech_server_keys;
+ std::vector<int> ech_is_retry_config;
std::string expect_certificate_types;
bool require_any_client_certificate = false;
std::string advertise_npn;
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index d33afa5..4ddb281 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -28,6 +28,7 @@
#include <openssl/stack.h>
#include "../crypto/internal.h"
+#include "../crypto/hpke/internal.h"
#include "internal.h"
@@ -186,13 +187,8 @@
// the common handshake logic. Resolve the remaining non-PSK parameters.
SSL *const ssl = hs->ssl;
SSLMessage msg;
- if (!ssl->method->get_message(ssl, &msg)) {
- return ssl_hs_read_message;
- }
SSL_CLIENT_HELLO client_hello;
- if (!ssl_client_hello_init(ssl, &client_hello, msg)) {
- OPENSSL_PUT_ERROR(SSL, SSL_R_CLIENTHELLO_PARSE_FAILED);
- ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+ if (!hs->GetClientHello(&msg, &client_hello)) {
return ssl_hs_error;
}
@@ -347,13 +343,8 @@
static enum ssl_hs_wait_t do_select_session(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl;
SSLMessage msg;
- if (!ssl->method->get_message(ssl, &msg)) {
- return ssl_hs_read_message;
- }
SSL_CLIENT_HELLO client_hello;
- if (!ssl_client_hello_init(ssl, &client_hello, msg)) {
- OPENSSL_PUT_ERROR(SSL, SSL_R_CLIENTHELLO_PARSE_FAILED);
- ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+ if (!hs->GetClientHello(&msg, &client_hello)) {
return ssl_hs_error;
}
@@ -527,6 +518,7 @@
}
ssl->method->next_message(ssl);
+ hs->ech_client_hello_buf.Reset();
hs->tls13_state = state13_send_server_hello;
return ssl_hs_ok;
}
@@ -534,7 +526,6 @@
static enum ssl_hs_wait_t do_send_hello_retry_request(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl;
-
ScopedCBB cbb;
CBB body, session_id, extensions;
uint16_t group_id;
@@ -576,12 +567,78 @@
return ssl_hs_error;
}
SSL_CLIENT_HELLO client_hello;
- if (!ssl_client_hello_init(ssl, &client_hello, msg)) {
+ if (!ssl_client_hello_init(ssl, &client_hello, msg.body)) {
OPENSSL_PUT_ERROR(SSL, SSL_R_CLIENTHELLO_PARSE_FAILED);
ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
return ssl_hs_error;
}
+ if (hs->ech_accept) {
+ // If we previously accepted the ClientHelloInner, check that the second
+ // ClientHello contains an encrypted_client_hello extension.
+ CBS ech_body;
+ if (!ssl_client_hello_get_extension(&client_hello, &ech_body,
+ TLSEXT_TYPE_encrypted_client_hello)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_EXTENSION);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_MISSING_EXTENSION);
+ return ssl_hs_error;
+ }
+
+ // Parse a ClientECH out of the extension body.
+ uint16_t kdf_id, aead_id;
+ CBS config_id, enc, payload;
+ if (!CBS_get_u16(&ech_body, &kdf_id) || //
+ !CBS_get_u16(&ech_body, &aead_id) ||
+ !CBS_get_u8_length_prefixed(&ech_body, &config_id) ||
+ !CBS_get_u16_length_prefixed(&ech_body, &enc) ||
+ !CBS_get_u16_length_prefixed(&ech_body, &payload) ||
+ CBS_len(&ech_body) != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+ return ssl_hs_error;
+ }
+
+ // Check that ClientECH.cipher_suite is unchanged and that
+ // ClientECH.config_id and ClientECH.enc are empty.
+ if (kdf_id != EVP_HPKE_CTX_get_kdf_id(hs->ech_hpke_ctx.get()) ||
+ aead_id != EVP_HPKE_CTX_get_aead_id(hs->ech_hpke_ctx.get()) ||
+ CBS_len(&config_id) > 0 || CBS_len(&enc) > 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+ return ssl_hs_error;
+ }
+
+ // Decrypt the payload with the HPKE context from the first ClientHello.
+ Array<uint8_t> encoded_client_hello_inner;
+ bool unused;
+ if (!ssl_client_hello_decrypt(
+ hs->ech_hpke_ctx.get(), &encoded_client_hello_inner, &unused,
+ &client_hello, kdf_id, aead_id, config_id, enc, payload)) {
+ // Decryption failure is fatal in the second ClientHello.
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECRYPTION_FAILED);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECRYPT_ERROR);
+ return ssl_hs_error;
+ }
+
+ // Recover the ClientHelloInner from the EncodedClientHelloInner.
+ uint8_t alert = SSL_AD_DECODE_ERROR;
+ bssl::Array<uint8_t> client_hello_inner;
+ if (!ssl_decode_client_hello_inner(ssl, &alert, &client_hello_inner,
+ encoded_client_hello_inner,
+ &client_hello)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+ return ssl_hs_error;
+ }
+ hs->ech_client_hello_buf = std::move(client_hello_inner);
+
+ // Reparse |client_hello| from the buffer owned by |hs|.
+ if (!hs->GetClientHello(&msg, &client_hello)) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+ return ssl_hs_error;
+ }
+ }
+
// We perform all our negotiation based on the first ClientHello (for
// consistency with what |select_certificate_cb| observed), which is in the
// transcript, so we can ignore most of this second one.
@@ -639,6 +696,7 @@
}
ssl->method->next_message(ssl);
+ hs->ech_client_hello_buf.Reset();
hs->tls13_state = state13_send_server_hello;
return ssl_hs_ok;
}
@@ -649,9 +707,8 @@
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.
+ assert(!hs->ech_accept || hs->ech_is_inner_present);
+
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.
diff --git a/tool/server.cc b/tool/server.cc
index 989d335..6993e09 100644
--- a/tool/server.cc
+++ b/tool/server.cc
@@ -61,6 +61,16 @@
"-ocsp-response", kOptionalArgument, "OCSP response file to send",
},
{
+ "-echconfig-key",
+ kOptionalArgument,
+ "File containing the private key corresponding to the ECHConfig.",
+ },
+ {
+ "-echconfig",
+ kOptionalArgument,
+ "File containing one ECHConfig.",
+ },
+ {
"-loop", kBooleanArgument,
"The server will continue accepting new sequential connections.",
},
@@ -261,6 +271,47 @@
}
}
+ if (args_map.count("-echconfig-key") + args_map.count("-echconfig") == 1) {
+ fprintf(stderr,
+ "-echconfig and -echconfig-key must be specified together.\n");
+ return false;
+ }
+
+ if (args_map.count("-echconfig-key") != 0) {
+ std::string echconfig_key_path = args_map["-echconfig-key"];
+ std::string echconfig_path = args_map["-echconfig"];
+
+ // Load the ECH private key.
+ ScopedFILE echconfig_key_file(fopen(echconfig_key_path.c_str(), "rb"));
+ std::vector<uint8_t> echconfig_key;
+ if (echconfig_key_file == nullptr ||
+ !ReadAll(&echconfig_key, echconfig_key_file.get())) {
+ fprintf(stderr, "Error reading %s\n", echconfig_key_path.c_str());
+ return false;
+ }
+
+ // Load the ECHConfig.
+ ScopedFILE echconfig_file(fopen(echconfig_path.c_str(), "rb"));
+ std::vector<uint8_t> echconfig;
+ if (echconfig_file == nullptr ||
+ !ReadAll(&echconfig, echconfig_file.get())) {
+ fprintf(stderr, "Error reading %s\n", echconfig_path.c_str());
+ return false;
+ }
+
+ bssl::UniquePtr<SSL_ECH_SERVER_CONFIG_LIST> configs(
+ SSL_ECH_SERVER_CONFIG_LIST_new());
+ if (!configs ||
+ !SSL_ECH_SERVER_CONFIG_LIST_add(configs.get(),
+ /*is_retry_config=*/1, echconfig.data(),
+ echconfig.size(), echconfig_key.data(),
+ echconfig_key.size()) ||
+ !SSL_CTX_set1_ech_server_config_list(ctx.get(), configs.get())) {
+ fprintf(stderr, "Error setting server's ECHConfig and private key\n");
+ return false;
+ }
+ }
+
if (args_map.count("-cipher") != 0 &&
!SSL_CTX_set_strict_cipher_list(ctx.get(), args_map["-cipher"].c_str())) {
fprintf(stderr, "Failed setting cipher list\n");