Commit 2f83f9560e for openssl.org
commit 2f83f9560e5f27c7680ef5ca5aae0c9237bc06f8
Author: David Benjamin <davidben@google.com>
Date: Wed Jun 17 20:58:44 2026 -0400
hpke: Fix non-spec-compliant handling of zero-length plaintexts
The zero-length plaintext is as valid of a plaintext as any. OpenSSL's
HPKE implementation incorrectly rejected it.
This behavior means OpenSSL-based HPKE applications are more prone to
bugs than non-OpenSSL HPKE applications. If one were to read RFC 9180,
one would expect that OSSL_HPKE_seal behaves uniformly on all byte
strings and is basically infallible. However, OpenSSL's limitation means
that it can fail on zero-length inputs.
See also this discussion:
https://mailarchive.ietf.org/arch/msg/hpke/74_7G35n6SOibkddgVIN7JYu_wM/
Fortunately, this is easy to fix without any API changes. No
documentation update is needed, as far as I can tell, because this
limitation was never documented.
Handling for (NULL, 0) edge cases:
In principle, this code should just work with (NULL, 0), however, two
problems plague it:
1. Prior to C2y, C had some language bugs in this area. See
https://developers.redhat.com/articles/2024/12/11/making-memcpynull-null-0-well-defined
2. EVP_CIPHER's API is extremely problematic and special-cases NULL in
places. The APIs proposed in #22628 will fix this, but, in the
meantime, follow the existing pattern of conditioning out the
zero-length operations.
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Merge-date: Thu Oct 29 09:50:44 2026
Merged-from: https://github.com/openssl/openssl/pull/31573
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index 0a301e462f..a489ad3dbf 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -149,7 +149,7 @@ static int hpke_aead_dec(OSSL_HPKE_CTX *hctx, const unsigned char *iv,
size_t taglen;
taglen = hctx->aead_info->taglen;
- if (ctlen <= taglen || *ptlen < ctlen - taglen
+ if (ctlen < taglen || *ptlen < ctlen - taglen
|| aadlen > INT_MAX || ctlen > INT_MAX) {
ERR_raise(ERR_LIB_CRYPTO, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
@@ -180,9 +180,13 @@ static int hpke_aead_dec(OSSL_HPKE_CTX *hctx, const unsigned char *iv,
goto err;
}
}
- if (EVP_DecryptUpdate(ctx, pt, &len, ct, (int)(ctlen - taglen)) != 1) {
- ERR_raise(ERR_LIB_CRYPTO, ERR_R_INTERNAL_ERROR);
- goto err;
+ if (ctlen > taglen) {
+ if (EVP_DecryptUpdate(ctx, pt, &len, ct, (int)(ctlen - taglen)) != 1) {
+ ERR_raise(ERR_LIB_CRYPTO, ERR_R_INTERNAL_ERROR);
+ goto err;
+ }
+ } else {
+ len = 0;
}
*ptlen = len;
if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG,
@@ -190,11 +194,19 @@ static int hpke_aead_dec(OSSL_HPKE_CTX *hctx, const unsigned char *iv,
ERR_raise(ERR_LIB_CRYPTO, ERR_R_INTERNAL_ERROR);
goto err;
}
- /* Finalise decryption. */
- if (EVP_DecryptFinal_ex(ctx, pt + len, &len) <= 0) {
+ /*
+ * Finalise decryption, which checks the tag. Ideally, we would simply write
+ * pt + len here, but the caller may have passed a zero-length output buffer
+ * if ctlen == taglen. pt may be NULL for a zero-length buffer. However,
+ * before C2y, NULL + 0 was undefined. While this will be fixed and applied
+ * to old C versions retroactively, we support old compilers and sanitizers.
+ * https://developers.redhat.com/articles/2024/12/11/making-memcpynull-null-0-well-defined
+ */
+ if (EVP_DecryptFinal_ex(ctx, len ? pt + len : pt, &len) <= 0) {
ERR_raise(ERR_LIB_CRYPTO, ERR_R_INTERNAL_ERROR);
goto err;
}
+ *ptlen += len;
erv = 1;
err:
@@ -228,7 +240,7 @@ static int hpke_aead_enc(OSSL_HPKE_CTX *hctx, const unsigned char *iv,
unsigned char tag[EVP_MAX_AEAD_TAG_LENGTH];
taglen = hctx->aead_info->taglen;
- if (*ctlen <= taglen || ptlen > *ctlen - taglen
+ if (*ctlen < taglen || ptlen > *ctlen - taglen
|| aadlen > INT_MAX || ptlen > INT_MAX) {
ERR_raise(ERR_LIB_CRYPTO, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
@@ -263,9 +275,13 @@ static int hpke_aead_enc(OSSL_HPKE_CTX *hctx, const unsigned char *iv,
goto err;
}
}
- if (EVP_EncryptUpdate(ctx, ct, &len, pt, (int)ptlen) != 1) {
- ERR_raise(ERR_LIB_CRYPTO, ERR_R_INTERNAL_ERROR);
- goto err;
+ if (ptlen != 0) {
+ if (EVP_EncryptUpdate(ctx, ct, &len, pt, (int)ptlen) != 1) {
+ ERR_raise(ERR_LIB_CRYPTO, ERR_R_INTERNAL_ERROR);
+ goto err;
+ }
+ } else {
+ len = 0;
}
*ctlen = len;
/* Finalise the encryption. */
@@ -1171,7 +1187,7 @@ int OSSL_HPKE_seal(OSSL_HPKE_CTX *ctx,
size_t seqlen = 0;
if (ctx == NULL || ct == NULL || ctlen == NULL || *ctlen == 0
- || pt == NULL || ptlen == 0) {
+ || (pt == NULL && ptlen != 0)) {
ERR_raise(ERR_LIB_CRYPTO, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
@@ -1212,7 +1228,7 @@ int OSSL_HPKE_open(OSSL_HPKE_CTX *ctx,
unsigned char seqbuf[OSSL_HPKE_MAX_NONCELEN];
size_t seqlen = 0;
- if (ctx == NULL || pt == NULL || ptlen == NULL || *ptlen == 0
+ if (ctx == NULL || ptlen == NULL || (pt == NULL && *ptlen != 0)
|| ct == NULL || ctlen == 0) {
ERR_raise(ERR_LIB_CRYPTO, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
diff --git a/test/hpke_test.c b/test/hpke_test.c
index f4e36419e8..9869a2a119 100644
--- a/test/hpke_test.c
+++ b/test/hpke_test.c
@@ -1462,9 +1462,15 @@ static int test_hpke_oddcalls(void)
/* second encap fail */
if (!TEST_false(OSSL_HPKE_encap(ctx, enc, &enclen, pub, publen, NULL, 0)))
goto end;
- plainlen = 0;
- /* should fail for no plaintext */
- if (!TEST_false(OSSL_HPKE_seal(ctx, cipher, &cipherlen, NULL, 0,
+ /* seal with NULL pt and non-zero ptlen */
+ if (!TEST_false(OSSL_HPKE_seal(ctx, cipher, &cipherlen, NULL, 0, NULL, 1)))
+ goto end;
+ /* seal with NULL output buffer */
+ if (!TEST_false(OSSL_HPKE_seal(ctx, NULL, &cipherlen, NULL, 0,
+ plain, plainlen)))
+ goto end;
+ /* seal with NULL ctlen */
+ if (!TEST_false(OSSL_HPKE_seal(ctx, cipher, NULL, NULL, 0,
plain, plainlen)))
goto end;
plainlen = sizeof(plain);
@@ -1508,6 +1514,14 @@ static int test_hpke_oddcalls(void)
cipher, cipherlen)))
goto end;
clearlen = OSSL_HPKE_TSTSIZE;
+ /* open with non-empty NULL output buffer */
+ if (!TEST_false(OSSL_HPKE_open(rctx, NULL, &clearlen, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
+ /* open with NULL ptlen */
+ if (!TEST_false(OSSL_HPKE_open(rctx, clear, NULL, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
/* seq wrap around test */
if (!TEST_true(OSSL_HPKE_CTX_set_seq(rctx, -1)))
goto end;
@@ -1521,6 +1535,49 @@ static int test_hpke_oddcalls(void)
goto end;
if (!TEST_mem_eq(plain, plainlen, clear, clearlen))
goto end;
+ /* the zero-length plaintext is a valid input */
+ plainlen = 0;
+ if (!TEST_true(OSSL_HPKE_seal(ctx, cipher, &cipherlen, NULL, 0,
+ plain, plainlen)))
+ goto end;
+ if (!TEST_true(OSSL_HPKE_open(rctx, clear, &clearlen, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
+ if (!TEST_mem_eq(plain, plainlen, clear, clearlen))
+ goto end;
+ /* repeat the test now that cipherlen and clearlen are tightly bounded */
+ if (!TEST_true(OSSL_HPKE_seal(ctx, cipher, &cipherlen, NULL, 0,
+ plain, plainlen)))
+ goto end;
+ if (!TEST_true(OSSL_HPKE_open(rctx, clear, &clearlen, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
+ if (!TEST_mem_eq(plain, plainlen, clear, clearlen))
+ goto end;
+ /* NULL is a valid way to express both the empty plaintext and the empty
+ * output buffer */
+ if (!TEST_true(OSSL_HPKE_seal(ctx, cipher, &cipherlen, NULL, 0,
+ NULL, 0)))
+ goto end;
+ clearlen = 0;
+ if (!TEST_true(OSSL_HPKE_open(rctx, NULL, &clearlen, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
+ if (!TEST_size_t_eq(clearlen, 0))
+ goto end;
+ /* corrupt the tag of an encryption of the empty string */
+ if (!TEST_true(OSSL_HPKE_seal(ctx, cipher, &cipherlen, NULL, 0,
+ plain, plainlen)))
+ goto end;
+ cipher[cipherlen - 1] ^= 1;
+ if (!TEST_false(OSSL_HPKE_open(rctx, clear, &clearlen, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
+ /* same as above, but with a NULL output buffer */
+ if (!TEST_false(OSSL_HPKE_open(rctx, NULL, &clearlen, NULL, 0,
+ cipher, cipherlen)))
+ goto end;
+
erv = 1;
end:
OSSL_HPKE_CTX_free(ctx);