Commit a1420a699d for openssl.org
commit a1420a699d2589c2c524ea1f569747f6aaa738f3
Author: mcrmck <willmccormack14@gmail.com>
Date: Sat Mar 7 21:51:17 2026 -0500
Implement RFC 8701 GREASE for TLS ClientHello
Add client-side GREASE (Generate Random Extensions And Sustain
Extensibility) support per RFC 8701. When SSL_OP_GREASE is set,
the TLS client injects reserved 0x?A?A-pattern values into the
ClientHello to prevent ecosystem ossification caused by servers
that reject unknown values.
GREASE values are injected into:
- Cipher suites (prepended)
- Supported versions extension (prepended)
- Supported groups extension (prepended)
- Signature algorithms extension (appended)
- Key share extension (prepended, 1 zero byte)
- Two standalone extensions (one empty, one with 1 zero byte)
The implementation uses lazy-seeded random values that remain
consistent across HelloRetryRequest retransmissions. GREASE values
from server responses are rejected as illegal parameters.
Add -grease option to s_client to enable GREASE from the command line.
Closes #9660
Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.foundation>
Reviewed-by: Neil Horman <nhorman@openssl.org>
MergeDate: Tue Mar 17 14:58:25 2026
(Merged from https://github.com/openssl/openssl/pull/30303)
diff --git a/CHANGES.md b/CHANGES.md
index 88b6a0ebde..90069f94d7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -61,6 +61,26 @@ OpenSSL Releases
*Daniel Kubec*
+ * Added support for RFC 8701 GREASE (Generate Random Extensions And Sustain
+ Extensibility). When `SSL_OP_GREASE` is set, the TLS client injects
+ reserved GREASE values into cipher suites, supported versions, supported
+ groups, signature algorithms, key share, and extensions in the ClientHello
+ to prevent ecosystem ossification. The `openssl s_client` command gains a
+ `-grease` option to enable this.
+
+ *William McCormack*
+
+### Changes between 3.6 and 4.0 [xx XXX xxxx]
+
+ * Added support for RFC 8701 GREASE (Generate Random Extensions And Sustain
+ Extensibility). When `SSL_OP_GREASE` is set, the TLS client injects
+ reserved GREASE values into cipher suites, supported versions, supported
+ groups, signature algorithms, key share, and extensions in the ClientHello
+ to prevent ecosystem ossification. The `openssl s_client` command gains a
+ `-grease` option to enable this.
+
+ *William McCormack*
+
### Changes between 3.6 and 4.0 [xx XXX xxxx]
* Added `-expected-rpks` option to the `openssl s_client`
diff --git a/apps/s_client.c b/apps/s_client.c
index b48b296966..592e3da79f 100644
--- a/apps/s_client.c
+++ b/apps/s_client.c
@@ -598,6 +598,7 @@ typedef enum OPTION_choice {
OPT_S_ENUM,
OPT_IGNORE_UNEXPECTED_EOF,
OPT_FALLBACKSCSV,
+ OPT_GREASE,
OPT_NOCMDS,
OPT_ADV,
OPT_PROXY,
@@ -671,6 +672,7 @@ const OPTIONS s_client_options[] = {
{ "read_buf", OPT_READ_BUF, 'p',
"Default read buffer size to be used for connections" },
{ "fallback_scsv", OPT_FALLBACKSCSV, '-', "Send the fallback SCSV" },
+ { "grease", OPT_GREASE, '-', "Send GREASE values in ClientHello (RFC 8701)" },
OPT_SECTION("Identity"),
{ "cert", OPT_CERT, '<', "Client certificate file to use" },
@@ -1031,6 +1033,7 @@ int s_client_main(int argc, char **argv)
#endif
int read_buf_len = 0;
int fallback_scsv = 0;
+ int grease = 0;
OPTION_CHOICE o;
#ifndef OPENSSL_NO_DTLS
int enable_timeouts = 0;
@@ -1518,6 +1521,9 @@ int s_client_main(int argc, char **argv)
case OPT_FALLBACKSCSV:
fallback_scsv = 1;
break;
+ case OPT_GREASE:
+ grease = 1;
+ break;
case OPT_KEYFORM:
if (!opt_format(opt_arg(), OPT_FMT_ANY, &key_format))
goto opthelp;
@@ -2304,6 +2310,8 @@ int s_client_main(int argc, char **argv)
if (fallback_scsv)
SSL_set_mode(con, SSL_MODE_SEND_FALLBACK_SCSV);
+ if (grease)
+ SSL_set_options(con, SSL_OP_GREASE);
if (!noservername && (servername != NULL || dane_tlsa_domain == NULL)) {
if (servername == NULL) {
diff --git a/doc/man1/openssl-s_client.pod.in b/doc/man1/openssl-s_client.pod.in
index 635da52be7..3e53b4d9a0 100644
--- a/doc/man1/openssl-s_client.pod.in
+++ b/doc/man1/openssl-s_client.pod.in
@@ -76,6 +76,7 @@ B<openssl> B<s_client>
[B<-sctp>]
[B<-sctp_label_bug>]
[B<-fallback_scsv>]
+[B<-grease>]
[B<-async>]
[B<-maxfraglen> I<len>]
[B<-max_send_frag>]
@@ -573,6 +574,13 @@ available where OpenSSL has support for SCTP enabled.
Send TLS_FALLBACK_SCSV in the ClientHello.
+=item B<-grease>
+
+Send GREASE (Generate Random Extensions And Sustain Extensibility) values in
+the ClientHello as defined in RFC 8701. This injects random reserved values
+into cipher suites, supported groups, supported versions, signature algorithms,
+key share, and extensions to prevent ecosystem ossification.
+
=item B<-async>
Switch on asynchronous mode. Cryptographic operations will be performed
diff --git a/doc/man3/SSL_CTX_set_options.pod b/doc/man3/SSL_CTX_set_options.pod
index d9aecb280b..ec66190d7a 100644
--- a/doc/man3/SSL_CTX_set_options.pod
+++ b/doc/man3/SSL_CTX_set_options.pod
@@ -408,6 +408,16 @@ ECH.
If set, TLS ClientHello messages emitted by the client will ignore the
ECHConfig config_id chosen by the server and use a random octet.
+=item SSL_OP_GREASE
+
+If set, TLS ClientHello messages will include GREASE (Generate Random
+Extensions And Sustain Extensibility) values as defined in RFC 8701. This
+injects random reserved values (matching the 0x?A?A pattern) into cipher
+suites, supported groups, supported versions, signature algorithms, key share
+entries, and extensions. GREASE values help prevent ecosystem ossification by
+ensuring servers and middleboxes tolerate unknown values. The injected values
+are consistent across HelloRetryRequest replays within the same connection.
+
=back
The following options no longer have any effect but their identifiers are
diff --git a/include/openssl/ssl.h.in b/include/openssl/ssl.h.in
index d7915db1aa..cba2cd80ba 100644
--- a/include/openssl/ssl.h.in
+++ b/include/openssl/ssl.h.in
@@ -453,6 +453,9 @@ typedef int (*SSL_async_callback_fn)(SSL *s, void *arg);
#define SSL_OP_ECH_GREASE_RETRY_CONFIG SSL_OP_BIT(40)
#endif
+/* RFC 8701: Send GREASE values in ClientHello */
+#define SSL_OP_GREASE SSL_OP_BIT(41)
+
/*
* Option "collections."
*/
diff --git a/ssl/ssl_ciph.c b/ssl/ssl_ciph.c
index d0e95f0011..fc12efaae1 100644
--- a/ssl/ssl_ciph.c
+++ b/ssl/ssl_ciph.c
@@ -2261,6 +2261,17 @@ int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk,
if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV)
maxlen -= 2;
+ /* RFC 8701: prepend a GREASE cipher suite value */
+ if ((s->options & SSL_OP_GREASE) && !s->server) {
+ uint16_t grease_cs = ossl_grease_value(s, OSSL_GREASE_CIPHER);
+
+ if (!WPACKET_put_bytes_u16(pkt, grease_cs)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+ totlen += 2;
+ }
+
for (i = 0; i < sk_SSL_CIPHER_num(sk) && totlen < maxlen; i++) {
const SSL_CIPHER *c;
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index b3de7a0982..be5f4a20f0 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -8496,3 +8496,37 @@ int SSL_CTX_get0_server_cert_type(const SSL_CTX *ctx, unsigned char **t, size_t
*len = ctx->server_cert_type_len;
return 1;
}
+
+/*
+ * RFC 8701 GREASE - returns a GREASE value (0x?A?A pattern) for the given
+ * index. Seeds are generated lazily on first use and remain stable for the
+ * lifetime of the connection so that HelloRetryRequest replays get identical
+ * values.
+ */
+uint16_t ossl_grease_value(SSL_CONNECTION *s, int index)
+{
+ uint16_t ret;
+
+ if (index < 0 || index > OSSL_GREASE_LAST_INDEX)
+ return 0x0A0A;
+
+ if (!s->ext.grease_seeded) {
+ if (RAND_bytes_ex(SSL_CONNECTION_GET_CTX(s)->libctx,
+ s->ext.grease_seed,
+ sizeof(s->ext.grease_seed), 0)
+ <= 0)
+ memset(s->ext.grease_seed, 0x42, sizeof(s->ext.grease_seed));
+ s->ext.grease_seeded = 1;
+ }
+
+ /* Map seed byte to 0x?A?A pattern */
+ ret = (s->ext.grease_seed[index] & 0xf0) | 0x0a;
+ ret |= ret << 8;
+
+ /* Ensure EXT2 differs from EXT1 */
+ if (index == OSSL_GREASE_EXT2
+ && ret == ossl_grease_value(s, OSSL_GREASE_EXT1))
+ ret ^= 0x1010;
+
+ return ret;
+}
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index e28cc2aa22..a7159d3c19 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -697,12 +697,23 @@ typedef enum tlsext_index_en {
TLSEXT_IDX_certificate_authorities,
TLSEXT_IDX_ech,
TLSEXT_IDX_outer_extensions,
+ TLSEXT_IDX_grease1,
+ TLSEXT_IDX_grease2,
TLSEXT_IDX_padding,
TLSEXT_IDX_psk,
/* Dummy index - must always be the last entry */
TLSEXT_IDX_num_builtins
} TLSEXT_INDEX;
+/* RFC 8701 GREASE seed indices */
+#define OSSL_GREASE_CIPHER 0
+#define OSSL_GREASE_GROUP 1
+#define OSSL_GREASE_EXT1 2
+#define OSSL_GREASE_EXT2 3
+#define OSSL_GREASE_VERSION 4
+#define OSSL_GREASE_SIGALG 5
+#define OSSL_GREASE_LAST_INDEX 5
+
DEFINE_LHASH_OF_EX(SSL_SESSION);
/* Needed in ssl_cert.c */
DEFINE_LHASH_OF_EX(X509_NAME);
@@ -1741,6 +1752,10 @@ struct ssl_connection_st {
#ifndef OPENSSL_NO_ECH
OSSL_ECH_CONN ech;
#endif
+
+ /* RFC 8701 GREASE */
+ uint8_t grease_seed[OSSL_GREASE_LAST_INDEX + 1];
+ int grease_seeded;
} ext;
/*
@@ -2505,6 +2520,11 @@ void ssl_sort_cipher_list(void);
int ssl_load_ciphers(SSL_CTX *ctx);
int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk,
WPACKET *pkt);
+uint16_t ossl_grease_value(SSL_CONNECTION *s, int index);
+static ossl_inline int ossl_is_grease_value(uint16_t val)
+{
+ return (val & 0x0f0f) == 0x0a0a && (val >> 8) == (val & 0xff);
+}
__owur int ssl_setup_sigalgs(SSL_CTX *ctx);
int ssl_load_groups(SSL_CTX *ctx);
int ssl_load_sigalgs(SSL_CTX *ctx);
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index 16467f782f..417255241c 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -464,6 +464,18 @@ static const EXTENSION_DEFINITION ext_defs[] = {
INVALID_EXTENSION,
INVALID_EXTENSION,
#endif /* END_OPENSSL_NO_ECH */
+ { /* RFC 8701 GREASE extension 1 - type is dynamic */
+ TLSEXT_TYPE_grease1,
+ SSL_EXT_CLIENT_HELLO,
+ 0,
+ NULL,
+ NULL, NULL, NULL, tls_construct_ctos_grease1, NULL },
+ { /* RFC 8701 GREASE extension 2 - type is dynamic */
+ TLSEXT_TYPE_grease2,
+ SSL_EXT_CLIENT_HELLO,
+ 0,
+ NULL,
+ NULL, NULL, NULL, tls_construct_ctos_grease2, NULL },
{ /* Must be immediately before pre_shared_key */
TLSEXT_TYPE_padding,
SSL_EXT_CLIENT_HELLO,
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index 7cdb594e8e..b2a5de6a87 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -335,6 +335,14 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt,
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return EXT_RETURN_FAIL;
}
+ /* RFC 8701: prepend a GREASE group value */
+ if ((s->options & SSL_OP_GREASE) && !s->server) {
+ if (!WPACKET_put_bytes_u16(pkt,
+ ossl_grease_value(s, OSSL_GREASE_GROUP))) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+ }
/* Copy group ID if supported */
for (i = 0; i < num_groups; i++) {
const TLS_GROUP_INFO *ginfo = NULL;
@@ -452,8 +460,19 @@ EXT_RETURN tls_construct_ctos_sig_algs(SSL_CONNECTION *s, WPACKET *pkt,
|| !WPACKET_start_sub_packet_u16(pkt)
/* Sub-packet for the actual list */
|| !WPACKET_start_sub_packet_u16(pkt)
- || !tls12_copy_sigalgs(s, pkt, salg, salglen)
- || !WPACKET_close(pkt)
+ || !tls12_copy_sigalgs(s, pkt, salg, salglen)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+ /* RFC 8701: append a GREASE signature algorithm value */
+ if ((s->options & SSL_OP_GREASE) && !s->server) {
+ if (!WPACKET_put_bytes_u16(pkt,
+ ossl_grease_value(s, OSSL_GREASE_SIGALG))) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+ }
+ if (!WPACKET_close(pkt)
|| !WPACKET_close(pkt)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return EXT_RETURN_FAIL;
@@ -740,6 +759,14 @@ EXT_RETURN tls_construct_ctos_supported_versions(SSL_CONNECTION *s, WPACKET *pkt
return EXT_RETURN_FAIL;
}
+ /* RFC 8701: prepend a GREASE version value */
+ if ((s->options & SSL_OP_GREASE) && !s->server) {
+ if (!WPACKET_put_bytes_u16(pkt,
+ ossl_grease_value(s, OSSL_GREASE_VERSION))) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+ }
for (currv = max_version; currv >= min_version; currv--) {
if (!WPACKET_put_bytes_u16(pkt, currv)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
@@ -873,6 +900,19 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt,
return EXT_RETURN_FAIL;
}
+ /* RFC 8701: prepend a GREASE key share entry (1 byte of 0x00) */
+ if ((s->options & SSL_OP_GREASE) && !s->server) {
+ uint16_t grease_group = ossl_grease_value(s, OSSL_GREASE_GROUP);
+
+ if (!WPACKET_put_bytes_u16(pkt, grease_group)
+ || !WPACKET_start_sub_packet_u16(pkt)
+ || !WPACKET_put_bytes_u8(pkt, 0)
+ || !WPACKET_close(pkt)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+ }
+
tls1_get_requested_keyshare_groups(s, &pgroups, &num_groups);
if (num_groups == 1 && pgroups[0] == 0) { /* Indication that no * prefix was used */
tls1_get_supported_groups(s, &pgroups, &num_groups);
@@ -2174,6 +2214,12 @@ int tls_parse_stoc_key_share(SSL_CONNECTION *s, PACKET *pkt,
return 0;
}
+ /* RFC 8701: reject GREASE values selected by the server */
+ if (ossl_is_grease_value(group_id)) {
+ SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER, SSL_R_BAD_KEY_SHARE);
+ return 0;
+ }
+
if ((context & SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) != 0) {
const uint16_t *pgroups = NULL;
size_t num_groups;
@@ -2818,3 +2864,55 @@ int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
return 1;
}
#endif /* END_OPENSSL_NO_ECH */
+
+/*
+ * RFC 8701 GREASE extension constructors. Each writes an empty extension
+ * whose type is a GREASE value (0x?A?A pattern).
+ */
+EXT_RETURN tls_construct_ctos_grease1(SSL_CONNECTION *s, WPACKET *pkt,
+ unsigned int context, X509 *x,
+ size_t chainidx)
+{
+ uint16_t grease_type;
+
+ if (!(s->options & SSL_OP_GREASE) || s->server)
+ return EXT_RETURN_NOT_SENT;
+
+ grease_type = ossl_grease_value(s, OSSL_GREASE_EXT1);
+
+ if (!WPACKET_put_bytes_u16(pkt, grease_type)
+ || !WPACKET_put_bytes_u16(pkt, 0)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+
+ return EXT_RETURN_SENT;
+}
+
+EXT_RETURN tls_construct_ctos_grease2(SSL_CONNECTION *s, WPACKET *pkt,
+ unsigned int context, X509 *x,
+ size_t chainidx)
+{
+ uint16_t grease_type;
+
+ if (!(s->options & SSL_OP_GREASE) || s->server)
+ return EXT_RETURN_NOT_SENT;
+
+ grease_type = ossl_grease_value(s, OSSL_GREASE_EXT2);
+
+ /*
+ * RFC 8701 recommends "varying length and contents" for GREASE
+ * extensions. Extension 1 is empty; extension 2 carries one zero byte
+ * so that servers are tested against both empty and non-empty unknown
+ * extensions. This mirrors the BoringSSL behaviour.
+ */
+ if (!WPACKET_put_bytes_u16(pkt, grease_type)
+ || !WPACKET_start_sub_packet_u16(pkt)
+ || !WPACKET_put_bytes_u8(pkt, 0)
+ || !WPACKET_close(pkt)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return EXT_RETURN_FAIL;
+ }
+
+ return EXT_RETURN_SENT;
+}
diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h
index 0c3cd3ac08..03e15f888c 100644
--- a/ssl/statem/statem_local.h
+++ b/ssl/statem/statem_local.h
@@ -43,6 +43,9 @@
/* Invalid extension ID for non-supported extensions */
#define TLSEXT_TYPE_invalid 0x10000
#define TLSEXT_TYPE_out_of_range 0x10001
+/* RFC 8701 GREASE extension placeholders (actual type is dynamic) */
+#define TLSEXT_TYPE_grease1 0x10002
+#define TLSEXT_TYPE_grease2 0x10003
unsigned int ossl_get_extension_type(size_t idx);
extern const unsigned char hrrrandom[];
@@ -478,6 +481,12 @@ EXT_RETURN tls_construct_ctos_psk_kex_modes(SSL_CONNECTION *s, WPACKET *pkt,
EXT_RETURN tls_construct_ctos_cookie(SSL_CONNECTION *s, WPACKET *pkt,
unsigned int context,
X509 *x, size_t chainidx);
+EXT_RETURN tls_construct_ctos_grease1(SSL_CONNECTION *s, WPACKET *pkt,
+ unsigned int context, X509 *x,
+ size_t chainidx);
+EXT_RETURN tls_construct_ctos_grease2(SSL_CONNECTION *s, WPACKET *pkt,
+ unsigned int context, X509 *x,
+ size_t chainidx);
EXT_RETURN tls_construct_ctos_padding(SSL_CONNECTION *s, WPACKET *pkt,
unsigned int context, X509 *x,
size_t chainidx);
diff --git a/test/ext_internal_test.c b/test/ext_internal_test.c
index 6c1aaed9f1..c88f026dee 100644
--- a/test/ext_internal_test.c
+++ b/test/ext_internal_test.c
@@ -79,6 +79,8 @@ static EXT_LIST ext_list[] = {
EXT_EXCEPTION(ech),
EXT_EXCEPTION(outer_extensions),
#endif
+ EXT_ENTRY(grease1),
+ EXT_ENTRY(grease2),
EXT_ENTRY(padding),
EXT_ENTRY(psk),
EXT_END(num_builtins)
diff --git a/test/sslapitest.c b/test/sslapitest.c
index 622e2a8469..de60ed8ada 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -10308,7 +10308,7 @@ static int test_session_cache_overflow(int idx)
* would free the get_sess_val, causing a use-after-free error.
*/
if (!TEST_true(CRYPTO_GET_REF(&get_sess_val->references, &references))
- || !TEST_int_ge(references, 2))
+ || !TEST_int_ge(references, 2))
goto end;
sess = SSL_get1_session(clientssl);
if (!TEST_ptr(sess))
@@ -14144,6 +14144,227 @@ err:
}
#endif
+#if !defined(OSSL_NO_USABLE_TLS1_3)
+/*
+ * RFC 8701 GREASE test helpers.
+ * We capture the raw ClientHello via msg_callback and then parse it with
+ * PACKET functions to confirm that GREASE values (matching 0x?A?A) are
+ * present in the expected fields.
+ */
+static unsigned char *grease_ch_buf;
+static size_t grease_ch_len;
+
+static int is_grease(unsigned int v)
+{
+ return (v & 0x0f0f) == 0x0a0a && (v >> 8) == (v & 0xff);
+}
+
+static void grease_msg_cb(int write_p, int version, int content_type,
+ const void *buf, size_t len, SSL *ssl, void *arg)
+{
+ const unsigned char *p = buf;
+
+ /*
+ * We want the outgoing (write_p == 1) handshake (content_type == 22)
+ * ClientHello (msg_type == 1). The buf starts at the handshake header:
+ * byte 0: msg_type, bytes 1-3: length
+ */
+ if (write_p != 1 || content_type != SSL3_RT_HANDSHAKE
+ || len < SSL3_HM_HEADER_LENGTH
+ || p[0] != SSL3_MT_CLIENT_HELLO)
+ return;
+
+ /* Only capture the first ClientHello (not HRR retry) */
+ if (grease_ch_buf != NULL)
+ return;
+
+ grease_ch_buf = OPENSSL_memdup(buf, len);
+ grease_ch_len = len;
+}
+
+/*
+ * Parse a captured ClientHello (starting from handshake header) and check
+ * that it contains GREASE values in cipher suites, extensions, supported
+ * groups, key shares, and signature algorithms.
+ * Returns 1 on success, 0 on failure.
+ */
+static int check_grease_in_client_hello(void)
+{
+ PACKET pkt, ciphers, session, compression, exts, ext_data;
+ PACKET inner;
+ unsigned int ext_type = 0, val = 0;
+ int found_grease_cipher = 0;
+ int found_grease_ext = 0;
+ int found_grease_group = 0;
+ int found_grease_kshare = 0;
+ int found_grease_sigalg = 0;
+ int found_grease_version = 0;
+
+ memset(&pkt, 0, sizeof(pkt));
+ memset(&ciphers, 0, sizeof(ciphers));
+ memset(&session, 0, sizeof(session));
+ memset(&compression, 0, sizeof(compression));
+ memset(&exts, 0, sizeof(exts));
+ memset(&ext_data, 0, sizeof(ext_data));
+ memset(&inner, 0, sizeof(inner));
+
+ if (!TEST_ptr(grease_ch_buf)
+ || !TEST_true(PACKET_buf_init(&pkt, grease_ch_buf,
+ grease_ch_len))
+ /* Skip handshake message header */
+ || !TEST_true(PACKET_forward(&pkt, SSL3_HM_HEADER_LENGTH))
+ /* Skip client_version + random */
+ || !TEST_true(PACKET_forward(&pkt,
+ CLIENT_VERSION_LEN + SSL3_RANDOM_SIZE))
+ /* Skip session_id */
+ || !TEST_true(PACKET_get_length_prefixed_1(&pkt, &session))
+ /* Get cipher suites */
+ || !TEST_true(PACKET_get_length_prefixed_2(&pkt, &ciphers))
+ /* Skip compression */
+ || !TEST_true(PACKET_get_length_prefixed_1(&pkt, &compression))
+ /* Get extensions */
+ || !TEST_true(PACKET_as_length_prefixed_2(&pkt, &exts)))
+ return 0;
+
+ /* Scan cipher suites for GREASE */
+ while (PACKET_remaining(&ciphers) > 0) {
+ if (!TEST_true(PACKET_get_net_2(&ciphers, &val)))
+ return 0;
+ if (is_grease(val))
+ found_grease_cipher = 1;
+ }
+
+ /* Scan extensions */
+ while (PACKET_remaining(&exts) > 0) {
+ if (!TEST_true(PACKET_get_net_2(&exts, &ext_type))
+ || !TEST_true(PACKET_get_length_prefixed_2(&exts,
+ &ext_data)))
+ return 0;
+
+ if (is_grease(ext_type))
+ found_grease_ext++;
+
+ /* Check for GREASE inside supported_versions */
+ if (ext_type == TLSEXT_TYPE_supported_versions) {
+ if (!TEST_true(PACKET_get_length_prefixed_1(&ext_data,
+ &inner)))
+ return 0;
+ while (PACKET_remaining(&inner) > 0) {
+ if (!TEST_true(PACKET_get_net_2(&inner, &val)))
+ return 0;
+ if (is_grease(val))
+ found_grease_version = 1;
+ }
+ }
+
+ /* Check for GREASE inside supported_groups */
+ if (ext_type == TLSEXT_TYPE_supported_groups) {
+ if (!TEST_true(PACKET_get_length_prefixed_2(&ext_data,
+ &inner)))
+ return 0;
+ while (PACKET_remaining(&inner) > 0) {
+ if (!TEST_true(PACKET_get_net_2(&inner, &val)))
+ return 0;
+ if (is_grease(val))
+ found_grease_group = 1;
+ }
+ }
+
+ /* Check for GREASE inside key_share */
+ if (ext_type == TLSEXT_TYPE_key_share) {
+ PACKET ks_entry;
+
+ memset(&ks_entry, 0, sizeof(ks_entry));
+ if (!TEST_true(PACKET_get_length_prefixed_2(&ext_data,
+ &inner)))
+ return 0;
+ while (PACKET_remaining(&inner) > 0) {
+ if (!TEST_true(PACKET_get_net_2(&inner, &val))
+ || !TEST_true(PACKET_get_length_prefixed_2(
+ &inner, &ks_entry)))
+ return 0;
+ if (is_grease(val))
+ found_grease_kshare = 1;
+ }
+ }
+
+ /* Check for GREASE inside signature_algorithms */
+ if (ext_type == TLSEXT_TYPE_signature_algorithms) {
+ if (!TEST_true(PACKET_get_length_prefixed_2(&ext_data,
+ &inner)))
+ return 0;
+ while (PACKET_remaining(&inner) > 0) {
+ if (!TEST_true(PACKET_get_net_2(&inner, &val)))
+ return 0;
+ if (is_grease(val))
+ found_grease_sigalg = 1;
+ }
+ }
+ }
+
+ if (!TEST_true(found_grease_cipher))
+ return 0;
+ if (!TEST_int_eq(found_grease_ext, 2))
+ return 0;
+ if (!TEST_true(found_grease_version))
+ return 0;
+ if (!TEST_true(found_grease_group))
+ return 0;
+ if (!TEST_true(found_grease_kshare))
+ return 0;
+ if (!TEST_true(found_grease_sigalg))
+ return 0;
+
+ return 1;
+}
+
+static int test_grease(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *serverssl = NULL, *clientssl = NULL;
+ int testresult = 0;
+
+ grease_ch_buf = NULL;
+ grease_ch_len = 0;
+
+ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+ TLS_client_method(),
+ TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey)))
+ goto end;
+
+ SSL_CTX_set_options(cctx, SSL_OP_GREASE);
+
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+ &clientssl, NULL, NULL)))
+ goto end;
+
+ SSL_set_msg_callback(clientssl, grease_msg_cb);
+
+ /* A full handshake should succeed - server must tolerate GREASE */
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+
+ /* Now verify the captured ClientHello contains GREASE values */
+ if (!TEST_true(check_grease_in_client_hello()))
+ goto end;
+
+ testresult = 1;
+
+end:
+ OPENSSL_free(grease_ch_buf);
+ grease_ch_buf = NULL;
+ grease_ch_len = 0;
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+
+ return testresult;
+}
+#endif /* !defined(OSSL_NO_USABLE_TLS1_3) */
+
static int test_ssl_conf_flags(void)
{
SSL_CONF_CTX *cctx = NULL;
@@ -14655,6 +14876,9 @@ int setup_tests(void)
ADD_ALL_TESTS(test_ssl_set_groups_unsupported_keyshare, 2);
ADD_TEST(test_ssl_conf_flags);
ADD_ALL_TESTS(test_http_verbs, 3);
+#if !defined(OSSL_NO_USABLE_TLS1_3)
+ ADD_TEST(test_grease);
+#endif
return 1;
err: