Skip to content

Commit ba423c9

Browse files
davidbenBoringssl LUCI CQ
authored and
Boringssl LUCI CQ
committed
Implement ClientHelloOuter handshakes.
If a client offers ECH, but the server rejects it, the client completes the handshake with ClientHelloOuter in order to authenticate retry keys. Implement this flow. This is largely allowing the existing handshake to proceed, but with some changes: - Certificate verification uses the other name. This CL routes this up to the built-in verifier and adds SSL_get0_ech_name_override for the callback. - We need to disable False Start to pick up server Finished in TLS 1.2. - Client certificates, notably in TLS 1.3 where they're encrypted, should only be revealed to the true server. Fortunately, not sending client certs is always an option, so do that. Channel ID has a similar issue. I've just omitted the extension in ClientHelloOuter because it's deprecated and is unlikely to be used with ECH at this point. ALPS may be worth some pondering but, the way it's currently used, is not sensitive. (Possibly we should change the draft to terminate the handshake before even sending that flight...) - The session is never offered in ClientHelloOuter, but our internal book-keeping doesn't quite notice. I had to replace ech_accept with a tri-state ech_status to correctly handle an edge case in SSL_get0_ech_name_override: when ECH + 0-RTT + reverify_on_resume are all enabled, the first certificate verification is for the 0-RTT session and should be against the true name, yet we have selected_ech_config && !ech_accept. A tri-state tracks when ECH is actually rejected. I've maintained this on the server as well, though the server never actually cares. Bug: 275 Change-Id: Ie55966ca3dc4ffcc8c381479f0fe9bcacd34d0f8 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/48135 Commit-Queue: David Benjamin <[email protected]> Reviewed-by: Adam Langley <[email protected]>
1 parent ca7ef8c commit ba423c9

21 files changed

+854
-115
lines changed

crypto/err/ssl.errordata

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ SSL,264,DUPLICATE_KEY_SHARE
5959
SSL,296,DUPLICATE_SIGNATURE_ALGORITHM
6060
SSL,283,EARLY_DATA_NOT_IN_USE
6161
SSL,144,ECC_CERT_NOT_FOR_SIGNING
62+
SSL,319,ECH_REJECTED
6263
SSL,310,ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH
6364
SSL,311,ECH_SERVER_CONFIG_UNSUPPORTED_EXTENSION
6465
SSL,313,ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS

include/openssl/ssl.h

+53-6
Original file line numberDiff line numberDiff line change
@@ -3557,6 +3557,11 @@ OPENSSL_EXPORT const char *SSL_early_data_reason_string(
35573557
// This can prevent observers from seeing cleartext information about the
35583558
// connection, such as the server_name extension.
35593559
//
3560+
// By default, BoringSSL will treat the server name, session ticket, and client
3561+
// certificate as secret, but most other parameters, such as the ALPN protocol
3562+
// list will be treated as public and sent in the cleartext ClientHello. Other
3563+
// APIs may be added for applications with different secrecy requirements.
3564+
//
35603565
// ECH support in BoringSSL is still experimental and under development.
35613566
//
35623567
// See https://tools.ietf.org/html/draft-ietf-tls-esni-10.
@@ -3573,16 +3578,57 @@ OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
35733578
// valid but none of the ECHConfigs implement supported parameters, it will
35743579
// return success and proceed without ECH.
35753580
//
3576-
// WARNING: Client ECH support is still incomplete and does not yet implement
3577-
// the recovery flow. It currently treats ECH rejection as a fatal error. Do not
3578-
// use this API yet.
3579-
//
3580-
// TODO(https://crbug.com/boringssl/275): When the recovery flow is implemented,
3581-
// fill in the remaining docs.
3581+
// If a supported ECHConfig is found, |ssl| will encrypt the true ClientHello
3582+
// parameters. If the server cannot decrypt it, e.g. due to a key mismatch, ECH
3583+
// has a recovery flow. |ssl| will handshake using the cleartext parameters,
3584+
// including a public name in the ECHConfig. If using
3585+
// |SSL_CTX_set_custom_verify|, callers should use |SSL_get0_ech_name_override|
3586+
// to verify the certificate with the public name. If using the built-in
3587+
// verifier, the |X509_STORE_CTX| will be configured automatically.
3588+
//
3589+
// If no other errors are found in this handshake, it will fail with
3590+
// |SSL_R_ECH_REJECTED|. Since it didn't use the true parameters, the connection
3591+
// cannot be used for application data. Instead, callers should handle this
3592+
// error by calling |SSL_get0_ech_retry_configs| and retrying the connection
3593+
// with updated ECH parameters. If the retry also fails with
3594+
// |SSL_R_ECH_REJECTED|, the caller should report a connection failure.
35823595
OPENSSL_EXPORT int SSL_set1_ech_config_list(SSL *ssl,
35833596
const uint8_t *ech_config_list,
35843597
size_t ech_config_list_len);
35853598

3599+
// SSL_get0_ech_name_override sets |*out_name| and |*out_name_len| to point to a
3600+
// buffer containing the ECH public name, if the server rejected ECH, or the
3601+
// empty string otherwise.
3602+
//
3603+
// This function should be called during the certificate verification callback
3604+
// (see |SSL_CTX_set_custom_verify|) if |ssl| is a client offering ECH. If
3605+
// |*out_name_len| is non-zero, the caller should verify the certificate against
3606+
// the result, interpreted as a DNS name, rather than the true server name. In
3607+
// this case, the handshake will never succeed and is only used to authenticate
3608+
// retry configs. See also |SSL_get0_ech_retry_configs|.
3609+
OPENSSL_EXPORT void SSL_get0_ech_name_override(const SSL *ssl,
3610+
const char **out_name,
3611+
size_t *out_name_len);
3612+
3613+
// SSL_get0_ech_retry_configs sets |*out_retry_configs| and
3614+
// |*out_retry_configs_len| to a buffer containing a serialized ECHConfigList.
3615+
// If the server did not provide an ECHConfigList, |*out_retry_configs_len| will
3616+
// be zero.
3617+
//
3618+
// When handling an |SSL_R_ECH_REJECTED| error code as a client, callers should
3619+
// use this function to recover from potential key mismatches. If the result is
3620+
// non-empty, the caller should retry the connection, passing this buffer to
3621+
// |SSL_set1_ech_config_list|. If the result is empty, the server has rolled
3622+
// back ECH support, and the caller should retry without ECH.
3623+
//
3624+
// This function must only be called in response to an |SSL_R_ECH_REJECTED|
3625+
// error code. Calling this function on |ssl|s that have not authenticated the
3626+
// rejection handshake will assert in debug builds and otherwise return an
3627+
// unparsable list.
3628+
OPENSSL_EXPORT void SSL_get0_ech_retry_configs(
3629+
const SSL *ssl, const uint8_t **out_retry_configs,
3630+
size_t *out_retry_configs_len);
3631+
35863632
// SSL_marshal_ech_config constructs a new serialized ECHConfig. On success, it
35873633
// sets |*out| to a newly-allocated buffer containing the result and |*out_len|
35883634
// to the size of the buffer. The caller must call |OPENSSL_free| on |*out| to
@@ -5502,6 +5548,7 @@ BSSL_NAMESPACE_END
55025548
#define SSL_R_COULD_NOT_PARSE_HINTS 316
55035549
#define SSL_R_INVALID_ECH_PUBLIC_NAME 317
55045550
#define SSL_R_INVALID_ECH_CONFIG_LIST 318
5551+
#define SSL_R_ECH_REJECTED 319
55055552
#define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
55065553
#define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
55075554
#define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020

include/openssl/x509.h

+1
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,7 @@ BORINGSSL_MAKE_DELETER(X509_REQ, X509_REQ_free)
18201820
BORINGSSL_MAKE_DELETER(X509_REVOKED, X509_REVOKED_free)
18211821
BORINGSSL_MAKE_DELETER(X509_SIG, X509_SIG_free)
18221822
BORINGSSL_MAKE_DELETER(X509_STORE, X509_STORE_free)
1823+
BORINGSSL_MAKE_UP_REF(X509_STORE, X509_STORE_up_ref)
18231824
BORINGSSL_MAKE_DELETER(X509_STORE_CTX, X509_STORE_CTX_free)
18241825
BORINGSSL_MAKE_DELETER(X509_VERIFY_PARAM, X509_VERIFY_PARAM_free)
18251826

ssl/encrypted_client_hello.cc

+44-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
BSSL_NAMESPACE_BEGIN
4141

4242
// ECH reuses the extension code point for the version number.
43-
static const uint16_t kECHConfigVersion = TLSEXT_TYPE_encrypted_client_hello;
43+
static constexpr uint16_t kECHConfigVersion =
44+
TLSEXT_TYPE_encrypted_client_hello;
4445

4546
static const decltype(&EVP_hpke_aes_128_gcm) kSupportedAEADs[] = {
4647
&EVP_hpke_aes_128_gcm,
@@ -993,6 +994,47 @@ int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ech_config_list,
993994
return ssl->config->client_ech_config_list.CopyFrom(span);
994995
}
995996

997+
void SSL_get0_ech_name_override(const SSL *ssl, const char **out_name,
998+
size_t *out_name_len) {
999+
// When ECH is rejected, we use the public name. Note that, if
1000+
// |SSL_CTX_set_reverify_on_resume| is enabled, we reverify the certificate
1001+
// before the 0-RTT point. If also offering ECH, we verify as if
1002+
// ClientHelloInner was accepted and do not override. This works because, at
1003+
// this point, |ech_status| will be |ssl_ech_none|. See the
1004+
// ECH-Client-Reject-EarlyDataReject-OverrideNameOnRetry tests in runner.go.
1005+
const SSL_HANDSHAKE *hs = ssl->s3->hs.get();
1006+
if (hs && ssl->s3->ech_status == ssl_ech_rejected) {
1007+
*out_name = reinterpret_cast<const char *>(
1008+
hs->selected_ech_config->public_name.data());
1009+
*out_name_len = hs->selected_ech_config->public_name.size();
1010+
} else {
1011+
*out_name = nullptr;
1012+
*out_name_len = 0;
1013+
}
1014+
}
1015+
1016+
void SSL_get0_ech_retry_configs(
1017+
const SSL *ssl, const uint8_t **out_retry_configs,
1018+
size_t *out_retry_configs_len) {
1019+
const SSL_HANDSHAKE *hs = ssl->s3->hs.get();
1020+
if (!hs || !hs->ech_authenticated_reject) {
1021+
// It is an error to call this function except in response to
1022+
// |SSL_R_ECH_REJECTED|. Returning an empty string risks the caller
1023+
// mistakenly believing the server has disabled ECH. Instead, return a
1024+
// non-empty ECHConfigList with a syntax error, so the subsequent
1025+
// |SSL_set1_ech_config_list| call will fail.
1026+
assert(0);
1027+
static const uint8_t kPlaceholder[] = {
1028+
kECHConfigVersion >> 8, kECHConfigVersion & 0xff, 0xff, 0xff, 0xff};
1029+
*out_retry_configs = kPlaceholder;
1030+
*out_retry_configs_len = sizeof(kPlaceholder);
1031+
return;
1032+
}
1033+
1034+
*out_retry_configs = hs->ech_retry_configs.data();
1035+
*out_retry_configs_len = hs->ech_retry_configs.size();
1036+
}
1037+
9961038
int SSL_marshal_ech_config(uint8_t **out, size_t *out_len, uint8_t config_id,
9971039
const EVP_HPKE_KEY *key, const char *public_name,
9981040
size_t max_name_len) {
@@ -1129,5 +1171,5 @@ int SSL_ech_accepted(const SSL *ssl) {
11291171
return ssl->s3->hs->selected_ech_config != nullptr;
11301172
}
11311173

1132-
return ssl->s3->ech_accept;
1174+
return ssl->s3->ech_status == ssl_ech_accepted;
11331175
}

ssl/extensions.cc

+24-11
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,11 @@ static bool ext_ech_parse_serverhello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
654654
return false;
655655
}
656656

