Commit 4dcc3b19d1 for openssl.org

commit 4dcc3b19d1f6c81b9a847560869defe33f73b4f7
Author: slontis <shane.lontis@oracle.com>
Date:   Wed Sep 13 10:26:47 2023 +1000

    Add PKCS12_check_version() API

    There was no check for the version = 3 when loading a PKCS #12 file.
    Rather than adding multiple _ex() functions that support checking
    the version number, a separate API has been added to do the check,
    which can be called after one of the d2i_PKCS12 functions.
    The check has been added to the openssl pkcs12 command.
    It is checked by default and can be disabled by using the option
    -noversioncheck.

    Resolves: https://github.com/openssl/openssl/issues/22030
    Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    Merge-date: Fri Oct 30 11:43:02 2026
    Merged-from: https://github.com/openssl/openssl/pull/22087

diff --git a/CHANGES.md b/CHANGES.md
index a414f7076d..f6cb395529 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -128,6 +128,13 @@ OpenSSL 4.2

    *Dmitry Belyavskiy*

+ * Added `PKCS12_check_version()` API function, that verifies that the loaded
+   PKCS #12 file has its version in accordance with [RFC 7292 Section 4].
+   The check has been enabled in the `openssl pkcs12` command by default,
+   and can be disabled using the newly introduced `-noversioncheck` option.
+
+   *Shane Lontis*
+
  * Added the ASCON-AEAD128 cipher as specified in NIST SP 800-232. ASCON-AEAD128
    provides authenticated encryption with associated data (AEAD) using 128-bit
    keys, nonces, and tags. The cipher is available through the EVP interface and
@@ -24642,6 +24649,7 @@ ndif
 [RFC 5297]: https://datatracker.ietf.org/doc/html/rfc5297
 [RFC 5652]: https://datatracker.ietf.org/doc/html/rfc5652
 [RFC 7250]: https://datatracker.ietf.org/doc/html/rfc7250
+[RFC 7292 Section 4]: https://datatracker.ietf.org/doc/html/rfc7292#section-4
 [RFC 7685]: https://datatracker.ietf.org/doc/html/rfc7685
 [RFC 7919]: https://datatracker.ietf.org/doc/html/rfc7919
 [RFC 8422]: https://datatracker.ietf.org/doc/html/rfc8422
diff --git a/apps/pkcs12.c b/apps/pkcs12.c
index 841eb8f98c..96d04b1868 100644
--- a/apps/pkcs12.c
+++ b/apps/pkcs12.c
@@ -110,6 +110,7 @@ typedef enum OPTION_choice {
     OPT_JDKTRUST,
     OPT_PBMAC1_PBKDF2,
     OPT_PBMAC1_PBKDF2_MD,
+    OPT_NOVERSIONCHECK,
 #ifndef OPENSSL_NO_DES
     OPT_LEGACY_ALG
 #endif
@@ -192,6 +193,8 @@ const OPTIONS pkcs12_options[] = {
     { "macsaltlen", OPT_MACSALTLEN, 'p', "Specify the salt len for MAC" },
     { "nomac", OPT_NOMAC, '-', "Don't generate MAC" },
     { "jdktrust", OPT_JDKTRUST, 's', "Mark certificate in PKCS #12 store as trusted for JDK compatibility" },
+    { "noversioncheck", OPT_NOVERSIONCHECK, '-',
+        "Don't check the loaded PKCS #12 version info is valid" },
     { NULL }
 };

@@ -207,6 +210,7 @@ int pkcs12_main(int argc, char **argv)
 #ifndef OPENSSL_NO_DES
     int use_legacy = 0;
 #endif
+    int version_check = 1;
     /* use library defaults for the iter, maciter, cert, and key PBE */
     int iter = 0, maciter = 0, pbmac1_pbkdf2 = 0;
     int macsaltlen = PKCS12_SALT_LEN;
@@ -404,6 +408,9 @@ int pkcs12_main(int argc, char **argv)
             use_legacy = 1;
             break;
 #endif
+        case OPT_NOVERSIONCHECK:
+            version_check = 0;
+            break;
         case OPT_PROV_CASES:
             if (!opt_provider(o))
                 goto end;
@@ -788,6 +795,10 @@ int pkcs12_main(int argc, char **argv)
         ERR_print_errors(bio_err);
         goto end;
     }
