From 423f7c866705667bc0b32c88dd5592ee47dca864 Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Wed, 27 May 2026 08:50:24 +0200 Subject: [PATCH] Add PKCS#12 --- doc/api_ref/contents.rst | 1 + doc/api_ref/pkcs12.rst | 252 ++++ doc/cli.rst | 30 + src/cli/pkcs12.cpp | 211 +++ src/examples/pkcs12_export.cpp | 33 + src/examples/pkcs12_export_chain.cpp | 43 + src/examples/pkcs12_parse.cpp | 45 + src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.cpp | 9 + src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.h | 17 + src/lib/pkcs12/info.txt | 22 + src/lib/pkcs12/pkcs12.cpp | 938 +++++++++++++ src/lib/pkcs12/pkcs12.h | 254 ++++ src/lib/pkcs12/pkcs12_pbe/info.txt | 23 + src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.cpp | 168 +++ src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.h | 60 + src/scripts/test_cli.py | 138 ++ .../data/pkcs12/cert-aes256cbc-no-key.p12 | Bin 0 -> 605 bytes src/tests/data/pkcs12/cert-key-aes256cbc.p12 | Bin 0 -> 948 bytes src/tests/data/pkcs12/cert-none-key-none.p12 | Bin 0 -> 756 bytes src/tests/data/pkcs12/cert-rc2-key-3des.p12 | Bin 0 -> 854 bytes .../data/pkcs12/envelopeddata_content.pfx | Bin 0 -> 41 bytes src/tests/data/pkcs12/java-truststore.p12 | Bin 0 -> 2845 bytes src/tests/data/pkcs12/key_bag_unencrypted.pfx | Bin 0 -> 654 bytes .../data/pkcs12/key_cert_spki_mismatch.pfx | Bin 0 -> 665 bytes src/tests/data/pkcs12/name-1-no-pwd.p12 | Bin 0 -> 3539 bytes src/tests/data/pkcs12/name-1-pwd.p12 | Bin 0 -> 3639 bytes src/tests/data/pkcs12/name-2-3-no-pwd.p12 | Bin 0 -> 3547 bytes src/tests/data/pkcs12/name-2-3-pwd.p12 | Bin 0 -> 3650 bytes src/tests/data/pkcs12/name-2-no-pwd.p12 | Bin 0 -> 3518 bytes src/tests/data/pkcs12/name-2-pwd.p12 | Bin 0 -> 3618 bytes src/tests/data/pkcs12/name-3-no-pwd.p12 | Bin 0 -> 3518 bytes src/tests/data/pkcs12/name-3-pwd.p12 | Bin 0 -> 3618 bytes src/tests/data/pkcs12/name-all-no-pwd.p12 | Bin 0 -> 3597 bytes src/tests/data/pkcs12/name-all-pwd.p12 | Bin 0 -> 3703 bytes src/tests/data/pkcs12/name-unicode-no-pwd.p12 | Bin 0 -> 3569 bytes src/tests/data/pkcs12/name-unicode-pwd.p12 | Bin 0 -> 3672 bytes src/tests/data/pkcs12/nesting_too_deep.pfx | Bin 0 -> 276 bytes .../data/pkcs12/no-cert-key-aes256cbc.p12 | Bin 0 -> 353 bytes .../data/pkcs12/no-cert-name-2-no-pwd.p12 | Bin 0 -> 2874 bytes src/tests/data/pkcs12/no-cert-name-2-pwd.p12 | Bin 0 -> 2932 bytes .../data/pkcs12/no-cert-name-3-no-pwd.p12 | Bin 0 -> 2874 bytes src/tests/data/pkcs12/no-cert-name-3-pwd.p12 | Bin 0 -> 2932 bytes .../data/pkcs12/no-cert-name-all-no-pwd.p12 | Bin 0 -> 2903 bytes .../data/pkcs12/no-cert-name-all-pwd.p12 | Bin 0 -> 2956 bytes .../pkcs12/no-cert-name-unicode-no-pwd.p12 | Bin 0 -> 2887 bytes .../data/pkcs12/no-cert-name-unicode-pwd.p12 | Bin 0 -> 2940 bytes .../data/pkcs12/no-cert-no-name-no-pwd.p12 | Bin 0 -> 2845 bytes src/tests/data/pkcs12/no-cert-no-name-pwd.p12 | Bin 0 -> 2900 bytes src/tests/data/pkcs12/no-name-no-pwd.p12 | Bin 0 -> 3489 bytes src/tests/data/pkcs12/no-name-pwd.p12 | Bin 0 -> 3586 bytes src/tests/data/pkcs12/no-password.p12 | Bin 0 -> 948 bytes src/tests/data/pkcs12/openssl_3des.p12 | Bin 0 -> 2461 bytes src/tests/data/pkcs12/openssl_aes256.p12 | Bin 0 -> 2603 bytes src/tests/data/pkcs12/pfx_version_2.pfx | Bin 0 -> 43 bytes .../data/pkcs12/safe_contents_bag_nested.pfx | Bin 0 -> 522 bytes src/tests/data/pkcs12/unknown_bag_crl.pfx | Bin 0 -> 62 bytes src/tests/data/pkcs12/unknown_bag_secret.pfx | Bin 0 -> 519 bytes src/tests/test_pkcs12.cpp | 1237 +++++++++++++++++ 58 files changed, 3481 insertions(+) create mode 100644 doc/api_ref/pkcs12.rst create mode 100644 src/cli/pkcs12.cpp create mode 100644 src/examples/pkcs12_export.cpp create mode 100644 src/examples/pkcs12_export_chain.cpp create mode 100644 src/examples/pkcs12_parse.cpp create mode 100644 src/lib/pkcs12/info.txt create mode 100644 src/lib/pkcs12/pkcs12.cpp create mode 100644 src/lib/pkcs12/pkcs12.h create mode 100644 src/lib/pkcs12/pkcs12_pbe/info.txt create mode 100644 src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.cpp create mode 100644 src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.h create mode 100644 src/tests/data/pkcs12/cert-aes256cbc-no-key.p12 create mode 100644 src/tests/data/pkcs12/cert-key-aes256cbc.p12 create mode 100644 src/tests/data/pkcs12/cert-none-key-none.p12 create mode 100644 src/tests/data/pkcs12/cert-rc2-key-3des.p12 create mode 100644 src/tests/data/pkcs12/envelopeddata_content.pfx create mode 100644 src/tests/data/pkcs12/java-truststore.p12 create mode 100644 src/tests/data/pkcs12/key_bag_unencrypted.pfx create mode 100644 src/tests/data/pkcs12/key_cert_spki_mismatch.pfx create mode 100644 src/tests/data/pkcs12/name-1-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-1-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-2-3-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-2-3-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-2-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-2-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-3-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-3-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-all-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-all-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-unicode-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/name-unicode-pwd.p12 create mode 100644 src/tests/data/pkcs12/nesting_too_deep.pfx create mode 100644 src/tests/data/pkcs12/no-cert-key-aes256cbc.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-2-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-2-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-3-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-3-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-all-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-all-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-unicode-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-name-unicode-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-no-name-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-cert-no-name-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-name-no-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-name-pwd.p12 create mode 100644 src/tests/data/pkcs12/no-password.p12 create mode 100644 src/tests/data/pkcs12/openssl_3des.p12 create mode 100644 src/tests/data/pkcs12/openssl_aes256.p12 create mode 100644 src/tests/data/pkcs12/pfx_version_2.pfx create mode 100644 src/tests/data/pkcs12/safe_contents_bag_nested.pfx create mode 100644 src/tests/data/pkcs12/unknown_bag_crl.pfx create mode 100644 src/tests/data/pkcs12/unknown_bag_secret.pfx create mode 100644 src/tests/test_pkcs12.cpp diff --git a/doc/api_ref/contents.rst b/doc/api_ref/contents.rst index a75bd8f46c3..dd9101b122c 100644 --- a/doc/api_ref/contents.rst +++ b/doc/api_ref/contents.rst @@ -16,6 +16,7 @@ API Reference cipher_modes pubkey x509 + pkcs12 tls credentials_manager bigint diff --git a/doc/api_ref/pkcs12.rst b/doc/api_ref/pkcs12.rst new file mode 100644 index 00000000000..b75cbfca90a --- /dev/null +++ b/doc/api_ref/pkcs12.rst @@ -0,0 +1,252 @@ +PKCS#12 +======================================== + +PKCS#12 (also known as PFX) is a file format defined in :rfc:`7292` for +storing cryptographic objects - typically a private key and its associated +X.509 certificate chain - protected by a password. It is widely used for +importing and exporting credentials in TLS servers, browsers, and +certificate management tools. + +This API is defined in ``botan/pkcs12.h``. + +.. versionadded:: 3.13 + +PKCS12_Export_Options +----------------------------------------- + +.. cpp:class:: PKCS12_Export_Options + + Options controlling how a PKCS#12 file is generated. Construct with the + password (mandatory) and an optional friendly name; tweak individual + fields with the chainable ``with_*`` mutators, or pick a preset via the + static pseudo-constructors below. + + .. cpp:function:: explicit PKCS12_Export_Options(std::string_view password, \ + std::optional friendly_name = {}) + + Constructs an options object with modern defaults: PBES2-SHA256-AES256 + key encryption, SHA-256 MAC, 100 000 KDF iterations, certificates + stored unencrypted. + + .. cpp:function:: static PKCS12_Export_Options modern(std::string_view password, \ + std::optional friendly_name = {}) + + Pseudo-constructor for modern defaults (identical to the regular + constructor). Spelled out for clarity at call sites. + + .. cpp:function:: static PKCS12_Export_Options legacy_compat(std::string_view password, \ + std::optional friendly_name = {}) + + Pseudo-constructor for legacy-compatible defaults: PBE-SHA1-3DES key + encryption, SHA-1 MAC, 2 048 KDF iterations. Use when interoperability + with old software (Java keytool pre-2019, legacy OpenSSL releases, + pre-Windows-10) is required. + + .. cpp:function:: PKCS12_Export_Options& with_friendly_name(std::string name) + + Overrides the friendly name. If unset, the bundle-level friendly name + (see :cpp:func:`PKCS12::set_friendly_name`) is used. + + .. cpp:function:: PKCS12_Export_Options& with_iterations(size_t n) + + Sets the KDF iteration count. Values of 0 or above 1 000 000 + (``PKCS12_MAX_ITERATIONS``) cause an ``Invalid_Argument`` exception at + export time. + + .. cpp:function:: PKCS12_Export_Options& with_key_encryption_algo(std::string algo) + + Algorithm used to encrypt the private key (PKCS8ShroudedKeyBag). + Supported values: + + - ``"PBES2-SHA256-AES256"`` - modern (default) + - ``"PBES2-SHA256-AES128"`` - modern + - ``"PBE-SHA1-3DES"`` - legacy + - ``"PBE-SHA1-2DES"`` - legacy + + .. cpp:function:: PKCS12_Export_Options& with_cert_encryption_algo(std::string algo) + + Algorithm used to encrypt certificates. If the algo is empty (default), + certificates are stored unencrypted. A non-empty value requires a + non-empty password. + + .. cpp:function:: PKCS12_Export_Options& with_mac_digest(std::string algo) + + Hash algorithm for the integrity MAC. Supported: ``"SHA-1"``, + ``"SHA-224"``, ``"SHA-256"``, ``"SHA-384"``, ``"SHA-512"``, + ``"SHA-512-256"``. Default is ``"SHA-256"``. + + .. cpp:function:: PKCS12_Export_Options& without_mac() + + Disables the integrity MAC. Generally not recommended; required if the + password is empty. + +PKCS12 +----------------------------------------- + +.. cpp:class:: PKCS12 + + PKCS#12/PFX bundle: parse, inspect, mutate, and export. The default + constructor produces an empty bundle that the caller fills via the + ``add_*`` / ``set_*`` mutators before invoking :cpp:func:`export_to`. + + .. cpp:function:: PKCS12() + + Constructs an empty bundle. + + .. cpp:function:: PKCS12(std::span data, std::string_view password) + + Parses a DER-encoded PFX file. Throws ``Decoding_Error`` if the file + is malformed, or ``Invalid_Authentication_Tag`` if MAC verification + fails. + + Accessors + ^^^^^^^^^ + + .. cpp:function:: const std::vector>& private_keys() const + + Private keys stored in the bundle, in storage order (parse-order for + parsed PFX, insertion-order for built ones). PKCS#12 supports multiple + keys per file. + + .. cpp:function:: const std::vector& certificates() const + + All certificates stored in the bundle. The end-entity, if any, comes + first when produced by parsing; insertion order is preserved for + bundles built in memory. + + .. cpp:function:: std::optional end_entity_certificate() const + + The first certificate whose ``subjectPublicKeyInfo`` matches one of + the stored private keys, or ``nullopt`` if none match (e.g. a + certificate-only or key-only bundle). + + .. cpp:function:: std::vector ca_certificates() const + + Convenience helper: every certificate except the one returned by + :cpp:func:`end_entity_certificate`. Returned in storage order. For a + key-less bundle, returns all certificates after the first. + + .. cpp:function:: const std::optional& friendly_name() const + + The ``friendlyName`` attribute, if present. + + .. cpp:function:: const std::optional>& local_key_id() const + + The ``localKeyId`` attribute, if present. + + .. cpp:function:: const std::vector& unknown_bag_types() const + + OIDs of bag types encountered during parsing but not handled by this + implementation (e.g. ``SecretBag``). Empty for normal PKCS#12 files + and for bundles constructed in-memory. + + Mutators + ^^^^^^^^ + + .. cpp:function:: void add_key(std::shared_ptr key) + + Adds a private key to the bundle. + + .. cpp:function:: void add_certificate(X509_Certificate cert) + + Adds a certificate. End-entity vs CA is determined at export time by + matching against stored keys. + + .. cpp:function:: void set_friendly_name(std::string name) + .. cpp:function:: void clear_friendly_name() + .. cpp:function:: void set_local_key_id(std::vector id) + .. cpp:function:: void clear_local_key_id() + + Set or clear the bundle-level friendly name / localKeyId attributes. + + Export + ^^^^^^ + + .. cpp:function:: std::vector export_to(const PKCS12_Export_Options& options, \ + RandomNumberGenerator& rng) const + + Serializes the bundle as a PKCS#12/PFX file. + + Throws ``Invalid_Argument`` if the options are inconsistent (e.g. + unsupported algorithm, MAC enabled with empty password) or if a + stored private key does not match any stored certificate. + + Iteration counts above ``PKCS12_MAX_ITERATIONS`` (1 000 000) and a + SafeContentsBag nesting depth above ``PKCS12_MAX_NESTING`` (10) are + rejected. + +Examples +----------------------------------------- + +Generating a PFX file from a freshly created key and self-signed certificate: + +.. literalinclude:: /../src/examples/pkcs12_export.cpp + :language: cpp + +Parsing a PFX file with error handling: + +.. literalinclude:: /../src/examples/pkcs12_parse.cpp + :language: cpp + +Building a PFX bundle that contains a CA certificate chain alongside the +end-entity key and certificate: + +.. literalinclude:: /../src/examples/pkcs12_export_chain.cpp + :language: cpp + +.. note:: + + The default encryption algorithm is ``PBES2-SHA256-AES256`` with + ``SHA-256`` MAC and 100 000 KDF iterations. For maximum compatibility + with legacy software (older Java keytool, legacy OpenSSL builds), use + :cpp:func:`PKCS12_Export_Options::legacy_compat` or explicitly configure + ``with_key_encryption_algo("PBE-SHA1-3DES")``, + ``with_mac_digest("SHA-1")``, and ``with_iterations(2048)``. + +Supported Algorithms +----------------------------------------- + +The following algorithms are available depending on which Botan modules are built: + +.. list-table:: + :header-rows: 1 + :widths: 25 30 25 20 + + * - Field + - Value + - Required module + - Notes + * - ``with_key_encryption_algo`` + - ``"PBES2-SHA256-AES256"`` + - ``pbes2``, ``aes`` + - Default; recommended for modern use + * - ``with_key_encryption_algo`` + - ``"PBES2-SHA256-AES128"`` + - ``pbes2``, ``aes`` + - Modern + * - ``with_key_encryption_algo`` + - ``"PBE-SHA1-3DES"`` + - ``pkcs12_pbe``, ``des`` + - Legacy; widest compatibility + * - ``with_key_encryption_algo`` + - ``"PBE-SHA1-2DES"`` + - ``pkcs12_pbe``, ``des`` + - Legacy + * - ``with_cert_encryption_algo`` + - Same as above, or ``""`` + - - + - Empty = certificates stored unencrypted + * - ``with_mac_digest`` + - ``"SHA-256"`` + - ``sha2_32`` + - Default; required by OpenSSL 3.x default policy + * - ``with_mac_digest`` + - ``"SHA-1"`` + - ``sha1`` + - Legacy; widest compatibility + * - ``with_mac_digest`` + - ``"SHA-384"``, ``"SHA-512"`` + - ``sha2_64`` + - Uncommon; supported for parsing and generation + +See :doc:`/cli` for the ``pkcs12_export`` / ``pkcs12_info`` CLI commands. diff --git a/doc/cli.rst b/doc/cli.rst index 0241753f3b6..4440f4fc092 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -218,6 +218,36 @@ X.509 ``trust_roots --dn --dn-only --display`` List the certificates in the system trust store. +PKCS#12 +---------------------------------------------- + +``pkcs12_export --output= --pass= --in-key-pass= --friendly-name= --key-cipher=PBES2-SHA256-AES256 --no-mac --cert-cipher= --mac-digest=SHA-256 --iterations=100000 key cert *ca_certs`` + Bundle a PKCS#8 *key*, its end-entity *cert*, and optionally one or more CA + certificates (*ca_certs*) into a PKCS#12/PFX file. The PFX password is + taken from ``--pass``; if the input key is itself encrypted, its passphrase + is taken from ``--in-key-pass``. + + ``--key-cipher`` selects the private-key encryption algorithm. Modern + choices (default): ``"PBES2-SHA256-AES256"``, ``"PBES2-SHA256-AES128"``. + Legacy interop: ``"PBE-SHA1-3DES"``, ``"PBE-SHA1-2DES"``. + ``--cert-cipher`` (empty by default) wraps the certificate bags in an + EncryptedData; leave empty for unencrypted certificates. + ``--mac-digest`` selects the integrity-MAC digest (``SHA-256`` default, + ``SHA-1`` for maximum interoperability). ``--no-mac`` omits the MAC + entirely (required when ``--pass`` is empty). + +``pkcs12_info --pass= --key-out= --cert-out= --chain-out= --out-key-pass= --out-key-cipher= --key-pbkdf-iter=100000 pfx_file`` + Inspect a PKCS#12/PFX file. Without output arguments, prints a summary of + the contents (key algorithm, end-entity subject/issuer/serial/validity, + SHA-1 and SHA-256 fingerprints, CA chain, friendly name). + + ``--key-out`` writes the extracted private key as PEM. When + ``--out-key-pass`` is set the key is re-encrypted with the algorithm from + ``--out-key-cipher`` (default ``AES-256/CBC``) using ``--key-pbkdf-iter`` + PBKDF iterations. ``--cert-out`` writes the end-entity certificate as PEM, + and ``--chain-out`` writes the CA certificate chain (one PEM block per + cert) if any are present. + TLS Server/Client ----------------------- diff --git a/src/cli/pkcs12.cpp b/src/cli/pkcs12.cpp new file mode 100644 index 00000000000..de13b492b1c --- /dev/null +++ b/src/cli/pkcs12.cpp @@ -0,0 +1,211 @@ +/* +* PKCS#12 CLI +* (C) 2026 Damiano Mazzella +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "cli.h" + +#if defined(BOTAN_HAS_PKCS12) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) + + #include + #include + #include + #include + #include + #include + #include + #include + #include + +namespace Botan_CLI { + +class PKCS12_Export final : public Command { + public: + PKCS12_Export() : + Command( + "pkcs12_export --output= --pass= --in-key-pass= --friendly-name= --key-cipher=PBES2-SHA256-AES256 --no-mac --cert-cipher= --mac-digest=SHA-256 --iterations=100000 key cert *ca_certs") { + } + + std::string group() const override { return "pkcs12"; } + + std::string description() const override { return "Export private key and certificate(s) to PKCS#12/PFX format"; } + + void go() override { + const std::string key_file = get_arg("key"); + const std::string key_pass = get_passphrase_arg("Key file password (empty if unencrypted)", "in-key-pass"); + const std::string pfx_pass = get_passphrase_arg("PFX password", "pass"); + + // Load private key + Botan::DataSource_Stream key_stream(key_file); + auto private_key = Botan::PKCS8::load_key(key_stream, key_pass); + if(!private_key) { + throw CLI_Error("Failed to load private key from " + key_file); + } + + // Load end-entity certificate + const Botan::X509_Certificate cert(get_arg("cert")); + + // Populate the bundle + Botan::PKCS12 bundle; + bundle.add_key(std::shared_ptr(std::move(private_key))); + bundle.add_certificate(cert); + for(const auto& ca_file : get_arg_list("ca_certs")) { + bundle.add_certificate(Botan::X509_Certificate(ca_file)); + } + + const std::string friendly_name = get_arg("friendly-name"); + if(!friendly_name.empty()) { + bundle.set_friendly_name(friendly_name); + } + + // Build export options + Botan::PKCS12_Export_Options options(pfx_pass); + options.with_iterations(get_arg_sz("iterations")); + options.with_key_encryption_algo(get_arg("key-cipher")); + + const std::string cert_cipher = get_arg("cert-cipher"); + if(!cert_cipher.empty()) { + options.with_cert_encryption_algo(cert_cipher); + } + + if(flag_set("no-mac")) { + options.without_mac(); + } + + const std::string mac_digest = get_arg("mac-digest"); + if(!mac_digest.empty()) { + options.with_mac_digest(mac_digest); + } + + const auto pfx_data = bundle.export_to(options, rng()); + + write_output(pfx_data); + } +}; + +BOTAN_REGISTER_COMMAND("pkcs12_export", PKCS12_Export); + +class PKCS12_Info final : public Command { + public: + PKCS12_Info() : + Command( + "pkcs12_info --pass= --key-out= --cert-out= --chain-out= --out-key-pass= --out-key-cipher= --key-pbkdf-iter=100000 pfx_file") { + } + + std::string group() const override { return "pkcs12"; } + + std::string description() const override { + return "Inspect a PKCS#12/PFX file (and optionally extract key and certificates)"; + } + + void go() override { + const std::string pfx_file = get_arg("pfx_file"); + const std::string pfx_pass = get_passphrase_arg("PFX password", "pass"); + + std::vector pfx_data = slurp_file(pfx_file); + + const Botan::PKCS12 bundle(pfx_data, pfx_pass); + + const bool has_key = !bundle.private_keys().empty(); + const auto end_entity = bundle.end_entity_certificate(); + + // Build CA chain list (everything except end-entity) + const std::vector ca_chain = bundle.ca_certificates(); + + // Output private key + const std::string key_out = get_arg("key-out"); + if(!key_out.empty() && !has_key) { + output() << "Warning: --key-out specified but PFX contains no private key\n"; + } + if(!key_out.empty() && has_key) { + const std::string key_pass = + get_passphrase_arg("Output key password (empty for unencrypted)", "out-key-pass"); + std::ofstream key_stream(key_out); + if(!key_stream) { + throw CLI_Error("Cannot open " + key_out + " for writing"); + } + + const auto& key = *bundle.private_keys().front(); + if(key_pass.empty()) { + key_stream << Botan::PKCS8::PEM_encode(key); + } else { + const std::string cipher = get_arg_or("out-key-cipher", "AES-256/CBC"); + const size_t iterations = get_arg_sz("key-pbkdf-iter"); + key_stream << Botan::PKCS8::PEM_encode_encrypted_pbkdf_iter(key, rng(), key_pass, iterations, cipher); + } + output() << "Private key written to " << key_out << "\n"; + } + + // Output end-entity certificate + const std::string cert_out = get_arg("cert-out"); + if(!cert_out.empty() && !end_entity.has_value()) { + output() << "Warning: --cert-out specified but PFX contains no end-entity certificate\n"; + } + if(!cert_out.empty() && end_entity.has_value()) { + std::ofstream cert_stream(cert_out); + if(!cert_stream) { + throw CLI_Error("Cannot open " + cert_out + " for writing"); + } + cert_stream << end_entity->PEM_encode(); + output() << "Certificate written to " << cert_out << "\n"; + } + + // Output CA chain + const std::string chain_out = get_arg("chain-out"); + if(!chain_out.empty() && ca_chain.empty()) { + output() << "Warning: --chain-out specified but PFX contains no CA certificates\n"; + } + if(!chain_out.empty() && !ca_chain.empty()) { + std::ofstream chain_stream(chain_out); + if(!chain_stream) { + throw CLI_Error("Cannot open " + chain_out + " for writing"); + } + for(const auto& ca_cert : ca_chain) { + chain_stream << ca_cert.PEM_encode(); + } + output() << "CA chain (" << ca_chain.size() << " certs) written to " << chain_out << "\n"; + } + + // If no output files specified, show info + if(key_out.empty() && cert_out.empty() && chain_out.empty()) { + output() << "PKCS#12 contents:\n\n"; + if(has_key) { + const auto& key = *bundle.private_keys().front(); + output() << "Private Key:\n"; + output() << " Algorithm: " << key.algo_name() << "\n"; + output() << " Key Size: " << key.key_length() << " bits\n\n"; + } + if(end_entity.has_value()) { + output() << "End-Entity Certificate:\n"; + output() << " Subject: " << end_entity->subject_dn().to_string() << "\n"; + output() << " Issuer: " << end_entity->issuer_dn().to_string() << "\n"; + output() << " Serial: " << Botan::hex_encode(end_entity->serial_number()) << "\n"; + output() << " Not Before: " << end_entity->not_before().readable_string() << "\n"; + output() << " Not After: " << end_entity->not_after().readable_string() << "\n"; + output() << " SHA-1 Fingerprint: " << end_entity->fingerprint("SHA-1") << "\n"; + output() << " SHA-256 Fingerprint: " << end_entity->fingerprint("SHA-256") << "\n\n"; + } + if(!ca_chain.empty()) { + output() << "CA Certificate Chain (" << ca_chain.size() << " certificates):\n"; + size_t idx = 1; + for(const auto& ca_cert : ca_chain) { + output() << " [" << idx++ << "] Subject: " << ca_cert.subject_dn().to_string() << "\n"; + output() << " Issuer: " << ca_cert.issuer_dn().to_string() << "\n"; + output() << " Not After: " << ca_cert.not_after().readable_string() << "\n"; + } + output() << "\n"; + } + if(bundle.friendly_name().has_value()) { + output() << "Friendly name: " << *bundle.friendly_name() << "\n"; + } + } + } +}; + +BOTAN_REGISTER_COMMAND("pkcs12_info", PKCS12_Info); + +} // namespace Botan_CLI + +#endif diff --git a/src/examples/pkcs12_export.cpp b/src/examples/pkcs12_export.cpp new file mode 100644 index 00000000000..6a007861530 --- /dev/null +++ b/src/examples/pkcs12_export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +int main() { + Botan::AutoSeeded_RNG rng; + + // Generate an ECDSA private key + self-signed certificate to bundle. + auto key = std::make_shared(rng, Botan::EC_Group::from_name("secp256r1")); + + const Botan::X509_Cert_Options cert_opts("example.com"); + const auto cert = Botan::X509::create_self_signed_cert(cert_opts, *key, "SHA-256", rng); + + // Populate the PKCS#12 bundle. + Botan::PKCS12 bundle; + bundle.add_key(key); + bundle.add_certificate(cert); + bundle.set_friendly_name("My Key"); + + // Export with modern defaults (PBES2-SHA256-AES256, SHA-256 MAC, + // 100 000 iterations). For maximum interoperability with legacy software + // use Botan::PKCS12_Export_Options::legacy_compat("secret") instead. + const auto pfx = bundle.export_to(Botan::PKCS12_Export_Options::modern("secret"), rng); + + std::cout << Botan::hex_encode(pfx) << '\n'; + return 0; +} diff --git a/src/examples/pkcs12_export_chain.cpp b/src/examples/pkcs12_export_chain.cpp new file mode 100644 index 00000000000..565afe2e3f2 --- /dev/null +++ b/src/examples/pkcs12_export_chain.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +int main() { + Botan::AutoSeeded_RNG rng; + const auto group = Botan::EC_Group::from_name("secp256r1"); + + // Issuing CA. + const Botan::ECDSA_PrivateKey ca_key(rng, group); + Botan::X509_Cert_Options ca_opts("Example CA"); + ca_opts.CA_key(); + const auto ca_cert = Botan::X509::create_self_signed_cert(ca_opts, ca_key, "SHA-256", rng); + + // End-entity, signed by the CA. + auto ee_key = std::make_shared(rng, group); + Botan::X509_Cert_Options ee_opts("example.com"); + ee_opts.dns = "example.com"; + const auto csr = Botan::X509::create_cert_req(ee_opts, *ee_key, "SHA-256", rng); + const Botan::X509_CA ca(ca_cert, ca_key, "SHA-256", rng); + const auto ee_cert = ca.sign_request(csr, rng, Botan::X509_Time("200101000000Z"), Botan::X509_Time("300101000000Z")); + + // Bundle: end-entity key + cert + issuing CA in the chain. + Botan::PKCS12 bundle; + bundle.add_key(ee_key); + bundle.add_certificate(ee_cert); + bundle.add_certificate(ca_cert); + + const auto pfx = bundle.export_to(Botan::PKCS12_Export_Options::modern("secret", "Server Key"), rng); + + std::cout << Botan::hex_encode(pfx) << '\n'; + return 0; +} diff --git a/src/examples/pkcs12_parse.cpp b/src/examples/pkcs12_parse.cpp new file mode 100644 index 00000000000..925b22099ef --- /dev/null +++ b/src/examples/pkcs12_parse.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +int main() { + // Read a hex-encoded PFX from stdin. + const std::string hex_input((std::istreambuf_iterator(std::cin)), std::istreambuf_iterator()); + const auto pfx_bytes = Botan::hex_decode(hex_input); + + try { + const Botan::PKCS12 bundle(pfx_bytes, "secret"); + + if(!bundle.private_keys().empty()) { + const auto& key = bundle.private_keys().front(); + std::cout << "Key: " << key->algo_name() << " (" << key->key_length() << " bits)\n"; + } + + if(const auto ee = bundle.end_entity_certificate()) { + std::cout << "End-entity: " << ee->subject_dn().to_string() << '\n' + << "Fingerprint (SHA-256): " << ee->fingerprint("SHA-256") << '\n'; + } + + for(const auto& ca : bundle.ca_certificates()) { + std::cout << "CA: " << ca.subject_dn().to_string() << '\n'; + } + + if(bundle.friendly_name()) { + std::cout << "Friendly name: " << *bundle.friendly_name() << '\n'; + } + } catch(const Botan::Invalid_Authentication_Tag&) { + std::cerr << "Wrong password or corrupted MAC\n"; + return 1; + } catch(const Botan::Decoding_Error& e) { + std::cerr << "Malformed or unsupported PFX file: " << e.what() << '\n'; + return 2; + } + return 0; +} diff --git a/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.cpp b/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.cpp index 0a4bf8606c2..409d59a5612 100644 --- a/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.cpp +++ b/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.cpp @@ -143,6 +143,15 @@ void pkcs12_kdf_with_hash(std::span out, } // namespace +void pkcs12_kdf(std::span out, + std::span pwd_bytes, + std::span salt, + size_t iterations, + uint8_t id, + HashFunction& hash) { + pkcs12_kdf_with_hash(out, pwd_bytes, salt, iterations, id, hash); +} + PKCS12_KDF::PKCS12_KDF(std::unique_ptr hash, uint8_t id, size_t iterations) : m_hash(std::move(hash)), m_id(id), m_iterations(iterations) { BOTAN_ARG_CHECK(m_hash != nullptr, "PKCS12-KDF: hash must not be null"); diff --git a/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.h b/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.h index 09b61db659f..906dbb1f2e2 100644 --- a/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.h +++ b/src/lib/pbkdf/pkcs12_kdf/pkcs12_kdf.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -52,6 +53,22 @@ class PKCS12_KDF final : public PasswordHash { size_t m_iterations; }; +/** +* Low-level PKCS#12 KDF (RFC 7292 Appendix B). +* +* Runs the KDF on a caller-supplied @p pwd_bytes buffer without applying +* @c pkcs12_encode_password. Intended for the very rare case (e.g. OpenSSL +* empty-password interop) where the RFC 7292 UCS-2 BE encoding + null +* terminator is not the desired input. Normal callers should use the +* PKCS12_KDF class. +*/ +BOTAN_TEST_API void pkcs12_kdf(std::span out, + std::span pwd_bytes, + std::span salt, + size_t iterations, + uint8_t id, + HashFunction& hash); + class PKCS12_KDF_Family final : public PasswordHashFamily { public: PKCS12_KDF_Family(std::unique_ptr hash, size_t id); diff --git a/src/lib/pkcs12/info.txt b/src/lib/pkcs12/info.txt new file mode 100644 index 00000000000..78fb4f50f85 --- /dev/null +++ b/src/lib/pkcs12/info.txt @@ -0,0 +1,22 @@ + +PKCS12 -> 20260305 + + + +name -> "PKCS#12" +brief -> "PKCS#12/PFX certificate and key bundle format" + + + +asn1 +x509 +pubkey +pkcs12_pbe +hmac +sha1 +sha2_32 + + + +pkcs12.h + \ No newline at end of file diff --git a/src/lib/pkcs12/pkcs12.cpp b/src/lib/pkcs12/pkcs12.cpp new file mode 100644 index 00000000000..e84558bef47 --- /dev/null +++ b/src/lib/pkcs12/pkcs12.cpp @@ -0,0 +1,938 @@ +/* +* PKCS#12 +* (C) 2026 Damiano Mazzella +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Botan { + +namespace { + +// Associates a parsed certificate with its bag attributes +struct ParsedCert { + X509_Certificate cert; + std::vector local_key_id; + std::string friendly_name; +}; + +/* +* Encode a friendly name as a BMPString (UTF-16BE) +* ASN1_String doesn't support encoding BMPStrings, so we do it manually +*/ +void encode_bmpstring(DER_Encoder& enc, std::string_view str) { + const std::vector utf16be = utf8_to_ucs2(str); + enc.add_object(ASN1_Type::BmpString, ASN1_Class::Universal, utf16be); +} + +/* +* Resolve a MAC digest OID to a hash name, throwing on unsupported algorithms. +*/ +std::string resolve_mac_hash(const OID& oid) { + if(oid == OID::from_string("SHA-1")) { + return "SHA-1"; + } + if(oid == OID::from_string("SHA-224")) { + return "SHA-224"; + } + if(oid == OID::from_string("SHA-256")) { + return "SHA-256"; + } + if(oid == OID::from_string("SHA-384")) { + return "SHA-384"; + } + if(oid == OID::from_string("SHA-512")) { + return "SHA-512"; + } + if(oid == OID::from_string("SHA-512-256")) { + return "SHA-512-256"; + } + throw Decoding_Error(fmt("Unsupported PKCS#12 MAC digest: {}", oid.to_formatted_string())); +} + +/* +* Validate PKCS12_Export_Options before starting export. +*/ +void validate_options(const PKCS12_Export_Options& opts) { + if(opts.iterations() == 0 || opts.iterations() > PKCS12_MAX_ITERATIONS) { + throw Invalid_Argument(fmt("PKCS#12: iteration count must be between 1 and {}", PKCS12_MAX_ITERATIONS)); + } + static const std::array supported_key_algos = { + "PBE-SHA1-3DES", + "PBE-SHA1-2DES", + "PBES2-SHA256-AES256", + "PBES2-SHA256-AES128", + }; + if(std::find(supported_key_algos.begin(), supported_key_algos.end(), opts.key_encryption_algo()) == + supported_key_algos.end()) { + throw Invalid_Argument(fmt("PKCS#12: unsupported key encryption algorithm '{}'", opts.key_encryption_algo())); + } + if(!opts.cert_encryption_algo().empty()) { + if(std::find(supported_key_algos.begin(), supported_key_algos.end(), opts.cert_encryption_algo()) == + supported_key_algos.end()) { + throw Invalid_Argument( + fmt("PKCS#12: unsupported cert encryption algorithm '{}'", opts.cert_encryption_algo())); + } + if(opts.password().empty()) { + throw Invalid_Argument("PKCS#12: cert_encryption_algo requires a non-empty password"); + } + } + if(opts.include_mac()) { + if(opts.password().empty()) { + throw Invalid_Argument("PKCS#12: include_mac requires a non-empty password"); + } + static const std::array supported_mac_digests = { + "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512", "SHA-512-256"}; + if(std::find(supported_mac_digests.begin(), supported_mac_digests.end(), opts.mac_digest()) == + supported_mac_digests.end()) { + throw Invalid_Argument(fmt("PKCS#12: unsupported MAC digest '{}'", opts.mac_digest())); + } + if(!HashFunction::create(opts.mac_digest())) { + throw Invalid_Argument(fmt("PKCS#12: MAC digest '{}' is not available in this build", opts.mac_digest())); + } + } +} + +/* +* Verify PKCS#12 MAC. +* +* When @p openssl_empty_pwd_compat is @c true and @p password is empty, the +* KDF is fed an empty byte string (OpenSSL non-conforming behavior) instead +* of the RFC 7292 form (a two-byte {0x00,0x00} terminator). +*/ +void verify_mac(std::span auth_safe_data, + std::span mac_value, + std::span mac_salt, + size_t iterations, + const std::string& hash_name, + std::string_view password, + bool openssl_empty_pwd_compat) { + auto hmac = MessageAuthenticationCode::create_or_throw(fmt("HMAC({})", hash_name)); + const size_t mac_key_len = hmac->output_length(); + + secure_vector mac_key(mac_key_len); + if(openssl_empty_pwd_compat && password.empty()) { + const auto hash = HashFunction::create_or_throw(hash_name); + pkcs12_kdf({mac_key.data(), mac_key_len}, {}, {mac_salt.data(), mac_salt.size()}, iterations, 3, *hash); + } else { + const PKCS12_KDF kdf(HashFunction::create_or_throw(hash_name), 3, iterations); + kdf.derive_key(mac_key.data(), mac_key_len, password.data(), password.size(), mac_salt.data(), mac_salt.size()); + } + + hmac->set_key(mac_key); + hmac->update(auth_safe_data); + if(!constant_time_compare(hmac->final(), mac_value)) { + throw Invalid_Authentication_Tag("PKCS#12 MAC verification failed"); + } +} + +/* +* Parse attributes from a SafeBag. Only FriendlyName and LocalKeyId are +* handled; other attributes are silently skipped (RFC 7292 sec.4.2). +*/ +void parse_bag_attributes(BER_Decoder& decoder, std::string& friendly_name, std::vector& local_key_id) { + if(!decoder.more_items()) { + return; + } + + const OID friendly_name_oid = OID::from_string("PKCS9.FriendlyName"); + const OID local_key_id_oid = OID::from_string("PKCS9.LocalKeyId"); + + BER_Decoder attrs = decoder.start_set(); + while(attrs.more_items()) { + OID attr_oid; + BER_Decoder attr_seq = attrs.start_sequence(); + attr_seq.decode(attr_oid); + + BER_Decoder values = attr_seq.start_set(); + if(attr_oid == friendly_name_oid) { + ASN1_String str; + values.decode(str); + friendly_name = str.value(); + } else if(attr_oid == local_key_id_oid) { + values.decode(local_key_id, ASN1_Type::OctetString); + } + values.discard_remaining(); + values.end_cons(); + attr_seq.discard_remaining(); + attr_seq.end_cons(); + } + attrs.end_cons(); +} + +// Helper bag carrying a parsed private key with its attributes. +struct ParsedKey { + std::shared_ptr key; + std::vector local_key_id; + std::string friendly_name; +}; + +/* +* Parse SafeContents (sequence of SafeBag) +*/ +void parse_safe_contents(BER_Decoder& decoder, + std::string_view password, + std::vector& cert_entries, + std::vector& key_entries, + std::vector& unknown_bag_types, + bool openssl_empty_pwd_compat, + size_t depth = 0) { + if(depth >= PKCS12_MAX_NESTING) { + throw Decoding_Error("PKCS#12: SafeContentsBag nesting too deep"); + } + const OID cert_bag_oid = OID::from_string("PKCS12.CertBag"); + const OID shrouded_key_bag_oid = OID::from_string("PKCS12.PKCS8ShroudedKeyBag"); + const OID key_bag_oid = OID::from_string("PKCS12.KeyBag"); + const OID safe_contents_bag_oid = OID::from_string("PKCS12.SafeContentsBag"); + const OID x509_cert_oid = OID::from_string("PKCS9.X509Certificate"); + + while(decoder.more_items()) { + OID bag_type; + std::string bag_friendly_name; + std::vector bag_key_id; + + BER_Decoder bag_seq = decoder.start_sequence(); + bag_seq.decode(bag_type); + + BER_Decoder bag_value = bag_seq.start_context_specific(0); + + bool pushed_cert = false; + bool pushed_key = false; + + if(bag_type == cert_bag_oid) { + OID cert_type; + BER_Decoder cert_bag = bag_value.start_sequence(); + cert_bag.decode(cert_type); + + if(cert_type == x509_cert_oid) { + std::vector cert_data; + BER_Decoder cert_value = cert_bag.start_context_specific(0); + cert_value.decode(cert_data, ASN1_Type::OctetString); + cert_value.verify_end(); + + cert_entries.push_back({X509_Certificate(cert_data), {}, {}}); + pushed_cert = true; + } else { + cert_bag.discard_remaining(); + } + cert_bag.end_cons(); + bag_value.verify_end(); + } else if(bag_type == shrouded_key_bag_oid) { + AlgorithmIdentifier pbe_algo; + std::vector encrypted_key; + + BER_Decoder shrouded = bag_value.start_sequence(); + shrouded.decode(pbe_algo); + shrouded.decode(encrypted_key, ASN1_Type::OctetString); + shrouded.verify_end(); + + auto decrypted = pkcs12_pbe_decrypt(encrypted_key, password, pbe_algo, openssl_empty_pwd_compat); + DataSource_Memory src(decrypted); + key_entries.push_back({std::shared_ptr(PKCS8::load_key(src)), {}, {}}); + pushed_key = true; + } else if(bag_type == key_bag_oid) { + secure_vector key_data; + bag_value.raw_bytes(key_data); + bag_value.verify_end(); + DataSource_Memory src(key_data); + key_entries.push_back({std::shared_ptr(PKCS8::load_key(src)), {}, {}}); + pushed_key = true; + } else if(bag_type == safe_contents_bag_oid) { + BER_Decoder nested_sc = bag_value.start_sequence(); + parse_safe_contents( + nested_sc, password, cert_entries, key_entries, unknown_bag_types, openssl_empty_pwd_compat, depth + 1); + nested_sc.verify_end(); + bag_value.verify_end(); + } else { + unknown_bag_types.push_back(bag_type); + bag_value.discard_remaining(); + } + + bag_value.end_cons(); + + parse_bag_attributes(bag_seq, bag_friendly_name, bag_key_id); + + if(pushed_cert && !cert_entries.empty()) { + if(!bag_key_id.empty()) { + cert_entries.back().local_key_id = bag_key_id; + } + if(!bag_friendly_name.empty()) { + cert_entries.back().friendly_name = bag_friendly_name; + } + } else if(pushed_key && !key_entries.empty()) { + if(!bag_key_id.empty()) { + key_entries.back().local_key_id = bag_key_id; + } + if(!bag_friendly_name.empty()) { + key_entries.back().friendly_name = bag_friendly_name; + } + } + + bag_seq.verify_end(); + } +} + +/* +* Parse AuthenticatedSafe (sequence of ContentInfo) +*/ +void parse_authenticated_safe(std::span data, + std::string_view password, + std::vector& cert_entries, + std::vector& key_entries, + std::vector& unknown_bag_types, + bool openssl_empty_pwd_compat) { + const OID pkcs7_data_oid = OID::from_string("PKCS7.Data"); + const OID pkcs7_enc_data_oid = OID::from_string("PKCS7.EncryptedData"); + + BER_Decoder auth_safe(data); + BER_Decoder seq = auth_safe.start_sequence(); + + while(seq.more_items()) { + OID content_type; + BER_Decoder content_info = seq.start_sequence(); + content_info.decode(content_type); + + if(content_type == pkcs7_data_oid) { + std::vector safe_contents_data; + BER_Decoder content = content_info.start_context_specific(0); + content.decode(safe_contents_data, ASN1_Type::OctetString); + content.verify_end(); + + BER_Decoder safe_contents(safe_contents_data); + BER_Decoder sc_seq = safe_contents.start_sequence(); + parse_safe_contents(sc_seq, password, cert_entries, key_entries, unknown_bag_types, openssl_empty_pwd_compat); + sc_seq.verify_end(); + safe_contents.verify_end(); + content_info.verify_end(); + } else if(content_type == pkcs7_enc_data_oid) { + BER_Decoder content = content_info.start_context_specific(0); + BER_Decoder enc_data = content.start_sequence(); + + size_t version = 0; + enc_data.decode(version); + + if(version != 0) { + throw Decoding_Error(fmt("PKCS#12: unsupported EncryptedData version: {}", version)); + } + + BER_Decoder enc_content_info = enc_data.start_sequence(); + OID enc_content_type; + AlgorithmIdentifier enc_algo; + enc_content_info.decode(enc_content_type); + enc_content_info.decode(enc_algo); + + if(enc_content_type != pkcs7_data_oid) { + throw Decoding_Error( + fmt("PKCS#12: EncryptedData contentType must be Data, got {}", enc_content_type.to_formatted_string())); + } + + std::vector encrypted_content; + const BER_Object enc_content_obj = enc_content_info.get_next_object(); + + if(enc_content_obj.is_a(0, ASN1_Class::ContextSpecific | ASN1_Class::Constructed)) { + const std::span raw(enc_content_obj.bits(), enc_content_obj.length()); + encrypted_content.reserve(raw.size()); + BER_Decoder chunks(raw); + while(chunks.more_items()) { + std::vector chunk; + chunks.decode(chunk, ASN1_Type::OctetString); + encrypted_content.insert(encrypted_content.end(), chunk.begin(), chunk.end()); + } + chunks.verify_end(); + } else if(enc_content_obj.is_a(0, ASN1_Class::ContextSpecific)) { + encrypted_content.assign(enc_content_obj.bits(), enc_content_obj.bits() + enc_content_obj.length()); + } else { + throw Decoding_Error("PKCS#12: Expected [0] context-specific for encrypted content"); + } + + enc_content_info.verify_end(); + enc_data.verify_end(); + + const secure_vector decrypted = + pkcs12_pbe_decrypt(encrypted_content, password, enc_algo, openssl_empty_pwd_compat); + + BER_Decoder safe_contents(decrypted); + BER_Decoder sc_seq = safe_contents.start_sequence(); + parse_safe_contents(sc_seq, password, cert_entries, key_entries, unknown_bag_types, openssl_empty_pwd_compat); + sc_seq.verify_end(); + safe_contents.verify_end(); + content.verify_end(); + content_info.verify_end(); + } else { + throw Decoding_Error( + fmt("PKCS#12: unsupported AuthenticatedSafe content type {}", content_type.to_formatted_string())); + } + } + + seq.verify_end(); + auth_safe.verify_end(); +} + +} // namespace + +// +// PKCS12_Export_Options +// + +PKCS12_Export_Options::PKCS12_Export_Options(std::string_view password, std::optional friendly_name) : + m_password(password), m_friendly_name(std::move(friendly_name)) {} + +PKCS12_Export_Options PKCS12_Export_Options::modern(std::string_view password, + std::optional friendly_name) { + return PKCS12_Export_Options(password, std::move(friendly_name)); +} + +PKCS12_Export_Options PKCS12_Export_Options::legacy_compat(std::string_view password, + std::optional friendly_name) { + PKCS12_Export_Options opts(password, std::move(friendly_name)); + opts.m_iterations = 2048; + opts.m_key_encryption_algo = "PBE-SHA1-3DES"; + opts.m_mac_digest = "SHA-1"; + return opts; +} + +PKCS12_Export_Options& PKCS12_Export_Options::with_friendly_name(std::string name) { + m_friendly_name = std::move(name); + return *this; +} + +PKCS12_Export_Options& PKCS12_Export_Options::with_iterations(size_t n) { + m_iterations = n; + return *this; +} + +PKCS12_Export_Options& PKCS12_Export_Options::with_key_encryption_algo(std::string algo) { + m_key_encryption_algo = std::move(algo); + return *this; +} + +PKCS12_Export_Options& PKCS12_Export_Options::with_cert_encryption_algo(std::string algo) { + m_cert_encryption_algo = std::move(algo); + return *this; +} + +PKCS12_Export_Options& PKCS12_Export_Options::with_mac_digest(std::string algo) { + m_mac_digest = std::move(algo); + return *this; +} + +PKCS12_Export_Options& PKCS12_Export_Options::without_mac() { + m_include_mac = false; + return *this; +} + +// +// PKCS12 +// + +PKCS12::PKCS12(std::span data, std::string_view password) { + std::vector cert_entries; + std::vector key_entries; + + BER_Decoder pfx(data); + BER_Decoder pfx_seq = pfx.start_sequence(); + + size_t version = 0; + pfx_seq.decode(version); + if(version != 3) { + throw Decoding_Error(fmt("Unsupported PKCS#12 version: {}", version)); + } + + OID auth_safe_type; + std::vector auth_safe_content; + + BER_Decoder auth_safe_info = pfx_seq.start_sequence(); + auth_safe_info.decode(auth_safe_type); + + const OID pkcs7_data_oid = OID::from_string("PKCS7.Data"); + if(auth_safe_type != pkcs7_data_oid) { + throw Decoding_Error("PKCS#12 authSafe must be of type Data"); + } + + BER_Decoder auth_safe_content_wrapper = auth_safe_info.start_context_specific(0); + auth_safe_content_wrapper.decode(auth_safe_content, ASN1_Type::OctetString); + auth_safe_content_wrapper.verify_end(); + auth_safe_info.verify_end(); + + // Tracks whether MAC verification succeeded with OpenSSL's non-conforming + // empty-password encoding; if so, the same convention is used for any + // subsequent EncryptedData / PKCS8ShroudedKeyBag decryption. + bool openssl_empty_pwd_compat = false; + + if(pfx_seq.more_items()) { + BER_Decoder mac_data = pfx_seq.start_sequence(); + + BER_Decoder digest_info = mac_data.start_sequence(); + AlgorithmIdentifier digest_algo; + std::vector mac_value; + digest_info.decode(digest_algo); + digest_info.decode(mac_value, ASN1_Type::OctetString); + digest_info.verify_end(); + + std::vector mac_salt; + size_t iterations = 1; + mac_data.decode(mac_salt, ASN1_Type::OctetString); + if(mac_data.more_items()) { + mac_data.decode(iterations); + } + mac_data.verify_end(); + if(iterations == 0 || iterations > PKCS12_MAX_ITERATIONS) { + throw Decoding_Error(fmt("PKCS#12 MAC has invalid iteration count: {}", iterations)); + } + + const std::string hash_name = resolve_mac_hash(digest_algo.oid()); + // Try RFC 7292 password encoding first. If MAC verification fails and + // the password is empty, retry with OpenSSL's non-conforming empty + // encoding (some OpenSSL releases pass an empty byte string to the KDF + // instead of the RFC {0x00,0x00} form when the password is empty). + // Propagate the chosen convention to any subsequent PBE decryption. + try { + verify_mac(auth_safe_content, mac_value, mac_salt, iterations, hash_name, password, false); + } catch(const Invalid_Authentication_Tag&) { + if(!password.empty()) { + throw; + } + verify_mac(auth_safe_content, mac_value, mac_salt, iterations, hash_name, password, true); + openssl_empty_pwd_compat = true; + } + } + + parse_authenticated_safe( + auth_safe_content, password, cert_entries, key_entries, m_unknown_bag_types, openssl_empty_pwd_compat); + + // Move all parsed keys into storage. + m_private_keys.reserve(key_entries.size()); + for(auto& ke : key_entries) { + m_private_keys.push_back(std::move(ke.key)); + } + + // Capture bundle-level attributes from the first key (if any), or from + // the end-entity certificate (if found below). + if(!key_entries.empty()) { + if(!key_entries.front().friendly_name.empty()) { + m_friendly_name = key_entries.front().friendly_name; + } + if(!key_entries.front().local_key_id.empty()) { + m_local_key_id = key_entries.front().local_key_id; + } + } + + // Reorder certificates so the end-entity (cert matching the first key) + // comes first; rest follow in original order. Match prefers localKeyId, + // falls back to subjectPublicKeyInfo comparison. + std::optional end_entity_idx; + if(!cert_entries.empty() && !m_private_keys.empty()) { + const auto& first_key = m_private_keys.front(); + const auto& first_key_id = key_entries.empty() ? std::vector{} : key_entries.front().local_key_id; + + if(!first_key_id.empty()) { + for(size_t i = 0; i < cert_entries.size(); ++i) { + if(cert_entries[i].local_key_id == first_key_id) { + end_entity_idx = i; + break; + } + } + } + if(!end_entity_idx) { + const auto key_spki = first_key->subject_public_key(); + for(size_t i = 0; i < cert_entries.size(); ++i) { + try { + if(cert_entries[i].cert.subject_public_key_info() == key_spki) { + end_entity_idx = i; + break; + } + } catch(const Decoding_Error&) { + // Certificate with unsupported key algorithm - skip + } + } + } + } + + m_certificates.reserve(cert_entries.size()); + if(end_entity_idx) { + m_certificates.push_back(std::move(cert_entries[*end_entity_idx].cert)); + if(!m_friendly_name && !cert_entries[*end_entity_idx].friendly_name.empty()) { + m_friendly_name = cert_entries[*end_entity_idx].friendly_name; + } + if(!m_local_key_id && !cert_entries[*end_entity_idx].local_key_id.empty()) { + m_local_key_id = cert_entries[*end_entity_idx].local_key_id; + } + for(size_t i = 0; i < cert_entries.size(); ++i) { + if(i != *end_entity_idx) { + // Still surface any friendly name found on non-end-entity certs + // when the bundle doesn't have one yet (some producers attach the + // attribute to the CA bag instead of the end-entity bag). + if(!m_friendly_name && !cert_entries[i].friendly_name.empty()) { + m_friendly_name = cert_entries[i].friendly_name; + } + m_certificates.push_back(std::move(cert_entries[i].cert)); + } + } + } else { + for(auto& ce : cert_entries) { + if(!m_friendly_name && !ce.friendly_name.empty()) { + m_friendly_name = ce.friendly_name; + } + m_certificates.push_back(std::move(ce.cert)); + } + } + + pfx_seq.verify_end(); + pfx_seq.end_cons(); + pfx.verify_end("PKCS#12: trailing data after PFX"); +} + +std::vector PKCS12::ca_certificates() const { + if(m_certificates.size() < 2) { + return {}; + } + const auto ee = end_entity_certificate(); + std::vector result; + result.reserve(m_certificates.size() - 1); + if(ee) { + // Skip the first certificate matching the end-entity (only one, in case + // the bundle contains multiple certs signed for the same key, e.g. an + // old leaf still kept alongside a renewed one). + const auto ee_spki = ee->subject_public_key_info(); + bool skipped = false; + for(const auto& c : m_certificates) { + if(!skipped && c.subject_public_key_info() == ee_spki) { + skipped = true; + continue; + } + result.push_back(c); + } + } else { + // No end-entity (e.g. key-less bundle): treat the first stored cert as + // the "primary" and surface the rest as CA / chain certs. This matches + // the storage order used by parsing. + for(size_t i = 1; i < m_certificates.size(); ++i) { + result.push_back(m_certificates[i]); + } + } + return result; +} + +std::optional PKCS12::end_entity_certificate() const { + if(m_certificates.empty() || m_private_keys.empty()) { + return std::nullopt; + } + const auto& first_key = m_private_keys.front(); + const auto key_spki = first_key->subject_public_key(); + for(const auto& c : m_certificates) { + try { + if(c.subject_public_key_info() == key_spki) { + return c; + } + } catch(const Decoding_Error&) { + // Skip certificates with unsupported algorithms + } + } + return std::nullopt; +} + +void PKCS12::add_key(std::shared_ptr key) { + if(!key) { + throw Invalid_Argument("PKCS12::add_key: key must not be null"); + } + m_private_keys.push_back(std::move(key)); +} + +void PKCS12::add_certificate(X509_Certificate cert) { + m_certificates.push_back(std::move(cert)); +} + +void PKCS12::set_friendly_name(std::string name) { + m_friendly_name = std::move(name); +} + +void PKCS12::clear_friendly_name() { + m_friendly_name.reset(); +} + +void PKCS12::set_local_key_id(std::vector id) { + m_local_key_id = std::move(id); +} + +void PKCS12::clear_local_key_id() { + m_local_key_id.reset(); +} + +std::vector PKCS12::export_to(const PKCS12_Export_Options& options, RandomNumberGenerator& rng) const { + if(m_private_keys.empty() && m_certificates.empty()) { + throw Invalid_Argument("PKCS#12::export_to requires at least a key or certificate"); + } + + validate_options(options); + + // Determine end-entity certificate(s). With a single key we pair it + // against a cert matching its SPKI; that pair gets the + // friendly_name/localKeyId from options or the bundle. + std::optional end_entity_idx; + if(!m_private_keys.empty() && !m_certificates.empty()) { + const auto& first_key = m_private_keys.front(); + const auto key_spki = first_key->subject_public_key(); + for(size_t i = 0; i < m_certificates.size(); ++i) { + try { + if(m_certificates[i].subject_public_key_info() == key_spki) { + end_entity_idx = i; + break; + } + } catch(const Decoding_Error&) { + // skip + } + } + if(!end_entity_idx) { + throw Invalid_Argument("PKCS#12::export_to: private key does not match any certificate"); + } + } + + const OID cert_bag_oid = OID::from_string("PKCS12.CertBag"); + const OID shrouded_key_oid = OID::from_string("PKCS12.PKCS8ShroudedKeyBag"); + const OID x509_cert_oid = OID::from_string("PKCS9.X509Certificate"); + const OID friendly_name_oid = OID::from_string("PKCS9.FriendlyName"); + const OID local_key_id_oid = OID::from_string("PKCS9.LocalKeyId"); + const OID pkcs7_data_oid = OID::from_string("PKCS7.Data"); + const OID pkcs7_enc_data_oid = OID::from_string("PKCS7.EncryptedData"); + + // Pick the friendly-name and local-key-id used by attribute encoding. + // Options take precedence over the bundle-level fields. + const std::optional& friendly_name = + options.friendly_name().has_value() ? options.friendly_name() : m_friendly_name; + + std::vector local_key_id; + if(m_local_key_id) { + local_key_id = *m_local_key_id; + } else if(end_entity_idx) { + local_key_id = m_certificates[*end_entity_idx].subject_public_key_bitstring_sha1(); + } else if(!m_private_keys.empty()) { + // Key-only bundle: derive from SHA-1 of the public key bits (matching the + // convention used by X509_Certificate::subject_public_key_bitstring_sha1). + auto sha1 = HashFunction::create_or_throw("SHA-1"); + const auto pub_bits = m_private_keys.front()->public_key_bits(); + sha1->update(pub_bits); + local_key_id = unlock(sha1->final()); + } + + auto write_attributes = [&](DER_Encoder& enc) { + const bool has_fn = friendly_name.has_value() && !friendly_name->empty(); + const bool has_id = !local_key_id.empty(); + if(!has_fn && !has_id) { + return; + } + enc.start_set(); + if(has_fn) { + enc.start_sequence(); + enc.encode(friendly_name_oid); + enc.start_set(); + encode_bmpstring(enc, *friendly_name); + enc.end_cons(); + enc.end_cons(); + } + if(has_id) { + enc.start_sequence(); + enc.encode(local_key_id_oid); + enc.start_set(); + enc.encode(local_key_id, ASN1_Type::OctetString); + enc.end_cons(); + enc.end_cons(); + } + enc.end_cons(); + }; + + // CertBags + std::vector cert_safe_contents; + if(!m_certificates.empty()) { + DER_Encoder cert_bags(cert_safe_contents); + cert_bags.start_sequence(); + + auto add_cert_bag = [&](const X509_Certificate& c, bool add_attrs) { + cert_bags.start_sequence(); + cert_bags.encode(cert_bag_oid); + + cert_bags.start_context_specific(0); + cert_bags.start_sequence(); + cert_bags.encode(x509_cert_oid); + cert_bags.start_context_specific(0); + cert_bags.encode(c.BER_encode(), ASN1_Type::OctetString); + cert_bags.end_cons(); + cert_bags.end_cons(); + cert_bags.end_cons(); + + if(add_attrs) { + write_attributes(cert_bags); + } + + cert_bags.end_cons(); + }; + + // End-entity first (so the file is read in the typical order), then + // the rest in their stored order. + if(end_entity_idx) { + add_cert_bag(m_certificates[*end_entity_idx], true); + for(size_t i = 0; i < m_certificates.size(); ++i) { + if(i != *end_entity_idx) { + add_cert_bag(m_certificates[i], false); + } + } + } else { + for(const auto& c : m_certificates) { + add_cert_bag(c, false); + } + } + + cert_bags.end_cons(); + } + + // Key SafeBag(s) + std::vector key_safe_contents; + if(!m_private_keys.empty()) { + DER_Encoder key_bags(key_safe_contents); + key_bags.start_sequence(); + + for(size_t i = 0; i < m_private_keys.size(); ++i) { + const Private_Key& key = *m_private_keys[i]; + + key_bags.start_sequence(); + key_bags.encode(shrouded_key_oid); + + secure_vector pkcs8_key = PKCS8::BER_encode(key); + auto [enc_algo, enc_key] = + pkcs12_pbe_encrypt(pkcs8_key, options.password(), options.key_encryption_algo(), options.iterations(), rng); + + key_bags.start_context_specific(0); + key_bags.start_sequence(); + key_bags.encode(enc_algo); + key_bags.encode(enc_key, ASN1_Type::OctetString); + key_bags.end_cons(); + key_bags.end_cons(); + + // Only the first key carries the bundle-level attributes (preserves + // the historical single-key behavior). + if(i == 0) { + write_attributes(key_bags); + } + + key_bags.end_cons(); + } + key_bags.end_cons(); + } + + // AuthenticatedSafe + std::vector auth_safe_content; + DER_Encoder auth_safe(auth_safe_content); + auth_safe.start_sequence(); + + if(!cert_safe_contents.empty()) { + if(!options.cert_encryption_algo().empty()) { + auto [enc_algo, enc_data] = pkcs12_pbe_encrypt( + cert_safe_contents, options.password(), options.cert_encryption_algo(), options.iterations(), rng); + + auth_safe.start_sequence(); + auth_safe.encode(pkcs7_enc_data_oid); + auth_safe.start_context_specific(0); + auth_safe.start_sequence(); + auth_safe.encode(size_t(0)); + auth_safe.start_sequence(); + auth_safe.encode(pkcs7_data_oid); + auth_safe.encode(enc_algo); + auth_safe.add_object(ASN1_Type(0), ASN1_Class::ContextSpecific, enc_data); + auth_safe.end_cons(); + auth_safe.end_cons(); + auth_safe.end_cons(); + auth_safe.end_cons(); + } else { + auth_safe.start_sequence(); + auth_safe.encode(pkcs7_data_oid); + auth_safe.start_context_specific(0); + auth_safe.encode(cert_safe_contents, ASN1_Type::OctetString); + auth_safe.end_cons(); + auth_safe.end_cons(); + } + } + + if(!key_safe_contents.empty()) { + auth_safe.start_sequence(); + auth_safe.encode(pkcs7_data_oid); + auth_safe.start_context_specific(0); + auth_safe.encode(key_safe_contents, ASN1_Type::OctetString); + auth_safe.end_cons(); + auth_safe.end_cons(); + } + + auth_safe.end_cons(); + + // PFX + std::vector pfx_data; + DER_Encoder pfx(pfx_data); + pfx.start_sequence(); + pfx.encode(size_t(3)); + + pfx.start_sequence(); + pfx.encode(pkcs7_data_oid); + pfx.start_context_specific(0); + pfx.encode(auth_safe_content, ASN1_Type::OctetString); + pfx.end_cons(); + pfx.end_cons(); + + if(options.include_mac()) { + const std::string& mac_hash = options.mac_digest(); + + auto hmac = MessageAuthenticationCode::create_or_throw(fmt("HMAC({})", mac_hash)); + + std::vector mac_salt(hmac->output_length()); + rng.randomize(mac_salt.data(), mac_salt.size()); + const size_t mac_key_len = hmac->output_length(); + secure_vector mac_key(mac_key_len); + const PKCS12_KDF kdf(HashFunction::create_or_throw(mac_hash), 3, options.iterations()); + kdf.derive_key(mac_key.data(), + mac_key_len, + options.password().data(), + options.password().size(), + mac_salt.data(), + mac_salt.size()); + + hmac->set_key(mac_key); + hmac->update(auth_safe_content); + const secure_vector mac_value = hmac->final(); + + pfx.start_sequence(); + pfx.start_sequence(); + const auto param_encoding = + (mac_hash == "SHA-1") ? AlgorithmIdentifier::USE_NULL_PARAM : AlgorithmIdentifier::USE_EMPTY_PARAM; + pfx.encode(AlgorithmIdentifier(OID::from_string(mac_hash), param_encoding)); + pfx.encode(mac_value, ASN1_Type::OctetString); + pfx.end_cons(); + pfx.encode(mac_salt, ASN1_Type::OctetString); + if(options.iterations() != 1) { + pfx.encode(options.iterations()); + } + pfx.end_cons(); + } + + pfx.end_cons(); + + return pfx_data; +} + +} // namespace Botan diff --git a/src/lib/pkcs12/pkcs12.h b/src/lib/pkcs12/pkcs12.h new file mode 100644 index 00000000000..7049f950e6a --- /dev/null +++ b/src/lib/pkcs12/pkcs12.h @@ -0,0 +1,254 @@ +/* +* PKCS#12 +* (C) 2026 Damiano Mazzella +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_PKCS12_H_ +#define BOTAN_PKCS12_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Botan { + +class RandomNumberGenerator; + +/// Maximum allowed SafeContentsBag nesting depth during parsing (anti-DoS). +inline constexpr size_t PKCS12_MAX_NESTING = 10; + +/** +* Options controlling PKCS#12/PFX export. +* +* Use one of the static pseudo-constructors for the common cases: +* +* - @ref modern - PBES2-SHA256-AES256, SHA-256 MAC, 100 000 iterations +* - @ref legacy_compat - PBE-SHA1-3DES, SHA-1 MAC, 2 048 iterations +* +* For custom configurations construct directly and use the @c with_*() +* mutators (chainable). Any field not set explicitly defaults to the +* "modern" value. +*/ +class BOTAN_PUBLIC_API(3, 13) PKCS12_Export_Options final { + public: + /** + * @param password password protecting the file. Empty is allowed + * (PKCS#12 defines an encoding for it), but must not + * be empty when @ref include_mac is true. + * @param friendly_name optional friendly name attribute stored on the + * private key bag and on the matching end-entity + * certificate bag. + */ + explicit PKCS12_Export_Options(std::string_view password, std::optional friendly_name = {}); + + /** + * Modern defaults: PBES2-SHA256-AES256, SHA-256 MAC, 100 000 iterations. + */ + static PKCS12_Export_Options modern(std::string_view password, std::optional friendly_name = {}); + + /** + * Legacy-compatible defaults: PBE-SHA1-3DES, SHA-1 MAC, 2 048 iterations. + * Use when interoperability with old software (Java keytool pre-2019, + * older OpenSSL releases, Windows pre-Windows-10) is required. + */ + static PKCS12_Export_Options legacy_compat(std::string_view password, + std::optional friendly_name = {}); + + /// Override the friendly-name attribute (otherwise taken from the bundle). + PKCS12_Export_Options& with_friendly_name(std::string name); + + /// Set number of KDF iterations. + PKCS12_Export_Options& with_iterations(size_t n); + + /// Set the private key encryption algorithm (PKCS#12 PBE or PBES2 name). + PKCS12_Export_Options& with_key_encryption_algo(std::string algo); + + /** + * Set the certificate encryption algorithm. Empty string (the default) + * means certificates are stored unencrypted (inside an unencrypted + * SafeContents); pass a non-empty algorithm to wrap them. + */ + PKCS12_Export_Options& with_cert_encryption_algo(std::string algo); + + /// Set the digest used for the integrity MAC. + PKCS12_Export_Options& with_mac_digest(std::string algo); + + /// Disable the integrity MAC. Generally not recommended. + PKCS12_Export_Options& without_mac(); + + const std::string& password() const { return m_password; } + + const std::optional& friendly_name() const { return m_friendly_name; } + + size_t iterations() const { return m_iterations; } + + const std::string& key_encryption_algo() const { return m_key_encryption_algo; } + + /// Empty means: store certificates unencrypted. + const std::string& cert_encryption_algo() const { return m_cert_encryption_algo; } + + const std::string& mac_digest() const { return m_mac_digest; } + + bool include_mac() const { return m_include_mac; } + + private: + std::string m_password; + std::optional m_friendly_name; + size_t m_iterations = 100000; + std::string m_key_encryption_algo = "PBES2-SHA256-AES256"; + std::string m_cert_encryption_algo; + std::string m_mac_digest = "SHA-256"; + bool m_include_mac = true; +}; + +/** +* PKCS#12/PFX bundle: parsed contents, mutable container, and exporter. +* +* PKCS#12 is a file format for storing cryptographic objects (private keys +* and X.509 certificates) together, typically protected by a password. +* +* The class can be used both to inspect an existing PFX and to build a new +* one. Construction from bytes parses an existing file; the default +* constructor produces an empty bundle that the caller populates with +* mutators (@ref add_key, @ref add_certificate, ...) before calling +* @ref export_to to serialize. +* +* @code +* // Parse +* Botan::PKCS12 p12(pfx_bytes, "password"); +* if(!p12.private_keys().empty()) { +* const auto& key = p12.private_keys().front(); +* // ... +* } +* if(auto ee = p12.end_entity_certificate()) { +* // ... +* } +* +* // Build +* Botan::PKCS12 out; +* out.set_friendly_name("My Bundle"); +* out.add_key(my_key); +* out.add_certificate(my_cert); +* for(const auto& ca : ca_chain) { +* out.add_certificate(ca); +* } +* const auto blob = out.export_to( +* Botan::PKCS12_Export_Options::modern("password"), rng); +* @endcode +*/ +class BOTAN_PUBLIC_API(3, 13) PKCS12 final { + public: + /// Construct an empty bundle. + PKCS12() = default; + + /** + * Parse a PKCS#12/PFX file. + * + * @param data the PFX file contents + * @param password the password to decrypt the file + * @throws Decoding_Error if parsing fails + * @throws Invalid_Authentication_Tag if MAC verification fails + */ + PKCS12(std::span data, std::string_view password); + + /** + * Private keys stored in the bundle, in the order they appear in the + * PFX (for a parsed file) or in insertion order (for a built one). + * PKCS#12 allows multiple keys per file; parsing currently surfaces all + * KeyBag / PKCS8ShroudedKeyBag entries. + */ + const std::vector>& private_keys() const { return m_private_keys; } + + /** + * Certificates stored in the bundle, in the order they appear in the + * PFX or in insertion order. The end-entity certificate (if any) is + * not separated from CA/intermediate certificates at storage level; + * use @ref end_entity_certificate to obtain it. + */ + const std::vector& certificates() const { return m_certificates; } + + /** + * @return the first certificate whose subjectPublicKeyInfo matches one + * of the stored private keys, or @c nullopt if none match + * (e.g. a certificate-only or key-only bundle). + */ + std::optional end_entity_certificate() const; + + /** + * Convenience helper: every certificate except the one returned by + * @ref end_entity_certificate. Returned in storage order. + */ + std::vector ca_certificates() const; + + /** + * Friendly-name attribute attached to the private key / end-entity + * certificate bag, if present. + */ + const std::optional& friendly_name() const { return m_friendly_name; } + + /** + * localKeyId attribute attached to the private key / end-entity + * certificate bag, if present. + */ + const std::optional>& local_key_id() const { return m_local_key_id; } + + /** + * OIDs of bag types encountered during parsing but not handled by this + * implementation (e.g. SecretBag). Empty for normal files and for + * bundles constructed in-memory. + */ + const std::vector& unknown_bag_types() const { return m_unknown_bag_types; } + + /// Add a private key. PKCS#12 supports multiple keys per file. + void add_key(std::shared_ptr key); + + /// Add a certificate. End-entity vs CA is determined at export time + /// by matching against stored keys. + void add_certificate(X509_Certificate cert); + + /// Set (or replace) the friendly-name attribute. + void set_friendly_name(std::string name); + + /// Clear the friendly-name attribute. + void clear_friendly_name(); + + /// Set (or replace) the localKeyId attribute. + void set_local_key_id(std::vector id); + + /// Clear the localKeyId attribute. + void clear_local_key_id(); + + /** + * Serialize the bundle as a PKCS#12/PFX file. + * + * @param options export options (password, algorithms, ...). + * @param rng RNG used to generate salts, IVs and (if requested) the + * localKeyId when none is set explicitly. + * @throws Invalid_Argument if @p options is internally inconsistent + * (e.g. unsupported algorithm or empty password with MAC). + * @throws Invalid_Argument if a stored private key does not match any + * stored certificate (this implementation requires the + * end-entity cert to be present when a key is exported). + */ + std::vector export_to(const PKCS12_Export_Options& options, RandomNumberGenerator& rng) const; + + private: + std::vector> m_private_keys; + std::vector m_certificates; + std::optional m_friendly_name; + std::optional> m_local_key_id; + std::vector m_unknown_bag_types; +}; + +} // namespace Botan + +#endif diff --git a/src/lib/pkcs12/pkcs12_pbe/info.txt b/src/lib/pkcs12/pkcs12_pbe/info.txt new file mode 100644 index 00000000000..32bba22555e --- /dev/null +++ b/src/lib/pkcs12/pkcs12_pbe/info.txt @@ -0,0 +1,23 @@ + +PKCS12_PBE -> 20260305 + + + +name -> "PKCS#12 PBE" +brief -> "PKCS#12 password-based encryption (RFC 7292 Appendix B)" + + + +asn1 +cbc +des +sha1 +sha2_32 +aes +pkcs12_kdf +pbes2 + + + +pkcs12_pbe.h + diff --git a/src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.cpp b/src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.cpp new file mode 100644 index 00000000000..1d87a0938c3 --- /dev/null +++ b/src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.cpp @@ -0,0 +1,168 @@ +/* +* PKCS#12 PBE (RFC 7292 Appendix B) +* (C) 2026 Damiano Mazzella +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace Botan { + +namespace { + +// Maps a PKCS#12 PBE OID/name to its cipher parameters +struct PKCS12_PBE_Params { + OID oid; + std::string cipher_name; + size_t key_len; +}; + +PKCS12_PBE_Params pkcs12_pbe_params_for_oid(const OID& oid) { + if(oid == OID::from_string("PBE-SHA1-3DES")) { + return {oid, "TripleDES/CBC", 24}; + } + if(oid == OID::from_string("PBE-SHA1-2DES")) { + return {oid, "TripleDES/CBC", 16}; + } + throw Decoding_Error(fmt("Unsupported PKCS#12 PBE algorithm: {}", oid.to_string())); +} + +PKCS12_PBE_Params pkcs12_pbe_params_for_algo(std::string_view algo) { + if(algo == "PBE-SHA1-3DES") { + return {OID::from_string("PBE-SHA1-3DES"), "TripleDES/CBC", 24}; + } + if(algo == "PBE-SHA1-2DES") { + return {OID::from_string("PBE-SHA1-2DES"), "TripleDES/CBC", 16}; + } + throw Invalid_Argument(fmt("Unsupported PKCS#12 PBE algorithm: {}", algo)); +} + +// Derive key and IV via PKCS#12 KDF; expands 2-key 3DES (key_len==16) to 24 bytes. +// When @p openssl_empty_pwd_compat is true and the password is empty, the KDF +// is fed an empty byte string (matching OpenSSL's non-conforming behavior); +// otherwise the RFC 7292 encoding (two-byte {0x00,0x00} terminator for any +// UTF-8 password, empty encoded as {0x00,0x00}) is used. +std::pair, secure_vector> pkcs12_derive_key_iv(std::string_view password, + const std::vector& salt, + size_t iterations, + const PKCS12_PBE_Params& params, + bool openssl_empty_pwd_compat) { + constexpr size_t iv_len = 8; // DES/3DES block size + secure_vector key(params.key_len); + secure_vector iv(iv_len); + + const auto hash = HashFunction::create_or_throw("SHA-1"); + const bool ossl_empty = openssl_empty_pwd_compat && password.empty(); + + if(ossl_empty) { + // Feed an empty password byte string to the KDF (OpenSSL compat). + pkcs12_kdf({key.data(), params.key_len}, {}, {salt.data(), salt.size()}, iterations, 1, *hash); + pkcs12_kdf({iv.data(), iv_len}, {}, {salt.data(), salt.size()}, iterations, 2, *hash); + } else { + const PKCS12_KDF kdf_key(hash->new_object(), 1, iterations); + kdf_key.derive_key(key.data(), params.key_len, password.data(), password.size(), salt.data(), salt.size()); + + const PKCS12_KDF kdf_iv(hash->new_object(), 2, iterations); + kdf_iv.derive_key(iv.data(), iv_len, password.data(), password.size(), salt.data(), salt.size()); + } + + if(params.key_len == 16) { // 2DES: expand to 24 bytes by repeating the first key + key.resize(24); + std::copy(key.begin(), key.begin() + 8, key.begin() + 16); + } + return {std::move(key), std::move(iv)}; +} + +} // namespace + +secure_vector pkcs12_pbe_decrypt(std::span ciphertext, + std::string_view password, + const AlgorithmIdentifier& pbe_algo, + bool openssl_empty_pwd_compat) { + const OID& oid = pbe_algo.oid(); + + if(oid == OID::from_string("PBE-PKCS5v20")) { + // PBES2 (RFC 8018) uses PBKDF2, which is unambiguous for empty + // passwords, so the OpenSSL empty-password quirk does not apply. + return pbes2_decrypt(ciphertext, password, pbe_algo.parameters()); + } + + std::vector salt; + size_t iterations = 0; + BER_Decoder(pbe_algo.parameters()) + .start_sequence() + .decode(salt, ASN1_Type::OctetString) + .decode(iterations) + .verify_end() + .end_cons(); + + if(iterations == 0 || iterations > PKCS12_MAX_ITERATIONS) { + throw Decoding_Error(fmt("PKCS#12 PBE has invalid iteration count: {}", iterations)); + } + + const auto params = pkcs12_pbe_params_for_oid(oid); + auto [key, iv] = pkcs12_derive_key_iv(password, salt, iterations, params, openssl_empty_pwd_compat); + + auto cipher = Cipher_Mode::create_or_throw(params.cipher_name, Cipher_Dir::Decryption); + cipher->set_key(key); + cipher->start(iv); + + secure_vector plaintext(ciphertext.begin(), ciphertext.end()); + cipher->finish(plaintext); + + return plaintext; +} + +std::pair> pkcs12_pbe_encrypt(std::span plaintext, + std::string_view password, + std::string_view algo, + size_t iterations, + RandomNumberGenerator& rng) { + if(iterations == 0 || iterations > PKCS12_MAX_ITERATIONS) { + throw Invalid_Argument(fmt("PKCS#12 PBE: iteration count must be between 1 and {}", PKCS12_MAX_ITERATIONS)); + } + + if(algo == "PBES2-SHA256-AES256") { + auto [aid, ct] = pbes2_encrypt_iter(plaintext, password, iterations, "AES-256/CBC", "SHA-256", rng); + return {std::move(aid), std::move(ct)}; + } + if(algo == "PBES2-SHA256-AES128") { + auto [aid, ct] = pbes2_encrypt_iter(plaintext, password, iterations, "AES-128/CBC", "SHA-256", rng); + return {std::move(aid), std::move(ct)}; + } + + const auto params = pkcs12_pbe_params_for_algo(algo); + + std::vector salt(8); + rng.randomize(salt.data(), salt.size()); + + auto [key, iv] = pkcs12_derive_key_iv(password, salt, iterations, params, false); + + auto cipher = Cipher_Mode::create_or_throw(params.cipher_name, Cipher_Dir::Encryption); + cipher->set_key(key); + cipher->start(iv); + + std::vector ciphertext(plaintext.begin(), plaintext.end()); + cipher->finish(ciphertext); + + std::vector enc_params; + DER_Encoder(enc_params).start_sequence().encode(salt, ASN1_Type::OctetString).encode(iterations).end_cons(); + + return {AlgorithmIdentifier(params.oid, enc_params), std::move(ciphertext)}; +} + +} // namespace Botan diff --git a/src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.h b/src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.h new file mode 100644 index 00000000000..943f118f01f --- /dev/null +++ b/src/lib/pkcs12/pkcs12_pbe/pkcs12_pbe.h @@ -0,0 +1,60 @@ +/* +* PKCS#12 PBE (RFC 7292 Appendix B) +* (C) 2026 Damiano Mazzella +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_PKCS12_PBE_H_ +#define BOTAN_PKCS12_PBE_H_ + +#include +#include +#include +#include +#include +#include + +namespace Botan { + +class RandomNumberGenerator; + +/// Maximum allowed iteration count for PKCS#12 KDF/PBE/MAC operations +inline constexpr size_t PKCS12_MAX_ITERATIONS = 1'000'000; + +/** +* Decrypt data protected by PKCS#12 PBE (RFC 7292 Appendix B) or PBES2. +* @param ciphertext the encrypted data +* @param password the decryption password +* @param pbe_algo the AlgorithmIdentifier from the EncryptedData structure +* @param openssl_empty_pwd_compat if @c true and @p password is empty, the +* password is fed to the PKCS#12 KDF as an empty byte string (matching +* OpenSSL's non-conforming behavior) instead of the RFC 7292 form +* (a two-byte {0x00,0x00} terminator). Has no effect on PBES2 or when +* the password is non-empty. +*/ +secure_vector pkcs12_pbe_decrypt(std::span ciphertext, + std::string_view password, + const AlgorithmIdentifier& pbe_algo, + bool openssl_empty_pwd_compat = false); + +/** +* Encrypt data using PKCS#12 PBE (RFC 7292 Appendix B) or PBES2. +* @param plaintext the data to encrypt +* @param password the encryption password +* @param algo algorithm name: "PBES2-SHA256-AES256", "PBES2-SHA256-AES128", +* "PBE-SHA1-3DES" (legacy), "PBE-SHA1-2DES" (legacy). +* Higher-level APIs default to "PBES2-SHA256-AES256" via PKCS12_Options. +* @param iterations PBKDF iteration count +* @param rng a random number generator +* @return the AlgorithmIdentifier and encrypted data +*/ +std::pair> pkcs12_pbe_encrypt(std::span plaintext, + std::string_view password, + std::string_view algo, + size_t iterations, + RandomNumberGenerator& rng); + +} // namespace Botan + +#endif diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py index 8a4e3647423..2d003b5acf6 100755 --- a/src/scripts/test_cli.py +++ b/src/scripts/test_cli.py @@ -1600,6 +1600,143 @@ def cli_pk_encrypt_tests(tmp_dir): test_cli("pk_decrypt", [rsa_priv_key, ctext_file, "--output=%s" % (recovered_file)], "") test_cli("hash", ["--no-fsname", "--algo=SHA-256", recovered_file], rng_output_hash) +def cli_pkcs12_tests(tmp_dir): + if not check_for_command("pkcs12_export") or not check_for_command("pkcs12_info"): + logging.info("Skipping PKCS#12 CLI tests: pkcs12_export/pkcs12_info not available") + return + + priv_key = os.path.join(tmp_dir, 'leaf.key') + cert_file = os.path.join(tmp_dir, 'leaf.crt') + pfx_file = os.path.join(tmp_dir, 'leaf.pfx') + out_key = os.path.join(tmp_dir, 'out.key') + out_cert = os.path.join(tmp_dir, 'out.crt') + + test_cli("keygen", ["--algo=ECDSA", "--params=secp256r1", "--output=" + priv_key], "") + test_cli("gen_self_signed", [priv_key, "PKCS12Test", "--output=" + cert_file], "") + + # Basic export (default: PBES2-SHA256-AES256, SHA-256 MAC) + test_cli("pkcs12_export", + ["--pass=hunter2", "--output=" + pfx_file, priv_key, cert_file], "") + + # pkcs12_info info-only (no output file args) + # Note: logging.error() is a hard failure in this harness (TestLogHandler increments TESTS_FAILED) + import_info = test_cli("pkcs12_info", ["--pass=hunter2", pfx_file], None) + if "ECDSA" not in import_info: + logging.error("pkcs12_info info missing key algorithm: %s", import_info) + if 'PKCS12Test' not in import_info: + logging.error("pkcs12_info info missing subject: %s", import_info) + # SHA-256 fingerprint should be in info output + if "SHA-256 Fingerprint" not in import_info: + logging.error("pkcs12_info info missing SHA-256 fingerprint: %s", import_info) + + # pkcs12_info: extract key and cert to files + test_cli("pkcs12_info", + ["--pass=hunter2", "--key-out=" + out_key, "--cert-out=" + out_cert, pfx_file], + None) + + if not os.path.exists(out_key): + logging.error("pkcs12_info did not write key file") + if not os.path.exists(out_cert): + logging.error("pkcs12_info did not write cert file") + + # Roundtrip: cert extracted from PFX must match original (DER fingerprint comparison) + fp_orig = test_cli("cert_info", ["--fingerprint", cert_file], None) + fp_extr = test_cli("cert_info", ["--fingerprint", out_cert], None) + fp_prefix = "Fingerprint: " + orig_fp = next((line for line in fp_orig.splitlines() if line.startswith(fp_prefix)), None) + extr_fp = next((line for line in fp_extr.splitlines() if line.startswith(fp_prefix)), None) + if orig_fp is None or extr_fp is None: + logging.error("pkcs12 roundtrip: cert_info did not produce fingerprint") + elif orig_fp != extr_fp: + logging.error("pkcs12 roundtrip: certificate mismatch (orig=%s extr=%s)", + orig_fp, extr_fp) + + # Roundtrip: public key derived from extracted key must match original + pub_orig = os.path.join(tmp_dir, 'orig.pub') + pub_extr = os.path.join(tmp_dir, 'extr.pub') + test_cli("pkcs8", ["--pub-out", "--output=" + pub_orig, priv_key], "") + test_cli("pkcs8", ["--pub-out", "--output=" + pub_extr, out_key], "") + fp_orig = test_cli("fingerprint", ["--no-fsname", pub_orig], None) + fp_extr = test_cli("fingerprint", ["--no-fsname", pub_extr], None) + if fp_orig != fp_extr: + logging.error("pkcs12 roundtrip: key mismatch after import (orig=%s extr=%s)", fp_orig, fp_extr) + + # Export with explicit legacy cipher for compatibility testing + pfx_legacy = os.path.join(tmp_dir, 'legacy.pfx') + test_cli("pkcs12_export", + ["--pass=hunter2", "--key-cipher=PBE-SHA1-3DES", "--mac-digest=SHA-1", + "--iterations=2048", "--output=" + pfx_legacy, priv_key, cert_file], "") + info_legacy = test_cli("pkcs12_info", ["--pass=hunter2", pfx_legacy], None) + if "ECDSA" not in info_legacy: + logging.error("pkcs12_info (legacy cipher) missing key algorithm: %s", info_legacy) + + # Export with friendly name + pfx_named = os.path.join(tmp_dir, 'named.pfx') + test_cli("pkcs12_export", + ["--pass=hunter2", "--friendly-name=MioTest", + "--output=" + pfx_named, priv_key, cert_file], "") + info_named = test_cli("pkcs12_info", ["--pass=hunter2", pfx_named], None) + if "MioTest" not in info_named: + logging.error("pkcs12 friendly-name not preserved in info output: %s", info_named) + + # Export with MAC SHA-256 explicitly + pfx_sha256mac = os.path.join(tmp_dir, 'sha256mac.pfx') + test_cli("pkcs12_export", + ["--pass=hunter2", "--mac-digest=SHA-256", + "--key-cipher=PBE-SHA1-3DES", "--iterations=2048", + "--output=" + pfx_sha256mac, priv_key, cert_file], "") + test_cli("pkcs12_info", ["--pass=hunter2", pfx_sha256mac], None) + + # Export with cert cipher + pfx_certenc = os.path.join(tmp_dir, 'certenc.pfx') + test_cli("pkcs12_export", + ["--pass=hunter2", "--cert-cipher=PBE-SHA1-3DES", + "--key-cipher=PBE-SHA1-3DES", "--iterations=2048", + "--output=" + pfx_certenc, priv_key, cert_file], "") + test_cli("pkcs12_info", ["--pass=hunter2", pfx_certenc], None) + + # Export with CA chain and test --chain-out + ca_key_file = os.path.join(tmp_dir, 'ca.key') + ca_cert_file = os.path.join(tmp_dir, 'ca.crt') + pfx_chain = os.path.join(tmp_dir, 'chain.pfx') + chain_out = os.path.join(tmp_dir, 'chain.pem') + + test_cli("keygen", ["--algo=ECDSA", "--params=secp256r1", "--output=" + ca_key_file], "") + test_cli("gen_self_signed", [ca_key_file, "TestCA", "--output=" + ca_cert_file], "") + + test_cli("pkcs12_export", + ["--pass=hunter2", "--key-cipher=PBE-SHA1-3DES", "--iterations=2048", + "--output=" + pfx_chain, priv_key, cert_file, ca_cert_file], "") + test_cli("pkcs12_info", + ["--pass=hunter2", "--chain-out=" + chain_out, pfx_chain], None) + if not os.path.exists(chain_out): + logging.error("pkcs12_info --chain-out did not produce a file") + + # Export key with output password protection + out_key_enc = os.path.join(tmp_dir, 'out_enc.key') + test_cli("pkcs12_info", + ["--pass=hunter2", + "--key-out=" + out_key_enc, + "--out-key-pass=keypassword", + "--key-pbkdf-iter=2048", + pfx_file], None) + if not os.path.exists(out_key_enc): + logging.error("pkcs12_info --out-key-pass: key file not written") + else: + with open(out_key_enc, encoding='utf-8') as f: + key_pem = f.read() + if "ENCRYPTED" not in key_pem: + logging.error("pkcs12_info --out-key-pass: output key is not encrypted") + + # Export with empty password and no MAC (key is still encrypted via PBE) + pfx_noenc = os.path.join(tmp_dir, 'noenc.pfx') + test_cli("pkcs12_export", + ["--pass=", "--no-mac", "--key-cipher=PBE-SHA1-3DES", + "--output=" + pfx_noenc, priv_key, cert_file], "") + info_noenc = test_cli("pkcs12_info", ["--pass=", pfx_noenc], None) + if "ECDSA" not in info_noenc: + logging.error("pkcs12_info (no-mac) missing key algorithm: %s", info_noenc) + def cli_uuid_tests(_tmp_dir): test_cli("uuid", [], "D80F88F6-ADBE-45AC-B10C-3602E67D985B") @@ -1899,6 +2036,7 @@ def main(args=None): cli_pbkdf_tune_tests, cli_pk_encrypt_tests, cli_pk_workfactor_tests, + cli_pkcs12_tests, cli_psk_db_tests, cli_rng_tests, cli_roughtime_check_tests, diff --git a/src/tests/data/pkcs12/cert-aes256cbc-no-key.p12 b/src/tests/data/pkcs12/cert-aes256cbc-no-key.p12 new file mode 100644 index 0000000000000000000000000000000000000000..2550c8e317bfb7c04d3c15a90a187887c3f6e6d4 GIT binary patch literal 605 zcmXqLVv1y9WHxAGl4s-8YV&CO&dbQoxS)wifTf9v2Pn(|#4HFYwgpX$Uk#cVKQb{g z0I6398Abz7IER(jz{Wrt!R0aFW8rAi=zd?(R?x%5#KB-7%f^||=E0cC%)+R}B2Z^{ zh5e08YRP?7s}q~74TBn*7$>MEne1U&uICYcZrO$a#4t9ma~Or9uXdcJR!!8KWSmahd@`C0T^ zn|(LS33-aGonU42>uk<{Ta87VKNsxJozi3T(>+8v=un_k;ZZy7(26GuL=M>mxtVpl zDQ*;=!De3nn8$=|stxMzh5VYj>>BCoUY`a-xJlnv~K+%Acja8eEnMsP3fkmV`_*wWq>2FLu?JaxX YxGxIze$SJ~!lBCgp#HaHbTKHF0d}MDw*UYD literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/cert-key-aes256cbc.p12 b/src/tests/data/pkcs12/cert-key-aes256cbc.p12 new file mode 100644 index 0000000000000000000000000000000000000000..5bb25fa0b671512f6a0d418443b9b1ea559b4af4 GIT binary patch literal 948 zcmXqLV&1^S$ZXKWT*k(!)#lOmotKfFaX}MvI!hCCGEg|)povKZA;q?!iAfYFB*?_b z0HnAOGK>bEa1JZ4fsKJQg3DvT$HJkpXU+3pJsNwNm^c^=WZ5_q+B_IjnOPXMSOnDf zDlXn_diILoqPCS8cW!QKXky&3s%q2DjkDO2GZt4DR!3He50Y6 zBDebU^~k$FvOaBJwbF#u$70FzLvx=dI;QNpQ6+ydZ1Iw1v65MrvMVx@=KM`kpKf6bg%|B&a zU+r1JBOn-B{cp!F0ll?#`KKPeUNz~%>f_5-Hi#}!OcuL0-Qe=`W{zD#mab<)OXkFs zF_?rl^5~umx+%_cBBgryydz73EaQ3&itZUoGp;+gH225mm+24wuJHc0}u-# zN-6Z7?z1$lBl3NMx&PSsEL^|1t=e&5tLUdk!VWK-OqY9~U3)0KeCo+v#R5xTboN`_ zTBMRxy4Fs|KjkD3%l^1{-G~aUW8!>?eh#yqZY_ulJM#By9IJ}_mh!3d9+ldkdw*;t z8^@akWsX{r22G5{@E8XrAx0IJCPoFIunZ7OuyI3E7Y`RB(}Ko_295U!C780{S*O$G zM6o59+j(g`%TG2b-PrtCvgc{Z0+z-J7C#;0t>QkO(BaWzY35eDQ2Bc8!7to3YDTjc zh5GDx_tt&;i>fJY#Tm~+nrt(aAGq0<7io~)keD@>ZWI$%W z@DazI?Kf*4zF$@7nKf_I8#eb{`2kTImwWtXP>w7LwU1WGpL;^>nvYOWVc=DjE{{J8 z^rUhWPh5Om7qWH2{?G3&wFCe-H)#RH^|CFAm9_rr;ZmL}U)A(zl zj)9?pq5&rxt2Q4qlN2ihi%1;HGU2lV?>yZcHy*BXTNrF6qW7AGBeuSl`_-qp_d%Hl E0Biq=yZ`_I literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/cert-none-key-none.p12 b/src/tests/data/pkcs12/cert-none-key-none.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b3f5c2a84f446a9ccd238fc7d091fd1d18bfb8e6 GIT binary patch literal 756 zcmXqLV*0?u$ZXKWw2h5ZtIebBJ1-+Uwg1^0IzcLa@(%>u(znKo?o8TieV!+OpVap)3;DO~Je}9JIpAkb;QoV6 z*Y_R$$+}qBK+u36=zdvYM#ldvtOm?L3fZ^Jo(u+VOo|K_h2Hm0kILJz&2jnp1J9S_ zt-BTYBqc=qUfa|?-{ofgj0P!5Y&(}`_(VTs@7ELBn_bSS{8-9eB&cYzQ`9S-Pu*yx zp{jv0JaRZi4aHbQR879w^iSz|>Y@Iv;HJveKaIZ@>KHWMfU5$=ZsRGI#^VN!M+_Pd zB1a!kqRXJMor#eFYuK>l8e}suGO{QzAImyF`*fwOob6P(9`U;;PA;F*C-Uq8C)3o) z8&3Xrd%Dnt8o@}c*9;95fk}r|n~#}Eij{#yL-u(&OX~= z;iTyEX#a^@Jo}i0pOoHVTXyu+`nk)J4f<8` z)h|u(;y<)p`IU(1Q9&J@WhZw2j1(=CmE%=nH|?H(;Q7gI*EjB&I3d68MO2naOT5y< zDz5xBd9m|C zk;akPhfB`9dn|ur`JM-p%AY<7xBC2I>;BSLKLYxvnz*vxvA=5EBsq z`+S~lFZXXVd(s?ndDb_BXXiIee`Bh+XnQN8PqY0M$Naa)j;U@~P_=}u)aGT%^v^~9 z8*O*-_4Gv^Tdwpi%_H{k&kqJWcS@uHBbW&(f)_M?VQKtm(D=@v@iiMaEULK}nHDr| zF=*U~693Gg_-Eb9bn4wV&X=J0XK9@9&G7MKw#CbWCa=5rG<)Uj{!31qmMf(B<)r0a z{BGlB?|H{l>q)lQkw=e&8!Rou7MJ?3RsAQNwnJPwsy^`b&H7M=a<3I9Qy(u{;4=Hm zkEPO#=Qrrd|9IB5`ds{!h1Cax=U#bxHTTj2?;Q`+td@Oty0*giUhM0$cf;miE}FLN zmRzQ0PwYv76 z3v~<(4HOMH*;uvtn3<$l8CXO#875A6A>etqwA?22%AMt|zO~NMEF9bKnZ+-hdRGzT FSpeIMcN+iz literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/envelopeddata_content.pfx b/src/tests/data/pkcs12/envelopeddata_content.pfx new file mode 100644 index 0000000000000000000000000000000000000000..0afd0a8f75ee38913471fe04de8d79e4f9e54e81 GIT binary patch literal 41 ocmXp|XJTYFP-5fMYV&CO&dbQoxImOe*g(*LA0foNfXRRX0G~bx1ONa4 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/java-truststore.p12 b/src/tests/data/pkcs12/java-truststore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..02d8e7220f2a3f78f3895af930915fd8d627e243 GIT binary patch literal 2845 zcmchZd0bN47RNboKt!<=CDT;Qv>bwc#39Wg)XLN*b4s%kB)|y-%;{Q5ighcq!AjG# z$RR7WvK%TiwaFa1)q8qN({5Ma)vPqF%%*o}`qcfr_wW7Vd=Bfk*WP=vSnIn0GPUVg z2nS@O4uYzM+*AIB45>hcGExI9BV9t}8uY39d8r6pI*Q8qK!(3WQ2V+|odn?uW%!M# zC?=4;imFufEIb>Q;n$*iF#6~f;;;}FtBO%beEZI7Gvq*(pn`h0&x3J*hp3?@&L1XF zu>PwM4WOd~<}aqvPleXX@GIHpdSv zPA%WGVuglre7s+==<@UuH!=sL_4~&#TdXuv{5BPhN2nC)Pi)A2a(Nh@d)T6XV0gR$ zQd`Boi$=J<5%PgRtFcOdM+FAR#()o<}7oF5G22_VA#qunG6pAy(R$n19z+OI9{_n_E+0iB0y! zvgUoWuVOs!y-w7Iof{|Fi+K^zj<*)3c@b?@uOF?lt82l@AUsBy2p{5={+FE_mfR$D zq_`VJ{GsJ-oOEetK}1V@bnl6U^EU*Hy7<|pwXF+B6x#MF7FP=S)kO80o(673k_y)pVC@8{&(*^vfQ$*&gd}xv!aHQXWCWs_HpUgEsHG zW-iin>ekE=)vEXu5r%HfaK5+EI&{1u(n0fuS&A9kF?dGkc=2zpGZd+AWp%v_>4^nfK>n3pO9*~;3<$wAdjXW;+$9i7&1N)SG zGoZkDfP8=O zJF9iw4_sxYF4Y+QJx)I8h`;On~v1^YwnxX{*dZ#~E7fT2!Xv+1UfFW1|S ziuN+9YrI+blP8YY5`%`%STyr9)D%zR{u~%9VaL`brmi>Y+>=`{b|--sdsCp1y{F^j zK+fQLC(qVRElcCIX5}x|4}MxZY-o>}k#{P&1V7a7_4G2U-|kTD?GMpT2l%}v7H(PU zaw+fT=Azsh-!N^8D|b+qar@zQ*Sf^fQ!BN5RB;oO+d z0-Ym_2J^q~!O9t5_k@W=lAqleVzg!?jBtrm`GFIbz@&@pIiLR+piQp}s9Us~OU@im zwu+s6_}Duu?o+eQ`txcKS8n-jlQ`;caijI_yL`Ey zJ1dCk?(0M6h@*ZWS*ce92Ttj!8~Dwkx_BY?#aQX^(H~0`Z85Y&AxsamQ*(?HG_OZj z=bfs5{l?D^@{IhU zuDal(P;U1~PlQ24mrJ0@kzzxbo%^X1-%@Sl+CcKWw7>EKm)&>OuU}ztn$|oxaIyR5 z>+?<4ibJjOr2~mv70cUmJ~~o;Q`>#j>Fc)s^)CYRU3^sgf+6c8Mtj$=u1cdqLP_hG ztxF3|%^P7PTe0(hSL_j*>NB#=Okd>udXs)}Qbw&JDOz{Zv}jL;PTxx^BdC(Vu;QCt z_deA2pg{fY=B#J#URoZ@gp#B=L&wWvl6_J#diri*JxVevnruR<+z|8!{y*Gf0YsTG zB@?+g7>hPFOzTbV;vL%94aAn-TxjBz^@ru<*IrL-6=5`e((3ZB9e|0C#n0+b2Ys-{ IVu_gl0@P;>(f|Me literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/key_bag_unencrypted.pfx b/src/tests/data/pkcs12/key_bag_unencrypted.pfx new file mode 100644 index 0000000000000000000000000000000000000000..471f5fb093c3787e7aa4310e9baf9c1548cb6133 GIT binary patch literal 654 zcmXqLV(MaIWHxAGYG&iqYV&CO&dbQoxS)xtgr$k804SUT#2KhkkwB?Xpm3l;<199A zs3smRM#cq=T?UQqOpFW$!ffn7@pHCJjBFfW1~Vf&ORhmS6C)#wg7{hyzOyM|*ZR!Q z$KBtw)T?Uuix8X1rVI9!cka!a)A(zl3$qgg%a`)N1rduq%sj$%TeSbLo&B(1x=uER z|Mq3h)ZoeAkG(cN?s7B#-RtUoPZ~>pb}e77*)Nr}v3T#nm318p7H*mR(V&TO1G4Lw z7c?=>2MW$+OCG z4Y;8GU}0u55Hl1p5MpBvWnmWPPOV7HEyzjLOU}lf zxH$DiVht{8gu&tngD?X{pi^b}Sfp4adSZO1@`OHWX1wsiV*kW#kG~dq>OT?PxWIr1 zq(qpJ@jnZb0fT`uh^NdVV<6RlTMJTvFncf5XnmFLanr z?lF9~{q3p^vtoH|Pctbp+KCBQh-tkK;amJCdS=t z+)!OST#U>Mniy9B1(&gLK?OO*fMzjHV`*ZX1QhQBV$%i8OpHuSf($p`clfX-GCgQ{ z;L;OmQ`%v`#l`{Db|4@z zzFl+r^HjC;%|Yp*zTpM}OZV(#l(9InVDc5luDeSQJze&6*1IdKy*;O$zV0}!b1IkL z=ADZp48jZ)fzFoYW07K!a1j*F;#zFF!KWcT=;Wb>tuw>jH4c|fur=TTDG_F5{LjK< zz+j*Z;wiJp7)UkX)`Ao`%ZE=&rBmz|iy{SRDuC=l52?9JswPJOS(A4KmnH-1*M zxRj*#gGs^9z9_NbB!v6*bTU|)IX-mE!|zZSZH z!tzUb;DU(79%dflx-HuO*Uo;}FI^`a!+-lSXKL`|@5f#nA9uN#|L%45z9)?(Kf9JM W*X)-{+E~2z;L5s=1q-)K{s;iLwd0}y literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-1-no-pwd.p12 b/src/tests/data/pkcs12/name-1-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..72a18eedf1cc5861e70edf0c3417c39acf2f728f GIT binary patch literal 3539 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-1-pwd.p12 b/src/tests/data/pkcs12/name-1-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..da279ffea958146288ffba99d3569f08a22c31b6 GIT binary patch literal 3639 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-2-3-no-pwd.p12 b/src/tests/data/pkcs12/name-2-3-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..e66d9599454366cbc730aa4c95fdc71a5e87ca32 GIT binary patch literal 3547 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-2-3-pwd.p12 b/src/tests/data/pkcs12/name-2-3-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..381dfbe9660b5622274d68f062215695dee3f75b GIT binary patch literal 3650 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 P$f$fnkaP@byY>G9>{-^V literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-2-no-pwd.p12 b/src/tests/data/pkcs12/name-2-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..2fb99fb0b540fa1fc3da271b28795cdc2c2d5e2f GIT binary patch literal 3518 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-3-no-pwd.p12 b/src/tests/data/pkcs12/name-3-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b2b5aad2141865cb7a93f25025a4f8166aaa919d GIT binary patch literal 3518 zcmb`KX;f3!7RPgwn~;P6N+5^`gn3TRC1KEF0RaU>&@hOo4?;*G5(p##L{ubdsPhQ5 zR1`!E4vbY16%;{55VTe6NX05>ah4^ZqV@#PCAROaet2J!b@tuooPF+p@88)MD722_ z5du(Xts+tEmHx^BEkpw;QfQ^23awOVP67(r#xaj**{Fh+1zNn#~YX!Xkl&u0~qEEe?gRi7RMcP=3u~`1K{xAFEkI( zNgBiZkVh$5KHJ_JkWvh8`BZwBT57W`1j>$2?01T z9dCyFMfdrJAEJ_v9radSI{k2Oa>cDV58}cceplw5955+(Ghgk(S9W}dJz$5`zix5( z*|)=TuvAkz-i%wP9~iA|C@wTKh#Bhd?d;+Tb!0j*XK;qaUP@#~Sk&(fI`5}sv$;2#yB#A8* z$|Px0fl$U4q{x`I0y~V0v4PXb6l-5e5?_?e^pPaXr4lhSf-e@u@#Vre7}BRWZFzRs zBrtI#db)c3FVm1Ek^lwX{L8n35OD+rUIz#5RJ;O@;Pyz)X4b4RUzZhF_+;%bM`~>B zva2<2u^e(BGuGz}Vt3a3(AAN@Yge zZPr1L;LQHMTfe2x-fAezr<-jbDiL{ZeWp!cyU~7bBA%GXz3Li3+3&dg`&sWc&kA2e zX?N|AR(Fisz5a!XN7sIDs%7>5j|*C^QY)S@I;(F{Lc?v&SM;*hwoYnGoc6Azye*n$ z5Ye&pj+5spgrC}&UlqEt%=3M78mCIvaaD+hy_-Orb>q(X+3n%u7{L3X4^Q|4_rcA$ zHln+^fF(QmKBQ@HzG>bA&{#so)f^s_>`P}@g2narF1=;g~j5$WYu3Ma?xz#_d`-D}+q ze>;avFdUMCOq-QMsMPgM}LaaULK%XB-|jdQgX1`8^K-KP!Q>mOgUu|&|Izhh`q zo?n9b>fj6I>$*uZdMI7e!C#>{=>q-yS!(7ZQjN68HG#RpPv=<9=fvhte&8Ld-2q4q%n4OtI>+J z2P?!ZH-pDpj`-ee$jPY~WSp7%cu8VI&Enr?<~qDz;)e@*I)Eo2IHW{@#6apT0H!dL zj4e<|wH10zJNt&NOeb?sbWG1KS~|b)4|3Z)FqcGjAR$De27;h7K>(aEItGZ6o*VD^y*wqdO2F z{YLhN7*>~+@p#l0SS$W1?GUfv&=1jY1&+Y}Q%HD(VWK{r44(gQWiA%R^Tl6QA!HLlfgm`J*~+7v zOzvH;_6})qvJu~&du46eckjHSo$nHzJl8LfXLB10T83=yuWR$@4Se^sh>b66+E6Mm zS$?&>=U#w+Wj(zon;UZ>CQ7pZV$Og(@x0DkJlwCrvuBS1K!)Byr_ zyhzR$Gebr3$$WX5RLC5mzmJ548328?7lE$r8yd#sNhESL^&>}^?+Q4W8|KOdTvy0^ zXY7k12>frCOOQ}*BV+m{52F~f0A?fWPaI=sdZ)<~BvO$)Q@xi0`8%Z%HKh?qX_zQG zuh3pZb+cyFN*B7V{5e4Ydat`!8(Q6~Z*X~;vXxzXY^FEy;Qn1M4;O^43`yx|W4AjG`XbJuz{^ zP1<=VOGWJM2z*@c-R-)guI+u2M}ERx3;SuCX-au~OLl3&F&T0~$1yku-mbDcdR6oEjb%;S zK2d5Jd?T4aS6z_9z=qxZ3kV&NY92$a0fG0^4G7d!)cj5UE*eJ3- z#Q+IoJ{kjT7Fg=NoEBp&u~04(j-(n(B8>+g>Qzlq*p&dRh+X8Qboew$D*h^NnD4xh zkT4(bNRAIL2vS%)LS1bXHfLpliv;Mh&f`75Os?JMCw$&l_N4f$5*tQXC+GV3K0%~= z=LG$Cl<nU&r6B#9@u*_*mh3)Pmd~c z3wv)Ik19qA?!4$+WW4Ccw{tCa?K44R3qD>aw$_?@o~HVr++KB57~9nt)?H#>&TRg( z=fs_s{v%D!B|9!=l=Wl^H5}S(KTKipayoc4R`kk8*GQaq8R8DFUguq=TNijY%Ej{+ zQlmK+XYD@Z{(_z57+ZL=WQ)Ygh`qVmaQf8#2kz&t$zpXUhfE&W(CKT@-xYwyFysr*{|o{y#nn{%xb~1kB)v5Ya)K qfS*Xjp?cQ_gKus5b$j%>qyDM9{gsB!Ln)I`a{ASZ>Yz;ATfPxHwQI*=2GJKDA!DDan7Lw?G3+bb=sQi^dQoz=XCNa{Sgfv4@0$D+( zf&cCIQ}gJ5b0b>u)#QgG!+afMJsycCXd+%qd?m>#R2byjSZv`dj1;FXJ`*bO5RD*; z{)~;mS{52D*jCCFjO$ z;PfVkBOg)B%%V%9(-_{$BCWBSP_#b#8)-*{8qC;g@qSH~ECs7|WiVyTmB$aXrRcZ! zuFkQ9f}LWrV$tshi%2x4ctPgWqtL8BF$Q4C0Nd2s-=|L#t75#Sl{Gl4%RIwq@4UN?K(CU`_SQBJ{ZnI~XgPtQNm8`MDs0H!bppNKq{CR<+Cq%& z`byKa<>v`dEc*O}ZS=s2K=7o+p-##|@uaXpLwn`&wdA>ErB^0%=U?8mDRsJ;z@rCo z?x(ayxE0qy(NXF{bUuXU%8cDfXP0k%FL$S<6*QjFQRq-a*$~_~j^Wzy^lDoW#iwJs zMHsryb&U6StX_*3cbUU@-5p;xiZm*(Mw&)8j`ZJ|L=}Wz^(mQh`edma7jB?l?y{`T zAr$^Ip?7%v8k|B^njhX*vTcV_7RSVZ(+i<(vuJm{`hMlqs?5Pz7nc5Tz zL;V_;&ozZW7z$&joXS*)he==!27(;j=HY-o9%*T=SvJCvE{9FjMII z7va3oSevbl3I(#XkS897$C)DX3p%u->-5{0Sx_OMaP^GD^5)dxuxeGrv1N6tdQlny zodR~mO!>>sWC+pLFMU|mTV+Col6|%dCc6E?hIoM`cZ6i;DQ(YK3 zn>65GgeDu`D(eCHuJz`Gig-0<_Nr0-nYSaA*hw7UVL}uP)O<{WudTSyd0WiSmvzH- z;H1i7NKLD8se@9!Gloq6W3%lIa#w2{<2ZwyYJ&W78NY zTkEXO*jjF6tz#sgwNyOY--D0X_K`B6SAauDbFTV>3BYw*{=iHpqk)}vKdFOj} zOmD@4d|{q_?;f95V_1|Lkb{?@}|wv+^P|yz%53)3XHYYvc9dbk zQ(Y%tO{o*K$QtxKOaGS+r`_@gj`YNFuP1k@wQ~^gSbchsSgel;(~r`|KwX1Gk|9oi z6uGC}b4fnTs}pyAhhumNrBH#Ii3TUJ3FwY!@gbSZ=S!j-AbiQLxZ1wQue#Ns-hUov z%Q;XaQu(OfbQLiq-JzS}*|2;PQ2%(YODe}1UU=~ki79I8d4`0>eYLL`Kw|z3PWCU% z{(2LBS}j2DZj?5sl&Wh88h@p(0}SgsAbG^l7sN`(WuKt~;Z;8-7bIrW^Nm7`e+*xa(+HjV z@ls7*B5KemFmZe(!3IFgiXV6D0h#y(K{g_riJ^k3a$Jh8k4Ew87l2*KbK z379oYZZoWYUB}Z6Wz+o>ZXRgFy)i)$xnWeiCWM#@jsWtRVwvRM?>_@9F+ATrPbR-b zzO!^4K*kIqKi1;-5Cd6C?d7+$T1KBM1&d1DDv(EZZI_bZ;7quPT?m)ukrGAEd6aJe zvm})`Y%ZHD;1hMJN1TBJQL&Yts<9tSsHMLLAihTeg5#9{CH_OnXAR#G9J_rOaGX!7W?pQ613sXCv7NE!-gZs82z|wXlX2!-ph8?1~W;zG)ubwH=cFC}z z@~|><9;Oc;#+{fOH@3w;iF6?w?S)(^W>o>D1c-EZUi}}vXqp(UDSC4XH0-=10hF|o zI`QcKxjGA=3q`h0)gAkM!V8B`Wt71Chc4j!=<0;OxHt`5E`I+pvh;M$xrBz_HLsn5 z1cqe}(5+zv3N8-@;kJv~M-dNVl+6l+-J!Z+sSGOWr$-4h!S7M5GTzWvqM2qmLVn?q zHJOh(ChEFpF4LC&_X!fEr@jOSL4QEB?6L}f3vqY1F{B1*g|D@2tht`J_{)Yo@-F6&Szu<5t zBf4@^K+JEMx0OCu2rLnZrP%mOfu?WHHg`(GFXRLBVF-4}sv!^hXa$uOM3(V$uuK(3 z_E;gfCS(%BWB;SGQP!A4Pen5)h84-sr5zEGGbmf6;Tjcq5{#(*&(>;Cw4L{EImdi& zQ`$Hzcv(H%QyVs3NH<$FddLJ9N>lctnmN}UHZg48)E+WaAQjnqk8R9>XpdN|%o&El z`>E%1NW_cIJ*?=`3fCrAqmhANNi4^5lMQJ^v+ zh$q%S$FK(+P+r$V%ll;u9MSgfnak^91fCY&pP4)ml3l?45V>UA`v;JXm)J|2I5kdb zok*nUt6ZXq2Jyh=6p5?6>(8^=S<9@hZ%VW{zvBhIkaY+y{uRt3`*3?p%9DNI4F9;TbyQp=RV4! z7`Q0jTh6*rbc-4{SxdmMn@C)2Tx1(iD#j&n)VldWHl%R|Rasla$zG8v{(n-MqSeBf zM)U_rkSAnOfn^z%U1x$eLzc>OH6VnB;+ku>EaO7?sRY39q3sIbt6FK_xl=rM?2y$QQ!plEM8Q+@(c7XHnm`ZFnft;pd$LY& zt%^IQPe|@J2N5+O=a(f@@bwDINu(Im>0x5sCbp0Ri53LIpSHx=9PiYq^izr;aQ0c` z=U+%9{e%4@lS5}6XcoeTJl3zo=3HenUCG z9JUY^1ieySRY)pD#_p?vtp1&}1Z`VEOjKZa)1Bs>C%^)iA9a?2!-Z|{TG^CIvES3# zyO98c%SWF^=tHya%4B~p=32^v3ANkI@&=!6;u|Fwv33Ul)*&LNQUAT1c8tnw{5~yo{-YxWMI>4b|U5h9Y<fr%y4t{43ykSEr-Vby9B90}VW{fawqKUe1D9Dh#=BiU7hQ)SgfpvFRj( z1+~{`#XUhFd;Ar1_dh) o0|FWa00a~R=?xy9thQniDp6aR6nvK#A~3-O2-)Y4kKMkM%Zq`|TmS$7 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-all-no-pwd.p12 b/src/tests/data/pkcs12/name-all-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f16920113a68a091e2de46fd99031d61876d89df GIT binary patch literal 3597 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 NN5i7a^yToePkAuf)H}$ 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/name-unicode-pwd.p12 b/src/tests/data/pkcs12/name-unicode-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..9c554aa2147a4d7b07ba8ecc13eb2e4192648e6a GIT binary patch literal 3672 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-key-aes256cbc.p12 b/src/tests/data/pkcs12/no-cert-key-aes256cbc.p12 new file mode 100644 index 0000000000000000000000000000000000000000..7c56b45047055546e52285298930cd25dd14ea70 GIT binary patch literal 353 zcmXqLVvJ>CWHxAGRA%GUYV&CO&dbQoxS)wqgr$j504U4@#2m;{jlWnLzZ*1uF=+hA z#tqfM!^Oz7pz)zW<2?gUxLQ_T0~-Tr1eeEvkA=f}((A^Q<>FyXOdJdbvTU3QZ61uN z%q)yrECS2q{{I)4^(}$-X^b_e6?YIzv Zx$ypXj}@HESvXG2wJAGlq8kVDBmn5Qh0p*1 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-name-2-no-pwd.p12 b/src/tests/data/pkcs12/no-cert-name-2-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..dcbe9aff1a8063a5f4b49074909c17f9c3223921 GIT binary patch literal 2874 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-name-3-no-pwd.p12 b/src/tests/data/pkcs12/no-cert-name-3-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..a0d22772408167b93f1ebf3d15b044a71eda96fc GIT binary patch literal 2874 zcmaKudpuNWAHe6#oSDYVP#FdhE}0ph6jG2$qosfsFJVeR`&Ux{H4L9aXmh8NQ!Dp7f%SAY7pg{{U6~ zK_E>k)o5re{54pHuSfN@Kn52T;;;}Fi*=;~^)F!|G9ClG5He6B;6h*m6&n(S(14ao zAW_x5_@ZEOvP4SvN#HRMIxtk}aa0`+H&GB16ts*UlvsLq~GBUWN7E{w_j%Gk}V9#~Cpzjmn9Vva@CM8Dg;d%#L z?!ldPBMr(X+J2XJS2M!f%Tr?>G0wOIq>T(eyqmH-M^BPRH9q)3A#lxksX^Vf*E%d7 zi_dkq!}d`-WxHvI|5&zv$Xc~lcAL1eZPw8rUK_e}opK|YRi661>gF9%*-KhyL@Mz<=Zc2cQwM5rkmqO>0)CKwJH($p-achz>_*VLi27DIZK}n z4IK|nDQz#9A9V-`U)=0m?W}kALh7gMA^%x?>GbfUJwJ1l`4M}MbTtm8STXKjuP)PB zRUY8H(G?nh|x)SDdx^aHG$$_r!C$bxgTA^7i7~DBC z-)+G#YlV;(56m=$cIKnU-W%rz^Xb{j5A};y`9AF{7=6rn$<^T8x{+G2q4m`@UVlmZ zhHF~9+JnayWbW^WH{-(*nigJLEV-uWuvsKVK36->#Ek@^~Q!W^4*WlD3W)qiS(r@x=fupEKPcdVB^@Yo1sNG*p>q zLeOUz#$oY=V6I9whgBmxnE?lVdBcv6)!f;zC9shFx3-tf{B{m(RPl^pGhhPfg@%O& z|ECxsps0+gs$#GJ13Xm{9bN=nzybm(O70EGM?Ik#Y+wtlr%J*?G(*+uBBGTwS_pJDgCO1Ho|yLrM#VePw$zaLw?#{Mzh&h>{?(oBcC{F@)< zJ>A{n((gMqSjfZ{H|#0m8E(4M+S~2pU0zG=&2)%rii!~LRIYX?{Vp;kr9i1puFy9* zcge65d81schFmQbay^F+8=2hR!6lM(l>Wlc@QzY$mZ>6x5YdaLSfjn%KCEQ>z- z?SU72+9)l5DLt$QP3}ahk&m0^ukOvqDb(r=GNbi43&(sG>retX66Muq;#2Rv3f&eE zwTT4zSsEWq5(c91?)ZlGa2cvRJ)QP&J}iUtP<;-N;nF8)KZ^vpEx}-*pzV^ife0~=+&J@SLjJF_S3XCV>&zYvDyCqBGL}Gz7 zO_fUq{gcv=iqa5DX|z!ew^Lq6^f0`t#jBmR{yUBb`tLbf=$Uud);SI(<}j zYW7dnG%N7Zk9bitVCaM~I9!sU#P_%PzNmHUVIQh#`MAy{FYd9a^^$CgTpD{bvnc;O zhpR<(bbOyjW&h+s-IwX^5*yw-(mf1pY_b+ zd4bk+b1V3|^vZ3dbsyrKOS@3aI`2MIY19BRkEbEjg?8-Gryt(1*4E5Nu9Y|LQJ)(_ zW1W7#&Fb78@|F7=8xBlSY9qQMX+TpokmKV?1O#+S(PL~nKOi~CoSIA&R1%N`?5AWD zR2GvQ5=g|c45{iKAc$dzc#=ehAcl_~wy0v9UH|&xgOxKUeYlBXV-QSHr~v z$G#b{afEfU?#&+Ng*$iZYLBUf)TM;hBzhMKyyj(GH6!K>MHu;NS~;0XR~SUcoo~%e zjO}@M;!=R+iq@Z>m1Pz5-@g!X49f3#-MMz|+WU*bOply2groC6-@`Xo8@XN~d0#qM zagHC|RUh1=ur8%HKJUHQadYHsgT3O=t<>V)H2w^m7RyfyXq+u=913I2)@OeZSYxTe zw#EJSM~re-xz$UBk*i5-ShxI-mN>s=X4pm-JW%Wxnd>vNEA^Hx9NDCQGCiwCk-kp% zy?N38EbXp0G-h}OfoU6Oaoz7w>%*gzzcRB2-F-DZ0z{H@%l;Fm6Ec=<%7+8zV{VwZ>_K8l8 Z>j)(!r_VLgwlNVV>g6@LM?C4?^Ea}=5N!Ye literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-name-3-pwd.p12 b/src/tests/data/pkcs12/no-cert-name-3-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..431586990057325748e47d69f88c61135112d71a GIT binary patch literal 2932 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 literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-name-all-no-pwd.p12 b/src/tests/data/pkcs12/no-cert-name-all-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ef3e3cec2d189a78945ac6d9f69dc7ea2190700d GIT binary patch literal 2903 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 VBm|`Pcg4fR=e?}O?^{}Z{sb556#xJL literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-name-unicode-pwd.p12 b/src/tests/data/pkcs12/no-cert-name-unicode-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..a57f49e595c7fc3ac3f889a0cf77ae14cbfb962c GIT binary patch literal 2940 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 GWBvy<<_lf` literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-cert-no-name-pwd.p12 b/src/tests/data/pkcs12/no-cert-no-name-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c23a7615e19baf2811a62dbd6bec74b6af41ba2d GIT binary patch literal 2900 zcmV-a3#;@nf(uXr0Ru3C3mOIqDuzgg_YDCD0ic2l3j~4-2QY#Q12BRL{{{&vhDe6@ z4FLxRpn?kUFoFu~0s#Opf(qpZ2`Yw2hW8Bt2LUh~1_~;MNQUATfPxCRX>rP=npBNV?We27r8N0CEY_v z$1iAV67D02~am8NXgp zRg*4RzSrpY@RXd89`dWLp<)a?_L9*o2C2(_$>OCIN;`w znLdwo?ewHzq_D}oxH)n&BmJW#|1UcWy}_Xe6z1@`@z>&~ z(=F%bMF8LC2qve>i656;MhF(-BpfLY-7|@n=kjEpTC0(sZYRBjtZwI7RC2bMcP4X< zx;tTfn$_!)>w1{5PnT+k$KL=uXE`;1janD{Phj&4W8XeLW@bYRi~J z*ztXt2jD{?gM}z75Fb>!)95`wHC;EpZYe(IYIhfd8PBuyU?TwtVSNLK6UCsp_HWQ2@0yjUq<#o4!9mkWZ>86>iA5GFl*T3roM>@gl@I~GcYqQ74G`dT_Q>Is?t%7Gpy#AWqMMjp#5hc*VsGw!Ev&*kaYyK}sWrKwr&L_vb%Qn-Iv z`UzCp(xkz8-9C4KV6k&8c6nt=H}{yR^fBO#jWB|t8YRCBT(e-n3-)OEKl8-$S3*Nc^;qd4yPd?tg0?<2Cwx> z+2aa-fQ1ABgVEwL7!OYSxcYv4)Q<>)MI1YRpwn;p!Qd6aD$d=WZpGz^4se#+d#{)1 zC`6v6wbZkIsQuFav4?6^Gq7Mu0_;U3Nh1qE}qUwCOe_tnSGA*9o|V!uH!Y!y5i ziYem_wMD)7X#rq&^CsJ0ns|Zj&FqUw0jHU$ah)t4|1g*qZWu;)z>+0tQx+Xh_GMkMT^f(iFa|Xd15lz2;hgw z&z9KC;iMN553`GO5TiOV_j$wv2~%Fs3=z6p%$fVJPnP9qRMYW3oSb%cDYz(bIwrq& zV#h$A%DHeguBPWTFYgr}@!1vAXe;OY!oc(D1%qS}DsB$>Rsa;PwX69z}X zuLLHf7TMky8;Vb1;@Y>2wEkvqu+x0~hbP%pT)DF?({d*v&FGIUx1d3ww`G#@&O?Dy zzG4a6t5}XsF#a335Y>dL5gdn#+IzI(`;+SB^vy`J>2#?HV?yI+X)10~9ecXKtd(ur zesTQR9VU8~fk}zfZ5#YjZqj4(gGIyz@evl=dL5+GLq_aR{ZV>^YXogyV}j$(Eym96 zz0bc+3dy-((58b)a8}#TT{RlMS_91!4W)Y9G+||~Wm0iap zJ7L+37y|2%DN?9Gcxs(PAPz|6Z6T6k?`_;v5EI^wy=_g;x$rSp72!Z4v#g zY;21U=kU90QqizVwa$NsF$LgMoLMT$vu=pes%hqGSp2i0M5S%O^emkIs=@w1o6Y$-J zy4{5aREQ81(^?*%3Ph(7oY)p=To*ClMc+R}3sC)qAdIuumSIqQAEs*n literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-name-no-pwd.p12 b/src/tests/data/pkcs12/no-name-no-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c71d24dc26aa2cddc8a224e001c4178d322c30ad GIT binary patch literal 3489 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 V7T!Cp*33c4f}ZF4_FZ;o{{r$O>FWRh literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-name-pwd.p12 b/src/tests/data/pkcs12/no-name-pwd.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f2ac4c1a9e635def1a8dbf53e98d547a887217a9 GIT binary patch literal 3586 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 I^`=6+0fR2miU0rr literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/no-password.p12 b/src/tests/data/pkcs12/no-password.p12 new file mode 100644 index 0000000000000000000000000000000000000000..994dabb2ab46c823ce6c28d6619eb4c6f9edd0cb GIT binary patch literal 948 zcmXqLV&1^S$ZXKWT+YU+)#lOmotKfFaX}Mv21^rj3Q#z~povKxA;q?!iAe$|B*MhV z0Hk;kGK>Z?Y+O(ico^9X_*ghtr%KqfXdN|YV&Y(EXkvWeZ+c*zX2;?`-eLxVAHH9T zxyRglO*~AlrA(Ro_m0HUWj>!L7e8ZR=ziYTF=0Wu#-%w~69alury24!nSWW+x2wqJ zZrX!H#(>~oK}ozH4UgYE9KTCAt;^@4kJu@{JsrYp{kGJ8QEFpQ>OQJ@Cc{{lYM?oNOsb;%ghmtX0uPtES5D?SNyfy zWD;Ag>kr)>xd%7?;(IOs@9~=zIZK~5US|KsQOt2|vSjx4OZU1gPkScZ$zOlR{=AiR zNy4&ZQ81g z3jLYo<`XjdeC}tNS1o!wZKu!^q4LFdlivO~`te7gMYa39vZe2)RxML44LS2%@6E^8 zCfnq1Yb>9}RO1$)=1_LvV9U|n)794qzWUN(d+WPGR~jDN~noCiMedM!*f617=wI#K2Itf@k$j>&MB?eXC%5OF;6 zbI)B3CR3Z-tJ{?y+)JIFKZR9Gb>DP@CPqV~1hJrrQHiCAQ4T091;k=(+_2=q#mKav zagRacPLwpm3`#TWr+w6%#U{QQlxA2OXS_2gQ=Tm7XuEa2m`v>z=9u`x+4t(XP2#Uk zZLCkTu}^7y;bH7ZIx5D$s8PA>Gr=l({*!*<8g!q}mQcV{o zemSrDcc0_7ZZS2~>N)oEn?UKJ^SY0I|- zbD!0EYuQVN2Ay`AS@=F{%K8gJC(UjJzPYu9_m!cCfg;?ioFaxIazYGd3{DKr42D43 zh{1)yk%7TL8KFefP>e<7&+9EWgg%|Loq5nWR`5 pSVVGt{@gpBdFSsHwut0iCOiYPr{;V9MqCOu*IyR}S=i~t!Hll}kz literal 0 HcmV?d00001 diff --git a/src/tests/data/pkcs12/openssl_3des.p12 b/src/tests/data/pkcs12/openssl_3des.p12 new file mode 100644 index 0000000000000000000000000000000000000000..34fa3e8a37eb7eb73d538ab79cf1fa842b5ae1a8 GIT binary patch literal 2461 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 literal 0 HcmV?d00001 diff --git a/src/tests/test_pkcs12.cpp b/src/tests/test_pkcs12.cpp new file mode 100644 index 00000000000..fab992b1eb1 --- /dev/null +++ b/src/tests/test_pkcs12.cpp @@ -0,0 +1,1237 @@ +/* +* PKCS#12 Tests +* (C) 2026 Damiano Mazzella +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "tests.h" + +#if defined(BOTAN_HAS_PKCS12) + #include + #include + #include + + #if defined(BOTAN_HAS_ECDSA) + #include + #include + #include + #include + #include + #include + #endif + +namespace Botan_Tests { + +namespace { + +class PKCS12_Tests final : public Test { + public: + std::vector run() override { + std::vector results; + + // Tests that only exercise parsing / bundle-shape logic and do not + // depend on any specific public-key algorithm are always run. + + results.push_back(test_empty_input()); + results.push_back(test_nesting_depth_exceeded()); + results.push_back(test_key_bag()); + results.push_back(test_safe_contents_bag()); + results.push_back(test_end_entity_without_match()); + results.push_back(test_unknown_bag_types()); + results.push_back(test_pfx_invalid_version()); + results.push_back(test_envelopeddata_rejected()); + results.push_back(test_crl_bag_unknown()); + results.push_back(test_add_null_key_rejected()); + results.push_back(test_local_key_id_empty()); + results.push_back(test_export_empty_bundle()); + + #if defined(BOTAN_HAS_ECDSA) + // The remaining tests generate ECDSA credentials on the fly and are + // therefore only compiled when ECDSA is available in this build. + + results.push_back(test_key_cert_mismatch()); + results.push_back(test_zero_iterations()); + results.push_back(test_max_iterations_exceeded()); + results.push_back(test_wrong_password()); + results.push_back(test_corrupted_pfx()); + results.push_back(test_no_mac()); + results.push_back(test_empty_password()); + results.push_back(test_cert_only()); + results.push_back(test_legacy_compat_flag()); + #if defined(BOTAN_HAS_PKCS5_PBES2) + results.push_back(test_mac_sha256()); + #endif + + results.push_back(test_basic_roundtrip()); + results.push_back(test_roundtrip_with_chain()); + results.push_back(test_no_friendly_name()); + results.push_back(test_custom_iterations()); + + results.push_back(test_builder_workflow()); + results.push_back(test_export_options_fluent()); + results.push_back(test_multiple_keys()); + results.push_back(test_clear_friendly_name()); + results.push_back(test_local_key_id_setters()); + results.push_back(test_options_friendly_name_override()); + + results.push_back(test_duplicate_certificate()); + results.push_back(test_friendly_name_non_bmp()); + results.push_back(test_trailing_data()); + + results.push_back(test_mac_digest("SHA-224")); + #if defined(BOTAN_HAS_SHA2_64) + results.push_back(test_mac_digest("SHA-384")); + results.push_back(test_mac_digest("SHA-512")); + results.push_back(test_mac_digest("SHA-512-256")); + #endif + + results.push_back(test_key_encryption("PBE-SHA1-3DES")); + results.push_back(test_key_encryption("PBE-SHA1-2DES")); + #if defined(BOTAN_HAS_PKCS5_PBES2) + results.push_back(test_key_encryption("PBES2-SHA256-AES256")); + results.push_back(test_key_encryption("PBES2-SHA256-AES128")); + #endif + + results.push_back(test_cert_encryption("")); // Unencrypted certs + results.push_back(test_cert_encryption("PBE-SHA1-3DES")); + results.push_back(test_cert_encryption("PBE-SHA1-2DES")); + #if defined(BOTAN_HAS_PKCS5_PBES2) + results.push_back(test_cert_encryption("PBES2-SHA256-AES256")); + results.push_back(test_cert_encryption("PBES2-SHA256-AES128")); + + results.push_back(test_mixed_encryption("PBES2-SHA256-AES256", "PBE-SHA1-3DES")); + results.push_back(test_mixed_encryption("PBE-SHA1-3DES", "PBES2-SHA256-AES128")); + + results.push_back(test_chain_with_pbes2()); + #endif + #endif + + // External file parsing tests (do not depend on ECDSA at compile time + // -- the parser surfaces whatever key type the file contains). + results.push_back(test_parse_file("openssl_3des.p12", "test123")); + results.push_back(test_parse_file("cert-none-key-none.p12", "cryptography")); + results.push_back(test_parse_file("name-1-pwd.p12", "password", true, true, true, true)); + results.push_back(test_parse_file("name-2-3-pwd.p12", "password", true, true, true, true)); + results.push_back(test_parse_file("name-2-pwd.p12", "password", true, true, true, true)); + results.push_back(test_parse_file("name-3-pwd.p12", "password", true, true, true, true)); + results.push_back(test_parse_file("name-all-pwd.p12", "password", true, true, true, true)); + results.push_back(test_parse_file("name-unicode-pwd.p12", "password", true, true, true, true)); + results.push_back(test_parse_file("no-cert-name-2-pwd.p12", "password", true, false, true, true)); + results.push_back(test_parse_file("no-cert-name-3-pwd.p12", "password", true, false, true, true)); + results.push_back(test_parse_file("no-cert-name-all-pwd.p12", "password", true, false, true, true)); + results.push_back(test_parse_file("no-cert-name-unicode-pwd.p12", "password", true, false, true, true)); + results.push_back(test_parse_file("no-cert-no-name-pwd.p12", "password", true, false, true, false)); + results.push_back(test_parse_file("no-name-pwd.p12", "password", true, true, true)); + results.push_back(test_parse_file("java-truststore.p12", "", true, false, true, true)); + // OpenSSL-generated files with an empty password: these use OpenSSL's + // non-conforming empty-password encoding (empty byte string instead of + // RFC 7292's {0x00,0x00}). The parser transparently falls back to it. + results.push_back(test_parse_file("name-1-no-pwd.p12", "", true, true, true, true)); + results.push_back(test_parse_file("name-2-3-no-pwd.p12", "", true, true, true, true)); + results.push_back(test_parse_file("name-2-no-pwd.p12", "", true, true, true, true)); + results.push_back(test_parse_file("no-cert-no-name-no-pwd.p12", "", true, false, true, false)); + results.push_back(test_parse_file("no-name-no-pwd.p12", "", true, true, true, false)); + results.push_back(test_parse_file("name-3-no-pwd.p12", "", true, true, true, true)); + results.push_back(test_parse_file("no-cert-name-all-no-pwd.p12", "", true, false, true, true)); + results.push_back(test_parse_file("name-all-no-pwd.p12", "", true, true, true, true)); + results.push_back(test_parse_file("name-unicode-no-pwd.p12", "", true, true, true, true)); + results.push_back(test_parse_file("no-cert-name-2-no-pwd.p12", "", true, false, true, true)); + results.push_back(test_parse_file("no-cert-name-3-no-pwd.p12", "", true, false, true, true)); + results.push_back(test_parse_file("no-cert-name-unicode-no-pwd.p12", "", true, false, true, true)); + // OpenSSL empty password (RC2 branch -- key encryption not supported, + // so parsing still fails, but with Decoding_Error, not auth-tag). + results.push_back(test_parse_file_unsupported_algorithm("no-password.p12", "")); + // Wrong password + results.push_back(test_parse_file_wrong_password("name-1-pwd.p12", "wrongpassword")); + // RC2 unsupported algorithm + results.push_back(test_parse_file_unsupported_algorithm("cert-rc2-key-3des.p12", "cryptography")); + #if defined(BOTAN_HAS_PKCS5_PBES2) + results.push_back(test_parse_file("openssl_aes256.p12", "test123")); + results.push_back(test_parse_file("cert-aes256cbc-no-key.p12", "cryptography", true, false)); + results.push_back(test_parse_file("cert-key-aes256cbc.p12", "cryptography")); + results.push_back(test_parse_file("no-cert-key-aes256cbc.p12", "cryptography", false, true)); + #endif + + return results; + } + + private: + #if defined(BOTAN_HAS_ECDSA) + // Helper to generate a test key and certificate + struct TestCredentials { + std::shared_ptr key; + Botan::X509_Certificate cert; + }; + + TestCredentials generate_credentials(Botan::RandomNumberGenerator& rng, + const std::string& cn = "Test Certificate") { + TestCredentials creds; + creds.key = std::make_shared(rng, Botan::EC_Group::from_name("secp256r1")); + + Botan::X509_Cert_Options opts; + opts.common_name = cn; + opts.country = "US"; + opts.dns = "localhost"; + + creds.cert = Botan::X509::create_self_signed_cert(opts, *creds.key, "SHA-256", rng); + return creds; + } + + // Verify parsed PKCS12 data matches original + void verify_parsed_data(Test::Result& result, + const Botan::PKCS12& parsed, + const Botan::ECDSA_PrivateKey& orig_key, + const Botan::X509_Certificate& orig_cert, + const std::string& expected_friendly_name = "") { + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + + if(!expected_friendly_name.empty()) { + result.test_str_eq("Friendly name", parsed.friendly_name().value_or(""), expected_friendly_name); + } + + if(!parsed.private_keys().empty()) { + result.test_str_eq("Key algorithm", parsed.private_keys().front()->algo_name(), "ECDSA"); + const auto parsed_bits = parsed.private_keys().front()->private_key_bits(); + const auto orig_bits = orig_key.private_key_bits(); + result.test_is_true("Key matches", parsed_bits == orig_bits); + } + + if(parsed.end_entity_certificate()) { + result.test_is_true("Certificate matches", + parsed.end_entity_certificate()->BER_encode() == orig_cert.BER_encode()); + } + } + + Test::Result test_key_cert_mismatch() { + Test::Result result("PKCS12 key-cert mismatch rejected"); + + auto rng = Test::new_rng("PKCS12_mismatch"); + const auto creds = generate_credentials(*rng); + const auto other = generate_credentials(*rng, "Other"); + + result.test_throws("mismatched key and certificate throws", [&]() { + Botan::PKCS12 bundle; + bundle.add_key(other.key); + bundle.add_certificate(creds.cert); + (void)bundle.export_to(Botan::PKCS12_Export_Options::legacy_compat("testpassword"), *rng); + }); + + return result; + } + + Test::Result test_basic_roundtrip() { + Test::Result result("PKCS12 basic roundtrip"); + + auto rng = Test::new_rng("PKCS12_basic"); + auto creds = generate_credentials(*rng); + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Basic Test Key"); + const std::vector pfx = + bundle.export_to(Botan::PKCS12_Export_Options::legacy_compat("testpassword"), *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "testpassword"); + verify_parsed_data(result, parsed, *creds.key, creds.cert, "Basic Test Key"); + + return result; + } + + Test::Result test_roundtrip_with_chain() { + Test::Result result("PKCS12 roundtrip with certificate chain"); + + auto rng = Test::new_rng("PKCS12_chain"); + + // Generate CA + const auto ca_key = std::make_shared(*rng, Botan::EC_Group::from_name("secp256r1")); + Botan::X509_Cert_Options ca_opts; + ca_opts.common_name = "Test CA"; + ca_opts.country = "US"; + ca_opts.CA_key(); + const Botan::X509_Certificate ca_cert = + Botan::X509::create_self_signed_cert(ca_opts, *ca_key, "SHA-256", *rng); + + // Create CA signer + const Botan::X509_CA ca(ca_cert, *ca_key, "SHA-256", *rng); + + // Generate end-entity + const auto ee_key = std::make_shared(*rng, Botan::EC_Group::from_name("secp256r1")); + Botan::X509_Cert_Options ee_opts; + ee_opts.common_name = "End Entity"; + ee_opts.country = "US"; + ee_opts.dns = "localhost"; + + const Botan::PKCS10_Request csr = Botan::X509::create_cert_req(ee_opts, *ee_key, "SHA-256", *rng); + const Botan::X509_Certificate ee_cert = + ca.sign_request(csr, *rng, Botan::X509_Time("200101000000Z"), Botan::X509_Time("300101000000Z")); + + // Create PKCS#12 with chain + const auto opts = Botan::PKCS12_Export_Options::modern("chaintest"); + const std::vector chain = {ca_cert}; + Botan::PKCS12 bundle; + bundle.add_key(ee_key); + bundle.add_certificate(ee_cert); + for(const auto& chain_cert : chain) { + bundle.add_certificate(chain_cert); + } + bundle.set_friendly_name("Chain Test Key"); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "chaintest"); + + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + result.test_sz_eq("CA certificates count", parsed.ca_certificates().size(), 1); + result.test_str_eq("Friendly name", parsed.friendly_name().value_or(""), "Chain Test Key"); + + // Verify CA cert in chain + if(!parsed.ca_certificates().empty()) { + result.test_is_true("CA cert matches", parsed.ca_certificates()[0].BER_encode() == ca_cert.BER_encode()); + } + + return result; + } + + Test::Result test_no_friendly_name() { + Test::Result result("PKCS12 without friendly name"); + + auto rng = Test::new_rng("PKCS12_no_name"); + auto creds = generate_credentials(*rng); + + // No friendly_name set on bundle or in options. + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("noname"); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "noname"); + + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + result.test_is_true("No friendly name", !parsed.friendly_name().has_value()); + + return result; + } + + Test::Result test_custom_iterations() { + Test::Result result("PKCS12 with custom iterations"); + + auto rng = Test::new_rng("PKCS12_iterations"); + auto creds = generate_credentials(*rng); + + // Test with non-default iteration count + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("itertest").with_iterations(5000); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "itertest"); + verify_parsed_data(result, parsed, *creds.key, creds.cert); + + return result; + } + + Test::Result test_key_encryption(const std::string& algo) { + Test::Result result("PKCS12 key encryption: " + algo); + + auto rng = Test::new_rng("PKCS12_key_enc_" + algo); + auto creds = generate_credentials(*rng); + + const auto opts = + Botan::PKCS12_Export_Options("keyenctest").with_key_encryption_algo(algo).with_iterations(2048); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Key Enc Test"); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "keyenctest"); + verify_parsed_data(result, parsed, *creds.key, creds.cert, "Key Enc Test"); + + return result; + } + + Test::Result test_cert_encryption(const std::string& algo) { + const std::string test_name = + algo.empty() ? "PKCS12 cert encryption: unencrypted" : "PKCS12 cert encryption: " + algo; + Test::Result result(test_name); + + auto rng = Test::new_rng("PKCS12_cert_enc_" + (algo.empty() ? "none" : algo)); + auto creds = generate_credentials(*rng); + + auto opts = Botan::PKCS12_Export_Options("certenctest").with_iterations(2048); + if(!algo.empty()) { + opts.with_cert_encryption_algo(algo); + } + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Cert Enc Test"); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "certenctest"); + verify_parsed_data(result, parsed, *creds.key, creds.cert, "Cert Enc Test"); + + return result; + } + + Test::Result test_mixed_encryption(const std::string& key_algo, const std::string& cert_algo) { + Test::Result result("PKCS12 mixed encryption: key=" + key_algo + " cert=" + cert_algo); + + auto rng = Test::new_rng("PKCS12_mixed_" + key_algo + "_" + cert_algo); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options("mixedtest") + .with_key_encryption_algo(key_algo) + .with_cert_encryption_algo(cert_algo) + .with_iterations(2048); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Mixed Enc Test"); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "mixedtest"); + verify_parsed_data(result, parsed, *creds.key, creds.cert, "Mixed Enc Test"); + + return result; + } + + Test::Result test_chain_with_pbes2() { + Test::Result result("PKCS12 chain with PBES2 encryption"); + + auto rng = Test::new_rng("PKCS12_chain_pbes2"); + + // Generate CA + const auto ca_key = std::make_shared(*rng, Botan::EC_Group::from_name("secp256r1")); + Botan::X509_Cert_Options ca_opts; + ca_opts.common_name = "PBES2 CA"; + ca_opts.country = "US"; + ca_opts.CA_key(); + const Botan::X509_Certificate ca_cert = + Botan::X509::create_self_signed_cert(ca_opts, *ca_key, "SHA-256", *rng); + + // Create intermediate CA + const Botan::X509_CA root_ca(ca_cert, *ca_key, "SHA-256", *rng); + + const auto int_key = std::make_shared(*rng, Botan::EC_Group::from_name("secp256r1")); + Botan::X509_Cert_Options int_opts; + int_opts.common_name = "Intermediate CA"; + int_opts.country = "US"; + int_opts.CA_key(); + + const Botan::PKCS10_Request int_csr = Botan::X509::create_cert_req(int_opts, *int_key, "SHA-256", *rng); + const Botan::X509_Certificate int_cert = + root_ca.sign_request(int_csr, *rng, Botan::X509_Time("200101000000Z"), Botan::X509_Time("300101000000Z")); + + // Create end-entity cert signed by intermediate + const Botan::X509_CA int_ca(int_cert, *int_key, "SHA-256", *rng); + + const auto ee_key = std::make_shared(*rng, Botan::EC_Group::from_name("secp256r1")); + Botan::X509_Cert_Options ee_opts; + ee_opts.common_name = "End Entity"; + ee_opts.country = "US"; + ee_opts.dns = "localhost"; + + const Botan::PKCS10_Request ee_csr = Botan::X509::create_cert_req(ee_opts, *ee_key, "SHA-256", *rng); + const Botan::X509_Certificate ee_cert = + int_ca.sign_request(ee_csr, *rng, Botan::X509_Time("200101000000Z"), Botan::X509_Time("300101000000Z")); + + // Create PKCS#12 with full chain and PBES2 + const auto opts = Botan::PKCS12_Export_Options("chainpbes2") + .with_key_encryption_algo("PBES2-SHA256-AES256") + .with_cert_encryption_algo("PBES2-SHA256-AES128") + .with_iterations(10000); + + const std::vector chain = {int_cert, ca_cert}; + Botan::PKCS12 bundle; + bundle.add_key(ee_key); + bundle.add_certificate(ee_cert); + for(const auto& ca : chain) { + bundle.add_certificate(ca); + } + bundle.set_friendly_name("Chain PBES2 Key"); + const std::vector pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "chainpbes2"); + + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + result.test_sz_eq("CA certificates count", parsed.ca_certificates().size(), 2); + result.test_str_eq("Friendly name", parsed.friendly_name().value_or(""), "Chain PBES2 Key"); + + // Verify key + if(!parsed.private_keys().empty()) { + const auto parsed_bits = parsed.private_keys().front()->private_key_bits(); + const auto orig_bits = ee_key->private_key_bits(); + result.test_is_true("Key matches", parsed_bits == orig_bits); + } + + // Verify end-entity cert + if(parsed.end_entity_certificate()) { + result.test_is_true("EE cert matches", + parsed.end_entity_certificate()->BER_encode() == ee_cert.BER_encode()); + } + + return result; + } + #endif + + Test::Result test_parse_file_wrong_password(const std::string& filename, const std::string& password = "") { + Test::Result result("PKCS12 parse file wrong password: " + filename); + + try { + const std::vector pfx_data = Test::read_binary_data_file("pkcs12/" + filename); + result.test_throws("wrong password throws", + [&]() { Botan::PKCS12(pfx_data, password); }); + } catch(const std::exception& e) { + result.test_failure("Failed to read PFX file", e.what()); + } + + return result; + } + + Test::Result test_parse_file_unsupported_algorithm(const std::string& filename, + const std::string& password = "") { + Test::Result result("PKCS12 parse file unsupported algorithm: " + filename); + + try { + const std::vector pfx_data = Test::read_binary_data_file("pkcs12/" + filename); + result.test_throws("unsupported algorithm throws", + [&]() { Botan::PKCS12(pfx_data, password); }); + } catch(const std::exception& e) { + result.test_failure("Failed to read PFX file", e.what()); + } + + return result; + } + + Test::Result test_parse_file(const std::string& filename, + const std::string& password = "", + bool has_cert = true, + bool has_key = true, + bool has_ca = false, + bool has_friendly_name = false, + const std::string& expected_friendly_name = "") { + Test::Result result("PKCS12 parse file: " + filename); + + try { + const std::vector pfx_data = Test::read_binary_data_file("pkcs12/" + filename); + const Botan::PKCS12 parsed(pfx_data, password); + + // has_cert here means "bundle contains at least one certificate" + // (not specifically an end-entity); has_ca means "bundle contains + // certificates other than the end-entity". + result.test_is_true("Has private key", !parsed.private_keys().empty() == has_key); + result.test_is_true("Has certificate", !parsed.certificates().empty() == has_cert); + result.test_is_true("Has CA certificates", !parsed.ca_certificates().empty() == has_ca); + result.test_is_true("Has friendly name", parsed.friendly_name().has_value() == has_friendly_name); + if(has_key && !parsed.private_keys().empty()) { + result.test_str_not_empty("Key algorithm", parsed.private_keys().front()->algo_name()); + } + + if(has_cert && !parsed.certificates().empty()) { + result.test_str_not_empty("Certificate CN", parsed.certificates().front().subject_info("CN").at(0)); + } + + if(has_ca && !parsed.ca_certificates().empty()) { + for(size_t i = 0; i < parsed.ca_certificates().size(); ++i) { + result.test_str_not_empty("CA certificate " + std::to_string(i) + " CN", + parsed.ca_certificates()[i].subject_info("CN").at(0)); + } + } + + if(has_friendly_name && parsed.friendly_name().has_value()) { + result.test_str_not_empty("Friendly name", *parsed.friendly_name()); + if(!expected_friendly_name.empty()) { + result.test_str_eq("Friendly name value", *parsed.friendly_name(), expected_friendly_name); + } + } + + result.test_success("Parsed PFX successfully"); + } catch(const std::exception& e) { + result.test_failure("Failed to parse PFX", e.what()); + } + + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + Test::Result test_wrong_password() { + Test::Result result("PKCS12 wrong password rejected"); + + auto rng = Test::new_rng("PKCS12_wrong_pass"); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("correct"); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + + result.test_throws("wrong password throws", + [&]() { Botan::PKCS12(pfx, "wrong"); }); + + return result; + } + + Test::Result test_corrupted_pfx() { + Test::Result result("PKCS12 corrupted data rejected"); + + auto rng = Test::new_rng("PKCS12_corrupt"); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("test"); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + auto pfx = bundle.export_to(opts, *rng); + + // Corrupt bytes in the middle of the data + pfx[pfx.size() / 2] ^= 0xFF; + + result.test_throws("corrupted pfx throws", [&]() { Botan::PKCS12(pfx, "test"); }); + + return result; + } + + Test::Result test_no_mac() { + Test::Result result("PKCS12 without MAC"); + + auto rng = Test::new_rng("PKCS12_no_mac"); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("nomactest").without_mac(); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const auto parsed = Botan::PKCS12(pfx, "nomactest"); + + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + + return result; + } + + Test::Result test_empty_password() { + Test::Result result("PKCS12 empty password roundtrip"); + + auto rng = Test::new_rng("PKCS12_empty_pass"); + auto creds = generate_credentials(*rng); + + // Empty password requires include_mac=false. + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("").without_mac(); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const auto parsed = Botan::PKCS12(pfx, ""); + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + + return result; + } + + Test::Result test_cert_only() { + Test::Result result("PKCS12 cert-only bundle"); + + auto rng = Test::new_rng("PKCS12_cert_only"); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("certonly"); + Botan::PKCS12 bundle; + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const auto parsed = Botan::PKCS12(pfx, "certonly"); + result.test_is_false("No private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", !parsed.certificates().empty()); + result.test_is_false("No end-entity (no key)", parsed.end_entity_certificate().has_value()); + + return result; + } + #endif + + Test::Result test_empty_input() { + Test::Result result("PKCS12 empty input rejected"); + + result.test_throws("empty bytes throws", + []() { Botan::PKCS12(std::span{}, "pass"); }); + + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + Test::Result test_zero_iterations() { + Test::Result result("PKCS12 zero iterations rejected"); + + auto rng = Test::new_rng("PKCS12_zero_iter"); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options("test").with_iterations(0); + + result.test_throws("zero iterations throws", [&]() { + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + (void)bundle.export_to(opts, *rng); + }); + + return result; + } + + Test::Result test_max_iterations_exceeded() { + Test::Result result("PKCS12 max iterations exceeded rejected"); + + auto rng = Test::new_rng("PKCS12_max_iter"); + auto creds = generate_credentials(*rng); + + // PKCS12_MAX_ITERATIONS + 1 + const auto opts = Botan::PKCS12_Export_Options("test").with_iterations(1'000'001); + + result.test_throws("max iterations exceeded throws", [&]() { + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + (void)bundle.export_to(opts, *rng); + }); + + return result; + } + #endif + + Test::Result test_nesting_depth_exceeded() { + Test::Result result("PKCS12 nesting depth exceeded rejected"); + const auto pfx = Test::read_binary_data_file("pkcs12/nesting_too_deep.pfx"); + result.test_throws("nesting too deep throws", [&]() { Botan::PKCS12(pfx, ""); }); + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + Test::Result test_legacy_compat_flag() { + Test::Result result("PKCS12 legacy_compat flag"); + + auto rng = Test::new_rng("PKCS12_legacy_compat"); + auto creds = generate_credentials(*rng); + + // legacy_compat should produce a file readable with "old" defaults + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("legacytest"); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const auto parsed = Botan::PKCS12(pfx, "legacytest"); + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + + return result; + } + #endif + + // Build a PFX (no MAC) with a KeyBag containing an unencrypted private key + // Parse a PFX containing an unencrypted KeyBag (RFC 7292 sec.4.2.1) -- + // rarely seen in the wild but explicitly allowed by the spec. + Test::Result test_key_bag() { + Test::Result result("PKCS12 KeyBag (unencrypted key)"); + const auto pfx = Test::read_binary_data_file("pkcs12/key_bag_unencrypted.pfx"); + const Botan::PKCS12 parsed(pfx, ""); + result.test_sz_eq("One private key", parsed.private_keys().size(), 1); + result.test_is_true("End-entity present", parsed.end_entity_certificate().has_value()); + return result; + } + + // Parse a PFX with a SafeContentsBag wrapping a CertBag (RFC 7292 sec.4.2.6). + Test::Result test_safe_contents_bag() { + Test::Result result("PKCS12 SafeContentsBag (nested)"); + const auto pfx = Test::read_binary_data_file("pkcs12/safe_contents_bag_nested.pfx"); + const Botan::PKCS12 parsed(pfx, ""); + result.test_is_false("No private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", !parsed.certificates().empty()); + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // Exercise the builder workflow: + // default ctor -> add_key / add_certificate / set_friendly_name -> export_to + // -> re-parse and verify. + Test::Result test_builder_workflow() { + Test::Result result("PKCS12 builder workflow (new API)"); + + auto rng = Test::new_rng("PKCS12_builder"); + auto creds = generate_credentials(*rng, "Builder Test"); + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Builder Bundle"); + + const auto opts = Botan::PKCS12_Export_Options::modern("builderpw"); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "builderpw"); + result.test_sz_eq("One private key", parsed.private_keys().size(), 1); + result.test_sz_eq("One certificate", parsed.certificates().size(), 1); + result.test_is_true("End-entity present", parsed.end_entity_certificate().has_value()); + result.test_is_true("Friendly name present", parsed.friendly_name().has_value()); + if(parsed.friendly_name().has_value()) { + result.test_str_eq("Friendly name value", *parsed.friendly_name(), "Builder Bundle"); + } + if(!parsed.private_keys().empty()) { + result.test_is_true("Key matches", + parsed.private_keys().front()->private_key_bits() == creds.key->private_key_bits()); + } + if(parsed.end_entity_certificate()) { + result.test_is_true("Certificate matches", + parsed.end_entity_certificate()->BER_encode() == creds.cert.BER_encode()); + } + + return result; + } + + // Verify the chainable PKCS12_Export_Options builder produces an + // equivalent file regardless of which mutator combination is used. + Test::Result test_export_options_fluent() { + Test::Result result("PKCS12_Export_Options fluent API"); + + auto rng = Test::new_rng("PKCS12_fluent"); + auto creds = generate_credentials(*rng); + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + + // Chain several with_* mutators; each returns *this. + auto opts = Botan::PKCS12_Export_Options("fluentpw") + .with_friendly_name("Fluent Key") + .with_iterations(2048) + .with_key_encryption_algo("PBE-SHA1-3DES") + .with_mac_digest("SHA-1"); + + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "fluentpw"); + result.test_is_true("Key parsed", !parsed.private_keys().empty()); + result.test_is_true("End-entity parsed", parsed.end_entity_certificate().has_value()); + result.test_is_true("Friendly name parsed", parsed.friendly_name().has_value()); + if(parsed.friendly_name().has_value()) { + result.test_str_eq("Friendly name from options", *parsed.friendly_name(), "Fluent Key"); + } + + // without_mac() returns *this and disables the MAC. The parser must + // accept the file without a MAC trailer. + auto opts_no_mac = Botan::PKCS12_Export_Options::legacy_compat("nomacpw").without_mac().with_iterations(2048); + const auto pfx_no_mac = bundle.export_to(opts_no_mac, *rng); + result.test_sz_gt("PFX (no MAC) generated", pfx_no_mac.size(), 0); + const Botan::PKCS12 parsed_no_mac(pfx_no_mac, "nomacpw"); + result.test_is_true("Key parsed (no MAC)", !parsed_no_mac.private_keys().empty()); + + return result; + } + #endif + + // export_to on an empty bundle must throw. + Test::Result test_export_empty_bundle() { + Test::Result result("PKCS12 export of empty bundle rejected"); + + auto rng = Test::new_rng("PKCS12_empty_export"); + Botan::PKCS12 bundle; + + result.test_throws( + "empty bundle throws", [&]() { (void)bundle.export_to(Botan::PKCS12_Export_Options::modern("pw"), *rng); }); + + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // Build a bundle with two private keys; verify both survive the + // export/parse roundtrip. PKCS#12 supports multiple key bags per file. + // The end-entity attribute (localKeyId, friendly name) is anchored to + // the first key/cert pair only by this implementation. + Test::Result test_multiple_keys() { + Test::Result result("PKCS12 multiple private keys"); + + auto rng = Test::new_rng("PKCS12_multi_key"); + auto creds1 = generate_credentials(*rng, "Primary"); + auto creds2 = generate_credentials(*rng, "Secondary"); + + // Clone both keys via PKCS#8 so the bundle owns its own copies. + auto clone_key = [](const Botan::Private_Key& k) { + Botan::DataSource_Memory s(Botan::PKCS8::BER_encode(k)); + return std::shared_ptr(Botan::PKCS8::load_key(s)); + }; + + Botan::PKCS12 bundle; + bundle.add_key(clone_key(*creds1.key)); + bundle.add_key(clone_key(*creds2.key)); + bundle.add_certificate(creds1.cert); + bundle.add_certificate(creds2.cert); + + const auto opts = Botan::PKCS12_Export_Options::modern("multipw", "Multi Key Bundle"); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "multipw"); + result.test_sz_eq("Two private keys", parsed.private_keys().size(), 2); + result.test_sz_eq("Two certificates", parsed.certificates().size(), 2); + result.test_is_true("End-entity present (paired with first key)", parsed.end_entity_certificate().has_value()); + + // The first parsed key should match one of the originals; same for + // the second. Order isn't guaranteed across producers, so compare + // the SET of SubjectPublicKeyInfo encodings. + std::vector> orig_spki = {creds1.key->subject_public_key(), + creds2.key->subject_public_key()}; + std::vector> parsed_spki; + for(const auto& k : parsed.private_keys()) { + parsed_spki.push_back(k->subject_public_key()); + } + std::sort(orig_spki.begin(), orig_spki.end()); + std::sort(parsed_spki.begin(), parsed_spki.end()); + result.test_is_true("Both keys' public material round-tripped", parsed_spki == orig_spki); + + return result; + } + + // clear_friendly_name() must remove a previously set friendly name + // and the exported PFX must then carry no friendly-name attribute. + Test::Result test_clear_friendly_name() { + Test::Result result("PKCS12 clear_friendly_name"); + + auto rng = Test::new_rng("PKCS12_clear_fn"); + auto creds = generate_credentials(*rng); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Will Be Cleared"); + result.test_is_true("Friendly name set", bundle.friendly_name().has_value()); + bundle.clear_friendly_name(); + result.test_is_false("Friendly name cleared", bundle.friendly_name().has_value()); + + const auto pfx = bundle.export_to(Botan::PKCS12_Export_Options::modern("clearpw"), *rng); + const Botan::PKCS12 parsed(pfx, "clearpw"); + result.test_is_false("No friendly name in parsed PFX", parsed.friendly_name().has_value()); + + return result; + } + + // set_local_key_id() / clear_local_key_id() must round-trip the + // attribute bytes verbatim through the PFX. + Test::Result test_local_key_id_setters() { + Test::Result result("PKCS12 set_local_key_id / clear_local_key_id"); + + auto rng = Test::new_rng("PKCS12_lki"); + auto creds = generate_credentials(*rng); + const std::vector custom_id = {0xDE, 0xAD, 0xBE, 0xEF, 0x42}; + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_local_key_id(custom_id); + result.test_is_true("Local key id set", bundle.local_key_id().has_value()); + if(bundle.local_key_id()) { + result.test_is_true("Local key id matches", *bundle.local_key_id() == custom_id); + } + + const auto pfx = bundle.export_to(Botan::PKCS12_Export_Options::modern("lkipw"), *rng); + const Botan::PKCS12 parsed(pfx, "lkipw"); + result.test_is_true("Local key id present in parsed PFX", parsed.local_key_id().has_value()); + if(parsed.local_key_id()) { + result.test_is_true("Local key id round-tripped", *parsed.local_key_id() == custom_id); + } + + // clear_local_key_id removes the explicit value (export then falls + // back to deriving the id from the cert's SPKI BIT STRING). + bundle.clear_local_key_id(); + result.test_is_false("Local key id cleared", bundle.local_key_id().has_value()); + const auto pfx2 = bundle.export_to(Botan::PKCS12_Export_Options::modern("lkipw"), *rng); + const Botan::PKCS12 parsed2(pfx2, "lkipw"); + result.test_is_true("Local key id auto-derived from cert", parsed2.local_key_id().has_value()); + + return result; + } + #endif + + // A parsed bundle with a key whose SPKI doesn't match any of the + // included certificates must report end_entity_certificate() == nullopt + // while still surfacing both the key and the cert(s). + // A parsed bundle whose (single) key SPKI doesn't match the (single) + // certificate: end_entity_certificate() must be nullopt while both are + // surfaced by private_keys() / certificates(). + Test::Result test_end_entity_without_match() { + Test::Result result("PKCS12 end_entity_certificate nullopt when no match"); + const auto pfx = Test::read_binary_data_file("pkcs12/key_cert_spki_mismatch.pfx"); + const Botan::PKCS12 parsed(pfx, ""); + result.test_is_true("Key parsed", !parsed.private_keys().empty()); + result.test_is_true("Cert parsed", !parsed.certificates().empty()); + result.test_is_false("No end-entity (SPKI mismatch)", parsed.end_entity_certificate().has_value()); + // ca_certificates() falls back to "all but first" when there is no + // end-entity; with a single mismatched cert it must therefore be empty. + result.test_is_true("ca_certificates empty", parsed.ca_certificates().empty()); + return result; + } + + // A bag with an OID the parser doesn't handle (e.g. PKCS12.SecretBag) + // must be silently skipped but recorded in unknown_bag_types(). + Test::Result test_unknown_bag_types() { + Test::Result result("PKCS12 unknown_bag_types reported"); + const auto pfx = Test::read_binary_data_file("pkcs12/unknown_bag_secret.pfx"); + const Botan::PKCS12 parsed(pfx, ""); + result.test_sz_eq("One unknown bag recorded", parsed.unknown_bag_types().size(), 1); + if(!parsed.unknown_bag_types().empty()) { + result.test_str_eq("Unknown bag is SecretBag", + parsed.unknown_bag_types().front().to_formatted_string(), + Botan::OID::from_string("PKCS12.SecretBag").to_formatted_string()); + } + result.test_is_true("Cert still parsed", !parsed.certificates().empty()); + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // When PKCS12_Export_Options::with_friendly_name is set, it must + // override the bundle-level friendly_name() during export. + Test::Result test_options_friendly_name_override() { + Test::Result result("PKCS12_Export_Options friendly_name overrides bundle"); + + auto rng = Test::new_rng("PKCS12_fn_override"); + auto creds = generate_credentials(*rng); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("Bundle-Level Name"); + + auto opts = Botan::PKCS12_Export_Options::modern("ovrpw").with_friendly_name("Options-Level Name"); + const auto pfx = bundle.export_to(opts, *rng); + + const Botan::PKCS12 parsed(pfx, "ovrpw"); + result.test_is_true("Parsed has friendly name", parsed.friendly_name().has_value()); + if(parsed.friendly_name()) { + result.test_str_eq("Options FN wins over bundle FN", *parsed.friendly_name(), "Options-Level Name"); + } + + // Also verify the opposite path: with no override in options, the + // bundle FN is what ends up in the PFX. + auto opts_no_ovr = Botan::PKCS12_Export_Options::modern("ovrpw"); + const auto pfx2 = bundle.export_to(opts_no_ovr, *rng); + const Botan::PKCS12 parsed2(pfx2, "ovrpw"); + result.test_is_true("Parsed has bundle FN", parsed2.friendly_name().has_value()); + if(parsed2.friendly_name()) { + result.test_str_eq("Bundle FN used when options unset", *parsed2.friendly_name(), "Bundle-Level Name"); + } + + return result; + } + #endif + + // add_key(nullptr) must throw Invalid_Argument. + Test::Result test_add_null_key_rejected() { + Test::Result result("PKCS12::add_key(nullptr) rejected"); + + Botan::PKCS12 bundle; + result.test_throws("add_key(nullptr) throws", + [&]() { bundle.add_key(std::shared_ptr{}); }); + + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // add_certificate does NOT deduplicate: the same certificate added twice + // appears twice in certificates() and the resulting PFX contains two CertBags. + Test::Result test_duplicate_certificate() { + Test::Result result("PKCS12 duplicate certificate not deduplicated"); + + auto rng = Test::new_rng("PKCS12_dup_cert"); + auto creds = generate_credentials(*rng); + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.add_certificate(creds.cert); // duplicate + result.test_sz_eq("Bundle keeps both copies", bundle.certificates().size(), 2); + + const auto pfx = bundle.export_to(Botan::PKCS12_Export_Options::modern("duppw"), *rng); + const Botan::PKCS12 parsed(pfx, "duppw"); + result.test_sz_eq("Parsed bundle keeps both copies", parsed.certificates().size(), 2); + // end_entity_certificate picks the first match by SPKI; ca_certificates + // returns the duplicate. + result.test_is_true("End-entity present", parsed.end_entity_certificate().has_value()); + result.test_sz_eq("ca_certificates() returns the duplicate", parsed.ca_certificates().size(), 1); + + return result; + } + #endif + + // set_local_key_id({}) records an explicit-but-empty value: has_value() is + // true and the contained vector is empty. (Distinct from "never set".) + Test::Result test_local_key_id_empty() { + Test::Result result("PKCS12 set_local_key_id with empty vector"); + + Botan::PKCS12 bundle; + result.test_is_false("Initially unset", bundle.local_key_id().has_value()); + bundle.set_local_key_id({}); + result.test_is_true("Explicit empty has_value", bundle.local_key_id().has_value()); + if(bundle.local_key_id()) { + result.test_is_true("Contained vector is empty", bundle.local_key_id()->empty()); + } + bundle.clear_local_key_id(); + result.test_is_false("Cleared", bundle.local_key_id().has_value()); + + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // A friendly name containing characters outside the BMP (codepoints + // above U+FFFF) cannot be encoded as BMPString and must be rejected + // during export. "\xF0\x9F\x94\x91" is U+1F511 (KEY). + Test::Result test_friendly_name_non_bmp() { + Test::Result result("PKCS12 non-BMP friendly name rejected on export"); + + auto rng = Test::new_rng("PKCS12_non_bmp"); + auto creds = generate_credentials(*rng); + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + bundle.set_friendly_name("\xF0\x9F\x94\x91"); // U+1F511 + + result.test_throws("non-BMP friendly name throws", [&]() { + (void)bundle.export_to(Botan::PKCS12_Export_Options::modern("bmppw"), *rng); + }); + + return result; + } + #endif + + // A PFX with version=2 (only 3 is accepted). + Test::Result test_pfx_invalid_version() { + Test::Result result("PKCS12 unsupported version rejected"); + const auto pfx = Test::read_binary_data_file("pkcs12/pfx_version_2.pfx"); + result.test_throws("version != 3 throws", [&]() { Botan::PKCS12(pfx, ""); }); + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // Trailing bytes after a valid PFX must be rejected (pfx_seq.verify_end()). + Test::Result test_trailing_data() { + Test::Result result("PKCS12 trailing data rejected"); + + auto rng = Test::new_rng("PKCS12_trailing"); + auto creds = generate_credentials(*rng); + + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + auto pfx = bundle.export_to(Botan::PKCS12_Export_Options::modern("trailpw"), *rng); + + pfx.push_back(0x42); + pfx.push_back(0x42); + pfx.push_back(0x42); + + result.test_throws("trailing bytes throw", [&]() { Botan::PKCS12(pfx, "trailpw"); }); + + return result; + } + #endif + + // An AuthenticatedSafe ContentInfo carrying EnvelopedData (PKCS7) must be + // rejected. EnvelopedData is in the spec but not implemented here. + Test::Result test_envelopeddata_rejected() { + Test::Result result("PKCS12 EnvelopedData content type rejected"); + const auto pfx = Test::read_binary_data_file("pkcs12/envelopeddata_content.pfx"); + result.test_throws("EnvelopedData throws", [&]() { Botan::PKCS12(pfx, ""); }); + return result; + } + + // A PFX with a CrlBag (RFC 7292 sec.4.2.4) -- not implemented here -- must be + // skipped but recorded in unknown_bag_types(). + Test::Result test_crl_bag_unknown() { + Test::Result result("PKCS12 CrlBag reported as unknown"); + const auto pfx = Test::read_binary_data_file("pkcs12/unknown_bag_crl.pfx"); + const Botan::PKCS12 parsed(pfx, ""); + result.test_sz_eq("One unknown bag reported", parsed.unknown_bag_types().size(), 1); + if(!parsed.unknown_bag_types().empty()) { + result.test_str_eq("Reported OID is CrlBag", + parsed.unknown_bag_types().front().to_formatted_string(), + Botan::OID::from_string("PKCS12.CRLBag").to_formatted_string()); + } + return result; + } + #if defined(BOTAN_HAS_ECDSA) + + // Roundtrip a PFX with a configurable MAC digest. Used to exercise the + // SHA-2 family beyond SHA-256 (the latter is covered by test_mac_sha256). + Test::Result test_mac_digest(const std::string& digest) { + Test::Result result("PKCS12 MAC digest: " + digest); + + auto rng = Test::new_rng("PKCS12_mac_" + digest); + auto creds = generate_credentials(*rng); + + const auto opts = Botan::PKCS12_Export_Options::legacy_compat("digestpw").with_mac_digest(digest); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const Botan::PKCS12 parsed(pfx, "digestpw"); + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + + return result; + } + #endif + + #if defined(BOTAN_HAS_PKCS5_PBES2) + #if defined(BOTAN_HAS_ECDSA) + Test::Result test_mac_sha256() { + Test::Result result("PKCS12 MAC SHA-256 roundtrip"); + + auto rng = Test::new_rng("PKCS12_mac_sha256"); + auto creds = generate_credentials(*rng); + + // Modern defaults: PBES2-SHA256-AES256 key encryption, SHA-256 MAC. + const auto opts = Botan::PKCS12_Export_Options::modern("mactest").with_iterations(2048); + Botan::PKCS12 bundle; + bundle.add_key(creds.key); + bundle.add_certificate(creds.cert); + const auto pfx = bundle.export_to(opts, *rng); + result.test_sz_gt("PFX data generated", pfx.size(), 0); + + const auto parsed = Botan::PKCS12(pfx, "mactest"); + result.test_is_true("Has private key", !parsed.private_keys().empty()); + result.test_is_true("Has certificate", parsed.end_entity_certificate().has_value()); + + return result; + } + #endif + #endif +}; + +BOTAN_REGISTER_TEST("pkcs12", "pkcs12_format", PKCS12_Tests); + +} // namespace + +} // namespace Botan_Tests + +#endif