Commit 96c76dc72b for openssl.org

commit 96c76dc72bb20a6c2b93dd91ac3cb4fbf0e15670
Author: sftcd <stephen.farrell@cs.tcd.ie>
Date:   Wed Apr 8 11:11:37 2026 +0100

    curl ECH+QUIC fix

    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Matt Caswell <matt@openssl.foundation>
    MergeDate: Sat Apr 11 18:29:37 2026
    (Merged from https://github.com/openssl/openssl/pull/30727)

diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
index 72e927ad27..15069492f2 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -937,6 +937,7 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr,
     EVP_PKEY_CTX *pctx = NULL;
     const char *label = NULL;
     unsigned char *p = NULL;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);

     if (for_hrr == 1) {
         label = OSSL_ECH_HRR_CONFIRM_STRING;
@@ -950,7 +951,7 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr,
 #endif
     memset(zeros, 0, EVP_MAX_MD_SIZE);
     /* We don't seem to have an hkdf-extract that's exposed by libcrypto */
-    pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+    pctx = EVP_PKEY_CTX_new_from_name(sctx->libctx, "HKDF", sctx->propq);
     if (pctx == NULL
         || EVP_PKEY_derive_init(pctx) != 1
         || EVP_PKEY_CTX_hkdf_mode(pctx,
diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c
index 1f5d79e021..dd05bf3702 100644
--- a/ssl/ech/ech_store.c
+++ b/ssl/ech/ech_store.c
@@ -1114,7 +1114,7 @@ int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry)
      * the BIO_f_buffer allows us to seek back to the start.
      */
     BIO_push(fbio, in);
-    if (!PEM_read_bio_PrivateKey(fbio, &priv, NULL, NULL)
+    if (!PEM_read_bio_PrivateKey_ex(fbio, &priv, NULL, NULL, es->libctx, es->propq)
         && BIO_seek(fbio, 0) < 0) {
         ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
         goto err;
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index cc6585493f..2f97850972 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -722,9 +722,16 @@ static int verify_extension(SSL_CONNECTION *s, unsigned int context,
         ENDPOINT role = ENDPOINT_BOTH;
         custom_ext_method *meth = NULL;

-        if ((context & SSL_EXT_CLIENT_HELLO) != 0)
+        if ((context & SSL_EXT_CLIENT_HELLO) != 0) {
+#ifndef OPENSSL_NO_ECH
+            if (s->ext.ech.attempted == 1 && s->ext.ech.ch_depth == 1)
+                role = ENDPOINT_CLIENT;
+            else
+                role = ENDPOINT_SERVER;
+#else
             role = ENDPOINT_SERVER;
-        else if ((context & SSL_EXT_TLS1_2_SERVER_HELLO) != 0)
+#endif
+        } else if ((context & SSL_EXT_TLS1_2_SERVER_HELLO) != 0)
             role = ENDPOINT_CLIENT;

         meth = custom_ext_find(meths, role, type, &offset);
@@ -812,8 +819,13 @@ int tls_collect_extensions(SSL_CONNECTION *s, PACKET *packet,
      * Initialise server side custom extensions. Client side is done during
      * construction of extensions for the ClientHello.
      */
+#ifndef OPENSSL_NO_ECH
+    if ((context & SSL_EXT_CLIENT_HELLO) != 0 && s->ext.ech.attempted == 0)
+        custom_ext_init(&s->cert->custext);
+#else
     if ((context & SSL_EXT_CLIENT_HELLO) != 0)
         custom_ext_init(&s->cert->custext);
+#endif

     num_exts = OSSL_NELEM(ext_defs) + (exts != NULL ? exts->meths_count : 0);
     raw_extensions = OPENSSL_calloc(num_exts, sizeof(*raw_extensions));
@@ -1072,10 +1084,15 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt,
     }

     /* Add custom extensions first */
-    if ((context & SSL_EXT_CLIENT_HELLO) != 0) {
+#ifndef OPENSSL_NO_ECH
+    if ((context & SSL_EXT_CLIENT_HELLO) != 0 && s->ext.ech.attempted == 0)
         /* On the server side with initialise during ClientHello parsing */
         custom_ext_init(&s->cert->custext);
-    }
+#else
+    if ((context & SSL_EXT_CLIENT_HELLO) != 0)
+        /* On the server side with initialise during ClientHello parsing */
+        custom_ext_init(&s->cert->custext);
+#endif
     if (!custom_ext_add(s, context, pkt, x, chainidx, max_version)) {
         /* SSLfatal() already called */
         return 0;
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index 3003466a02..36c698c163 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -1489,7 +1489,14 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pk
 #ifndef OPENSSL_NO_ECH
     /* same session ID is used for inner/outer when doing ECH */
     if (s->ext.ech.es != NULL) {
-        sess_id_len = sizeof(s->tmp_session_id);
+        if (s->version != TLS1_3_VERSION) {
+            SSLfatal(s, SSL_AD_PROTOCOL_VERSION, SSL_R_UNSUPPORTED_SSL_VERSION);
+            return CON_FUNC_ERROR;
+        }
+        if ((s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0)
+            sess_id_len = sizeof(s->tmp_session_id);
+        else
+            sess_id_len = 0;
     } else {
 #endif
         if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) {
diff --git a/test/quicapitest.c b/test/quicapitest.c
index ad2c8ca2a9..84a5c9566f 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -22,6 +22,7 @@
 #include "internal/quic_error.h"

 static OSSL_LIB_CTX *libctx = NULL;
+static char *propq = NULL;
 static OSSL_PROVIDER *defctxnull = NULL;
 static char *certsdir = NULL;
 static char *cert = NULL;
@@ -3424,6 +3425,98 @@ static int test_quic_peer_addr_v6(void)
         "::2", 4434);
 }

+/* Test ECH with quic */
+static int test_ech(void)
+{
+    /*
+     * Don't try this test if various ECC things are set of unavailable
+     * or we're in a no-ech build
+     */
+#if defined(OPENSSL_NO_EC) || defined(OPENSSL_NO_ECX) || defined(OPENSSL_NO_ECH)
+    propq = NULL; /* avoid unused var warning */
+    return 1;
+#else
+    SSL_CTX *cctx = NULL, *sctx = NULL;
+    SSL *clientquic = NULL;
+    char *rinner = NULL, *router = NULL;
+    const char *inner = "inner.example.com";
+    QUIC_TSERVER *qtserv = NULL;
+    int testresult = 0;
+    /* p256 ech key pair with public name server.example */
+    const char echpem[] = "-----BEGIN PRIVATE KEY-----\n"
+                          "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Ygt9nhASeoYbzo2\n"
+                          "Nz/jGFAdeTo25SVYWQvnf86qzbahRANCAARS9QqkjJU311J7kS8LsyISJ8xYFbJ5\n"
+                          "5BX/pu4QiFXJ3dEGrjYh4PDH/ehFfaqZgtRRg2r/AP+vwkLiP2mqCfdv\n"
+                          "-----END PRIVATE KEY-----\n"
+                          "-----BEGIN ECHCONFIG-----\n"
+                          "AGL+DQBezwAQAEEEUvUKpIyVN9dSe5EvC7MiEifMWBWyeeQV/6buEIhVyd3RBq42\n"
+                          "IeDwx/3oRX2qmYLUUYNq/wD/r8JC4j9pqgn3bwAEAAEAAQAOc2VydmVyLmV4YW1w\n"
+                          "bGUAAA==\n"
+                          "-----END ECHCONFIG-----\n";
+    const char ec_pub[] = "AGL+DQBezwAQAEEEUvUKpIyVN9dSe5EvC7MiEifMWBWyeeQV/6buEIhVyd3RBq42"
+                          "IeDwx/3oRX2qmYLUUYNq/wD/r8JC4j9pqgn3bwAEAAEAAQAOc2VydmVyLmV4YW1w"
+                          "bGUAAA==";
+    size_t ec_publen = sizeof(ec_pub) - 1;
+    BIO *in = NULL;
+    OSSL_ECHSTORE *es = NULL;
+
+    /* HPKE and FIPS are not friends, so don't test in that case */
+    if (is_fips) {
+        TEST_info("No real ECH test as is_fips is set\n");
+        return 1;
+    } else {
+        TEST_info("Doing real ECH test as is_fips is not set\n");
+    }
+
+    /* make an OSSL_ECHSTORE for echpem */
+    if ((in = BIO_new(BIO_s_mem())) == NULL
+        || BIO_write(in, echpem, (int)strlen(echpem)) <= 0
+        || !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq))
+        || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY)))
+        goto err;
+
+    cctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_client_method());
+    sctx = SSL_CTX_new_ex(libctx, NULL, TLS_method());
+    /* set OSSL_ECHSTORE for server */
+    if (!TEST_ptr(sctx) || !TEST_true(SSL_CTX_set1_echstore(sctx, es)))
+        goto err;
+
+    if (!TEST_ptr(cctx)
+        || !TEST_true(qtest_create_quic_objects(libctx, cctx, sctx, cert,
+            privkey,
+            QTEST_FLAG_FAKE_TIME,
+            &qtserv,
+            &clientquic, NULL, NULL)))
+        goto err;
+
+    /* set echconfig for client */
+    if (!TEST_true(SSL_set1_ech_config_list(clientquic,
+            (unsigned char *)ec_pub, ec_publen))
+        || !TEST_true(SSL_set_tlsext_host_name(clientquic, inner)))
+        goto err;
+    /* we expect the connection to succeed */
+    if (!TEST_true(qtest_create_quic_connection(qtserv, clientquic)))
+        goto err;
+    SSL_set_verify_result(clientquic, X509_V_OK);
+    if (!TEST_int_eq(SSL_ech_get1_status(clientquic, &rinner, &router),
+            SSL_ECH_STATUS_SUCCESS))
+        goto err;
+
+    testresult = 1;
+err:
+    ossl_quic_tserver_free(qtserv);
+    SSL_free(clientquic);
+    OPENSSL_free(router);
+    OPENSSL_free(rinner);
+    SSL_CTX_free(cctx);
+    SSL_CTX_free(sctx);
+    OSSL_ECHSTORE_free(es);
+    BIO_free_all(in);
+
+    return testresult;
+#endif
+}
+
 /***********************************************************************************/
 OPT_TEST_DECLARE_USAGE("provider config certsdir datadir\n")

@@ -3531,6 +3624,7 @@ int setup_tests(void)
     ADD_TEST(test_client_hello_retry);
     ADD_TEST(test_quic_peer_addr_v6);
     ADD_TEST(test_quic_peer_addr_v4);
+    ADD_TEST(test_ech);

     return 1;
 err: