Introduce EVP_HPKE_{AEAD,KDF} types.

This replaces the ID-based API with one that is more static linker
friendly. For ECH, it doesn't make a difference because we currently
pull in all the options we've implemented. But this means other HPKE
uses need not pull in everything ECH needs and vice versa.

Along the way, fix an inconsistency: we prefixed all the AEAD constants
with "AEAD", but not the others. Since the rest of the name already
determines everything, go with the shorter version.

Bug: 410
Change-Id: I56e46c13b43c97e15eeb45204cde7019dd21e250
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/47327
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index cc0def5..3e98159 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -31,13 +31,19 @@
 
 // This file implements draft-irtf-cfrg-hpke-08.
 
+struct evp_hpke_kdf_st {
+  uint16_t id;
+  // We only support HKDF-based KDFs.
+  const EVP_MD *(*hkdf_md_func)(void);
+};
+
+struct evp_hpke_aead_st {
+  uint16_t id;
+  const EVP_AEAD *(*aead_func)(void);
+};
+
 #define KEM_CONTEXT_LEN (2 * X25519_PUBLIC_VALUE_LEN)
 
-// This is strlen("HPKE") + 3 * sizeof(uint16_t).
-#define HPKE_SUITE_ID_LEN 10
-
-#define HPKE_MODE_BASE 0
-
 static const char kHpkeVersionId[] = "HPKE-v1";
 
 static int add_label_string(CBB *cbb, const char *label) {
@@ -51,20 +57,6 @@
     'K', 'E', 'M', EVP_HPKE_DHKEM_X25519_HKDF_SHA256 >> 8,
     EVP_HPKE_DHKEM_X25519_HKDF_SHA256 & 0x00ff};
 
-// The suite_id for non-KEM pieces of HPKE is defined as concat("HPKE",
-// I2OSP(kem_id, 2), I2OSP(kdf_id, 2), I2OSP(aead_id, 2)).
-static int hpke_build_suite_id(uint8_t out[HPKE_SUITE_ID_LEN], uint16_t kdf_id,
-                               uint16_t aead_id) {
-  CBB cbb;
-  int ret = CBB_init_fixed(&cbb, out, HPKE_SUITE_ID_LEN) &&
-            add_label_string(&cbb, "HPKE") &&
-            CBB_add_u16(&cbb, EVP_HPKE_DHKEM_X25519_HKDF_SHA256) &&
-            CBB_add_u16(&cbb, kdf_id) &&
-            CBB_add_u16(&cbb, aead_id);
-  CBB_cleanup(&cbb);
-  return ret;
-}
-
 static int hpke_labeled_extract(const EVP_MD *hkdf_md, uint8_t *out_key,
                                 size_t *out_len, const uint8_t *salt,
                                 size_t salt_len, const uint8_t *suite_id,
@@ -108,81 +100,62 @@
                                    const uint8_t kem_context[KEM_CONTEXT_LEN]) {
   uint8_t prk[EVP_MAX_MD_SIZE];
   size_t prk_len;
-  static const char kEaePrkLabel[] = "eae_prk";
   if (!hpke_labeled_extract(hkdf_md, prk, &prk_len, NULL, 0, kX25519SuiteID,
-                            sizeof(kX25519SuiteID), kEaePrkLabel, dh,
+                            sizeof(kX25519SuiteID), "eae_prk", dh,
                             X25519_PUBLIC_VALUE_LEN)) {
     return 0;
   }
-  static const char kPRKExpandLabel[] = "shared_secret";
   if (!hpke_labeled_expand(hkdf_md, out_key, out_len, prk, prk_len,
                            kX25519SuiteID, sizeof(kX25519SuiteID),
-                           kPRKExpandLabel, kem_context, KEM_CONTEXT_LEN)) {
+                           "shared_secret", kem_context, KEM_CONTEXT_LEN)) {
     return 0;
   }
   return 1;
 }
 
-uint16_t EVP_HPKE_CTX_get_aead_id(const EVP_HPKE_CTX *hpke) {
-  return hpke->aead_id;
+// This is strlen("HPKE") + 3 * sizeof(uint16_t).
+#define HPKE_SUITE_ID_LEN 10
+
+// The suite_id for non-KEM pieces of HPKE is defined as concat("HPKE",
+// I2OSP(kem_id, 2), I2OSP(kdf_id, 2), I2OSP(aead_id, 2)).
+static int hpke_build_suite_id(const EVP_HPKE_CTX *hpke,
+                               uint8_t out[HPKE_SUITE_ID_LEN]) {
+  CBB cbb;
+  int ret = CBB_init_fixed(&cbb, out, HPKE_SUITE_ID_LEN) &&
+            add_label_string(&cbb, "HPKE") &&
+            CBB_add_u16(&cbb, EVP_HPKE_DHKEM_X25519_HKDF_SHA256) &&
+            CBB_add_u16(&cbb, hpke->kdf->id) &&
+            CBB_add_u16(&cbb, hpke->aead->id);
+  CBB_cleanup(&cbb);
+  return ret;
 }
 
-uint16_t EVP_HPKE_CTX_get_kdf_id(const EVP_HPKE_CTX *hpke) {
-  return hpke->kdf_id;
-}
-
-const EVP_AEAD *EVP_HPKE_get_aead(uint16_t aead_id) {
-  switch (aead_id) {
-    case EVP_HPKE_AEAD_AES_128_GCM:
-      return EVP_aead_aes_128_gcm();
-    case EVP_HPKE_AEAD_AES_256_GCM:
-      return EVP_aead_aes_256_gcm();
-    case EVP_HPKE_AEAD_CHACHA20POLY1305:
-      return EVP_aead_chacha20_poly1305();
-  }
-  OPENSSL_PUT_ERROR(EVP, ERR_R_INTERNAL_ERROR);
-  return NULL;
-}
-
-const EVP_MD *EVP_HPKE_get_hkdf_md(uint16_t kdf_id) {
-  switch (kdf_id) {
-    case EVP_HPKE_HKDF_SHA256:
-      return EVP_sha256();
-  }
-  OPENSSL_PUT_ERROR(EVP, ERR_R_INTERNAL_ERROR);
-  return NULL;
-}
+#define HPKE_MODE_BASE 0
 
 static int hpke_key_schedule(EVP_HPKE_CTX *hpke, const uint8_t *shared_secret,
                              size_t shared_secret_len, const uint8_t *info,
                              size_t info_len) {
-  // Attempt to get an EVP_AEAD*.
-  const EVP_AEAD *aead = EVP_HPKE_get_aead(hpke->aead_id);
-  if (aead == NULL) {
-    return 0;
-  }
-
   uint8_t suite_id[HPKE_SUITE_ID_LEN];
-  if (!hpke_build_suite_id(suite_id, hpke->kdf_id, hpke->aead_id)) {
+  if (!hpke_build_suite_id(hpke, suite_id)) {
     return 0;
   }
 
   // psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id)
-  static const char kPskIdHashLabel[] = "psk_id_hash";
+  // TODO(davidben): Precompute this value and store it with the EVP_HPKE_KDF.
+  const EVP_MD *hkdf_md = hpke->kdf->hkdf_md_func();
   uint8_t psk_id_hash[EVP_MAX_MD_SIZE];
   size_t psk_id_hash_len;
-  if (!hpke_labeled_extract(hpke->hkdf_md, psk_id_hash, &psk_id_hash_len, NULL,
-                            0, suite_id, sizeof(suite_id), kPskIdHashLabel,
-                            NULL, 0)) {
+  if (!hpke_labeled_extract(hkdf_md, psk_id_hash, &psk_id_hash_len, NULL, 0,
+                            suite_id, sizeof(suite_id), "psk_id_hash", NULL,
+                            0)) {
     return 0;
   }
 
   // info_hash = LabeledExtract("", "info_hash", info)
-  static const char kInfoHashLabel[] = "info_hash";
   uint8_t info_hash[EVP_MAX_MD_SIZE];
   size_t info_hash_len;
-  if (!hpke_labeled_extract(hpke->hkdf_md, info_hash, &info_hash_len, NULL, 0,
-                            suite_id, sizeof(suite_id), kInfoHashLabel, info,
+  if (!hpke_labeled_extract(hkdf_md, info_hash, &info_hash_len, NULL, 0,
+                            suite_id, sizeof(suite_id), "info_hash", info,
                             info_len)) {
     return 0;
   }
@@ -200,45 +173,37 @@
   }
 
   // secret = LabeledExtract(shared_secret, "secret", psk)
-  static const char kSecretExtractLabel[] = "secret";
   uint8_t secret[EVP_MAX_MD_SIZE];
   size_t secret_len;
-  if (!hpke_labeled_extract(hpke->hkdf_md, secret, &secret_len, shared_secret,
+  if (!hpke_labeled_extract(hkdf_md, secret, &secret_len, shared_secret,
                             shared_secret_len, suite_id, sizeof(suite_id),
-                            kSecretExtractLabel, NULL, 0)) {
+                            "secret", NULL, 0)) {
     return 0;
   }
 
   // key = LabeledExpand(secret, "key", key_schedule_context, Nk)
-  static const char kKeyExpandLabel[] = "key";
+  const EVP_AEAD *aead = hpke->aead->aead_func();
   uint8_t key[EVP_AEAD_MAX_KEY_LENGTH];
   const size_t kKeyLen = EVP_AEAD_key_length(aead);
-  if (!hpke_labeled_expand(hpke->hkdf_md, key, kKeyLen, secret, secret_len,
-                           suite_id, sizeof(suite_id), kKeyExpandLabel, context,
-                           context_len)) {
-    return 0;
-  }
-
-  // Initialize the HPKE context's AEAD context, storing a copy of |key|.
-  if (!EVP_AEAD_CTX_init(&hpke->aead_ctx, aead, key, kKeyLen, 0, NULL)) {
+  if (!hpke_labeled_expand(hkdf_md, key, kKeyLen, secret, secret_len, suite_id,
+                           sizeof(suite_id), "key", context, context_len) ||
+      !EVP_AEAD_CTX_init(&hpke->aead_ctx, aead, key, kKeyLen,
+                         EVP_AEAD_DEFAULT_TAG_LENGTH, NULL)) {
     return 0;
   }
 
   // base_nonce = LabeledExpand(secret, "base_nonce", key_schedule_context, Nn)
-  static const char kNonceExpandLabel[] = "base_nonce";
-  if (!hpke_labeled_expand(hpke->hkdf_md, hpke->base_nonce,
+  if (!hpke_labeled_expand(hkdf_md, hpke->base_nonce,
                            EVP_AEAD_nonce_length(aead), secret, secret_len,
-                           suite_id, sizeof(suite_id), kNonceExpandLabel,
-                           context, context_len)) {
+                           suite_id, sizeof(suite_id), "base_nonce", context,
+                           context_len)) {
     return 0;
   }
 
   // exporter_secret = LabeledExpand(secret, "exp", key_schedule_context, Nh)
-  static const char kExporterSecretExpandLabel[] = "exp";
-  if (!hpke_labeled_expand(hpke->hkdf_md, hpke->exporter_secret,
-                           EVP_MD_size(hpke->hkdf_md), secret, secret_len,
-                           suite_id, sizeof(suite_id),
-                           kExporterSecretExpandLabel, context, context_len)) {
+  if (!hpke_labeled_expand(hkdf_md, hpke->exporter_secret, EVP_MD_size(hkdf_md),
+                           secret, secret_len, suite_id, sizeof(suite_id),
+                           "exp", context, context_len)) {
     return 0;
   }
 
@@ -290,6 +255,33 @@
   return 1;
 }
 
+const EVP_HPKE_KDF *EVP_hpke_hkdf_sha256(void) {
+  static const EVP_HPKE_KDF kKDF = {EVP_HPKE_HKDF_SHA256, &EVP_sha256};
+  return &kKDF;
+}
+
+uint16_t EVP_HPKE_KDF_id(const EVP_HPKE_KDF *kdf) { return kdf->id; }
+
+const EVP_HPKE_AEAD *EVP_hpke_aes_128_gcm(void) {
+  static const EVP_HPKE_AEAD kAEAD = {EVP_HPKE_AES_128_GCM,
+                                      &EVP_aead_aes_128_gcm};
+  return &kAEAD;
+}
+
+const EVP_HPKE_AEAD *EVP_hpke_aes_256_gcm(void) {
+  static const EVP_HPKE_AEAD kAEAD = {EVP_HPKE_AES_256_GCM,
+                                      &EVP_aead_aes_256_gcm};
+  return &kAEAD;
+}
+
+const EVP_HPKE_AEAD *EVP_hpke_chacha20_poly1305(void) {
+  static const EVP_HPKE_AEAD kAEAD = {EVP_HPKE_CHACHA20_POLY1305,
+                                      &EVP_aead_chacha20_poly1305};
+  return &kAEAD;
+}
+
+uint16_t EVP_HPKE_AEAD_id(const EVP_HPKE_AEAD *aead) { return aead->id; }
+
 void EVP_HPKE_CTX_init(EVP_HPKE_CTX *ctx) {
   OPENSSL_memset(ctx, 0, sizeof(EVP_HPKE_CTX));
   EVP_AEAD_CTX_zero(&ctx->aead_ctx);
@@ -300,23 +292,25 @@
 }
 
 int EVP_HPKE_CTX_setup_base_s_x25519(EVP_HPKE_CTX *hpke, uint8_t *out_enc,
-                                     size_t out_enc_len, uint16_t kdf_id,
-                                     uint16_t aead_id,
+                                     size_t out_enc_len,
+                                     const EVP_HPKE_KDF *kdf,
+                                     const EVP_HPKE_AEAD *aead,
                                      const uint8_t *peer_public_value,
                                      size_t peer_public_value_len,
                                      const uint8_t *info, size_t info_len) {
   uint8_t seed[X25519_PRIVATE_KEY_LEN];
   RAND_bytes(seed, sizeof(seed));
   return EVP_HPKE_CTX_setup_base_s_x25519_with_seed_for_testing(
-      hpke, out_enc, out_enc_len, kdf_id, aead_id, peer_public_value,
+      hpke, out_enc, out_enc_len, kdf, aead, peer_public_value,
       peer_public_value_len, info, info_len, seed, sizeof(seed));
 }
 
 int EVP_HPKE_CTX_setup_base_s_x25519_with_seed_for_testing(
-    EVP_HPKE_CTX *hpke, uint8_t *out_enc, size_t out_enc_len, uint16_t kdf_id,
-    uint16_t aead_id, const uint8_t *peer_public_value,
-    size_t peer_public_value_len, const uint8_t *info, size_t info_len,
-    const uint8_t *seed, size_t seed_len) {
+    EVP_HPKE_CTX *hpke, uint8_t *out_enc, size_t out_enc_len,
+    const EVP_HPKE_KDF *kdf, const EVP_HPKE_AEAD *aead,
+    const uint8_t *peer_public_value, size_t peer_public_value_len,
+    const uint8_t *info, size_t info_len, const uint8_t *seed,
+    size_t seed_len) {
   if (out_enc_len != X25519_PUBLIC_VALUE_LEN) {
     OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_BUFFER_SIZE);
     return 0;
@@ -331,12 +325,8 @@
   }
 
   hpke->is_sender = 1;
-  hpke->kdf_id = kdf_id;
-  hpke->aead_id = aead_id;
-  hpke->hkdf_md = EVP_HPKE_get_hkdf_md(kdf_id);
-  if (hpke->hkdf_md == NULL) {
-    return 0;
-  }
+  hpke->kdf = kdf;
+  hpke->aead = aead;
   X25519_public_from_private(out_enc, seed);
   uint8_t shared_secret[SHA256_DIGEST_LENGTH];
   if (!hpke_encap(hpke, shared_secret, peer_public_value, seed, out_enc) ||
@@ -347,13 +337,11 @@
   return 1;
 }
 
-int EVP_HPKE_CTX_setup_base_r_x25519(EVP_HPKE_CTX *hpke, uint16_t kdf_id,
-                                     uint16_t aead_id, const uint8_t *enc,
-                                     size_t enc_len, const uint8_t *public_key,
-                                     size_t public_key_len,
-                                     const uint8_t *private_key,
-                                     size_t private_key_len,
-                                     const uint8_t *info, size_t info_len) {
+int EVP_HPKE_CTX_setup_base_r_x25519(
+    EVP_HPKE_CTX *hpke, const EVP_HPKE_KDF *kdf, const EVP_HPKE_AEAD *aead,
+    const uint8_t *enc, size_t enc_len, const uint8_t *public_key,
+    size_t public_key_len, const uint8_t *private_key, size_t private_key_len,
+    const uint8_t *info, size_t info_len) {
   if (enc_len != X25519_PUBLIC_VALUE_LEN) {
     OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_PEER_KEY);
     return 0;
@@ -365,12 +353,8 @@
   }
 
   hpke->is_sender = 0;
-  hpke->kdf_id = kdf_id;
-  hpke->aead_id = aead_id;
-  hpke->hkdf_md = EVP_HPKE_get_hkdf_md(kdf_id);
-  if (hpke->hkdf_md == NULL) {
-    return 0;
-  }
+  hpke->kdf = kdf;
+  hpke->aead = aead;
   uint8_t shared_secret[SHA256_DIGEST_LENGTH];
   if (!hpke_decap(hpke, shared_secret, enc, public_key, private_key) ||
       !hpke_key_schedule(hpke, shared_secret, sizeof(shared_secret), info,
@@ -398,11 +382,6 @@
   }
 }
 
-size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *hpke) {
-  assert(hpke->is_sender);
-  return EVP_AEAD_max_overhead(hpke->aead_ctx.aead);
-}
-
 int EVP_HPKE_CTX_open(EVP_HPKE_CTX *hpke, uint8_t *out, size_t *out_len,
                       size_t max_out_len, const uint8_t *in, size_t in_len,
                       const uint8_t *ad, size_t ad_len) {
@@ -455,15 +434,27 @@
                         size_t secret_len, const uint8_t *context,
                         size_t context_len) {
   uint8_t suite_id[HPKE_SUITE_ID_LEN];
-  if (!hpke_build_suite_id(suite_id, hpke->kdf_id, hpke->aead_id)) {
+  if (!hpke_build_suite_id(hpke, suite_id)) {
     return 0;
   }
-  static const char kExportExpandLabel[] = "sec";
-  if (!hpke_labeled_expand(hpke->hkdf_md, out, secret_len,
-                           hpke->exporter_secret, EVP_MD_size(hpke->hkdf_md),
-                           suite_id, sizeof(suite_id), kExportExpandLabel,
-                           context, context_len)) {
+  const EVP_MD *hkdf_md = hpke->kdf->hkdf_md_func();
+  if (!hpke_labeled_expand(hkdf_md, out, secret_len, hpke->exporter_secret,
+                           EVP_MD_size(hkdf_md), suite_id, sizeof(suite_id),
+                           "sec", context, context_len)) {
     return 0;
   }
   return 1;
 }
+
+size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *hpke) {
+  assert(hpke->is_sender);
+  return EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(&hpke->aead_ctx));
+}
+
+const EVP_HPKE_AEAD *EVP_HPKE_CTX_aead(const EVP_HPKE_CTX *hpke) {
+  return hpke->aead;
+}
+
+const EVP_HPKE_KDF *EVP_HPKE_CTX_kdf(const EVP_HPKE_CTX *hpke) {
+  return hpke->kdf;
+}
diff --git a/crypto/hpke/hpke_test.cc b/crypto/hpke/hpke_test.cc
index e4aa932..82ba229 100644
--- a/crypto/hpke/hpke_test.cc
+++ b/crypto/hpke/hpke_test.cc
@@ -35,6 +35,16 @@
 namespace bssl {
 namespace {
 
+const decltype(&EVP_hpke_aes_128_gcm) kAllAEADs[] = {
+    &EVP_hpke_aes_128_gcm,
+    &EVP_hpke_aes_256_gcm,
+    &EVP_hpke_chacha20_poly1305,
+};
+
+const decltype(&EVP_hpke_hkdf_sha256) kAllKDFs[] = {
+    &EVP_hpke_hkdf_sha256,
+};
+
 // HPKETestVector corresponds to one array member in the published
 // test-vectors.json.
 class HPKETestVector {
@@ -45,24 +55,26 @@
   bool ReadFromFileTest(FileTest *t);
 
   void Verify() const {
-    ScopedEVP_HPKE_CTX sender_ctx;
-    ScopedEVP_HPKE_CTX receiver_ctx;
-
-    ASSERT_GT(secret_key_e_.size(), 0u);
+    const EVP_HPKE_AEAD *aead = GetAEAD();
+    ASSERT_TRUE(aead);
+    const EVP_HPKE_KDF *kdf = GetKDF();
+    ASSERT_TRUE(kdf);
 
     // Set up the sender.
+    ScopedEVP_HPKE_CTX sender_ctx;
     uint8_t enc[X25519_PUBLIC_VALUE_LEN];
     ASSERT_TRUE(EVP_HPKE_CTX_setup_base_s_x25519_with_seed_for_testing(
-        sender_ctx.get(), enc, sizeof(enc), kdf_id_, aead_id_,
-        public_key_r_.data(), public_key_r_.size(), info_.data(), info_.size(),
-        secret_key_e_.data(), secret_key_e_.size()));
+        sender_ctx.get(), enc, sizeof(enc), kdf, aead, public_key_r_.data(),
+        public_key_r_.size(), info_.data(), info_.size(), secret_key_e_.data(),
+        secret_key_e_.size()));
     EXPECT_EQ(Bytes(enc), Bytes(public_key_e_));
 
     // Set up the receiver.
+    ScopedEVP_HPKE_CTX receiver_ctx;
     ASSERT_TRUE(EVP_HPKE_CTX_setup_base_r_x25519(
-        receiver_ctx.get(), kdf_id_, aead_id_, enc, sizeof(enc),
-        public_key_r_.data(), public_key_r_.size(), secret_key_r_.data(),
-        secret_key_r_.size(), info_.data(), info_.size()));
+        receiver_ctx.get(), kdf, aead, enc, sizeof(enc), public_key_r_.data(),
+        public_key_r_.size(), secret_key_r_.data(), secret_key_r_.size(),
+        info_.data(), info_.size()));
 
     VerifyEncryptions(sender_ctx.get(), receiver_ctx.get());
     VerifyExports(sender_ctx.get());
@@ -70,6 +82,24 @@
   }
 
  private:
+  const EVP_HPKE_AEAD *GetAEAD() const {
+    for (const auto aead : kAllAEADs) {
+      if (EVP_HPKE_AEAD_id(aead()) == aead_id_) {
+        return aead();
+      }
+    }
+    return nullptr;
+  }
+
+  const EVP_HPKE_KDF *GetKDF() const {
+    for (const auto kdf : kAllKDFs) {
+      if (EVP_HPKE_KDF_id(kdf()) == kdf_id_) {
+        return kdf();
+      }
+    }
+    return nullptr;
+  }
+
   void VerifyEncryptions(EVP_HPKE_CTX *sender_ctx,
                          EVP_HPKE_CTX *receiver_ctx) const {
     for (const Encryption &task : encryptions_) {
@@ -212,10 +242,6 @@
 // generates new keys for each context. Test this codepath by checking we can
 // decrypt our own messages.
 TEST(HPKETest, RoundTrip) {
-  uint16_t kdf_ids[] = {EVP_HPKE_HKDF_SHA256};
-  uint16_t aead_ids[] = {EVP_HPKE_AEAD_AES_128_GCM, EVP_HPKE_AEAD_AES_256_GCM,
-                         EVP_HPKE_AEAD_CHACHA20POLY1305};
-
   const uint8_t info_a[] = {1, 1, 2, 3, 5, 8};
   const uint8_t info_b[] = {42, 42, 42};
   const uint8_t ad_a[] = {1, 2, 4, 8, 16};
@@ -228,10 +254,10 @@
   uint8_t public_key_r[X25519_PUBLIC_VALUE_LEN];
   X25519_keypair(public_key_r, secret_key_r);
 
-  for (uint16_t kdf_id : kdf_ids) {
-    SCOPED_TRACE(kdf_id);
-    for (uint16_t aead_id : aead_ids) {
-      SCOPED_TRACE(aead_id);
+  for (const auto kdf : kAllKDFs) {
+    SCOPED_TRACE(EVP_HPKE_KDF_id(kdf()));
+    for (const auto aead : kAllAEADs) {
+      SCOPED_TRACE(EVP_HPKE_AEAD_id(aead()));
       for (const Span<const uint8_t> &info : info_values) {
         SCOPED_TRACE(Bytes(info));
         for (const Span<const uint8_t> &ad : ad_values) {
@@ -240,15 +266,15 @@
           ScopedEVP_HPKE_CTX sender_ctx;
           uint8_t enc[X25519_PUBLIC_VALUE_LEN];
           ASSERT_TRUE(EVP_HPKE_CTX_setup_base_s_x25519(
-              sender_ctx.get(), enc, sizeof(enc), kdf_id, aead_id, public_key_r,
+              sender_ctx.get(), enc, sizeof(enc), kdf(), aead(), public_key_r,
               sizeof(public_key_r), info.data(), info.size()));
 
           // Set up the receiver.
           ScopedEVP_HPKE_CTX receiver_ctx;
           ASSERT_TRUE(EVP_HPKE_CTX_setup_base_r_x25519(
-              receiver_ctx.get(), kdf_id, aead_id, enc, sizeof(enc),
-              public_key_r, sizeof(public_key_r), secret_key_r,
-              sizeof(secret_key_r), info.data(), info.size()));
+              receiver_ctx.get(), kdf(), aead(), enc, sizeof(enc), public_key_r,
+              sizeof(public_key_r), secret_key_r, sizeof(secret_key_r),
+              info.data(), info.size()));
 
           const char kCleartextPayload[] = "foobar";
 
@@ -295,25 +321,21 @@
   uint8_t public_key_r[X25519_PUBLIC_VALUE_LEN];
   X25519_keypair(public_key_r, secret_key_r);
 
-  uint16_t kdf_ids[] = {EVP_HPKE_HKDF_SHA256};
-  uint16_t aead_ids[] = {EVP_HPKE_AEAD_AES_128_GCM, EVP_HPKE_AEAD_AES_256_GCM,
-                         EVP_HPKE_AEAD_CHACHA20POLY1305};
-
-  for (uint16_t kdf_id : kdf_ids) {
-    SCOPED_TRACE(kdf_id);
-    for (uint16_t aead_id : aead_ids) {
-      SCOPED_TRACE(aead_id);
+  for (const auto kdf : kAllKDFs) {
+    SCOPED_TRACE(EVP_HPKE_KDF_id(kdf()));
+    for (const auto aead : kAllAEADs) {
+      SCOPED_TRACE(EVP_HPKE_AEAD_id(aead()));
       // Set up the sender, passing in kSmallOrderPoint as |peer_public_value|.
       ScopedEVP_HPKE_CTX sender_ctx;
       uint8_t enc[X25519_PUBLIC_VALUE_LEN];
       ASSERT_FALSE(EVP_HPKE_CTX_setup_base_s_x25519(
-          sender_ctx.get(), enc, sizeof(enc), kdf_id, aead_id, kSmallOrderPoint,
+          sender_ctx.get(), enc, sizeof(enc), kdf(), aead(), kSmallOrderPoint,
           sizeof(kSmallOrderPoint), nullptr, 0));
 
       // Set up the receiver, passing in kSmallOrderPoint as |enc|.
       ScopedEVP_HPKE_CTX receiver_ctx;
       ASSERT_FALSE(EVP_HPKE_CTX_setup_base_r_x25519(
-          receiver_ctx.get(), kdf_id, aead_id, kSmallOrderPoint,
+          receiver_ctx.get(), kdf(), aead(), kSmallOrderPoint,
           sizeof(kSmallOrderPoint), public_key_r, sizeof(public_key_r),
           secret_key_r, sizeof(secret_key_r), nullptr, 0));
     }
@@ -333,7 +355,7 @@
   // Set up the receiver.
   ScopedEVP_HPKE_CTX receiver_ctx;
   ASSERT_TRUE(EVP_HPKE_CTX_setup_base_r_x25519(
-      receiver_ctx.get(), EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM,
+      receiver_ctx.get(), EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(),
       kMockEnc, sizeof(kMockEnc), public_key_r, sizeof(public_key_r),
       secret_key_r, sizeof(secret_key_r), nullptr, 0));
 
@@ -360,9 +382,8 @@
   ScopedEVP_HPKE_CTX sender_ctx;
   uint8_t enc[X25519_PUBLIC_VALUE_LEN];
   ASSERT_TRUE(EVP_HPKE_CTX_setup_base_s_x25519(
-      sender_ctx.get(), enc, sizeof(enc), EVP_HPKE_HKDF_SHA256,
-      EVP_HPKE_AEAD_AES_128_GCM, public_key_r, sizeof(public_key_r), nullptr,
-      0));
+      sender_ctx.get(), enc, sizeof(enc), EVP_hpke_hkdf_sha256(),
+      EVP_hpke_aes_128_gcm(), public_key_r, sizeof(public_key_r), nullptr, 0));
 
   // Call Open() on the sender.
   uint8_t cleartext[128];
@@ -380,9 +401,8 @@
   ScopedEVP_HPKE_CTX sender_ctx;
   uint8_t bogus_enc[X25519_PUBLIC_VALUE_LEN + 5];
   ASSERT_FALSE(EVP_HPKE_CTX_setup_base_s_x25519(
-      sender_ctx.get(), bogus_enc, sizeof(bogus_enc), EVP_HPKE_HKDF_SHA256,
-      EVP_HPKE_AEAD_AES_128_GCM, public_key_r, sizeof(public_key_r), nullptr,
-      0));
+      sender_ctx.get(), bogus_enc, sizeof(bogus_enc), EVP_hpke_hkdf_sha256(),
+      EVP_hpke_aes_128_gcm(), public_key_r, sizeof(public_key_r), nullptr, 0));
   uint32_t err = ERR_get_error();
   EXPECT_EQ(ERR_LIB_EVP, ERR_GET_LIB(err));
   EXPECT_EQ(EVP_R_INVALID_BUFFER_SIZE, ERR_GET_REASON(err));
@@ -398,7 +418,7 @@
 
   ScopedEVP_HPKE_CTX receiver_ctx;
   ASSERT_FALSE(EVP_HPKE_CTX_setup_base_r_x25519(
-      receiver_ctx.get(), EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM,
+      receiver_ctx.get(), EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(),
       bogus_enc, sizeof(bogus_enc), public_key, sizeof(public_key), private_key,
       sizeof(private_key), nullptr, 0));
   uint32_t err = ERR_get_error();
@@ -412,8 +432,8 @@
   ScopedEVP_HPKE_CTX sender_ctx;
   uint8_t enc[X25519_PUBLIC_VALUE_LEN];
   ASSERT_FALSE(EVP_HPKE_CTX_setup_base_s_x25519(
-      sender_ctx.get(), enc, sizeof(enc), EVP_HPKE_HKDF_SHA256,
-      EVP_HPKE_AEAD_AES_128_GCM, bogus_public_key_r, sizeof(bogus_public_key_r),
+      sender_ctx.get(), enc, sizeof(enc), EVP_hpke_hkdf_sha256(),
+      EVP_hpke_aes_128_gcm(), bogus_public_key_r, sizeof(bogus_public_key_r),
       nullptr, 0));
   uint32_t err = ERR_get_error();
   EXPECT_EQ(ERR_LIB_EVP, ERR_GET_LIB(err));
@@ -437,7 +457,7 @@
   {
     // Test base mode with |bogus_public_key|.
     ASSERT_FALSE(EVP_HPKE_CTX_setup_base_r_x25519(
-        receiver_ctx.get(), EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM,
+        receiver_ctx.get(), EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(),
         enc, sizeof(enc), bogus_public_key, sizeof(bogus_public_key),
         private_key, sizeof(private_key), nullptr, 0));
     uint32_t err = ERR_get_error();
@@ -448,7 +468,7 @@
   {
     // Test base mode with |bogus_private_key|.
     ASSERT_FALSE(EVP_HPKE_CTX_setup_base_r_x25519(
-        receiver_ctx.get(), EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM,
+        receiver_ctx.get(), EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(),
         enc, sizeof(enc), public_key, sizeof(public_key), bogus_private_key,
         sizeof(bogus_private_key), nullptr, 0));
     uint32_t err = ERR_get_error();
diff --git a/crypto/hpke/internal.h b/crypto/hpke/internal.h
index 54382c8..6dee25a 100644
--- a/crypto/hpke/internal.h
+++ b/crypto/hpke/internal.h
@@ -32,35 +32,44 @@
 //
 // See https://tools.ietf.org/html/draft-irtf-cfrg-hpke-08.
 
-// EVP_HPKE_DHKEM_* are KEM identifiers.
+
+// Parameters.
+//
+// An HPKE context is parameterized by KEM, KDF, and AEAD algorithms.
+
+typedef struct evp_hpke_kdf_st EVP_HPKE_KDF;
+typedef struct evp_hpke_aead_st EVP_HPKE_AEAD;
+
+// The following constants are KEM identifiers.
 #define EVP_HPKE_DHKEM_X25519_HKDF_SHA256 0x0020
 
-// EVP_HPKE_AEAD_* are AEAD identifiers.
-#define EVP_HPKE_AEAD_AES_128_GCM 0x0001
-#define EVP_HPKE_AEAD_AES_256_GCM 0x0002
-#define EVP_HPKE_AEAD_CHACHA20POLY1305 0x0003
-
-// EVP_HPKE_HKDF_* are HKDF identifiers.
+// The following constants are KDF identifiers.
 #define EVP_HPKE_HKDF_SHA256 0x0001
 
-// EVP_HPKE_MAX_OVERHEAD contains the largest value that
-// |EVP_HPKE_CTX_max_overhead| would ever return for any context.
-#define EVP_HPKE_MAX_OVERHEAD EVP_AEAD_MAX_OVERHEAD
+// The following functions are KDF algorithms which may be used with HPKE.
+OPENSSL_EXPORT const EVP_HPKE_KDF *EVP_hpke_hkdf_sha256(void);
+
+// EVP_HPKE_KDF_id returns the HPKE KDF identifier for |kdf|.
+OPENSSL_EXPORT uint16_t EVP_HPKE_KDF_id(const EVP_HPKE_KDF *kdf);
+
+// The following constants are AEAD identifiers.
+#define EVP_HPKE_AES_128_GCM 0x0001
+#define EVP_HPKE_AES_256_GCM 0x0002
+#define EVP_HPKE_CHACHA20_POLY1305 0x0003
+
+// The following functions are AEAD algorithms which may be used with HPKE.
+OPENSSL_EXPORT const EVP_HPKE_AEAD *EVP_hpke_aes_128_gcm(void);
+OPENSSL_EXPORT const EVP_HPKE_AEAD *EVP_hpke_aes_256_gcm(void);
+OPENSSL_EXPORT const EVP_HPKE_AEAD *EVP_hpke_chacha20_poly1305(void);
+
+// EVP_HPKE_AEAD_id returns the HPKE AEAD identifier for |aead|.
+OPENSSL_EXPORT uint16_t EVP_HPKE_AEAD_id(const EVP_HPKE_AEAD *aead);
 
 
 // Encryption contexts.
 
 // An |EVP_HPKE_CTX| is an HPKE encryption context.
-typedef struct evp_hpke_ctx_st {
-  const EVP_MD *hkdf_md;
-  EVP_AEAD_CTX aead_ctx;
-  uint16_t kdf_id;
-  uint16_t aead_id;
-  uint8_t base_nonce[EVP_AEAD_MAX_NONCE_LENGTH];
-  uint8_t exporter_secret[EVP_MAX_MD_SIZE];
-  uint64_t seq;
-  int is_sender;
-} EVP_HPKE_CTX;
+typedef struct evp_hpke_ctx_st EVP_HPKE_CTX;
 
 // EVP_HPKE_CTX_init initializes an already-allocated |EVP_HPKE_CTX|. The caller
 // should then use one of the |EVP_HPKE_CTX_setup_*| functions.
@@ -74,12 +83,6 @@
 
 
 // Setting up HPKE contexts.
-//
-// In each of the following functions, |hpke| must have been initialized with
-// |EVP_HPKE_CTX_init|. |kdf_id| selects the KDF for non-KEM HPKE operations and
-// must be one of the |EVP_HPKE_HKDF_*| constants. |aead_id| selects the AEAD
-// for the "open" and "seal" operations and must be one of the |EVP_HPKE_AEAD_*|
-// constants.
 
 // EVP_HPKE_CTX_setup_base_s_x25519 sets up |hpke| as a sender context that can
 // encrypt for the private key corresponding to |peer_public_value| (the
@@ -90,18 +93,19 @@
 // key, to |out_enc|. It will fail if the buffer's size in |out_enc_len| is not
 // exactly |X25519_PUBLIC_VALUE_LEN|.
 OPENSSL_EXPORT int EVP_HPKE_CTX_setup_base_s_x25519(
-    EVP_HPKE_CTX *hpke, uint8_t *out_enc, size_t out_enc_len, uint16_t kdf_id,
-    uint16_t aead_id, const uint8_t *peer_public_value,
-    size_t peer_public_value_len, const uint8_t *info, size_t info_len);
+    EVP_HPKE_CTX *hpke, uint8_t *out_enc, size_t out_enc_len,
+    const EVP_HPKE_KDF *kdf, const EVP_HPKE_AEAD *aead,
+    const uint8_t *peer_public_value, size_t peer_public_value_len,
+    const uint8_t *info, size_t info_len);
 
 // EVP_HPKE_CTX_setup_base_s_x25519_with_seed_for_testing behaves like
 // |EVP_HPKE_CTX_setup_base_s_x25519|, but takes a seed value to behave
 // deterministically. This seed is the sender's ephemeral X25519 key.
 OPENSSL_EXPORT int EVP_HPKE_CTX_setup_base_s_x25519_with_seed_for_testing(
-    EVP_HPKE_CTX *hpke, uint8_t *out_enc, size_t out_enc_len, uint16_t kdf_id,
-    uint16_t aead_id, const uint8_t *peer_public_value,
-    size_t peer_public_value_len, const uint8_t *info, size_t info_len,
-    const uint8_t *seed, size_t seed_len);
+    EVP_HPKE_CTX *hpke, uint8_t *out_enc, size_t out_enc_len,
+    const EVP_HPKE_KDF *kdf, const EVP_HPKE_AEAD *aead,
+    const uint8_t *peer_public_value, size_t peer_public_value_len,
+    const uint8_t *info, size_t info_len, const uint8_t *seed, size_t seed_len);
 
 // EVP_HPKE_CTX_setup_base_r_x25519 sets up |hpke| as a recipient context that
 // can decrypt messages. It returns one on success, and zero otherwise.
@@ -110,10 +114,10 @@
 // |enc| is the encapsulated shared secret from the sender. If |enc| is invalid,
 // this function will fail.
 OPENSSL_EXPORT int EVP_HPKE_CTX_setup_base_r_x25519(
-    EVP_HPKE_CTX *hpke, uint16_t kdf_id, uint16_t aead_id, const uint8_t *enc,
-    size_t enc_len, const uint8_t *public_key, size_t public_key_len,
-    const uint8_t *private_key, size_t private_key_len, const uint8_t *info,
-    size_t info_len);
+    EVP_HPKE_CTX *hpke, const EVP_HPKE_KDF *kdf, const EVP_HPKE_AEAD *aead,
+    const uint8_t *enc, size_t enc_len, const uint8_t *public_key,
+    size_t public_key_len, const uint8_t *private_key, size_t private_key_len,
+    const uint8_t *info, size_t info_len);
 
 
 // Using an HPKE context.
@@ -166,28 +170,38 @@
                                        const uint8_t *context,
                                        size_t context_len);
 
+// EVP_HPKE_MAX_OVERHEAD contains the largest value that
+// |EVP_HPKE_CTX_max_overhead| would ever return for any context.
+#define EVP_HPKE_MAX_OVERHEAD EVP_AEAD_MAX_OVERHEAD
+
 // EVP_HPKE_CTX_max_overhead returns the maximum number of additional bytes
 // added by sealing data with |EVP_HPKE_CTX_seal|. The |hpke| context must be
 // set up as a sender.
 OPENSSL_EXPORT size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *hpke);
 
-// EVP_HPKE_CTX_get_aead_id returns |hpke|'s configured AEAD. The returned value
-// is one of the |EVP_HPKE_AEAD_*| constants, or zero if the context has not
-// been set up.
-OPENSSL_EXPORT uint16_t EVP_HPKE_CTX_get_aead_id(const EVP_HPKE_CTX *hpke);
+// EVP_HPKE_CTX_aead returns |hpke|'s configured AEAD, or NULL if the context
+// has not been set up.
+OPENSSL_EXPORT const EVP_HPKE_AEAD *EVP_HPKE_CTX_aead(const EVP_HPKE_CTX *hpke);
 
-// EVP_HPKE_CTX_get_aead_id returns |hpke|'s configured KDF. The returned value
-// is one of the |EVP_HPKE_HKDF_*| constants, or zero if the context has not
-// been set up.
-OPENSSL_EXPORT uint16_t EVP_HPKE_CTX_get_kdf_id(const EVP_HPKE_CTX *hpke);
+// EVP_HPKE_CTX_kdf returns |hpke|'s configured KDF, or NULL if the context
+// has not been set up.
+OPENSSL_EXPORT const EVP_HPKE_KDF *EVP_HPKE_CTX_kdf(const EVP_HPKE_CTX *hpke);
 
-// EVP_HPKE_get_aead returns the AEAD corresponding to |aead_id|, or NULL if
-// |aead_id| is not a known AEAD identifier.
-OPENSSL_EXPORT const EVP_AEAD *EVP_HPKE_get_aead(uint16_t aead_id);
 
-// EVP_HPKE_get_hkdf_md returns the hash function associated with |kdf_id|, or
-// NULL if |kdf_id| is not a known KDF identifier that uses HKDF.
-OPENSSL_EXPORT const EVP_MD *EVP_HPKE_get_hkdf_md(uint16_t kdf_id);
+// Private structures.
+//
+// The following structures are exported so their types are stack-allocatable,
+// but accessing or modifying their fields is forbidden.
+
+struct evp_hpke_ctx_st {
+  const EVP_HPKE_AEAD *aead;
+  const EVP_HPKE_KDF *kdf;
+  EVP_AEAD_CTX aead_ctx;
+  uint8_t base_nonce[EVP_AEAD_MAX_NONCE_LENGTH];
+  uint8_t exporter_secret[EVP_MAX_MD_SIZE];
+  uint64_t seq;
+  int is_sender;
+};
 
 
 #if defined(__cplusplus)
diff --git a/ssl/encrypted_client_hello.cc b/ssl/encrypted_client_hello.cc
index 959017e..da23319 100644
--- a/ssl/encrypted_client_hello.cc
+++ b/ssl/encrypted_client_hello.cc
@@ -12,13 +12,17 @@
  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
 
+#include <openssl/ssl.h>
+
+#include <assert.h>
+
 #include <openssl/bytestring.h>
 #include <openssl/curve25519.h>
 #include <openssl/err.h>
 #include <openssl/hkdf.h>
-#include <openssl/ssl.h>
 
 #include "internal.h"
+#include "../crypto/hpke/internal.h"
 
 
 #if defined(OPENSSL_MSAN)
@@ -29,6 +33,22 @@
 
 BSSL_NAMESPACE_BEGIN
 
+static const decltype(&EVP_hpke_aes_128_gcm) kSupportedAEADs[] = {
+    &EVP_hpke_aes_128_gcm,
+    &EVP_hpke_aes_256_gcm,
+    &EVP_hpke_chacha20_poly1305,
+};
+
+static const EVP_HPKE_AEAD *get_ech_aead(uint16_t aead_id) {
+  for (const auto aead_func : kSupportedAEADs) {
+    const EVP_HPKE_AEAD *aead = aead_func();
+    if (aead_id == EVP_HPKE_AEAD_id(aead)) {
+      return aead;
+    }
+  }
+  return nullptr;
+}
+
 // ssl_client_hello_write_without_extensions serializes |client_hello| into
 // |out|, omitting the length-prefixed extensions. It serializes individual
 // fields, starting with |client_hello->version|, and ignores the
@@ -316,18 +336,18 @@
 }
 
 
-bool ECHServerConfig::Init(Span<const uint8_t> raw,
+bool ECHServerConfig::Init(Span<const uint8_t> ech_config,
                            Span<const uint8_t> private_key,
                            bool is_retry_config) {
   assert(!initialized_);
   is_retry_config_ = is_retry_config;
 
-  if (!raw_.CopyFrom(raw)) {
+  if (!ech_config_.CopyFrom(ech_config)) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
     return false;
   }
-  // Read from |raw_| so we can save Spans with the same lifetime as |this|.
-  CBS reader(raw_);
+  // Read from |ech_config_| so we can save Spans with the same lifetime as |this|.
+  CBS reader(ech_config_);
 
   uint16_t version;
   if (!CBS_get_u16(&reader, &version)) {
@@ -386,12 +406,9 @@
       OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
       return false;
     }
-    // This parser fails when it encounters any bytes it does not understand. If
-    // the config lists any unsupported cipher suites, that is a parse error.
-    if (kdf_id != EVP_HPKE_HKDF_SHA256 ||
-        (aead_id != EVP_HPKE_AEAD_AES_128_GCM &&
-         aead_id != EVP_HPKE_AEAD_AES_256_GCM &&
-         aead_id != EVP_HPKE_AEAD_CHACHA20POLY1305)) {
+    // The server promises to support every option in the ECHConfig, so reject
+    // any unsupported cipher suites.
+    if (kdf_id != EVP_HPKE_HKDF_SHA256 || get_ech_aead(aead_id) == nullptr) {
       OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG);
       return false;
     }
@@ -414,10 +431,14 @@
   return true;
 }
 
-bool ECHServerConfig::SupportsCipherSuite(uint16_t kdf_id,
-                                          uint16_t aead_id) const {
+bool ECHServerConfig::SetupContext(EVP_HPKE_CTX *ctx, uint16_t kdf_id,
+                                   uint16_t aead_id,
+                                   Span<const uint8_t> enc) const {
   assert(initialized_);
+
+  // Check the cipher suite is supported by this ECHServerConfig.
   CBS cbs(cipher_suites_);
+  bool cipher_ok = false;
   while (CBS_len(&cbs) != 0) {
     uint16_t supported_kdf_id, supported_aead_id;
     if (!CBS_get_u16(&cbs, &supported_kdf_id) ||
@@ -425,10 +446,30 @@
       return false;
     }
     if (kdf_id == supported_kdf_id && aead_id == supported_aead_id) {
-      return true;
+      cipher_ok = true;
+      break;
     }
   }
-  return false;
+  if (!cipher_ok) {
+    return false;
+  }
+
+  static const uint8_t kInfoLabel[] = "tls ech";
+  ScopedCBB info_cbb;
+  if (!CBB_init(info_cbb.get(), sizeof(kInfoLabel) + ech_config_.size()) ||
+      !CBB_add_bytes(info_cbb.get(), kInfoLabel,
+                     sizeof(kInfoLabel) /* includes trailing NUL */) ||
+      !CBB_add_bytes(info_cbb.get(), ech_config_.data(), ech_config_.size())) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+    return false;
+  }
+
+  assert(kdf_id == EVP_HPKE_HKDF_SHA256);
+  assert(get_ech_aead(aead_id) != NULL);
+  return EVP_HPKE_CTX_setup_base_r_x25519(
+      ctx, EVP_hpke_hkdf_sha256(), get_ech_aead(aead_id), enc.data(),
+      enc.size(), public_key_.data(), public_key_.size(), private_key_,
+      sizeof(private_key_), CBB_data(info_cbb.get()), CBB_len(info_cbb.get()));
 }
 
 BSSL_NAMESPACE_END
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index 5d6b3e5..5da6b40 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -621,35 +621,10 @@
     if (hs->ech_server_config_list) {
       for (const ECHServerConfig &ech_config :
            hs->ech_server_config_list->configs) {
-        // Skip this config if the client-provided config_id does not match or
-        // if the client indicated an unsupported HPKE ciphersuite.
-        if (config_id != ech_config.config_id() ||
-            !ech_config.SupportsCipherSuite(kdf_id, aead_id)) {
-          continue;
-        }
-
-        static const uint8_t kInfoLabel[] = "tls ech";
-        ScopedCBB info_cbb;
-        if (!CBB_init(info_cbb.get(),
-                      sizeof(kInfoLabel) + ech_config.raw().size()) ||
-            !CBB_add_bytes(info_cbb.get(), kInfoLabel,
-                           sizeof(kInfoLabel) /* includes trailing NUL */) ||
-            !CBB_add_bytes(info_cbb.get(), ech_config.raw().data(),
-                           ech_config.raw().size())) {
-          OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
-          return ssl_hs_error;
-        }
-
-        // Set up a fresh HPKE context for each decryption attempt.
         hs->ech_hpke_ctx.Reset();
-
-        if (CBS_len(&enc) != X25519_PUBLIC_VALUE_LEN ||
-            !EVP_HPKE_CTX_setup_base_r_x25519(
-                hs->ech_hpke_ctx.get(), kdf_id, aead_id, CBS_data(&enc),
-                CBS_len(&enc), ech_config.public_key().data(),
-                ech_config.public_key().size(), ech_config.private_key().data(),
-                ech_config.private_key().size(), CBB_data(info_cbb.get()),
-                CBB_len(info_cbb.get()))) {
+        if (config_id != ech_config.config_id() ||
+            !ech_config.SetupContext(hs->ech_hpke_ctx.get(), kdf_id, aead_id,
+                                     enc)) {
           // Ignore the error and try another ECHConfig.
           ERR_clear_error();
           continue;
diff --git a/ssl/internal.h b/ssl/internal.h
index a51a0e1..f1085a1 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1443,33 +1443,27 @@
   bool Init(Span<const uint8_t> ech_config, Span<const uint8_t> private_key,
             bool is_retry_config);
 
-  // SupportsCipherSuite returns true when this ECHConfig supports the HPKE
-  // ciphersuite composed of |kdf_id| and |aead_id|. This function must only be
-  // called on an initialized object.
-  bool SupportsCipherSuite(uint16_t kdf_id, uint16_t aead_id) const;
+  // SetupContext sets up |ctx| for a new connection, given the specified
+  // HPKE ciphersuite and encapsulated KEM key. It returns true on success and
+  // false on error. This function may only be called on an initialized object.
+  bool SetupContext(EVP_HPKE_CTX *ctx, uint16_t kdf_id, uint16_t aead_id,
+                    Span<const uint8_t> enc) const;
 
-  Span<const uint8_t> raw() const {
+  Span<const uint8_t> ech_config() const {
     assert(initialized_);
-    return raw_;
-  }
-  Span<const uint8_t> public_key() const {
-    assert(initialized_);
-    return public_key_;
-  }
-  Span<const uint8_t> private_key() const {
-    assert(initialized_);
-    return MakeConstSpan(private_key_, sizeof(private_key_));
+    return ech_config_;
   }
   bool is_retry_config() const {
     assert(initialized_);
     return is_retry_config_;
   }
   uint8_t config_id() const {
+    assert(initialized_);
     return config_id_;
   }
 
  private:
-  Array<uint8_t> raw_;
+  Array<uint8_t> ech_config_;
   Span<const uint8_t> public_key_;
   Span<const uint8_t> cipher_suites_;
 
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 3d283f1..5f79e86 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -1573,7 +1573,7 @@
   ASSERT_TRUE(MakeECHConfig(
       &ech_config, 0x42, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
       MakeConstSpan(kECHConfigPublicKey, sizeof(kECHConfigPublicKey) - 1),
-      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM},
+      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM},
       /*extensions=*/{}));
 
   bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
@@ -1640,7 +1640,7 @@
   std::vector<uint8_t> ech_config;
   ASSERT_TRUE(MakeECHConfig(
       &ech_config, 0x42, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, kECHConfigPublicKey,
-      std::vector<uint16_t>{0x002 /* HKDF-SHA384 */, EVP_HPKE_AEAD_AES_128_GCM},
+      std::vector<uint16_t>{0x002 /* HKDF-SHA384 */, EVP_HPKE_AES_128_GCM},
       /*extensions=*/{}));
   EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
       config_list.get(), /*is_retry_config=*/1, ech_config.data(),
@@ -1660,7 +1660,7 @@
       0x77, 0x0e, 0xe8, 0xd1, 0xc9, 0xce, 0x0a, 0x8b, 0xb4, 0x6a};
   ASSERT_TRUE(MakeECHConfig(
       &ech_config, 0x42, 0x0010 /* DHKEM(P-256, HKDF-SHA256) */, kP256PublicKey,
-      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM},
+      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM},
       /*extensions=*/{}));
   EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
       config_list.get(), /*is_retry_config=*/1, ech_config.data(),
@@ -1670,7 +1670,7 @@
   static const uint8_t kExtensions[] = {0x00, 0x01, 0x00, 0x00};
   ASSERT_TRUE(MakeECHConfig(
       &ech_config, 0x42, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, kECHConfigPublicKey,
-      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AEAD_AES_128_GCM},
+      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM},
       kExtensions));
   EXPECT_FALSE(SSL_ECH_SERVER_CONFIG_LIST_add(
       config_list.get(), /*is_retry_config=*/1, ech_config.data(),
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index cc6cf05..0dc5306 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -618,13 +618,10 @@
     return true;
   }
 
-  constexpr uint16_t kdf_id = EVP_HPKE_HKDF_SHA256;
-  const uint16_t aead_id = EVP_has_aes_hardware()
-                               ? EVP_HPKE_AEAD_AES_128_GCM
-                               : EVP_HPKE_AEAD_CHACHA20POLY1305;
-  const EVP_AEAD *aead = EVP_HPKE_get_aead(aead_id);
-  assert(aead != nullptr);
-
+  const uint16_t kdf_id = EVP_HPKE_HKDF_SHA256;
+  const uint16_t aead_id = EVP_has_aes_hardware() ? EVP_HPKE_AES_128_GCM
+                                                  : EVP_HPKE_CHACHA20_POLY1305;
+  constexpr size_t kAEADOverhead = 16;  // Both AEADs have a 16-byte tag.
   uint8_t ech_config_id;
   RAND_bytes(&ech_config_id, 1);
 
@@ -671,9 +668,9 @@
   // range of 96 to 192. Note that this estimate does not fully capture
   // optional extensions like GREASE, but the rounding gives some leeway.
 
-  uint8_t payload[EVP_AEAD_MAX_OVERHEAD + 192];
+  uint8_t payload[kAEADOverhead + 192];
   const size_t payload_len =
-      EVP_AEAD_max_overhead(aead) + 32 * random_size(96 / 32, 192 / 32);
+      kAEADOverhead + 32 * random_size(96 / 32, 192 / 32);
   assert(payload_len <= sizeof(payload));
   RAND_bytes(payload, payload_len);
 
@@ -768,8 +765,8 @@
     if (!config.is_retry_config()) {
       continue;
     }
-    if (!CBB_add_bytes(&retry_configs, config.raw().data(),
-                       config.raw().size())) {
+    if (!CBB_add_bytes(&retry_configs, config.ech_config().data(),
+                       config.ech_config().size())) {
       return false;
     }
   }
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 676591b..6b9867d 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -636,10 +636,10 @@
 
     // Check that ClientECH.cipher_suite is unchanged and that
     // ClientECH.enc is empty.
-    if (kdf_id != EVP_HPKE_CTX_get_kdf_id(hs->ech_hpke_ctx.get()) ||
-        aead_id != EVP_HPKE_CTX_get_aead_id(hs->ech_hpke_ctx.get()) ||
-        config_id != hs->ech_config_id ||
-        CBS_len(&enc) > 0) {
+    if (kdf_id != EVP_HPKE_KDF_id(EVP_HPKE_CTX_kdf(hs->ech_hpke_ctx.get())) ||
+        aead_id !=
+            EVP_HPKE_AEAD_id(EVP_HPKE_CTX_aead(hs->ech_hpke_ctx.get())) ||
+        config_id != hs->ech_config_id || CBS_len(&enc) > 0) {
       OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
       ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
       return ssl_hs_error;