From a3bc00483991ff172c3838461465ce360dd50ded Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 2 Nov 2026 23:07:37 +0800 Subject: [PATCH 1/2] Adapt native TLS extensions to Boring 5.4.0 --- .gitattributes | 4 +- boring-sys/patches/client-fingerprint.patch | 259 ++++++++++++-------- boring-sys/patches/jls-client.patch | 74 +++--- boring-sys/patches/reality-client.patch | 167 ++++++------- boring-sys/patches/shadow-tls-v3.patch | 75 +++--- docs/client-fingerprint.md | 4 + docs/reality-client.md | 2 +- tokio-boring/tests/selected_fingerprint.rs | 15 +- 8 files changed, 330 insertions(+), 270 deletions(-) diff --git a/.gitattributes b/.gitattributes index e5c60314a..fd5691044 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ *.toml text eol=lf -# Unified-diff context lines intentionally contain one leading space. -boring-sys/patches/*.patch whitespace=-blank-at-eol +# Unified diffs preserve source whitespace, including space-prefixed Go tabs. +boring-sys/patches/*.patch -whitespace diff --git a/boring-sys/patches/client-fingerprint.patch b/boring-sys/patches/client-fingerprint.patch index f1e8f1010..2351e0dda 100644 --- a/boring-sys/patches/client-fingerprint.patch +++ b/boring-sys/patches/client-fingerprint.patch @@ -1,43 +1,42 @@ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h -index 055b3e0..d6bcbb6 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h -@@ -5512,6 +5512,14 @@ OPENSSL_EXPORT void SSL_CTX_set_permute_extensions(SSL_CTX *ctx, int enabled); +@@ -5688,6 +5688,14 @@ + // SSL_set_permute_extensions configures whether sockets on `ssl` should // permute extensions. For now, this is only implemented for the ClientHello. OPENSSL_EXPORT void SSL_set_permute_extensions(SSL *ssl, int enabled); - ++ +// Opt-in named profile encoding from this fork. Only built-in, implemented +// profile IDs are accepted (1 = Chrome120, 2 = Chrome133, 3 = Firefox120, +// 4 = Safari16, 5 = iOS14, 6 = Android11, 7 = Edge85, +// 8 = 360 7.5, 9 = QQ 11.1). -+// Trust, versions, ALPN and sessions -+// remain caller-owned. Fresh stream clients only; no raw ClientHello API. ++// Trust, versions, ALPN and sessions remain caller-owned. ++// Fresh stream clients only; no raw ClientHello API. +OPENSSL_EXPORT int SSL_set_client_fingerprint(SSL *ssl, unsigned profile); -+ + // SSL_max_seal_overhead returns the maximum overhead, in bytes, of sealing a - // record with |ssl|. - OPENSSL_EXPORT size_t SSL_max_seal_overhead(const SSL *ssl); + // record with `ssl`. diff --git a/ssl/encrypted_client_hello.cc b/ssl/encrypted_client_hello.cc -index e288f3c..8b50abc 100644 --- a/ssl/encrypted_client_hello.cc +++ b/ssl/encrypted_client_hello.cc -@@ -736,9 +736,13 @@ static bool setup_ech_grease(SSL_HANDSHAKE *hs) { +@@ -741,9 +741,13 @@ } const uint16_t kdf_id = EVP_HPKE_HKDF_SHA256; -- const bool has_aes_hw = hs->ssl->config->aes_hw_override +- const EVP_HPKE_AEAD *aead = EVP_has_aes_hardware() +- ? EVP_hpke_aes_128_gcm() +- : EVP_hpke_chacha20_poly1305(); + // Chrome's GREASE shape is fixed by its template, not the local CPU. + const bool has_aes_hw = hs->config->client_fingerprint == 3 -+ ? random_size(0, 1) != 0 -+ : hs->config->client_fingerprint != 0 || -+ (hs->ssl->config->aes_hw_override - ? hs->ssl->config->aes_hw_override_value -- : EVP_has_aes_hardware(); -+ : EVP_has_aes_hardware()); - const EVP_HPKE_AEAD *aead = - has_aes_hw ? EVP_hpke_aes_128_gcm() : EVP_hpke_chacha20_poly1305(); ++ ? random_size(0, 1) != 0 ++ : hs->config->client_fingerprint != 0 || ++ EVP_has_aes_hardware(); ++ const EVP_HPKE_AEAD *aead = ++ has_aes_hw ? EVP_hpke_aes_128_gcm() : EVP_hpke_chacha20_poly1305(); static_assert(ssl_grease_ech_config_id < sizeof(hs->grease_seed), -@@ -765,7 +769,8 @@ static bool setup_ech_grease(SSL_HANDSHAKE *hs) { + "hs->grease_seed is too small"); + uint8_t config_id = hs->grease_seed[ssl_grease_ech_config_id]; +@@ -768,7 +772,8 @@ // estimate maximum_name_length to be between 32 and 100 bytes. Then round up // to a multiple of 32, to match RFC 9849, section 6.1.3. const size_t payload_len = @@ -48,24 +47,23 @@ index e288f3c..8b50abc 100644 CBB enc_cbb, payload_cbb; uint8_t *payload; diff --git a/ssl/extensions.cc b/ssl/extensions.cc -index db1a291..32908a7 100644 --- a/ssl/extensions.cc +++ b/ssl/extensions.cc -@@ -348,6 +348,13 @@ bool tls12_add_verify_sigalgs(const SSL_HANDSHAKE *hs, CBB *out) { - if (!CBB_add_u16(out, sigalg)) { - return false; +@@ -364,6 +364,13 @@ + continue; } + if (!CBB_add_u16(out, sigalg)) { ++ return false; ++ } + // Safari repeats this signature scheme. Keep real verification preferences + // unique and preserve the duplicate only in the final transcript encoding. + if ((hs->config->client_fingerprint == 4 || + hs->config->client_fingerprint == 5) && sigalg == 0x0805 && + !CBB_add_u16(out, sigalg)) { -+ return false; -+ } + return false; + } } - return true; - } -@@ -941,6 +948,15 @@ static bool ext_ticket_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out, +@@ -966,6 +973,15 @@ SSL_get_options(ssl) & SSL_OP_NO_TICKET) { return true; } @@ -81,7 +79,18 @@ index db1a291..32908a7 100644 // Renegotiation does not participate in session resumption. However, still // advertise the extension to avoid potentially breaking servers which carry -@@ -2726,6 +2741,12 @@ static bool ext_supported_groups_add_clienthello(const SSL_HANDSHAKE *hs, +@@ -1052,7 +1068,9 @@ + return false; + } + // Add a fake signature algorithm. See RFC 8701. +- if (hs->ssl->ctx->grease_enabled && ++ // Fixed profiles retain their declared signature list; the unconfigured ++ // path continues to use the native signature-algorithm GREASE extension. ++ if (hs->config->client_fingerprint == 0 && hs->ssl->ctx->grease_enabled && + !CBB_add_u16(&sigalgs_cbb, + ssl_get_grease_value(hs, ssl_grease_signature_algorithm))) { + return false; +@@ -2779,6 +2797,12 @@ } } @@ -94,7 +103,7 @@ index db1a291..32908a7 100644 return CBB_flush(out_compressible); } -@@ -3257,6 +3278,12 @@ static bool ext_quic_transport_params_add_serverhello_legacy(SSL_HANDSHAKE *hs, +@@ -3315,6 +3339,12 @@ static bool ext_delegated_credential_add_clienthello( const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible, ssl_client_hello_type_t type) { @@ -107,7 +116,7 @@ index db1a291..32908a7 100644 return true; } -@@ -4056,8 +4083,27 @@ static bool ext_server_cert_type_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) { +@@ -4114,8 +4144,27 @@ return true; } @@ -135,7 +144,7 @@ index db1a291..32908a7 100644 { TLSEXT_TYPE_server_name, ext_sni_add_clienthello, -@@ -4297,6 +4343,63 @@ static_assert(kNumExtensions <= +@@ -4355,6 +4404,63 @@ "too many extensions for received bitset"); bool ssl_setup_extension_permutation(SSL_HANDSHAKE *hs) { @@ -199,46 +208,77 @@ index db1a291..32908a7 100644 if (!hs->config->permute_extensions) { return true; } -@@ -4521,6 +4609,8 @@ bool ssl_add_clienthello_tlsext(SSL_HANDSHAKE *hs, CBB *out, CBB *out_encoded, - // images aligned. - size_t psk_len = ext_pre_shared_key_clienthello_length(hs, type); - if (!SSL_is_dtls(ssl) && !SSL_is_quic(ssl) && -+ hs->config->client_fingerprint != 2 && hs->config->client_fingerprint != 3 && -+ hs->config->client_fingerprint != 6 && hs->config->client_fingerprint != 8 && - !ssl->s3->used_hello_retry_request) { - msg_len += 2 /* length prefix */ + CBB_len(&extensions) + psk_len; - // The length of the padding extension, excluding the four-byte extension +@@ -4510,6 +4616,22 @@ + return true; + } + ++// Fixed legacy profiles retain their conditional RFC7685 padding. The native ++// PSK encoder and its binders remain unchanged; only the encoded length is used. ++static size_t profile_pre_shared_key_clienthello_length( ++ const SSL_HANDSHAKE *hs, ssl_client_hello_type_t type) { ++ if (!should_offer_psk(hs, type)) { ++ return 0; ++ } ++ size_t len = 2 + 2 + 2 + 2; ++ for (const auto &psk : hs->pre_shared_keys) { ++ len += 2 + ssl_pre_shared_key_identity(psk).size(); ++ len += 4; ++ len += 1 + EVP_MD_size(ssl_pre_shared_key_hash(psk)); ++ } ++ return len; ++} ++ + bool ssl_add_clienthello_tlsext(SSL_HANDSHAKE *hs, CBB *out, CBB *out_encoded, + ssl_client_hello_type_t type) { + // `out` must contain the start of a ClientHello, which means it must begin +@@ -4521,6 +4643,7 @@ + return ssl_add_clienthello_tlsext_inner(hs, out, out_encoded); + } + ++ size_t profile_msg_len = SSL3_HM_HEADER_LENGTH + CBB_len(out); + assert(out_encoded == nullptr); // Only ClientHelloInner needs two outputs. + SSLImpl *const ssl = hs->ssl; + CBB extensions; +@@ -4575,8 +4698,33 @@ + // intolerant to the last extension being zero-length. See + // https://crbug.com/363583. + bool offering_psk = should_offer_psk(hs, type); +- if (!offering_psk && last_was_empty && !SSL_is_dtls(ssl) && +- !SSL_is_quic(ssl) && !ssl->s3->used_hello_retry_request) { ++ const uint8_t profile = hs->config->client_fingerprint; ++ if (profile != 0) { ++ // Chrome133, Firefox, Android and 360 do not use conditional padding. ++ if (profile != 2 && profile != 3 && profile != 6 && profile != 8 && ++ !SSL_is_dtls(ssl) && !SSL_is_quic(ssl) && ++ !ssl->s3->used_hello_retry_request) { ++ const size_t psk_len = profile_pre_shared_key_clienthello_length(hs, type); ++ profile_msg_len += 2 + CBB_len(&extensions) + psk_len; ++ size_t padding_len = 0; ++ if (last_was_empty && !offering_psk) { ++ padding_len = 1; ++ profile_msg_len += 4 + padding_len; ++ } ++ if (profile_msg_len > 0xff && profile_msg_len < 0x200) { ++ if (padding_len != 0) { ++ profile_msg_len -= 4 + padding_len; ++ } ++ padding_len = 0x200 - profile_msg_len; ++ padding_len = padding_len >= 5 ? padding_len - 4 : 1; ++ } ++ if (padding_len != 0 && ++ !add_padding_extension(&extensions, TLSEXT_TYPE_padding, padding_len)) { ++ return false; ++ } ++ } ++ } else if (!offering_psk && last_was_empty && !SSL_is_dtls(ssl) && ++ !SSL_is_quic(ssl) && !ssl->s3->used_hello_retry_request) { + if (!add_padding_extension(&extensions, TLSEXT_TYPE_padding, 1)) { + return false; + } diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc -index 7ab6d2c..03a02e3 100644 --- a/ssl/handshake_client.cc +++ b/ssl/handshake_client.cc -@@ -126,12 +126,22 @@ static bool ssl_write_client_cipher_list(const SSL_HANDSHAKE *hs, CBB *out, - SSL_CIPHER_AES_128_GCM_SHA256, - SSL_CIPHER_CHACHA20_POLY1305_SHA256, - }; -+ static const uint16_t kCiphersFirefox[] = { -+ SSL_CIPHER_AES_128_GCM_SHA256, -+ SSL_CIPHER_CHACHA20_POLY1305_SHA256, -+ SSL_CIPHER_AES_256_GCM_SHA384, -+ }; - -- const bool has_aes_hw = ssl->config->aes_hw_override -+ // The named template has a fixed TLS 1.3 order on every CPU. This only -+ // controls advertising order; the native policy still filters each cipher. -+ const bool has_aes_hw = ssl->config->client_fingerprint != 0 || -+ (ssl->config->aes_hw_override - ? ssl->config->aes_hw_override_value -- : EVP_has_aes_hardware(); -+ : EVP_has_aes_hardware()); - const bssl::Span ciphers = -- ssl->config->compliance_policy == ssl_compliance_policy_cnsa_202407 -+ ssl->config->client_fingerprint == 3 -+ ? bssl::Span(kCiphersFirefox) -+ : ssl->config->compliance_policy == ssl_compliance_policy_cnsa_202407 - ? bssl::Span(kCiphersCNSA) - : (has_aes_hw ? bssl::Span(kCiphersAESHardware) - : bssl::Span(kCiphersNoAESHardware)); -@@ -157,6 +167,25 @@ static bool ssl_write_client_cipher_list(const SSL_HANDSHAKE *hs, CBB *out, +@@ -122,6 +122,25 @@ continue; } any_enabled = true; @@ -265,10 +305,9 @@ index 7ab6d2c..03a02e3 100644 return false; } diff --git a/ssl/internal.h b/ssl/internal.h -index 2bea8f6..c97af5b 100644 --- a/ssl/internal.h +++ b/ssl/internal.h -@@ -3368,6 +3368,9 @@ struct SSL_CONFIG { +@@ -3417,6 +3417,9 @@ X509_VERIFY_PARAM *param = nullptr; @@ -276,13 +315,12 @@ index 2bea8f6..c97af5b 100644 + uint8_t client_fingerprint = 0; + // crypto - UniquePtr cipher_list; + UniquePtr cipher_list; // for TLS 1.2 ciphers. diff --git a/ssl/ssl_cipher.cc b/ssl/ssl_cipher.cc -index 1ef231c..2737570 100644 --- a/ssl/ssl_cipher.cc +++ b/ssl/ssl_cipher.cc -@@ -75,6 +75,19 @@ static constexpr SSL_CIPHER kCiphers[] = { +@@ -77,6 +77,19 @@ SSL_HANDSHAKE_MAC_DEFAULT, }, @@ -302,10 +340,14 @@ index 1ef231c..2737570 100644 // PSK cipher suites. // Cipher 8C -@@ -189,6 +202,19 @@ static constexpr SSL_CIPHER kCiphers[] = { - SSL_HANDSHAKE_MAC_DEFAULT, - }, - +@@ -187,6 +200,19 @@ + SSL_kECDHE, + SSL_aECDSA, + SSL_AES256, ++ SSL_SHA1, ++ SSL_HANDSHAKE_MAC_DEFAULT, ++ }, ++ + // Opt-in profile patch: reuse native ECDHE/RSA and 3DES primitives for + // Safari's real, selectable legacy suite. Deprecated/default-off as 3DES. + { @@ -315,14 +357,10 @@ index 1ef231c..2737570 100644 + SSL_kECDHE, + SSL_aRSA_SIGN, + SSL_3DES, -+ SSL_SHA1, -+ SSL_HANDSHAKE_MAC_DEFAULT, -+ }, -+ - // Cipher C013 - { - TLS1_TXT_ECDHE_RSA_WITH_AES_128_CBC_SHA, -@@ -654,6 +680,7 @@ bool ssl_cipher_is_deprecated(const SSL_CIPHER *cipher) { + SSL_SHA1, + SSL_HANDSHAKE_MAC_DEFAULT, + }, +@@ -782,6 +808,7 @@ return cipher->protocol_id == SSL_CIPHER_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 || cipher->protocol_id == SSL_CIPHER_ECDHE_RSA_WITH_AES_128_CBC_SHA256 || @@ -330,7 +368,7 @@ index 1ef231c..2737570 100644 cipher->algorithm_enc == SSL_3DES; } -@@ -1042,10 +1069,12 @@ bool ssl_create_cipher_list(UniquePtr *out_cipher_list, +@@ -1172,10 +1199,12 @@ SSL_CIPHER_RSA_WITH_AES_128_GCM_SHA256, SSL_CIPHER_RSA_WITH_AES_256_GCM_SHA384, SSL_CIPHER_RSA_WITH_AES_128_CBC_SHA, @@ -344,25 +382,52 @@ index 1ef231c..2737570 100644 // Set up a linked list of ciphers. diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc -index 89702ea..76fce9a 100644 --- a/ssl/ssl_lib.cc +++ b/ssl/ssl_lib.cc -@@ -3163,6 +3163,18 @@ void SSL_set_permute_extensions(SSL *ssl, int enabled) { - ssl->config->permute_extensions = !!enabled; +@@ -3324,6 +3324,46 @@ + ssl_impl->config->permute_extensions = !!enabled; } +int SSL_set_client_fingerprint(SSL *ssl, unsigned profile) { -+ if (!ssl || !ssl->config || ssl->server || SSL_is_dtls(ssl) || SSL_is_quic(ssl) || -+ ssl->s3->initial_handshake_complete || -+ (ssl->s3->hs != nullptr && ssl->s3->hs->max_version != 0) || -+ ssl->config->client_fingerprint != 0 || profile < 1 || profile > 9) { ++ auto *ssl_impl = FromOpaque(ssl); ++ if (!ssl || !ssl_impl->config || ssl_impl->server || SSL_is_dtls(ssl) || ++ SSL_is_quic(ssl) || ssl_impl->s3->initial_handshake_complete || ++ (ssl_impl->s3->hs != nullptr && ssl_impl->s3->hs->max_version != 0) || ++ ssl_impl->config->client_fingerprint != 0 || profile < 1 || profile > 9) { + OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } -+ ssl->config->client_fingerprint = static_cast(profile); ++ // Configure the real native TLS1.3 list so the wire offer and the server's ++ // selection checks share one policy. Preserve the caller's allowed ciphers. ++ static const uint16_t kChrome[] = { ++ SSL_CIPHER_AES_128_GCM_SHA256, SSL_CIPHER_AES_256_GCM_SHA384, ++ SSL_CIPHER_CHACHA20_POLY1305_SHA256}; ++ static const uint16_t kFirefox[] = { ++ SSL_CIPHER_AES_128_GCM_SHA256, SSL_CIPHER_CHACHA20_POLY1305_SHA256, ++ SSL_CIPHER_AES_256_GCM_SHA384}; ++ static const uint16_t kCnsa[] = { ++ SSL_CIPHER_AES_256_GCM_SHA384, SSL_CIPHER_AES_128_GCM_SHA256, ++ SSL_CIPHER_CHACHA20_POLY1305_SHA256}; ++ const Span order = ++ profile == 3 ? Span(kFirefox) ++ : ssl_impl->config->compliance_policy == ssl_compliance_policy_cnsa_202407 ++ ? Span(kCnsa) ++ : Span(kChrome); ++ uint16_t ciphers[3]; ++ size_t num_ciphers = 0; ++ for (uint16_t cipher : order) { ++ if (ssl_impl->config->tls13_cipher_list.Contains(cipher)) { ++ ciphers[num_ciphers++] = cipher; ++ } ++ } ++ if (num_ciphers != 0 && ++ !SSL_set1_tls13_ciphers(ssl, ciphers, nullptr, num_ciphers)) { ++ return 0; ++ } ++ ssl_impl->config->client_fingerprint = static_cast(profile); + return 1; +} + int32_t SSL_get_ticket_age_skew(const SSL *ssl) { - return ssl->s3->ticket_age_skew; + return FromOpaque(ssl)->s3->ticket_age_skew; } diff --git a/boring-sys/patches/jls-client.patch b/boring-sys/patches/jls-client.patch index 7740904ea..1d337fd92 100644 --- a/boring-sys/patches/jls-client.patch +++ b/boring-sys/patches/jls-client.patch @@ -1,7 +1,7 @@ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h -@@ -2771,6 +2771,14 @@ +@@ -2920,6 +2920,14 @@ // early data, actual ECH, REALITY, DTLS, QUIC and server mode are rejected. OPENSSL_EXPORT int SSL_set1_shadow_tls_v3_client( SSL *ssl, const uint8_t *password, size_t password_len); @@ -14,12 +14,12 @@ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h + SSL *ssl, const uint8_t *username, size_t username_len, + const uint8_t *password, size_t password_len); - // SSL_set1_server_supported_groups_hint, when |ssl| is a client, indicates that - // the server is likely to support groups listed in |server_groups|, in order of + // SSL_set1_server_supported_groups_hint, when `ssl` is a client, indicates that + // the server is likely to support groups listed in `server_groups`, in order of diff --git a/ssl/internal.h b/ssl/internal.h --- a/ssl/internal.h +++ b/ssl/internal.h -@@ -1805,6 +1805,8 @@ +@@ -1802,6 +1802,8 @@ bool reality_auth_pending = false; bool reality_hello_sealed = false; bool shadow_tls_v3_hello_sealed = false; @@ -28,7 +28,7 @@ diff --git a/ssl/internal.h b/ssl/internal.h bool reality_authenticated = false; void ClearRealityAuth(); -@@ -2320,6 +2322,7 @@ +@@ -2309,6 +2311,7 @@ // ssl_add_client_hello constructs a ClientHello and adds it to the outgoing // flight. It returns true on success and false on error. bool ssl_add_client_hello(SSL_HANDSHAKE *hs); @@ -36,7 +36,7 @@ diff --git a/ssl/internal.h b/ssl/internal.h struct ParsedServerHello { CBS raw; -@@ -3390,6 +3393,19 @@ +@@ -3440,6 +3443,19 @@ }; std::optional shadow_tls_v3; @@ -54,12 +54,12 @@ diff --git a/ssl/internal.h b/ssl/internal.h + std::optional jls_client; + // conf_max_version is the maximum acceptable version configured by - // |SSL_set_max_proto_version|. Note this version is not normalized in DTLS - // and is further constrained by |SSL_OP_NO_*|. + // `SSL_set_max_proto_version`. Note this version is not normalized in DTLS + // and is further constrained by `SSL_OP_NO_*`. diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc --- a/ssl/ssl_lib.cc +++ b/ssl/ssl_lib.cc -@@ -738,6 +738,15 @@ +@@ -735,6 +735,15 @@ bool early_return = false; int ret = ssl_run_handshake(hs, &early_return); @@ -73,15 +73,16 @@ diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc + } + } ssl_do_info_callback( - ssl, ssl->server ? SSL_CB_ACCEPT_EXIT : SSL_CB_CONNECT_EXIT, ret); - if (ret <= 0) { -@@ -2085,6 +2094,30 @@ + ssl_impl, ssl_impl->server ? SSL_CB_ACCEPT_EXIT : SSL_CB_CONNECT_EXIT, + ret); +@@ -2195,6 +2204,31 @@ return 1; } -+int SSL_set1_jls_client(SSL *ssl, const uint8_t *username, ++int SSL_set1_jls_client(SSL *ssl_arg, const uint8_t *username, + size_t username_len, const uint8_t *password, + size_t password_len) { ++ auto *ssl = FromOpaque(ssl_arg); + if (!ssl || !ssl->config || !username || username_len == 0 || + username_len > 65535 || !password || password_len == 0 || + password_len > 65535 || ssl->server || SSL_is_dtls(ssl) || @@ -103,10 +104,10 @@ diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc + return 1; +} + - int SSL_set1_shadow_tls_v3_client(SSL *ssl, const uint8_t *password, + int SSL_set1_shadow_tls_v3_client(SSL *ssl_arg, const uint8_t *password, size_t password_len) { - if (!ssl || !ssl->config || !password || password_len == 0 || -@@ -2093,6 +2126,7 @@ + auto *ssl = FromOpaque(ssl_arg); +@@ -2204,6 +2238,7 @@ ssl->s3->initial_handshake_complete || (ssl->s3->hs && ssl->s3->hs->max_version != 0) || ssl->config->reality_client || ssl->config->shadow_tls_v3 || @@ -114,12 +115,12 @@ diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc !ssl->config->client_ech_config_list.empty()) { OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); return 0; -@@ -2123,6 +2157,7 @@ - ssl->enable_early_data || ssl->s3->initial_handshake_complete || - (ssl->s3->hs && ssl->s3->hs->max_version != 0) || - ssl->config->reality_client || ssl->config->shadow_tls_v3 || -+ ssl->config->jls_client || - !ssl->config->client_ech_config_list.empty()) { +@@ -2235,6 +2270,7 @@ + ssl_impl->enable_early_data || ssl_impl->s3->initial_handshake_complete || + (ssl_impl->s3->hs && ssl_impl->s3->hs->max_version != 0) || + ssl_impl->config->reality_client || ssl_impl->config->shadow_tls_v3 || ++ ssl_impl->config->jls_client || + !ssl_impl->config->client_ech_config_list.empty()) { OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); return 0; diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc @@ -133,7 +134,7 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc #include #include "../crypto/bytestring/internal.h" -@@ -305,6 +306,110 @@ +@@ -270,6 +271,110 @@ return true; } @@ -244,10 +245,10 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc static bool seal_shadow_tls_v3_client_hello(SSL_HANDSHAKE *hs, Span msg) { if (!hs->config->shadow_tls_v3 || hs->shadow_tls_v3_hello_sealed) { -@@ -340,6 +445,16 @@ +@@ -305,6 +410,16 @@ bool ssl_add_client_hello(SSL_HANDSHAKE *hs) { - SSL *const ssl = hs->ssl; + SSLImpl *const ssl = hs->ssl; + if (hs->config->jls_client && + (SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl->server || ssl->session || + ssl->enable_early_data || ssl->s3->initial_handshake_complete || @@ -261,7 +262,7 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc if (hs->config->shadow_tls_v3) { if (SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl->server || ssl->session || ssl->enable_early_data || -@@ -374,7 +489,8 @@ +@@ -339,7 +454,8 @@ } if (!seal_reality_client_hello(hs, Span(msg.data(), msg.size())) || @@ -271,7 +272,7 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc return false; } return ssl->method->add_message(ssl, std::move(msg)); -@@ -824,7 +940,8 @@ +@@ -789,7 +905,8 @@ // If this client only accepts non-certificate modes, then the server must // support TLS 1.3. @@ -284,7 +285,7 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc --- a/ssl/tls13_client.cc +++ b/ssl/tls13_client.cc -@@ -221,7 +221,7 @@ +@@ -218,7 +218,7 @@ hs->new_cipher = cipher; const bool is_hrr = is_hello_retry_request(server_hello); @@ -293,7 +294,7 @@ diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_MESSAGE); ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNEXPECTED_MESSAGE); return ssl_hs_error; -@@ -485,6 +485,11 @@ +@@ -482,6 +482,11 @@ SSL3_RANDOM_SIZE); } @@ -308,10 +309,10 @@ diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc diff --git a/ssl/handshake.cc b/ssl/handshake.cc --- a/ssl/handshake.cc +++ b/ssl/handshake.cc -@@ -333,6 +333,23 @@ +@@ -331,6 +331,23 @@ enum ssl_verify_result_t ssl_verify_peer_cert(SSL_HANDSHAKE *hs) { - SSL *const ssl = hs->ssl; + SSLImpl *const ssl = hs->ssl; + if (hs->config->jls_client) { + // JLS authenticates identity through the ServerHello random. The native + // parser, CertificateVerify and Finished still prove the peer's TLS key. @@ -332,7 +333,7 @@ diff --git a/ssl/handshake.cc b/ssl/handshake.cc if (hs->config->reality_client) { if (!verify_reality_certificate(hs)) { hs->new_session->verify_result = X509_V_ERR_APPLICATION_VERIFICATION; -@@ -760,7 +777,8 @@ +@@ -743,7 +760,8 @@ } // Run the state machine again. @@ -345,7 +346,7 @@ diff --git a/ssl/handshake.cc b/ssl/handshake.cc diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc --- a/ssl/ssl_test.cc +++ b/ssl/ssl_test.cc -@@ -2082,6 +2082,174 @@ +@@ -2026,6 +2026,175 @@ return true; } @@ -381,9 +382,10 @@ diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc +} + +static void ExpectJlsCredentials(const SSL *ssl, bool retained) { -+ ASSERT_TRUE(ssl->config); -+ ASSERT_TRUE(ssl->config->jls_client); -+ const auto &state = *ssl->config->jls_client; ++ const auto *ssl_impl = FromOpaque(ssl); ++ ASSERT_TRUE(ssl_impl->config); ++ ASSERT_TRUE(ssl_impl->config->jls_client); ++ const auto &state = *ssl_impl->config->jls_client; + EXPECT_EQ(state.username.size(), retained ? sizeof(kJlsTestUser) - 1 : 0); + EXPECT_EQ(state.password.size(), retained ? sizeof(kJlsTestPassword) - 1 : 0); +} diff --git a/boring-sys/patches/reality-client.patch b/boring-sys/patches/reality-client.patch index 74670206d..f13544d48 100644 --- a/boring-sys/patches/reality-client.patch +++ b/boring-sys/patches/reality-client.patch @@ -1,11 +1,11 @@ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h -index 055b3e02..a44ca66b 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h -@@ -2736,6 +2736,33 @@ OPENSSL_EXPORT int SSL_set1_client_key_shares(SSL *ssl, +@@ -2885,6 +2885,33 @@ + OPENSSL_EXPORT int SSL_set1_client_key_shares(SSL *ssl, const uint16_t *group_ids, size_t num_group_ids); - ++ +// SSL_set1_reality_client enables experimental classic REALITY on a fresh +// client connection. It copies a 32-byte X25519 server public key, a zero to +// eight byte short ID, and a three byte client version. Configure supported @@ -32,15 +32,13 @@ index 055b3e02..a44ca66b 100644 + SSL *ssl, const uint8_t server_public_key[32], const uint8_t *short_id, + size_t short_id_len, const uint8_t client_version[3], + int require_x25519mlkem768); -+ - // SSL_set1_server_supported_groups_hint, when |ssl| is a client, indicates that - // the server is likely to support groups listed in |server_groups|, in order of - // decreasing server preference. This function returns one on success and zero + + // SSL_set1_server_supported_groups_hint, when `ssl` is a client, indicates that + // the server is likely to support groups listed in `server_groups`, in order of diff --git a/ssl/internal.h b/ssl/internal.h -index 2bea8f62..06a280fa 100644 --- a/ssl/internal.h +++ b/ssl/internal.h -@@ -894,6 +894,13 @@ class SSLKeyShare { +@@ -933,6 +933,13 @@ virtual bool Decap(Array *out_secret, uint8_t *out_alert, Span ciphertext) = 0; @@ -51,25 +49,25 @@ index 2bea8f62..06a280fa 100644 + return false; + } + - // SerializePrivateKey writes the private key to |out|, returning true if - // successful and false otherwise. It should be called after |Generate|. + // SerializePrivateKey writes the private key to `out`, returning true if + // successful and false otherwise. It should be called after `Generate`. virtual bool SerializePrivateKey(CBB *out) { return false; } -@@ -1794,6 +1801,12 @@ struct SSL_HANDSHAKE { - // |SSL_OP_NO_*| and |SSL_CTX_set_max_proto_version| APIs. +@@ -1790,6 +1797,12 @@ + // max_version is the maximum accepted protocol version, taking account both + // `SSL_OP_NO_*` and `SSL_CTX_set_max_proto_version` APIs. uint16_t max_version = 0; - ++ + uint8_t reality_auth_key[32] = {}; + bool reality_auth_pending = false; + bool reality_hello_sealed = false; + bool reality_authenticated = false; + void ClearRealityAuth(); -+ + InplaceVector secret; InplaceVector early_traffic_secret; - InplaceVector client_handshake_secret; -@@ -3356,6 +3369,15 @@ struct SSL_CONFIG { - // ssl is a non-owning pointer to the parent |SSL| object. - SSL *const ssl = nullptr; +@@ -3405,6 +3418,15 @@ + // ssl is a non-owning pointer to the parent `SSL` object. + SSLImpl *const ssl = nullptr; + struct RealityClient { + uint8_t server_public_key[32] = {}; @@ -81,13 +79,12 @@ index 2bea8f62..06a280fa 100644 + std::optional reality_client; + // conf_max_version is the maximum acceptable version configured by - // |SSL_set_max_proto_version|. Note this version is not normalized in DTLS - // and is further constrained by |SSL_OP_NO_*|. + // `SSL_set_max_proto_version`. Note this version is not normalized in DTLS + // and is further constrained by `SSL_OP_NO_*`. diff --git a/ssl/ssl_key_share.cc b/ssl/ssl_key_share.cc -index d155b552..a30e9185 100644 --- a/ssl/ssl_key_share.cc +++ b/ssl/ssl_key_share.cc -@@ -139,7 +139,17 @@ class ECKeyShare : public SSLKeyShare { +@@ -139,7 +139,17 @@ class X25519KeyShare : public SSLKeyShare { public: @@ -106,7 +103,8 @@ index d155b552..a30e9185 100644 uint16_t GroupID() const override { return SSL_GROUP_X25519; } -@@ -286,7 +296,18 @@ class X25519Kyber768KeyShare : public SSLKeyShare { +@@ -443,7 +453,18 @@ + // RFC 10024 class X25519MLKEM768KeyShare : public SSLKeyShare { public: - X25519MLKEM768KeyShare() {} @@ -125,12 +123,10 @@ index d155b552..a30e9185 100644 uint16_t GroupID() const override { return SSL_GROUP_X25519_MLKEM768; } - bool Generate(CBB *out) override { diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc -index 89702eaa..90f05614 100644 --- a/ssl/ssl_lib.cc +++ b/ssl/ssl_lib.cc -@@ -2082,6 +2082,111 @@ int SSL_set1_client_key_shares(SSL *ssl, const uint16_t *group_ids, +@@ -2195,6 +2195,112 @@ return 1; } @@ -145,14 +141,15 @@ index 89702eaa..90f05614 100644 + const uint8_t *short_id, size_t short_id_len, + const uint8_t client_version[3], + int require_x25519mlkem768) { -+ if (!ssl || !ssl->config || !server_public_key || !client_version || ++ auto *ssl_impl = FromOpaque(ssl); ++ if (!ssl || !ssl_impl->config || !server_public_key || !client_version || + (require_x25519mlkem768 < 0 || require_x25519mlkem768 > 2) || -+ short_id_len > 8 || (short_id_len != 0 && !short_id) || ssl->server || -+ SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl->session || -+ ssl->enable_early_data || ssl->s3->initial_handshake_complete || -+ (ssl->s3->hs && ssl->s3->hs->max_version != 0) || -+ ssl->config->reality_client || -+ !ssl->config->client_ech_config_list.empty()) { ++ short_id_len > 8 || (short_id_len != 0 && !short_id) || ssl_impl->server || ++ SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl_impl->session || ++ ssl_impl->enable_early_data || ssl_impl->s3->initial_handshake_complete || ++ (ssl_impl->s3->hs && ssl_impl->s3->hs->max_version != 0) || ++ ssl_impl->config->reality_client || ++ !ssl_impl->config->client_ech_config_list.empty()) { + OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } @@ -163,21 +160,21 @@ index 89702eaa..90f05614 100644 + // Build replacements before mutating the connection; no profile dependency. + Array groups; + Array flags; -+ if (!groups.CopyFrom(ssl->config->supported_group_list) || -+ !flags.CopyFrom(ssl->config->supported_group_list_flags)) { ++ if (!groups.CopyFrom(ssl_impl->config->supported_group_list) || ++ !flags.CopyFrom(ssl_impl->config->supported_group_list_flags)) { + return 0; + } + size_t count = 0; + size_t rank = 0; + std::optional last_rank; + for (size_t i = 0; i < groups.size(); i++) { -+ if (i != 0 && (ssl->config->supported_group_list_flags[i - 1] & ++ if (i != 0 && (ssl_impl->config->supported_group_list_flags[i - 1] & + SSL_GROUP_FLAG_EQUAL_PREFERENCE_WITH_NEXT) == 0) { + rank++; + } + if (allow_hybrid || groups[i] != SSL_GROUP_X25519_MLKEM768) { + groups[count] = groups[i]; -+ flags[count] = ssl->config->supported_group_list_flags[i] & ++ flags[count] = ssl_impl->config->supported_group_list_flags[i] & + ~SSL_GROUP_FLAG_EQUAL_PREFERENCE_WITH_NEXT; + if (last_rank.has_value() && *last_rank == rank) { + flags[count - 1] |= SSL_GROUP_FLAG_EQUAL_PREFERENCE_WITH_NEXT; @@ -189,8 +186,8 @@ index 89702eaa..90f05614 100644 + groups.Shrink(count); + flags.Shrink(count); + InplaceVector shares; -+ if (ssl->config->client_key_share_selections) { -+ for (uint16_t group : *ssl->config->client_key_share_selections) { ++ if (ssl_impl->config->client_key_share_selections) { ++ for (uint16_t group : *ssl_impl->config->client_key_share_selections) { + if ((allow_hybrid || group != SSL_GROUP_X25519_MLKEM768) && + !shares.TryPushBack(group)) { + return 0; @@ -223,12 +220,12 @@ index 89702eaa..90f05614 100644 + return 0; + } + } -+ ssl->config->supported_group_list = std::move(groups); -+ ssl->config->supported_group_list_flags = std::move(flags); -+ ssl->config->client_key_share_selections.emplace(); -+ ssl->config->client_key_share_selections->CopyFrom(shares); -+ ssl->config->reality_client.emplace(); -+ auto &reality = *ssl->config->reality_client; ++ ssl_impl->config->supported_group_list = std::move(groups); ++ ssl_impl->config->supported_group_list_flags = std::move(flags); ++ ssl_impl->config->client_key_share_selections.emplace(); ++ ssl_impl->config->client_key_share_selections->CopyFrom(shares); ++ ssl_impl->config->reality_client.emplace(); ++ auto &reality = *ssl_impl->config->reality_client; + reality.allow_x25519mlkem768 = allow_hybrid; + reality.require_x25519mlkem768 = require_hybrid; + OPENSSL_memcpy(reality.server_public_key, server_public_key, 32); @@ -243,7 +240,6 @@ index 89702eaa..90f05614 100644 const uint16_t *server_groups, size_t num_server_groups) { diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc -index 7ab6d2c5..f7036fd8 100644 --- a/ssl/handshake_client.cc +++ b/ssl/handshake_client.cc @@ -31,6 +31,7 @@ @@ -254,7 +250,7 @@ index 7ab6d2c5..f7036fd8 100644 #include #include #include -@@ -216,6 +217,93 @@ bool ssl_write_client_hello_without_extensions(const SSL_HANDSHAKE *hs, +@@ -181,6 +182,93 @@ return true; } @@ -346,9 +342,9 @@ index 7ab6d2c5..f7036fd8 100644 +} + bool ssl_add_client_hello(SSL_HANDSHAKE *hs) { - SSL *const ssl = hs->ssl; + SSLImpl *const ssl = hs->ssl; ScopedCBB cbb; -@@ -232,6 +320,9 @@ bool ssl_add_client_hello(SSL_HANDSHAKE *hs) { +@@ -197,6 +285,9 @@ return false; } @@ -358,23 +354,23 @@ index 7ab6d2c5..f7036fd8 100644 return ssl->method->add_message(ssl, std::move(msg)); } -@@ -362,6 +453,15 @@ static enum ssl_hs_wait_t do_start_connect(SSL_HANDSHAKE *hs) { - return ssl_hs_error; - } +@@ -324,6 +415,15 @@ + // Freeze the version range. + if (!ssl_get_version_range(hs, &hs->min_version, &hs->max_version)) { ++ return ssl_hs_error; ++ } ++ + if (hs->config->reality_client && + (hs->max_version != TLS1_3_VERSION || SSL_is_dtls(ssl) || + SSL_is_quic(ssl) || ssl->session || ssl->enable_early_data || + ssl->s3->initial_handshake_complete || + !hs->config->client_ech_config_list.empty())) { + OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); -+ return ssl_hs_error; -+ } -+ - uint8_t ech_enc[EVP_HPKE_MAX_ENC_LENGTH]; - size_t ech_enc_len; - if (!ssl_select_ech_config(hs, ech_enc, &ech_enc_len)) { -@@ -670,7 +770,7 @@ static enum ssl_hs_wait_t do_read_server_hello(SSL_HANDSHAKE *hs) { + return ssl_hs_error; + } + +@@ -635,7 +735,7 @@ // If this client only accepts non-certificate modes, then the server must // support TLS 1.3. @@ -384,10 +380,9 @@ index 7ab6d2c5..f7036fd8 100644 ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_PROTOCOL_VERSION); return ssl_hs_error; diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc -index c92bb275..7ad541dd 100644 --- a/ssl/tls13_client.cc +++ b/ssl/tls13_client.cc -@@ -221,6 +221,11 @@ static enum ssl_hs_wait_t do_read_hello_retry_request(SSL_HANDSHAKE *hs) { +@@ -218,6 +218,11 @@ hs->new_cipher = cipher; const bool is_hrr = is_hello_retry_request(server_hello); @@ -400,13 +395,12 @@ index c92bb275..7ad541dd 100644 (is_hrr && !hs->transcript.UpdateForHelloRetryRequest())) { return ssl_hs_error; diff --git a/ssl/extensions.cc b/ssl/extensions.cc -index db1a2913..84e06973 100644 --- a/ssl/extensions.cc +++ b/ssl/extensions.cc -@@ -354,6 +354,20 @@ bool tls12_add_verify_sigalgs(const SSL_HANDSHAKE *hs, CBB *out) { +@@ -372,6 +372,20 @@ bool tls12_check_peer_sigalg(const SSL_HANDSHAKE *hs, uint8_t *out_alert, - uint16_t sigalg, EVP_PKEY *pkey) { + uint16_t sigalg, const EVP_PKEY *pkey) { + if (hs->config->reality_client) { + // The browser's advertised list is unchanged. Only an authenticated + // REALITY certificate may use this internal Ed25519 exception. The caller @@ -424,23 +418,22 @@ index db1a2913..84e06973 100644 // The peer must have selected an algorithm that is consistent with its public // key, the TLS version, and what we advertised. Span sigalgs = tls12_get_verify_sigalgs(hs); -@@ -2507,6 +2521,14 @@ bool ssl_ext_key_share_parse_serverhello(SSL_HANDSHAKE *hs, - return false; - } - +@@ -2560,6 +2574,14 @@ + CBS_len(contents) != 0) { + OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR); + *out_alert = SSL_AD_DECODE_ERROR; ++ return false; ++ } ++ + if (hs->config->reality_client && + hs->config->reality_client->require_x25519mlkem768 && + group_id != SSL_GROUP_X25519_MLKEM768) { + *out_alert = SSL_AD_ILLEGAL_PARAMETER; + OPENSSL_PUT_ERROR(SSL, SSL_R_WRONG_CURVE); -+ return false; -+ } -+ - const auto key_share_it = - std::find_if(hs->key_shares.begin(), hs->key_shares.end(), - [group_id](const auto &key_share) { + return false; + } + diff --git a/ssl/handshake.cc b/ssl/handshake.cc -index 0148d5c9..c9613dd0 100644 --- a/ssl/handshake.cc +++ b/ssl/handshake.cc @@ -22,6 +22,8 @@ @@ -452,23 +445,21 @@ index 0148d5c9..c9613dd0 100644 #include #include -@@ -73,9 +75,15 @@ SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg) +@@ -71,7 +73,13 @@ } SSL_HANDSHAKE::~SSL_HANDSHAKE() { + ClearRealityAuth(); ssl->ctx->x509_method->hs_flush_cached_ca_names(this); - } - ++} ++ +void SSL_HANDSHAKE::ClearRealityAuth() { + OPENSSL_cleanse(reality_auth_key, sizeof(reality_auth_key)); + reality_auth_pending = false; -+} -+ + } + bool SSL_HANDSHAKE::GetClientHello(SSLMessage *out_msg, - SSL_CLIENT_HELLO *out_client_hello) { - if (!ech_client_hello_buf.empty()) { -@@ -265,8 +273,76 @@ static bool peer_certificates_equal(const SSL_SESSION *session, +@@ -263,8 +271,76 @@ return true; } @@ -531,7 +522,7 @@ index 0148d5c9..c9613dd0 100644 +} + enum ssl_verify_result_t ssl_verify_peer_cert(SSL_HANDSHAKE *hs) { - SSL *const ssl = hs->ssl; + SSLImpl *const ssl = hs->ssl; + if (hs->config->reality_client) { + if (!verify_reality_certificate(hs)) { + hs->new_session->verify_result = X509_V_ERR_APPLICATION_VERIFICATION; @@ -542,10 +533,10 @@ index 0148d5c9..c9613dd0 100644 + hs->new_session->verify_result = X509_V_OK; + return ssl_verify_ok; + } - const SSL_SESSION *prev_session = ssl->s3->established_session.get(); + const SSLSession *prev_session = ssl->s3->established_session.get(); if (prev_session != nullptr) { // If renegotiating, the server must not change the server certificate. See -@@ -544,6 +620,7 @@ int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) { +@@ -542,6 +618,7 @@ // it set to keep the condition sticky. switch (hs->wait) { case ssl_hs_error: @@ -553,7 +544,7 @@ index 0148d5c9..c9613dd0 100644 ERR_restore_state(hs->error.get()); return -1; -@@ -683,8 +760,17 @@ int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) { +@@ -666,8 +743,17 @@ } // Run the state machine again. diff --git a/boring-sys/patches/shadow-tls-v3.patch b/boring-sys/patches/shadow-tls-v3.patch index 536ff0872..d33c5de03 100644 --- a/boring-sys/patches/shadow-tls-v3.patch +++ b/boring-sys/patches/shadow-tls-v3.patch @@ -1,11 +1,11 @@ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h -@@ -2764,7 +2764,15 @@ +@@ -2912,6 +2912,14 @@ + SSL *ssl, const uint8_t server_public_key[32], const uint8_t *short_id, size_t short_id_len, const uint8_t client_version[3], int require_x25519mlkem768); - --// SSL_set1_server_supported_groups_hint, when |ssl| is a client, indicates that ++ +// SSL_set1_shadow_tls_v3_client authenticates the final encoded ClientHello +// session ID before transcript commitment. Copies a bounded, nonempty password +// on a fresh TCP client SSL. No certificate/signature/Finished bypass is added. @@ -13,31 +13,24 @@ diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h +// early data, actual ECH, REALITY, DTLS, QUIC and server mode are rejected. +OPENSSL_EXPORT int SSL_set1_shadow_tls_v3_client( + SSL *ssl, const uint8_t *password, size_t password_len); -+ -+// SSL_set1_server_supported_groups_hint, when |ssl| is a client, indicates that - // the server is likely to support groups listed in |server_groups|, in order of - // decreasing server preference. This function returns one on success and zero - // on error. This may be used when receiving a server hint, such as described in + + // SSL_set1_server_supported_groups_hint, when `ssl` is a client, indicates that + // the server is likely to support groups listed in `server_groups`, in order of diff --git a/ssl/internal.h b/ssl/internal.h --- a/ssl/internal.h +++ b/ssl/internal.h -@@ -1803,7 +1803,8 @@ - +@@ -1801,6 +1801,7 @@ uint8_t reality_auth_key[32] = {}; bool reality_auth_pending = false; -- bool reality_hello_sealed = false; -+ bool reality_hello_sealed = false; + bool reality_hello_sealed = false; + bool shadow_tls_v3_hello_sealed = false; bool reality_authenticated = false; void ClearRealityAuth(); -@@ -3375,7 +3376,19 @@ - uint8_t client_version[3] = {}; - bool require_x25519mlkem768 = false; +@@ -3427,6 +3428,18 @@ }; -- std::optional reality_client; -+ std::optional reality_client; -+ + std::optional reality_client; + + struct ShadowTlsV3 { + Array password; + void ClearPassword() { @@ -49,19 +42,20 @@ diff --git a/ssl/internal.h b/ssl/internal.h + ~ShadowTlsV3() { ClearPassword(); } + }; + std::optional shadow_tls_v3; - ++ // conf_max_version is the maximum acceptable version configured by - // |SSL_set_max_proto_version|. Note this version is not normalized in DTLS + // `SSL_set_max_proto_version`. Note this version is not normalized in DTLS + // and is further constrained by `SSL_OP_NO_*`. diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc --- a/ssl/ssl_lib.cc +++ b/ssl/ssl_lib.cc -@@ -2085,7 +2085,27 @@ +@@ -2195,6 +2195,27 @@ return 1; } --int SSL_set1_reality_client(SSL *ssl, const uint8_t server_public_key[32], -+int SSL_set1_shadow_tls_v3_client(SSL *ssl, const uint8_t *password, ++int SSL_set1_shadow_tls_v3_client(SSL *ssl_arg, const uint8_t *password, + size_t password_len) { ++ auto *ssl = FromOpaque(ssl_arg); + if (!ssl || !ssl->config || !password || password_len == 0 || + password_len > 65535 || ssl->server || SSL_is_dtls(ssl) || + SSL_is_quic(ssl) || ssl->session || ssl->enable_early_data || @@ -80,38 +74,33 @@ diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc + return 1; +} + -+int SSL_set1_reality_client(SSL *ssl, const uint8_t server_public_key[32], + int SSL_set1_reality_client(SSL *ssl, const uint8_t server_public_key[32], const uint8_t *short_id, size_t short_id_len, const uint8_t client_version[3]) { - return SSL_set1_reality_client_ex(ssl, server_public_key, short_id, -@@ -2102,7 +2122,7 @@ - SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl->session || - ssl->enable_early_data || ssl->s3->initial_handshake_complete || - (ssl->s3->hs && ssl->s3->hs->max_version != 0) || -- ssl->config->reality_client || -+ ssl->config->reality_client || ssl->config->shadow_tls_v3 || - !ssl->config->client_ech_config_list.empty()) { +@@ -2213,7 +2234,7 @@ + SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl_impl->session || + ssl_impl->enable_early_data || ssl_impl->s3->initial_handshake_complete || + (ssl_impl->s3->hs && ssl_impl->s3->hs->max_version != 0) || +- ssl_impl->config->reality_client || ++ ssl_impl->config->reality_client || ssl_impl->config->shadow_tls_v3 || + !ssl_impl->config->client_ech_config_list.empty()) { OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); return 0; diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc --- a/ssl/handshake_client.cc +++ b/ssl/handshake_client.cc -@@ -30,7 +30,8 @@ - #include +@@ -31,6 +31,7 @@ #include #include --#include -+#include + #include +#include #include #include #include -@@ -321,8 +322,60 @@ +@@ -269,8 +270,60 @@ return true; } --bool ssl_add_client_hello(SSL_HANDSHAKE *hs) { -- SSL *const ssl = hs->ssl; +static bool seal_shadow_tls_v3_client_hello(SSL_HANDSHAKE *hs, + Span msg) { + if (!hs->config->shadow_tls_v3 || hs->shadow_tls_v3_hello_sealed) { @@ -145,8 +134,8 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc + return true; +} + -+bool ssl_add_client_hello(SSL_HANDSHAKE *hs) { -+ SSL *const ssl = hs->ssl; + bool ssl_add_client_hello(SSL_HANDSHAKE *hs) { + SSLImpl *const ssl = hs->ssl; + if (hs->config->shadow_tls_v3) { + if (SSL_is_dtls(ssl) || SSL_is_quic(ssl) || ssl->server || + ssl->session || ssl->enable_early_data || @@ -169,7 +158,7 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc ScopedCBB cbb; CBB body; ssl_client_hello_type_t type = hs->selected_ech_config -@@ -337,7 +390,8 @@ +@@ -285,7 +338,8 @@ return false; } @@ -182,7 +171,7 @@ diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc diff --git a/ssl/handshake.cc b/ssl/handshake.cc --- a/ssl/handshake.cc +++ b/ssl/handshake.cc -@@ -760,7 +760,7 @@ +@@ -743,7 +743,7 @@ } // Run the state machine again. diff --git a/docs/client-fingerprint.md b/docs/client-fingerprint.md index 09d5b456e..36c7c80c0 100644 --- a/docs/client-fingerprint.md +++ b/docs/client-fingerprint.md @@ -1,5 +1,9 @@ # Named ClientHello profiles +The fork follows boring 5.4.0 and the official BoringSSL 0.20261020.0 baseline +(`d3bf4795ad5b79d646cddf2fec687da5b9b502db`). Native extensions remain opt-in +build patches; the checked-out BoringSSL submodule contains upstream source. + Enable `boring/client-fingerprint` or `tokio-boring/client-fingerprint` on this fork. The implemented fixed profiles are `ClientFingerprint::Chrome120` (classic X25519), `Chrome133` (ordinary TLS with native X25519MLKEM768 and X25519 shares, diff --git a/docs/reality-client.md b/docs/reality-client.md index 6dd31aa0f..69b12123f 100644 --- a/docs/reality-client.md +++ b/docs/reality-client.md @@ -1,6 +1,6 @@ # REALITY client (opt-in, classic by default) -This branch adds an opt-in `reality` feature on the existing 5.2.0 fork baseline. +The opt-in `reality` feature is maintained on the 5.4.0 fork baseline. It is not by itself a production migration or a published release. The BoringSSL submodule remains pinned; an opt-in build-time patch is shipped alongside the fork's existing patches. No private memory-layout assumptions, diff --git a/tokio-boring/tests/selected_fingerprint.rs b/tokio-boring/tests/selected_fingerprint.rs index 887ce1760..80de34c49 100644 --- a/tokio-boring/tests/selected_fingerprint.rs +++ b/tokio-boring/tests/selected_fingerprint.rs @@ -526,6 +526,15 @@ async fn chrome133_negotiates_only_new_alps_with_absent_empty_and_nonempty_resul #[tokio::test] async fn chrome133_real_ticket_resumption_and_rejected_ticket_full_handshake() { + profile_ticket_resumption(ClientFingerprint::Chrome133, "X25519MLKEM768").await; +} + +#[tokio::test] +async fn chrome120_padding_preserves_real_ticket_resumption_and_rejection() { + profile_ticket_resumption(ClientFingerprint::Chrome120, "X25519").await; +} + +async fn profile_ticket_resumption(profile: ClientFingerprint, group: &'static str) { tokio::time::timeout(std::time::Duration::from_secs(10), async { let tickets: Arc>> = Arc::default(); let saved = tickets.clone(); @@ -533,13 +542,13 @@ async fn chrome133_real_ticket_resumption_and_rejected_ticket_full_handshake() { b.set_session_cache_mode(SslSessionCacheMode::CLIENT | SslSessionCacheMode::NO_INTERNAL); b.set_new_session_callback(move |_, s| saved.lock().unwrap().push(s)); b.set_custom_verify_callback(SslVerifyMode::PEER, |_| Ok(())); - let connector = FingerprintConnector::new(b, ClientFingerprint::Chrome133).unwrap(); - let mut acceptor = server(SslVersion::TLS1_3, "X25519MLKEM768").build(); + let connector = FingerprintConnector::new(b, profile).unwrap(); + let mut acceptor = server(SslVersion::TLS1_3, group).build(); for (round, expected_reuse) in [(0, false), (1, true), (2, false)] { // A distinct real ticket key rejects the offered previous ticket, // then performs a fresh authenticated handshake rather than failing. if round == 2 { - acceptor = server(SslVersion::TLS1_3, "X25519MLKEM768").build(); + acceptor = server(SslVersion::TLS1_3, group).build(); } let peer = acceptor.clone(); let (io, remote) = tokio::io::duplex(4096); From f73406e7eb06c4a77719f74b6a8bbd74940d9e24 Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 2 Nov 2026 23:57:41 +0800 Subject: [PATCH 2/2] test(tls): verify padding on real PSK client hellos --- tokio-boring/tests/selected_fingerprint.rs | 144 ++++++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/tokio-boring/tests/selected_fingerprint.rs b/tokio-boring/tests/selected_fingerprint.rs index 80de34c49..4d9e75a35 100644 --- a/tokio-boring/tests/selected_fingerprint.rs +++ b/tokio-boring/tests/selected_fingerprint.rs @@ -10,8 +10,12 @@ use boring::{ x509::X509, }; use foreign_types::ForeignTypeRef; -use std::sync::{Arc, Mutex}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use std::{ + pin::Pin, + sync::{Arc, Mutex}, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; struct ForgedSignature; impl boring::ssl::PrivateKeyMethod for ForgedSignature { @@ -534,21 +538,142 @@ async fn chrome120_padding_preserves_real_ticket_resumption_and_rejection() { profile_ticket_resumption(ClientFingerprint::Chrome120, "X25519").await; } +#[derive(Debug)] +struct ClientHelloIo { + inner: tokio::io::DuplexStream, + hello: Vec, + reading: bool, +} + +impl AsyncRead for ClientHelloIo { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.reading = true; + Pin::new(&mut self.inner).poll_read(cx, buf) + } +} + +impl AsyncWrite for ClientHelloIo { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bytes: &[u8], + ) -> Poll> { + let result = Pin::new(&mut self.inner).poll_write(cx, bytes); + if let (false, Poll::Ready(Ok(written))) = (self.reading, &result) { + assert!(self.hello.len() + written <= 65_536); + self.hello.extend_from_slice(&bytes[..*written]); + } + result + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } +} + +fn assert_padded_psk_hello(mut records: &[u8]) { + fn take<'a>(bytes: &mut &'a [u8], len: usize) -> &'a [u8] { + let (taken, rest) = bytes.split_at(len); + *bytes = rest; + taken + } + + fn vector<'a>(bytes: &mut &'a [u8], width: usize) -> &'a [u8] { + let len = take(bytes, width) + .iter() + .fold(0usize, |len, byte| len * 256 + usize::from(*byte)); + take(bytes, len) + } + + let mut hello = Vec::new(); + while !records.is_empty() { + assert_eq!(take(&mut records, 1), [22]); + take(&mut records, 2); + hello.extend_from_slice(vector(&mut records, 2)); + } + let mut message = hello.as_slice(); + assert_eq!(take(&mut message, 1), [1]); + let mut body = vector(&mut message, 3); + assert!(message.is_empty()); + take(&mut body, 34); + vector(&mut body, 1); + vector(&mut body, 2); + vector(&mut body, 1); + let mut extensions = vector(&mut body, 2); + assert!(body.is_empty()); + let mut padding = None; + let mut last = None; + while !extensions.is_empty() { + let kind = u16::from_be_bytes(take(&mut extensions, 2).try_into().unwrap()); + let payload = vector(&mut extensions, 2); + if kind == 21 { + assert!(padding.replace(payload.len()).is_none()); + assert!(payload.iter().all(|byte| *byte == 0)); + } + if kind == 41 { + assert!(!payload.is_empty()); + } + last = Some(kind); + } + assert_eq!(last, Some(41), "the real PSK extension must remain last"); + let padding = padding.expect("a PSK-bearing hello in the padding range must be padded"); + let unpadded = hello.len() - 4 - padding; + assert!( + (256..=507).contains(&unpadded), + "unpadded length {unpadded}" + ); + assert_eq!(hello.len(), 512); + assert_eq!(padding, 512 - unpadded - 4); +} + async fn profile_ticket_resumption(profile: ClientFingerprint, group: &'static str) { tokio::time::timeout(std::time::Duration::from_secs(10), async { + let check_padding = profile == ClientFingerprint::Chrome120; let tickets: Arc>> = Arc::default(); let saved = tickets.clone(); let mut b = SslConnector::builder(SslMethod::tls()).unwrap(); b.set_session_cache_mode(SslSessionCacheMode::CLIENT | SslSessionCacheMode::NO_INTERNAL); b.set_new_session_callback(move |_, s| saved.lock().unwrap().push(s)); b.set_custom_verify_callback(SslVerifyMode::PEER, |_| Ok(())); + if check_padding { + b.set_min_proto_version(Some(SslVersion::TLS1_3)).unwrap(); + } let connector = FingerprintConnector::new(b, profile).unwrap(); - let mut acceptor = server(SslVersion::TLS1_3, group).build(); + let make_acceptor = || { + let builder = server(SslVersion::TLS1_3, group); + if check_padding { + // SAFETY: the live builder exclusively owns this context and + // the native API copies the single cipher ID. SHA256 keeps the + // genuine ticket and binder sizes deterministic for padding. + let cipher = [0x1301u16]; + assert_eq!( + unsafe { + boring_sys::SSL_CTX_set1_tls13_ciphers( + builder.as_ptr(), + cipher.as_ptr(), + std::ptr::null(), + cipher.len(), + ) + }, + 1 + ); + } + builder.build() + }; + let mut acceptor = make_acceptor(); for (round, expected_reuse) in [(0, false), (1, true), (2, false)] { // A distinct real ticket key rejects the offered previous ticket, // then performs a fresh authenticated handshake rather than failing. if round == 2 { - acceptor = server(SslVersion::TLS1_3, group).build(); + acceptor = make_acceptor(); } let peer = acceptor.clone(); let (io, remote) = tokio::io::duplex(4096); @@ -562,6 +687,9 @@ async fn profile_ticket_resumption(profile: ClientFingerprint, group: &'static s }); let mut config = connector.configure(b"\x02h2").unwrap(); config.set_verify_hostname(false); + if check_padding { + config.set_enable_ech_grease(false); + } if round != 0 { let ticket = tickets.lock().unwrap().pop().expect("real server ticket"); // All three connections have the same identity and trust policy. @@ -569,10 +697,18 @@ async fn profile_ticket_resumption(profile: ClientFingerprint, group: &'static s config.set_session(&ticket).unwrap(); } } + let io = ClientHelloIo { + inner: io, + hello: Vec::new(), + reading: false, + }; let mut tls = tokio_boring::connect(config, "profile.test", io) .await .unwrap(); assert_eq!(tls.ssl().session_reused(), expected_reuse); + if check_padding && round != 0 { + assert_padded_psk_hello(&tls.get_ref().hello); + } let mut response = [0; 12]; tls.read_exact(&mut response).await.unwrap(); assert_eq!(&response, b"ticket-ready");