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);