]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
s_client and s_server options for ECH
authorsftcd <stephen.farrell@cs.tcd.ie>
Thu, 14 Aug 2025 18:17:07 +0000 (19:17 +0100)
committerTomas Mraz <tomas@openssl.org>
Tue, 18 Nov 2025 08:34:30 +0000 (09:34 +0100)
Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/28270)

apps/s_client.c
apps/s_server.c
doc/man1/openssl-s_client.pod.in
doc/man1/openssl-s_server.pod.in
ssl/ech/ech_internal.c
ssl/ech/ech_ssl_apis.c
ssl/ech/ech_store.c
ssl/statem/extensions.c
ssl/statem/extensions_clnt.c
test/ech_test.c
test/recipes/82-test_ech_client_server.t [new file with mode: 0644]

index f9f11f642797ab4fe8a5b3dfa1ce7e3008fb38dd..9fcf533ed9d2cf0abfcf1c9aeed88fb0c60704e5 100644 (file)
@@ -108,7 +108,12 @@ static BIO *bio_c_out = NULL;
 static int c_quiet = 0;
 static char *sess_out = NULL;
 # ifndef OPENSSL_NO_ECH
-static char *ech_config_list = NULL;
+static char *ech_config_list = NULL, *ech_grease_suite = NULL;
+static const char *sni_outer_name = NULL;
+static int ech_grease = 0, ech_ignore_cid = 0;
+static int ech_select = OSSL_ECHSTORE_ALL;
+static int ech_grease_type = OSSL_ECH_CURRENT_VERSION;
+static int ech_no_outer_sni = 0;
 # endif
 static SSL_SESSION *psksess = NULL;
 
@@ -526,7 +531,10 @@ typedef enum OPTION_choice {
     OPT_SCTP_LABEL_BUG,
     OPT_KTLS,
 # ifndef OPENSSL_NO_ECH
-    OPT_ECHCONFIGLIST,
+    OPT_ECHCONFIGLIST, OPT_SNIOUTER, OPT_ALPN_OUTER,
+    OPT_ECH_SELECT, OPT_ECH_IGNORE_CONFIG_ID,
+    OPT_ECH_GREASE, OPT_ECH_GREASE_SUITE, OPT_ECH_GREASE_TYPE,
+    OPT_ECH_NO_OUTER_SNI,
 # endif
     OPT_R_ENUM, OPT_PROV_ENUM
 } OPTION_CHOICE;
@@ -728,14 +736,31 @@ const OPTIONS s_client_options[] = {
     {"enable_pha", OPT_ENABLE_PHA, '-', "Enable post-handshake-authentication"},
     {"enable_server_rpk", OPT_ENABLE_SERVER_RPK, '-', "Enable raw public keys (RFC7250) from the server"},
     {"enable_client_rpk", OPT_ENABLE_CLIENT_RPK, '-', "Enable raw public keys (RFC7250) from the client"},
-# ifndef OPENSSL_NO_ECH
-    {"ech_config_list", OPT_ECHCONFIGLIST, 's',
-     "Set ECHConfigList, value is base 64 encoded ECHConfigList"},
-# endif
 #ifndef OPENSSL_NO_SRTP
     {"use_srtp", OPT_USE_SRTP, 's',
      "Offer SRTP key management with a colon-separated profile list"},
 #endif
+
+# ifndef OPENSSL_NO_ECH
+    {"ech_config_list", OPT_ECHCONFIGLIST, 's',
+     "Set ECHConfigList, value is base64-encoded ECHConfigList"},
+    {"ech_outer_alpn", OPT_ALPN_OUTER, 's',
+     "Specify outer ALPN value, when using ECH (comma-separated list)"},
+    {"ech_outer_sni", OPT_SNIOUTER, 's',
+     "The name to put in the outer CH when overriding the server's choice"},
+    {"ech_no_outer_sni", OPT_ECH_NO_OUTER_SNI, '-',
+     "Do not send the server name (SNI) extension in the outer ClientHello"},
+    {"ech_select", OPT_ECH_SELECT, 'n',
+     "Select one ECHConfig from the set provided via -ech_config_list"},
+    {"ech_grease", OPT_ECH_GREASE, '-',
+     "Send GREASE values when not really using ECH"},
+    {"ech_grease_suite", OPT_ECH_GREASE_SUITE, 's',
+     "Use this HPKE suite for GREASE values when not really using ECH"},
+    {"ech_grease_type", OPT_ECH_GREASE_TYPE, 'n',
+     "Use this TLS extension type for GREASE values when not really using ECH"},
+    {"ech_ignore_cid", OPT_ECH_IGNORE_CONFIG_ID, '-',
+     "Ignore the server-chosen ECH config ID and send a random value"},
+# endif
 #ifndef OPENSSL_NO_SRP
     {"srpuser", OPT_SRPUSER, 's', "(deprecated) SRP authentication for 'user'"},
     {"srppass", OPT_SRPPASS, 's', "(deprecated) Password for 'user'"},
@@ -934,6 +959,11 @@ int s_client_main(int argc, char **argv)
     char *sname_alloc = NULL;
     int noservername = 0;
     const char *alpn_in = NULL;
+# ifndef OPENSSL_NO_ECH
+    const char *alpn_outer_in = NULL;
+    int rv = 0;
+    OSSL_ECHSTORE *es = NULL;
+# endif
     tlsextctx tlsextcbp = { NULL, 0 };
     const char *ssl_config = NULL;
 #define MAX_SI_TYPES 100
@@ -1541,6 +1571,30 @@ int s_client_main(int argc, char **argv)
         case OPT_ECHCONFIGLIST:
             ech_config_list = opt_arg();
             break;
+        case OPT_ALPN_OUTER:
+            alpn_outer_in = opt_arg();
+            break;
+        case OPT_SNIOUTER:
+            sni_outer_name = opt_arg();
+            break;
+        case OPT_ECH_SELECT:
+            ech_select = atoi(opt_arg());
+            break;
+        case OPT_ECH_GREASE:
+            ech_grease = 1;
+            break;
+        case OPT_ECH_GREASE_SUITE:
+            ech_grease_suite = opt_arg();
+            break;
+        case OPT_ECH_GREASE_TYPE:
+            ech_grease_type = atoi(opt_arg());
+            break;
+        case OPT_ECH_IGNORE_CONFIG_ID:
+            ech_ignore_cid = 1;
+            break;
+        case OPT_ECH_NO_OUTER_SNI:
+            ech_no_outer_sni = 1;
+            break;
 # endif
         case OPT_NOSERVERNAME:
             noservername = 1;
@@ -1654,7 +1708,16 @@ int s_client_main(int argc, char **argv)
             goto opthelp;
         }
     }
-
+# ifndef OPENSSL_NO_ECH
+    if ((alpn_outer_in != NULL || sni_outer_name != NULL
+         || ech_no_outer_sni == 1)
+        && ech_config_list == NULL) {
+        BIO_printf(bio_err, "%s: Can't use -ech_outer_sni nor "
+                   "-ech_outer_alpn nor -no_ech_outer_sni without "
+                   "-ech_config_list\n", prog);
+        goto opthelp;
+    }
+# endif
 #ifndef OPENSSL_NO_NEXTPROTONEG
     if (min_version == TLS1_3_VERSION && next_proto_neg_in != NULL) {
         BIO_printf(bio_err, "Cannot supply -nextprotoneg with TLSv1.3\n");
@@ -1885,6 +1948,13 @@ int s_client_main(int argc, char **argv)
         SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);
 #endif
 
+# ifndef OPENSSL_NO_ECH
+    if (ech_grease != 0)
+        SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE);
+    if (ech_ignore_cid != 0)
+        SSL_CTX_set_options(ctx, SSL_OP_ECH_IGNORE_CID);
+# endif
+
     if (vpmtouched && !SSL_CTX_set1_param(ctx, vpm)) {
         BIO_printf(bio_err, "Error setting verify params\n");
         goto end;
@@ -2090,6 +2160,26 @@ int s_client_main(int argc, char **argv)
     if (set_keylog_file(ctx, keylog_file))
         goto end;
 
+# ifndef OPENSSL_NO_ECH
+    if (alpn_outer_in != NULL) {
+        size_t alpn_outer_len;
+        unsigned char *alpn_outer = NULL;
+
+        alpn_outer = next_protos_parse(&alpn_outer_len, alpn_outer_in);
+        if (alpn_outer == NULL) {
+            BIO_printf(bio_err, "Error parsing -ech_outer_alpn argument\n");
+            goto end;
+        }
+        if (SSL_CTX_ech_set1_outer_alpn_protos(ctx, alpn_outer,
+                                               alpn_outer_len) != 1) {
+            BIO_printf(bio_err, "Error setting ALPN-OUTER\n");
+            OPENSSL_free(alpn_outer);
+            goto end;
+        }
+        OPENSSL_free(alpn_outer);
+    }
+# endif
+
     con = SSL_new(ctx);
     if (con == NULL)
         goto end;
@@ -2109,6 +2199,25 @@ int s_client_main(int argc, char **argv)
         }
     }
 