657+
if (!ssl_is_valid_ech_config_list(*contents)) {
658+
*out_alert = SSL_AD_DECODE_ERROR;
659+
return false;
660+
}
661+
657662
// The server may only send retry configs in response to ClientHelloOuter (or
658663
// ECH GREASE), not ClientHelloInner. The unsolicited extension rule checks
659664
// this implicitly because the ClientHelloInner has no encrypted_client_hello
@@ -663,14 +668,13 @@ static bool ext_ech_parse_serverhello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
663668
// https://github.com/tlswg/draft-ietf-tls-esni/pull/422 is merged, a later
664669
// draft will fold encrypted_client_hello and ech_is_inner together. Then this
665670
// assert should become a runtime check.
666-
assert(!ssl->s3->ech_accept);
667-
668-
// TODO(https://crbug.com/boringssl/275): When the implementing the
669-
// ClientHelloOuter flow, save the retry configs.
670-
if (!ssl_is_valid_ech_config_list(*contents)) {
671-
*out_alert = SSL_AD_DECODE_ERROR;
671+
assert(ssl->s3->ech_status != ssl_ech_accepted);
672+
if (hs->selected_ech_config &&
673+
!hs->ech_retry_configs.CopyFrom(*contents)) {
674+
*out_alert = SSL_AD_INTERNAL_ERROR;
672675
return false;
673676
}
677+
674678
return true;
675679
}
676680

@@ -685,8 +689,8 @@ static bool ext_ech_parse_clienthello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
685689

686690
static bool ext_ech_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
687691
SSL *const ssl = hs->ssl;
688-
if (ssl_protocol_version(ssl) < TLS1_3_VERSION || //
689-
ssl->s3->ech_accept || //
692+
if (ssl_protocol_version(ssl) < TLS1_3_VERSION ||
693+
ssl->s3->ech_status == ssl_ech_accepted || //
690694
hs->ech_keys == nullptr) {
691695
return true;
692696
}
@@ -1634,12 +1638,21 @@ static bool ext_channel_id_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
16341638
CBB *out_compressible,
16351639
ssl_client_hello_type_t type) {
16361640
const SSL *const ssl = hs->ssl;
1637-
if (!hs->config->channel_id_private || SSL_is_dtls(ssl)) {
1641+
if (!hs->config->channel_id_private || SSL_is_dtls(ssl) ||
1642+
// Don't offer Channel ID in ClientHelloOuter. ClientHelloOuter handshakes
1643+
// are not authenticated for the name that can learn the Channel ID.
1644+
//
1645+
// We could alternatively offer the extension but sign with a random key.
1646+
// For other extensions, we try to align |ssl_client_hello_outer| and
1647+
// |ssl_client_hello_unencrypted|, to improve the effectiveness of ECH
1648+
// GREASE. However, Channel ID is deprecated and unlikely to be used with
1649+
// ECH, so do the simplest thing.
1650+
type == ssl_client_hello_outer) {
16381651
return true;
16391652
}
16401653

1641-
if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_channel_id) ||
1642-
!CBB_add_u16(out_compressible, 0 /* length */)) {
1654+
if (!CBB_add_u16(out, TLSEXT_TYPE_channel_id) ||
1655+
!CBB_add_u16(out, 0 /* length */)) {
16431656
return false;
16441657
}
16451658

ssl/handshake.cc

+9
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg)
128128
: ssl(ssl_arg),
129129
ech_present(false),
130130
ech_is_inner_present(false),
131+
ech_authenticated_reject(false),
131132
scts_requested(false),
132133
handshake_finalized(false),
133134
accept_psk_mode(false),
@@ -715,6 +716,10 @@ int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) {
715716
return -1;
716717

717718
case ssl_hs_early_return:
719+
if (!ssl->server) {
720+
// On ECH reject, the handshake should never complete.
721+
assert(ssl->s3->ech_status != ssl_ech_rejected);
722+
}
718723
*out_early_return = true;
719724
hs->wait = ssl_hs_ok;
720725
return 1;
@@ -734,6 +739,10 @@ int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) {
734739
return -1;
735740
}
736741
if (hs->wait == ssl_hs_ok) {
742+
if (!ssl->server) {
743+
// On ECH reject, the handshake should never complete.
744+
assert(ssl->s3->ech_status != ssl_ech_rejected);
745+
}
737746
// The handshake has completed.
738747
*out_early_return = false;
739748
return 1;

0 commit comments

Comments
 (0)