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;
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;
{"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'"},
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
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;
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");
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;
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;
}
}
+# 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");
goto end;
}
if (!SSL_set_session(con, sess)) {
+ SSL_SESSION_free(sess);
BIO_printf(bio_err, "Can't set session\n");
goto end;
}
}
# 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) {
bio_c_out = NULL;
BIO_free(bio_c_msg);
bio_c_msg = NULL;
+# ifndef OPENSSL_NO_ECH
+ OSSL_ECHSTORE_free(es);
+# endif
return ret;
}
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;
return;
}
+/* outcomes marked as "odd" shouldn't happen in s_client */
static void print_ech_status(BIO *bio, SSL *s, int estat)
{
switch (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);
#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;
}
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
#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
/*
#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);
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;
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;
return SSL_TLSEXT_ERR_OK;
}
+# endif
+
/* Structure passed to cert status callback */
typedef struct tlsextstatusctx_st {
int timeout;
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,
#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"},
{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)
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
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
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;
if (s_cert2 == NULL)
goto end;
+# ifndef OPENSSL_NO_ECH
+ tlsextcbp.scert = s_cert2;
+# endif
}
}
#if !defined(OPENSSL_NO_NEXTPROTONEG)
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) {
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;
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))
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
#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);
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"))
}
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, "<");
+ break;
+ case '>':
+ BIO_puts(io, ">");
+ break;
+ case '&':
+ BIO_puts(io, "&");
+ break;
+ default:
+ BIO_write(io, myp, 1);
+ break;
+ }
+ BIO_write(io, " ", 1);
+ }
+ BIO_puts(io, "\n");
+# endif
+
ssl_print_secure_renegotiation_notes(io, con);
/*
[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
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)
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.
{- $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
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
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.
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) {
}
/* 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),
/* 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.
if (s->ext.ech.grease_suite == NULL)
return 0;
s->ext.ech.attempted = 1;
+ s->ext.ech.grease = OSSL_ECH_IS_GREASE;
return 1;
}
return 0;
s->ext.ech.attempted_type = type;
s->ext.ech.attempted = 1;
+ s->ext.ech.grease = OSSL_ECH_IS_GREASE;
return 1;
}
/* 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);
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;
#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;
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 */
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);
#include <openssl/hpke.h>
#include "testutil.h"
#include "helpers/ssltestlib.h"
+#include "internal/packet.h"
#ifndef OPENSSL_NO_ECH
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
|| !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;
# 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 */
/*
&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;
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;
}
SSL_free(serverssl);
SSL_CTX_free(cctx);
SSL_CTX_free(sctx);
+ ch_test_cb_ok = 0;
return res;
}
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)
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:
--- /dev/null
+#! /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();
+