+# ifndef OPENSSL_NO_ECH
+    if (ech_grease_suite != NULL) {
+        if (SSL_ech_set1_grease_suite(con, ech_grease_suite) != 1) {
+            ERR_print_errors(bio_err);
+            goto end;
+        }
+    }
+    /* no point in setting to our default */
+    if (ech_grease_type != OSSL_ECH_CURRENT_VERSION) {
+        BIO_printf(bio_err, "Setting GREASE ECH type 0x%4x\n", ech_grease_type);
+        if (SSL_ech_set_grease_type(con, ech_grease_type) != 1) {
+            BIO_printf(bio_err, "Can't set GREASE ECH type 0x%4x\n",
+                       ech_grease_type);
+            ERR_print_errors(bio_err);
+            goto end;
+        }
+    }
+# endif
+
     if (sess_in != NULL) {
         SSL_SESSION *sess;
         BIO *stmp = BIO_new_file(sess_in, "r");
@@ -2123,6 +2232,7 @@ int s_client_main(int argc, char **argv)
             goto end;
         }
         if (!SSL_set_session(con, sess)) {
+            SSL_SESSION_free(sess);
             BIO_printf(bio_err, "Can't set session\n");
             goto end;
         }
@@ -2145,10 +2255,47 @@ int s_client_main(int argc, char **argv)
     }
 
 # ifndef OPENSSL_NO_ECH
-    if (ech_config_list != NULL
-        && SSL_set1_ech_config_list(con, (unsigned char *)ech_config_list,
-                                    strlen(ech_config_list)) != 1)
-        goto end;
+    if (ech_config_list != NULL) {
+        if (SSL_set1_ech_config_list(con, (unsigned char *)ech_config_list,
+                                     strlen(ech_config_list)) != 1) {
+            BIO_printf(bio_err, "%s: error setting ECHConfigList.\n", prog);
+            goto end;
+        }
+        if (ech_no_outer_sni == 1) {
+            if (sni_outer_name != NULL) {
+                BIO_printf(bio_err, "%s: can't set -ech_no_outer_sni and "
+                           "-ech_outer_sni together.\n", prog);
+                goto end;
+            }
+            if (SSL_ech_set1_outer_server_name(con, NULL, 1) != 1) {
+                BIO_printf(bio_err, "%s: setting no ECH outer name failed.\n",
+                           prog);
+                ERR_print_errors(bio_err);
+                goto end;
+            }
+        }
+        if (sni_outer_name != NULL) {
+            rv = SSL_ech_set1_outer_server_name(con, sni_outer_name, 0);
+            if (rv != 1) {
+                BIO_printf(bio_err, "%s: setting ECH outer name to %s failed.\n",
+                           prog, sni_outer_name);
+                ERR_print_errors(bio_err);
+                goto end;
+            }
+        }
+    }
+    if (ech_select != OSSL_ECHSTORE_ALL) {
+        if ((es = SSL_get1_echstore(con)) == NULL
+            || OSSL_ECHSTORE_downselect(es, ech_select) != 1
+            || SSL_set1_echstore(con, es) != 1) {
+            BIO_printf(bio_err, "%s: ECH downselect to (%d) failed.\n",
+                       prog, ech_select);
+            ERR_print_errors(bio_err);
+            goto end;
+        }
+        OSSL_ECHSTORE_free(es);
+        es = NULL;
+    }
 # endif
 
     if (dane_tlsa_domain != NULL) {
@@ -3371,6 +3518,9 @@ int s_client_main(int argc, char **argv)
     bio_c_out = NULL;
     BIO_free(bio_c_msg);
     bio_c_msg = NULL;
+# ifndef OPENSSL_NO_ECH
+    OSSL_ECHSTORE_free(es);
+# endif
     return ret;
 }
 
@@ -3446,11 +3596,12 @@ static void print_ech_retry_configs(BIO *bio, SSL *s)
     for (ind = 0; ind != cnt; ind++) {
         if (OSSL_ECHSTORE_get1_info(es, ind, &secs, &pn, &ec,
                                     &has_priv, &for_retry) != 1) {
-            BIO_printf(bio, "ECH: Error getting retry-config %d\n", ind);
+            BIO_printf(bio, "ECH: Error getting retry-config %d.\n", ind);
             goto end;
         }
-        BIO_printf(bio, "ECH: entry: %d public_name: %s age: %d%s\n",
-                   ind, pn, (int)secs, has_priv ? " (has private key)" : "");
+        BIO_printf(bio, "ECH: entry: %d public_name: %s age: %lld%s\n",
+                   ind, pn, (long long)secs,
+                   has_priv ? " (has private key)" : "");
         BIO_printf(bio, "ECH: \t%s\n", ec);
         OPENSSL_free(pn);
         pn = NULL;
@@ -3466,6 +3617,7 @@ end:
     return;
 }
 
