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>
grpc-202302
Daniel McArdle 4 years ago committed by CQ bot account: commit-bot@chromium.org
parent 61d5aabc06
commit 00e434d67e
  1. 5
      crypto/err/ssl.errordata
  2. 17
      crypto/hpke/hpke.c
  3. 13
      crypto/hpke/internal.h
  4. 1
      fuzz/CMakeLists.txt
  5. 52
      fuzz/decode_client_hello_inner.cc
  6. 1
      include/openssl/base.h
  7. 77
      include/openssl/ssl.h
  8. 1
      include/openssl/tls1.h
  9. 1
      ssl/CMakeLists.txt
  10. 444
      ssl/encrypted_client_hello.cc
  11. 2
      ssl/handoff.cc
  12. 23
      ssl/handshake.cc
  13. 151
      ssl/handshake_server.cc
  14. 136
      ssl/internal.h
  15. 57
      ssl/ssl_lib.cc
  16. 225
      ssl/ssl_test.cc
  17. 39
      ssl/t1_lib.cc
  18. 116
      ssl/test/runner/common.go
  19. 4
      ssl/test/runner/conn.go
  20. 1195
      ssl/test/runner/handshake_client.go
  21. 137
      ssl/test/runner/handshake_messages.go
  22. 22
      ssl/test/runner/hpke/hpke.go
  23. 952
      ssl/test/runner/runner.go
  24. 93
      ssl/test/test_config.cc
  25. 3
      ssl/test/test_config.h
  26. 91
      ssl/tls13_server.cc
  27. 51
      tool/server.cc

@ -58,6 +58,9 @@ SSL,264,DUPLICATE_KEY_SHARE
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,156,HTTP_REQUEST
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,235,UNKNOWN_STATE
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

