Commit d218dcf1bc for openssl.org

commit d218dcf1bc23916db58c386c656c213da193e0f6
Author: Igor Ustinov <igus@openssl.foundation>
Date:   Tue Oct 20 12:39:47 2026 +0200

    CMS_get0_signers() returns only the signers that passed verification

    With CMS_VERIFY_PARTIAL, CMS_verify() succeeds as long as at least
    one SignerInfo verifies, but CMS_get0_signers() still returned the
    certificate of every SignerInfo, defeating "signed by X" or
    "signed by N signers" policies. The same applied to the certificates
    written by "openssl cms -verify -verify_partial -signer".

    This commit fixes CMS_get0_signers() behaviour returning only
    certificates of signers that passed verification.
    Nothing changes if CMS_VERIFY_PARTIAL isn't set.

    Reported-by: Anthropic OSS scan id ANT-2026-GF4OIPVH

    Assisted-by: Claude:claude-opus-5-5
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Matt Caswell <matt@openssl.foundation>
    Merge-date: Wed Oct 21 15:39:29 2026
    Merged-from: https://github.com/openssl/openssl/pull/33279

diff --git a/crypto/cms/cms_sd.c b/crypto/cms/cms_sd.c
index f0d581e8ed..f0a2ee1229 100644
--- a/crypto/cms/cms_sd.c
+++ b/crypto/cms/cms_sd.c
@@ -847,7 +847,7 @@ STACK_OF(X509) *CMS_get0_signers(CMS_ContentInfo *cms)
     sinfos = CMS_get0_SignerInfos(cms);
     for (i = 0; i < sk_CMS_SignerInfo_num(sinfos); i++) {
         si = sk_CMS_SignerInfo_value(sinfos, i);
-        if (si->signer != NULL) {
+        if (si->signer != NULL && si->verify_result) {
             if (!ossl_x509_add_cert_new(&signers, si->signer,
                     X509_ADD_FLAG_DEFAULT)) {
                 sk_X509_free(signers);
diff --git a/doc/man3/CMS_verify.pod b/doc/man3/CMS_verify.pod
index 92e1012210..958f6d87cb 100644
--- a/doc/man3/CMS_verify.pod
+++ b/doc/man3/CMS_verify.pod
@@ -60,6 +60,9 @@ are used when retrieving algorithms from providers.

 CMS_get0_signers() retrieves the signer certificate(s) from I<cms>; it may only
 be called after a successful CMS_verify() or CMS_SignedData_verify() operation.
+Only the certificates of signatures that passed verification are returned.
+This matters when B<CMS_VERIFY_PARTIAL> is set, where the verification may
+succeed even though some of the signatures failed.

 CMS_SignerInfo_verify() verifies the signed attributes attached to the given
 SignerInfo B<si>.
@@ -172,7 +175,8 @@ CMS_verify() returns 1 for a successful verification and 0 if an error occurred.
 CMS_SignedData_verify() returns a memory BIO containing the verified content,
 or NULL on error.

-CMS_get0_signers() returns all signers or NULL if an error occurred.
+CMS_get0_signers() returns the certificates of all signers whose signatures
+passed verification, or NULL if an error occurred.

 The error can be obtained from L<ERR_get_error(3)>.

diff --git a/test/cmsapitest.c b/test/cmsapitest.c
index eff7884142..a38793c3c8 100644
--- a/test/cmsapitest.c
+++ b/test/cmsapitest.c
@@ -752,6 +752,7 @@ static int test_CMS_verify_result_partial_independence(void)
     EVP_PKEY *pkey1 = NULL, *pkey2 = NULL;
     X509 *cert1 = NULL, *local_cert2 = NULL;
     STACK_OF(CMS_SignerInfo) *sinfos;
+    STACK_OF(X509) *signers = NULL;
     CMS_SignerInfo *si1, *si2;
     ASN1_OCTET_STRING *sig;
     int ret = 0;
@@ -830,6 +831,12 @@ static int test_CMS_verify_result_partial_independence(void)
         || !TEST_size_t_gt(BIO_ctrl_pending(out), 0))
         goto end;

+    /* Only the signer whose signature verified may be reported */
+    if (!TEST_ptr(signers = CMS_get0_signers(cms))
+        || !TEST_int_eq(sk_X509_num(signers), 1)
+        || !TEST_int_eq(X509_cmp(sk_X509_value(signers, 0), local_cert2), 0))
+        goto end;
+
     /* now also break signer 2's signature: the overall call must now fail */
     if (!TEST_ptr(sig = CMS_SignerInfo_get0_signature(si2))
         || !TEST_size_t_gt(ASN1_STRING_get_length(sig), 0))
@@ -858,6 +865,7 @@ static int test_CMS_verify_result_partial_independence(void)

     ret = 1;
 end:
+    sk_X509_free(signers);
     CMS_ContentInfo_free(cms);
     X509_STORE_free(store);
     X509_free(cert1);
diff --git a/test/recipes/80-test_cms.t b/test/recipes/80-test_cms.t
index 41cfcee278..2f5af7d871 100644
--- a/test/recipes/80-test_cms.t
+++ b/test/recipes/80-test_cms.t
@@ -1848,13 +1848,14 @@ subtest "PWRI missing keyDerivationAlgorithm regression" => sub {
 };

 subtest "sign and verify with multiple keys and -verify_partial" => sub {
-    plan tests => 9;
+    plan tests => 10;

     my $smrsa2 = catfile($smdir, "smrsa2.pem");
     my $sig1 = "sig1.cms";
     my $out1 = "out1.txt";
     my $sig2 = "sig2.cms";
     my $out2 = "out2.txt";
+    my $signers = "signers.pem";

     ok(run(app(['openssl', 'cms',
                 @defaultprov,
@@ -1898,9 +1899,14 @@ subtest "sign and verify with multiple keys and -verify_partial" => sub {
                 '-verify', '-in', $sig2, '-inform', 'DER',
                 '-CAfile', $smrsa1, '-partial_chain',
                 '-verify_partial',
+                '-signer', $signers,
                 '-out', $out2,
                ])),
        "verify two signatures with only rsa1");
+    # only the certificate of the verified signature may be written out
+    is(scalar(grep { /^-----BEGIN CERTIFICATE-----/ }
+              do { open(my $fh, '<', $signers) or die; <$fh> }), 1,
+       "only the verified signer is output");

     # because the smrsa2 signature can be verified, overall verification succeeds
     ok(run(app(['openssl', 'cms',