+/* outcomes marked as "odd" shouldn't happen in s_client */
 static void print_ech_status(BIO *bio, SSL *s, int estat)
 {
     switch (estat) {
@@ -3482,7 +3634,7 @@ static void print_ech_status(BIO *bio, SSL *s, int estat)
         BIO_printf(bio, "ECH: success: %d\n", estat);
         break;
     case SSL_ECH_STATUS_GREASE_ECH:
-        BIO_printf(bio, "ECH: GREASE+retry-configs%d\n", estat);
+        BIO_printf(bio, "ECH: GREASE+retry-configs%d\n", estat);
         break;
     case SSL_ECH_STATUS_BACKEND:
         BIO_printf(bio, "ECH: BACKEND: %d\n", estat);
@@ -3523,6 +3675,10 @@ static void print_stuff(BIO *bio, SSL *s, int full)
 #ifndef OPENSSL_NO_CT
     const SSL_CTX *ctx = SSL_get_SSL_CTX(s);
 #endif
+# ifndef OPENSSL_NO_ECH
+    char *inner = NULL, *outer = NULL;
+    int estat = 0;
+# endif
 
     if (full) {
         int got_a_chain = 0;
@@ -3753,22 +3909,17 @@ static void print_stuff(BIO *bio, SSL *s, int full)
     }
     BIO_printf(bio, "---\n");
 # ifndef OPENSSL_NO_ECH
-    {
-        char *inner = NULL, *outer = NULL;
-        int estat = 0;
-
-        estat = SSL_ech_get1_status(s, &inner, &outer);
-        print_ech_status(bio, s, estat);
-        if (estat == SSL_ECH_STATUS_SUCCESS) {
-            BIO_printf(bio, "ECH: inner: %s\n", inner);
-            BIO_printf(bio, "ECH: outer: %s\n", outer);
-        }
-        if (estat == SSL_ECH_STATUS_FAILED_ECH
-            || estat == SSL_ECH_STATUS_FAILED_ECH_BAD_NAME)
-            print_ech_retry_configs(bio, s);
-        OPENSSL_free(inner);
-        OPENSSL_free(outer);
+    estat = SSL_ech_get1_status(s, &inner, &outer);
+    print_ech_status(bio, s, estat);
+    if (estat == SSL_ECH_STATUS_SUCCESS) {
+        BIO_printf(bio, "ECH: inner: %s\n", inner);
+        BIO_printf(bio, "ECH: outer: %s\n", outer);
     }
+    if (estat == SSL_ECH_STATUS_FAILED_ECH
+        || estat == SSL_ECH_STATUS_FAILED_ECH_BAD_NAME)
+        print_ech_retry_configs(bio, s);
+    OPENSSL_free(inner);
+    OPENSSL_free(outer);
     BIO_printf(bio, "---\n");
 # endif
 
index 82590f9adbbd874f94be696c3f0e5968b8427328..572787edde14cdeb017c0609ae705b4bac434740 100644 (file)
@@ -18,6 +18,9 @@
 #if defined(_WIN32)
 /* Included before async.h to avoid some warnings */
 # include <windows.h>
+# if !defined(OPENSSL_NO_ECH) && !defined(PATH_MAX)
+#  define PATH_MAX 4096
+# endif
 #endif
 
 #include <openssl/e_os2.h>
 #include <openssl/decoder.h>
 #include "internal/sockets.h" /* for openssl_fdset() */
 
+#ifndef OPENSSL_NO_ECH
+/* to use tracing, if configured and requested */
+# ifndef OPENSSL_NO_SSL_TRACE
+#  include <openssl/trace.h>
+# endif
+/* sockaddr stuff  */
+# if defined(_WIN32)
+#  include <winsock.h>
+#  include <ws2ipdef.h>
+#  include <ws2tcpip.h>
+# else
+#  include <netinet/in.h>
+#  include <sys/socket.h>
+#  include <arpa/inet.h>
+#  include <netdb.h>
+# endif
+/* for timing in some TRACE statements */
+# include <time.h>
+# include "internal/o_dir.h" /* for OPENSSL_DIR_read */
+#endif
+
 #ifndef OPENSSL_NO_SOCK
 
 /*
@@ -59,6 +83,11 @@ typedef unsigned int u_int;
 #include "internal/sockets.h"
 #include "internal/statem.h"
 
+# ifndef OPENSSL_NO_ECH
+/* needed for X509_check_host in some CI builds "no-http" */
+#  include <openssl/x509v3.h>
+# endif
+
 static int not_resumable_sess_cb(SSL *s, int is_forward_secure);
 static int sv_body(int s, int stype, int prot, unsigned char *context);
 static int www_body(int s, int stype, int prot, unsigned char *context);
@@ -72,6 +101,10 @@ static void init_session_cache_ctx(SSL_CTX *sctx);
 static void free_sessions(void);
 static void print_connection_info(SSL *con);
 
+# ifndef OPENSSL_NO_ECH
+static unsigned int ech_print_cb(SSL *s, const char *str);
+# endif
+
 static const int bufsize = 16 * 1024;
 static int accept_socket = -1;
 
@@ -420,8 +453,194 @@ typedef struct tlsextctx_st {
     char *servername;
     BIO *biodebug;
     int extension_error;
+    X509 *scert; /* ECH needs 2nd cert for testing */
 } tlsextctx;
 
+# ifndef OPENSSL_NO_ECH
+static unsigned int ech_print_cb(SSL *s, const char *str)
+{
+    if (str != NULL)
+        BIO_printf(bio_s_out, "ECH Server callback printing: \n%s\n", str);
+    return 1;
+}
+
+/*
+ * The server has possibly 2 TLS server names basically in ctx and ctx2.  So we
+ * need to check if any client-supplied SNI in the inner/outer matches either
+ * and serve whichever is appropriate.  X509_check_host is the way to do that,
+ * given an X509* pointer.
+ *
+ * We default to the "main" ctx if the client-supplied SNI does not match the
+ * ctx2 certificate.  We don't fail if the client-supplied SNI matches neither,
+ * but just continue with the "main" ctx.  If the client-supplied SNI matches
+ * both ctx and ctx2, then we'll switch to ctx2 anyway - we don't try for a
+ * "best" match in that case.
+ *
+ * Note that since we attempt ECH decryption whenever configured to do that,
+ * the only way to get the "outer" SNI is via SSL_ech_get1_status.
+ */
+
+/* apparently 26 is all we need, but round it up to 32 to be on the safe side */
+#  define ECH_TIME_STR_LEN 32
+
+static int ssl_ech_servername_cb(SSL *s, int *ad, void *arg)
+{
+    tlsextctx *p = (tlsextctx *) arg;
+    time_t now = time(0); /* For a bit of basic logging */
+    int sockfd = 0, res = 0, echrv = 0;
+    size_t srv = 0;
+    struct sockaddr_storage ss;
+    socklen_t salen = sizeof(ss);
+    struct sockaddr *sa;
+    char clientip[INET6_ADDRSTRLEN], lstr[ECH_TIME_STR_LEN];
+    const char *servername = NULL;
+    char *inner_sni = NULL, *outer_sni = NULL;
+    struct tm local;
+#  if !defined(OPENSSL_SYS_WINDOWS)
+    struct tm *local_p = NULL;
+#  else
+    errno_t grv;
+#  endif
+
+#  if !defined(OPENSSL_SYS_WINDOWS)
+    local_p = gmtime_r(&now, &local);
+    if (local_p != &local) {
+        strcpy(lstr, "sometime");
+    } else {
+        srv = strftime(lstr, ECH_TIME_STR_LEN, "%c", &local);
+        if (srv == 0)
+            strcpy(lstr, "sometime");
+    }
+#  else
+    grv = gmtime_s(&local, &now);
+    if (grv != 0) {
+        strcpy(lstr, "sometime");
+    } else {
+        srv = strftime(lstr, ECH_TIME_STR_LEN, "%c", &local);
+        if (srv == 0)
+            strcpy(lstr, "sometime");
+    }
+#  endif
+    memset(clientip, 0, INET6_ADDRSTRLEN);
+    strncpy(clientip, "unknown", INET6_ADDRSTRLEN);
+    memset(&ss, 0, salen);
+    sa = (struct sockaddr *)&ss;
+    res = BIO_get_fd(SSL_get_wbio(s), &sockfd);
+    if (res != -1) {
+#  if !defined(_WIN32)
+        res = getpeername(sockfd, sa, &salen);
+#  else
+        res = getpeername(sockfd, sa, (int *)&salen);
+#  endif
+        if (res == 0)
+            res = getnameinfo(sa, salen, clientip, INET6_ADDRSTRLEN,
+                              0, 0, NI_NUMERICHOST);
+    }
+    /* Name that matches "main" ctx */
+    servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
+    echrv = SSL_ech_get1_status(s, &inner_sni, &outer_sni);
+    if (p->biodebug != NULL) {
+        /* spit out basic logging */
+        BIO_printf(p->biodebug,
+                   "ssl_ech_servername_cb: connection from %s at %s\n",
+                   clientip, lstr);
+        /* Client supplied SNI from inner and outer */
+        switch (echrv) {
+        case SSL_ECH_STATUS_BACKEND:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: ECH backend got inner ECH\n");
+            break;
+        case SSL_ECH_STATUS_NOT_CONFIGURED:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: ECH not configured\n");
+            break;
+        case SSL_ECH_STATUS_GREASE:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: attempt we think is GREASE\n");
+            break;
+        case SSL_ECH_STATUS_NOT_TRIED:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: not attempted\n");
+            break;
+        case SSL_ECH_STATUS_FAILED:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: tried but failed\n");
+            break;
+        case SSL_ECH_STATUS_BAD_CALL:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: bad input to API\n");
+            break;
+        case SSL_ECH_STATUS_BAD_NAME:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: worked but bad name\n");
+            break;
+        case SSL_ECH_STATUS_SUCCESS:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: success: outer %s, inner: %s\n",
+                       (outer_sni == NULL ? "none" : outer_sni),
+                       (inner_sni == NULL ? "none" : inner_sni));
+            break;
+        default:
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: Error getting ECH status\n");
+            break;
+        }
+    }
+    OPENSSL_free(inner_sni);
+    OPENSSL_free(outer_sni);
+    if (servername != NULL && p->biodebug != NULL) {
+        const char *cp = servername;
+        unsigned char uc;
+
+        BIO_printf(p->biodebug,
+                   "ssl_ech_servername_cb: Hostname in TLS extension: \"");
+        while ((uc = *cp++) != 0)
+            BIO_printf(p->biodebug,
+                       isascii(uc) && isprint(uc) ? "%c" : "\\x%02x", uc);
+        BIO_printf(p->biodebug, "\"\n");
+        if (p->servername != NULL)
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: ctx servername: %s\n",
+                       p->servername);
+        else
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: ctx servername is NULL\n");
+        if (p->scert == NULL)
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: No 2nd cert! That's bad.\n");
+    }
+    if (p->servername == NULL)
+        return SSL_TLSEXT_ERR_NOACK;
+    if (p->scert == NULL)
+        return SSL_TLSEXT_ERR_NOACK;
+    if (echrv == SSL_ECH_STATUS_SUCCESS && servername != NULL) {
+        if (ctx2 != NULL) {
+            int check_host = X509_check_host(p->scert, servername, 0, 0, NULL);
+
+            if (check_host == 1) {
+                if (p->biodebug != NULL)
+                    BIO_printf(p->biodebug,
+                               "ssl_ech_servername_cb: Switching context.\n");
+                SSL_set_SSL_CTX(s, ctx2);
+            } else {
+                if (p->biodebug != NULL)
+                    BIO_printf(p->biodebug,
+                               "ssl_ech_servername_cb: Not switching context "
+                               "- no name match (%d).\n", check_host);
+            }
+        }
+    } else {
+        if (p->biodebug != NULL)
+            BIO_printf(p->biodebug,
+                       "ssl_ech_servername_cb: Not switching context "
+                       "- no ECH SUCCESS\n");
+    }
+    return SSL_TLSEXT_ERR_OK;
+}
+/* Below is the "original" ssl_servername_cb, before ECH */
+
+# else
+
 static int ssl_servername_cb(SSL *s, int *ad, void *arg)
 {
     tlsextctx *p = (tlsextctx *) arg;
@@ -452,6 +671,8 @@ static int ssl_servername_cb(SSL *s, int *ad, void *arg)
     return SSL_TLSEXT_ERR_OK;
 }
 
+# endif
+
 /* Structure passed to cert status callback */
 typedef struct tlsextstatusctx_st {
     int timeout;
@@ -960,6 +1181,10 @@ typedef enum OPTION_choice {
     OPT_TFO, OPT_CERT_COMP,
     OPT_ENABLE_SERVER_RPK,
     OPT_ENABLE_CLIENT_RPK,
+# ifndef OPENSSL_NO_ECH
+    OPT_ECH_PEM, OPT_ECH_DIR, OPT_ECH_NORETRY,
+    OPT_ECH_TRIALDECRYPT, OPT_ECH_GREASE_RT,
+# endif
     OPT_R_ENUM,
     OPT_S_ENUM,
     OPT_V_ENUM,
@@ -1209,6 +1434,19 @@ const OPTIONS s_server_options[] = {
 #endif
     {"alpn", OPT_ALPN, 's',
      "Set the advertised protocols for the ALPN extension (comma-separated list)"},
+
+# ifndef OPENSSL_NO_ECH
+    {"ech_key", OPT_ECH_PEM, 's', "Load ECH PEM-formatted key pair"},
+    {"ech_dir", OPT_ECH_DIR, 's', "Load ECH key pairs (for retries) " \
+     "from the specified directory"},
+    {"ech_noretry_dir", OPT_ECH_NORETRY, 's', "Load ECH key pairs (not " \
+     "for retry) from the specified directory"},
+    {"ech_trialdecrypt", OPT_ECH_TRIALDECRYPT, '-',
+     "Do trial decryption even if ECH record_digest matching fails"},
+    {"ech_greaseretries", OPT_ECH_GREASE_RT, '-',
+     "Set server to GREASE retry_config values"},
+# endif
+
 #ifndef OPENSSL_NO_KTLS
     {"ktls", OPT_KTLS, '-', "Enable Kernel TLS for sending and receiving"},
     {"sendfile", OPT_SENDFILE, '-', "Use sendfile to response file with -WWW"},
@@ -1224,6 +1462,64 @@ const OPTIONS s_server_options[] = {
     {NULL}
 };
 
+# ifndef OPENSSL_NO_ECH
+static int ech_load_dir(SSL_CTX *lctx, const char *thedir,
+                        int for_retry, int *nloaded)
+{
+    size_t elen = strlen(thedir);
+    OPENSSL_DIR_CTX *d = NULL;
+    const char *thisfile = NULL;
+    OSSL_ECHSTORE *es = NULL;
+    BIO *in = NULL;
+    int loaded = 0;
+
+    if ((elen + 7) >= PATH_MAX) { /* too long, go away */
+        BIO_printf(bio_err, "'%s' too long - exiting\n", thedir);
+        return 0;
+    }
+    if (app_isdir(thedir) <= 0) { /* if not a directory, ignore it */
+        BIO_printf(bio_err, "'%s' not a directory - exiting\n", thedir);
+        return 0;
+    }
+    if ((es = SSL_CTX_get1_echstore(lctx)) == NULL
+        && (es = OSSL_ECHSTORE_new(app_get0_libctx(),
+                                   app_get0_propq())) == NULL) {
+        BIO_printf(bio_err, "internal error\n");
+        return 0;
+    }
+    while ((thisfile = OPENSSL_DIR_read(&d, thedir))) {
+        char filepath[PATH_MAX];
+        int r;
+
+#  ifdef OPENSSL_SYS_VMS
+        r = BIO_snprintf(filepath, sizeof(filepath), "%s%s", thedir, thisfile);
+#  else
+        r = BIO_snprintf(filepath, sizeof(filepath), "%s/%s", thedir, thisfile);
+#  endif
+        if (r < 0
+            || app_isdir(filepath) > 0
+            || (in = BIO_new_file(filepath, "r")) == NULL
+            || OSSL_ECHSTORE_read_pem(es, in, for_retry) != 1) {
+            BIO_printf(bio_err, "Failed reading from: %s\n", thisfile);
+            continue;
+        }
+        BIO_free_all(in);
+        if (bio_s_out != NULL)
+            BIO_printf(bio_s_out, "Added ECH key pair from: %s\n", thisfile);
+        loaded++;
+    }
+    if (SSL_CTX_set1_echstore(lctx, es) != 1) {
+        BIO_printf(bio_err, "internal error\n");
+        return 0;
+    }
+    if (bio_s_out != NULL)
+        BIO_printf(bio_s_out, "Added %d ECH key pairs from: %s\n",
+                   loaded, thedir);
+    *nloaded = loaded;
+    return 1;
+}
+# endif
+
 #define IS_PROT_FLAG(o) \
  (o == OPT_SSL3 || o == OPT_TLS1 || o == OPT_TLS1_1 || o == OPT_TLS1_2 \
   || o == OPT_TLS1_3 || o == OPT_DTLS || o == OPT_DTLS1 || o == OPT_DTLS1_2)
@@ -1266,7 +1562,7 @@ int s_server_main(int argc, char *argv[])
     OPTION_CHOICE o;
     EVP_PKEY *s_key2 = NULL;
     X509 *s_cert2 = NULL;
-    tlsextctx tlsextcbp = { NULL, NULL, SSL_TLSEXT_ERR_ALERT_WARNING };
+    tlsextctx tlsextcbp = { NULL, NULL, SSL_TLSEXT_ERR_ALERT_WARNING, NULL };
     const char *ssl_config = NULL;
     int read_buf_len = 0;
 #ifndef OPENSSL_NO_NEXTPROTONEG
@@ -1304,6 +1600,14 @@ int s_server_main(int argc, char *argv[])
     int max_early_data = -1, recv_max_early_data = -1;
     char *psksessf = NULL;
     int no_ca_names = 0;
+# ifndef OPENSSL_NO_ECH
+    char *echkeyfile = NULL;
+    char *echkeydir = NULL;
+    char *echnoretrydir = NULL;
+    int ech_files_loaded = 0;
+    int echtrialdecrypt = 0; /* trial decryption off by default */
+    int echgrease_rc = 0; /* retry_config GREASEing off by default */
+# endif
 #ifndef OPENSSL_NO_SCTP
     int sctp_label_bug = 0;
 #endif
@@ -1904,6 +2208,23 @@ int s_server_main(int argc, char *argv[])
         case OPT_HTTP_SERVER_BINMODE:
             http_server_binmode = 1;
             break;
+# ifndef OPENSSL_NO_ECH
+        case OPT_ECH_PEM:
+            echkeyfile = opt_arg();
+            break;
+        case OPT_ECH_DIR:
+            echkeydir = opt_arg();
+            break;
+        case OPT_ECH_NORETRY:
+            echnoretrydir = opt_arg();
+            break;
+        case OPT_ECH_TRIALDECRYPT:
+            echtrialdecrypt = 1;
+            break;
+        case OPT_ECH_GREASE_RT:
+            echgrease_rc = 1;
+            break;
+# endif
         case OPT_NOCANAMES:
             no_ca_names = 1;
             break;
@@ -2063,6 +2384,9 @@ int s_server_main(int argc, char *argv[])
 
             if (s_cert2 == NULL)
                 goto end;
+# ifndef OPENSSL_NO_ECH
+            tlsextcbp.scert = s_cert2;
+# endif
         }
     }
 #if !defined(OPENSSL_NO_NEXTPROTONEG)
@@ -2275,12 +2599,69 @@ int s_server_main(int argc, char *argv[])
         goto end;
     }
 
+# ifndef OPENSSL_NO_ECH
+    if (echtrialdecrypt != 0)
+        SSL_CTX_set_options(ctx, SSL_OP_ECH_TRIALDECRYPT);
+    if (echgrease_rc != 0)
+        SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE_RETRY_CONFIG);
+    if (echkeyfile != NULL) {
+        OSSL_ECHSTORE *es = NULL;
+        BIO *in = NULL;
+
+        if ((in = BIO_new_file(echkeyfile, "r")) == NULL
+            || (es = OSSL_ECHSTORE_new(app_get0_libctx(),
+                                       app_get0_propq())) == 0
+            || OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY) != 1
+            || SSL_CTX_set1_echstore(ctx, es) != 1) {
+            BIO_printf(bio_err, "Failed reading: %s\n", echkeyfile);
+            OSSL_ECHSTORE_free(es);
+            BIO_free_all(in);
+            goto end;
+        }
+        OSSL_ECHSTORE_free(es);
+        BIO_free_all(in);
+        if (bio_s_out != NULL)
+            BIO_printf(bio_s_out, "Added ECH key pair from: %s\n", echkeyfile);
+        ech_files_loaded++;
+    }
+    if (echkeydir != NULL) {
+        int nloaded = 0;
+
+        if (ech_load_dir(ctx, echkeydir, OSSL_ECH_FOR_RETRY, &nloaded) != 1) {
+            BIO_printf(bio_err, "error loading from %s\n", echkeydir);
+            goto end;
+        }
+        ech_files_loaded += nloaded;
+    }
+    if (echnoretrydir != NULL) {
+        int nloaded = 0;
+
+        if (ech_load_dir(ctx, echnoretrydir, OSSL_ECH_NO_RETRY,
+                         &nloaded) != 1) {
+            BIO_printf(bio_err, "error loading from %s\n", echnoretrydir);
+            goto end;
+        }
+        ech_files_loaded += nloaded;
+    }
+    if ((echkeyfile != NULL || echkeydir != NULL || echnoretrydir != NULL)
+        && bio_s_out != NULL) {
+        BIO_printf(bio_s_out, "Loaded %d ECH key pairs in total\n",
+                   ech_files_loaded);
+    }
+# endif
+
     if (s_cert2) {
         ctx2 = SSL_CTX_new_ex(app_get0_libctx(), app_get0_propq(), meth);
         if (ctx2 == NULL) {
             ERR_print_errors(bio_err);
             goto end;
         }
+# ifndef OPENSSL_NO_ECH
+        if (echtrialdecrypt != 0)
+            SSL_CTX_set_options(ctx2, SSL_OP_ECH_TRIALDECRYPT);
+        if (echgrease_rc != 0)
+            SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE_RETRY_CONFIG);
+# endif
     }
 
     if (ctx2 != NULL) {
@@ -2339,6 +2720,13 @@ int s_server_main(int argc, char *argv[])
     if (alpn_ctx.data)
         SSL_CTX_set_alpn_select_cb(ctx, alpn_cb, &alpn_ctx);
 
+    /*
+     * If we have a 2nd context to which we might switch, then set
+     * the same alpn callback for that too.
+     */
+    if (s_cert2 != NULL && alpn_ctx.data != NULL)
+        SSL_CTX_set_alpn_select_cb(ctx2, alpn_cb, &alpn_ctx);
+
     if (!no_dhe) {
         EVP_PKEY *dhpkey = NULL;
 
@@ -2413,9 +2801,21 @@ int s_server_main(int argc, char *argv[])
         goto end;
     }
 
+# ifndef OPENSSL_NO_ECH
+    /*
+     * Giving the same chain to the 2nd key pair works for our tests.
+     * It would be better to supply s_chain_file2 as a new CLA in case
+     * the paths are very different but as that's not needed for tests,
+     * I didn't do it.
+     */
+    if (ctx2 != NULL
+        && !set_cert_key_stuff(ctx2, s_cert2, s_key2, s_chain, build_chain))
+        goto end;
+# else
     if (ctx2 != NULL
         && !set_cert_key_stuff(ctx2, s_cert2, s_key2, NULL, build_chain))
         goto end;
+# endif
 
     if (s_dcert != NULL) {
         if (!set_cert_key_stuff(ctx, s_dcert, s_dkey, s_dchain, build_chain))
@@ -2497,10 +2897,19 @@ int s_server_main(int argc, char *argv[])
             goto end;
         }
         tlsextcbp.biodebug = bio_s_out;
+# ifndef OPENSSL_NO_ECH
+        SSL_CTX_set_tlsext_servername_callback(ctx2, ssl_ech_servername_cb);
+        SSL_CTX_set_tlsext_servername_arg(ctx2, &tlsextcbp);
+        SSL_CTX_set_tlsext_servername_callback(ctx, ssl_ech_servername_cb);
+        SSL_CTX_set_tlsext_servername_arg(ctx, &tlsextcbp);
+        SSL_CTX_ech_set_callback(ctx2, ech_print_cb);
+        SSL_CTX_ech_set_callback(ctx, ech_print_cb);
+# else
         SSL_CTX_set_tlsext_servername_callback(ctx2, ssl_servername_cb);
         SSL_CTX_set_tlsext_servername_arg(ctx2, &tlsextcbp);
         SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb);
         SSL_CTX_set_tlsext_servername_arg(ctx, &tlsextcbp);
+# endif
     }
 
 #ifndef OPENSSL_NO_SRP
@@ -2528,6 +2937,11 @@ int s_server_main(int argc, char *argv[])
 #endif
     if (set_keylog_file(ctx, keylog_file))
         goto end;
+# ifndef OPENSSL_NO_ECH
+    /* not really an ECH issue but needed */
+    if (ctx2 != NULL && set_keylog_file(ctx2, keylog_file))
+        goto end;
+# endif
 
     if (max_early_data >= 0)
         SSL_CTX_set_max_early_data(ctx, max_early_data);
@@ -3542,6 +3956,10 @@ static int www_body(int s, int stype, int prot, unsigned char *context)
             X509 *peer = NULL;
             STACK_OF(SSL_CIPHER) *sk;
             static const char *space = "                          ";
+# ifndef OPENSSL_NO_ECH
+            char *ech_inner = NULL, *ech_outer = NULL;
+            int echrv = 0;
+# endif
 
             if (www == 1 && HAS_PREFIX(buf, "GET /reneg")) {
                 if (HAS_PREFIX(buf, "GET /renegcert"))
@@ -3605,6 +4023,80 @@ static int www_body(int s, int stype, int prot, unsigned char *context)
             }
             BIO_puts(io, "\n");
 
+# ifndef OPENSSL_NO_ECH
+            /* Customise output a bit to show ECH info at top */
+            BIO_puts(io, "<h1>OpenSSL with ECH</h1>\n");
+            BIO_puts(io, "<h2>\n");
+            echrv = SSL_ech_get1_status(con, &ech_inner, &ech_outer);
+            switch (echrv) {
+            case SSL_ECH_STATUS_NOT_TRIED:
+                BIO_puts(io, "ECH not attempted\n");
+                break;
+            case SSL_ECH_STATUS_FAILED:
+                BIO_puts(io, "ECH tried but failed\n");
+                break;
+            case SSL_ECH_STATUS_FAILED_ECH:
+                BIO_puts(io, "ECH tried but we got ECH which is weird\n");
+                break;
+            case SSL_ECH_STATUS_BAD_NAME:
+                BIO_puts(io, "ECH worked but bad name\n");
+                break;
+            case SSL_ECH_STATUS_BACKEND:
+                BIO_printf(io, "ECH acting as backend\n");
+                break;
+            case SSL_ECH_STATUS_NOT_CONFIGURED:
+                BIO_printf(io, "ECH not configured\n");
+                break;
+            case SSL_ECH_STATUS_GREASE:
+                BIO_printf(io, "ECH attempt we interpret as GREASE\n");
+                break;
+            case SSL_ECH_STATUS_GREASE_ECH:
+                BIO_printf(io, "ECH attempt we interpret as GREASE, + ECH\n");
+                break;
+            case SSL_ECH_STATUS_BAD_CALL:
+                BIO_printf(io, "ECH bad input to API\n");
+                break;
+            case SSL_ECH_STATUS_SUCCESS:
+                BIO_printf(io, "ECH success: outer sni: %s, inner sni: %s\n",
+                           (ech_outer == NULL ? "none" : ech_outer),
+                           (ech_inner == NULL ? "none" : ech_inner));
+                break;
+            default:
+                BIO_printf(io, " Error getting ECH status\n");
+                break;
+            }
+            BIO_puts(io, "</h2>\n");
+            BIO_puts(io, "<h2>TLS Session details</h2>\n");
+            BIO_puts(io, "<pre>\n");
+            /*
+             * also dump session info to server stdout for debugging
+             */
+            SSL_SESSION_print(bio_s_out, SSL_get_session(con));
+            BIO_puts(io, "<pre>\n");
+            BIO_puts(io, "\n");
+            for (i = 0; i < local_argc; i++) {
+                const char *myp;
+
+                for (myp = local_argv[i]; *myp; myp++)
+                    switch (*myp) {
+                    case '<':
+                        BIO_puts(io, "&lt;");
+                        break;
+                    case '>':
+                        BIO_puts(io, "&gt;");
+                        break;
+                    case '&':
+                        BIO_puts(io, "&amp;");
+                        break;
+                    default:
+                        BIO_write(io, myp, 1);
+                        break;
+                    }
+                BIO_write(io, " ", 1);
+            }
+            BIO_puts(io, "\n");
+# endif
+
             ssl_print_secure_renegotiation_notes(io, con);
 
             /*
index 5579a9c85f558a1065f20bd2871c8e0fc7b7b186..8de5efb7e18d15c9426fcfd4a0bcba77e06f9d03 100644 (file)
@@ -125,6 +125,14 @@ B<openssl> B<s_client>
 [B<-enable_client_rpk>]
 [I<host>:I<port>]
 [B<-ech_config_list>]
+[B<-ech_outer_alpn> I<protocols>]
+[B<-ech_grease>]
+[B<-ech_grease_suite> I<suite>]
+[B<-ech_grease_type> I<type>]
+[B<-ech_ignore_cid>]
+[B<-ech_outer_sni> I<value>]
+[B<-ech_no_outer_sni>]
+[B<-ech_select> I<config-index>]
 
 =head1 DESCRIPTION
 
@@ -832,6 +840,63 @@ nor B<-connect> are provided, falls back to attempting to connect to
 I<localhost> on port I<4433>.
 If the host string is an IPv6 address, it must be enclosed in C<[> and C<]>.
 
+=item B<-ech_outer_alpn> I<protocols>
+
+When doing Encrypted Client Hello (ECH), this allows the caller to specify
+ALPN values to use in the outer ClientHello. (A "normal" ALPN value
+specified via -alpn will be used in the inner ClientHello.)
+
+=item B<-ech_grease>
+
+When not really doing Encrypted Client Hello (ECH), one can emit a so-called
+GREASE value, which is essentially a random value in order to try ensure that
+server code is less likely to ossify.
+
+=item B<-ech_grease_suite> I<suite>
+
+When B<-ech_grease> is specified, one can choose which ECH ciphersuite to use
+via this parameter.
+
+The comma-separated suite string names an HPKE suite in the form of
+I<kem>,I<kdf>,I<aead>, e.g. "x25519,hkdf-sha256,aes256gcm" or can use
+the numeric values (in decimal or hexadecimal form) from the HPKE specification
+so "0x20,0x01,0x02" is the same as the previous example.
+
+KEM values supported: p256 or 0x10; p384 or 0x11, p521 or 0x12, x25519 or 0x20, x448 or 0x21
+
+KDF values supported: hkdf-sha256 or 0x01, hkdf-sha384 or 0x02, hkdf-sha512 or 0x03
+
+AEAD values supported: aes128gcm or 0x01, aes256gcm or 0x02, chachapoly1305 or 0x03
+
+=item B<-ech_grease_type> I<type>
+
+Allows the client to set the TLS extension type for a GREASEd ECH value
+(currently equivalent to the ECH version number).  The current default is
+0xfe0d.
+
+=item B<-ech_ignore_cid>
+
+Encrypted Client Hello (ECH) extensions contain a configuration identifier
+(cid) taken from the ECHConfigList usually found in the domain name system
+(DNS). As those identifiers could be revealing, the client has the option to
+use a random value instead.
+
+=item B<-ech_outer_sni> I<value>
+
+When doing Encrypted Client Hello (ECH), this allows the caller to specify a
+subject name indication (SNI) value to use in the outer ClientHello over-riding
+the public_name value from the relevant ECHConfigList.
+
+=item B<-ech_no_outer_sni>
+
+Setting this flag means no SNI will be emitted in the outer ClientHello.
+
+=item B<-ech_select> I<config-index>
+
+If an ECHConfigList contains more than one ECHConfig then the client will by
+default use the first that works. This allows the caller to specify which
+ECHConfig to use (using a zero-based index).
+
 =back
 
 =head1 CONNECTED COMMANDS (BASIC)
@@ -1058,6 +1123,8 @@ The
 and B<-ocsp_check_all>
 options were added in OpenSSL 3.6.
 
+The B<ech> options were added in OpenSSL 4.0.
+
 =head1 COPYRIGHT
 
 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
index 4c30c9c628330860de08e29af432f4b66d0c4053..c3bb6d3bf3982cdf2ed06cdee221148a6a7aa3c6 100644 (file)
@@ -135,6 +135,11 @@ B<openssl> B<s_server>
 {- $OpenSSL::safe::opt_engine_synopsis -}{- $OpenSSL::safe::opt_provider_synopsis -}
 [B<-enable_server_rpk>]
 [B<-enable_client_rpk>]
+[B<-ech_key> I<filename>]
+[B<-ech_dir> I<dirname>]
+[B<-ech_noretry_dir> I<dirname>]
+[B<-ech_trialdecrypt>]
+[B<-ech_greaseretries>]
 
 =head1 DESCRIPTION
 
@@ -824,6 +829,32 @@ certificates can still elect to send X.509 certificates as usual.
 
 Raw public keys are extracted from the configured certificate/private key.
 
+=item B<-ech_key> I<filename>
+
+Load one Encrypted Client Hello (ECH) key pair.
+
+=item B<-ech_dir> I<dirname>
+
+Attempt to load an ECH key pair from every file in the named directory.
+Any keys successfully loaded will be returned in 'retry_configs'.
+
+=item B<-ech_noretry_dir> I<dirname>
+
+Attempt to load an ECH key pair from every file in the named directory.
+Keys loaded will not be returned in 'retry_configs'.
+
+=item B<-ech_trialdecrypt>
+
+When an Encrypted Client Hello (ECH) extension is seen in a ClientHello,
+attempt to decrypt with all known ECH private keys if necessary. Without
+this, the ECH "config_id" is used to match against the loaded ECH private
+keys and decryption is only attempted when there's a match.
+
+=item B<-ech_greaseretries>
+
+If set, servers will add GREASEy ECHConfig values to those sent
+in retry_configs.
+
 =back
 
 =head1 CONNECTED COMMANDS
@@ -938,6 +969,8 @@ options were added in OpenSSL 3.2.
 
 The B<-status_all> option was added in OpenSSL 3.6.
 
+The B<ech> options were added in OpenSSL 4.0.
+
 =head1 COPYRIGHT
 
 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
index 89fc63d19b0835edcb1ee9478724c44935726576..233536e59ab88d6ed7ceb3baa801e613ea519783 100644 (file)
@@ -388,7 +388,7 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee,
     num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
     /* allow API-set pref to override */
     hn = s->ext.ech.outer_hostname;
-    hnlen = (hn == NULL ? 0 : strlen(hn));
+    hnlen = (hn == NULL ? 0 : (unsigned int)strlen(hn));
     if (hnlen != 0)
         nameoverride = 1;
     if (s->ext.ech.no_outer == 1) {
@@ -513,7 +513,7 @@ int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded,
     }
     /* now copy the rest, as "proper" exts, into encoded inner */
     for (ind = 0; ind < TLSEXT_IDX_num_builtins; ind++) {
-        if (raws[ind].present == 0 || ossl_ech_2bcompressed(ind) == 1)
+        if (raws[ind].present == 0 || ossl_ech_2bcompressed((int)ind) == 1)
             continue;
         if (!WPACKET_put_bytes_u16(&inner, raws[ind].type)
             || !WPACKET_sub_memcpy_u16(&inner, PACKET_data(&raws[ind].data),
@@ -632,15 +632,16 @@ size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee,
         /* do weirder padding if SNI present in inner */
         if (s->ext.hostname != NULL) {
             isnilen = strlen(s->ext.hostname) + 9;
-            innersnipadding = (mnl > isnilen) ? mnl - isnilen : 0;
+            innersnipadding = (mnl > isnilen) ? (int)(mnl - isnilen) : 0;
         } else {
-            innersnipadding = mnl + 9;
+            innersnipadding = (int)mnl + 9;
         }
     }
     /* padding is after the inner client hello has been encoded */
-    length_with_snipadding = innersnipadding + encoded_len;
+    length_with_snipadding = innersnipadding + (int)encoded_len;
     length_of_padding = 31 - ((length_with_snipadding - 1) % 32);
-    length_with_padding = encoded_len + length_of_padding + innersnipadding;
+    length_with_padding = (int)encoded_len + length_of_padding
+        + innersnipadding;
     /*
      * Finally - make sure final result is longer than padding target
      * and a multiple of our padding increment.
index 45d04c616e9644333ef894967a60d1e9c5285eca..45e1ead61645442b34bd7be72df5585c56684e9a 100644 (file)
@@ -260,6 +260,7 @@ int SSL_ech_set1_grease_suite(SSL *ssl, const char *suite)
     if (s->ext.ech.grease_suite == NULL)
         return 0;
     s->ext.ech.attempted = 1;
+    s->ext.ech.grease = OSSL_ECH_IS_GREASE;
     return 1;
 }
 
@@ -272,6 +273,7 @@ int SSL_ech_set_grease_type(SSL *ssl, uint16_t type)
         return 0;
     s->ext.ech.attempted_type = type;
     s->ext.ech.attempted = 1;
+    s->ext.ech.grease = OSSL_ECH_IS_GREASE;
     return 1;
 }
 
index ee9fbd76feca024da6fd09936b040d50f121e9e7..a9a1b32561f1dff321996dd838090c9a415d015c 100644 (file)
@@ -271,7 +271,7 @@ static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee)
     /* check no mandatory exts (with high bit set in type) */
     num = (ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts));
     for (ind = 0; ind != num; ind++) {
-        OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, ind);
+        OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, (int)ind);
 
         if (oe->type & 0x8000) {
             ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
@@ -331,7 +331,7 @@ static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt,
         ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
         goto err;
     }
