Merge pull request #453 from h2o/kazuho/ech-grease
add support for ECH greasing
diff --git a/include/picotls.h b/include/picotls.h
index 972bc9d..fe74d40 100644
--- a/include/picotls.h
+++ b/include/picotls.h
@@ -579,6 +579,7 @@
typedef const struct st_ptls_hpke_cipher_suite_t {
ptls_hpke_cipher_suite_id_t id;
+ const char *name; /* in form of "<kdf>/<aead>" using the sames specified in IANA HPKE registry */
ptls_hash_algorithm_t *hash;
ptls_aead_algorithm_t *aead;
} ptls_hpke_cipher_suite_t;
@@ -942,7 +943,8 @@
*/
struct {
/**
- * config offered by server e.g., by HTTPS RR
+ * Config offered by server e.g., by HTTPS RR. If config.base is non-NULL but config.len is zero, a grease ECH will
+ * be sent, assuming that X25519-SHA256 KEM and SHA256-AES-128-GCM HPKE cipher is available.
*/
ptls_iovec_t configs;
/**
diff --git a/lib/openssl.c b/lib/openssl.c
index db3f98b..511315e 100644
--- a/lib/openssl.c
+++ b/lib/openssl.c
@@ -2009,19 +2009,23 @@
ptls_hpke_cipher_suite_t ptls_openssl_hpke_aes128gcmsha256 = {
.id = {.kdf = PTLS_HPKE_HKDF_SHA256, .aead = PTLS_HPKE_AEAD_AES_128_GCM},
+ .name = "HKDF-SHA256/AES-128-GCM",
.hash = &ptls_openssl_sha256,
.aead = &ptls_openssl_aes128gcm};
ptls_hpke_cipher_suite_t ptls_openssl_hpke_aes128gcmsha512 = {
.id = {.kdf = PTLS_HPKE_HKDF_SHA512, .aead = PTLS_HPKE_AEAD_AES_128_GCM},
+ .name = "HKDF-SHA512/AES-128-GCM",
.hash = &ptls_openssl_sha512,
.aead = &ptls_openssl_aes128gcm};
ptls_hpke_cipher_suite_t ptls_openssl_hpke_aes256gcmsha384 = {
.id = {.kdf = PTLS_HPKE_HKDF_SHA384, .aead = PTLS_HPKE_AEAD_AES_256_GCM},
+ .name = "HKDF-SHA384/AES-256-GCM",
.hash = &ptls_openssl_sha384,
.aead = &ptls_openssl_aes256gcm};
#if PTLS_OPENSSL_HAVE_CHACHA20_POLY1305
ptls_hpke_cipher_suite_t ptls_openssl_hpke_chacha20poly1305sha256 = {
.id = {.kdf = PTLS_HPKE_HKDF_SHA256, .aead = PTLS_HPKE_AEAD_CHACHA20POLY1305},
+ .name = "HKDF-SHA256/ChaCha20Poly1305",
.hash = &ptls_openssl_sha256,
.aead = &ptls_openssl_chacha20poly1305};
#endif
diff --git a/lib/picotls.c b/lib/picotls.c
index 976212f..83ac410 100644
--- a/lib/picotls.c
+++ b/lib/picotls.c
@@ -172,6 +172,7 @@
*/
struct st_ptls_ech_t {
uint8_t offered : 1;
+ uint8_t offered_grease : 1;
uint8_t accepted : 1;
uint8_t config_id;
ptls_hpke_kem_t *kem;
@@ -1161,6 +1162,50 @@
return ret;
}
+static void client_setup_ech_grease(struct st_ptls_ech_t *ech, void (*random_bytes)(void *, size_t), ptls_hpke_kem_t **kems,
+ ptls_hpke_cipher_suite_t **ciphers, const char *sni_name)
+{
+ static const size_t x25519_key_size = 32;
+ uint8_t random_secret[PTLS_AES128_KEY_SIZE + PTLS_AES_IV_SIZE];
+
+ /* pick up X25519, AES-128-GCM or bail out */
+ for (size_t i = 0; kems[i] != NULL; ++i) {
+ if (kems[i]->id == PTLS_HPKE_KEM_X25519_SHA256) {
+ ech->kem = kems[i];
+ break;
+ }
+ }
+ for (size_t i = 0; ciphers[i] != NULL; ++i) {
+ if (ciphers[i]->id.kdf == PTLS_HPKE_HKDF_SHA256 && ciphers[i]->id.aead == PTLS_HPKE_AEAD_AES_128_GCM) {
+ ech->cipher = ciphers[i];
+ break;
+ }
+ }
+ if (ech->kem == NULL || ech->cipher == NULL)
+ goto Fail;
+
+ /* aead is generated from random */
+ random_bytes(random_secret, sizeof(random_secret));
+ ech->aead = ptls_aead_new_direct(ech->cipher->aead, 1, random_secret, random_secret + PTLS_AES128_KEY_SIZE);
+
+ /* `enc` is random bytes */
+ if ((ech->client.enc.base = malloc(x25519_key_size)) == NULL)
+ goto Fail;
+ ech->client.enc.len = x25519_key_size;
+ random_bytes(ech->client.enc.base, ech->client.enc.len);
+
+ /* setup the rest (inner_client_random is left zeros) */
+ random_bytes(&ech->config_id, sizeof(ech->config_id));
+ ech->client.max_name_length = 64;
+ if ((ech->client.public_name = duplicate_as_str(sni_name, strlen(sni_name))) == NULL)
+ goto Fail;
+
+ return;
+
+Fail:
+ clear_ech(ech, 0);
+}
+
#define ECH_CONFIRMATION_SERVER_HELLO "ech accept confirmation"
#define ECH_CONFIRMATION_HRR "hrr ech accept confirmation"
static int ech_calc_confirmation(ptls_key_schedule_t *sched, void *dst, const uint8_t *inner_random, const char *label,
@@ -2265,13 +2310,18 @@
if (properties != NULL) {
/* try to use ECH (ignore broken ECHConfigList; it is delivered insecurely) */
- if (!is_second_flight && sni_name != NULL && tls->ctx->ech.client.ciphers != NULL &&
- properties->client.ech.configs.len != 0) {
- struct st_decoded_ech_config_t decoded;
- client_decode_ech_config_list(tls->ctx, &decoded, properties->client.ech.configs);
- if (decoded.kem != NULL && decoded.cipher != NULL) {
- if ((ret = client_setup_ech(&tls->ech, &decoded, tls->ctx->random_bytes)) != 0)
- goto Exit;
+ if (!is_second_flight && sni_name != NULL && tls->ctx->ech.client.ciphers != NULL) {
+ if (properties->client.ech.configs.len != 0) {
+ struct st_decoded_ech_config_t decoded;
+ client_decode_ech_config_list(tls->ctx, &decoded, properties->client.ech.configs);
+ if (decoded.kem != NULL && decoded.cipher != NULL) {
+ if ((ret = client_setup_ech(&tls->ech, &decoded, tls->ctx->random_bytes)) != 0)
+ goto Exit;
+ }
+ } else {
+ /* zero-length config indicates ECH greasing */
+ client_setup_ech_grease(&tls->ech, tls->ctx->random_bytes, tls->ctx->ech.client.kems, tls->ctx->ech.client.ciphers,
+ sni_name);
}
}
/* setup resumption-related data. If successful, resumption_secret becomes a non-zero value. */
@@ -2404,7 +2454,11 @@
memcpy(tls->ech.client.first_ech.base,
emitter->buf->base + ech_size_offset - outer_ech_header_size(tls->ech.client.enc.len), len);
tls->ech.client.first_ech.len = len;
- tls->ech.offered = 1;
+ if (properties->client.ech.configs.len != 0) {
+ tls->ech.offered = 1;
+ } else {
+ tls->ech.offered_grease = 1;
+ }
}
/* update hash */
ptls__key_schedule_update_hash(tls->key_schedule, emitter->buf->base + mess_start, emitter->buf->off - mess_start, 1);
@@ -2546,7 +2600,7 @@
break;
case PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO:
assert(sh->is_retry_request);
- if (!tls->ech.offered) {
+ if (!(tls->ech.offered || tls->ech.offered_grease)) {
ret = PTLS_ALERT_UNSUPPORTED_EXTENSION;
goto Exit;
}
@@ -2659,6 +2713,8 @@
key_schedule_select_outer(tls->key_schedule);
Exit:
+ PTLS_PROBE(ECH_SELECTION, tls, !!tls->ech.accepted);
+ PTLS_LOG_CONN(ech_selection, tls, { PTLS_LOG_ELEMENT_BOOL(is_ech, tls->ech.accepted); });
ptls_clear_memory(confirm_hash_expected, sizeof(confirm_hash_expected));
return ret;
}
@@ -2682,10 +2738,17 @@
if ((ret = key_schedule_select_cipher(tls->key_schedule, tls->cipher_suite, 0)) != 0)
goto Exit;
key_schedule_transform_post_ch1hash(tls->key_schedule);
- if (tls->ech.aead != NULL &&
- (ret = client_ech_select_hello(tls, message, sh.retry_request.ech != NULL ? sh.retry_request.ech - message.base : 0,
- ECH_CONFIRMATION_HRR)) != 0)
- goto Exit;
+ if (tls->ech.aead != NULL) {
+ size_t confirm_hash_off = 0;
+ if (tls->ech.offered) {
+ if (sh.retry_request.ech != NULL)
+ confirm_hash_off = sh.retry_request.ech - message.base;
+ } else {
+ assert(tls->ech.offered_grease);
+ }
+ if ((ret = client_ech_select_hello(tls, message, confirm_hash_off, ECH_CONFIRMATION_HRR)) != 0)
+ goto Exit;
+ }
ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
return handle_hello_retry_request(tls, emitter, &sh, message, properties);
}
@@ -2695,9 +2758,14 @@
goto Exit;
/* check if ECH is accepted */
- static const size_t confirm_hash_off =
- PTLS_HANDSHAKE_HEADER_SIZE + 2 /* legacy_version */ + PTLS_HELLO_RANDOM_SIZE - PTLS_ECH_CONFIRM_LENGTH;
if (tls->ech.aead != NULL) {
+ size_t confirm_hash_off = 0;
+ if (tls->ech.offered) {
+ confirm_hash_off =
+ PTLS_HANDSHAKE_HEADER_SIZE + 2 /* legacy_version */ + PTLS_HELLO_RANDOM_SIZE - PTLS_ECH_CONFIRM_LENGTH;
+ } else {
+ assert(tls->ech.offered_grease);
+ }
if ((ret = client_ech_select_hello(tls, message, confirm_hash_off, ECH_CONFIRMATION_SERVER_HELLO)) != 0)
goto Exit;
}
@@ -2837,7 +2905,7 @@
break;
case PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO: {
/* accept retry_configs only if we offered ECH but rejected */
- if (!(tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, NULL))) {
+ if (!((tls->ech.offered || tls->ech.offered_grease) && !ptls_is_ech_handshake(tls, NULL, NULL))) {
ret = PTLS_ALERT_UNSUPPORTED_EXTENSION;
goto Exit;
}
@@ -4198,6 +4266,10 @@
ch->ech.cipher_suite, ch->ech.enc, ptls_iovec_init(ech_info_prefix, sizeof(ech_info_prefix)))) != NULL)
tls->ech.config_id = ch->ech.config_id;
}
+ if (!is_second_flight) {
+ PTLS_PROBE(ECH_SELECTION, tls, tls->ech.aead != NULL);
+ PTLS_LOG_CONN(ech_selection, tls, { PTLS_LOG_ELEMENT_BOOL(is_ech, tls->ech.aead != NULL); });
+ }
if (tls->ech.aead != NULL) {
/* now that AEAD context is available, create AAD and decrypt inner CH */
if ((ech.encoded_ch_inner = malloc(ch->ech.payload.len - tls->ech.aead->algo->tag_size)) == NULL ||
diff --git a/picotls-probes.d b/picotls-probes.d
index 614a571..fbafa83 100644
--- a/picotls-probes.d
+++ b/picotls-probes.d
@@ -26,4 +26,5 @@
probe client_random(struct st_ptls_t *tls, const void *bytes);
probe receive_message(struct st_ptls_t *tls, uint8_t message, const void *bytes, size_t len, int result);
probe new_secret(struct st_ptls_t *tls, const char *label, const char *secret_hex);
+ probe ech_selection(struct st_ptls_t *tls, int is_ech);
};
diff --git a/picotls.xcodeproj/project.pbxproj b/picotls.xcodeproj/project.pbxproj
index ef65447..9a917b3 100644
--- a/picotls.xcodeproj/project.pbxproj
+++ b/picotls.xcodeproj/project.pbxproj
@@ -203,6 +203,7 @@
081F00CC291A358800534A86 /* asn1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1.h; sourceTree = "<group>"; };
081F00CD291A358800534A86 /* pembase64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pembase64.h; sourceTree = "<group>"; };
081F00CE291A358800534A86 /* ptlsbcrypt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ptlsbcrypt.h; sourceTree = "<group>"; };
+ 08B3298229419DFC009D6766 /* ech-live.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "ech-live.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
08F0FDF52910F67A00EE657D /* hpke.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hpke.c; sourceTree = "<group>"; };
105900241DC8D37500FB4085 /* aes.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = aes.c; path = src/aes.c; sourceTree = "<group>"; };
105900251DC8D37500FB4085 /* aes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = aes.h; path = src/aes.h; sourceTree = "<group>"; };
@@ -260,7 +261,7 @@
E95EBCCA227EA0180022C32D /* dtrace-utils.cmake */ = {isa = PBXFileReference; lastKnownFileType = text; path = "dtrace-utils.cmake"; sourceTree = "<group>"; };
E97577002212405300D1EF74 /* ffx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ffx.h; sourceTree = "<group>"; };
E97577022212405D00D1EF74 /* ffx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ffx.c; sourceTree = "<group>"; };
- E97577072213148800D1EF74 /* e2e.t */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.perl; path = e2e.t; sourceTree = "<group>"; };
+ E97577072213148800D1EF74 /* e2e.t */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.perl; path = e2e.t; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
E99B75DE1F5CDDB500CF503E /* asn1.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = asn1.c; sourceTree = "<group>"; };
E99B75DF1F5CDDB500CF503E /* pembase64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pembase64.c; sourceTree = "<group>"; };
E9B43DBF24619D1700824E51 /* fusion.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = fusion.c; sourceTree = "<group>"; };
@@ -452,6 +453,7 @@
children = (
106530FE1DAD8A3C005B2C60 /* cli.c */,
E97577072213148800D1EF74 /* e2e.t */,
+ 08B3298229419DFC009D6766 /* ech-live.t */,
E9B43DE224619D7E00824E51 /* fusion.c */,
081F00C92918823200534A86 /* hpke.c */,
1059003D1DC8D4E300FB4085 /* minicrypto.c */,
diff --git a/t/cli.c b/t/cli.c
index 18bba34..67bfd4d 100644
--- a/t/cli.c
+++ b/t/cli.c
@@ -57,21 +57,6 @@
/* sentinels indicating that the endpoint is in benchmark mode */
static const char input_file_is_benchmark[] = "is:benchmark";
-static struct {
- ptls_iovec_t config_list;
- struct {
- struct {
- ptls_hpke_kem_t *kem;
- ptls_key_exchange_context_t *ctx;
- } list[16];
- size_t count;
- } keyex;
- struct {
- ptls_iovec_t configs;
- char *fn;
- } retry;
-} ech;
-
static ptls_hpke_kem_t *find_kem(ptls_key_exchange_algorithm_t *algo)
{
for (size_t i = 0; ptls_openssl_hpke_kems[i] != NULL; ++i)
@@ -82,65 +67,6 @@
return NULL;
}
-static ptls_aead_context_t *create_ech_opener(ptls_ech_create_opener_t *self, ptls_hpke_kem_t **kem,
- ptls_hpke_cipher_suite_t **cipher, ptls_t *tls, uint8_t config_id,
- ptls_hpke_cipher_suite_id_t cipher_id, ptls_iovec_t enc, ptls_iovec_t info_prefix)
-{
- const uint8_t *src = ech.config_list.base, *const end = src + ech.config_list.len;
- size_t index = 0;
- int ret = 0;
-
- /* look for the cipher implementation; this should better be specific to each ECHConfig (as each of them may advertise different
- * set of values) */
- *cipher = NULL;
- for (size_t i = 0; ptls_openssl_hpke_cipher_suites[i] != NULL; ++i) {
- if (ptls_openssl_hpke_cipher_suites[i]->id.kdf == cipher_id.kdf &&
- ptls_openssl_hpke_cipher_suites[i]->id.aead == cipher_id.aead) {
- *cipher = ptls_openssl_hpke_cipher_suites[i];
- break;
- }
- }
- if (*cipher == NULL)
- goto Exit;
-
- ptls_decode_open_block(src, end, 2, {
- uint16_t version;
- if ((ret = ptls_decode16(&version, &src, end)) != 0)
- goto Exit;
- do {
- ptls_decode_open_block(src, end, 2, {
- if (src == end) {
- ret = PTLS_ALERT_DECODE_ERROR;
- goto Exit;
- }
- if (*src == config_id) {
- /* this is the ECHConfig that we have been looking for */
- if (index >= ech.keyex.count) {
- fprintf(stderr, "ECH key missing for config %zu\n", index);
- return NULL;
- }
- uint8_t *info = malloc(info_prefix.len + end - (src - 4));
- memcpy(info, info_prefix.base, info_prefix.len);
- memcpy(info + info_prefix.len, src - 4, end - (src - 4));
- ptls_aead_context_t *aead;
- ptls_hpke_setup_base_r(ech.keyex.list[index].kem, *cipher, ech.keyex.list[index].ctx, &aead, enc,
- ptls_iovec_init(info, info_prefix.len + end - (src - 4)));
- free(info);
- *kem = ech.keyex.list[index].kem;
- return aead;
- }
- ++index;
- src = end;
- });
- } while (src != end);
- });
-
-Exit:
- if (ret != 0)
- fprintf(stderr, "ECH decode error:%d\n", ret);
- return NULL;
-}
-
static void shift_buffer(ptls_buffer_t *buf, size_t delta)
{
if (delta != 0) {
@@ -243,6 +169,7 @@
if ((ret = ptls_handshake(tls, &encbuf, bytebuf + off, &leftlen, hsprop)) == 0) {
state = IN_1RTT;
assert(ptls_is_server(tls) || hsprop->client.early_data_acceptance != PTLS_EARLY_DATA_ACCEPTANCE_UNKNOWN);
+ ech_save_retry_configs();
/* release data sent as early-data, if server accepted it */
if (hsprop->client.early_data_acceptance == PTLS_EARLY_DATA_ACCEPTED)
shift_buffer(&ptbuf, early_bytes_sent);
@@ -253,15 +180,7 @@
} else {
if (ret == PTLS_ALERT_ECH_REQUIRED) {
assert(!ptls_is_server(tls));
- if (ech.retry.configs.base != NULL) {
- FILE *fp;
- if ((fp = fopen(ech.retry.fn, "wt")) == NULL) {
- fprintf(stderr, "failed to write to ECH config file:%s:%s\n", ech.retry.fn, strerror(errno));
- exit(1);
- }
- fwrite(ech.retry.configs.base, 1, ech.retry.configs.len, fp);
- fclose(fp);
- }
+ ech_save_retry_configs();
}
if (encbuf.off != 0)
repeat_while_eintr(write(sockfd, encbuf.base, encbuf.off), { break; });
@@ -465,8 +384,9 @@
" -N named-group named group to be used (default: secp256r1)\n"
" -s session-file file to read/write the session ticket\n"
" -S require public key exchange when resuming a session\n"
- " -E echconfiglist file that contains ECHConfigList; overwritten when\n"
- " receiving retry_configs from the server\n"
+ " -E echconfiglist file that contains ECHConfigList or an empty file to\n"
+ " grease ECH; will be overwritten when receiving\n"
+ " retry_configs from the server\n"
" -e when resuming a session, send first 8,192 bytes of input\n"
" as early data\n"
" -r public-key-file use raw public keys (RFC 7250). When set and running as a\n"
@@ -520,7 +440,6 @@
ptls_key_exchange_algorithm_t *key_exchanges[128] = {NULL};
ptls_cipher_suite_t *cipher_suites[128] = {NULL};
- ptls_ech_create_opener_t ech_opener = {.cb = create_ech_opener};
ptls_context_t ctx = {
.random_bytes = ptls_openssl_random_bytes,
.get_time = &ptls_get_time,
@@ -594,42 +513,12 @@
case 'S':
ctx.require_dhe_on_psk = 1;
break;
- case 'E': {
- FILE *fp;
- if ((fp = fopen(optarg, "rt")) == NULL) {
- fprintf(stderr, "failed to open ECHConfigList file:%s:%s\n", optarg, strerror(errno));
- return 1;
- }
- ech.config_list.base = malloc(65536);
- if ((ech.config_list.len = fread(ech.config_list.base, 1, 65536, fp)) == 65536) {
- fprintf(stderr, "ECHConfigList is too large:%s\n", optarg);
- return 1;
- }
- fclose(fp);
- ech.retry.fn = optarg;
- } break;
- case 'K': {
- FILE *fp;
- EVP_PKEY *pkey;
- int ret;
- if ((fp = fopen(optarg, "rt")) == NULL) {
- fprintf(stderr, "failed to open ECH private key file:%s:%s\n", optarg, strerror(errno));
- return 1;
- }
- if ((pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)) == NULL) {
- fprintf(stderr, "failed to load private key from file:%s\n", optarg);
- return 1;
- }
- if ((ret = ptls_openssl_create_key_exchange(&ech.keyex.list[ech.keyex.count].ctx, pkey)) != 0) {
- fprintf(stderr, "failed to load private key from file:%s:picotls-error:%d", optarg, ret);
- return 1;
- }
- ech.keyex.list[ech.keyex.count].kem = find_kem(ech.keyex.list[ech.keyex.count].ctx->algo);
- ++ech.keyex.count;
- EVP_PKEY_free(pkey);
- fclose(fp);
- ctx.ech.server.create_opener = &ech_opener;
- } break;
+ case 'E':
+ ech_setup_configs(optarg);
+ break;
+ case 'K':
+ ech_setup_key(&ctx, optarg);
+ break;
case 'l':
setup_log_event(&ctx, optarg);
break;
diff --git a/t/ech-live.t b/t/ech-live.t
new file mode 100755
index 0000000..b917273
--- /dev/null
+++ b/t/ech-live.t
@@ -0,0 +1,46 @@
+#! /usr/bin/env perl
+
+use strict;
+use warnings;
+use File::Temp qw(tempdir);
+use POSIX ":sys_wait_h";
+use Test::More;
+
+$ENV{BINARY_DIR} ||= ".";
+my $cli = "$ENV{BINARY_DIR}/cli";
+my $tempdir = tempdir(CLEANUP => 1);
+
+plan skip_all => "skipping live tests (setenv LIVE_TESTS=1 to run them)"
+ unless $ENV{LIVE_TESTS};
+
+subtest "crypto.cloudflare.com" => sub {
+ my $req_fn = "$tempdir/req";
+ my $ech_config_fn = "$tempdir/echconfiglist";
+ my $fetch = sub {
+ open my $fh, "$cli -I -E $ech_config_fn crypto.cloudflare.com 443 < $req_fn |"
+ or die "failed to open $cli to connect to crypto.cloudflare.com";
+ join "", <$fh>;
+ };
+
+ { # build request as a temporary file
+ open my $fh, ">", $req_fn
+ or die "failed to create file:$req_fn:$!";
+ print $fh "GET /cdn-cgi/trace HTTP/1.0\r\nHost: crypto.cloudflare.com\r\n\r\n";
+ close $fh;
+ }
+
+ { # create empty ECHConfigList file so as to grease and obtain true config
+ open my $fh, ">", $ech_config_fn
+ or die "failed to create file:$ech_config_fn:$!";
+ close $fh;
+ }
+
+ my $resp = $fetch->();
+ like $resp, qr/^sni=plaintext$/m, "response to grease";
+ isnt +(stat $req_fn)[7], 0, "echconfiglist is non-empty";
+
+ $resp = $fetch->();
+ like $resp, qr/^sni=encrypted$/m, "response to innerCH";
+};
+
+done_testing;
diff --git a/t/minicrypto.c b/t/minicrypto.c
index 30395cf..17a05d2 100644
--- a/t/minicrypto.c
+++ b/t/minicrypto.c
@@ -155,7 +155,7 @@
ptls_minicrypto_key_exchanges,
ptls_minicrypto_cipher_suites,
{&cert, 1},
- {NULL},
+ {{NULL}},
NULL,
NULL,
&sign_certificate.super};
diff --git a/t/openssl.c b/t/openssl.c
index 8e79b15..b4c73b0 100644
--- a/t/openssl.c
+++ b/t/openssl.c
@@ -599,7 +599,7 @@
ptls_minicrypto_key_exchanges,
ptls_minicrypto_cipher_suites,
{&minicrypto_certificate, 1},
- {NULL},
+ {{NULL}},
NULL,
NULL,
&minicrypto_sign_certificate.super};
diff --git a/t/util.h b/t/util.h
index 3c59243..0ead3f7 100644
--- a/t/util.h
+++ b/t/util.h
@@ -251,6 +251,149 @@
ctx->encrypt_ticket = &sc.super;
}
+static struct {
+ ptls_iovec_t config_list;
+ struct {
+ struct {
+ ptls_hpke_kem_t *kem;
+ ptls_key_exchange_context_t *ctx;
+ } list[16];
+ size_t count;
+ } keyex;
+ struct {
+ ptls_iovec_t configs;
+ char *fn;
+ } retry;
+} ech;
+
+static ptls_aead_context_t *ech_create_opener(ptls_ech_create_opener_t *self, ptls_hpke_kem_t **kem,
+ ptls_hpke_cipher_suite_t **cipher, ptls_t *tls, uint8_t config_id,
+ ptls_hpke_cipher_suite_id_t cipher_id, ptls_iovec_t enc, ptls_iovec_t info_prefix)
+{
+ const uint8_t *src = ech.config_list.base, *const end = src + ech.config_list.len;
+ size_t index = 0;
+ int ret = 0;
+
+ /* look for the cipher implementation; this should better be specific to each ECHConfig (as each of them may advertise different
+ * set of values) */
+ *cipher = NULL;
+ for (size_t i = 0; ptls_openssl_hpke_cipher_suites[i] != NULL; ++i) {
+ if (ptls_openssl_hpke_cipher_suites[i]->id.kdf == cipher_id.kdf &&
+ ptls_openssl_hpke_cipher_suites[i]->id.aead == cipher_id.aead) {
+ *cipher = ptls_openssl_hpke_cipher_suites[i];
+ break;
+ }
+ }
+ if (*cipher == NULL)
+ goto Exit;
+
+ ptls_decode_open_block(src, end, 2, {
+ uint16_t version;
+ if ((ret = ptls_decode16(&version, &src, end)) != 0)
+ goto Exit;
+ do {
+ ptls_decode_open_block(src, end, 2, {
+ if (src == end) {
+ ret = PTLS_ALERT_DECODE_ERROR;
+ goto Exit;
+ }
+ if (*src == config_id) {
+ /* this is the ECHConfig that we have been looking for */
+ if (index >= ech.keyex.count) {
+ fprintf(stderr, "ECH key missing for config %zu\n", index);
+ return NULL;
+ }
+ uint8_t *info = malloc(info_prefix.len + end - (src - 4));
+ memcpy(info, info_prefix.base, info_prefix.len);
+ memcpy(info + info_prefix.len, src - 4, end - (src - 4));
+ ptls_aead_context_t *aead;
+ ptls_hpke_setup_base_r(ech.keyex.list[index].kem, *cipher, ech.keyex.list[index].ctx, &aead, enc,
+ ptls_iovec_init(info, info_prefix.len + end - (src - 4)));
+ free(info);
+ *kem = ech.keyex.list[index].kem;
+ return aead;
+ }
+ ++index;
+ src = end;
+ });
+ } while (src != end);
+ });
+
+Exit:
+ if (ret != 0)
+ fprintf(stderr, "ECH decode error:%d\n", ret);
+ return NULL;
+}
+
+static void ech_save_retry_configs(void)
+{
+ if (ech.retry.configs.base == NULL)
+ return;
+
+ FILE *fp;
+ if ((fp = fopen(ech.retry.fn, "wt")) == NULL) {
+ fprintf(stderr, "failed to write to ECH config file:%s:%s\n", ech.retry.fn, strerror(errno));
+ exit(1);
+ }
+ fwrite(ech.retry.configs.base, 1, ech.retry.configs.len, fp);
+ fclose(fp);
+}
+
+static void ech_setup_configs(const char *fn)
+{
+ FILE *fp;
+
+ if ((fp = fopen(fn, "rt")) == NULL) {
+ fprintf(stderr, "failed to open ECHConfigList file:%s:%s\n", fn, strerror(errno));
+ exit(1);
+ }
+ ech.config_list.base = malloc(65536);
+ if ((ech.config_list.len = fread(ech.config_list.base, 1, 65536, fp)) == 65536) {
+ fprintf(stderr, "ECHConfigList is too large:%s\n", fn);
+ exit(1);
+ }
+ fclose(fp);
+ ech.retry.fn = strdup(fn);
+}
+
+static void ech_setup_key(ptls_context_t *ctx, const char *fn)
+{
+ FILE *fp;
+ EVP_PKEY *pkey;
+ int ret;
+
+ if ((fp = fopen(fn, "rt")) == NULL) {
+ fprintf(stderr, "failed to open ECH private key file:%s:%s\n", fn, strerror(errno));
+ exit(1);
+ }
+ if ((pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)) == NULL) {
+ fprintf(stderr, "failed to load private key from file:%s\n", fn);
+ exit(1);
+ }
+ if ((ret = ptls_openssl_create_key_exchange(&ech.keyex.list[ech.keyex.count].ctx, pkey)) != 0) {
+ fprintf(stderr, "failed to load private key from file:%s:picotls-error:%d", fn, ret);
+ exit(1);
+ }
+ EVP_PKEY_free(pkey);
+ fclose(fp);
+
+ for (size_t i = 0; ptls_openssl_hpke_kems[i] != NULL; ++i) {
+ if (ptls_openssl_hpke_kems[i]->keyex == ech.keyex.list[ech.keyex.count].ctx->algo) {
+ ech.keyex.list[ech.keyex.count].kem = ptls_openssl_hpke_kems[i];
+ break;
+ }
+ }
+ if (ech.keyex.list[ech.keyex.count].kem == NULL) {
+ fprintf(stderr, "kem unknown for private key:%s\n", fn);
+ exit(1);
+ }
+
+ ++ech.keyex.count;
+
+ static ptls_ech_create_opener_t opener = {.cb = ech_create_opener};
+ ctx->ech.server.create_opener = &opener;
+}
+
static inline int resolve_address(struct sockaddr *sa, socklen_t *salen, const char *host, const char *port, int family, int type,
int proto)
{