Skip to content

Commit

Permalink
support setting more PKCS12 serialization encryption options
Browse files Browse the repository at this point in the history
This is limited support, but makes it possible to set two different PBES
choices as well as set KDF rounds and MAC algorithm
  • Loading branch information
reaperhulk committed Sep 4, 2022
1 parent abb1f54 commit 00921fa
Show file tree
Hide file tree
Showing 6 changed files with 377 additions and 13 deletions.
59 changes: 52 additions & 7 deletions docs/hazmat/primitives/asymmetric/serialization.rst
Expand Up @@ -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
~~~~~

Expand Down Expand Up @@ -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::

Expand Down Expand Up @@ -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.

Expand All @@ -1033,25 +1052,51 @@ 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)

Set the number of rounds the Key Derivation Function should use. The
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)

Turns the builder into an instance of
: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
Expand Down
72 changes: 69 additions & 3 deletions src/cryptography/hazmat/backends/openssl/backend.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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)

Expand Down
94 changes: 91 additions & 3 deletions src/cryptography/hazmat/primitives/_serialization.py
Expand Up @@ -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"
Expand All @@ -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)

Expand Down Expand Up @@ -69,24 +77,98 @@ 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:
if not isinstance(password, bytes) or len(password) == 0:
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,
)


Expand All @@ -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
5 changes: 5 additions & 0 deletions src/cryptography/hazmat/primitives/serialization/pkcs12.py
Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 00921fa

Please sign in to comment.