-    ech_content_length = PACKET_remaining(&ver_pkt);
+    ech_content_length = (unsigned int)PACKET_remaining(&ver_pkt);
     switch (ee->version) {
     case OSSL_ECH_RFCXXXX_VERSION:
         break;
index ba78bcedeabc7627d65b65525c0ec95c9f22a2ad..deed81e4b7935953dade7c62d380fe26dc0a8b31 100644 (file)
@@ -1144,7 +1144,7 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt,
 
 #ifndef OPENSSL_NO_ECH
             /* do compressed in pass 0, non-compressed in pass 1 */
-            if (ossl_ech_2bcompressed(i) == pass)
+            if (ossl_ech_2bcompressed((int)i) == pass)
                 continue;
             /* stash index - needed for COMPRESS ECH handling */
             s->ext.ech.ext_ind = (int)i;
index a77943ace01ace4a5ea2aed3b828128270b912f6..b1f6c70be72a91e847ea42640ad9e7d0ef5095d9 100644 (file)
@@ -2526,14 +2526,14 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
     size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0;
     size_t info_len = OSSL_ECH_MAX_INFO_LEN, clear_len = 0, encoded_len = 0;
     /* whether or not we've been asked to GREASE, one way or another */
-    int grease_opt_set = (s->ext.ech.grease == OSSL_ECH_IS_GREASE
+    int grease_opt_set = ((s->ext.ech.grease == OSSL_ECH_IS_GREASE)
                           || ((s->options & SSL_OP_ECH_GREASE) != 0));
 
     /* if we're not doing real ECH and not GREASEing then exit */
     if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech && grease_opt_set == 0)
         return EXT_RETURN_NOT_SENT;
     /* send grease if not really attempting ECH */
-    if (s->ext.ech.attempted == 0 && grease_opt_set == 1) {
+    if (grease_opt_set == 1) {
         if (s->hello_retry_request == SSL_HRR_PENDING
             && s->ext.ech.sent != NULL) {
             /* re-tx already sent GREASEy ECH */
@@ -2732,7 +2732,7 @@ int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
         return 0;
     }
     rval = PACKET_data(&rcfgs_pkt);
-    rlen = PACKET_remaining(&rcfgs_pkt);
+    rlen = (unsigned int)PACKET_remaining(&rcfgs_pkt);
     OPENSSL_free(s->ext.ech.returned);
     s->ext.ech.returned = NULL;
     srval = OPENSSL_malloc(rlen + 2);
index 07fd9bddf495b1ccd229244ba28e390b354bbd17..fcd4795aa58a39bae412651268aab2f6300ac9af 100644 (file)
@@ -11,6 +11,7 @@
 #include <openssl/hpke.h>
 #include "testutil.h"
 #include "helpers/ssltestlib.h"
+#include "internal/packet.h"
 
 #ifndef OPENSSL_NO_ECH
 
@@ -23,15 +24,58 @@ static char *certsdir = NULL;
 static char *cert = NULL;
 static char *privkey = NULL;
 static char *rootcert = NULL;
+static int ch_test_cb_ok = 0;
 
 /* TODO(ECH): add some testing of SSL_OP_ECH_IGNORE_CID */
 
-/* callback */
-static unsigned int test_cb(SSL *s, const char *str)
+/* ECH callback */
+static unsigned int ech_test_cb(SSL *s, const char *str)
 {
+    if (verbose)
+        TEST_info("ech_test_cb called");
     return 1;
 }
 
+/* ClientHello callback */
+static int ch_test_cb(SSL *ssl, int *al, void *arg)
+{
+    char *servername = NULL;
+    const unsigned char *pos;
+    size_t remaining;
+    unsigned int servname_type;
+    PACKET pkt, sni, hostname;
+
+    if (verbose) {
+        TEST_info("ch_test_cb called");
+        if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_ech, &pos, &remaining)) {
+            TEST_info("there is an ECH extension");
+        } else {
+            TEST_info("there is NO ECH extension");
+        }
+    }
+    if (!SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &pos,
+                                   &remaining)
+            || remaining <= 2)
+        goto give_up;
+    if (!PACKET_buf_init(&pkt, pos, remaining)
+        || !PACKET_as_length_prefixed_2(&pkt, &sni)
+        || !PACKET_get_1(&sni, &servname_type)
+        || servname_type != TLSEXT_NAMETYPE_host_name
+        || !PACKET_as_length_prefixed_2(&sni, &hostname)
+        || (PACKET_remaining(&hostname) > TLSEXT_MAXLEN_host_name)
+        || PACKET_contains_zero_byte(&hostname)
+        || !PACKET_strndup(&hostname, &servername))
+        goto give_up;
+    if (verbose)
+        TEST_info("servername: %s", servername);
+    OPENSSL_free(servername);
+    /* signal to caller all is good */
+    ch_test_cb_ok = 1;
+    return 1;
+give_up:
+    return 0;
+}
+
 /*
  * The define/vars below and the 3 callback functions are modified
  * from test/sslapitest.c
@@ -1095,8 +1139,8 @@ static int ech_api_basic_calls(void)
         || !TEST_false(rclen)
         || !TEST_ptr_eq(rc, NULL))
         goto end;
-    SSL_CTX_ech_set_callback(ctx, test_cb);
-    SSL_ech_set_callback(s, test_cb);
+    SSL_CTX_ech_set_callback(ctx, ech_test_cb);
+    SSL_ech_set_callback(s, ech_test_cb);
 
     /* all good */
     rv = 1;
