Commit c36a9b463e for openssl.org
commit c36a9b463e765001c2d558bb73f06b0bf8612158
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Fri Jun 19 18:52:37 2026 +0200
statem: add direct tests for tls_construct_client_hello
Add a new test which calls tls_construct_client_hello() directly,
creating an SSL_CONNECTION enough to invoke the construct function
without making a full handshake, then verifies the produced
ClientHello structurally and by round-tripping it through the
server-side tls_process_client_hello().
The covered branches include TLS 1.3 (with and without middlebox
compat), TLS 1.2, DTLS, session resumption, HelloRetryRequest, the DTLS
cookie and client_random reuse paths, the deterministic error branches
(WPACKET overflow and no usable ciphers), and the ECH wrapper path
(happy path with server-side decryption, the TLS 1.2 version error, and
OOM). Out-of-memory branches are exercised with the mfail tests.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
MergeDate: Tue Jun 30 08:54:48 2026
(Merged from https://github.com/openssl/openssl/pull/31618)
diff --git a/test/build.info b/test/build.info
index 20aec5eb3e..ac51116e56 100644
--- a/test/build.info
+++ b/test/build.info
@@ -1167,6 +1167,11 @@ IF[{- !$disabled{tests} -}]
INCLUDE[ext_internal_test]=.. ../include ../apps/include
DEPEND[ext_internal_test]=../libcrypto.a ../libssl.a libtestutil.a
+ PROGRAMS{noinst}=statem_clnt_construct_test
+ SOURCE[statem_clnt_construct_test]=statem_clnt_construct_test.c
+ INCLUDE[statem_clnt_construct_test]=.. ../include ../apps/include
+ DEPEND[statem_clnt_construct_test]=../libcrypto.a ../libssl.a libtestutil.a
+
PROGRAMS{noinst}=algorithmid_test
SOURCE[algorithmid_test]=algorithmid_test.c
INCLUDE[algorithmid_test]=../include ../apps/include
diff --git a/test/recipes/25-test_statem_clnt.t b/test/recipes/25-test_statem_clnt.t
new file mode 100644
index 0000000000..4352029eed
--- /dev/null
+++ b/test/recipes/25-test_statem_clnt.t
@@ -0,0 +1,19 @@
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use OpenSSL::Test;
+use OpenSSL::Test::Utils;
+
+setup("test_statem_clnt");
+
+plan skip_all => "No TLS protocol enabled in this build"
+ if disabled("tls1_2") && disabled("tls1_3");
+
+plan tests => 1;
+
+ok(run(test(["statem_clnt_construct_test"])), "statem_clnt construct functions");
diff --git a/test/statem_clnt_construct_test.c b/test/statem_clnt_construct_test.c
new file mode 100644
index 0000000000..43dc330dec
--- /dev/null
+++ b/test/statem_clnt_construct_test.c
@@ -0,0 +1,734 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+/*
+ * Direct tests for tls_construct_client_hello(): prime a client SSL_CONNECTION
+ * enough to call the construct function without a full handshake, then check
+ * the result structurally and by round-tripping it through the server parser.
+ * OOM branches are covered with mfail tests.
+ */
+
+#include <openssl/ssl.h>
+#ifndef OPENSSL_NO_ECH
+#include <openssl/ech.h>
+#include <openssl/hpke.h>
+#endif
+
+#include "internal/ssl_unwrap.h"
+#include "../ssl/ssl_local.h"
+#include "../ssl/statem/statem_local.h"
+#include "testutil.h"
+
+/*
+ * TLS 1.3 needs a key-share group, so it is unusable when both EC and DH are
+ * disabled even though the protocol itself is compiled in (e.g. no-bulk).
+ */
+#if defined(OPENSSL_NO_TLS1_3) \
+ || (defined(OPENSSL_NO_EC) && defined(OPENSSL_NO_DH))
+#define OSSL_NO_USABLE_TLS1_3
+#endif
+
+/* ECH needs a curve (EC/ECX) for its default suite and usable TLS 1.3. */
+#if defined(OPENSSL_NO_ECH) || defined(OPENSSL_NO_EC) \
+ || defined(OPENSSL_NO_ECX) || defined(OSSL_NO_USABLE_TLS1_3)
+#define OSSL_NO_USABLE_ECH
+#endif
+
+/*
+ * Helpers down to prime_ssl() are generic and reusable by tests for any
+ * statem_clnt construct function; the ClientHello-specific code follows.
+ */
+
+/* Connection configuration shared by the construct tests. */
+typedef struct {
+ int is_dtls;
+ int min_ver; /* 0 for library default */
+ int max_ver; /* 0 for library default */
+ int clear_midbox; /* clear SSL_OP_ENABLE_MIDDLEBOX_COMPAT */
+} CH_CONFIG;
+
+static const SSL_METHOD *client_method(const CH_CONFIG *cfg)
+{
+ return cfg->is_dtls ? DTLS_client_method() : TLS_client_method();
+}
+
+/* Handshake message header length (msg precedes the ClientHello body). */
+static size_t hdr_len(const CH_CONFIG *cfg)
+{
+ return cfg->is_dtls ? DTLS1_HM_HEADER_LENGTH : SSL3_HM_HEADER_LENGTH;
+}
+
+static const SSL_METHOD *server_method(const CH_CONFIG *cfg)
+{
+ return cfg->is_dtls ? DTLS_server_method() : TLS_server_method();
+}
+
+static SSL_CTX *new_ctx(const CH_CONFIG *cfg, const SSL_METHOD *meth)
+{
+ SSL_CTX *ctx = SSL_CTX_new(meth);
+
+ if (ctx == NULL)
+ return NULL;
+ if ((cfg->min_ver != 0
+ && !SSL_CTX_set_min_proto_version(ctx, cfg->min_ver))
+ || (cfg->max_ver != 0
+ && !SSL_CTX_set_max_proto_version(ctx, cfg->max_ver))) {
+ SSL_CTX_free(ctx);
+ return NULL;
+ }
+ return ctx;
+}
+
+/*
+ * Set up the init_buf and handshake state the write state machine would have
+ * established before calling a construct function. For the client a WPACKET
+ * with the handshake header is emitted into init_buf. initbuf_len of 0 means
+ * full size; a small value exercises WPACKET failures.
+ */
+static int prime_ssl(SSL *ssl, int is_client, size_t initbuf_len, WPACKET *pkt)
+{
+ SSL_CONNECTION *s = SSL_CONNECTION_FROM_SSL(ssl);
+
+ if (!TEST_ptr(s))
+ return 0;
+
+ if (is_client)
+ SSL_set_connect_state(ssl);
+ else
+ SSL_set_accept_state(ssl);
+
+ if (initbuf_len == 0)
+ initbuf_len = SSL3_RT_MAX_PLAIN_LENGTH;
+
+ if (!TEST_ptr(s->init_buf = BUF_MEM_new())
+ || !TEST_true(BUF_MEM_grow(s->init_buf, initbuf_len)))
+ return 0;
+
+ if (!TEST_true(tls_setup_handshake(s)))
+ return 0;
+
+ if (pkt != NULL
+ && (!TEST_true(WPACKET_init(pkt, s->init_buf))
+ || !TEST_true(ssl_set_handshake_header(s, pkt,
+ SSL3_MT_CLIENT_HELLO))))
+ return 0;
+
+ return 1;
+}
+
+/*
+ * ===========================================================================
+ * tls_construct_client_hello
+ * ===========================================================================
+ */
+
+/* Finalize the constructed message and return its bytes (header + body). */
+static int finish_ch(SSL *ssl, WPACKET *pkt, unsigned char **msg,
+ size_t *msglen)
+{
+ SSL_CONNECTION *s = SSL_CONNECTION_FROM_SSL(ssl);
+
+ if (!TEST_true(ssl_close_construct_packet(s, pkt, SSL3_MT_CLIENT_HELLO))
+ || !TEST_true(WPACKET_get_total_written(pkt, msglen))
+ || !TEST_true(WPACKET_finish(pkt)))
+ return 0;
+
+ *msg = (unsigned char *)s->init_buf->data;
+ return 1;
+}
+
+/* Recover the session_id length, the main branching difference in construct. */
+static int get_ch_sessid_len(const CH_CONFIG *cfg, const unsigned char *msg,
+ size_t msglen, size_t *sidlen)
+{
+ PACKET pkt = { 0 }, sessid = { 0 }, cookie = { 0 };
+ PACKET ciphers = { 0 }, comp = { 0 };
+ unsigned int legacy_version;
+ size_t hl = hdr_len(cfg);
+
+ if (!TEST_size_t_gt(msglen, hl)
+ || !TEST_true(PACKET_buf_init(&pkt, msg + hl, msglen - hl)))
+ return 0;
+
+ if (!TEST_true(PACKET_get_net_2(&pkt, &legacy_version))
+ || !TEST_true(PACKET_forward(&pkt, SSL3_RANDOM_SIZE))
+ || !TEST_true(PACKET_get_length_prefixed_1(&pkt, &sessid)))
+ return 0;
+
+ if (cfg->is_dtls
+ && !TEST_true(PACKET_get_length_prefixed_1(&pkt, &cookie)))
+ return 0;
+
+ /* Sanity: cipher list non-empty, compression present and contains NULL. */
+ if (!TEST_true(PACKET_get_length_prefixed_2(&pkt, &ciphers))
+ || !TEST_size_t_gt(PACKET_remaining(&ciphers), 0)
+ || !TEST_true(PACKET_get_length_prefixed_1(&pkt, &comp))
+ || !TEST_size_t_gt(PACKET_remaining(&comp), 0))
+ return 0;
+
+ *sidlen = PACKET_remaining(&sessid);
+ return 1;
+}
+
+/* Run the produced ClientHello body through the server-side parser. */
+static int roundtrip_process_ch(const CH_CONFIG *cfg, const unsigned char *msg,
+ size_t msglen)
+{
+ SSL_CTX *sctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ PACKET pkt;
+ int ret = 0;
+
+ if (!TEST_ptr(sctx = new_ctx(cfg, server_method(cfg)))
+ || !TEST_ptr(ssl = SSL_new(sctx)))
+ goto err;
+
+ if (!prime_ssl(ssl, 0, 0, NULL))
+ goto err;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+
+ if (!TEST_true(PACKET_buf_init(&pkt, msg + hdr_len(cfg),
+ msglen - hdr_len(cfg))))
+ goto err;
+
+ if (!TEST_int_eq(tls_process_client_hello(s, &pkt),
+ MSG_PROCESS_CONTINUE_PROCESSING))
+ goto err;
+
+ ret = 1;
+err:
+ SSL_free(ssl);
+ SSL_CTX_free(sctx);
+ return ret;
+}
+
+/*
+ * Construct a ClientHello, assert its session_id length and optionally
+ * round-trip it. prep injects extra state just before construct;
+ * expect_random asserts the produced client random (to check reuse).
+ * Both may be NULL.
+ */
+static int do_construct_ch(const CH_CONFIG *cfg,
+ int (*prep)(SSL_CONNECTION *s), size_t expect_sidlen, int roundtrip,
+ const unsigned char *expect_random)
+{
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ unsigned char *msg = NULL;
+ size_t msglen = 0, sidlen = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(cfg, client_method(cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx)))
+ goto err;
+ if (cfg->clear_midbox)
+ SSL_clear_options(ssl, SSL_OP_ENABLE_MIDDLEBOX_COMPAT);
+
+ if (!prime_ssl(ssl, 1, 0, &pkt))
+ goto err;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+
+ if (prep != NULL && !prep(s)) {
+ WPACKET_cleanup(&pkt);
+ goto err;
+ }
+
+ if (!TEST_int_eq(tls_construct_client_hello(s, &pkt), CON_FUNC_SUCCESS)) {
+ WPACKET_cleanup(&pkt);
+ goto err;
+ }
+ if (!finish_ch(ssl, &pkt, &msg, &msglen))
+ goto err;
+
+ if (!get_ch_sessid_len(cfg, msg, msglen, &sidlen)
+ || !TEST_size_t_eq(sidlen, expect_sidlen))
+ goto err;
+
+ /* The client random follows the 2-byte legacy_version in the body. */
+ if (expect_random != NULL
+ && !TEST_mem_eq(msg + hdr_len(cfg) + 2, SSL3_RANDOM_SIZE,
+ expect_random, SSL3_RANDOM_SIZE))
+ goto err;
+
+ if (roundtrip && !roundtrip_process_ch(cfg, msg, msglen))
+ goto err;
+
+ ret = 1;
+err:
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+
+/* Expect tls_construct_client_hello() to fail (CON_FUNC_ERROR). */
+static int do_construct_ch_expect_fail(const CH_CONFIG *cfg,
+ int (*prep)(SSL_CONNECTION *s),
+ size_t initbuf_len, int empty_ciphers)
+{
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ int have_pkt = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(cfg, client_method(cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx)))
+ goto err;
+
+ if (empty_ciphers) {
+ /* Leave no usable cipher: aNULL is disabled at default sec level. */
+ int r1 = SSL_set_ciphersuites(ssl, "");
+ int r2 = SSL_set_cipher_list(ssl, "aNULL");
+
+ (void)r1;
+ (void)r2;
+ }
+
+ if (!prime_ssl(ssl, 1, initbuf_len, &pkt))
+ goto err;
+ have_pkt = 1;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+
+ if (prep != NULL && !prep(s))
+ goto err;
+
+ if (!TEST_int_eq(tls_construct_client_hello(s, &pkt), CON_FUNC_ERROR))
+ goto err;
+
+ ret = 1;
+err:
+ if (have_pkt)
+ WPACKET_cleanup(&pkt);
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+
+/* TLS 1.2 happy-path / session tests */
+
+#ifndef OPENSSL_NO_TLS1_2
+static int test_construct_ch_tls12(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_2_VERSION, TLS1_2_VERSION, 0 };
+
+ /* Fresh (non-resumable) TLS 1.2 session: empty session id. */
+ return do_construct_ch(&cfg, NULL, 0, 1, NULL);
+}
+
+/* Resumable non-TLS1.3 session: construct reuses its session id. */
+static int prep_resume(SSL_CONNECTION *s)
+{
+ SSL *ssl = SSL_CONNECTION_GET_SSL(s);
+ SSL_SESSION *sess = SSL_SESSION_new();
+ int ret = 0;
+
+ if (!TEST_ptr(sess))
+ goto err;
+ sess->ssl_version = TLS1_2_VERSION;
+ sess->session_id_length = sizeof(sess->session_id);
+ memset(sess->session_id, 0x5A, sess->session_id_length);
+ sess->cipher = sk_SSL_CIPHER_value(SSL_get_ciphers(ssl), 0);
+ if (!TEST_ptr(sess->cipher) || !TEST_true(SSL_set_session(ssl, sess)))
+ goto err;
+ ret = 1;
+err:
+ SSL_SESSION_free(sess);
+ return ret;
+}
+
+static int test_construct_ch_resume(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_2_VERSION, TLS1_2_VERSION, 0 };
+
+ /* Resumed session: the pre-loaded 32-byte session id is sent. */
+ return do_construct_ch(&cfg, prep_resume, SSL_MAX_SSL_SESSION_ID_LENGTH, 1,
+ NULL);
+}
+#endif /* OPENSSL_NO_TLS1_2 */
+
+/* TLS 1.3 happy-path / HRR tests */
+
+#ifndef OSSL_NO_USABLE_TLS1_3
+static int test_construct_ch_tls13(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_3_VERSION, TLS1_3_VERSION, 0 };
+
+ /* Middlebox compat is on by default: a random 32-byte session id. */
+ return do_construct_ch(&cfg, NULL, SSL_MAX_SSL_SESSION_ID_LENGTH, 1, NULL);
+}
+
+static int test_construct_ch_tls13_no_middlebox(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_3_VERSION, TLS1_3_VERSION, 1 };
+
+ /* No middlebox compat: empty session id. */
+ return do_construct_ch(&cfg, NULL, 0, 1, NULL);
+}
+
+static int prep_hrr(SSL_CONNECTION *s)
+{
+ SSL *ssl = SSL_CONNECTION_GET_SSL(s);
+ SSL_SESSION *sess = SSL_SESSION_new();
+ int ret = 0;
+
+ /* Under HRR construct skips ssl_get_new_session(), so a session must
+ * already exist (created for the first ClientHello). */
+ if (!TEST_ptr(sess))
+ goto err;
+ sess->ssl_version = TLS1_3_VERSION;
+ sess->cipher = sk_SSL_CIPHER_value(SSL_get_ciphers(ssl), 0);
+ if (!TEST_ptr(sess->cipher) || !TEST_true(SSL_set_session(ssl, sess)))
+ goto err;
+ s->hello_retry_request = SSL_HRR_COMPLETE;
+ ret = 1;
+err:
+ SSL_SESSION_free(sess);
+ return ret;
+}
+
+static int test_construct_ch_hrr(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_3_VERSION, TLS1_3_VERSION, 0 };
+
+ /* TLS 1.3 + middlebox compat still emits a 32-byte session id under HRR. */
+ return do_construct_ch(&cfg, prep_hrr, SSL_MAX_SSL_SESSION_ID_LENGTH, 1,
+ NULL);
+}
+#endif /* OSSL_NO_USABLE_TLS1_3 */
+
+/* DTLS happy-path / cookie / random-reuse tests */
+
+#ifndef OPENSSL_NO_DTLS
+static int test_construct_ch_dtls(void)
+{
+ CH_CONFIG cfg = { 1, 0, 0, 0 };
+
+ /* DTLS uses a different server parser path; skip the roundtrip. */
+ return do_construct_ch(&cfg, NULL, 0, 0, NULL);
+}
+
+static int prep_dtls_cookie(SSL_CONNECTION *s)
+{
+ /* A HelloVerifyRequest cookie is echoed in the ClientHello. */
+ static const unsigned char cookie[16] = {
+ 0xc0, 0x01, 0xc0, 0x02, 0xc0, 0x03, 0xc0, 0x04,
+ 0xc0, 0x05, 0xc0, 0x06, 0xc0, 0x07, 0xc0, 0x08
+ };
+
+ memcpy(s->d1->cookie, cookie, sizeof(cookie));
+ s->d1->cookie_len = sizeof(cookie);
+ return 1;
+}
+
+static int test_construct_ch_dtls_cookie(void)
+{
+ CH_CONFIG cfg = { 1, 0, 0, 0 };
+
+ return do_construct_ch(&cfg, prep_dtls_cookie, 0, 0, NULL);
+}
+
+/* A recognizable, all-non-zero client random to detect reuse. */
+static const unsigned char reused_random[SSL3_RANDOM_SIZE] = {
+ 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
+ 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
+ 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
+ 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf
+};
+
+static int prep_dtls_client_random(SSL_CONNECTION *s)
+{
+ /* DTLS reuses an already-set client random (HelloVerifyRequest reply). */
+ memcpy(s->s3.client_random, reused_random, sizeof(reused_random));
+ return 1;
+}
+
+static int test_construct_ch_dtls_client_random(void)
+{
+ CH_CONFIG cfg = { 1, 0, 0, 0 };
+
+ return do_construct_ch(&cfg, prep_dtls_client_random, 0, 0, reused_random);
+}
+#endif /* OPENSSL_NO_DTLS */
+
+/* Deterministic error-branch tests */
+
+static int test_construct_ch_small_buf(void)
+{
+ /* Default version: the overflow is version-independent. */
+ CH_CONFIG cfg = { 0, 0, 0, 0 };
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ /*
+ * Fixed, non-growable buffer that overflows part-way through the body (a
+ * BUF_MEM-backed WPACKET would just grow), hitting a WPACKET write failure.
+ */
+ unsigned char buf[40];
+ int have_pkt = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(&cfg, client_method(&cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx)))
+ goto err;
+ SSL_set_connect_state(ssl);
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+ if (!TEST_ptr(s)
+ || !TEST_ptr(s->init_buf = BUF_MEM_new())
+ || !TEST_true(BUF_MEM_grow(s->init_buf, SSL3_RT_MAX_PLAIN_LENGTH))
+ || !TEST_true(tls_setup_handshake(s)))
+ goto err;
+
+ if (!TEST_true(WPACKET_init_static_len(&pkt, buf, sizeof(buf), 0)))
+ goto err;
+ have_pkt = 1;
+ if (!TEST_true(ssl_set_handshake_header(s, &pkt, SSL3_MT_CLIENT_HELLO)))
+ goto err;
+
+ if (!TEST_int_eq(tls_construct_client_hello(s, &pkt), CON_FUNC_ERROR))
+ goto err;
+
+ ret = 1;
+err:
+ if (have_pkt)
+ WPACKET_cleanup(&pkt);
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+
+static int test_construct_ch_no_ciphers(void)
+{
+ CH_CONFIG cfg = { 0, 0, 0, 0 };
+
+ return do_construct_ch_expect_fail(&cfg, NULL, 0, 1);
+}
+
+/* Allocation-failure (mfail) tests */
+
+/* Compiled when any mfail caller below (TLS 1.2, TLS 1.3 or ECH) is. */
+#if !defined(OSSL_NO_USABLE_TLS1_3) || !defined(OPENSSL_NO_TLS1_2)
+static int mfail_construct_ch_common(const CH_CONFIG *cfg,
+ int (*prep)(SSL_CONNECTION *s))
+{
+ SSL_CTX *cctx = NULL;
+ SSL *ssl = NULL;
+ SSL_CONNECTION *s;
+ WPACKET pkt;
+ int ok = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(cctx = new_ctx(cfg, client_method(cfg)))
+ || !TEST_ptr(ssl = SSL_new(cctx)))
+ goto err;
+ if (cfg->clear_midbox)
+ SSL_clear_options(ssl, SSL_OP_ENABLE_MIDDLEBOX_COMPAT);
+
+ if (!prime_ssl(ssl, 1, 0, &pkt))
+ goto err;
+ s = SSL_CONNECTION_FROM_SSL(ssl);
+
+ if (prep != NULL && !prep(s)) {
+ WPACKET_cleanup(&pkt);
+ goto err;
+ }
+
+ MFAIL_start();
+ ok = (tls_construct_client_hello(s, &pkt) == CON_FUNC_SUCCESS);
+ MFAIL_end();
+
+ WPACKET_cleanup(&pkt);
+
+ /* 1 on clean success, 0 on an injected allocation failure. */
+ ret = ok ? 1 : 0;
+err:
+ SSL_free(ssl);
+ SSL_CTX_free(cctx);
+ return ret;
+}
+#endif
+
+#if !defined(OSSL_NO_USABLE_TLS1_3) && !defined(OPENSSL_NO_CACHED_FETCH)
+static int mfail_construct_ch_tls13(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_3_VERSION, TLS1_3_VERSION, 0 };
+
+ return mfail_construct_ch_common(&cfg, NULL);
+}
+#endif
+
+#ifndef OPENSSL_NO_TLS1_2
+static int mfail_construct_ch_tls12(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_2_VERSION, TLS1_2_VERSION, 0 };
+
+ return mfail_construct_ch_common(&cfg, NULL);
+}
+#endif
+
+#ifndef OSSL_NO_USABLE_ECH
+/* ECH path tests */
+
+/* Attach an ECH config so construct takes the ECH wrapper path. */
+static int prep_ech(SSL_CONNECTION *s)
+{
+ SSL *ssl = SSL_CONNECTION_GET_SSL(s);
+ OSSL_ECHSTORE *es = NULL;
+ OSSL_HPKE_SUITE suite = OSSL_HPKE_SUITE_DEFAULT;
+ int ret = 0;
+
+ if (!TEST_ptr(es = OSSL_ECHSTORE_new(NULL, NULL))
+ || !TEST_true(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, 0,
+ "example.com", suite))
+ || !TEST_true(SSL_set1_echstore(ssl, es)))
+ goto err;
+ ret = 1;
+err:
+ OSSL_ECHSTORE_free(es);
+ return ret;
+}
+
+/*
+ * ECH happy path: the server reuses the client's store, which holds the private
+ * key needed to decrypt the inner ClientHello, so the round-trip can succeed.
+ */
+static int test_construct_ch_ech(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_3_VERSION, TLS1_3_VERSION, 0 };
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *cssl = NULL, *sssl = NULL;
+ SSL_CONNECTION *cs, *ss;
+ OSSL_ECHSTORE *es = NULL;
+ OSSL_HPKE_SUITE suite = OSSL_HPKE_SUITE_DEFAULT;
+ WPACKET pkt;
+ PACKET rpkt;
+ unsigned char *msg = NULL;
+ size_t msglen = 0, sidlen = 0;
+ int ret = 0;
+
+ if (!TEST_ptr(es = OSSL_ECHSTORE_new(NULL, NULL))
+ || !TEST_true(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, 0,
+ "example.com", suite)))
+ goto err;
+
+ /* Client: construct the outer ClientHello with ECH. */
+ if (!TEST_ptr(cctx = new_ctx(&cfg, client_method(&cfg)))
+ || !TEST_ptr(cssl = SSL_new(cctx))
+ || !TEST_true(SSL_set1_echstore(cssl, es))
+ || !prime_ssl(cssl, 1, 0, &pkt))
+ goto err;
+ cs = SSL_CONNECTION_FROM_SSL(cssl);
+ if (!TEST_int_eq(tls_construct_client_hello(cs, &pkt), CON_FUNC_SUCCESS)) {
+ WPACKET_cleanup(&pkt);
+ goto err;
+ }
+ if (!finish_ch(cssl, &pkt, &msg, &msglen)
+ || !get_ch_sessid_len(&cfg, msg, msglen, &sidlen)
+ || !TEST_size_t_eq(sidlen, SSL_MAX_SSL_SESSION_ID_LENGTH))
+ goto err;
+
+ /* Server: decrypt and process the outer using the same store. */
+ if (!TEST_ptr(sctx = new_ctx(&cfg, server_method(&cfg)))
+ || !TEST_ptr(sssl = SSL_new(sctx))
+ || !TEST_true(SSL_set1_echstore(sssl, es))
+ || !prime_ssl(sssl, 0, 0, NULL))
+ goto err;
+ ss = SSL_CONNECTION_FROM_SSL(sssl);
+ if (!TEST_true(PACKET_buf_init(&rpkt, msg + hdr_len(&cfg),
+ msglen - hdr_len(&cfg)))
+ || !TEST_int_eq(tls_process_client_hello(ss, &rpkt),
+ MSG_PROCESS_CONTINUE_PROCESSING))
+ goto err;
+
+ ret = 1;
+err:
+ OSSL_ECHSTORE_free(es);
+ SSL_free(cssl);
+ SSL_free(sssl);
+ SSL_CTX_free(cctx);
+ SSL_CTX_free(sctx);
+ return ret;
+}
+
+#ifndef OPENSSL_NO_TLS1_2
+static int test_construct_ch_ech_tls12(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_2_VERSION, TLS1_2_VERSION, 0 };
+
+ /* ECH requires TLS 1.3: the inner construct fails the version check. */
+ return do_construct_ch_expect_fail(&cfg, prep_ech, 0, 0);
+}
+#endif /* OPENSSL_NO_TLS1_2 */
+
+#ifndef OPENSSL_NO_CACHED_FETCH
+static int mfail_construct_ch_ech(void)
+{
+ CH_CONFIG cfg = { 0, TLS1_3_VERSION, TLS1_3_VERSION, 0 };
+
+ return mfail_construct_ch_common(&cfg, prep_ech);
+}
+#endif /* OPENSSL_NO_CACHED_FETCH */
+#endif /* OSSL_NO_USABLE_ECH */
+
+int setup_tests(void)
+{
+ ADD_TEST(test_construct_ch_small_buf);
+ ADD_TEST(test_construct_ch_no_ciphers);
+
+#ifndef OPENSSL_NO_TLS1_2
+ ADD_TEST(test_construct_ch_tls12);
+ ADD_TEST(test_construct_ch_resume);
+ ADD_MFAIL_TEST(mfail_construct_ch_tls12);
+#endif
+
+#ifndef OSSL_NO_USABLE_TLS1_3
+ ADD_TEST(test_construct_ch_tls13);
+ ADD_TEST(test_construct_ch_tls13_no_middlebox);
+ ADD_TEST(test_construct_ch_hrr);
+#ifndef OPENSSL_NO_CACHED_FETCH
+ /*
+ * The non-cached mfail run takes too long and does not test too much extra
+ * so better to skip it.
+ */
+#if defined(OPENSSL_NO_ECX)
+ /*
+ * Without ECX the key_share falls back to EC keygen, which makes a
+ * best-effort param-cache allocation whose failure does not propagate;
+ * only crash/leak checking is meaningful then.
+ *
+ * No caching also needs no check.
+ */
+ ADD_MFAIL_NO_CHECK_TEST(mfail_construct_ch_tls13);
+#else
+ ADD_MFAIL_TEST(mfail_construct_ch_tls13);
+#endif /* OPENSSL_NO_ECX */
+#endif /* OPENSSL_NO_CACHED_FETCH */
+#endif /* OSSL_NO_USABLE_TLS1_3 */
+
+#ifndef OPENSSL_NO_DTLS
+ ADD_TEST(test_construct_ch_dtls);
+ ADD_TEST(test_construct_ch_dtls_cookie);
+ ADD_TEST(test_construct_ch_dtls_client_random);
+#endif
+
+#ifndef OSSL_NO_USABLE_ECH
+ ADD_TEST(test_construct_ch_ech);
+#ifndef OPENSSL_NO_TLS1_2
+ ADD_TEST(test_construct_ch_ech_tls12);
+#endif
+#ifndef OPENSSL_NO_CACHED_FETCH
+ ADD_MFAIL_TEST(mfail_construct_ch_ech);
+#endif /* OPENSSL_NO_CACHED_FETCH */
+#endif /* OSSL_NO_USABLE_ECH */
+ return 1;
+}