+    if (version_check && !PKCS12_check_version(p12)) {
+        BIO_printf(bio_err, "PKCS #12 file contains an invalid version number\n");
+        goto end;
+    }

     if (!noprompt) {
         if (1) {
diff --git a/crypto/pkcs12/p12_init.c b/crypto/pkcs12/p12_init.c
index fe264a89d6..8ad78e6dd1 100644
--- a/crypto/pkcs12/p12_init.c
+++ b/crypto/pkcs12/p12_init.c
@@ -13,8 +13,10 @@
 #include "crypto/pkcs7.h"
 #include "p12_local.h"

-/* Initialise a PKCS12 structure to take data */
+/* Refer to https://datatracker.ietf.org/doc/html/rfc7292#section-4 */
+#define PKCS12_VERSION 3

+/* Initialise a PKCS12 structure to take data */
 PKCS12 *PKCS12_init_ex(int mode, OSSL_LIB_CTX *ctx, const char *propq)
 {
     PKCS12 *pkcs12;
@@ -23,7 +25,7 @@ PKCS12 *PKCS12_init_ex(int mode, OSSL_LIB_CTX *ctx, const char *propq)
         ERR_raise(ERR_LIB_PKCS12, ERR_R_ASN1_LIB);
         return NULL;
     }
-    if (!ASN1_INTEGER_set(pkcs12->version, 3))
+    if (!ASN1_INTEGER_set(pkcs12->version, PKCS12_VERSION))
         goto err;
     pkcs12->authsafes->type = OBJ_nid2obj(mode);

@@ -51,6 +53,13 @@ err:
     return NULL;
 }

+int PKCS12_check_version(const PKCS12 *p12)
+{
+    if (p12 == NULL)
+        return 0;
+    return ASN1_INTEGER_get(p12->version) == PKCS12_VERSION;
+}
+
 PKCS12 *PKCS12_init(int mode)
 {
     return PKCS12_init_ex(mode, NULL, NULL);
diff --git a/demos/pkcs12/pkread.c b/demos/pkcs12/pkread.c
index 29bd9b21e1..d26f890370 100644
--- a/demos/pkcs12/pkread.c
+++ b/demos/pkcs12/pkread.c
@@ -13,7 +13,7 @@
 #include <openssl/err.h>
 #include <openssl/pkcs12.h>

-/* Simple PKCS#12 file reader */
+/* Simple PKCS #12 file reader */

 static char *find_friendly_name(PKCS12 *p12)
 {
@@ -67,12 +67,17 @@ int main(int argc, char **argv)
     p12 = d2i_PKCS12_fp(fp, NULL);
     fclose(fp);
     if (p12 == NULL) {
-        fprintf(stderr, "Error reading PKCS#12 file\n");
+        fprintf(stderr, "Error reading PKCS #12 file\n");
+        ERR_print_errors_fp(stderr);
+        goto err;
+    }
+    if (!PKCS12_check_version(p12)) {
+        fprintf(stderr, "PKCS #12 file has incorrect version\n");
         ERR_print_errors_fp(stderr);
         goto err;
     }
     if (!PKCS12_parse(p12, argv[2], &pkey, &cert, &ca)) {
-        fprintf(stderr, "Error parsing PKCS#12 file\n");
+        fprintf(stderr, "Error parsing PKCS #12 file\n");
         ERR_print_errors_fp(stderr);
         goto err;
     }
diff --git a/doc/build.info b/doc/build.info
index 2f400822d7..3ee1e0d8ee 100644
--- a/doc/build.info
+++ b/doc/build.info
@@ -1963,6 +1963,10 @@ DEPEND[html/man3/PKCS12_add_safe.html]=man3/PKCS12_add_safe.pod
 GENERATE[html/man3/PKCS12_add_safe.html]=man3/PKCS12_add_safe.pod
 DEPEND[man/man3/PKCS12_add_safe.3]=man3/PKCS12_add_safe.pod
 GENERATE[man/man3/PKCS12_add_safe.3]=man3/PKCS12_add_safe.pod
+DEPEND[html/man3/PKCS12_check_version.html]=man3/PKCS12_check_version.pod
+GENERATE[html/man3/PKCS12_check_version.html]=man3/PKCS12_check_version.pod
+DEPEND[man/man3/PKCS12_check_version.3]=man3/PKCS12_check_version.pod
+GENERATE[man/man3/PKCS12_check_version.3]=man3/PKCS12_check_version.pod
 DEPEND[html/man3/PKCS12_create.html]=man3/PKCS12_create.pod
 GENERATE[html/man3/PKCS12_create.html]=man3/PKCS12_create.pod
 DEPEND[man/man3/PKCS12_create.3]=man3/PKCS12_create.pod
@@ -3570,6 +3574,7 @@ html/man3/PKCS12_add_cert.html \
 html/man3/PKCS12_add_friendlyname_asc.html \
 html/man3/PKCS12_add_localkeyid.html \
 html/man3/PKCS12_add_safe.html \
+html/man3/PKCS12_check_version.html \
 html/man3/PKCS12_create.html \
 html/man3/PKCS12_decrypt_secretbag.html \
 html/man3/PKCS12_decrypt_skey.html \
@@ -4252,6 +4257,7 @@ man/man3/PKCS12_add_cert.3 \
 man/man3/PKCS12_add_friendlyname_asc.3 \
 man/man3/PKCS12_add_localkeyid.3 \
 man/man3/PKCS12_add_safe.3 \
+man/man3/PKCS12_check_version.3 \
 man/man3/PKCS12_create.3 \
 man/man3/PKCS12_decrypt_secretbag.3 \
 man/man3/PKCS12_decrypt_skey.3 \
diff --git a/doc/man1/openssl-pkcs12.pod.in b/doc/man1/openssl-pkcs12.pod.in
index 8cd57c3812..7674d0bc59 100644
--- a/doc/man1/openssl-pkcs12.pod.in
+++ b/doc/man1/openssl-pkcs12.pod.in
@@ -44,6 +44,7 @@ PKCS#12 input (parsing) options:
 [B<-idea>]
 [B<-noenc>]
 [B<-nodes>]
+[B<-noversioncheck>]

 PKCS#12 output (export) options:

@@ -143,6 +144,11 @@ No certificates will be output.
 This option inhibits all credentials output,
 and so the input is just verified.

+=item B<-noversioncheck>
+
+This option disables checking of the version information.
+This should not be required, but is supplied for backwards compatibility.
+
 =item B<-legacy>

 Use legacy mode of operation and automatically load the legacy provider.
@@ -489,10 +495,13 @@ L<ossl_store-file(7)>
 =head1 HISTORY

 The B<-nodes> option was deprecated in OpenSSL 3.0, too; use B<-noenc> instead.
+
 The B<-macsaltlen> option default changed from 8 to 16 bytes in OpenSSL 3.6.

 The B<-engine> option was removed in OpenSSL 4.0.

+The B<-noversioncheck> option was added in OpenSSL 4.2.
+
 =head1 COPYRIGHT

 Copyright 2000-2026 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/doc/man3/PKCS12_check_version.pod b/doc/man3/PKCS12_check_version.pod
new file mode 100644
index 0000000000..407a5d72de
--- /dev/null
+++ b/doc/man3/PKCS12_check_version.pod
@@ -0,0 +1,59 @@
+=pod
+
+=head1 NAME
+
+PKCS12_check_version - Checks the version of a PKCS #12 structure
+
+=head1 SYNOPSIS
+
+ #include <openssl/pkcs12.h>
+
+ int PKCS12_check_version(const PKCS12 *p12);
+
+=head1 DESCRIPTION
+
+PKCS12_check_version() checks that the version info of a I<p12> PKCS #12 object
+is correct.
+
+=head1 NOTES
+
+A PKCS12 object can be loaded using d2i_PKC12(), d2i_PKC12_bio()
+or d2i_PKC12_fp().  The loaded object contains a version number, which should
+have the value of 3, in accordance with RFC 7292.
+As this version number was not previously checked on loading,
+adding the check to these functions would potentially break existing
+PKCS12 files. Instead of adding an B<_ex> variant of all these functions,
+the function PKCS12_check_version() has been added instead.
+PKCS12_check_version() should be called after loading a PKCS #12
+object, before calling either PKCS12_parse() or PKCS12_unpack_authsafes().
+
+=head1 RETURN VALUES
+
+PKCS12_check_version() returns 1 if the version info of a I<p12> object
+is correct, otherwise it returns 0.
+
+=head1 CONFORMING TO
+
+IETF RFC 7292 (L<https://tools.ietf.org/html/rfc7292>).
+
+=head1 SEE ALSO
+
+d2i_PKCS12(3),
+d2i_PKC12_bio(3),
+d2i_PKC12_fp(3),
+PKCS12_parse(3).
+
+=head1 HISTORY
+
+PKCS12_check_version() was added in OpenSSL 4.2.
+
+=head1 COPYRIGHT
+
+Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+
+Licensed under the Apache License 2.0 (the "License").  You may not use
+this file except in compliance with the License.  You can obtain a copy
+in the file LICENSE in the source distribution or at
+L<https://www.openssl.org/source/license.html>.
+
+=cut
diff --git a/include/openssl/pkcs12.h.in b/include/openssl/pkcs12.h.in
index 1c470f15e0..2e056aa26c 100644
--- a/include/openssl/pkcs12.h.in
+++ b/include/openssl/pkcs12.h.in
@@ -322,6 +322,7 @@ int PKCS12_parse_ex(PKCS12 *p12, const char *pass,

 int PKCS12_parse(PKCS12 *p12, const char *pass, EVP_PKEY **pkey, X509 **cert,
     STACK_OF(X509) **ca);
+int PKCS12_check_version(const PKCS12 *p12);
 typedef int PKCS12_create_cb(PKCS12_SAFEBAG *bag, void *cbarg);
 PKCS12 *PKCS12_create(const char *pass, const char *name, EVP_PKEY *pkey,
     X509 *cert, STACK_OF(X509) *ca, int nid_key, int nid_cert,
diff --git a/test/pkcs12_api_test.c b/test/pkcs12_api_test.c
index 940b7625f0..e9a544b61b 100644
--- a/test/pkcs12_api_test.c
+++ b/test/pkcs12_api_test.c
@@ -60,6 +60,7 @@ static const char *in_pass = "";
 static int has_key = 0;
 static int has_cert = 0;
 static int has_ca = 0;
+static int version_pass = 1;
 static int expected_ca_count = -1;
 static const char *expected_cert_file = NULL;
 static const char *expected_ca_file = NULL;
@@ -122,6 +123,8 @@ static int pkcs12_parse_test(void)
         if (!TEST_ptr(p12))
             goto err;

+        if (!TEST_int_eq(PKCS12_check_version(p12), version_pass))
+            goto err;
         if (!TEST_true(PKCS12_parse(p12, in_pass, &key, &cert, &ca)))
             goto err;

@@ -559,6 +562,25 @@ err:
     return ret;
 }

+static int pkcs12_check_version_test(void)
+{
+    int ret = 0;
+    PKCS12 *p12 = NULL;
+
+    if (in_file != NULL) {
+        p12 = PKCS12_load(in_file);
+        if (!TEST_ptr(p12))
+            goto err;
+
+        if (!TEST_int_eq(PKCS12_check_version(p12), version_pass))
+            goto err;
+    }
+    ret = 1;
+err:
+    PKCS12_free(p12);
+    return ret;
+}
+
 typedef enum OPTION_choice {
     OPT_ERR = -1,
     OPT_EOF = 0,
@@ -574,6 +596,7 @@ typedef enum OPTION_choice {
     OPT_MISMATCHED_P12,
     OPT_IN_NUM_SKEYS,
     OPT_LEGACY,
+    OPT_BAD_VERSION,
     OPT_TEST_ENUM
 } OPTION_CHOICE;

@@ -593,6 +616,7 @@ const OPTIONS *test_get_options(void)
         { "mismatched-key-pass", OPT_MISMATCHED_P12, '-', "Input has key encrypted with a different password" },
         { "num-skeys", OPT_IN_NUM_SKEYS, 'n', "Number of symmetric keys in the input file" },
         { "legacy", OPT_LEGACY, '-', "Test the legacy APIs" },
+        { "badversion", OPT_BAD_VERSION, '-', "Test is expected to fail the version info check" },
         { NULL }
     };
     return options;
@@ -844,6 +868,9 @@ int setup_tests(void)
         case OPT_IN_NUM_SKEYS:
             num_skeys = opt_int_arg();
             break;
+        case OPT_BAD_VERSION:
+            version_pass = 0;
+            break;
         case OPT_TEST_CASES:
             break;
         default:
@@ -857,19 +884,22 @@ int setup_tests(void)
         return 0;
     }

-    ADD_TEST(test_null_args);
-    ADD_TEST(pkcs12_parse_test);
-    ADD_ALL_TESTS(test_parse_combinations, 8);
-    ADD_ALL_TESTS(test_parse_cert_placement, 8);
-    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);
-    ADD_TEST(test_PKCS12_set_pbmac1_pbkdf2_saltlen_zero);
-    ADD_TEST(test_PKCS12_set_pbmac1_pbkdf2_invalid_saltlen);
-    ADD_TEST(test_parse_ex_skey);
-    ADD_ALL_TESTS(test_parse_ex_libctx, 2);
-    ADD_ALL_TESTS(test_parse_ex_skey_libctx, 4);
+    ADD_TEST(pkcs12_check_version_test);
+    if (version_pass) {
+        ADD_TEST(test_null_args);
+        ADD_TEST(pkcs12_parse_test);
+        ADD_ALL_TESTS(test_parse_combinations, 8);
+        ADD_ALL_TESTS(test_parse_cert_placement, 8);
+        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);
+        ADD_TEST(test_PKCS12_set_pbmac1_pbkdf2_saltlen_zero);
+        ADD_TEST(test_PKCS12_set_pbmac1_pbkdf2_invalid_saltlen);
+        ADD_TEST(test_parse_ex_skey);
+        ADD_ALL_TESTS(test_parse_ex_libctx, 2);
+        ADD_ALL_TESTS(test_parse_ex_skey_libctx, 4);
+    }
     return 1;
 }

diff --git a/test/recipes/80-test_pkcs12.t b/test/recipes/80-test_pkcs12.t
index deea098c8b..557b886e10 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 => 119 + ($no_fips ? 0 : 5);
+plan tests => 122 + ($no_fips ? 0 : 5);

 # Test different PKCS#12 formats
 ok(run(test(["pkcs12_format_test"])), "test pkcs12 formats");
@@ -892,4 +892,23 @@ ok(run(test(["pkcs12_api_test",
     ok(scalar @skey_matches == 2, "OSSL_STORE output shows two symmetric keys");
 }

+my $badversionfile = srctop_file("test", "recipes", "80-test_pkcs12_data",
+                                 "bad_version.p12");
+
+ok(run(app(["openssl", "pkcs12", "-info", "-noout",
+            "-in", $badversionfile,
+            "-noversioncheck",
+            "-passin", "pass:password"])),
+   "Test that a PKCS #12 file with a bad version and -noversioncheck passes");
+ok(!run(app(["openssl", "pkcs12", "-info", "-noout",
+            "-in", $badversionfile,
+            "-passin", "pass:password"])),
+    "Test that a PKCS #12 file with a bad version fails");
+
+ok(run(test(["pkcs12_api_test",
+             "-in", $badversionfile,
+             "-pass", "password",
+             "-badversion"])),
+   "Test PKCS #12 version info is invalid");
+
 SetConsoleOutputCP($savedcp) if (defined($savedcp));
diff --git a/test/recipes/80-test_pkcs12_data/bad_version.p12 b/test/recipes/80-test_pkcs12_data/bad_version.p12
new file mode 100644
index 0000000000..96dddf87f9
Binary files /dev/null and b/test/recipes/80-test_pkcs12_data/bad_version.p12 differ
diff --git a/util/libcrypto.num b/util/libcrypto.num
index ab4d1e440b..c9809d8ef2 100644
--- a/util/libcrypto.num
+++ b/util/libcrypto.num
@@ -5747,3 +5747,4 @@ PKCS12_PARSE_CTX_set_cert               ?	4_2_0	EXIST::FUNCTION:
 PKCS12_PARSE_CTX_set_ca                 ?	4_2_0	EXIST::FUNCTION:
 PKCS12_PARSE_CTX_set_skeys              ?	4_2_0	EXIST::FUNCTION:
 PKCS12_parse_ex                         ?	4_2_0	EXIST::FUNCTION:
+PKCS12_check_version                    ?	4_2_0	EXIST::FUNCTION: