Make ECH server APIs take EVP_HPKE_KEY.

Previously we would extract the KEM ID from the ECHConfig and then parse
the private key using the corresponding KEM type. This CL makes it take
a pre-pared EVP_HPKE_KEY and checks it matches. This does require the
caller pass the key type through externally, which is probably prudent?
(On the other hand we are still inferring config from the rest of the
ECHConfig... maybe we can add an API to extract the EVP_HPKE_KEM from a
serialized ECHConfig if it becomes a problem. I could see runner or tool
wanting that out of convenience.)

The immediate motivation is to add APIs to programmatically construct
ECHConfigs. I'm thinking we can pass a const EVP_HPKE_KEY * to specify
the key, at which point it's weird for SSL_ECH_KEYS_add to look
different.

Bug: 275
Change-Id: I2d424323885103d3fe0a99a9012c160baa8653bd
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/48002
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 6840e83..fe50bc2 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -38,9 +38,11 @@
 struct evp_hpke_kem_st {
   uint16_t id;
   size_t public_key_len;
+  size_t private_key_len;
   size_t seed_len;
-  int (*init_key)(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem,
-                  const uint8_t *priv_key, size_t priv_key_len);
+  int (*init_key)(EVP_HPKE_KEY *key, const uint8_t *priv_key,
+                  size_t priv_key_len);
+  int (*generate_key)(EVP_HPKE_KEY *key);
   int (*encap_with_seed)(const EVP_HPKE_KEM *kem, uint8_t *out_shared_secret,
                          size_t *out_shared_secret_len, uint8_t *out_enc,
                          size_t *out_enc_len, size_t max_enc,
@@ -130,8 +132,8 @@
                              kem_context_len);
 }
 
-static int x25519_init_key(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem,
-                           const uint8_t *priv_key, size_t priv_key_len) {
+static int x25519_init_key(EVP_HPKE_KEY *key, const uint8_t *priv_key,
+                           size_t priv_key_len) {
   if (priv_key_len != X25519_PRIVATE_KEY_LEN) {
     OPENSSL_PUT_ERROR(EVP, EVP_R_DECODE_ERROR);
     return 0;
@@ -142,6 +144,11 @@
   return 1;
 }
 
+static int x25519_generate_key(EVP_HPKE_KEY *key) {
+  X25519_keypair(key->public_key, key->private_key);
+  return 1;
+}
+
 static int x25519_encap_with_seed(
     const EVP_HPKE_KEM *kem, uint8_t *out_shared_secret,
     size_t *out_shared_secret_len, uint8_t *out_enc, size_t *out_enc_len,
@@ -207,8 +214,10 @@
   static const EVP_HPKE_KEM kKEM = {
       /*id=*/EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
       /*public_key_len=*/X25519_PUBLIC_VALUE_LEN,
+      /*private_key_len=*/X25519_PRIVATE_KEY_LEN,
       /*seed_len=*/X25519_PRIVATE_KEY_LEN,
       x25519_init_key,
+      x25519_generate_key,
       x25519_encap_with_seed,
       x25519_decap,
   };
@@ -226,17 +235,37 @@
   // future.
 }
 
+int EVP_HPKE_KEY_copy(EVP_HPKE_KEY *dst, const EVP_HPKE_KEY *src) {
+  // For now, |EVP_HPKE_KEY| is trivially copyable.
+  OPENSSL_memcpy(dst, src, sizeof(EVP_HPKE_KEY));
+  return 1;
+}
+
 int EVP_HPKE_KEY_init(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem,
                       const uint8_t *priv_key, size_t priv_key_len) {
   EVP_HPKE_KEY_zero(key);
   key->kem = kem;
-  if (!kem->init_key(key, kem, priv_key, priv_key_len)) {
+  if (!kem->init_key(key, priv_key, priv_key_len)) {
     key->kem = NULL;
     return 0;
   }
   return 1;
 }
 
+int EVP_HPKE_KEY_generate(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem) {
+  EVP_HPKE_KEY_zero(key);
+  key->kem = kem;
+  if (!kem->generate_key(key)) {
+    key->kem = NULL;
+    return 0;
+  }
+  return 1;
+}
+
+const EVP_HPKE_KEM *EVP_HPKE_KEY_kem(const EVP_HPKE_KEY *key) {
+  return key->kem;
+}
+
 int EVP_HPKE_KEY_public_key(const EVP_HPKE_KEY *key, uint8_t *out,
                             size_t *out_len, size_t max_out) {
   if (max_out < key->kem->public_key_len) {
@@ -248,6 +277,17 @@
   return 1;
 }
 
+int EVP_HPKE_KEY_private_key(const EVP_HPKE_KEY *key, uint8_t *out,
+                            size_t *out_len, size_t max_out) {
+  if (max_out < key->kem->private_key_len) {
+    OPENSSL_PUT_ERROR(EVP, EVP_R_INVALID_BUFFER_SIZE);
+    return 0;
+  }
+  OPENSSL_memcpy(out, key->private_key, key->kem->private_key_len);
+  *out_len = key->kem->private_key_len;
+  return 1;
+}
+
 
 // Supported KDFs and AEADs.
 
diff --git a/crypto/hpke/hpke_test.cc b/crypto/hpke/hpke_test.cc
index 1b4ccdd..a7bfe75 100644
--- a/crypto/hpke/hpke_test.cc
+++ b/crypto/hpke/hpke_test.cc
@@ -63,7 +63,7 @@
     const EVP_HPKE_KDF *kdf = GetKDF();
     ASSERT_TRUE(kdf);
 
-    // Set up the sender.
+    // Test the sender.
     ScopedEVP_HPKE_CTX sender_ctx;
     uint8_t enc[EVP_HPKE_MAX_ENC_LENGTH];
     size_t enc_len;
@@ -72,26 +72,41 @@
         public_key_r_.data(), public_key_r_.size(), info_.data(), info_.size(),
         secret_key_e_.data(), secret_key_e_.size()));
     EXPECT_EQ(Bytes(enc, enc_len), Bytes(public_key_e_));
+    VerifySender(sender_ctx.get());
 
-    // Import the receiver key.
-    ScopedEVP_HPKE_KEY key;
-    ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), kem, secret_key_r_.data(),
+    // Test the recipient.
+    ScopedEVP_HPKE_KEY base_key;
+    ASSERT_TRUE(EVP_HPKE_KEY_init(base_key.get(), kem, secret_key_r_.data(),
                                   secret_key_r_.size()));
-    uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH];
-    size_t public_key_len;
-    ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key, &public_key_len,
-                                        sizeof(public_key)));
-    EXPECT_EQ(Bytes(public_key, public_key_len), Bytes(public_key_r_));
+    for (bool copy : {false, true}) {
+      SCOPED_TRACE(copy);
+      const EVP_HPKE_KEY *key = base_key.get();
+      ScopedEVP_HPKE_KEY key_copy;
+      if (copy) {
+        ASSERT_TRUE(EVP_HPKE_KEY_copy(key_copy.get(), base_key.get()));
+        key = key_copy.get();
+      }
 
-    // Set up the receiver.
-    ScopedEVP_HPKE_CTX receiver_ctx;
-    ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient(receiver_ctx.get(), key.get(), kdf,
-                                             aead, enc, enc_len, info_.data(),
-                                             info_.size()));
+      uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH];
+      size_t public_key_len;
+      ASSERT_TRUE(EVP_HPKE_KEY_public_key(key, public_key, &public_key_len,
+                                          sizeof(public_key)));
+      EXPECT_EQ(Bytes(public_key, public_key_len), Bytes(public_key_r_));
 