@@ -1145,6 +1189,7 @@ end:
 # define OSSL_ECH_TEST_EARLY    2
 # define OSSL_ECH_TEST_CUSTOM   3
 # define OSSL_ECH_TEST_ENOE     4 /* early + no-ech */
+# define OSSL_ECH_TEST_CBS      5 /* test callbacks */
 /* note: early-data is prohibited after HRR so no tests for that */
 
 /*
@@ -1224,6 +1269,10 @@ static int test_ech_roundtrip_helper(int idx, int combo)
                                                  &server, NULL, &server)))
             goto end;
     }
+    if (combo == OSSL_ECH_TEST_CBS) {
+        SSL_CTX_ech_set_callback(sctx, ech_test_cb);
+        SSL_CTX_set_client_hello_cb(sctx, ch_test_cb, NULL);
+    }
     if (combo != OSSL_ECH_TEST_ENOE
         && !TEST_true(SSL_CTX_set1_echstore(cctx, es)))
         goto end;
@@ -1259,9 +1308,11 @@ static int test_ech_roundtrip_helper(int idx, int combo)
     if (combo == OSSL_ECH_TEST_ENOE
         && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_NOT_CONFIGURED))
         goto end;
+    if (combo == OSSL_ECH_TEST_CBS && !TEST_int_eq(ch_test_cb_ok, 1))
+        goto end;
     /* all good */
     if (combo == OSSL_ECH_TEST_BASIC || combo == OSSL_ECH_TEST_HRR
-        || combo == OSSL_ECH_TEST_CUSTOM) {
+        || combo == OSSL_ECH_TEST_CUSTOM || combo == OSSL_ECH_TEST_CBS) {
         res = 1;
         goto end;
     }
@@ -1334,6 +1385,7 @@ end:
     SSL_free(serverssl);
     SSL_CTX_free(cctx);
     SSL_CTX_free(sctx);
+    ch_test_cb_ok = 0;
     return res;
 }
 
@@ -1377,6 +1429,14 @@ static int ech_enoe_test(int idx)
     return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_ENOE);
 }
 
+/* Test a roundtrip with ECH, and callbacks */
+static int ech_cb_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech + callbacks test ");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CBS);
+}
+
 #endif
 
 int setup_tests(void)
@@ -1420,6 +1480,7 @@ int setup_tests(void)
     ADD_ALL_TESTS(test_ech_early, suite_combos);
     ADD_ALL_TESTS(ech_custom_test, suite_combos);
     ADD_ALL_TESTS(ech_enoe_test, suite_combos);
+    ADD_ALL_TESTS(ech_cb_test, suite_combos);
     /* TODO(ECH): add more test code as other PRs done */
     return 1;
 err:
diff --git a/test/recipes/82-test_ech_client_server.t b/test/recipes/82-test_ech_client_server.t
new file mode 100644 (file)
index 0000000..9197c52
--- /dev/null
@@ -0,0 +1,342 @@
+#! /usr/bin/env perl
+# Copyright 2023-2025 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 strict;
+use warnings;
+
+use IPC::Open3;
+use OpenSSL::Test qw/:DEFAULT srctop_file bldtop_file/;
+use OpenSSL::Test::Utils;
+use Symbol 'gensym';
+
+# servers randomly pick a port, then set this for clients to use
+# we also record the pid so we can kill it later if needed
+my $s_server_port = 0;
+my $s_server_pid = 0;
+my $s_client_match = 0;
+
+my $test_name = "test_ech_client_server";
+setup($test_name);
+
+plan skip_all => "$test_name requires EC cryptography"
+    if disabled("ec") || disabled("ecx");
+plan skip_all => "$test_name requires sock enabled"
+    if disabled("sock");
+plan skip_all => "$test_name requires TLSv1.3 enabled"
+    if disabled("tls1_3");
+plan skip_all => "$test_name is not available Windows or VMS"
+    if $^O =~ /^(VMS|MSWin32|msys)$/;
+
+plan tests => 18;
+
+my $shlib_wrap   = bldtop_file("util", "shlib_wrap.sh");
+my $apps_openssl = bldtop_file("apps", "openssl");
+
+my $echconfig_pem         = srctop_file("test", "certs", "ech-eg.pem");
+my $badconfig_pem         = srctop_file("test", "certs", "ech-mid.pem");
+my $server_pem            = srctop_file("test", "certs", "echserver.pem");
+my $server_key            = srctop_file("test", "certs", "echserver.key");
+my $root_pem              = srctop_file("test", "certs", "rootcert.pem");
+
+sub extract_ecl()
+{
+    # extract b64 encoded ECHConfigList from pem file
+    my $lb64 = "";
+    my $inwanted = 0;
+    open( my $fh, '<', $echconfig_pem ) or die "Can't open $echconfig_pem $!";
+    while( my $line = <$fh>) {
+        chomp $line;
+        if ( $line =~ /^-----BEGIN ECHCONFIG/) {
+            $inwanted = 1;
+        } elsif ( $line =~ /^-----END ECHCONFIG/) {
+            $inwanted = 0;
+        } elsif ($inwanted == 1) {
+            $lb64 .= $line;
+        }
+    }
+    print("base64 ECHConfigList: $lb64\n");
+    return($lb64);
+}
+
+my $good_b64 = extract_ecl();
+
+sub start_ech_client_server
+{
+    my ( $test_type, $winpattern ) = @_;
+
+    # start an s_server listening on some random port, with ECH enabled
+    # and willing to accept one request
+
+    # openssl s_server -accept 0 -naccept 1
+    #                  -key $server_key -cert $server_cert
+    #                  -key2 $server_key -cert2 $server_cert
+    #                  -ech_key $echconfig_pem
+    #                  -servername example.com
+    #                  -tls1_3
+    my @s_server_cmd;
+    if ($test_type eq "cid-free" ) {
+        # turn on trial-decrypt, so client can use random CID
+        @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+                         "-cert", $server_pem, "-key", $server_key,
+                         "-cert2", $server_pem, "-key2", $server_key,
+                         "-ech_key", $echconfig_pem,
+                         "-servername", "example.com",
+                         "-ech_trialdecrypt",
+                         "-tls1_3");
+    } else {
+        # default for all other tests (for now)
+        @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+                         "-cert", $server_pem, "-key", $server_key,
+                         "-cert2", $server_pem, "-key2", $server_key,
+                         "-ech_key", $echconfig_pem,
+                         "-servername", "example.com",
+                         "-ech_greaseretries",
+                         "-tls1_3");
+    }
+    print("@s_server_cmd\n");
+    $s_server_pid = open3(my $s_server_i, my $s_server_o,
+                             my $s_server_e = gensym,
+                             $shlib_wrap, $apps_openssl, @s_server_cmd);
+    # we're looking for...
+    # ACCEPT 0.0.0.0:45921
+    # ACCEPT [::]:45921
+    $s_server_port = "0";
+    while (<$s_server_o>) {
+        print($_);
+        chomp;
+        if (/^ACCEPT 0.0.0.0:(\d+)/) {
+            $s_server_port = $1;
+            last;
+        } elsif (/^ACCEPT \[::\]:(\d+)/) {
+            $s_server_port = $1;
+            last;
+        } elsif (/^Using default/) {
+            ;
+        } elsif (/^Added ECH key pair/) {
+            ;
+        } elsif (/^Loaded/) {
+            ;
+        } elsif (/^Setting secondary/) {
+            ;
+        } else {
+            last;
+        }
+    }
+    # openssl s_client -connect localhost:NNNNN
+    #                  -servername server.example
+    #                  -CAfile test/certs/rootcert.pem
+    #                  -ech_config_list "ADn+...AA="
+    #                  -prexit
+    my @s_client_cmd;
+    if ($test_type eq "GREASE-suite" ) {
+        # GREASE
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease_suite", "0x21,2,3",
+                         "-prexit");
+    } elsif ($test_type eq "lots-of-options" ) {
+        # real ECH with lots of options
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_outer_sni", "foodle.doodle",
+                         "-ech_select", "0",
+                         "-alpn", "http/1.1",
+                         "-ech_outer_alpn", "http451",
+                         "-prexit");
+    } elsif ($test_type eq "GREASE-type" ) {
+        # GREASE with suite
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease_type", "12345",
+                         "-prexit");
+    } elsif ($test_type eq "GREASE" ) {
+        # GREASE with suite
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease",
+                         "-prexit");
+    } elsif ($test_type eq "no-outer" ) {
+        # Real ECH, no outer SNI
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_no_outer_sni",
+                         "-prexit");
+    } elsif ($test_type eq "bad-ech" ) {
+        # bad ECH
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", "AEH+DQA91wAgACCBdNrnZxqNrUXSyimqqnfmNG4lHtVsbmaaIeRoUoFWFQAEAAEAAQAOc2VydmVyLmV4YW1wbGUAAA==",
+                         "-prexit");
+    } elsif ($test_type eq "cid-free" ) {
+        # Real ECH, ignore CID
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_ignore_cid",
+                         "-prexit");
+    } elsif ($test_type eq "cid-wrong" ) {
+        # Real ECH, ignore CID, no trial decrypt
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_ignore_cid",
+                         "-prexit");
+    } else {
+        # Real ECH, and default
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-prexit");
+    }
+    print("@s_client_cmd\n");
+    local (*sc_input);
+    my $s_client_pid = open3(*sc_input, my $s_client_o,
+                             my $s_client_e = gensym,
+                             $shlib_wrap, $apps_openssl, @s_client_cmd);
+    print sc_input "Q\n";
+    close(sc_input);
+    waitpid($s_client_pid, 0);
+    # the output from s_client that we want to check is written to its
+    # stdout, e.g: "^ECH: success, yay!"
+    $s_client_match = 0;
+    while (<$s_client_o>) {
+        print($_);
+        chomp;
+        if (/$winpattern/) {
+            $s_client_match = 1;
+            last;
+        }
+    }
+    my $stillthere = kill 0, $s_server_pid;
+    if ($stillthere) {
+       print("s_server process ($s_server_pid) is not dead yet.\n");
+       kill 'HUP', $s_server_pid;
+    }
+}
+
+sub basic_test {
+    print("\n\nBasic test.\n");
+    my $tt = "basic";
+    my $win = "^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with ECH on command line");
+}
+
+sub wrong_test {
+    print("\n\nWrong ECHConfig test.\n");
+    # hardcoded 'cause we want a fail
+    my $tt="bad-ech",
+    my $win="^ECH: failed.retry-configs: -105";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with bad ECH");
+}
+
+sub grease_test {
+    print("\n\nGREASE ECHConfig test.\n");
+    my $tt="GREASE";
+    my $win="^ECH: GREASE";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with GREASE ECH");
+}
+
+sub grease_suite_test {
+    print("\n\nGREASE suite ECHConfig test.\n");
+    my $tt="GREASE-suite";
+    my $win="^ECH: GREASE";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with GREASE-suite ECH");
+}
+
+sub grease_type_test {
+    print("\n\nGREASE type ECH test.\n");
+    my $tt="GREASE-type";
+    my $win="^ECH: GREASE";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with GREASE-type ECH");
+}
+
+sub lots_of_options_test {
+    print("\n\nLots of options ECH test.\n");
+    my $tt="lots-of-options";
+    my $win="^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with lots of ECH options");
+}
+
+sub no_outer_test {
+    print("\n\nNo outer SNI test.\n");
+    my $tt = "no-outer";
+    my $win = "^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with no outer SNI ECH");
+}
+
+sub cid_free_test {
+    print("\n\nIgnore CIDs test.\n");
+    my $tt = "cid-free";
+    my $win = "^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client/s_server with no CID/trial decrypt");
+}
+
+sub cid_wrong_test {
+    print("\n\nIgnore CIDs test.\n");
+    my $tt = "cid-wrong";
+    my $win = "^ECH: failed";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client/s_server with no CID/no trial decrypt");
+}
+
+basic_test();
+wrong_test();
+grease_test();
+grease_suite_test();
+grease_type_test();
+lots_of_options_test();
+no_outer_test();
+cid_free_test();
+cid_wrong_test();
+