Commit 4ea4a71361 for openssl.org

commit 4ea4a7136112c135229e6a73fc301f54c617313b
Author: Dmitry Belyavskiy <beldmit@gmail.com>
Date:   Wed Sep 9 18:10:06 2026 +0200

    Regression test for PKCS12_parse

    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Reviewed-by: Simo Sorce <simo@redhat.com>
    Reviewed-by: Todd Short <todd.short@me.com>
    Merge-date: Tue Sep 15 15:23:48 2026
    Merged-from: https://github.com/openssl/openssl/pull/32773

diff --git a/doc/man3/PKCS12_parse.pod b/doc/man3/PKCS12_parse.pod
index 834af687a4..cb63616d10 100644
--- a/doc/man3/PKCS12_parse.pod
+++ b/doc/man3/PKCS12_parse.pod
@@ -23,13 +23,21 @@ certificate to B<*cert> and any additional certificates to B<*ca>.

 Each of the parameters B<pkey>, B<cert>, and B<ca> can be NULL in which case
 the private key, the corresponding certificate, or the additional certificates,
-respectively, will be discarded.
+respectively, will not be returned to the caller.
 If any of B<pkey> and B<cert> is non-NULL the variable it points to is
 initialized.
 If B<ca> is non-NULL and B<*ca> is NULL a new STACK will be allocated.
 If B<ca> is non-NULL and B<*ca> is a valid STACK
 then additional certificates are appended in the given order to B<*ca>.

+The corresponding certificate is identified by matching it against the private
+key. This means that B<*cert> is only set when both B<pkey> and B<cert> are
+non-NULL and a private key was found.
+When B<pkey> is NULL or B<cert> is NULL, no matching is performed and the
+certificate that would have been the corresponding one is treated as an
+additional certificate: it is added to B<*ca> if B<ca> is non-NULL,
+or freed otherwise.
+
 The B<friendlyName> and B<localKeyID> attributes (if present) on each
 certificate will be stored in the B<alias> and B<keyid> attributes of the
 B<X509> structure.
diff --git a/test/pkcs12_api_test.c b/test/pkcs12_api_test.c
index b615c9d7a3..2a8105a892 100644
--- a/test/pkcs12_api_test.c
+++ b/test/pkcs12_api_test.c
@@ -60,6 +60,10 @@ static const char *in_pass = "";
 static int has_key = 0;
 static int has_cert = 0;
 static int has_ca = 0;
+static int expected_ca_count = -1;
+static const char *expected_cert_file = NULL;
+static const char *expected_ca_file = NULL;
+static int mismatched_key_pass = 0;

 static int changepass(PKCS12 *p12, EVP_PKEY *key, X509 *cert, STACK_OF(X509) *ca)
 {
@@ -108,6 +112,9 @@ static int pkcs12_parse_test(void)
     X509 *cert = NULL;
     STACK_OF(X509) *ca = NULL;

+    if (mismatched_key_pass)
+        return TEST_skip("not applicable with mismatched key password");
+
     if (in_file != NULL) {
         p12 = PKCS12_load(in_file);
         if (!TEST_ptr(p12))
@@ -134,6 +141,80 @@ err:
     return TEST_true(ret);
 }

+static int test_parse_combinations(int idx)
+{
+    int ret = 0;
+    PKCS12 *p12 = NULL;
+    EVP_PKEY *key = NULL;
+    X509 *cert = NULL;
+    STACK_OF(X509) *ca = NULL;
+    int want_key = (idx >> 2) & 1;
+    int want_cert = (idx >> 1) & 1;
+    int want_ca = idx & 1;
+
+    if (in_file == NULL || !has_key || !has_cert)
+        return 1;
+
+    if (!TEST_int_ge(expected_ca_count, 0))
+        return TEST_skip("test_parse_combinations requires -ca-count parameter");
+
+    TEST_info("combination %d: want_key=%d want_cert=%d want_ca=%d",
+        idx, want_key, want_cert, want_ca);
+
+    if (!TEST_ptr(p12 = PKCS12_load(in_file)))
+        goto err;
+    if (!TEST_true(PKCS12_parse(p12, in_pass,
+            want_key ? &key : NULL,
+            want_cert ? &cert : NULL,
+            want_ca ? &ca : NULL)))
+        goto err;
+
+    if (want_key) {
+        if (!TEST_ptr(key))
+            goto err;
+    }
+
+    if (want_cert) {
+        /*
+         * PKCS12_parse only sets *cert when the key is also requested and
+         * found, because it matches certs against *pkey.
+         */
+        if (want_key) {
+            if (!TEST_ptr(cert))
+                goto err;
+        } else {
+            if (!TEST_ptr_null(cert))
+                goto err;
+        }
+    }
+
+    if (want_ca) {
+        int actual_ca_count = ca == NULL ? 0 : sk_X509_num(ca);
+        int expected_count = expected_ca_count;
+
+        /*
+         * The matching cert is only excluded from the CA stack when both
+         * key and cert pointers are provided.  Otherwise it ends up in CA.
+         */
+        if (!want_key || !want_cert)
+            expected_count++;
+
+        if (!TEST_int_eq(actual_ca_count, expected_count))
+            goto err;
+    }
+
+    ret = 1;
+err:
+    if (!ret)
+        TEST_info("failed combination %d: want_key=%d want_cert=%d want_ca=%d",
+            idx, want_key, want_cert, want_ca);
+    PKCS12_free(p12);
+    EVP_PKEY_free(key);
+    X509_free(cert);
+    OSSL_STACK_OF_X509_free(ca);
+    return ret;
+}
+
 /*
  * If appending an additional certificate to the CA stack fails,
  * PKCS12_parse() should free its own allocated CA stack.
@@ -309,6 +390,10 @@ typedef enum OPTION_choice {
     OPT_IN_HAS_KEY,
     OPT_IN_HAS_CERT,
     OPT_IN_HAS_CA,
+    OPT_CA_COUNT,
+    OPT_EXPECTED_CERT,
+    OPT_EXPECTED_CA,
+    OPT_MISMATCHED_P12,
     OPT_LEGACY,
     OPT_TEST_ENUM
 } OPTION_CHOICE;
@@ -322,6 +407,10 @@ const OPTIONS *test_get_options(void)
         { "has-key", OPT_IN_HAS_KEY, 'n', "Whether the input file does contain an user key" },
         { "has-cert", OPT_IN_HAS_CERT, 'n', "Whether the input file does contain an user certificate" },
         { "has-ca", OPT_IN_HAS_CA, 'n', "Whether the input file does contain other certificate" },
+        { "ca-count", OPT_CA_COUNT, 'n', "Expected number of CA certificates" },
+        { "expected-cert", OPT_EXPECTED_CERT, '<', "PEM file of expected main certificate" },
+        { "expected-ca", OPT_EXPECTED_CA, '<', "PEM file of expected CA certificates in order" },
+        { "mismatched-key-pass", OPT_MISMATCHED_P12, '-', "Input has key encrypted with a different password" },
         { "legacy", OPT_LEGACY, '-', "Test the legacy APIs" },
         { NULL }
     };
@@ -336,6 +425,8 @@ static int test_PKCS12_set_pbmac1_pbkdf2_saltlen_zero(void)
     STACK_OF(X509) *ca = NULL;
     PKCS12 *p12 = NULL;

+    if (mismatched_key_pass)
+        return TEST_skip("not applicable with mismatched key password");
     if (!TEST_ptr(p12 = PKCS12_load(in_file)))
         return 0;
     if (!TEST_true(PKCS12_parse(p12, in_pass, &key, &cert, &ca)))
@@ -364,6 +455,8 @@ static int test_PKCS12_set_pbmac1_pbkdf2_invalid_saltlen(void)
     STACK_OF(X509) *ca = NULL;
     PKCS12 *p12 = NULL;

+    if (mismatched_key_pass)
+        return TEST_skip("not applicable with mismatched key password");
     if (!TEST_ptr(p12 = PKCS12_load(in_file)))
         return 0;
     if (!TEST_true(PKCS12_parse(p12, in_pass, &key, &cert, &ca)))
@@ -384,6 +477,139 @@ err:
     return ret;
 }

+static int test_parse_cert_placement(void)
+{
+    int ret = 0, i;
+    PKCS12 *p12 = NULL;
+    EVP_PKEY *key = NULL;
+    X509 *cert = NULL, *exp_cert = NULL, *x = NULL;
+    STACK_OF(X509) *ca = NULL, *exp_ca = NULL;
+    BIO *bio = NULL;
+
+    if (in_file == NULL
+        || (expected_cert_file == NULL && expected_ca_file == NULL))
+        return 1;
+
+    p12 = PKCS12_load(in_file);
+    if (!TEST_ptr(p12))
+        goto err;
+
+    if (!TEST_true(PKCS12_parse(p12, in_pass, &key, &cert, &ca)))
+        goto err;
+
+    if (has_key && !TEST_ptr(key))
+        goto err;
+
+    if (expected_cert_file != NULL) {
+        bio = BIO_new_file(expected_cert_file, "rb");
+        if (!TEST_ptr(bio))
+            goto err;
+        exp_cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
+        BIO_free(bio);
+        bio = NULL;
+        if (!TEST_ptr(exp_cert))
+            goto err;
+        if (!TEST_ptr(cert))
+            goto err;
+        if (!TEST_int_eq(X509_cmp(cert, exp_cert), 0)) {
+            TEST_info("main cert does not match expected cert");
+            goto err;
+        }
+    }
+
+    if (expected_ca_file != NULL) {
+        int actual_count, expected_count;
+
+        exp_ca = sk_X509_new_null();
+        if (!TEST_ptr(exp_ca))
+            goto err;
+
+        bio = BIO_new_file(expected_ca_file, "rb");
+        if (!TEST_ptr(bio))
+            goto err;
+        while ((x = PEM_read_bio_X509(bio, NULL, NULL, NULL)) != NULL) {
+            if (!sk_X509_push(exp_ca, x)) {
+                X509_free(x);
+                x = NULL;
+                goto err;
+            }
+            x = NULL;
+        }
+        ERR_clear_error();
+        BIO_free(bio);
+        bio = NULL;
+
+        actual_count = ca == NULL ? 0 : sk_X509_num(ca);
+        expected_count = sk_X509_num(exp_ca);
+
+        if (!TEST_int_eq(actual_count, expected_count))
+            goto err;
+
+        for (i = 0; i < expected_count; i++) {
+            if (!TEST_int_eq(X509_cmp(sk_X509_value(ca, i),
+                                 sk_X509_value(exp_ca, i)),
+                    0)) {
+                TEST_info("CA cert mismatch at index %d", i);
+                goto err;
+            }
+        }
+    }
+
+    ret = 1;
+err:
+    PKCS12_free(p12);
+    EVP_PKEY_free(key);
+    X509_free(cert);
+    OSSL_STACK_OF_X509_free(ca);
+    X509_free(exp_cert);
+    OSSL_STACK_OF_X509_free(exp_ca);
+    BIO_free(bio);
+    return ret;
+}
+
+/*
+ * Load a pre-built PKCS#12 whose MAC and cert use one password but whose
+ * private key is encrypted with a different one.  Parsing without requesting
+ * the key must succeed (cert lands in the CA stack); requesting it must fail.
+ */
+static int test_parse_mismatched_key_password(void)
+{
+    int ret = 0;
+    PKCS12 *p12 = NULL;
+    EVP_PKEY *key = NULL;
+    X509 *cert = NULL;
+    STACK_OF(X509) *ca = NULL;
+
+    if (in_file == NULL || !mismatched_key_pass)
+        return 1;
+
+    p12 = PKCS12_load(in_file);
+    if (!TEST_ptr(p12))
+        goto err;
+
+    /* Omitting the key pointer succeeds; cert lands in the CA stack */
+    if (!TEST_true(PKCS12_parse(p12, in_pass, NULL, NULL, &ca)))
+        goto err;
+    if (!TEST_ptr(ca) || !TEST_int_eq(sk_X509_num(ca), 1))
+        goto err;
+    OSSL_STACK_OF_X509_free(ca);
+    ca = NULL;
+
+    /* Requesting the key fails: wrong password for the shrouded key bag */
+    ERR_set_mark();
+    if (!TEST_false(PKCS12_parse(p12, in_pass, &key, &cert, &ca)))
+        goto err;
+    ERR_pop_to_mark();
+
+    ret = 1;
+err:
+    PKCS12_free(p12);
+    EVP_PKEY_free(key);
+    X509_free(cert);
+    OSSL_STACK_OF_X509_free(ca);
+    return ret;
+}
+
 int setup_tests(void)
 {
     OPTION_CHOICE o;
@@ -407,6 +633,18 @@ int setup_tests(void)
         case OPT_IN_HAS_CA:
             has_ca = opt_int_arg();
             break;
+        case OPT_CA_COUNT:
+            expected_ca_count = opt_int_arg();
+            break;
+        case OPT_EXPECTED_CERT:
+            expected_cert_file = opt_arg();
+            break;
+        case OPT_EXPECTED_CA:
+            expected_ca_file = opt_arg();
+            break;
+        case OPT_MISMATCHED_P12:
+            mismatched_key_pass = 1;
+            break;
         case OPT_TEST_CASES:
             break;
         default:
@@ -422,6 +660,9 @@ int setup_tests(void)

     ADD_TEST(test_null_args);
     ADD_TEST(pkcs12_parse_test);
+    ADD_ALL_TESTS(test_parse_combinations, 8);
+    ADD_TEST(test_parse_cert_placement);
+    ADD_TEST(test_parse_mismatched_key_password);
     ADD_MFAIL_NO_CHECK_TEST(pkcs12_parse_mfail_test);
     ADD_MFAIL_NO_CHECK_TEST(pkcs12_parse_existing_ca_mfail_test);
     ADD_ALL_TESTS(pkcs12_create_ex2_test, 3);
diff --git a/test/recipes/80-test_pkcs12.t b/test/recipes/80-test_pkcs12.t
index ad38af5ba0..0b553a7585 100644
--- a/test/recipes/80-test_pkcs12.t
+++ b/test/recipes/80-test_pkcs12.t
@@ -57,7 +57,7 @@ $ENV{OPENSSL_WIN32_UTF8}=1;
 my $no_fips = disabled('fips') || ($ENV{NO_FIPS} // 0);
 my $no_err =  disabled('err') || disabled('autoerrinit');

-plan tests => 65 + ($no_fips ? 0 : 5);
+plan tests => 68 + ($no_fips ? 0 : 5);

 # Test different PKCS#12 formats
 ok(run(test(["pkcs12_format_test"])), "test pkcs12 formats");
@@ -399,6 +399,7 @@ ok(run(test(["pkcs12_api_test",
              "-has-ca", 1,
              "-has-key", 1,
              "-has-cert", 1,
+             "-ca-count", 1,
              ])), "Test pkcs12_parse()");

 ok(run(test(["pkcs12_api_test",
@@ -412,8 +413,50 @@ ok(run(test(["pkcs12_api_test",
              "-has-ca", 1,
              "-has-key", 1,
              "-has-cert", 1,
+             "-ca-count", 1,
              ])), "Test pkcs12_parse()");

+# Test PKCS12_parse cert placement: two certs sharing a key + unrelated cert.
+# The cert from -in should be returned as the main cert; the other cert
+# matching the key (from -certfile) should go to the CA stack.
+{
+    my $extra_certs = "extra_certs.pem";
+    open(my $out, '>', $extra_certs) or die "Cannot create $extra_certs: $!";
+    for my $f (srctop_file(@path, "ee-cert2.pem"),
+               srctop_file(@path, "ca-cert.pem")) {
+        open(my $in, '<', $f) or die "Cannot read $f: $!";
+        print $out $_ while <$in>;
+        close $in;
+    }
+    close $out;
+
+    my $twocert_p12 = "twocert.p12";
+    ok(run(app(["openssl", "pkcs12", "-export",
+                "-inkey", srctop_file(@path, "ee-key.pem"),
+                "-in", srctop_file(@path, "ee-cert.pem"),
+                "-certfile", $extra_certs,
+                "-passout", "pass:", "-nomac", "-out", $twocert_p12])),
+       "export PKCS#12 with two certs sharing a key and unrelated cert");
+
+    ok(run(test(["pkcs12_api_test",
+                 "-in", $twocert_p12,
+                 "-has-key", 1,
+                 "-has-cert", 1,
+                 "-has-ca", 1,
+                 "-ca-count", 2,
+                 "-expected-cert", srctop_file(@path, "ee-cert.pem"),
+                 "-expected-ca", $extra_certs,
+                 ])), "Test PKCS12_parse cert placement with shared key");
+}
+
+# Test PKCS12_parse with a key encrypted using a different password than the MAC.
+# Omitting the key succeeds; requesting it fails.
+ok(run(test(["pkcs12_api_test",
+             "-in", srctop_file("test", "recipes", "80-test_pkcs12_data",
+                                "mismatched_key_pass.p12"),
+             "-mismatched-key-pass",
+             ])), "Test PKCS12_parse with mismatched key password");
+
 # Test against CVE-2025-69421, octet parameter is expected, but
 # NULL is being received and dereferenced

diff --git a/test/recipes/80-test_pkcs12_data/mismatched_key_pass.p12 b/test/recipes/80-test_pkcs12_data/mismatched_key_pass.p12
new file mode 100644
index 0000000000..d5fb51ae40
Binary files /dev/null and b/test/recipes/80-test_pkcs12_data/mismatched_key_pass.p12 differ