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