-    VerifyEncryptions(sender_ctx.get(), receiver_ctx.get());
-    VerifyExports(sender_ctx.get());
-    VerifyExports(receiver_ctx.get());
+      uint8_t private_key[EVP_HPKE_MAX_PRIVATE_KEY_LENGTH];
+      size_t private_key_len;
+      ASSERT_TRUE(EVP_HPKE_KEY_private_key(key, private_key, &private_key_len,
+                                           sizeof(private_key)));
+      EXPECT_EQ(Bytes(private_key, private_key_len), Bytes(secret_key_r_));
+
+      // Set up the recipient.
+      ScopedEVP_HPKE_CTX recipient_ctx;
+      ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient(recipient_ctx.get(), key, kdf,
+                                               aead, enc, enc_len, info_.data(),
+                                               info_.size()));
+
+      VerifyRecipient(recipient_ctx.get());
+    }
   }
 
  private:
@@ -113,28 +128,33 @@
     return nullptr;
   }
 
-  void VerifyEncryptions(EVP_HPKE_CTX *sender_ctx,
-                         EVP_HPKE_CTX *receiver_ctx) const {
+  void VerifySender(EVP_HPKE_CTX *ctx) const {
     for (const Encryption &task : encryptions_) {
       std::vector<uint8_t> encrypted(task.plaintext.size() +
-                                     EVP_HPKE_CTX_max_overhead(sender_ctx));
+                                     EVP_HPKE_CTX_max_overhead(ctx));
       size_t encrypted_len;
-      ASSERT_TRUE(EVP_HPKE_CTX_seal(
-          sender_ctx, encrypted.data(), &encrypted_len, encrypted.size(),
-          task.plaintext.data(), task.plaintext.size(), task.aad.data(),
-          task.aad.size()));
+      ASSERT_TRUE(EVP_HPKE_CTX_seal(ctx, encrypted.data(), &encrypted_len,
+                                    encrypted.size(), task.plaintext.data(),
+                                    task.plaintext.size(), task.aad.data(),
+                                    task.aad.size()));
 
       ASSERT_EQ(Bytes(encrypted.data(), encrypted_len), Bytes(task.ciphertext));
+    }
+    VerifyExports(ctx);
+  }
 
+  void VerifyRecipient(EVP_HPKE_CTX *ctx) const {
+    for (const Encryption &task : encryptions_) {
       std::vector<uint8_t> decrypted(task.ciphertext.size());
       size_t decrypted_len;
-      ASSERT_TRUE(EVP_HPKE_CTX_open(
-          receiver_ctx, decrypted.data(), &decrypted_len, decrypted.size(),
-          task.ciphertext.data(), task.ciphertext.size(), task.aad.data(),
-          task.aad.size()));
+      ASSERT_TRUE(EVP_HPKE_CTX_open(ctx, decrypted.data(), &decrypted_len,
+                                    decrypted.size(), task.ciphertext.data(),
+                                    task.ciphertext.size(), task.aad.data(),
+                                    task.aad.size()));
 
       ASSERT_EQ(Bytes(decrypted.data(), decrypted_len), Bytes(task.plaintext));
     }
+    VerifyExports(ctx);
   }
 
   void VerifyExports(EVP_HPKE_CTX *ctx) const {
@@ -262,16 +282,13 @@
   Span<const uint8_t> info_values[] = {{nullptr, 0}, info_a, info_b};
   Span<const uint8_t> ad_values[] = {{nullptr, 0}, ad_a, ad_b};
 
-  // Generate the receiver's keypair.
-  uint8_t secret_key_r[X25519_PRIVATE_KEY_LEN];
-  RAND_bytes(secret_key_r, sizeof(secret_key_r));
+  // Generate the recipient's keypair.
   ScopedEVP_HPKE_KEY key;
-  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
-                                secret_key_r, sizeof(secret_key_r)));
+  ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256()));
   uint8_t public_key_r[X25519_PUBLIC_VALUE_LEN];
   size_t public_key_r_len;
