Commit 60dba6e93b for openssl.org
commit 60dba6e93b28d8269a8e223793914a2796d8640d
Author: sftcd <stephen.farrell@cs.tcd.ie>
Date: Wed Nov 4 12:13:53 2026 +0000
If ECH failed, check outer ALPN used if it was set
Reviewed-by: Richard Levitte <levitte@openssl.foundation>
Reviewed-by: Frederik Wedel-Heinen <fwh.openssl@gmail.com>
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Merge-date: Sun Nov 8 09:51:00 2026
Merged-from: https://github.com/openssl/openssl/pull/33415
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index a5fef5c1f2..edb4d19bab 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -2102,6 +2102,8 @@ int tls_parse_stoc_alpn(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
size_t len;
PACKET confpkt, protpkt;
int valid = 0;
+ size_t eff_alpn_len = 0;
+ unsigned char *eff_alpn = NULL;
/* We must have requested it. */
if (!s->s3.alpn_sent) {
@@ -2121,8 +2123,21 @@ int tls_parse_stoc_alpn(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
return 0;
}
+ eff_alpn = s->ext.alpn;
+ eff_alpn_len = s->ext.alpn_len;
+#ifndef OPENSSL_NO_ECH
+ /*
+ * If we tried ECH and it failed, then we need to compare vs. the
+ * outer alpn, if one was set.
+ */
+ if (s->ext.ech.es != NULL && s->ext.ech.success == 0
+ && s->ext.ech.alpn_outer != NULL) {
+ eff_alpn = s->ext.ech.alpn_outer;
+ eff_alpn_len = s->ext.ech.alpn_outer_len;
+ }
+#endif
/* It must be a protocol that we sent */
- if (!PACKET_buf_init(&confpkt, s->ext.alpn, s->ext.alpn_len)) {
+ if (!PACKET_buf_init(&confpkt, eff_alpn, eff_alpn_len)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
diff --git a/test/ech_test.c b/test/ech_test.c
index e00c618b05..ad38ae2a7f 100644
--- a/test/ech_test.c
+++ b/test/ech_test.c
@@ -1813,6 +1813,166 @@ end:
return res;
}
+/* ALPN callback selector for ech_alpn_in_out_test */
+static const char *inneralpn = "inner";
+static const char *outeralpn = "outer";
+static int alpn_select_cb(SSL *ssl, const unsigned char **out,
+ unsigned char *outlen, const unsigned char *in,
+ unsigned int inlen, void *arg)
+{
+ unsigned int protlen = 0;
+ const unsigned char *prot;
+
+ for (prot = in; prot < in + inlen; prot += protlen) {
+ protlen = *prot++;
+ if (in + inlen < prot + protlen)
+ return SSL_TLSEXT_ERR_NOACK;
+ if (protlen == strlen(inneralpn)
+ && memcmp(prot, inneralpn, protlen) == 0) {
+ *out = prot;
+ *outlen = protlen;
+ return SSL_TLSEXT_ERR_OK;
+ }
+ if (protlen == strlen(outeralpn)
+ && memcmp(prot, outeralpn, protlen) == 0) {
+ *out = prot;
+ *outlen = protlen;
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+ return SSL_TLSEXT_ERR_NOACK;
+}
+/*
+ * Test roundtrip with inner/outer ALPN variations and
+ * ensure errors and retry-configs are handled properly.
+ * There's a few cases to test, as usual we pick between
+ * 'em using the idx parameter.
+ *
+ * idx : case
+ * 0 : nominal operation, ECH success, set inner and outer alpns
+ * inner alpn used
+ * 1 : bad ECHConfig, ECH required, outer alpn used and retry-configs
+ * provided and available
+ * 2 : bad ECHConfig, ECH required, no outer alpn set (so we get
+ * inner) and retry-configs provided and available
+ */
+static int ech_alpn_in_out_test(int idx)
+{
+ int res = 0, clientstatus, serverstatus;
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL;
+ char *supplied_inner = "inner.server.example"; /* inner set via ECH API */
+ char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL;
+ unsigned char alpn_inner[] = { /* "inner, secret, http/1.1" */
+ 0x05, 0x69, 0x6e, 0x6e, 0x65, 0x72,
+ 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
+ 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31
+ };
+ size_t alpn_inner_len = sizeof(alpn_inner);
+ unsigned char alpn_outer[] = { /* "outer, public, h2" */
+ 0x05, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x06, 0x70,
+ 0x75, 0x62, 0x6c, 0x69, 0x63, 0x02, 0x68, 0x32
+ };
+ size_t alpn_outer_len = sizeof(alpn_outer);
+ const unsigned char *sel_alpn = NULL;
+ unsigned int sel_alpn_len = 0;
+ unsigned char *retryconfig = NULL;
+ size_t retryconfiglen = 0;
+ BIO *in = NULL;
+ OSSL_ECHSTORE *es = NULL;
+
+ /* set up client/server using pem_kp2 */
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp2, (int)strlen(pem_kp2)) <= 0
+ || !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq))
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY))
+ || !TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+ TLS_client_method(), TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey))
+ || !TEST_true(SSL_CTX_set1_echstore(sctx, es))
+ || !TEST_false(SSL_CTX_set_alpn_protos(cctx, alpn_inner,
+ (unsigned int)alpn_inner_len))
+ || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+ &clientssl, NULL, NULL))
+ || !TEST_true(SSL_set_tlsext_host_name(clientssl, supplied_inner)))
+ goto end;
+ /* for test 0 and 1, setup outer alpn, but not for test 2 */
+ if (idx != 2 && !TEST_true(SSL_ech_set1_outer_alpn_protos(clientssl, alpn_outer, alpn_outer_len)))
+ goto end;
+ /* setup alpn callback for server */
+ SSL_CTX_set_alpn_select_cb(sctx, alpn_select_cb, NULL);
+ if (idx == 0) {
+ /* set the correct ECHConfigList on client */
+ if (!TEST_true(SSL_set1_ech_config_list(clientssl,
+ (unsigned char *)ec_kp2, ec_kp2len)))
+ goto end;
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+ }
+ if (idx == 1 || idx == 2) {
+ /* set the wrong ECHConfigList on client */
+ if (!TEST_true(SSL_set1_ech_config_list(clientssl,
+ (unsigned char *)ec_kp1, ec_kp1len)))
+ goto end;
+ if (!TEST_false(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_SSL))
+ || !TEST_int_eq(ERR_GET_REASON(ERR_peek_last_error()),
+ SSL_R_ECH_REQUIRED))
+ goto end;
+ }
+ /* get ECH results */
+ serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+ SSL_set_verify_result(clientssl, X509_V_OK); /* override cert check */
+ clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
+ /* check expected ECH and alpn results */
+ SSL_get0_alpn_selected(clientssl, &sel_alpn, &sel_alpn_len);
+ switch (idx) {
+ case 0:
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)
+ || !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)
+ || !TEST_mem_eq(sel_alpn, sel_alpn_len, inneralpn, strlen(inneralpn)))
+ goto end;
+ break;
+ case 1:
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_GREASE)
+ || !TEST_int_eq(clientstatus, SSL_ECH_STATUS_FAILED_ECH)
+ || !TEST_mem_eq(sel_alpn, sel_alpn_len, outeralpn, strlen(outeralpn)))
+ goto end;
+ /* if that happened, we should get retry-configs */
+ if (!TEST_true(SSL_ech_get1_retry_config(clientssl, &retryconfig,
+ &retryconfiglen)))
+ goto end;
+ break;
+ case 2:
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_GREASE)
+ || !TEST_int_eq(clientstatus, SSL_ECH_STATUS_FAILED_ECH)
+ || !TEST_mem_eq(sel_alpn, sel_alpn_len, inneralpn, strlen(inneralpn)))
+ goto end;
+ if (!TEST_true(SSL_ech_get1_retry_config(clientssl, &retryconfig,
+ &retryconfiglen)))
+ goto end;
+ break;
+ default:
+ TEST_error("Should not get here");
+ goto end;
+ }
+ res = 1; /* all good */
+end:
+ OSSL_ECHSTORE_free(es);
+ OPENSSL_free(sinner);
+ OPENSSL_free(souter);
+ OPENSSL_free(cinner);
+ OPENSSL_free(couter);
+ BIO_free_all(in);
+ SSL_free(clientssl);
+ SSL_free(serverssl);
+ SSL_CTX_free(cctx);
+ SSL_CTX_free(sctx);
+ OPENSSL_free(retryconfig);
+ return res;
+}
+
/* Test roundtrip with GREASE'd ECH, then again with retry-config */
static int ech_grease_test(int idx)
{
@@ -2559,6 +2719,7 @@ int setup_tests(void)
ADD_ALL_TESTS(ech_cb_test, suite_combos);
ADD_ALL_TESTS(ech_v12_test, suite_combos);
ADD_ALL_TESTS(ech_in_out_test, 14);
+ ADD_ALL_TESTS(ech_alpn_in_out_test, 3);
ADD_ALL_TESTS(ech_grease_test, 4);
ADD_ALL_TESTS(test_ech_no_inner, suite_combos);
ADD_ALL_TESTS(test_ech_keylog_random, 4);