From eaaa456cf54b6cb1430e09688de53c11572b086b Mon Sep 17 00:00:00 2001 From: Will Childs-Klein Date: Thu, 8 Oct 2026 22:48:15 +0000 Subject: [PATCH 1/4] Hold HPKE key and context buffers out of line EVP_HPKE_KEY and EVP_HPKE_CTX are public, stack-allocatable types, so their sizes are ABI. Adding the ML-KEM KEMs grew EVP_HPKE_KEY from 72 to 4808 bytes, which overflows the object a caller built against an earlier header reserves. Sizing every buffer per algorithm and holding it behind a pointer makes both sizes independent of the algorithms on offer. --- crypto/hpke/hpke.c | 204 +++++++++++++++++++++++++++++++-------- crypto/hpke/hpke_test.cc | 108 +++++++++++++++++++++ include/openssl/hpke.h | 40 ++++---- 3 files changed, 292 insertions(+), 60 deletions(-) diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c index a4ae56861aa..123de574c67 100644 --- a/crypto/hpke/hpke.c +++ b/crypto/hpke/hpke.c @@ -24,6 +24,16 @@ // This file implements RFC 9180 and draft-ietf-hpke-pq-05. +// Callers stack-allocate |EVP_HPKE_KEY| and |EVP_HPKE_CTX|, so both sizes are +// ABI: a caller built against one release and run against a later one reserves +// the older size and overflows it. Each holds pointers and scalars only, so +// these bounds fail the build if an algorithm-dependent buffer moves back +// inline. They count pointers so that they hold on ILP32 as well. +OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_KEY) <= 8 * sizeof(void *), + evp_hpke_key_must_not_hold_inline_buffers) +OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_CTX) <= 16 * sizeof(void *), + evp_hpke_ctx_must_not_hold_inline_buffers) + // MAX_SEED_LEN is the largest |seed_len| of any KEM and MAX_SHARED_SECRET_LEN // the largest Nsecret. Both are 32 for every KEM this file implements: X25519 // seeds an ephemeral private key with 32 bytes and ML-KEM takes 32 bytes of @@ -35,6 +45,10 @@ struct evp_hpke_kem_st { uint16_t id; size_t public_key_len; size_t private_key_len; + // expanded_private_key_len is the length of the KEM's internal private key + // representation, or zero for KEMs which use the serialized private key + // directly. + size_t expanded_private_key_len; size_t seed_len; size_t enc_len; int (*init_key)(EVP_HPKE_KEY *key, const uint8_t *priv_key, @@ -340,6 +354,7 @@ const EVP_HPKE_KEM *EVP_hpke_x25519_hkdf_sha256(void) { /*id=*/EVP_HPKE_DHKEM_X25519_HKDF_SHA256, /*public_key_len=*/X25519_PUBLIC_VALUE_LEN, /*private_key_len=*/X25519_PRIVATE_KEY_LEN, + /*expanded_private_key_len=*/0, /*seed_len=*/X25519_PRIVATE_KEY_LEN, /*enc_len=*/X25519_PUBLIC_VALUE_LEN, x25519_init_key, @@ -383,9 +398,6 @@ OPENSSL_STATIC_ASSERT(MLKEM_SEED_LEN == MLKEM512_KEYGEN_SEED_LEN && ml_kem_keygen_seed_is_not_64_bytes) OPENSSL_STATIC_ASSERT(MLKEM_SEED_LEN <= EVP_HPKE_MAX_PRIVATE_KEY_LENGTH, evp_hpke_key_private_key_too_small_for_ml_kem) -OPENSSL_STATIC_ASSERT(MLKEM1024_SECRET_KEY_BYTES <= - EVP_HPKE_MAX_EXPANDED_PRIVATE_KEY_LENGTH, - evp_hpke_key_expanded_private_key_too_small) OPENSSL_STATIC_ASSERT(MLKEM1024_PUBLIC_KEY_BYTES <= EVP_HPKE_MAX_PUBLIC_KEY_LENGTH, evp_hpke_key_public_key_too_small_for_ml_kem) @@ -592,6 +604,7 @@ const EVP_HPKE_KEM *EVP_hpke_mlkem512(void) { /*id=*/EVP_HPKE_MLKEM512, /*public_key_len=*/MLKEM512_PUBLIC_KEY_BYTES, /*private_key_len=*/MLKEM_SEED_LEN, + /*expanded_private_key_len=*/MLKEM512_SECRET_KEY_BYTES, /*seed_len=*/MLKEM512_ENCAPS_SEED_LEN, /*enc_len=*/MLKEM512_CIPHERTEXT_BYTES, mlkem512_init_key, @@ -637,6 +650,7 @@ const EVP_HPKE_KEM *EVP_hpke_mlkem768(void) { /*id=*/EVP_HPKE_MLKEM768, /*public_key_len=*/MLKEM768_PUBLIC_KEY_BYTES, /*private_key_len=*/MLKEM_SEED_LEN, + /*expanded_private_key_len=*/MLKEM768_SECRET_KEY_BYTES, /*seed_len=*/MLKEM768_ENCAPS_SEED_LEN, /*enc_len=*/MLKEM768_CIPHERTEXT_BYTES, mlkem768_init_key, @@ -682,6 +696,7 @@ const EVP_HPKE_KEM *EVP_hpke_mlkem1024(void) { /*id=*/EVP_HPKE_MLKEM1024, /*public_key_len=*/MLKEM1024_PUBLIC_KEY_BYTES, /*private_key_len=*/MLKEM_SEED_LEN, + /*expanded_private_key_len=*/MLKEM1024_SECRET_KEY_BYTES, /*seed_len=*/MLKEM1024_ENCAPS_SEED_LEN, /*enc_len=*/MLKEM1024_CIPHERTEXT_BYTES, mlkem1024_init_key, @@ -706,6 +721,45 @@ size_t EVP_HPKE_KEM_private_key_len(const EVP_HPKE_KEM *kem) { size_t EVP_HPKE_KEM_enc_len(const EVP_HPKE_KEM *kem) { return kem->enc_len; } +// hpke_key_buffer_len returns the size of the single allocation backing a key +// for |kem|, which is partitioned by |hpke_key_alloc|. +static size_t hpke_key_buffer_len(const EVP_HPKE_KEM *kem) { + return kem->private_key_len + kem->public_key_len + + kem->expanded_private_key_len; +} + +// hpke_key_alloc points |key|'s buffers into one allocation sized for +// |key->kem|, which must already be set. It returns one on success and zero on +// allocation failure, after which the buffers are left NULL. +static int hpke_key_alloc(EVP_HPKE_KEY *key) { + const EVP_HPKE_KEM *kem = key->kem; + uint8_t *buf = OPENSSL_zalloc(hpke_key_buffer_len(kem)); + if (buf == NULL) { + return 0; + } + // |private_key| owns the allocation, so it comes first and is what + // |hpke_key_free| frees. + key->private_key = buf; + key->public_key = buf + kem->private_key_len; + key->expanded_private_key = + kem->expanded_private_key_len == 0 + ? NULL + : buf + kem->private_key_len + kem->public_key_len; + return 1; +} + +// hpke_key_free cleanses and releases |key|'s buffers, leaving the pointers +// dangling for the caller to clear. +static void hpke_key_free(EVP_HPKE_KEY *key) { + if (key->private_key == NULL) { + return; + } + // Cleanse with a barrier, which |OPENSSL_free| does not guarantee for the + // whole buffer and which a plain memset does not guarantee at all. + OPENSSL_cleanse(key->private_key, hpke_key_buffer_len(key->kem)); + OPENSSL_free(key->private_key); +} + void EVP_HPKE_KEY_zero(EVP_HPKE_KEY *key) { OPENSSL_memset(key, 0, sizeof(EVP_HPKE_KEY)); } @@ -714,18 +768,15 @@ void EVP_HPKE_KEY_cleanup(EVP_HPKE_KEY *key) { if (key == NULL) { return; } - // Cleanse the private key with a barrier, which |EVP_HPKE_KEY_zero|'s plain - // memset does not guarantee on its own, then return the whole struct to the - // zero state so that the result is indistinguishable from a key which has - // only ever been passed to |EVP_HPKE_KEY_zero|. + // Return the struct to the zero state so that the result is indistinguishable + // from a key which has only ever been passed to |EVP_HPKE_KEY_zero|. // // Clearing |kem| in particular means that using |key| after cleanup fails, // rather than silently operating on whatever key the all-zero private key // describes: every 64-byte string is a valid ML-KEM seed, and a zero X25519 // scalar is clamped to a valid one, so a cleansed key would otherwise still // "work". - OPENSSL_cleanse(key->private_key, sizeof(key->private_key)); - OPENSSL_cleanse(key->expanded_private_key, sizeof(key->expanded_private_key)); + hpke_key_free(key); EVP_HPKE_KEY_zero(key); } @@ -751,13 +802,18 @@ int EVP_HPKE_KEY_copy(EVP_HPKE_KEY *dst, const EVP_HPKE_KEY *src) { // undefined. There is also nothing to do. return 1; } - // The copy below already overwrites every byte of |dst|, including any key - // material it held, so this cleanse is not load-bearing today. It is here so - // that a future field which the copy does not cover cannot silently leave a - // private key behind, and to match |EVP_HPKE_KEY_move|. EVP_HPKE_KEY_cleanup(dst); - // For now, |EVP_HPKE_KEY| is trivially copyable. - OPENSSL_memcpy(dst, src, sizeof(EVP_HPKE_KEY)); + if (src->kem == NULL) { + // |src| is in the zero state, which |dst| now matches. + return 1; + } + dst->kem = src->kem; + if (!hpke_key_alloc(dst)) { + EVP_HPKE_KEY_cleanup(dst); + return 0; + } + OPENSSL_memcpy(dst->private_key, src->private_key, + hpke_key_buffer_len(src->kem)); return 1; } @@ -768,24 +824,21 @@ void EVP_HPKE_KEY_move(EVP_HPKE_KEY *out, EVP_HPKE_KEY *in) { return; } EVP_HPKE_KEY_cleanup(out); - // For now, |EVP_HPKE_KEY| is trivially movable. OPENSSL_memcpy(out, in, sizeof(EVP_HPKE_KEY)); - // Cleanse rather than zero |in|: it still holds the private key and, for - // ML-KEM, the expanded private key, and |EVP_HPKE_KEY_zero| alone is a plain - // memset. |EVP_HPKE_KEY_cleanup| leaves |in| in the zero state as well. - EVP_HPKE_KEY_cleanup(in); + // |out| now owns the buffers, so |in| is only cleared. Cleansing it here + // would erase and free key material |out| still points at. + EVP_HPKE_KEY_zero(in); } static int hpke_key_init(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem, const uint8_t *priv_key, size_t priv_key_len) { - // |key| may already hold key material, so cleanse it before reusing the - // struct. |EVP_HPKE_KEY_zero| alone would be a plain memset, which is not a - // guaranteed erase, and |EVP_HPKE_KEY_cleanup| leaves |key| in the zero state - // anyway. It only writes to |key|, so it is also safe on an uninitialized one - // and imposes no precondition on callers. + // |key| may already own buffers, so release them before reusing the struct. + // |EVP_HPKE_KEY_zero| would drop the pointers and leak. Callers reach this + // from the zero state or from an initialized key, which is what + // |EVP_HPKE_KEY_cleanup| requires. EVP_HPKE_KEY_cleanup(key); key->kem = kem; - if (!kem->init_key(key, priv_key, priv_key_len)) { + if (!hpke_key_alloc(key) || !kem->init_key(key, priv_key, priv_key_len)) { // |init_key| may have failed partway and left key material behind, so // cleanse rather than only clearing |kem|. EVP_HPKE_KEY_cleanup(key); @@ -795,10 +848,10 @@ static int hpke_key_init(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem, } static int hpke_key_generate(EVP_HPKE_KEY *key, const EVP_HPKE_KEM *kem) { - // See |hpke_key_init| for why |key| is cleansed before it is reused. + // See |hpke_key_init| for why |key| is cleaned up before it is reused. EVP_HPKE_KEY_cleanup(key); key->kem = kem; - if (!kem->generate_key(key)) { + if (!hpke_key_alloc(key) || !kem->generate_key(key)) { // As in |hpke_key_init|, cleanse in case |generate_key| failed partway. EVP_HPKE_KEY_cleanup(key); return 0; @@ -905,6 +958,48 @@ static int hpke_build_suite_id(const EVP_HPKE_CTX *ctx, #define HPKE_MODE_BASE 0 #define HPKE_MODE_AUTH 2 +// hpke_ctx_secrets_len returns the size of the single allocation holding +// |ctx|'s base nonce and exporter secret, which |hpke_ctx_alloc| partitions. +static size_t hpke_ctx_secrets_len(const EVP_HPKE_CTX *ctx) { + return EVP_AEAD_nonce_length(EVP_HPKE_AEAD_aead(ctx->aead)) + + EVP_MD_size(ctx->kdf->hkdf_md_func()); +} + +// hpke_ctx_free cleanses and releases |ctx|'s buffers, leaving the pointers +// dangling for the caller to clear. +static void hpke_ctx_free(EVP_HPKE_CTX *ctx) { + if (ctx->aead_ctx != NULL) { + EVP_AEAD_CTX_cleanup(ctx->aead_ctx); + OPENSSL_free(ctx->aead_ctx); + } + if (ctx->base_nonce != NULL) { + // |base_nonce| owns the allocation the exporter secret is also carved from. + OPENSSL_cleanse(ctx->base_nonce, hpke_ctx_secrets_len(ctx)); + OPENSSL_free(ctx->base_nonce); + } +} + +// hpke_ctx_alloc allocates |ctx|'s AEAD context and secret buffers, sized for +// |ctx->aead| and |ctx->kdf|, which must already be set. It returns one on +// success and zero on allocation failure. +static int hpke_ctx_alloc(EVP_HPKE_CTX *ctx) { + assert(ctx->aead_ctx == NULL && ctx->base_nonce == NULL); + ctx->aead_ctx = OPENSSL_zalloc(sizeof(EVP_AEAD_CTX)); + uint8_t *secrets = OPENSSL_zalloc(hpke_ctx_secrets_len(ctx)); + if (ctx->aead_ctx == NULL || secrets == NULL) { + OPENSSL_free(secrets); + OPENSSL_free(ctx->aead_ctx); + ctx->aead_ctx = NULL; + ctx->base_nonce = NULL; + ctx->exporter_secret = NULL; + return 0; + } + ctx->base_nonce = secrets; + ctx->exporter_secret = + secrets + EVP_AEAD_nonce_length(EVP_HPKE_AEAD_aead(ctx->aead)); + return 1; +} + static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, const uint8_t *shared_secret, size_t shared_secret_len, const uint8_t *info, @@ -914,6 +1009,10 @@ static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, return 0; } + if (!hpke_ctx_alloc(ctx)) { + return 0; + } + // psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id) // TODO(davidben): Precompute this value and store it with the EVP_HPKE_KDF. const EVP_MD *hkdf_md = ctx->kdf->hkdf_md_func(); @@ -961,7 +1060,7 @@ static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, const size_t kKeyLen = EVP_AEAD_key_length(aead); if (!hpke_labeled_expand(hkdf_md, key, kKeyLen, secret, secret_len, suite_id, sizeof(suite_id), "key", context, context_len) || - !EVP_AEAD_CTX_init(&ctx->aead_ctx, aead, key, kKeyLen, + !EVP_AEAD_CTX_init(ctx->aead_ctx, aead, key, kKeyLen, EVP_AEAD_DEFAULT_TAG_LENGTH, NULL)) { return 0; } @@ -986,11 +1085,13 @@ static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, void EVP_HPKE_CTX_zero(EVP_HPKE_CTX *ctx) { OPENSSL_memset(ctx, 0, sizeof(EVP_HPKE_CTX)); - EVP_AEAD_CTX_zero(&ctx->aead_ctx); } void EVP_HPKE_CTX_cleanup(EVP_HPKE_CTX *ctx) { - EVP_AEAD_CTX_cleanup(&ctx->aead_ctx); + hpke_ctx_free(ctx); + // Returning to the zero state makes a second call a no-op, so that the + // buffers cannot be freed twice. + EVP_HPKE_CTX_zero(ctx); } EVP_HPKE_CTX *EVP_HPKE_CTX_new(void) { @@ -1031,7 +1132,10 @@ static int hpke_ctx_setup_sender_with_seed_for_testing( const uint8_t *peer_public_key, size_t peer_public_key_len, const uint8_t *info, size_t info_len, const uint8_t *seed, size_t seed_len) { - EVP_HPKE_CTX_zero(ctx); + // |ctx| may already have been set up, in which case it owns buffers. Cleanup + // releases them and leaves the zero state this function builds on; the + // documented precondition for a setup function allows it. + EVP_HPKE_CTX_cleanup(ctx); ctx->is_sender = 1; ctx->kem = kem; ctx->kdf = kdf; @@ -1061,7 +1165,10 @@ static int hpke_ctx_setup_recipient(EVP_HPKE_CTX *ctx, const EVP_HPKE_KEY *key, OPENSSL_PUT_ERROR(EVP, EVP_R_NO_KEY_SET); return 0; } - EVP_HPKE_CTX_zero(ctx); + // |ctx| may already have been set up, in which case it owns buffers. Cleanup + // releases them and leaves the zero state this function builds on; the + // documented precondition for a setup function allows it. + EVP_HPKE_CTX_cleanup(ctx); ctx->is_sender = 0; ctx->kem = key->kem; ctx->kdf = kdf; @@ -1122,7 +1229,10 @@ static int hpke_ctx_setup_auth_sender_with_seed_for_testing( return 0; } - EVP_HPKE_CTX_zero(ctx); + // |ctx| may already have been set up, in which case it owns buffers. Cleanup + // releases them and leaves the zero state this function builds on; the + // documented precondition for a setup function allows it. + EVP_HPKE_CTX_cleanup(ctx); ctx->is_sender = 1; ctx->kem = key->kem; ctx->kdf = kdf; @@ -1158,7 +1268,10 @@ static int hpke_ctx_setup_auth_recipient( return 0; } - EVP_HPKE_CTX_zero(ctx); + // |ctx| may already have been set up, in which case it owns buffers. Cleanup + // releases them and leaves the zero state this function builds on; the + // documented precondition for a setup function allows it. + EVP_HPKE_CTX_cleanup(ctx); ctx->is_sender = 0; ctx->kem = key->kem; ctx->kdf = kdf; @@ -1207,12 +1320,17 @@ static int hpke_ctx_open(EVP_HPKE_CTX *ctx, uint8_t *out, size_t *out_len, OPENSSL_PUT_ERROR(EVP, ERR_R_OVERFLOW); return 0; } + if (ctx->aead_ctx == NULL) { + // The context was never set up, so there is no key to use. + OPENSSL_PUT_ERROR(EVP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } uint8_t nonce[EVP_AEAD_MAX_NONCE_LENGTH]; - const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx.aead); + const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx->aead); hpke_nonce(ctx, nonce, nonce_len); - if (!EVP_AEAD_CTX_open(&ctx->aead_ctx, out, out_len, max_out_len, nonce, + if (!EVP_AEAD_CTX_open(ctx->aead_ctx, out, out_len, max_out_len, nonce, nonce_len, in, in_len, ad, ad_len)) { return 0; } @@ -1231,12 +1349,17 @@ static int hpke_ctx_seal(EVP_HPKE_CTX *ctx, uint8_t *out, size_t *out_len, OPENSSL_PUT_ERROR(EVP, ERR_R_OVERFLOW); return 0; } + if (ctx->aead_ctx == NULL) { + // The context was never set up, so there is no key to use. + OPENSSL_PUT_ERROR(EVP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } uint8_t nonce[EVP_AEAD_MAX_NONCE_LENGTH]; - const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx.aead); + const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx->aead); hpke_nonce(ctx, nonce, nonce_len); - if (!EVP_AEAD_CTX_seal(&ctx->aead_ctx, out, out_len, max_out_len, nonce, + if (!EVP_AEAD_CTX_seal(ctx->aead_ctx, out, out_len, max_out_len, nonce, nonce_len, in, in_len, ad, ad_len)) { return 0; } @@ -1262,7 +1385,8 @@ static int hpke_ctx_export(const EVP_HPKE_CTX *ctx, uint8_t *out, size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *ctx) { assert(ctx->is_sender); - return EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(&ctx->aead_ctx)); + assert(ctx->aead_ctx != NULL); + return EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(ctx->aead_ctx)); } const EVP_HPKE_KEM *EVP_HPKE_CTX_kem(const EVP_HPKE_CTX *ctx) { diff --git a/crypto/hpke/hpke_test.cc b/crypto/hpke/hpke_test.cc index d067f66d700..5cae6ccca17 100644 --- a/crypto/hpke/hpke_test.cc +++ b/crypto/hpke/hpke_test.cc @@ -1380,6 +1380,114 @@ TEST(HPKETest, SelfCopyAndMoveAreSafe) { } } +// A key's buffers are sized for its KEM, so re-initializing one across KEMs +// resizes them. Going from the largest KEM to the smallest and back exercises +// both directions. +TEST(HPKETest, ReinitAcrossKEMsResizesBuffers) { + const EVP_HPKE_KEM *kems[] = {EVP_hpke_mlkem1024(), + EVP_hpke_x25519_hkdf_sha256(), + EVP_hpke_mlkem512()}; + ScopedEVP_HPKE_KEY key; + for (const EVP_HPKE_KEM *kem : kems) { + SCOPED_TRACE(EVP_HPKE_KEM_id(kem)); + ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), kem)); + ASSERT_EQ(EVP_HPKE_KEM_id(EVP_HPKE_KEY_kem(key.get())), + EVP_HPKE_KEM_id(kem)); + + uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH]; + size_t public_key_len = 0; + ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key, &public_key_len, + sizeof(public_key))); + EXPECT_EQ(public_key_len, EVP_HPKE_KEM_public_key_len(kem)); + } +} + +// Setting up the same context twice must release the first set of buffers and +// leave the context usable, rather than leaking them. +TEST(HPKETest, RepeatedSetupIsUsable) { + ScopedEVP_HPKE_KEY key; + ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_mlkem768())); + + uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH]; + size_t public_key_len = 0; + ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key, &public_key_len, + sizeof(public_key))); + + ScopedEVP_HPKE_CTX sender_ctx; + for (int i = 0; i < 2; i++) { + SCOPED_TRACE(i); + uint8_t enc[EVP_HPKE_MAX_ENC_LENGTH]; + size_t enc_len = 0; + ASSERT_TRUE(EVP_HPKE_CTX_setup_sender( + sender_ctx.get(), enc, &enc_len, sizeof(enc), EVP_HPKE_KEY_kem(key.get()), + EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(), public_key, + public_key_len, nullptr, 0)); + + static const uint8_t kPlaintext[] = "setup twice"; + uint8_t ciphertext[sizeof(kPlaintext) + EVP_HPKE_MAX_OVERHEAD]; + size_t ciphertext_len = 0; + ASSERT_TRUE(EVP_HPKE_CTX_seal(sender_ctx.get(), ciphertext, &ciphertext_len, + sizeof(ciphertext), kPlaintext, + sizeof(kPlaintext), nullptr, 0)); + + ScopedEVP_HPKE_CTX recipient_ctx; + ASSERT_TRUE(EVP_HPKE_CTX_setup_recipient( + recipient_ctx.get(), key.get(), EVP_hpke_hkdf_sha256(), + EVP_hpke_aes_128_gcm(), enc, enc_len, nullptr, 0)); + uint8_t plaintext[sizeof(kPlaintext)]; + size_t plaintext_len = 0; + ASSERT_TRUE(EVP_HPKE_CTX_open(recipient_ctx.get(), plaintext, &plaintext_len, + sizeof(plaintext), ciphertext, ciphertext_len, + nullptr, 0)); + EXPECT_EQ(Bytes(kPlaintext), Bytes(plaintext, plaintext_len)); + } +} + +// |EVP_HPKE_CTX_cleanup| returns the context to the zero state, so calling it +// again must not release the same buffers twice. +TEST(HPKETest, RepeatedCtxCleanupIsNoOp) { + ScopedEVP_HPKE_KEY key; + ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256())); + + uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH]; + size_t public_key_len = 0; + ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key, &public_key_len, + sizeof(public_key))); + + EVP_HPKE_CTX ctx; + EVP_HPKE_CTX_zero(&ctx); + uint8_t enc[EVP_HPKE_MAX_ENC_LENGTH]; + size_t enc_len = 0; + ASSERT_TRUE(EVP_HPKE_CTX_setup_sender( + &ctx, enc, &enc_len, sizeof(enc), EVP_hpke_x25519_hkdf_sha256(), + EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(), public_key, + public_key_len, nullptr, 0)); + EVP_HPKE_CTX_cleanup(&ctx); + EVP_HPKE_CTX_cleanup(&ctx); + + EVP_HPKE_CTX zeroed; + EVP_HPKE_CTX_zero(&zeroed); + EXPECT_EQ(Bytes(reinterpret_cast(&ctx), sizeof(ctx)), + Bytes(reinterpret_cast(&zeroed), sizeof(zeroed))); +} + +// Sealing or opening on a context which was never set up has no key to use, so +// it must fail rather than dereference the unset AEAD context. +TEST(HPKETest, UnsetCtxFailsCleanly) { + EVP_HPKE_CTX ctx; + EVP_HPKE_CTX_zero(&ctx); + + static const uint8_t kInput[] = "unset"; + uint8_t out[sizeof(kInput) + EVP_HPKE_MAX_OVERHEAD]; + size_t out_len = 0; + EXPECT_FALSE(EVP_HPKE_CTX_seal(&ctx, out, &out_len, sizeof(out), kInput, + sizeof(kInput), nullptr, 0)); + EXPECT_FALSE(EVP_HPKE_CTX_open(&ctx, out, &out_len, sizeof(out), kInput, + sizeof(kInput), nullptr, 0)); + ERR_clear_error(); + EVP_HPKE_CTX_cleanup(&ctx); +} + // ML-KEM decapsulation is implicitly rejecting: a corrupt but correctly-sized // "enc" yields a pseudorandom shared secret rather than an error, so recipient // setup succeeds and only the AEAD detects the problem. This is the opposite of diff --git a/include/openssl/hpke.h b/include/openssl/hpke.h index f02a1ec501c..fb510b0d011 100644 --- a/include/openssl/hpke.h +++ b/include/openssl/hpke.h @@ -51,10 +51,8 @@ OPENSSL_EXPORT uint16_t EVP_HPKE_KEM_id(const EVP_HPKE_KEM *kem); // for all KEMs currently supported by this library. // // Note this grew from 32 to 1568 when the ML-KEM KEMs were added. Callers which -// size buffers by this constant must be rebuilt against this header, as must -// callers which stack-allocate an |EVP_HPKE_KEY|: that struct grew from 72 bytes -// to 4808, both because of this constant and because it now caches an expanded -// ML-KEM decapsulation key. See |EVP_HPKE_MAX_EXPANDED_PRIVATE_KEY_LENGTH|. +// size buffers by this constant must be rebuilt against this header. Prefer +// |EVP_HPKE_KEM_public_key_len|, which reports the length for one KEM. #define EVP_HPKE_MAX_PUBLIC_KEY_LENGTH 1568 // EVP_HPKE_KEM_public_key_len returns the length of a public key for |kem|. @@ -129,10 +127,12 @@ OPENSSL_EXPORT const EVP_AEAD *EVP_HPKE_AEAD_aead(const EVP_HPKE_AEAD *aead); // This may be used for more uniform cleanup of |EVP_HPKE_KEY|. OPENSSL_EXPORT void EVP_HPKE_KEY_zero(EVP_HPKE_KEY *key); -// EVP_HPKE_KEY_cleanup cleanses the private key material in |key| and returns -// |key| to the zero state, as if it had been passed to |EVP_HPKE_KEY_zero|. -// |key| may be NULL. This clears the KEM too, so, as with a zeroed key, |key| -// must be re-initialized before it is used again. +// EVP_HPKE_KEY_cleanup cleanses the private key material in |key|, releases the +// memory it references, and returns |key| to the zero state, as if it had been +// passed to |EVP_HPKE_KEY_zero|. |key| may be NULL, but otherwise must have been +// initialized with |EVP_HPKE_KEY_zero| or one of the functions that set up a +// key. This clears the KEM too, so, as with a zeroed key, |key| must be +// re-initialized before it is used again. OPENSSL_EXPORT void EVP_HPKE_KEY_cleanup(EVP_HPKE_KEY *key); // EVP_HPKE_KEY_new returns a newly-allocated |EVP_HPKE_KEY|, or NULL on error. @@ -386,33 +386,33 @@ OPENSSL_EXPORT const EVP_HPKE_KDF *EVP_HPKE_CTX_kdf(const EVP_HPKE_CTX *ctx); // // The following structures are exported so their types are stack-allocatable, // but accessing or modifying their fields is forbidden. +// +// Every buffer they own is held out of line and sized for the configured +// algorithm, so that adding a KEM, an AEAD or a digest cannot change either +// structure's size. A caller which stack-allocates one of these against one +// release and runs against a later one would otherwise overflow it. struct evp_hpke_ctx_st { const EVP_HPKE_KEM *kem; 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]; + EVP_AEAD_CTX *aead_ctx; + uint8_t *base_nonce; + uint8_t *exporter_secret; uint64_t seq; int is_sender; }; -// EVP_HPKE_MAX_EXPANDED_PRIVATE_KEY_LENGTH is the maximum length of a KEM's -// internal private key representation, where it differs from the serialized -// form. It is sized for an expanded ML-KEM-1024 decapsulation key. -#define EVP_HPKE_MAX_EXPANDED_PRIVATE_KEY_LENGTH 3168 - struct evp_hpke_key_st { const EVP_HPKE_KEM *kem; - uint8_t private_key[EVP_HPKE_MAX_PRIVATE_KEY_LENGTH]; - uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH]; + uint8_t *private_key; + uint8_t *public_key; // expanded_private_key holds the KEM's internal private key representation, // for KEMs where it differs from the serialized form. It is derived once when // the key is initialized, so that decapsulation does not have to repeat key // generation. KEMs which use the serialized private key directly leave it - // unused. - uint8_t expanded_private_key[EVP_HPKE_MAX_EXPANDED_PRIVATE_KEY_LENGTH]; + // NULL. + uint8_t *expanded_private_key; }; From f0a06deacb3bdb3fa2917b93cff7ac2f028bb607 Mon Sep 17 00:00:00 2001 From: Will Childs-Klein Date: Fri, 9 Oct 2026 18:32:01 +0000 Subject: [PATCH 2/4] Drop the HPKE struct size static asserts The bounds were hand-picked, so they pin nothing a reviewer would not already see in the header diff. --- crypto/hpke/hpke.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c index 123de574c67..d95249ab5dd 100644 --- a/crypto/hpke/hpke.c +++ b/crypto/hpke/hpke.c @@ -24,16 +24,6 @@ // This file implements RFC 9180 and draft-ietf-hpke-pq-05. -// Callers stack-allocate |EVP_HPKE_KEY| and |EVP_HPKE_CTX|, so both sizes are -// ABI: a caller built against one release and run against a later one reserves -// the older size and overflows it. Each holds pointers and scalars only, so -// these bounds fail the build if an algorithm-dependent buffer moves back -// inline. They count pointers so that they hold on ILP32 as well. -OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_KEY) <= 8 * sizeof(void *), - evp_hpke_key_must_not_hold_inline_buffers) -OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_CTX) <= 16 * sizeof(void *), - evp_hpke_ctx_must_not_hold_inline_buffers) - // MAX_SEED_LEN is the largest |seed_len| of any KEM and MAX_SHARED_SECRET_LEN // the largest Nsecret. Both are 32 for every KEM this file implements: X25519 // seeds an ephemeral private key with 32 bytes and ML-KEM takes 32 bytes of From 94b0e732f2231638a7bc4725e3f70828d186688d Mon Sep 17 00:00:00 2001 From: Will Childs-Klein Date: Fri, 9 Oct 2026 18:57:48 +0000 Subject: [PATCH 3/4] Revert "Drop the HPKE struct size static asserts" This reverts commit f0a06deacb3bdb3fa2917b93cff7ac2f028bb607. --- crypto/hpke/hpke.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c index d95249ab5dd..123de574c67 100644 --- a/crypto/hpke/hpke.c +++ b/crypto/hpke/hpke.c @@ -24,6 +24,16 @@ // This file implements RFC 9180 and draft-ietf-hpke-pq-05. +// Callers stack-allocate |EVP_HPKE_KEY| and |EVP_HPKE_CTX|, so both sizes are +// ABI: a caller built against one release and run against a later one reserves +// the older size and overflows it. Each holds pointers and scalars only, so +// these bounds fail the build if an algorithm-dependent buffer moves back +// inline. They count pointers so that they hold on ILP32 as well. +OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_KEY) <= 8 * sizeof(void *), + evp_hpke_key_must_not_hold_inline_buffers) +OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_CTX) <= 16 * sizeof(void *), + evp_hpke_ctx_must_not_hold_inline_buffers) + // MAX_SEED_LEN is the largest |seed_len| of any KEM and MAX_SHARED_SECRET_LEN // the largest Nsecret. Both are 32 for every KEM this file implements: X25519 // seeds an ephemeral private key with 32 bytes and ML-KEM takes 32 bytes of From 17ef5f9d2b8f9adafe08d8f45f8cbf97979f3d8c Mon Sep 17 00:00:00 2001 From: Will Childs-Klein Date: Fri, 9 Oct 2026 19:11:30 +0000 Subject: [PATCH 4/4] Leave EVP_HPKE_CTX alone Its layout is unchanged between FIPS 5.x and main, so reshaping it here would introduce an ABI break rather than repair one. Only EVP_HPKE_KEY grew when the ML-KEM KEMs landed. --- crypto/hpke/hpke.c | 108 ++++++--------------------------------- crypto/hpke/hpke_test.cc | 45 ---------------- include/openssl/hpke.h | 14 ++--- 3 files changed, 24 insertions(+), 143 deletions(-) diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c index 123de574c67..b3238d5f95a 100644 --- a/crypto/hpke/hpke.c +++ b/crypto/hpke/hpke.c @@ -24,15 +24,12 @@ // This file implements RFC 9180 and draft-ietf-hpke-pq-05. -// Callers stack-allocate |EVP_HPKE_KEY| and |EVP_HPKE_CTX|, so both sizes are -// ABI: a caller built against one release and run against a later one reserves -// the older size and overflows it. Each holds pointers and scalars only, so -// these bounds fail the build if an algorithm-dependent buffer moves back -// inline. They count pointers so that they hold on ILP32 as well. -OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_KEY) <= 8 * sizeof(void *), +// Callers stack-allocate |EVP_HPKE_KEY|, so its size is ABI: a caller built +// against one release and run against a later one reserves the older size and +// overflows it. The struct is four pointers on any ABI, so spelling that out +// fails the build if KEM-dependent key material moves back inline. +OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_KEY) == 4 * sizeof(void *), evp_hpke_key_must_not_hold_inline_buffers) -OPENSSL_STATIC_ASSERT(sizeof(EVP_HPKE_CTX) <= 16 * sizeof(void *), - evp_hpke_ctx_must_not_hold_inline_buffers) // MAX_SEED_LEN is the largest |seed_len| of any KEM and MAX_SHARED_SECRET_LEN // the largest Nsecret. Both are 32 for every KEM this file implements: X25519 @@ -958,48 +955,6 @@ static int hpke_build_suite_id(const EVP_HPKE_CTX *ctx, #define HPKE_MODE_BASE 0 #define HPKE_MODE_AUTH 2 -// hpke_ctx_secrets_len returns the size of the single allocation holding -// |ctx|'s base nonce and exporter secret, which |hpke_ctx_alloc| partitions. -static size_t hpke_ctx_secrets_len(const EVP_HPKE_CTX *ctx) { - return EVP_AEAD_nonce_length(EVP_HPKE_AEAD_aead(ctx->aead)) + - EVP_MD_size(ctx->kdf->hkdf_md_func()); -} - -// hpke_ctx_free cleanses and releases |ctx|'s buffers, leaving the pointers -// dangling for the caller to clear. -static void hpke_ctx_free(EVP_HPKE_CTX *ctx) { - if (ctx->aead_ctx != NULL) { - EVP_AEAD_CTX_cleanup(ctx->aead_ctx); - OPENSSL_free(ctx->aead_ctx); - } - if (ctx->base_nonce != NULL) { - // |base_nonce| owns the allocation the exporter secret is also carved from. - OPENSSL_cleanse(ctx->base_nonce, hpke_ctx_secrets_len(ctx)); - OPENSSL_free(ctx->base_nonce); - } -} - -// hpke_ctx_alloc allocates |ctx|'s AEAD context and secret buffers, sized for -// |ctx->aead| and |ctx->kdf|, which must already be set. It returns one on -// success and zero on allocation failure. -static int hpke_ctx_alloc(EVP_HPKE_CTX *ctx) { - assert(ctx->aead_ctx == NULL && ctx->base_nonce == NULL); - ctx->aead_ctx = OPENSSL_zalloc(sizeof(EVP_AEAD_CTX)); - uint8_t *secrets = OPENSSL_zalloc(hpke_ctx_secrets_len(ctx)); - if (ctx->aead_ctx == NULL || secrets == NULL) { - OPENSSL_free(secrets); - OPENSSL_free(ctx->aead_ctx); - ctx->aead_ctx = NULL; - ctx->base_nonce = NULL; - ctx->exporter_secret = NULL; - return 0; - } - ctx->base_nonce = secrets; - ctx->exporter_secret = - secrets + EVP_AEAD_nonce_length(EVP_HPKE_AEAD_aead(ctx->aead)); - return 1; -} - static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, const uint8_t *shared_secret, size_t shared_secret_len, const uint8_t *info, @@ -1009,10 +964,6 @@ static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, return 0; } - if (!hpke_ctx_alloc(ctx)) { - return 0; - } - // psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id) // TODO(davidben): Precompute this value and store it with the EVP_HPKE_KDF. const EVP_MD *hkdf_md = ctx->kdf->hkdf_md_func(); @@ -1060,7 +1011,7 @@ static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, const size_t kKeyLen = EVP_AEAD_key_length(aead); if (!hpke_labeled_expand(hkdf_md, key, kKeyLen, secret, secret_len, suite_id, sizeof(suite_id), "key", context, context_len) || - !EVP_AEAD_CTX_init(ctx->aead_ctx, aead, key, kKeyLen, + !EVP_AEAD_CTX_init(&ctx->aead_ctx, aead, key, kKeyLen, EVP_AEAD_DEFAULT_TAG_LENGTH, NULL)) { return 0; } @@ -1085,13 +1036,11 @@ static int hpke_key_schedule(EVP_HPKE_CTX *ctx, uint8_t mode, void EVP_HPKE_CTX_zero(EVP_HPKE_CTX *ctx) { OPENSSL_memset(ctx, 0, sizeof(EVP_HPKE_CTX)); + EVP_AEAD_CTX_zero(&ctx->aead_ctx); } void EVP_HPKE_CTX_cleanup(EVP_HPKE_CTX *ctx) { - hpke_ctx_free(ctx); - // Returning to the zero state makes a second call a no-op, so that the - // buffers cannot be freed twice. - EVP_HPKE_CTX_zero(ctx); + EVP_AEAD_CTX_cleanup(&ctx->aead_ctx); } EVP_HPKE_CTX *EVP_HPKE_CTX_new(void) { @@ -1132,10 +1081,7 @@ static int hpke_ctx_setup_sender_with_seed_for_testing( const uint8_t *peer_public_key, size_t peer_public_key_len, const uint8_t *info, size_t info_len, const uint8_t *seed, size_t seed_len) { - // |ctx| may already have been set up, in which case it owns buffers. Cleanup - // releases them and leaves the zero state this function builds on; the - // documented precondition for a setup function allows it. - EVP_HPKE_CTX_cleanup(ctx); + EVP_HPKE_CTX_zero(ctx); ctx->is_sender = 1; ctx->kem = kem; ctx->kdf = kdf; @@ -1165,10 +1111,7 @@ static int hpke_ctx_setup_recipient(EVP_HPKE_CTX *ctx, const EVP_HPKE_KEY *key, OPENSSL_PUT_ERROR(EVP, EVP_R_NO_KEY_SET); return 0; } - // |ctx| may already have been set up, in which case it owns buffers. Cleanup - // releases them and leaves the zero state this function builds on; the - // documented precondition for a setup function allows it. - EVP_HPKE_CTX_cleanup(ctx); + EVP_HPKE_CTX_zero(ctx); ctx->is_sender = 0; ctx->kem = key->kem; ctx->kdf = kdf; @@ -1229,10 +1172,7 @@ static int hpke_ctx_setup_auth_sender_with_seed_for_testing( return 0; } - // |ctx| may already have been set up, in which case it owns buffers. Cleanup - // releases them and leaves the zero state this function builds on; the - // documented precondition for a setup function allows it. - EVP_HPKE_CTX_cleanup(ctx); + EVP_HPKE_CTX_zero(ctx); ctx->is_sender = 1; ctx->kem = key->kem; ctx->kdf = kdf; @@ -1268,10 +1208,7 @@ static int hpke_ctx_setup_auth_recipient( return 0; } - // |ctx| may already have been set up, in which case it owns buffers. Cleanup - // releases them and leaves the zero state this function builds on; the - // documented precondition for a setup function allows it. - EVP_HPKE_CTX_cleanup(ctx); + EVP_HPKE_CTX_zero(ctx); ctx->is_sender = 0; ctx->kem = key->kem; ctx->kdf = kdf; @@ -1320,17 +1257,12 @@ static int hpke_ctx_open(EVP_HPKE_CTX *ctx, uint8_t *out, size_t *out_len, OPENSSL_PUT_ERROR(EVP, ERR_R_OVERFLOW); return 0; } - if (ctx->aead_ctx == NULL) { - // The context was never set up, so there is no key to use. - OPENSSL_PUT_ERROR(EVP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); - return 0; - } uint8_t nonce[EVP_AEAD_MAX_NONCE_LENGTH]; - const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx->aead); + const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx.aead); hpke_nonce(ctx, nonce, nonce_len); - if (!EVP_AEAD_CTX_open(ctx->aead_ctx, out, out_len, max_out_len, nonce, + if (!EVP_AEAD_CTX_open(&ctx->aead_ctx, out, out_len, max_out_len, nonce, nonce_len, in, in_len, ad, ad_len)) { return 0; } @@ -1349,17 +1281,12 @@ static int hpke_ctx_seal(EVP_HPKE_CTX *ctx, uint8_t *out, size_t *out_len, OPENSSL_PUT_ERROR(EVP, ERR_R_OVERFLOW); return 0; } - if (ctx->aead_ctx == NULL) { - // The context was never set up, so there is no key to use. - OPENSSL_PUT_ERROR(EVP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); - return 0; - } uint8_t nonce[EVP_AEAD_MAX_NONCE_LENGTH]; - const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx->aead); + const size_t nonce_len = EVP_AEAD_nonce_length(ctx->aead_ctx.aead); hpke_nonce(ctx, nonce, nonce_len); - if (!EVP_AEAD_CTX_seal(ctx->aead_ctx, out, out_len, max_out_len, nonce, + if (!EVP_AEAD_CTX_seal(&ctx->aead_ctx, out, out_len, max_out_len, nonce, nonce_len, in, in_len, ad, ad_len)) { return 0; } @@ -1385,8 +1312,7 @@ static int hpke_ctx_export(const EVP_HPKE_CTX *ctx, uint8_t *out, size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *ctx) { assert(ctx->is_sender); - assert(ctx->aead_ctx != NULL); - return EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(ctx->aead_ctx)); + return EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(&ctx->aead_ctx)); } const EVP_HPKE_KEM *EVP_HPKE_CTX_kem(const EVP_HPKE_CTX *ctx) { diff --git a/crypto/hpke/hpke_test.cc b/crypto/hpke/hpke_test.cc index 5cae6ccca17..ce5502822a1 100644 --- a/crypto/hpke/hpke_test.cc +++ b/crypto/hpke/hpke_test.cc @@ -1443,51 +1443,6 @@ TEST(HPKETest, RepeatedSetupIsUsable) { } } -// |EVP_HPKE_CTX_cleanup| returns the context to the zero state, so calling it -// again must not release the same buffers twice. -TEST(HPKETest, RepeatedCtxCleanupIsNoOp) { - ScopedEVP_HPKE_KEY key; - ASSERT_TRUE(EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256())); - - uint8_t public_key[EVP_HPKE_MAX_PUBLIC_KEY_LENGTH]; - size_t public_key_len = 0; - ASSERT_TRUE(EVP_HPKE_KEY_public_key(key.get(), public_key, &public_key_len, - sizeof(public_key))); - - EVP_HPKE_CTX ctx; - EVP_HPKE_CTX_zero(&ctx); - uint8_t enc[EVP_HPKE_MAX_ENC_LENGTH]; - size_t enc_len = 0; - ASSERT_TRUE(EVP_HPKE_CTX_setup_sender( - &ctx, enc, &enc_len, sizeof(enc), EVP_hpke_x25519_hkdf_sha256(), - EVP_hpke_hkdf_sha256(), EVP_hpke_aes_128_gcm(), public_key, - public_key_len, nullptr, 0)); - EVP_HPKE_CTX_cleanup(&ctx); - EVP_HPKE_CTX_cleanup(&ctx); - - EVP_HPKE_CTX zeroed; - EVP_HPKE_CTX_zero(&zeroed); - EXPECT_EQ(Bytes(reinterpret_cast(&ctx), sizeof(ctx)), - Bytes(reinterpret_cast(&zeroed), sizeof(zeroed))); -} - -// Sealing or opening on a context which was never set up has no key to use, so -// it must fail rather than dereference the unset AEAD context. -TEST(HPKETest, UnsetCtxFailsCleanly) { - EVP_HPKE_CTX ctx; - EVP_HPKE_CTX_zero(&ctx); - - static const uint8_t kInput[] = "unset"; - uint8_t out[sizeof(kInput) + EVP_HPKE_MAX_OVERHEAD]; - size_t out_len = 0; - EXPECT_FALSE(EVP_HPKE_CTX_seal(&ctx, out, &out_len, sizeof(out), kInput, - sizeof(kInput), nullptr, 0)); - EXPECT_FALSE(EVP_HPKE_CTX_open(&ctx, out, &out_len, sizeof(out), kInput, - sizeof(kInput), nullptr, 0)); - ERR_clear_error(); - EVP_HPKE_CTX_cleanup(&ctx); -} - // ML-KEM decapsulation is implicitly rejecting: a corrupt but correctly-sized // "enc" yields a pseudorandom shared secret rather than an error, so recipient // setup succeeds and only the AEAD detects the problem. This is the opposite of diff --git a/include/openssl/hpke.h b/include/openssl/hpke.h index fb510b0d011..32e84ea003e 100644 --- a/include/openssl/hpke.h +++ b/include/openssl/hpke.h @@ -387,18 +387,18 @@ OPENSSL_EXPORT const EVP_HPKE_KDF *EVP_HPKE_CTX_kdf(const EVP_HPKE_CTX *ctx); // The following structures are exported so their types are stack-allocatable, // but accessing or modifying their fields is forbidden. // -// Every buffer they own is held out of line and sized for the configured -// algorithm, so that adding a KEM, an AEAD or a digest cannot change either -// structure's size. A caller which stack-allocates one of these against one -// release and runs against a later one would otherwise overflow it. +// |EVP_HPKE_KEY| holds its key material out of line, sized for the configured +// KEM, so that adding a KEM cannot change the structure's size. A caller which +// stack-allocates one against one release and runs against a later one would +// otherwise overflow it. struct evp_hpke_ctx_st { const EVP_HPKE_KEM *kem; const EVP_HPKE_AEAD *aead; const EVP_HPKE_KDF *kdf; - EVP_AEAD_CTX *aead_ctx; - uint8_t *base_nonce; - uint8_t *exporter_secret; + 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; };