-  ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key_r, &public_key_r_len,
-                                      sizeof(public_key_r)));
+  ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key_r,
+                                      &public_key_r_len, sizeof(public_key_r)));
 
   for (const auto kdf : kAllKDFs) {
     SCOPED_TRACE(EVP_HPKE_KDF_id(kdf()));
@@ -290,15 +307,15 @@
               EVP_hpke_x25519_hkdf_sha256(), kdf(), aead(), public_key_r,
               public_key_r_len, info.data(), info.size()));
 
-          // Set up the receiver.
-          ScopedEVP_HPKE_CTX receiver_ctx;
-          ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient(receiver_ctx.get(), key.get(),
-                                                   kdf(), aead(), enc, enc_len,
-                                                   info.data(), info.size()));
+          // Set up the recipient.
+          ScopedEVP_HPKE_CTX recipient_ctx;
+          ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient(
+              recipient_ctx.get(), key.get(), kdf(), aead(), enc, enc_len,
+              info.data(), info.size()));
 
           const char kCleartextPayload[] = "foobar";
 
-          // Have sender encrypt message for the receiver.
+          // Have sender encrypt message for the recipient.
           std::vector<uint8_t> ciphertext(
               sizeof(kCleartextPayload) +
               EVP_HPKE_CTX_max_overhead(sender_ctx.get()));
@@ -309,10 +326,10 @@
               reinterpret_cast<const uint8_t *>(kCleartextPayload),
               sizeof(kCleartextPayload), ad.data(), ad.size()));
 
-          // Have receiver decrypt the message.
+          // Have recipient decrypt the message.
           std::vector<uint8_t> cleartext(ciphertext.size());
           size_t cleartext_len;
-          ASSERT_TRUE(EVP_HPKE_CTX_open(receiver_ctx.get(), cleartext.data(),
+          ASSERT_TRUE(EVP_HPKE_CTX_open(recipient_ctx.get(), cleartext.data(),
                                         &cleartext_len, cleartext.size(),
                                         ciphertext.data(), ciphertext_len,
                                         ad.data(), ad.size()));
@@ -336,12 +353,8 @@
       0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8,
   };
 
-  // Generate a valid keypair for the receiver.
-  uint8_t secret_key_r[X25519_PRIVATE_KEY_LEN];
-  RAND_bytes(secret_key_r, sizeof(secret_key_r));
   ScopedEVP_HPKE_KEY key;
-  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
-                                secret_key_r, sizeof(secret_key_r)));
+  ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256()));
 
   for (const auto kdf : kAllKDFs) {
     SCOPED_TRACE(EVP_HPKE_KDF_id(kdf()));
@@ -356,38 +369,34 @@
           EVP_hpke_x25519_hkdf_sha256(), kdf(), aead(), kSmallOrderPoint,
           sizeof(kSmallOrderPoint), nullptr, 0));
 
-      // Set up the receiver, passing in kSmallOrderPoint as |enc|.
-      ScopedEVP_HPKE_CTX receiver_ctx;
+      // Set up the recipient, passing in kSmallOrderPoint as |enc|.
+      ScopedEVP_HPKE_CTX recipient_ctx;
       ASSERT_FALSE(EVP_HPKE_CTX_setup_recipient(
-          receiver_ctx.get(), key.get(), kdf(), aead(), kSmallOrderPoint,
+          recipient_ctx.get(), key.get(), kdf(), aead(), kSmallOrderPoint,
           sizeof(kSmallOrderPoint), nullptr, 0));
     }
   }
 }
 
-// Test that Seal() fails when the context has been initialized as a receiver.
-TEST(HPKETest, ReceiverInvalidSeal) {
+// Test that Seal() fails when the context has been initialized as a recipient.
+TEST(HPKETest, RecipientInvalidSeal) {
   const uint8_t kMockEnc[X25519_PUBLIC_VALUE_LEN] = {0xff};
   const char kCleartextPayload[] = "foobar";
 
-  // Generate the receiver's keypair.
-  uint8_t secret_key_r[X25519_PRIVATE_KEY_LEN];
-  RAND_bytes(secret_key_r, sizeof(secret_key_r));
   ScopedEVP_HPKE_KEY key;
-  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
-                                secret_key_r, sizeof(secret_key_r)));
+  ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256()));
 
-  // Set up the receiver.
-  ScopedEVP_HPKE_CTX receiver_ctx;
+  // Set up the recipient.
+  ScopedEVP_HPKE_CTX recipient_ctx;
   ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient(
-      receiver_ctx.get(), key.get(), EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(),
-      kMockEnc, sizeof(kMockEnc), nullptr, 0));
+      recipient_ctx.get(), key.get(), EVP_hpke_hkdf_sha256(),
+      EVP_hpke_aes_128_gcm(), kMockEnc, sizeof(kMockEnc), nullptr, 0));
 
-  // Call Seal() on the receiver.
+  // Call Seal() on the recipient.
   size_t ciphertext_len;
   uint8_t ciphertext[100];
   ASSERT_FALSE(EVP_HPKE_CTX_seal(
-      receiver_ctx.get(), ciphertext, &ciphertext_len, sizeof(ciphertext),
+      recipient_ctx.get(), ciphertext, &ciphertext_len, sizeof(ciphertext),
       reinterpret_cast<const uint8_t *>(kCleartextPayload),
       sizeof(kCleartextPayload), nullptr, 0));
 }
@@ -397,7 +406,7 @@
   const uint8_t kMockCiphertext[100] = {0xff};
   const size_t kMockCiphertextLen = 80;
 
-  // Generate the receiver's keypair.
+  // Generate the recipient's keypair.
   uint8_t secret_key_r[X25519_PRIVATE_KEY_LEN];
   uint8_t public_key_r[X25519_PUBLIC_VALUE_LEN];
   X25519_keypair(public_key_r, secret_key_r);
@@ -454,18 +463,15 @@
   EXPECT_EQ(size_t{X25519_PUBLIC_VALUE_LEN}, enc_len);
 }
 
-TEST(HPKETest, SetupReceiverWrongLengthEnc) {
-  uint8_t private_key[X25519_PRIVATE_KEY_LEN];
-  RAND_bytes(private_key, sizeof(private_key));
+TEST(HPKETest, SetupRecipientWrongLengthEnc) {
   ScopedEVP_HPKE_KEY key;
-  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
-                                private_key, sizeof(private_key)));
+  ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256()));
 
   const uint8_t bogus_enc[X25519_PUBLIC_VALUE_LEN + 5] = {0xff};
 
-  ScopedEVP_HPKE_CTX receiver_ctx;
+  ScopedEVP_HPKE_CTX recipient_ctx;
   ASSERT_FALSE(EVP_HPKE_CTX_setup_recipient(
-      receiver_ctx.get(), key.get(), EVP_hpke_hkdf_sha256(),
+      recipient_ctx.get(), key.get(), EVP_hpke_hkdf_sha256(),
       EVP_hpke_aes_128_gcm(), bogus_enc, sizeof(bogus_enc), nullptr, 0));
   uint32_t err = ERR_get_error();
   EXPECT_EQ(ERR_LIB_EVP, ERR_GET_LIB(err));
@@ -489,7 +495,7 @@
   ERR_clear_error();
 }
 
-TEST(HPKETest, InvalidReceiverKey) {
+TEST(HPKETest, InvalidRecipientKey) {
   const uint8_t private_key[X25519_PUBLIC_VALUE_LEN + 5] = {0xff};
   ScopedEVP_HPKE_KEY key;
   EXPECT_FALSE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
diff --git a/fuzz/ssl_ctx_api.cc b/fuzz/ssl_ctx_api.cc
index 3739e87..da0a2d3 100644
--- a/fuzz/ssl_ctx_api.cc
+++ b/fuzz/ssl_ctx_api.cc
@@ -22,6 +22,7 @@
 #include <openssl/bytestring.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/hpke.h>
 #include <openssl/rsa.h>
 #include <openssl/ssl.h>
 #include <openssl/stack.h>
@@ -503,10 +504,15 @@
             !CBS_get_u16_length_prefixed(cbs, &private_key)) {
           return;
         }
-        SSL_ECH_KEYS_add(keys.get(), is_retry_config, CBS_data(&ech_config),
-                         CBS_len(&ech_config), CBS_data(&private_key),
-                         CBS_len(&private_key));
-        SSL_CTX_set1_ech_keys(ctx, keys.get());
+        bssl::ScopedEVP_HPKE_KEY key;
+        if (!EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
+                               CBS_data(&private_key), CBS_len(&private_key)) ||
+            !SSL_ECH_KEYS_add(keys.get(), is_retry_config,
+                              CBS_data(&ech_config), CBS_len(&ech_config),
+                              key.get()) ||
+            !SSL_CTX_set1_ech_keys(ctx, keys.get())) {
+          return;
+        }
       },
   };
 
diff --git a/include/openssl/hpke.h b/include/openssl/hpke.h
index 358ca23..2f14f77 100644
--- a/include/openssl/hpke.h
+++ b/include/openssl/hpke.h
@@ -80,7 +80,8 @@
 // with the |EVP_HPKE_KEY| type.
 
 // EVP_HPKE_KEY_zero sets an uninitialized |EVP_HPKE_KEY| to the zero state. The
-// caller should then use |EVP_HPKE_KEY_init| to finish initializing |key|.
+// caller should then use |EVP_HPKE_KEY_init|, |EVP_HPKE_KEY_copy|, or
+// |EVP_HPKE_KEY_generate| to finish initializing |key|.
 //
 // It is safe, but not necessary to call |EVP_HPKE_KEY_cleanup| in this state.
 // This may be used for more uniform cleanup of |EVP_HPKE_KEY|.
@@ -89,6 +90,13 @@
 // EVP_HPKE_KEY_cleanup releases memory referenced by |key|.
 OPENSSL_EXPORT void EVP_HPKE_KEY_cleanup(EVP_HPKE_KEY *key);
 
+// EVP_HPKE_KEY_copy sets |dst| to a copy of |src|. It returns one on success
+// and zero on error. On success, the caller must call |EVP_HPKE_KEY_cleanup| to
+// release |dst|. On failure, calling |EVP_HPKE_KEY_cleanup| is safe, but not
+// necessary.
+OPENSSL_EXPORT int EVP_HPKE_KEY_copy(EVP_HPKE_KEY *dst,
+                                     const EVP_HPKE_KEY *src);
+
 // EVP_HPKE_KEY_init decodes |priv_key| as a private key for |kem| and
 // initializes |key| with the result. It returns one on success and zero if
 // |priv_key| was invalid. On success, the caller must call
@@ -98,6 +106,13 @@
                                      const uint8_t *priv_key,
                                      size_t priv_key_len);
 
+// EVP_HPKE_KEY_generate sets |key| to a newly-generated key using |kem|.
+OPENSSL_EXPORT int EVP_HPKE_KEY_generate(EVP_HPKE_KEY *key,
+                                         const EVP_HPKE_KEM *kem);
+
+// EVP_HPKE_KEY_kem returns the HPKE KEM used by |key|.
+OPENSSL_EXPORT const EVP_HPKE_KEM *EVP_HPKE_KEY_kem(const EVP_HPKE_KEY *key);
+
 // EVP_HPKE_MAX_PUBLIC_KEY_LENGTH is the maximum length of a public key for all
 // KEMs supported by this library.
 #define EVP_HPKE_MAX_PUBLIC_KEY_LENGTH 32
@@ -111,6 +126,19 @@
                                            uint8_t *out, size_t *out_len,
                                            size_t max_out);
 
+// EVP_HPKE_MAX_PRIVATE_KEY_LENGTH is the maximum length of a private key for
+// all KEMs supported by this library.
+#define EVP_HPKE_MAX_PRIVATE_KEY_LENGTH 32
+
+// EVP_HPKE_KEY_private_key writes |key|'s private key to |out| and sets
+// |*out_len| to the number of bytes written. On success, it returns one and
+// writes at most |max_out| bytes. If |max_out| is too small, it returns zero.
+// Setting |max_out| to |EVP_HPKE_MAX_PRIVATE_KEY_LENGTH| will ensure the
+// private key fits.
+OPENSSL_EXPORT int EVP_HPKE_KEY_private_key(const EVP_HPKE_KEY *key,
+                                            uint8_t *out, size_t *out_len,
+                                            size_t max_out);
+
 
 // Encryption contexts.
 //
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 191cf4b..5b07ebf 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3574,21 +3574,21 @@
 // SSL_ECH_KEYS_free releases memory associated with |keys|.
 OPENSSL_EXPORT void SSL_ECH_KEYS_free(SSL_ECH_KEYS *keys);
 
-// SSL_ECH_KEYS_add appends an ECHConfig in |ech_config| and its
-// corresponding private key in |private_key| to |keys|. When |is_retry_config|
-// is non-zero, this config will be returned to the client on configuration
-// mismatch. It returns one on success and zero on error. See also
-// |SSL_CTX_set1_ech_keys|.
+// SSL_ECH_KEYS_add decodes |ech_config| as an ECHConfig and appends it with
+// |key| to |keys|. If |is_retry_config| is non-zero, this config will be
+// returned to the client on configuration mismatch. It returns one on success
+// and zero on error.
 //
 // This function should be called successively to register each ECHConfig in
 // decreasing order of preference. This configuration must be completed before
 // setting |keys| on an |SSL_CTX| with |SSL_CTX_set1_ech_keys|. After that
 // point, |keys| is immutable; no more ECHConfig values may be added.
+//
+// See also |SSL_CTX_set1_ech_keys|.
 OPENSSL_EXPORT int SSL_ECH_KEYS_add(SSL_ECH_KEYS *keys, int is_retry_config,
                                     const uint8_t *ech_config,
                                     size_t ech_config_len,
-                                    const uint8_t *private_key,
-                                    size_t private_key_len);
+                                    const EVP_HPKE_KEY *key);
 
 // SSL_CTX_set1_ech_keys configures |ctx| to use |keys| to decrypt encrypted
 // ClientHellos. It returns one on success, and zero on failure. If |keys| does
diff --git a/ssl/encrypted_client_hello.cc b/ssl/encrypted_client_hello.cc
index 46590bd..d1adc68 100644
--- a/ssl/encrypted_client_hello.cc
+++ b/ssl/encrypted_client_hello.cc
@@ -352,8 +352,7 @@
 
 
 bool ECHServerConfig::Init(Span<const uint8_t> ech_config,
-                           Span<const uint8_t> private_key,
-                           bool is_retry_config) {
+                           const EVP_HPKE_KEY *key, bool is_retry_config) {
   assert(!initialized_);
   is_retry_config_ = is_retry_config;
 
@@ -420,30 +419,25 @@
     }
   }
 
-  // We only support one KEM.
-  if (kem_id != EVP_HPKE_DHKEM_X25519_HKDF_SHA256) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG);
-    return false;
-  }
-  if (!EVP_HPKE_KEY_init(key_.get(), EVP_hpke_x25519_hkdf_sha256(),
-                         private_key.data(), private_key.size())) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
-    return false;
-  }
-  // Check the public key in the ECHConfig matches the private key.
+  // Check the public key in the ECHConfig matches |key|.
   uint8_t expected_public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH];
   size_t expected_public_key_len;
-  if (!EVP_HPKE_KEY_public_key(key_.get(), expected_public_key,
+  if (!EVP_HPKE_KEY_public_key(key, expected_public_key,
                                &expected_public_key_len,
                                sizeof(expected_public_key))) {
     return false;
   }
-  if (MakeConstSpan(expected_public_key, expected_public_key_len) !=
-      public_key) {
+  if (kem_id != EVP_HPKE_KEM_id(EVP_HPKE_KEY_kem(key)) ||
+      MakeConstSpan(expected_public_key, expected_public_key_len) !=
+          public_key) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH);
     return false;
   }
 
+  if (!EVP_HPKE_KEY_copy(key_.get(), key)) {
+    return false;
+  }
+
   initialized_ = true;
   return true;
 }
@@ -517,13 +511,12 @@
 
 int SSL_ECH_KEYS_add(SSL_ECH_KEYS *configs, int is_retry_config,
                      const uint8_t *ech_config, size_t ech_config_len,
-                     const uint8_t *private_key, size_t private_key_len) {
+                     const EVP_HPKE_KEY *key) {
   UniquePtr<ECHServerConfig> parsed_config = MakeUnique<ECHServerConfig>();
   if (!parsed_config) {
     return 0;
   }
-  if (!parsed_config->Init(MakeConstSpan(ech_config, ech_config_len),
-                           MakeConstSpan(private_key, private_key_len),
+  if (!parsed_config->Init(MakeConstSpan(ech_config, ech_config_len), key,
                            !!is_retry_config)) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
     return 0;
diff --git a/ssl/internal.h b/ssl/internal.h
index 8aceac7..62a9a06 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1438,11 +1438,9 @@
   ~ECHServerConfig() = default;
   ECHServerConfig &operator=(ECHServerConfig &&) = delete;
 
-  // Init parses |ech_config| as an ECHConfig and saves a copy of |private_key|.
-  // It returns true on success and false on error. It will also error if
-  // |private_key| is not a valid X25519 private key or it does not correspond
-  // to the parsed public key.
-  bool Init(Span<const uint8_t> ech_config, Span<const uint8_t> private_key,
+  // Init parses |ech_config| as an ECHConfig and saves a copy of |key|.
+  // It returns true on success and false on error.
+  bool Init(Span<const uint8_t> ech_config, const EVP_HPKE_KEY *key,
             bool is_retry_config);
 
   // SetupContext sets up |ctx| for a new connection, given the specified
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index f939bfe..a8e6a0c 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -32,6 +32,7 @@
 #include <openssl/curve25519.h>
 #include <openssl/err.h>
 #include <openssl/hmac.h>
+#include <openssl/hpke.h>
 #include <openssl/pem.h>
 #include <openssl/sha.h>
 #include <openssl/ssl.h>
@@ -1539,40 +1540,48 @@
 }
 
 TEST(SSLTest, ECHKeys) {
-  // kWrongPrivateKey is an unrelated, but valid X25519 private key.
-  const uint8_t kWrongPrivateKey[X25519_PRIVATE_KEY_LEN] = {
-      0xbb, 0xfe, 0x08, 0xf7, 0x31, 0xde, 0x9c, 0x8a, 0xf2, 0x06, 0x4a,
-      0x18, 0xd7, 0x8b, 0x79, 0x31, 0xe2, 0x53, 0xdd, 0x63, 0x8f, 0x58,
-      0x42, 0xda, 0x21, 0x0e, 0x61, 0x97, 0x29, 0xcc, 0x17, 0x71};
-
   bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
   ASSERT_TRUE(ctx);
 
   bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
   ASSERT_TRUE(keys);
 
-  // Adding an ECHConfig with the wrong private key is an error.
+  // Adding an ECHConfig with the wrong public key is an error.
+  bssl::ScopedEVP_HPKE_KEY wrong_key;
+  ASSERT_TRUE(
+      EVP_HPKE_KEY_generate(wrong_key.get(), EVP_hpke_x25519_hkdf_sha256()));
   ASSERT_FALSE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1, kECHConfig,
-                                sizeof(kECHConfig), kWrongPrivateKey,
-                                sizeof(kWrongPrivateKey)));
+                                sizeof(kECHConfig), wrong_key.get()));
   uint32_t err = ERR_get_error();
   EXPECT_EQ(ERR_LIB_SSL, ERR_GET_LIB(err));
   EXPECT_EQ(SSL_R_ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH,
             ERR_GET_REASON(err));
   ERR_clear_error();
 
-  // Adding an ECHConfig with the matching private key succeeds.
+  // Adding an ECHConfig with the right public key, but wrong KEM ID, is an
+  // error.
+  bssl::ScopedEVP_HPKE_KEY key;
+  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
+                                kECHPrivateKey, sizeof(kECHPrivateKey)));
+  std::vector<uint8_t> ech_config;
+  ASSERT_TRUE(MakeECHConfig(
+      &ech_config, 0x42, 0x0010 /* DHKEM(P-256, HKDF-SHA256) */, kECHPublicKey,
+      std::vector<uint16_t>{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM},
+      /*extensions=*/{}));
+  EXPECT_FALSE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1,
+                                ech_config.data(), ech_config.size(),
+                                key.get()));
+
+  // Adding an ECHConfig with the matching public key succeeds.
   ASSERT_TRUE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1, kECHConfig,
-                               sizeof(kECHConfig), kECHPrivateKey,
-                               sizeof(kECHPrivateKey)));
+                               sizeof(kECHConfig), key.get()));
 
   ASSERT_TRUE(SSL_CTX_set1_ech_keys(ctx.get(), keys.get()));
 
   // Build a new config list and replace the old one on |ctx|.
   bssl::UniquePtr<SSL_ECH_KEYS> next_keys(SSL_ECH_KEYS_new());
   ASSERT_TRUE(SSL_ECH_KEYS_add(next_keys.get(), /*is_retry_config=*/1,
-                               kECHConfig, sizeof(kECHConfig), kECHPrivateKey,
-                               sizeof(kECHPrivateKey)));
+                               kECHConfig, sizeof(kECHConfig),key.get()));
   ASSERT_TRUE(SSL_CTX_set1_ech_keys(ctx.get(), next_keys.get()));
 }
 
@@ -1587,11 +1596,14 @@
   bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
   ASSERT_TRUE(ctx);
 
+  bssl::ScopedEVP_HPKE_KEY key;
+  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
+                                kECHPrivateKey, sizeof(kECHPrivateKey)));
   bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
   ASSERT_TRUE(keys);
   ASSERT_FALSE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1,
                                 ech_config.data(), ech_config.size(),
-                                kECHPrivateKey, sizeof(kECHPrivateKey)));
+                                key.get()));
 
   uint32_t err = ERR_peek_error();
   EXPECT_EQ(ERR_LIB_SSL, ERR_GET_LIB(err));
@@ -1609,10 +1621,11 @@
   bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
   ASSERT_TRUE(keys);
 
-  // Adding an ECHConfig with the matching private key succeeds.
+  bssl::ScopedEVP_HPKE_KEY key;
+  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
+                                kECHPrivateKey, sizeof(kECHPrivateKey)));
   ASSERT_TRUE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/0, kECHConfig,
-                               sizeof(kECHConfig), kECHPrivateKey,
-                               sizeof(kECHPrivateKey)));
+                               sizeof(kECHConfig), key.get()));
 
   ASSERT_FALSE(SSL_CTX_set1_ech_keys(ctx.get(), keys.get()));
   uint32_t err = ERR_peek_error();
@@ -1623,8 +1636,7 @@
   // Add the same ECHConfig to the list, but this time mark it as a retry
   // config.
   ASSERT_TRUE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1, kECHConfig,
-                               sizeof(kECHConfig), kECHPrivateKey,
-                               sizeof(kECHPrivateKey)));
+                               sizeof(kECHConfig), key.get()));
   ASSERT_TRUE(SSL_CTX_set1_ech_keys(ctx.get(), keys.get()));
 }
 
@@ -1632,12 +1644,15 @@
 TEST(SSLTest, UnsupportedECHConfig) {
   bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
   ASSERT_TRUE(keys);
+  bssl::ScopedEVP_HPKE_KEY key;
+  ASSERT_TRUE(EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
+                                kECHPrivateKey, sizeof(kECHPrivateKey)));
 
   // Unsupported versions are rejected.
   static const uint8_t kUnsupportedVersion[] = {0xff, 0xff, 0x00, 0x00};
   EXPECT_FALSE(SSL_ECH_KEYS_add(
       keys.get(), /*is_retry_config=*/1, kUnsupportedVersion,
-      sizeof(kUnsupportedVersion), kECHPrivateKey, sizeof(kECHPrivateKey)));
+      sizeof(kUnsupportedVersion), key.get()));
 
   // Unsupported cipher suites are rejected. (We only support HKDF-SHA256.)
   std::vector<uint8_t> ech_config;
@@ -1647,27 +1662,7 @@
       /*extensions=*/{}));
   EXPECT_FALSE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1,
                                 ech_config.data(), ech_config.size(),
-                                kECHPrivateKey, sizeof(kECHPrivateKey)));
-
-  // Unsupported KEMs are rejected.
-  static const uint8_t kP256PublicKey[] = {
-      0x04, 0xe6, 0x2b, 0x69, 0xe2, 0xbf, 0x65, 0x9f, 0x97, 0xbe, 0x2f,
-      0x1e, 0x0d, 0x94, 0x8a, 0x4c, 0xd5, 0x97, 0x6b, 0xb7, 0xa9, 0x1e,
-      0x0d, 0x46, 0xfb, 0xdd, 0xa9, 0xa9, 0x1e, 0x9d, 0xdc, 0xba, 0x5a,
-      0x01, 0xe7, 0xd6, 0x97, 0xa8, 0x0a, 0x18, 0xf9, 0xc3, 0xc4, 0xa3,
-      0x1e, 0x56, 0xe2, 0x7c, 0x83, 0x48, 0xdb, 0x16, 0x1a, 0x1c, 0xf5,
-      0x1d, 0x7e, 0xf1, 0x94, 0x2d, 0x4b, 0xcf, 0x72, 0x22, 0xc1};
-  static const uint8_t kP256PrivateKey[] = {
-      0x07, 0x0f, 0x08, 0x72, 0x7a, 0xd4, 0xa0, 0x4a, 0x9c, 0xdd, 0x59,
-      0xc9, 0x4d, 0x89, 0x68, 0x77, 0x08, 0xb5, 0x6f, 0xc9, 0x5d, 0x30,
-      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_AES_128_GCM},
-      /*extensions=*/{}));
-  EXPECT_FALSE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1,
-                                ech_config.data(), ech_config.size(),
-                                kP256PrivateKey, sizeof(kP256PrivateKey)));
+                                key.get()));
 
   // Unsupported extensions are rejected.
   static const uint8_t kExtensions[] = {0x00, 0x01, 0x00, 0x00};
@@ -1677,7 +1672,7 @@
       kExtensions));
   EXPECT_FALSE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1,
                                 ech_config.data(), ech_config.size(),
