send ECH_REQUIRED alert if rejected, saving retry_configs correctly
diff --git a/include/picotls.h b/include/picotls.h
index 14a3b46..1382499 100644
--- a/include/picotls.h
+++ b/include/picotls.h
@@ -210,6 +210,7 @@
#define PTLS_ALERT_UNRECOGNIZED_NAME 112
#define PTLS_ALERT_CERTIFICATE_REQUIRED 116
#define PTLS_ALERT_NO_APPLICATION_PROTOCOL 120
+#define PTLS_ALERT_ECH_REQUIRED 121
/* TLS 1.2 */
#define PTLS_TLS12_MASTER_SECRET_SIZE 48
diff --git a/lib/picotls.c b/lib/picotls.c
index 25159f5..2d2ab7d 100644
--- a/lib/picotls.c
+++ b/lib/picotls.c
@@ -2812,7 +2812,8 @@
ret = PTLS_ERROR_NO_MEMORY;
goto Exit;
}
- *properties->client.ech.retry_configs = ptls_iovec_init(src, end - src);
+ memcpy(properties->client.ech.retry_configs->base, src, end - src);
+ properties->client.ech.retry_configs->len = end - src;
}
src = end;
} break;
@@ -3224,6 +3225,12 @@
goto Exit;
ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
+ /* if ECH was rejected, close the connection with ECH_REQUIRED alert after verifying messages up to Finished */
+ if (tls->client.first_ech.base != NULL && !ptls_is_ech_handshake(tls)) {
+ ret = PTLS_ALERT_ECH_REQUIRED;
+ goto Exit;
+ }
+
/* update traffic keys by using messages upto ServerFinished, but commission them after sending ClientFinished */
if ((ret = key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0))) != 0)
goto Exit;
diff --git a/t/openssl.c b/t/openssl.c
index 97e3dcf..da3c733 100644
--- a/t/openssl.c
+++ b/t/openssl.c
@@ -390,7 +390,10 @@
.cipher_suites = ptls_openssl_cipher_suites,
.tls12_cipher_suites = ptls_openssl_tls12_cipher_suites,
.certificates = {&cert, 1},
- .ech = {ptls_openssl_hpke_cipher_suites, ptls_openssl_hpke_kems, &ech_create_opener},
+ .ech = {.ciphers = ptls_openssl_hpke_cipher_suites,
+ .kems = ptls_openssl_hpke_kems,
+ .create_opener = &ech_create_opener,
+ .retry_configs = {(uint8_t *)ECH_CONFIG_LIST, sizeof(ECH_CONFIG_LIST) - 1}},
.sign_certificate = &openssl_sign_certificate.super};
assert(openssl_ctx.cipher_suites[0]->hash->digest_size == 48); /* sha384 */
ptls_context_t openssl_ctx_sha256only = openssl_ctx;
diff --git a/t/picotls.c b/t/picotls.c
index 835f9ed..4b04657 100644
--- a/t/picotls.c
+++ b/t/picotls.c
@@ -1261,6 +1261,53 @@
ptls_buffer_dispose(&sbuf);
}
+static void test_ech_config_mismatch(void)
+{
+ ptls_t *client, *server;
+ ptls_buffer_t cbuf, sbuf;
+ size_t consumed;
+ int ret;
+ ptls_iovec_t retry_configs = {NULL};
+ ptls_handshake_properties_t client_hs_prop = {
+ .client.ech = {
+ .configs = ptls_iovec_init((void *)ECH_ALTERNATIVE_CONFIG_LIST, sizeof(ECH_ALTERNATIVE_CONFIG_LIST) - 1),
+ .retry_configs = &retry_configs,
+ }};
+
+ client = ptls_new(ctx, 0);
+ server = ptls_new(ctx_peer, 1);
+ ptls_buffer_init(&cbuf, "", 0);
+ ptls_buffer_init(&sbuf, "", 0);
+
+ ret = ptls_handshake(client, &cbuf, NULL, NULL, &client_hs_prop);
+ ok(ret == PTLS_ERROR_IN_PROGRESS);
+
+ consumed = cbuf.off;
+ ret = ptls_handshake(server, &sbuf, cbuf.base, &consumed, NULL);
+ ok(ret == 0);
+ ok(cbuf.off == consumed);
+ cbuf.off = 0;
+
+ consumed = sbuf.off;
+ ret = ptls_handshake(client, &cbuf, sbuf.base, &consumed, &client_hs_prop);
+ ok(ret == PTLS_ALERT_ECH_REQUIRED);
+ ok(sbuf.off == consumed);
+ ok(retry_configs.len == sizeof(ECH_CONFIG_LIST) - 1);
+ ok(memcmp(retry_configs.base, ECH_CONFIG_LIST, retry_configs.len) == 0);
+ sbuf.off = 0;
+
+ consumed = cbuf.off;
+ ret = ptls_handshake(server, &sbuf, cbuf.base, &consumed, NULL);
+ ok(ret == PTLS_ALERT_TO_PEER_ERROR(PTLS_ALERT_ECH_REQUIRED));
+ ok(cbuf.off == consumed);
+
+ ptls_free(client);
+ ptls_free(server);
+ ptls_buffer_dispose(&cbuf);
+ ptls_buffer_dispose(&sbuf);
+ free(retry_configs.base);
+}
+
typedef uint8_t traffic_secrets_t[2 /* is_enc */][4 /* epoch */][PTLS_MAX_DIGEST_SIZE /* octets */];
static int on_update_traffic_key(ptls_update_traffic_key_t *self, ptls_t *tls, int is_enc, size_t epoch, const void *secret)
@@ -1596,10 +1643,8 @@
ctx->ech.ciphers = NULL;
subtest("ech (server-only)", test_all_handshakes_core);
ctx->ech.ciphers = orig_ech_ciphers.client;
- ctx_peer->ech.ciphers = NULL;
- subtest("ech (client-only)", test_all_handshakes_core);
- ctx_peer->ech.ciphers = orig_ech_ciphers.server;
}
+ subtest("ech-config-match", test_ech_config_mismatch);
}
ctx_peer->sign_certificate = sc_orig;
diff --git a/t/test.h b/t/test.h
index 66667ef..5513ce4 100644
--- a/t/test.h
+++ b/t/test.h
@@ -51,12 +51,17 @@
"\x1d\x99\x42\xe0\xa2\xb7\x75\xbb\x14\x03\x79\x9a\xf6\x07\xd8\xa5\xab\x2b\x3a\x70\x8b\x77\x85\x70\x8a\x98\x38\x9b\x35\x09\xf6" \
"\x62\x6b\x29\x4a\xa7\xa7\xf9\x3b\xde\xd8\xc8\x90\x57\xf2\x76\x2a\x23\x0b\x01\x68\xc6\x9a\xf2"
-/* test vector from RFC 9180 A.3 */
+/* test vector using RFC 9180 A.3 */
#define ECH_CONFIG_LIST \
"\x00\x63\xfe\x0d\x00\x5f\x12\x00\x10\x00\x41\x04\xfe\x8c\x19\xce\x09\x05\x19\x1e\xbc\x29\x8a\x92\x45\x79\x25\x31\xf2\x6f\x0c" \
"\xec\xe2\x46\x06\x39\xe8\xbc\x39\xcb\x7f\x70\x6a\x82\x6a\x77\x9b\x4c\xf9\x69\xb8\xa0\xe5\x39\xc7\xf6\x2f\xb3\xd3\x0a\xd6\xaa" \
"\x8f\x80\xe3\x0f\x1d\x12\x8a\xaf\xd6\x8a\x2c\xe7\x2e\xa0\x00\x08\x00\x02\x00\x02\x00\x01\x00\x01\x40\x0b\x65\x78\x61\x6d\x70" \
"\x6c\x65\x2e\x63\x6f\x6d\x00\x00"
+/* key from a third party (that we do not know the corresponding private key) */
+#define ECH_ALTERNATIVE_CONFIG_LIST \
+ "\x00\x46\xfe\x0d\x00\x42\xf4\x00\x20\x00\x20\x53\x85\xed\xda\xc8\x63\xe8\x02\x00\x05\x12\xcf\xc0\x45\xda\xda\x26\x37\x93\x17" \
+ "\xe5\xfe\xe8\x9f\x0e\x14\xb5\xad\x78\x4b\x99\x78\x00\x04\x00\x01\x00\x01\x00\x13\x63\x6c\x6f\x75\x64\x66\x6c\x61\x72\x65\x2d" \
+ "\x65\x73\x6e\x69\x2e\x63\x6f\x6d\x00\x00"
#define ECH_PRIVATE_KEY \
"-----BEGIN PRIVATE KEY-----\n" \
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg885/2uV+GjENh/Hr\n" \