@ -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 @@ static int add_label_string(CBB *cbb, const char *label) {
// 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 @@ static int hpke_build_suite_id(uint8_t out[HPKE_SUITE_ID_LEN], uint16_t kdf_id,
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 @@ static int hpke_extract_and_expand(const EVP_MD *hkdf_md, uint8_t *out_key,
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:

@ -33,6 +33,9 @@ extern "C" {
//
// 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 @@ OPENSSL_EXPORT int EVP_HPKE_CTX_export(const EVP_HPKE_CTX *hpke, uint8_t *out,
// 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);

@ -29,3 +29,4 @@ fuzzer(dtls_server ssl)
fuzzer(dtls_client ssl)
fuzzer(ssl_ctx_api ssl)
fuzzer(session ssl)
fuzzer(decode_client_hello_inner ssl)

@ -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;
}

@ -431,6 +431,7 @@ typedef struct spake2_ctx_st SPAKE2_CTX;
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;

@ -3575,7 +3575,7 @@ OPENSSL_EXPORT const char *SSL_early_data_reason_string(
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 @@ OPENSSL_EXPORT const char *SSL_early_data_reason_string(
// 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 @@ BSSL_NAMESPACE_BEGIN
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 @@ BSSL_NAMESPACE_END
#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

@ -257,6 +257,7 @@ extern "C" {
// 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

@ -10,6 +10,7 @@ add_library(
d1_srtp.cc
dtls_method.cc
dtls_record.cc
encrypted_client_hello.cc
handoff.cc
handshake.cc
handshake_client.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

@ -93,7 +93,7 @@ bool SSL_serialize_handoff(const SSL *ssl, CBB *out,
!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;
}

@ -126,6 +126,7 @@ BSSL_NAMESPACE_BEGIN
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 @@ void SSL_HANDSHAKE::ResizeSecrets(size_t hash_len) {
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()) {

@ -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 @@ static enum ssl_hs_wait_t do_read_client_hello(SSL_HANDSHAKE *hs) {
}
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 @@ static enum ssl_hs_wait_t do_read_client_hello(SSL_HANDSHAKE *hs) {
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 @@ static enum ssl_hs_wait_t do_read_client_hello(SSL_HANDSHAKE *hs) {
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_read_client_hello(SSL_HANDSHAKE *hs) {
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 @@ static enum ssl_hs_wait_t do_select_certificate(SSL_HANDSHAKE *hs) {
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 @@ static enum ssl_hs_wait_t do_select_parameters(SSL_HANDSHAKE *hs) {
}
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 @@ enum ssl_hs_wait_t ssl_server_handshake(SSL_HANDSHAKE *hs) {
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 @@ const char *ssl_server_handshake_state(SSL_HANDSHAKE *hs) {
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:

@ -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 @@ class GrowableArray {
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 @@ bool tls13_verify_psk_binder(SSL_HANDSHAKE *hs, SSL_SESSION *session,
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 ssl_grease_index_t {
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 @@ struct SSL_HANDSHAKE {
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 @@ struct SSL_HANDSHAKE {
// 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 @@ struct SSL_HANDSHAKE {
// |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 @@ struct SSL_HANDSHAKE {
// 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 @@ bool ssl_log_secret(const SSL *ssl, const char *label,
// 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 @@ struct ssl_ctx_st {
// 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 @@ struct ssl_session_st {
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

@ -2186,6 +2186,63 @@ int SSL_CTX_set_tlsext_servername_arg(SSL_CTX *ctx, void *arg) {
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) {

@ -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 @@ TEST(SSLTest, AddClientCA) {
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);

@ -209,11 +209,11 @@ static bool is_post_quantum_group(uint16_t id) {
}
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 @@ static bool ext_sni_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
}
// Encrypted Client Hello (ECH)
// Encrypted ClientHello (ECH)
//
// https://tools.ietf.org/html/draft-ietf-tls-esni-09
@ -748,6 +748,35 @@ static bool ext_ech_parse_clienthello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
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 @@ static const struct tls_extension kExtensions[] = {
ext_ech_add_clienthello,
ext_ech_parse_serverhello,
ext_ech_parse_clienthello,
dont_add_serverhello,
ext_ech_add_serverhello,
},
{
TLSEXT_TYPE_ech_is_inner,

@ -16,6 +16,8 @@ import (
"strings"
"sync"
"time"
"boringssl.googlesource.com/boringssl/ssl/test/runner/hpke"
)
const (
@ -129,6 +131,7 @@ const (
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 @@ type ConnectionState struct {
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 @@ type Config struct {
// 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 @@ type ProtocolBugs struct {
// 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
// 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
// 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
// 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 @@ func (c *Config) defaultCurves() map[CurveID]bool {
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 @@ func (c *Config) isSupportedVersion(wireVers uint16, isDTLS bool) (uint16, bool)
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
}

@ -119,6 +119,9 @@ type Conn struct {
// 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 @@ func (c *Conn) ConnectionState() ConnectionState {
state.QUICTransportParamsLegacy = c.quicTransportParamsLegacy
state.HasApplicationSettings = c.hasApplicationSettings
state.PeerApplicationSettings = c.peerApplicationSettings
state.ECHAccepted = c.echAccepted
}
return state

File diff suppressed because it is too large Load Diff

@ -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 @@ type ECHConfig struct {
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 @@ func MarshalECHConfig(e *ECHConfig) []byte {
}
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 @@ type clientHelloMsg struct {
compressedCertAlgs []uint16
delegatedCredentials bool
alpsProtocols []string
outerExtensions []uint16
prefixExtensions []uint16
}
@ -358,34 +385,21 @@ func (m *clientHelloMsg) marshalKeyShares(bb *byteBuilder) {
}
}
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 @@ func (m *clientHelloMsg) marshal() []byte {
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 @@ func (m *clientHelloMsg) marshal() []byte {
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 @@ func (m *clientHelloMsg) marshal() []byte {
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 @@ func (m *clientHelloMsg) marshal() []byte {
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 @@ func (m *serverExtensions) marshal(extensions *byteBuilder) {
}
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 @@ func (m *serverExtensions) unmarshal(data byteReader, version uint16) bool {
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

@ -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 @@ const (
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 @@ func SetupPSKReceiverX25519(kdfID, aeadID uint16, enc, secretKeyR, info, psk, ps
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()

@ -554,6 +554,8 @@ type connectionExpectations struct {
// 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 @@ func doExchange(test *testCase, config *Config, conn net.Conn, isResume bool, tr
}
}
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 @@ func addDelegatedCredentialTests() {
})
}
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.
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)
}
// 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,
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{"-enable-ech-grease"},
})
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 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,
// 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{"-enable-ech-grease", "-expect-hrr"},
})
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,
},
})
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,
// 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,
},
},
},
MaxNameLen: 42,
}
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",
},
})
retryConfigUnsupportedVersion := []byte{
// version
0xba, 0xdd,
// length
0x00, 0x05,
// contents
0x05, 0x04, 0x03, 0x02, 0x01,
}
// 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",
},
})
var validAndInvalidConfigs []byte
validAndInvalidConfigs = append(validAndInvalidConfigs, MarshalECHConfig(&retryConfigValid)...)
validAndInvalidConfigs = append(validAndInvalidConfigs, retryConfigUnsupportedVersion...)
// 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 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,
// 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{"-enable-ech-grease"},
})
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 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},
// 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,
},
},
flags: []string{"-enable-ech-grease"},
shouldFail: true,
expectedLocalError: "remote error: error decoding message",
expectedError: ":ERROR_PARSING_EXTENSION:",
})
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 that the server responds to an empty ECH extension with the acceptance
// confirmation.
testCases = append(testCases, testCase{
testType: serverTest,
name: "ECH-Server-ECHIsInner",
config: Config{
MinVersion: VersionTLS13,
MaxVersion: VersionTLS13,
Bugs: ProtocolBugs{
SendECHIsInner: []byte{},
ExpectServerAcceptECH: true,
// Test 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,
},
},
resumeSession: true,
})
flags: []string{
"-expect-server-name", "public.example",
},
})
// 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},
// 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,
},
},
},
shouldFail: true,
expectedLocalError: "remote error: illegal parameter",
expectedError: ":ERROR_PARSING_EXTENSION:",
})
flags: []string{"-enable-ech-grease"},
})
// When ech_is_inner extension is absent, the server should not accept ECH.
testCases = append(testCases, testCase{
testType: serverTest,
name: "ECH-Server-ECHIsInner-Absent",
config: Config{
MinVersion: VersionTLS13,
MaxVersion: VersionTLS13,
Bugs: ProtocolBugs{
ExpectServerAcceptECH: false,
// Test 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,
},
},
},
resumeSession: true,
})
flags: []string{"-enable-ech-grease", "-expect-hrr"},
})
// 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{},
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,
},
},
},
// Check that the client sees the TLS 1.3 downgrade signal in
// ServerHello.random.
shouldFail: true,
expectedLocalError: "tls: downgrade from TLS 1.3 detected",
})
flags: []string{"-enable-ech-grease"},
})
// 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,
// 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},
},
},
},
shouldFail: true,
expectedLocalError: "remote error: illegal parameter",
expectedError: ":UNEXPECTED_EXTENSION:",
})
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:",
})
}
}
func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {

@ -238,6 +238,12 @@ const Flag<std::vector<int>> kIntVectorFlags[] = {
{"-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 @@ const Flag<std::vector<std::pair<std::string, std::string>>>
{"-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 @@ bool ParseFlag(char *flag, int argc, char **argv, int *i,
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 @@ bool ParseFlag(char *flag, int argc, char **argv, int *i,
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 @@ bool ParseFlag(char *flag, int argc, char **argv, int *i,
}
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 @@ bssl::UniquePtr<SSL> TestConfig::NewSSL(
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) {

@ -40,6 +40,9 @@ struct TestConfig {
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;

@ -28,6 +28,7 @@
#include <openssl/stack.h>
#include "../crypto/internal.h"
#include "../crypto/hpke/internal.h"
#include "internal.h"
@ -186,13 +187,8 @@ static enum ssl_hs_wait_t do_select_parameters(SSL_HANDSHAKE *hs) {
// 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 bool quic_ticket_compatible(const SSL_SESSION *session,
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 @@ static enum ssl_hs_wait_t do_select_session(SSL_HANDSHAKE *hs) {
}
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_select_session(SSL_HANDSHAKE *hs) {
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 @@ static enum ssl_hs_wait_t do_read_second_client_hello(SSL_HANDSHAKE *hs) {
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 @@ static enum ssl_hs_wait_t do_read_second_client_hello(SSL_HANDSHAKE *hs) {
}
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 @@ static enum ssl_hs_wait_t do_send_server_hello(SSL_HANDSHAKE *hs) {
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.

@ -60,6 +60,16 @@ static const struct argument kArguments[] = {
{
"-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 @@ bool Server(const std::vector<std::string> &args) {
}
}
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");

Loading…
Cancel
Save