-                                kECHPrivateKey, sizeof(kECHPrivateKey)));
+                                key.get()));
 }
 
 static void AppendSession(SSL_SESSION *session, void *arg) {
diff --git a/ssl/test/fuzzer.h b/ssl/test/fuzzer.h
index 07c2d6d..509cfdb 100644
--- a/ssl/test/fuzzer.h
+++ b/ssl/test/fuzzer.h
@@ -26,6 +26,7 @@
 #include <openssl/bytestring.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/hpke.h>
 #include <openssl/rand.h>
 #include <openssl/rsa.h>
 #include <openssl/ssl.h>
@@ -456,14 +457,13 @@
 
     if (role_ == kServer) {
       bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
-      if (!keys) {
-        return false;
-      }
-      if (!SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/true, kECHConfig,
-                            sizeof(kECHConfig), kECHKey, sizeof(kECHKey))) {
-        return false;
-      }
-      if (!SSL_CTX_set1_ech_keys(ctx_.get(), keys.get())) {
+      bssl::ScopedEVP_HPKE_KEY key;
+      if (!keys ||
+          !EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(), kECHKey,
+                             sizeof(kECHKey)) ||
+          !SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/true, kECHConfig,
+                            sizeof(kECHConfig), key.get()) ||
+          !SSL_CTX_set1_ech_keys(ctx_.get(), keys.get())) {
         return false;
       }
     }
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 9e68229..7978a70 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -22,6 +22,7 @@
 #include <memory>
 
 #include <openssl/base64.h>
+#include <openssl/hpke.h>
 #include <openssl/rand.h>
 #include <openssl/ssl.h>
 
@@ -1737,12 +1738,15 @@
       const std::string &ech_config = ech_server_configs[i];
       const std::string &ech_private_key = ech_server_keys[i];
       const int is_retry_config = ech_is_retry_config[i];
-      if (!SSL_ECH_KEYS_add(
+      bssl::ScopedEVP_HPKE_KEY key;
+      if (!EVP_HPKE_KEY_init(
+              key.get(), EVP_hpke_x25519_hkdf_sha256(),
+              reinterpret_cast<const uint8_t *>(ech_private_key.data()),
+              ech_private_key.size()) ||
+          !SSL_ECH_KEYS_add(
               keys.get(), is_retry_config,
               reinterpret_cast<const uint8_t *>(ech_config.data()),
-              ech_config.size(),
-              reinterpret_cast<const uint8_t *>(ech_private_key.data()),
-              ech_private_key.size())) {
+              ech_config.size(), key.get())) {
         return nullptr;
       }
     }
diff --git a/tool/server.cc b/tool/server.cc
index 858e8a1..18b692d 100644
--- a/tool/server.cc
+++ b/tool/server.cc
@@ -17,6 +17,7 @@
 #include <memory>
 
 #include <openssl/err.h>
+#include <openssl/hpke.h>
 #include <openssl/rand.h>
 #include <openssl/ssl.h>
 
@@ -61,12 +62,12 @@
         "-ocsp-response", kOptionalArgument, "OCSP response file to send",
     },
     {
-        "-echconfig-key",
+        "-ech-key",
         kOptionalArgument,
         "File containing the private key corresponding to the ECHConfig.",
     },
     {
-        "-echconfig",
+        "-ech-config",
         kOptionalArgument,
         "File containing one ECHConfig.",
     },
@@ -271,40 +272,41 @@
     }
   }
 
-  if (args_map.count("-echconfig-key") + args_map.count("-echconfig") == 1) {
+  if (args_map.count("-ech-key") + args_map.count("-ech-config") == 1) {
     fprintf(stderr,
-            "-echconfig and -echconfig-key must be specified together.\n");
+            "-ech-config and -ech-key must be specified together.\n");
     return false;
   }
 
-  if (args_map.count("-echconfig-key") != 0) {
-    std::string echconfig_key_path = args_map["-echconfig-key"];
-    std::string echconfig_path = args_map["-echconfig"];
-
+  if (args_map.count("-ech-key") != 0) {
     // Load the ECH private key.
-    ScopedFILE echconfig_key_file(fopen(echconfig_key_path.c_str(), "rb"));
-    std::vector<uint8_t> echconfig_key;
-    if (echconfig_key_file == nullptr ||
-        !ReadAll(&echconfig_key, echconfig_key_file.get())) {
-      fprintf(stderr, "Error reading %s\n", echconfig_key_path.c_str());
+    std::string ech_key_path = args_map["-ech-key"];
+    ScopedFILE ech_key_file(fopen(ech_key_path.c_str(), "rb"));
+    std::vector<uint8_t> ech_key;
+    if (ech_key_file == nullptr ||
+        !ReadAll(&ech_key, ech_key_file.get())) {
+      fprintf(stderr, "Error reading %s\n", ech_key_path.c_str());
       return false;
     }
 
     // Load the ECHConfig.
-    ScopedFILE echconfig_file(fopen(echconfig_path.c_str(), "rb"));
-    std::vector<uint8_t> echconfig;
-    if (echconfig_file == nullptr ||
-        !ReadAll(&echconfig, echconfig_file.get())) {
-      fprintf(stderr, "Error reading %s\n", echconfig_path.c_str());
+    std::string ech_config_path = args_map["-ech-config"];
+    ScopedFILE ech_config_file(fopen(ech_config_path.c_str(), "rb"));
+    std::vector<uint8_t> ech_config;
+    if (ech_config_file == nullptr ||
+        !ReadAll(&ech_config, ech_config_file.get())) {
+      fprintf(stderr, "Error reading %s\n", ech_config_path.c_str());
       return false;
     }
 
     bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
+    bssl::ScopedEVP_HPKE_KEY key;
     if (!keys ||
+        !EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
+                           ech_key.data(), ech_key.size()) ||
         !SSL_ECH_KEYS_add(keys.get(),
-                          /*is_retry_config=*/1, echconfig.data(),
-                          echconfig.size(), echconfig_key.data(),
-                          echconfig_key.size()) ||
+                          /*is_retry_config=*/1, ech_config.data(),
+                          ech_config.size(), key.get()) ||
         !SSL_CTX_set1_ech_keys(ctx.get(), keys.get())) {
       fprintf(stderr, "Error setting server's ECHConfig and private key\n");
       return false;