From 00921fac4212b892c24de4ed94d7117aaaf23a39 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 26 Aug 2022 10:24:23 +0800 Subject: [PATCH] support setting more PKCS12 serialization encryption options This is limited support, but makes it possible to set two different PBES choices as well as set KDF rounds and MAC algorithm --- .../primitives/asymmetric/serialization.rst | 59 ++++++++- .../hazmat/backends/openssl/backend.py | 72 ++++++++++- .../hazmat/primitives/_serialization.py | 94 +++++++++++++- .../hazmat/primitives/serialization/pkcs12.py | 5 + tests/hazmat/primitives/test_pkcs12.py | 122 ++++++++++++++++++ tests/hazmat/primitives/test_serialization.py | 38 ++++++ 6 files changed, 377 insertions(+), 13 deletions(-) diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 3361ce6f3bcf4..0ea8da059047d 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -579,6 +579,24 @@ file suffix. A list of :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12Certificate` instances. +.. class:: PBES + + .. versionadded:: 38.0.0 + + An enumeration of password-based encryption schemes used in PKCS12. These + values are used with + :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryptionBuilder`. + + .. attribute:: PBESv1SHA1And3KeyTripleDESCBC + + PBESv1 using SHA1 as the KDF PRF and 3-key triple DES as the cipher. + + .. attribute:: PBESv2SHA256AndAES256CBC + + PBESv2 using SHA256 as the KDF PRF and AES256 as the cipher. This is + only supported on OpenSSL 3.0.0 or newer. + + PKCS7 ~~~~~ @@ -850,7 +868,7 @@ Serialization Formats For most use cases, :class:`BestAvailableEncryption` is preferred. - :returns KeySerializationEncryptionBuilder: A new builder. + :returns :class:`KeySerializationEncryptionBuilder`: A new builder. .. doctest:: @@ -1022,7 +1040,8 @@ Serialization Encryption Types Encrypt using the best available encryption for a given key. This is a curated encryption choice and the algorithm may change over - time. + time. The encryption algorithm may vary based on which version of OpenSSL + the library is compiled against. :param bytes password: The password to use for encryption. @@ -1033,8 +1052,11 @@ Serialization Encryption Types .. class:: KeySerializationEncryptionBuilder - A builder that can be used to configure how key data is encrypted. To - create one, call :meth:`PrivateFormat.encryption_builder`. + .. versionadded:: 38.0.0 + + A builder that can be used to configure how data is encrypted. To + create one, call :meth:`PrivateFormat.encryption_builder`. Different + serialization types will use different options on this builder. .. method:: kdf_rounds(rounds) @@ -1042,7 +1064,29 @@ Serialization Encryption Types meaning of the number of rounds varies on the KDF being used. :param int rounds: Number of rounds. - :returns KeySerializationEncryptionBuilder: A new builder. + + .. method:: cert_encryption_algorithm(algorithm) + + Set the encryption algorithm to use when encrypting certificates in + a PKCS12 structure. + + :param algorithm: A value from the :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PBES` + enumeration. + + .. method:: key_encryption_algorithm(algorithm) + + Set the encryption algorithm to use when encrypting the key in a + PKCS12 structure. + + :param algorithm: A value from the :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PBES` + enumeration. + + .. method:: mac_algorithm(algorithm) + + Set the MAC algorithm to use for a PKCS12 structure. + + :param algorithm: An instance of a + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` .. method:: build(password) @@ -1050,8 +1094,9 @@ Serialization Encryption Types :class:`KeySerializationEncryption` with a given password. :param bytes password: The password. - :returns KeySerializationEncryption: A key key serialization - encryption that can be passed to ``private_bytes`` methods. + :returns: A :class:`KeySerializationEncryption` encryption object + that can be passed to methods like ``private_bytes`` or + :func:`~cryptography.hazmat.primitives.serialization.pkcs12.serialize_key_and_certificates`. .. _`a bug in Firefox`: https://bugzilla.mozilla.org/show_bug.cgi?id=773111 .. _`PKCS3`: https://www.teletrust.de/fileadmin/files/oid/oid_pkcs-3v1-4.pdf diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 180083fa94035..bdba750a433bc 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -116,6 +116,7 @@ from cryptography.hazmat.primitives.kdf import scrypt from cryptography.hazmat.primitives.serialization import pkcs7, ssh from cryptography.hazmat.primitives.serialization.pkcs12 import ( + PBES, PKCS12Certificate, PKCS12KeyAndCertificates, _ALLOWED_PKCS12_TYPES, @@ -2263,20 +2264,71 @@ def serialize_key_and_certificates_to_pkcs12( nid_key = -1 pkcs12_iter = 0 mac_iter = 0 + mac_alg = self._ffi.NULL elif isinstance( encryption_algorithm, serialization.BestAvailableEncryption ): # PKCS12 encryption is hopeless trash and can never be fixed. - # This is the least terrible option. - nid_cert = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC - nid_key = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC + # OpenSSL 3 supports PBESv2, but Libre and Boring do not, so + # we use PBESv1 with 3DES on the older paths. + if self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + nid_cert = self._lib.NID_aes_256_cbc + nid_key = self._lib.NID_aes_256_cbc + else: + nid_cert = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC + nid_key = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC # At least we can set this higher than OpenSSL's default pkcs12_iter = 20000 # mac_iter chosen for compatibility reasons, see: # https://www.openssl.org/docs/man1.1.1/man3/PKCS12_create.html # Did we mention how lousy PKCS12 encryption is? mac_iter = 1 + # MAC algorithm can only be set on OpenSSL 3.0.0+ + mac_alg = self._ffi.NULL password = encryption_algorithm.password + elif isinstance( + encryption_algorithm, serialization._KeySerializationEncryption + ): + # Default to OpenSSL's defaults. Behavior will vary based on the + # version of OpenSSL cryptography is compiled against. + nid_cert = 0 + nid_key = 0 + # Use the default iters we use in best available + pkcs12_iter = 20000 + # See the Best Available comment for why this is 1 + mac_iter = 1 + password = encryption_algorithm.password + certencalg = encryption_algorithm._cert_encryption_algorithm + keyencalg = encryption_algorithm._key_encryption_algorithm + if certencalg is PBES.PBESv1SHA1And3KeyTripleDESCBC: + nid_cert = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC + elif certencalg is PBES.PBESv2SHA256AndAES256CBC: + if not self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + raise UnsupportedAlgorithm( + "PBESv2 is not supported by this version of OpenSSL" + ) + nid_cert = self._lib.NID_aes_256_cbc + + if keyencalg is PBES.PBESv1SHA1And3KeyTripleDESCBC: + nid_key = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC + elif keyencalg is PBES.PBESv2SHA256AndAES256CBC: + if not self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + raise UnsupportedAlgorithm( + "PBESv2 is not supported by this version of OpenSSL" + ) + nid_cert = self._lib.NID_aes_256_cbc + + if encryption_algorithm._mac_algorithm is not None: + mac_alg = self._evp_md_non_null_from_algorithm( + encryption_algorithm._mac_algorithm + ) + self.openssl_assert(mac_alg != self._ffi.NULL) + else: + mac_alg = self._ffi.NULL + + if encryption_algorithm._kdf_rounds is not None: + pkcs12_iter = encryption_algorithm._kdf_rounds + else: raise ValueError("Unsupported key encryption type") @@ -2326,6 +2378,20 @@ def serialize_key_and_certificates_to_pkcs12( 0, ) + if ( + self._lib.Cryptography_HAS_PKCS12_SET_MAC + and mac_alg != self._ffi.NULL + ): + self._lib.PKCS12_set_mac( + p12, + password_buf, + -1, + self._ffi.NULL, + 0, + mac_iter, + mac_alg, + ) + self.openssl_assert(p12 != self._ffi.NULL) p12 = self._ffi.gc(p12, self._lib.PKCS12_free) diff --git a/src/cryptography/hazmat/primitives/_serialization.py b/src/cryptography/hazmat/primitives/_serialization.py index 73a7b5a49295c..ae58fb2e545dc 100644 --- a/src/cryptography/hazmat/primitives/_serialization.py +++ b/src/cryptography/hazmat/primitives/_serialization.py @@ -6,11 +6,17 @@ import typing from cryptography import utils +from cryptography.hazmat.primitives.hashes import HashAlgorithm # This exists to break an import cycle. These classes are normally accessible # from the serialization module. +class PBES(utils.Enum): + PBESv1SHA1And3KeyTripleDESCBC = "PBESv1 using SHA1 and 3-Key TripleDES" + PBESv2SHA256AndAES256CBC = "PBESv2 using SHA256 PBKDF2 and AES256 CBC" + + class Encoding(utils.Enum): PEM = "PEM" DER = "DER" @@ -25,11 +31,13 @@ class PrivateFormat(utils.Enum): TraditionalOpenSSL = "TraditionalOpenSSL" Raw = "Raw" OpenSSH = "OpenSSH" + PKCS12 = "PKCS12" def encryption_builder(self) -> "KeySerializationEncryptionBuilder": - if self is not PrivateFormat.OpenSSH: + if self not in (PrivateFormat.OpenSSH, PrivateFormat.PKCS12): raise ValueError( "encryption_builder only supported with PrivateFormat.OpenSSH" + " and PrivateFormat.PKCS12" ) return KeySerializationEncryptionBuilder(self) @@ -69,16 +77,85 @@ def __init__( format: PrivateFormat, *, _kdf_rounds: typing.Optional[int] = None, + _mac_algorithm: typing.Optional[HashAlgorithm] = None, + _cert_encryption_algorithm: typing.Optional[PBES] = None, + _key_encryption_algorithm: typing.Optional[PBES] = None, ) -> None: self._format = format self._kdf_rounds = _kdf_rounds + self._mac_algorithm = _mac_algorithm + self._cert_encryption_algorithm = _cert_encryption_algorithm + self._key_encryption_algorithm = _key_encryption_algorithm def kdf_rounds(self, rounds: int) -> "KeySerializationEncryptionBuilder": if self._kdf_rounds is not None: raise ValueError("kdf_rounds already set") + + if not isinstance(rounds, int) or rounds < 1: + raise ValueError("kdf_rounds must be a positive integer") + + return KeySerializationEncryptionBuilder( + self._format, + _kdf_rounds=rounds, + _mac_algorithm=self._mac_algorithm, + _cert_encryption_algorithm=self._cert_encryption_algorithm, + _key_encryption_algorithm=self._key_encryption_algorithm, + ) + + def mac_algorithm( + self, algorithm: HashAlgorithm + ) -> "KeySerializationEncryptionBuilder": + if self._format is not PrivateFormat.PKCS12: + raise TypeError( + "mac_algorithm only supported with PrivateFormat.PKCS12" + ) + + if self._mac_algorithm is not None: + raise ValueError("mac_algorithm already set") + return KeySerializationEncryptionBuilder( + self._format, + _kdf_rounds=self._kdf_rounds, + _mac_algorithm=algorithm, + _cert_encryption_algorithm=self._cert_encryption_algorithm, + _key_encryption_algorithm=self._key_encryption_algorithm, + ) + + def cert_encryption_algorithm( + self, algorithm: PBES + ) -> "KeySerializationEncryptionBuilder": + if self._format is not PrivateFormat.PKCS12: + raise TypeError( + "cert_encryption_algorithm only supported with " + "PrivateFormat.PKCS12" + ) + + if self._cert_encryption_algorithm is not None: + raise ValueError("cert_encryption_algorithm already set") + return KeySerializationEncryptionBuilder( + self._format, + _kdf_rounds=self._kdf_rounds, + _mac_algorithm=self._mac_algorithm, + _cert_encryption_algorithm=algorithm, + _key_encryption_algorithm=self._key_encryption_algorithm, + ) + + def key_encryption_algorithm( + self, algorithm: PBES + ) -> "KeySerializationEncryptionBuilder": + if self._format is not PrivateFormat.PKCS12: + raise TypeError( + "key_encryption_algorithm only supported with " + "PrivateFormat.PKCS12" + ) + if self._key_encryption_algorithm is not None: + raise ValueError("key_encryption_algorithm already set") return KeySerializationEncryptionBuilder( - self._format, _kdf_rounds=rounds + self._format, + _kdf_rounds=self._kdf_rounds, + _mac_algorithm=self._mac_algorithm, + _cert_encryption_algorithm=self._cert_encryption_algorithm, + _key_encryption_algorithm=algorithm, ) def build(self, password: bytes) -> KeySerializationEncryption: @@ -86,7 +163,12 @@ def build(self, password: bytes) -> KeySerializationEncryption: raise ValueError("Password must be 1 or more bytes.") return _KeySerializationEncryption( - self._format, password, kdf_rounds=self._kdf_rounds + self._format, + password, + kdf_rounds=self._kdf_rounds, + mac_algorithm=self._mac_algorithm, + cert_encryption_algorithm=self._cert_encryption_algorithm, + key_encryption_algorithm=self._key_encryption_algorithm, ) @@ -97,8 +179,14 @@ def __init__( password: bytes, *, kdf_rounds: typing.Optional[int], + mac_algorithm: typing.Optional[HashAlgorithm], + cert_encryption_algorithm: typing.Optional[PBES], + key_encryption_algorithm: typing.Optional[PBES], ): self._format = format self.password = password self._kdf_rounds = kdf_rounds + self._mac_algorithm = mac_algorithm + self._cert_encryption_algorithm = cert_encryption_algorithm + self._key_encryption_algorithm = key_encryption_algorithm diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs12.py b/src/cryptography/hazmat/primitives/serialization/pkcs12.py index 791befd283478..c900f444027da 100644 --- a/src/cryptography/hazmat/primitives/serialization/pkcs12.py +++ b/src/cryptography/hazmat/primitives/serialization/pkcs12.py @@ -6,6 +6,7 @@ from cryptography import x509 from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives._serialization import PBES as PBES from cryptography.hazmat.primitives.asymmetric import ( dsa, ec, @@ -17,6 +18,10 @@ PRIVATE_KEY_TYPES, ) +# PBES is a PKCS12 specific item, but it lives in _serialization to avoid an +# import cycle. We re-export it here for the public API. +__all__ = ["PBES"] + _ALLOWED_PKCS12_TYPES = typing.Union[ rsa.RSAPrivateKey, diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py index ddb0c648d73b2..58673948d32df 100644 --- a/tests/hazmat/primitives/test_pkcs12.py +++ b/tests/hazmat/primitives/test_pkcs12.py @@ -9,6 +9,7 @@ import pytest from cryptography import x509 +from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends.openssl.backend import _RC2 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ( @@ -24,6 +25,7 @@ load_pem_private_key, ) from cryptography.hazmat.primitives.serialization.pkcs12 import ( + PBES, PKCS12Certificate, PKCS12KeyAndCertificates, load_key_and_certificates, @@ -530,6 +532,126 @@ def test_generate_unsupported_encryption_type(self, backend): ) assert str(exc.value) == "Unsupported key encryption type" + @pytest.mark.parametrize( + ("algorithm", "password"), + [ + ( + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + .cert_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + .mac_algorithm(hashes.SHA1()) + .build(b"password"), + b"password", + ), + ( + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + .cert_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + .mac_algorithm(hashes.SHA1()) + .kdf_rounds(2000) + .build(b"password"), + b"password", + ), + ( + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .cert_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .mac_algorithm(hashes.SHA256()) + .build(b"password"), + b"password", + ), + ( + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .cert_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .mac_algorithm(hashes.SHA256()) + .kdf_rounds(2000) + .build(b"password"), + b"password", + ), + ( + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .cert_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .mac_algorithm(hashes.SHA1()) + .build(b"password"), + b"password", + ), + ( + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .cert_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .build(b"password"), + b"password", + ), + ], + ) + def test_key_serialization_encryption(self, backend, algorithm, password): + if ( + algorithm._key_encryption_algorithm + is PBES.PBESv2SHA256AndAES256CBC + or algorithm._cert_encryption_algorithm + is PBES.PBESv2SHA256AndAES256CBC + ) and not backend._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER: + pytest.skip("PBESv2 is not supported on OpenSSL < 3.0") + + key = ec.generate_private_key(ec.SECP256R1()) + cacert, cakey = _load_ca(backend) + now = datetime.utcnow() + cert = ( + x509.CertificateBuilder() + .subject_name(cacert.subject) + .issuer_name(cacert.subject) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now) + .sign(cakey, hashes.SHA256()) + ) + assert isinstance(cert, x509.Certificate) + p12 = serialize_key_and_certificates( + b"name", key, cert, [cacert], algorithm + ) + parsed_key, parsed_cert, parsed_more_certs = load_key_and_certificates( + p12, password, backend + ) + assert parsed_cert == cert + assert isinstance(parsed_key, ec.EllipticCurvePrivateKey) + assert parsed_key.public_key().public_bytes( + Encoding.PEM, PublicFormat.SubjectPublicKeyInfo + ) == key.public_key().public_bytes( + Encoding.PEM, PublicFormat.SubjectPublicKeyInfo + ) + assert parsed_more_certs == [cacert] + + @pytest.mark.supported( + only_if=lambda backend: ( + not backend._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER + ), + skip_message="Requires OpenSSL < 3.0.0 (or Libre/Boring)", + ) + @pytest.mark.parametrize( + ("algorithm"), + [ + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .cert_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + .build(b"password"), + serialization.PrivateFormat.PKCS12.encryption_builder() + .key_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + .cert_encryption_algorithm(PBES.PBESv2SHA256AndAES256CBC) + .build(b"password"), + ], + ) + def test_key_serialization_encryption_unsupported( + self, algorithm, backend + ): + cacert, cakey = _load_ca(backend) + with pytest.raises(UnsupportedAlgorithm): + serialize_key_and_certificates( + b"name", cakey, cacert, [], algorithm + ) + @pytest.mark.skip_fips( reason="PKCS12 unsupported in FIPS mode. So much bad crypto in it." diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 35f0e83f99fd2..e408489078683 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -19,6 +19,7 @@ x25519, x448, ) +from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.serialization import ( BestAvailableEncryption, Encoding, @@ -36,6 +37,7 @@ load_ssh_public_key, ssh, ) +from cryptography.hazmat.primitives.serialization.pkcs12 import PBES from .fixtures_rsa import RSA_KEY_2048 @@ -2445,9 +2447,45 @@ def test_duplicate_kdf_rounds(self): with pytest.raises(ValueError): b.kdf_rounds(12) + def test_invalid_kdf_rounds(self): + b = PrivateFormat.OpenSSH.encryption_builder() + with pytest.raises(ValueError): + b.kdf_rounds(0) + with pytest.raises(ValueError): + b.kdf_rounds(-1) + with pytest.raises(ValueError): + b.kdf_rounds("string") # type: ignore[arg-type] + def test_invalid_password(self): b = PrivateFormat.OpenSSH.encryption_builder() with pytest.raises(ValueError): b.build(12) # type: ignore[arg-type] with pytest.raises(ValueError): b.build(b"") + + def test_unsupported_type_for_methods(self): + b = PrivateFormat.OpenSSH.encryption_builder() + with pytest.raises(TypeError): + b.key_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + with pytest.raises(TypeError): + b.cert_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + with pytest.raises(TypeError): + b.mac_algorithm(SHA1()) + + def test_duplicate_mac_algorithm(self): + b = PrivateFormat.PKCS12.encryption_builder().mac_algorithm(SHA1()) + with pytest.raises(ValueError): + b.mac_algorithm(SHA1()) + + def test_duplicate_cert_encryption_algorithm(self): + b = PrivateFormat.PKCS12.encryption_builder() + b = b.cert_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + with pytest.raises(ValueError): + b.cert_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC) + + def test_duplicate_key_encryption_algorithm(self): + b = PrivateFormat.PKCS12.encryption_builder().key_encryption_algorithm( + PBES.PBESv1SHA1And3KeyTripleDESCBC + ) + with pytest.raises(ValueError): + b.key_encryption_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC)