Commit 5f48317d6d for openssl.org

commit 5f48317d6d847e555bf788f8b71d51ac032a6344
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Sun Jul 19 10:10:14 2026 +0900

    Restore empty raw PKCS#1 verify-recover behavior

    EVP_PKEY_verify_recover() with RSA PKCS#1 v1.5 padding and no configured
    signature digest rejected a valid signature whose recovered payload is
    empty, instead of returning success with a recovered length of zero.

    RSA_public_decrypt() returns -1 on error and otherwise the number of
    recovered bytes, which may legitimately be zero for a raw PKCS#1 v1.5
    signature that encodes an empty payload.  Pull request #28306 ("Make
    error checks on RSA_public_decrypt() consistent") changed the error
    check in the raw, no-digest verify-recover path from "ret < 0" to
    "ret <= 0", so a valid zero recovered length was mistaken for an error.

    Restore the "< 0" check in that branch only, leaving the digest-aware
    PKCS#1, X9.31, PSS and ordinary verification paths untouched.

    Add test_RSA_verify_recover_empty_payload, which performs a complete
    zero-length raw PKCS#1 v1.5 sign-and-recover round trip through EVP and
    fails on the unmodified implementation specifically at the
    EVP_PKEY_verify_recover() call.  OpenSSL 3.0.13 recovers the same
    signature and reports a zero-byte result.

    The behavior regressed as a result of #28306.

    Fixes #32000

    Assisted-by: OpenCode:GLM-5.2

    Reviewed-by: Paul Dale <paul.dale@oracle.com>
    Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
    MergeDate: Fri Jul 24 13:06:58 2026
    (Merged from https://github.com/openssl/openssl/pull/32001)

diff --git a/providers/implementations/signature/rsa_sig.c b/providers/implementations/signature/rsa_sig.c
index 3d56bd0185..232414935f 100644
--- a/providers/implementations/signature/rsa_sig.c
+++ b/providers/implementations/signature/rsa_sig.c
@@ -1026,7 +1026,13 @@ static int rsa_verify_recover(void *vprsactx,
         }
         ret = RSA_public_decrypt((int)siglen, sig, rout, prsactx->rsa,
             prsactx->pad_mode);
-        if (ret <= 0) {
+        /*
+         * RSA_public_decrypt() returns -1 on error and otherwise the number
+         * of recovered bytes, which may legitimately be zero for a raw
+         * PKCS#1 v1.5 signature that encodes an empty payload.  Treat only
+         * a negative result as an error.
+         */
+        if (ret < 0) {
             ERR_raise(ERR_LIB_PROV, ERR_R_RSA_LIB);
             return 0;
         }
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index 22e4689982..00ed62f264 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -4244,6 +4244,76 @@ done:
     return ret;
 }

+/*
+ * A raw RSA PKCS#1 v1.5 signature whose recovered data is empty must be
+ * recovered successfully with a length of zero, not rejected as an error.
+ */
+static int test_RSA_verify_recover_empty_payload(void)
+{
+    int ret = 0;
+    int recovered_cap = 0;
+    EVP_PKEY *pkey = NULL;
+    EVP_PKEY_CTX *sign_ctx = NULL, *verify_ctx = NULL;
+    unsigned char *sig = NULL, *recovered = NULL;
+    size_t sig_len = 0, recovered_len = 0;
+    /*
+     * The signed input has zero length, but a valid non-null address is still
+     * passed so the result does not depend on how lower layers treat NULL for
+     * zero-length data.
+     */
+    const unsigned char empty[] = { 0 };
+
+    if (OSSL_PROVIDER_available(testctx, "fips"))
+        return TEST_skip("Test skipped for FIPS provider");
+
+    if (!TEST_ptr(pkey = load_example_rsa_key())
+        || !TEST_ptr(sign_ctx = EVP_PKEY_CTX_new_from_pkey(testctx, pkey, NULL))
+        || !TEST_int_gt(EVP_PKEY_sign_init(sign_ctx), 0)
+        || !TEST_int_gt(EVP_PKEY_CTX_set_rsa_padding(sign_ctx, RSA_PKCS1_PADDING), 0)
+        /*
+         * Deliberately do not configure a signature digest so that the raw
+         * PKCS#1 v1.5 sign and verify-recover paths are exercised.
+         */
+        || !TEST_int_gt(EVP_PKEY_sign(sign_ctx, NULL, &sig_len, empty, 0), 0)
+        || !TEST_ptr(sig = OPENSSL_malloc(sig_len))
+        || !TEST_int_gt(EVP_PKEY_sign(sign_ctx, sig, &sig_len, empty, 0), 0)
+        || !TEST_int_gt(recovered_cap = EVP_PKEY_get_size(pkey), 0)
+        || !TEST_ptr(recovered = OPENSSL_malloc(recovered_cap))
+        || !TEST_ptr(verify_ctx = EVP_PKEY_CTX_new_from_pkey(testctx, pkey, NULL))
+        || !TEST_int_gt(EVP_PKEY_verify_recover_init(verify_ctx), 0)
+        || !TEST_int_gt(EVP_PKEY_CTX_set_rsa_padding(verify_ctx, RSA_PKCS1_PADDING),
+            0))
+        goto done;
+
+    /* Size-query call must succeed. */
+    recovered_len = (size_t)recovered_cap;
+    if (!TEST_int_gt(EVP_PKEY_verify_recover(verify_ctx, NULL,
+                         &recovered_len, sig, sig_len),
+            0))
+        goto done;
+
+    /*
+     * The actual recovery call is essential: a NULL output buffer would only
+     * run the size-query path, which never decodes the signature and so would
+     * not reproduce the regression.
+     */
+    recovered_len = (size_t)recovered_cap;
+    if (!TEST_int_gt(EVP_PKEY_verify_recover(verify_ctx, recovered,
+                         &recovered_len, sig, sig_len),
+            0)
+        || !TEST_size_t_eq(recovered_len, 0))
+        goto done;
+
+    ret = 1;
+done:
+    EVP_PKEY_CTX_free(sign_ctx);
+    EVP_PKEY_CTX_free(verify_ctx);
+    EVP_PKEY_free(pkey);
+    OPENSSL_free(sig);
+    OPENSSL_free(recovered);
+    return ret;
+}
+
 static int test_RSA_encrypt(void)
 {
     int ret = 0;
@@ -9238,6 +9308,7 @@ int setup_tests(void)
     ADD_TEST(test_RSA_OAEP_set_get_params);
     ADD_TEST(test_RSA_OAEP_set_null_label);
     ADD_TEST(test_RSA_verify_recover_rejects_short_buffer);
+    ADD_TEST(test_RSA_verify_recover_empty_payload);
     ADD_TEST(test_RSA_encrypt);
 #ifndef OPENSSL_NO_DEPRECATED_3_0
     ADD_TEST(test_RSA_legacy);