s_client and s_server options for ECH

Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/28270)
This commit is contained in:
sftcd
2025-08-14 19:17:07 +01:00
committed by Matt Caswell
parent a732ff7aa2
commit a2e5848d9d
12 changed files with 1213 additions and 48 deletions
+1
View File
@@ -14,6 +14,7 @@ ignore-words-list =
ADDAD,
addin,
adin,
ADn,
AFAIR,
afile,
afterAll,
+189 -30
View 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;
@@ -613,6 +618,14 @@ typedef enum OPTION_choice {
OPT_KTLS,
#ifndef OPENSSL_NO_ECH
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
@@ -807,14 +820,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'" },
@@ -1009,6 +1039,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
@@ -1596,6 +1631,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;
@@ -1709,7 +1768,17 @@ 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_puts(bio_err, "Cannot supply -nextprotoneg with TLSv1.3\n");
@@ -1938,6 +2007,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_puts(bio_err, "Error setting verify params\n");
goto end;
@@ -2132,6 +2208,27 @@ 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;
@@ -2151,6 +2248,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");
@@ -2165,6 +2281,7 @@ int s_client_main(int argc, char **argv)
goto end;
}
if (!SSL_set_session(con, sess)) {
SSL_SESSION_free(sess);
BIO_puts(bio_err, "Can't set session\n");
goto end;
}
@@ -2187,11 +2304,49 @@ 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) {
@@ -3390,6 +3545,9 @@ end:
bio_c_out = NULL;
BIO_free(bio_c_msg);
bio_c_msg = NULL;
#ifndef OPENSSL_NO_ECH
OSSL_ECHSTORE_free(es);
#endif
return ret;
}
@@ -3466,11 +3624,12 @@ static void print_ech_retry_configs(BIO *bio, SSL *s)
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;
@@ -3486,6 +3645,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) {
@@ -3502,7 +3662,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);
@@ -3543,6 +3703,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;
@@ -3773,22 +3937,17 @@ static void print_stuff(BIO *bio, SSL *s, int full)
}
BIO_puts(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_puts(bio, "---\n");
#endif
+500 -1
View 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>
@@ -26,6 +29,27 @@
#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;
@@ -421,8 +454,195 @@ 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;
@@ -453,6 +673,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;
@@ -1065,6 +1287,13 @@ typedef enum OPTION_choice {
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,
@@ -1307,6 +1536,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" },
@@ -1322,6 +1564,65 @@ 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_TLS1 || o == OPT_TLS1_1 || o == OPT_TLS1_2 \
|| o == OPT_TLS1_3 || o == OPT_DTLS || o == OPT_DTLS1 || o == OPT_DTLS1_2)
@@ -1363,7 +1664,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
@@ -1401,6 +1702,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
@@ -2000,6 +2309,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;
@@ -2159,6 +2485,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)
@@ -2370,12 +2699,71 @@ 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) {
@@ -2434,6 +2822,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;
@@ -2508,9 +2903,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))
@@ -2591,10 +2998,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
@@ -2622,6 +3038,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);
@@ -3637,6 +4058,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"))
@@ -3700,6 +4125,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);
/*
+67
View File
@@ -124,6 +124,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
@@ -818,6 +826,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)
@@ -1044,6 +1109,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.
+33
View File
@@ -135,6 +135,11 @@ B<openssl> B<s_server>
{- $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
@@ -817,6 +822,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
@@ -931,6 +962,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.
The B<engine> option was removed in OpenSSL 4.0.
=head1 COPYRIGHT
+7 -6
View File
@@ -392,7 +392,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) {
@@ -518,7 +518,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),
@@ -637,15 +637,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.
+2
View 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;
}
+2 -2
View File
@@ -269,7 +269,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);
@@ -329,7 +329,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;
+1 -1
View File
@@ -1088,7 +1088,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;
+3 -3
View File
@@ -2576,14 +2576,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 */
@@ -2786,7 +2786,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);
+66 -5
View 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
@@ -1097,8 +1141,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;
@@ -1147,6 +1191,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 */
/*
@@ -1226,6 +1271,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;
@@ -1261,9 +1310,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;
}
@@ -1336,6 +1387,7 @@ end:
SSL_free(serverssl);
SSL_CTX_free(cctx);
SSL_CTX_free(sctx);
ch_test_cb_ok = 0;
return res;
}
@@ -1379,6 +1431,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)
@@ -1422,6 +1482,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:
+342
View File
@@ -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();