Implement PSK variants of HPKE setup functions.

Change-Id: Ic190ac1efbb079e42bb22b39083ccb96e0a61e57
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/42664
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/err/evp.errordata b/crypto/err/evp.errordata
index 390dec0..d0f09d4 100644
--- a/crypto/err/evp.errordata
+++ b/crypto/err/evp.errordata
@@ -3,6 +3,7 @@
 EVP,102,DECODE_ERROR
 EVP,103,DIFFERENT_KEY_TYPES
 EVP,104,DIFFERENT_PARAMETERS
+EVP,136,EMPTY_PSK
 EVP,105,ENCODE_ERROR
 EVP,106,EXPECTING_AN_EC_KEY_KEY
 EVP,107,EXPECTING_AN_RSA_KEY
diff --git a/crypto/evp/evp.c b/crypto/evp/evp.c
index 60fdf64..653d657 100644
--- a/crypto/evp/evp.c
+++ b/crypto/evp/evp.c
@@ -76,6 +76,10 @@
 // TODO(davidben): Fix Node to not touch the error queue itself and remove this.
 OPENSSL_DECLARE_ERROR_REASON(EVP, NOT_XOF_OR_INVALID_LENGTH)
 
+// The HPKE module uses the EVP error namespace, but it lives in another
+// directory.
+OPENSSL_DECLARE_ERROR_REASON(EVP, EMPTY_PSK)
+
 EVP_PKEY *EVP_PKEY_new(void) {
   EVP_PKEY *ret;
 
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index cacc19e..2e0a581 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -38,6 +38,7 @@
 #define HPKE_SUITE_ID_LEN 10
 
 #define HPKE_MODE_BASE 0
+#define HPKE_MODE_PSK 1
 
 static const char kHpkeRfcId[] = "HPKE-05 ";
 
@@ -115,7 +116,7 @@
                             X25519_PUBLIC_VALUE_LEN)) {
     return 0;
   }
-  const char kPRKExpandLabel[] = "shared_secret";
+  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)) {
@@ -150,9 +151,28 @@
   return NULL;
 }
 
-static int hpke_key_schedule(EVP_HPKE_CTX *hpke, const uint8_t *shared_secret,
+static int hpke_key_schedule(EVP_HPKE_CTX *hpke, uint8_t mode,
+                             const uint8_t *shared_secret,
                              size_t shared_secret_len, const uint8_t *info,
-                             size_t info_len) {
+                             size_t info_len, const uint8_t *psk,
+                             size_t psk_len, const uint8_t *psk_id,
+                             size_t psk_id_len) {
+  // Verify the PSK inputs.
+  switch (mode) {
+    case HPKE_MODE_BASE:
+      // This is an internal error, unreachable from the caller.
+      assert(psk_len == 0 && psk_id_len == 0);
+      break;
+    case HPKE_MODE_PSK:
+      if (psk_len == 0 || psk_id_len == 0) {
+        OPENSSL_PUT_ERROR(EVP, EVP_R_EMPTY_PSK);
+        return 0;
+      }
+      break;
+    default:
+      return 0;
+  }
+
   // Attempt to get an EVP_AEAD*.
   const EVP_AEAD *aead = hpke_get_aead(hpke->aead_id);
   if (aead == NULL) {
@@ -170,7 +190,7 @@
   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)) {
+                            psk_id, psk_id_len)) {
     return 0;
   }
 
@@ -189,7 +209,7 @@
   size_t context_len;
   CBB context_cbb;
   if (!CBB_init_fixed(&context_cbb, context, sizeof(context)) ||
-      !CBB_add_u8(&context_cbb, HPKE_MODE_BASE) ||
+      !CBB_add_u8(&context_cbb, mode) ||
       !CBB_add_bytes(&context_cbb, psk_id_hash, psk_id_hash_len) ||
       !CBB_add_bytes(&context_cbb, info_hash, info_hash_len) ||
       !CBB_finish(&context_cbb, NULL, &context_len)) {
@@ -201,8 +221,8 @@
   uint8_t psk_hash[EVP_MAX_MD_SIZE];
   size_t psk_hash_len;
   if (!hpke_labeled_extract(hpke->hkdf_md, psk_hash, &psk_hash_len, NULL, 0,
-                            suite_id, sizeof(suite_id), kPskHashLabel, NULL,
-                            0)) {
+                            suite_id, sizeof(suite_id), kPskHashLabel, psk,
+                            psk_len)) {
     return 0;
   }
 
@@ -338,8 +358,9 @@
   uint8_t shared_secret[SHA256_DIGEST_LENGTH];
   if (!hpke_encap(hpke, shared_secret, peer_public_value, ephemeral_private,
                   ephemeral_public) ||
-      !hpke_key_schedule(hpke, shared_secret, sizeof(shared_secret), info,
-                         info_len)) {
+      !hpke_key_schedule(hpke, HPKE_MODE_BASE, shared_secret,
+                         sizeof(shared_secret), info, info_len, NULL, 0, NULL,
+                         0)) {
     return 0;
   }
   return 1;
@@ -360,8 +381,74 @@
   }
   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,
-                         info_len)) {
+      !hpke_key_schedule(hpke, HPKE_MODE_BASE, shared_secret,
+                         sizeof(shared_secret), info, info_len, NULL, 0, NULL,
+                         0)) {
+    return 0;
+  }
+  return 1;
+}
+
+int EVP_HPKE_CTX_setup_psk_s_x25519(
+    EVP_HPKE_CTX *hpke, uint8_t out_enc[X25519_PUBLIC_VALUE_LEN],
+    uint16_t kdf_id, uint16_t aead_id,
+    const uint8_t peer_public_value[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t *info, size_t info_len, const uint8_t *psk, size_t psk_len,
+    const uint8_t *psk_id, size_t psk_id_len) {
+  // The GenerateKeyPair() step technically belongs in the KEM's Encap()
+  // function, but we've moved it up a layer to make it easier for tests to
+  // inject an ephemeral keypair.
+  uint8_t ephemeral_private[X25519_PRIVATE_KEY_LEN];
+  X25519_keypair(out_enc, ephemeral_private);
+  return EVP_HPKE_CTX_setup_psk_s_x25519_for_test(
+      hpke, kdf_id, aead_id, peer_public_value, info, info_len, psk, psk_len,
+      psk_id, psk_id_len, ephemeral_private, out_enc);
+}
+
+int EVP_HPKE_CTX_setup_psk_s_x25519_for_test(
+    EVP_HPKE_CTX *hpke, uint16_t kdf_id, uint16_t aead_id,
+    const uint8_t peer_public_value[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t *info, size_t info_len, const uint8_t *psk, size_t psk_len,
+    const uint8_t *psk_id, size_t psk_id_len,
+    const uint8_t ephemeral_private[X25519_PRIVATE_KEY_LEN],
+    const uint8_t ephemeral_public[X25519_PUBLIC_VALUE_LEN]) {
+  hpke->is_sender = 1;
+  hpke->kdf_id = kdf_id;
+  hpke->aead_id = aead_id;
+  hpke->hkdf_md = hpke_get_kdf(kdf_id);
+  if (hpke->hkdf_md == NULL) {
+    return 0;
+  }
+  uint8_t shared_secret[SHA256_DIGEST_LENGTH];
+  if (!hpke_encap(hpke, shared_secret, peer_public_value, ephemeral_private,
+                  ephemeral_public) ||
+      !hpke_key_schedule(hpke, HPKE_MODE_PSK, shared_secret,
+                         sizeof(shared_secret), info, info_len, psk, psk_len,
+                         psk_id, psk_id_len)) {
+    return 0;
+  }
+  return 1;
+}
+
+int EVP_HPKE_CTX_setup_psk_r_x25519(
+    EVP_HPKE_CTX *hpke, uint16_t kdf_id, uint16_t aead_id,
+    const uint8_t enc[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t public_key[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t private_key[X25519_PRIVATE_KEY_LEN], const uint8_t *info,
+    size_t info_len, const uint8_t *psk, size_t psk_len, const uint8_t *psk_id,
+    size_t psk_id_len) {
+  hpke->is_sender = 0;
+  hpke->kdf_id = kdf_id;
+  hpke->aead_id = aead_id;
+  hpke->hkdf_md = hpke_get_kdf(kdf_id);
+  if (hpke->hkdf_md == NULL) {
+    return 0;
+  }
+  uint8_t shared_secret[SHA256_DIGEST_LENGTH];
+  if (!hpke_decap(hpke, shared_secret, enc, public_key, private_key) ||
+      !hpke_key_schedule(hpke, HPKE_MODE_PSK, shared_secret,
+                         sizeof(shared_secret), info, info_len, psk, psk_len,
+                         psk_id, psk_id_len)) {
     return 0;
   }
   return 1;
diff --git a/crypto/hpke/hpke_test.cc b/crypto/hpke/hpke_test.cc
index 6f942d2..49c9b06 100644
--- a/crypto/hpke/hpke_test.cc
+++ b/crypto/hpke/hpke_test.cc
@@ -13,8 +13,8 @@
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
 
 #include <cstdint>
-#include <string>
 #include <limits>
+#include <string>
 #include <vector>
 
 #include <gtest/gtest.h>
@@ -23,7 +23,9 @@
 #include <openssl/curve25519.h>
 #include <openssl/digest.h>
 #include <openssl/err.h>
+#include <openssl/evp.h>
 #include <openssl/sha.h>
+#include <openssl/span.h>
 
 #include "../test/file_test.h"
 #include "../test/test_util.h"
@@ -33,31 +35,66 @@
 namespace bssl {
 namespace {
 
-// HpkeTestVector corresponds to one array member in the published
+enum class HPKEMode {
+  kBase = 0,
+  kPSK = 1,
+};
+
+// HPKETestVector corresponds to one array member in the published
 // test-vectors.json.
-class HpkeTestVector {
+class HPKETestVector {
  public:
-  explicit HpkeTestVector() = default;
-  ~HpkeTestVector() = default;
+  explicit HPKETestVector() = default;
+  ~HPKETestVector() = default;
 
   bool ReadFromFileTest(FileTest *t);
 
   void Verify() const {
-    // Set up the sender.
     ScopedEVP_HPKE_CTX sender_ctx;
-    ASSERT_GT(secret_key_e_.size(), 0u);
-
-    ASSERT_TRUE(EVP_HPKE_CTX_setup_base_s_x25519_for_test(
-        sender_ctx.get(), kdf_id_, aead_id_, public_key_r_.data(), info_.data(),
-        info_.size(), secret_key_e_.data(), public_key_e_.data()));
-
-    // 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_, public_key_e_.data(),
-        public_key_r_.data(), secret_key_r_.data(), info_.data(),
-        info_.size()));
+    switch (mode_) {
+      case HPKEMode::kBase:
+        ASSERT_GT(secret_key_e_.size(), 0u);
+        ASSERT_EQ(psk_.size(), 0u);
+        ASSERT_EQ(psk_id_.size(), 0u);
+
+        // Set up the sender.
+        ASSERT_TRUE(EVP_HPKE_CTX_setup_base_s_x25519_for_test(
+            sender_ctx.get(), kdf_id_, aead_id_, public_key_r_.data(),
+            info_.data(), info_.size(), secret_key_e_.data(),
+            public_key_e_.data()));
+
+        // Set up the receiver.
+        ASSERT_TRUE(EVP_HPKE_CTX_setup_base_r_x25519(
+            receiver_ctx.get(), kdf_id_, aead_id_, public_key_e_.data(),
+            public_key_r_.data(), secret_key_r_.data(), info_.data(),
+            info_.size()));
+        break;
+
+      case HPKEMode::kPSK:
+        ASSERT_GT(secret_key_e_.size(), 0u);
+        ASSERT_GT(psk_.size(), 0u);
+        ASSERT_GT(psk_id_.size(), 0u);
+
+        // Set up the sender.
+        ASSERT_TRUE(EVP_HPKE_CTX_setup_psk_s_x25519_for_test(
+            sender_ctx.get(), kdf_id_, aead_id_, public_key_r_.data(),
+            info_.data(), info_.size(), psk_.data(), psk_.size(),
+            psk_id_.data(), psk_id_.size(), secret_key_e_.data(),
+            public_key_e_.data()));
+
+        // Set up the receiver.
+        ASSERT_TRUE(EVP_HPKE_CTX_setup_psk_r_x25519(
+            receiver_ctx.get(), kdf_id_, aead_id_, public_key_e_.data(),
+            public_key_r_.data(), secret_key_r_.data(), info_.data(),
+            info_.size(), psk_.data(), psk_.size(), psk_id_.data(),
+            psk_id_.size()));
+        break;
+      default:
+        FAIL() << "Unsupported mode";
+        return;
+    }
 
     VerifyEncryptions(sender_ctx.get(), receiver_ctx.get());
     VerifyExports(sender_ctx.get());
@@ -112,6 +149,7 @@
     std::vector<uint8_t> exportValue;
   };
 
+  HPKEMode mode_;
   uint16_t kdf_id_;
   uint16_t aead_id_;
   std::vector<uint8_t> context_;
@@ -122,6 +160,8 @@
   std::vector<uint8_t> secret_key_r_;
   std::vector<Encryption> encryptions_;
   std::vector<Export> exports_;
+  std::vector<uint8_t> psk_;     // Empty when mode is not PSK.
+  std::vector<uint8_t> psk_id_;  // Empty when mode is not PSK.
 };
 
 // Match FileTest's naming scheme for duplicated attribute names.
@@ -156,7 +196,13 @@
 }
 
 
-bool HpkeTestVector::ReadFromFileTest(FileTest *t) {
+bool HPKETestVector::ReadFromFileTest(FileTest *t) {
+  uint8_t mode_tmp;
+  if (!FileTestReadInt(t, &mode_tmp, "mode")) {
+    return false;
+  }
+  mode_ = static_cast<HPKEMode>(mode_tmp);
+
   if (!FileTestReadInt(t, &kdf_id_, "kdf_id") ||
       !FileTestReadInt(t, &aead_id_, "aead_id") ||
       !t->GetBytes(&info_, "info") ||
@@ -167,6 +213,13 @@
     return false;
   }
 
+  if (mode_ == HPKEMode::kPSK) {
+    if (!t->GetBytes(&psk_, "psk") ||
+        !t->GetBytes(&psk_id_, "psk_id")) {
+      return false;
+    }
+  }
+
   for (int i = 1; t->HasAttribute(BuildAttrName("aad", i)); i++) {
     Encryption encryption;
     if (!t->GetBytes(&encryption.aad, BuildAttrName("aad", i)) ||
@@ -194,7 +247,7 @@
 
 TEST(HPKETest, VerifyTestVectors) {
   FileTestGTest("crypto/hpke/hpke_test_vectors.txt", [](FileTest *t) {
-    HpkeTestVector test_vec;
+    HPKETestVector test_vec;
     EXPECT_TRUE(test_vec.ReadFromFileTest(t));
     test_vec.Verify();
   });
@@ -356,6 +409,63 @@
                                  kMockCiphertextLen, nullptr, 0));
 }
 
+// Test that the PSK variants of Setup functions fail when any of the PSK inputs
+// are empty.
+TEST(HPKETest, EmptyPSK) {
+  const uint8_t kMockEnc[X25519_PUBLIC_VALUE_LEN] = {0xff};
+  const uint8_t kMockPSK[100] = {0xff};
+  const bssl::Span<const uint8_t> kPSKValues[] = {
+      {kMockPSK, sizeof(kMockPSK)},
+      {nullptr, 0},
+  };
+
+  // Generate the receiver'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);
+
+  // Vary the PSK and PSKID inputs for the sender and receiver, trying all four
+  // permutations of empty and nonempty inputs.
+
+  for (const auto psk : kPSKValues) {
+    for (const auto psk_id : kPSKValues) {
+      const bool kExpectSuccess = psk.size() > 0 && psk_id.size() > 0;
+
+      ASSERT_EQ(ERR_get_error(), 0u);
+
+      ScopedEVP_HPKE_CTX sender_ctx;
+      uint8_t enc[X25519_PUBLIC_VALUE_LEN];
+      ASSERT_EQ(EVP_HPKE_CTX_setup_psk_s_x25519(
+                    sender_ctx.get(), enc, EVP_HPKE_HKDF_SHA256,
+                    EVP_HPKE_AEAD_AES_GCM_128, public_key_r, nullptr, 0,
+                    psk.data(), psk.size(), psk_id.data(), psk_id.size()),
+                kExpectSuccess);
+
+      if (!kExpectSuccess) {
+        uint32_t err = ERR_get_error();
+        EXPECT_EQ(ERR_LIB_EVP, ERR_GET_LIB(err));
+        EXPECT_EQ(EVP_R_EMPTY_PSK, ERR_GET_REASON(err));
+      }
+      ERR_clear_error();
+
+      ScopedEVP_HPKE_CTX receiver_ctx;
+      ASSERT_EQ(
+          EVP_HPKE_CTX_setup_psk_r_x25519(
+              receiver_ctx.get(), EVP_HPKE_HKDF_SHA256,
+              EVP_HPKE_AEAD_AES_GCM_128, kMockEnc, public_key_r, secret_key_r,
+              nullptr, 0, psk.data(), psk.size(), psk_id.data(), psk_id.size()),
+          kExpectSuccess);
+
+      if (!kExpectSuccess) {
+        uint32_t err = ERR_get_error();
+        EXPECT_EQ(ERR_LIB_EVP, ERR_GET_LIB(err));
+        EXPECT_EQ(EVP_R_EMPTY_PSK, ERR_GET_REASON(err));
+      }
+      ERR_clear_error();
+    }
+  }
+}
+
 TEST(HPKETest, InternalParseIntSafe) {
   uint8_t u8 = 0xff;
   ASSERT_FALSE(ParseIntSafe(&u8, "-1"));
diff --git a/crypto/hpke/hpke_test_vectors.txt b/crypto/hpke/hpke_test_vectors.txt
index 10669d4..9d787e9 100644
--- a/crypto/hpke/hpke_test_vectors.txt
+++ b/crypto/hpke/hpke_test_vectors.txt
@@ -1,3 +1,4 @@
+mode = 0
 kdf_id = 1
 aead_id = 1
 info = 4f6465206f6e2061204772656369616e2055726e
@@ -66,6 +67,78 @@
 exportLength = 32
 exportValue = c4823eeb3efd2d5216b2d3b16e542bf57470dc9b9ea9af6bce85b151a3589d90
 
+mode = 1
+kdf_id = 1
+aead_id = 1
+info = 4f6465206f6e2061204772656369616e2055726e
+skRm = 4b41ef269169090551fcea177ecdf622bca86d82298e21cd93119b804ccc5eab
+skEm = e7d2b539792a48a24451303ccd0cfe77176b6cb06823c439edfd217458a1398a
+pkRm = a5c85773bed3a831e7096f7df4ff5d1d8bac48fc97bfac366141efab91892a3a
+pkEm = 08d39d3e7f9b586341b6004dafba9679d2bd9340066edb247e3e919013efcd0f
+psk = 5db3b80a81cb63ca59470c83414ef70a
+psk_id = 456e6e796e20447572696e206172616e204d6f726961
+# encryptions[0]
+aad = 436f756e742d30
+ciphertext = fb68f911b4e4033d1547f646ea30c9cee987fb4b4a8c30918e5de6e96de32fc63466f2fc05e09aeff552489741
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[1]
+aad = 436f756e742d31
+ciphertext = 85e7472fbb7e2341af35fb2a0795df9a85caa99a8f584056b11d452bc160470672e297f9892ce2c5020e794ae1
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[2]
+aad = 436f756e742d32
+ciphertext = 74229b7491102bcf94cf7633888bc48baa4e5a73cc544bfad4ff61585506facb44b359ade03c0b2b35c6430e4c
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[3]
+aad = 436f756e742d33
+ciphertext = 013476197af9440a8be89a0cf7d3802eae519d5f5b39cb600e8b285e16ad90c3d903f6108946616723e9a93b73
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[4]
+aad = 436f756e742d34
+ciphertext = 5aeb09a3798d21dc2ca01f5c255624c9c8c20d75d79d19269eca7b280be0cb7851fae82b646bd5673d10368276
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[5]
+aad = 436f756e742d35
+ciphertext = e9cac48b89f3f8898a85562007854b9f61bdf2d2c3e32e9b6162e9fa2f83924d138194528946d96cf7988685a0
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[6]
+aad = 436f756e742d36
+ciphertext = 2aa76414e0cb28ba7ba0f24d800bc4fec24d51cd1f75e839233ee10610bda97f3daf46fadb53ca01762bbe8a04
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[7]
+aad = 436f756e742d37
+ciphertext = 96148b343eb53df8d528af57214e65de028461ac69f2d9e371cb0aa4d732201d693766a17fd49ec6025bc98705
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[8]
+aad = 436f756e742d38
+ciphertext = 39b7e966e0ada05d8cd8a9beb5765941baad38473f18f705443f882a207ff96bfe1c71ae386e97e2fa91960bbe
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[9]
+aad = 436f756e742d39
+ciphertext = 25bd0d0614a38b19a05dff783a1bbd003c25cade55ba0e24e234b803991cae60ba7d105d35e47519a8cf598580
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# exports[0]
+exportContext = 436f6e746578742d30
+exportLength = 32
+exportValue = bd292b132fae00243851451c3f3a87e9e11c3293c14d61b114b7e12e07245ffd
+# exports[1]
+exportContext = 436f6e746578742d31
+exportLength = 32
+exportValue = 695de26bc9336caee01cb04826f6e224f4d2108066ab17fc18f0c993dce05f24
+# exports[2]
+exportContext = 436f6e746578742d32
+exportLength = 32
+exportValue = c53f26ef1bf4f5fd5469d807c418a0e103d035c76ccdbc6afb5bc42b24968f6c
+# exports[3]
+exportContext = 436f6e746578742d33
+exportLength = 32
+exportValue = 8cea4a595dfe3de84644ca8ea7ea9401a345f0db29bb4beebc2c471afc602ec4
+# exports[4]
+exportContext = 436f6e746578742d34
+exportLength = 32
+exportValue = e6313f12f6c2054c69018f273211c54fcf2439d90173392eaa34b4caac929068
+
+mode = 0
 kdf_id = 1
 aead_id = 2
 info = 4f6465206f6e2061204772656369616e2055726e
@@ -134,6 +207,78 @@
 exportLength = 32
 exportValue = 807d14a692749503f44e54660e8ebe4e93311715b9ba7d540973b2bb3c606825
 
+mode = 1
+kdf_id = 1
+aead_id = 2
+info = 4f6465206f6e2061204772656369616e2055726e
+skRm = cb798ffb68e7b3db33913c365bf45a811fca8382522ac8815b12b26d3377a049
+skEm = bf3981d4d9b43ea83365747e25d8ba71e14d21cca47e35e70b5311e1d48f8a0d
+pkRm = 4a9269e9db60008f5c7f3c7d39bb8c4923bc2f244b3ed9085191ef54cd10cf0c
+pkEm = 8f65c9f75b4b774d36052dfac0bdd37a03f309b92c9a9ca47e903683be34d04d
+psk = 5db3b80a81cb63ca59470c83414ef70a
+psk_id = 456e6e796e20447572696e206172616e204d6f726961
+# encryptions[0]
+aad = 436f756e742d30
+ciphertext = ad2a3e08e10413e7bdf9e89f2db169338fc68bcf8dc7bb073ca779024996de5922d338cf8407d34109cd2fdccf
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[1]
+aad = 436f756e742d31
+ciphertext = d7c94516707aef83e37dc5cbe3e9668260de5954899d54e8ecab3f1cfba8556557f1ff2238f817e0eb75d3cbb7
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[2]
+aad = 436f756e742d32
+ciphertext = 371412a9a86704990e8d7170282134096fc623c74411d5ff95380692a74c438deb0e38f41bfba0562042e987a0
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[3]
+aad = 436f756e742d33
+ciphertext = 4e3486ffca6d42f064c885d169210f6fcce2b3d4981d185d4b1a5c1e82733c14f14fcb8b1f16dd1e7b707907ab
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[4]
+aad = 436f756e742d34
+ciphertext = 45f532caeed9f6c35a990812773cfd688f686288dfcb500ae04f8fac4d3704204bb051e704c422edcc3107737b
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[5]
+aad = 436f756e742d35
+ciphertext = 1c0aee5fa393a0c4e2dbd70f7ce475542c71fd402b6fb8431855ac8fbafc6801c777996f8243c53a7d96d131c8
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[6]
+aad = 436f756e742d36
+ciphertext = 66cd0eeb97fc59c1863898f9b7f1f67c82c5aede5794c17937f5e0909641af770c4973aec2a21967c0f17a64ba
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[7]
+aad = 436f756e742d37
+ciphertext = c96bb3363b31b582476239e1eb0792d2ac632ddaa7a1dc9ac7f9d588b62970016040a278e448256f5bbbf09ed7
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[8]
+aad = 436f756e742d38
+ciphertext = 71b3055db5efe1165e685d25c4a749b6fdf8cb7a59f7e3e76cfbf63c109db9387fc751cc9c36cf886dd0f79411
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[9]
+aad = 436f756e742d39
+ciphertext = 14372e32a6dee0536a11b66343eb436c099d7adf658900fa624a45d6f1a8e84297c56ec6e05b2745605dfcd99e
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# exports[0]
+exportContext = 436f6e746578742d30
+exportLength = 32
+exportValue = c52ecbb65af1c6764ce7d2fd1131d5f050ee2f943a4fe56e9c855b44385b00cf
+# exports[1]
+exportContext = 436f6e746578742d31
+exportLength = 32
+exportValue = 42c5cf4f81152d05bacc9323e805eab8e429850dd029937c2c42f17ce7fea09b
+# exports[2]
+exportContext = 436f6e746578742d32
+exportLength = 32
+exportValue = 89d7f97327d51d61a4ac04b2507e51a977c8706bd932941f5acf1f542cfd034b
+# exports[3]
+exportContext = 436f6e746578742d33
+exportLength = 32
+exportValue = 581e3a66a1ad5309c3295825bc03407c7d9e34673e61aed2c543b47764577783
+# exports[4]
+exportContext = 436f6e746578742d34
+exportLength = 32
+exportValue = 599f10537288a9ec87d53c16aaa5881715061e6152a5b51b1e0433a396b38d10
+
+mode = 0
 kdf_id = 1
 aead_id = 3
 info = 4f6465206f6e2061204772656369616e2055726e
@@ -202,6 +347,78 @@
 exportLength = 32
 exportValue = d4f8878dbc471935e86cdee08746e53837bbb4b6013003bebb0bc1cc3e074085
 
+mode = 1
+kdf_id = 1
+aead_id = 3
+info = 4f6465206f6e2061204772656369616e2055726e
+skRm = a6ab4e1bb782d580d837843089d65ebe271a0ee9b5a951777cecf1293c58c150
+skEm = 4bfdb62b95ae2a1f29f20ea49e24aa2673e0d240c6e967f668f55ed5dee996dc
+pkRm = c49b46ed73ecb7d3a6a3e44f54b8f00f9ab872b57dd79ded66d7231a14c64144
+pkEm = f4639297e3305b03d34dd5d86522ddc6ba11a608a0003670a30734823cdd3763
+psk = 5db3b80a81cb63ca59470c83414ef70a
+psk_id = 456e6e796e20447572696e206172616e204d6f726961
+# encryptions[0]
+aad = 436f756e742d30
+ciphertext = f97ca72675b8199e8ffec65b4c200d901110b177b246f241b6f9716fb60b35b32a6d452675534b591e8141468a
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[1]
+aad = 436f756e742d31
+ciphertext = 57796e2b9dd0ddf807f1a7cb5884dfc50e61468c4fd69fa03963731e51674ca88fee94eeac3290734e1627ded6
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[2]
+aad = 436f756e742d32
+ciphertext = b514150af1057151687d0036a9b4a3ad50fb186253f839d8433622baa85719ed5d2532017a0ce7b9ca0007f276
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[3]
+aad = 436f756e742d33
+ciphertext = 50a645f0f9bddac7b1029dba61921d2cdc10258e6d67e4918000eab0d617fb04a655caeeab308eb159585ae07a
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[4]
+aad = 436f756e742d34
+ciphertext = 6232e4a184dbff7361f9e4d6bfaaf97631225ee317e63cb09e8f74fc93efeedb6385d4f4cb2e30ffb82aea0e1f
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[5]
+aad = 436f756e742d35
+ciphertext = ab801465f2080c1b9a06b582a919b51fc289e1b5b14bbad0b09cd92a82d27a1de1b934fd809cde8f19ef988373
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[6]
+aad = 436f756e742d36
+ciphertext = 83248649e62ac67c3b9d5525b886c04960b00b02df2d34c91284e8ed537feba132b03d12b868822af1e583118d
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[7]
+aad = 436f756e742d37
+ciphertext = 5ad03248b8e5270a654b090df5eb8955120d5cdc00f5dfb004942125cec1fbcbaef7d9fdef284bddc134018b74
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[8]
+aad = 436f756e742d38
+ciphertext = 56333a4ee1e5512cf2ffa1fd135fa54ba666f4388cf654fda9d7696ccfca1c51facda5a9bf80c9ac789026955a
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[9]
+aad = 436f756e742d39
+ciphertext = e352a356575dcee382c8d2489bc45dc3a757979638e952dbac969eb092e9c616d8654e9dec8d1c0777e39478c3
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# exports[0]
+exportContext = 436f6e746578742d30
+exportLength = 32
+exportValue = 735400cd9b9193daffe840f412074728ade6b1978e9ae27957aacd588dbd7c9e
+# exports[1]
+exportContext = 436f6e746578742d31
+exportLength = 32
+exportValue = cf4e351e1943d171ff2d88726f18160086ecbec52a8151dba8cf5ba0737a6097
+# exports[2]
+exportContext = 436f6e746578742d32
+exportLength = 32
+exportValue = 8e23b44d4f23dd906d1c100580a670d171132c9786212c4ca2876a1541a84fae
+# exports[3]
+exportContext = 436f6e746578742d33
+exportLength = 32
+exportValue = 56252a940ece53d4013eb619b444ee1d019a08eec427ded2b6dbf24be624a4a0
+# exports[4]
+exportContext = 436f6e746578742d34
+exportLength = 32
+exportValue = fc6cdca9ce8ab062401478ffd16ee1c07e2b15d7c781d4227f07c6043d937fad
+
+mode = 0
 kdf_id = 3
 aead_id = 1
 info = 4f6465206f6e2061204772656369616e2055726e
@@ -270,6 +487,78 @@
 exportLength = 32
 exportValue = 1538807322f02dbded405d10de3aafc4f6d365d9aefbef081d114dcedbe1cae2
 
+mode = 1
+kdf_id = 3
+aead_id = 1
+info = 4f6465206f6e2061204772656369616e2055726e
+skRm = ac1f54ecf81f53a71c5be7f734346fffe084ba6f5966d2b3173df819145bd722
+skEm = 20c9020273ac6193f27fb69af406cdab8154090c28aa2c7b870b92513d8805f4
+pkRm = 7e8561e6b753c6992df8d89cc1e447bebd4e21fcd4d1dc868f6b2ea663ce7e18
+pkEm = 7c35810fdc4009af7cb98dca0838ad32495c71c3003be650ed1ea0f6cd1ecc3c
+psk = 5db3b80a81cb63ca59470c83414ef70a
+psk_id = 456e6e796e20447572696e206172616e204d6f726961
+# encryptions[0]
+aad = 436f756e742d30
+ciphertext = 13d119d97709546435a5fbc99a39d9ac3f2ef459c9841305582282c435aab714af1df2f52bd07f196bfe9294f5
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[1]
+aad = 436f756e742d31
+ciphertext = e8f9743ee3eeb41cebade4ba2f0d758b679c560a53a720aeb88017bbd778537b02b3eca27bca61fa13898847ec
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[2]
+aad = 436f756e742d32
+ciphertext = 7f0218fe141ffccc6229d096516a27cce109e1300f59a3500288cc1bb57c765b91b4a240075493d94abb9e4cf9
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[3]
+aad = 436f756e742d33
+ciphertext = b893adce352a2b1c780f67475062a9eb827ba2fb2e7ec4ae21e27d6663e1a988d81390b50d99b4de2b451afce1
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[4]
+aad = 436f756e742d34
+ciphertext = 8e733ddc1dd2b80ad6decf1b0ce08a28e9f5e644a7439621cd028ac9599c7657dac34173f7d5489e34be099462
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[5]
+aad = 436f756e742d35
+ciphertext = 4551173fc2233def235d52c738dc5095bc7abb6e3c3c7f8e58ae983bf68cb5b6fdfdf334b9bef4e1c5b11c2884
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[6]
+aad = 436f756e742d36
+ciphertext = 3384441833b623b36930d89a3e795fcc31e703d460ec48dc43be345794a2d73af9f4283494f23b904ba609197d
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[7]
+aad = 436f756e742d37
+ciphertext = ad5a5977bd3486ec319850a52fb8e7cba74d0e2c2ee261ec9a6b2bfb579f7a55f8bac5ab774c2bc28d86623ff7
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[8]
+aad = 436f756e742d38
+ciphertext = 8d4466e0d99086ee1c0171a111a2c1ea5736ba9324eb20aa0d071dcd8c4581ea1dda96e25dd3f341c84c9c3529
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[9]
+aad = 436f756e742d39
+ciphertext = adf8ca449b303657e9d81bf9566d7562d9b28d9c63758bfa93586f5ba69a2fe2dca1dafd022831de39b6453c28
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# exports[0]
+exportContext = 436f6e746578742d30
+exportLength = 32
+exportValue = 899f63d5646d1116e63029c0c03a3a8b63b815af58a0e197c440e8075daa220d
+# exports[1]
+exportContext = 436f6e746578742d31
+exportLength = 32
+exportValue = a2da1ff2773723418e07c63d39455d70be0865d6d0fb29e355eda599a62441da
+# exports[2]
+exportContext = 436f6e746578742d32
+exportLength = 32
+exportValue = 1c361d5b9b14c6f75660c8c960b908394c0281895fbba9288730822511a24171
+# exports[3]
+exportContext = 436f6e746578742d33
+exportLength = 32
+exportValue = b5258b7e39ea3a68177b50a5a492b6cb8083707a1756e5a7d5d4370556c856de
+# exports[4]
+exportContext = 436f6e746578742d34
+exportLength = 32
+exportValue = 81ddfe54c7894f987e2945333a5ec809068890587759b6407feb1d6f1ca26e6e
+
+mode = 0
 kdf_id = 3
 aead_id = 2
 info = 4f6465206f6e2061204772656369616e2055726e
@@ -338,6 +627,78 @@
 exportLength = 32
 exportValue = 5c49cd3ebcb956aeb7e41c9a0f2d4b4c9501f66cc544d8d7fa62ce96d2938857
 
+mode = 1
+kdf_id = 3
+aead_id = 2
+info = 4f6465206f6e2061204772656369616e2055726e
+skRm = 939a0b48510924ccf6449c0eaaa1069bb41ab9cf54d090e6c6eb9aaab6051d69
+skEm = d3668380f7053086047e1f8a66e0e32c45c1086002b6a67dc9a942058a655073
+pkRm = 4af18720a608d13ce17268aa1721876e96ec4173a40788ddfd60c886b4c08605
+pkEm = 2ed53926385da5d41c76e75a636cd795fea5200d0e7ef6affd4b741a1196be37
+psk = 5db3b80a81cb63ca59470c83414ef70a
+psk_id = 456e6e796e20447572696e206172616e204d6f726961
+# encryptions[0]
+aad = 436f756e742d30
+ciphertext = b5039652103f36bdcf1380b947ed8b6dc3413b98cff2c6451aff5fabee7234ace274918eb665f6d08850a70093
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[1]
+aad = 436f756e742d31
+ciphertext = f1a1beb03a0c2d3f756d992cba6712247e24561e8407aef299285cefc337beaf249b8b92325a6718feffb1cfad
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[2]
+aad = 436f756e742d32
+ciphertext = 0f5f58f918a19f55efc146bf3ecd5c72bb0c00893c02ca56cf291904e1e1f8f6d0768f1cfed9d64d3f7f35d912
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[3]
+aad = 436f756e742d33
+ciphertext = 14177f71f6716d128baaff214e360bb9b8dc0ddb34ba7bcb8d0dcb01c548d42d3c43875ec8ff083acba591ca24
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[4]
+aad = 436f756e742d34
+ciphertext = f8ee21f3af210dacb7aea148b444a98e643f161040db1350429c366a667bbc4b0cb6dbf047c2ba9f86a8ecf425
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[5]
+aad = 436f756e742d35
+ciphertext = 9f5ffc686ca660de6101a9dbf412c61de6fc574954f72f7d2c652c0ab61e8597170ec713b5314e41f0601fbf49
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[6]
+aad = 436f756e742d36
+ciphertext = 3f415ff7bc48fbdb69babce3045049ae2bc2a6983ddaf08cc9b3368362f7918bb73a4872c37fed3e3d808a9c1e
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[7]
+aad = 436f756e742d37
+ciphertext = bd2199064b596d3ef9a77fa8a5db93f1510bf996e320da92635e435c4b59120702c344825fc012393dcbddc05e
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[8]
+aad = 436f756e742d38
+ciphertext = 789faf060328cb1d359a7a30a3ce9d303612ef9c9da56c78fee82334953d425bb1613e757b2d7ab1aafd5be927
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[9]
+aad = 436f756e742d39
+ciphertext = ac41c4a676448ef3f39a471f588d8576bb30817055ce8ac4404b61a4fdbb71adbababd15117cec7371aac83b0e
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# exports[0]
+exportContext = 436f6e746578742d30
+exportLength = 32
+exportValue = e98921e4c437992d7ac035d74dfae232ef41227e2f27e4e4d801b4bd8934a5b6
+# exports[1]
+exportContext = 436f6e746578742d31
+exportLength = 32
+exportValue = 1269029586ff64d8bde114359411f61c22211e8725a2b86ea145b8b9151c3915
+# exports[2]
+exportContext = 436f6e746578742d32
+exportLength = 32
+exportValue = b8f393d7cea5934e16f4de8ef9a8912104b741ecd464d4c84886f7eef28cc301
+# exports[3]
+exportContext = 436f6e746578742d33
+exportLength = 32
+exportValue = eb5c8739d172a30b48235316e13a12fa8abd05d72cc5c330fd3ab3de3371ddc4
+# exports[4]
+exportContext = 436f6e746578742d34
+exportLength = 32
+exportValue = 0b15c1a0dccac801a3078a8a01145c940ade7d9993111bf614ac69d073913a6f
+
+mode = 0
 kdf_id = 3
 aead_id = 3
 info = 4f6465206f6e2061204772656369616e2055726e
@@ -405,3 +766,74 @@
 exportContext = 436f6e746578742d34
 exportLength = 32
 exportValue = 1474f8e9dac70e76041b4bb365f1fc4e1e2509f43c188b5d15b8d89113c197f7
+
+mode = 1
+kdf_id = 3
+aead_id = 3
+info = 4f6465206f6e2061204772656369616e2055726e
+skRm = c9221f98c17117ab4216e062481934ad21a12c8ba833a0611ca1910fac031563
+skEm = 9e38eaa97787575e564949bd84845965e910ae90a17bf841f68a4c791385afd3
+pkRm = 4ca2dcf3f5fa2b8b782536f695b3279f03569857a88e90075d5e4a67e4c4a44d
+pkEm = 886c15a9d1c3609a3f3f6be3b5d1b60817f6a557e2b7344456dbc8ae49f5e93c
+psk = 5db3b80a81cb63ca59470c83414ef70a
+psk_id = 456e6e796e20447572696e206172616e204d6f726961
+# encryptions[0]
+aad = 436f756e742d30
+ciphertext = 2f3eba789b7706b85fd2fcadf189f640a726160a7a0ef22b6483aaf69210c02d4c22113740235783dbd70ca1b7
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[1]
+aad = 436f756e742d31
+ciphertext = ea76c297d6440fcc6e753980678adf65389171c591cd2ce0cc6de56c4560e6642ca5df5d910b3006b653372657
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[2]
+aad = 436f756e742d32
+ciphertext = 7bed7b81628f1892518ced3b292cdf8e17dd0fa270600177a5c122204bfc381bc01f9bd5de77c4a568055ad19c
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[3]
+aad = 436f756e742d33
+ciphertext = 38ad518591bbb6bbbe114e08e76a90b0040ef5571dbeb9a4ca778b72eba6e729c75efaf476e1dc69d2a95ba304
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[4]
+aad = 436f756e742d34
+ciphertext = a38f5cf41d1ad3802ec4043067e4ff1aff3c557d1961cb76f6fbecea500f45ce780c27cc2894057093298d782e
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[5]
+aad = 436f756e742d35
+ciphertext = 0844adf506c653ff4cb9eaa595b4a6382055d53bfe4490a323d3ed0369b33a206f6fbed3a2c2409a646edcce70
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[6]
+aad = 436f756e742d36
+ciphertext = 747700aa0de178a38e2ac8ee358e0d79d2b6026a8e86e215312d359933da08bcbb443e9e2280932e2e37ca0c7d
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[7]
+aad = 436f756e742d37
+ciphertext = 31b7a490d05bd9e31b6ddc82b2eef9d6500675273e5c215c2f3fd4a28f04b73c752a7b7f89c2220cc5d26d3ef4
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[8]
+aad = 436f756e742d38
+ciphertext = b57d6cb945a21064c1dd0a293ae28d315e7160764e20d48f50ba9be6d3d530cd064a851f7f9fd16f61fd8afef0
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# encryptions[9]
+aad = 436f756e742d39
+ciphertext = 1bb75c2eb81165d8c95d893c9dc1425d2de1628696c488d6ff0213674a6bdba09a3ad6ef506ca339d64f02e46e
+plaintext = 4265617574792069732074727574682c20747275746820626561757479
+# exports[0]
+exportContext = 436f6e746578742d30
+exportLength = 32
+exportValue = bf8d87036ace4835ee917a504016601ce7b5add0b8d72aa64ea493ea241786f4
+# exports[1]
+exportContext = 436f6e746578742d31
+exportLength = 32
+exportValue = 9583f302506aabded442f4a3c4cb976682fed99c4bcbdf5ad41c2f00d3fbc27a
+# exports[2]
+exportContext = 436f6e746578742d32
+exportLength = 32
+exportValue = fa169c1e9ca8582381a72b5067da174995500198484520d60d22bf9bb4c34ead
+# exports[3]
+exportContext = 436f6e746578742d33
+exportLength = 32
+exportValue = 56fbba55aef923240daf63ebd30508e891cdffbe0bfe03306727320d6dfa29e6
+# exports[4]
+exportContext = 436f6e746578742d34
+exportLength = 32
+exportValue = eb30f91427038c6e3716310c0ffc431ca9ea2b10c2f264f7aee82f86f817b8e2
diff --git a/crypto/hpke/internal.h b/crypto/hpke/internal.h
index 79f65aa..87c049a 100644
--- a/crypto/hpke/internal.h
+++ b/crypto/hpke/internal.h
@@ -27,9 +27,10 @@
 // Hybrid Public Key Encryption.
 //
 // Hybrid Public Key Encryption (HPKE) enables a sender to encrypt messages to a
-// receiver with a public key.
+// receiver with a public key. Optionally, the sender may authenticate its
+// possession of a pre-shared key to the recipient.
 //
-// See https://tools.ietf.org/html/draft-irtf-cfrg-hpke-04.
+// See https://tools.ietf.org/html/draft-irtf-cfrg-hpke-05.
 
 // EVP_HPKE_AEAD_* are AEAD identifiers.
 #define EVP_HPKE_AEAD_AES_GCM_128 0x0001
@@ -78,13 +79,11 @@
 // 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."
-//
-// See https://www.ietf.org/id/draft-irtf-cfrg-hpke-04.html#section-5.1.1.
 
 // 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
 // recipient's public key). It returns one on success, and zero otherwise. Note
-// that this function may fail if |peer_public_value| is invalid.
+// that this function will fail if |peer_public_value| is invalid.
 //
 // This function writes the encapsulated shared secret to |out_enc|.
 OPENSSL_EXPORT int EVP_HPKE_CTX_setup_base_s_x25519(
@@ -106,7 +105,7 @@
 // EVP_HPKE_CTX_setup_base_r_x25519 sets up |hpke| as a recipient context that
 // can decrypt messages. |private_key| is the recipient's private key, and |enc|
 // is the encapsulated shared secret from the sender. Note that this function
-// may fail if |enc| is invalid.
+// will fail if |enc| is invalid.
 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[X25519_PUBLIC_VALUE_LEN],
@@ -114,6 +113,52 @@
     const uint8_t private_key[X25519_PRIVATE_KEY_LEN], const uint8_t *info,
     size_t info_len);
 
+// EVP_HPKE_CTX_setup_psk_s_x25519 sets up |hpke| as a sender context that can
+// encrypt for the private key corresponding to |peer_public_value| (the
+// recipient's public key) and authenticate its possession of a PSK. It returns
+// one on success, and zero otherwise. Note that this function will fail if
+// |peer_public_value| is invalid.
+//
+// The PSK and its ID must be provided in |psk| and |psk_id|, respectively. Both
+// must be nonempty (|psk_len| and |psk_id_len| must be non-zero), or this
+// function will fail.
+//
+// This function writes the encapsulated shared secret to |out_enc|.
+OPENSSL_EXPORT int EVP_HPKE_CTX_setup_psk_s_x25519(
+    EVP_HPKE_CTX *hpke, uint8_t out_enc[X25519_PUBLIC_VALUE_LEN],
+    uint16_t kdf_id, uint16_t aead_id,
+    const uint8_t peer_public_value[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t *info, size_t info_len, const uint8_t *psk, size_t psk_len,
+    const uint8_t *psk_id, size_t psk_id_len);
+
+// EVP_HPKE_CTX_setup_psk_s_x25519_for_test behaves like
+// |EVP_HPKE_CTX_setup_psk_s_x25519|, but takes a pre-generated ephemeral sender
+// key.
+OPENSSL_EXPORT int EVP_HPKE_CTX_setup_psk_s_x25519_for_test(
+    EVP_HPKE_CTX *hpke, uint16_t kdf_id, uint16_t aead_id,
+    const uint8_t peer_public_value[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t *info, size_t info_len, const uint8_t *psk, size_t psk_len,
+    const uint8_t *psk_id, size_t psk_id_len,
+    const uint8_t ephemeral_private[X25519_PRIVATE_KEY_LEN],
+    const uint8_t ephemeral_public[X25519_PUBLIC_VALUE_LEN]);
+
+// EVP_HPKE_CTX_setup_psk_r_x25519 sets up |hpke| as a recipient context that
+// can decrypt messages. Future open (decrypt) operations will fail if the
+// sender does not possess the PSK indicated by |psk| and |psk_id|.
+// |private_key| is the recipient's private key, and |enc| is the encapsulated
+// shared secret from the sender. If |enc| is invalid, this function will fail.
+//
+// The PSK and its ID must be provided in |psk| and |psk_id|, respectively. Both
+// must be nonempty (|psk_len| and |psk_id_len| must be non-zero), or this
+// function will fail.
+OPENSSL_EXPORT int EVP_HPKE_CTX_setup_psk_r_x25519(
+    EVP_HPKE_CTX *hpke, uint16_t kdf_id, uint16_t aead_id,
+    const uint8_t enc[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t public_key[X25519_PUBLIC_VALUE_LEN],
+    const uint8_t private_key[X25519_PRIVATE_KEY_LEN], const uint8_t *info,
+    size_t info_len, const uint8_t *psk, size_t psk_len, const uint8_t *psk_id,
+    size_t psk_id_len);
+
 
 // Using an HPKE context.
 
diff --git a/crypto/hpke/translate_test_vectors.py b/crypto/hpke/translate_test_vectors.py
index 6d73228..d53787a 100755
--- a/crypto/hpke/translate_test_vectors.py
+++ b/crypto/hpke/translate_test_vectors.py
@@ -28,6 +28,7 @@
 import sys
 
 HPKE_MODE_BASE = 0
+HPKE_MODE_PSK = 1
 HPKE_DHKEM_X25519_SHA256 = 0x0020
 
 
@@ -46,11 +47,17 @@
   lines = []
   for test in test_vecs:
     # Filter out test cases that we don't use.
-    if (test["mode"] != HPKE_MODE_BASE or
+    if (test["mode"] not in [HPKE_MODE_BASE, HPKE_MODE_PSK] or
         test["kem_id"] != HPKE_DHKEM_X25519_SHA256):
       continue
 
-    for key in ("kdf_id", "aead_id", "info", "skRm", "skEm", "pkRm", "pkEm"):
+    keys = ["mode", "kdf_id", "aead_id", "info", "skRm", "skEm", "pkRm", "pkEm"]
+
+    if test["mode"] == HPKE_MODE_PSK:
+      keys.append("psk")
+      keys.append("psk_id")
+
+    for key in keys:
       lines.append("{} = {}".format(key, str(test[key])))
 
     for i, enc in enumerate(test["encryptions"]):
diff --git a/include/openssl/evp.h b/include/openssl/evp.h
index e647cdf..da114d4 100644
--- a/include/openssl/evp.h
+++ b/include/openssl/evp.h
@@ -1117,5 +1117,6 @@
 #define EVP_R_INVALID_PARAMETERS 133
 #define EVP_R_INVALID_PEER_KEY 134
 #define EVP_R_NOT_XOF_OR_INVALID_LENGTH 135
+#define EVP_R_EMPTY_PSK 136
 
 #endif  // OPENSSL_HEADER_EVP_H