Skip to content

Commit d377d46

Browse files
committed
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
1 parent abb1f54 commit d377d46

File tree

6 files changed

+372
-13
lines changed

6 files changed

+372
-13
lines changed

docs/hazmat/primitives/asymmetric/serialization.rst

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,24 @@ file suffix.
579579
A list of :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12Certificate`
580580
instances.
581581

582+
.. class:: PBES
583+
584+
.. versionadded:: 38.0.0
585+
586+
An enumeration of password-based encryption schemes used in PKCS12. These
587+
values are used with
588+
:class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryptionBuilder`.
589+
590+
.. attribute:: PBESv1SHA1And3KeyTripleDESCBC
591+
592+
PBESv1 using SHA1 as the KDF PRF and 3-key triple DES as the cipher.
593+
594+
.. attribute:: PBESv2SHA256AndAES256CBC
595+
596+
PBESv2 using SHA256 as the KDF PRF and AES256 as the cipher. This is
597+
only supported on OpenSSL 3.0.0 or newer.
598+
599+
582600
PKCS7
583601
~~~~~
584602

@@ -850,7 +868,7 @@ Serialization Formats
850868

851869
For most use cases, :class:`BestAvailableEncryption` is preferred.
852870

853-
:returns KeySerializationEncryptionBuilder: A new builder.
871+
:returns :class:`KeySerializationEncryptionBuilder`: A new builder.
854872

855873
.. doctest::
856874

@@ -1022,7 +1040,8 @@ Serialization Encryption Types
10221040

10231041
Encrypt using the best available encryption for a given key.
10241042
This is a curated encryption choice and the algorithm may change over
1025-
time.
1043+
time. The encryption algorithm may vary based on which version of OpenSSL
1044+
the library is compiled against.
10261045

10271046
:param bytes password: The password to use for encryption.
10281047

@@ -1033,25 +1052,51 @@ Serialization Encryption Types
10331052

10341053
.. class:: KeySerializationEncryptionBuilder
10351054

1036-
A builder that can be used to configure how key data is encrypted. To
1037-
create one, call :meth:`PrivateFormat.encryption_builder`.
1055+
.. versionadded:: 38.0.0
1056+
1057+
A builder that can be used to configure how data is encrypted. To
1058+
create one, call :meth:`PrivateFormat.encryption_builder`. Different
1059+
serialization types will use different options on this builder.
10381060

10391061
.. method:: kdf_rounds(rounds)
10401062

10411063
Set the number of rounds the Key Derivation Function should use. The
10421064
meaning of the number of rounds varies on the KDF being used.
10431065

10441066
:param int rounds: Number of rounds.
1045-
:returns KeySerializationEncryptionBuilder: A new builder.
1067+
1068+
.. method:: cert_encryption_algorithm(algorithm)
1069+
1070+
Set the encryption algorithm to use when encrypting certificates in
1071+
a PKCS12 structure.
1072+
1073+
:param algorithm: A value from the :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PBES`
1074+
enumeration.
1075+
1076+
.. method:: key_encryption_algorithm(algorithm)
1077+
1078+
Set the encryption algorithm to use when encrypting the key in a
1079+
PKCS12 structure.
1080+
1081+
:param algorithm: A value from the :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PBES`
1082+
enumeration.
1083+
1084+
.. method:: mac_algorithm(algorithm)
1085+
1086+
Set the MAC algorithm to use for a PKCS12 structure.
1087+
1088+
:param algorithm: An instance of a
1089+
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
10461090

10471091
.. method:: build(password)
10481092

10491093
Turns the builder into an instance of
10501094
:class:`KeySerializationEncryption` with a given password.
10511095

10521096
:param bytes password: The password.
1053-
:returns KeySerializationEncryption: A key key serialization
1054-
encryption that can be passed to ``private_bytes`` methods.
1097+
:returns: A :class:`KeySerializationEncryption` encryption object
1098+
that can be passed to methods like ``private_bytes`` or
1099+
:func:`~cryptography.hazmat.primitives.serialization.pkcs12.serialize_key_and_certificates`.
10551100

10561101
.. _`a bug in Firefox`: https://bugzilla.mozilla.org/show_bug.cgi?id=773111
10571102
.. _`PKCS3`: https://www.teletrust.de/fileadmin/files/oid/oid_pkcs-3v1-4.pdf

src/cryptography/hazmat/backends/openssl/backend.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
from cryptography.hazmat.primitives.kdf import scrypt
117117
from cryptography.hazmat.primitives.serialization import pkcs7, ssh
118118
from cryptography.hazmat.primitives.serialization.pkcs12 import (
119+
PBES,
119120
PKCS12Certificate,
120121
PKCS12KeyAndCertificates,
121122
_ALLOWED_PKCS12_TYPES,
@@ -2263,20 +2264,71 @@ def serialize_key_and_certificates_to_pkcs12(
22632264
nid_key = -1
22642265
pkcs12_iter = 0
22652266
mac_iter = 0
2267+
mac_alg = self._ffi.NULL
22662268
elif isinstance(
22672269
encryption_algorithm, serialization.BestAvailableEncryption
22682270
):
22692271
# PKCS12 encryption is hopeless trash and can never be fixed.
2270-
# This is the least terrible option.
2271-
nid_cert = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC
2272-
nid_key = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC
2272+
# OpenSSL 3 supports PBESv2, but Libre and Boring do not, so
2273+
# we use PBESv1 with 3DES on the older paths.
2274+
if self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER:
2275+
nid_cert = self._lib.NID_aes_256_cbc
2276+
nid_key = self._lib.NID_aes_256_cbc
2277+
else:
2278+
nid_cert = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC
2279+
nid_key = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC
22732280
# At least we can set this higher than OpenSSL's default
22742281
pkcs12_iter = 20000
22752282
# mac_iter chosen for compatibility reasons, see:
22762283
# https://www.openssl.org/docs/man1.1.1/man3/PKCS12_create.html
22772284
# Did we mention how lousy PKCS12 encryption is?
22782285
mac_iter = 1
2286+
# MAC algorithm can only be set on OpenSSL 3.0.0+
2287+
mac_alg = self._ffi.NULL
22792288
password = encryption_algorithm.password
2289+
elif isinstance(
2290+
encryption_algorithm, serialization._KeySerializationEncryption
2291+
):
2292+
# Default to OpenSSL's defaults. Behavior will vary based on the
2293+
# version of OpenSSL cryptography is compiled against.
2294+
nid_cert = 0
2295+
nid_key = 0
2296+
# Use the default iters we use in best available
2297+
pkcs12_iter = 20000
2298+
# See the Best Available comment for why this is 1
2299+
mac_iter = 1
2300+
password = encryption_algorithm.password
2301+
certencalg = encryption_algorithm._cert_encryption_algorithm
2302+
keyencalg = encryption_algorithm._key_encryption_algorithm
2303+
if certencalg is PBES.PBESv1SHA1And3KeyTripleDESCBC:
2304+
nid_cert = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC
2305+
elif certencalg is PBES.PBESv2SHA256AndAES256CBC:
2306+
if not self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER:
2307+
raise UnsupportedAlgorithm(
2308+
"PBESv2 is not supported by this version of OpenSSL"
2309+
)
2310+
nid_cert = self._lib.NID_aes_256_cbc
2311+
2312+
if keyencalg is PBES.PBESv1SHA1And3KeyTripleDESCBC:
2313+
nid_key = self._lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC
2314+
elif keyencalg is PBES.PBESv2SHA256AndAES256CBC:
2315+
if not self._lib.CRYPTOGRAPHY_OPENSSL_300_OR_GREATER:
2316+
raise UnsupportedAlgorithm(
2317+
"PBESv2 is not supported by this version of OpenSSL"
2318+
)
2319+
nid_cert = self._lib.NID_aes_256_cbc
2320+
2321+
if encryption_algorithm._mac_algorithm is not None:
2322+
mac_alg = self._evp_md_non_null_from_algorithm(
2323+
encryption_algorithm._mac_algorithm
2324+
)
2325+
self.openssl_assert(mac_alg != self._ffi.NULL)
2326+
else:
2327+
mac_alg = self._ffi.NULL
2328+
2329+
if encryption_algorithm._kdf_rounds is not None:
2330+
pkcs12_iter = encryption_algorithm._kdf_rounds
2331+
22802332
else:
22812333
raise ValueError("Unsupported key encryption type")
22822334

@@ -2326,6 +2378,20 @@ def serialize_key_and_certificates_to_pkcs12(
23262378
0,
23272379
)
23282380

2381+
if (
2382+
self._lib.Cryptography_HAS_PKCS12_SET_MAC
2383+
and mac_alg != self._ffi.NULL
2384+
):
2385+
self._lib.PKCS12_set_mac(
2386+
p12,
2387+
password_buf,
2388+
-1,
2389+
self._ffi.NULL,
2390+
0,
2391+
mac_iter,
2392+
mac_alg,
2393+
)
2394+
23292395
self.openssl_assert(p12 != self._ffi.NULL)
23302396
p12 = self._ffi.gc(p12, self._lib.PKCS12_free)
23312397

src/cryptography/hazmat/primitives/_serialization.py

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
import typing
77

88
from cryptography import utils
9+
from cryptography.hazmat.primitives.hashes import HashAlgorithm
910

1011
# This exists to break an import cycle. These classes are normally accessible
1112
# from the serialization module.
1213

1314

15+
class PBES(utils.Enum):
16+
PBESv1SHA1And3KeyTripleDESCBC = "PBESv1 using SHA1 and 3-Key TripleDES"
17+
PBESv2SHA256AndAES256CBC = "PBESv2 using SHA256 PBKDF2 and AES256 CBC"
18+
19+
1420
class Encoding(utils.Enum):
1521
PEM = "PEM"
1622
DER = "DER"
@@ -25,11 +31,13 @@ class PrivateFormat(utils.Enum):
2531
TraditionalOpenSSL = "TraditionalOpenSSL"
2632
Raw = "Raw"
2733
OpenSSH = "OpenSSH"
34+
PKCS12 = "PKCS12"
2835

2936
def encryption_builder(self) -> "KeySerializationEncryptionBuilder":
30-
if self is not PrivateFormat.OpenSSH:
37+
if self not in (PrivateFormat.OpenSSH, PrivateFormat.PKCS12):
3138
raise ValueError(
3239
"encryption_builder only supported with PrivateFormat.OpenSSH"
40+
" and PrivateFormat.PKCS12"
3341
)
3442
return KeySerializationEncryptionBuilder(self)
3543

@@ -69,24 +77,98 @@ def __init__(
6977
format: PrivateFormat,
7078
*,
7179
_kdf_rounds: typing.Optional[int] = None,
80+
_mac_algorithm: typing.Optional[HashAlgorithm] = None,
81+
_cert_encryption_algorithm: typing.Optional[PBES] = None,
82+
_key_encryption_algorithm: typing.Optional[PBES] = None,
7283
) -> None:
7384
self._format = format
7485

7586
self._kdf_rounds = _kdf_rounds
87+
self._mac_algorithm = _mac_algorithm
88+
self._cert_encryption_algorithm = _cert_encryption_algorithm
89+
self._key_encryption_algorithm = _key_encryption_algorithm
7690

7791
def kdf_rounds(self, rounds: int) -> "KeySerializationEncryptionBuilder":
7892
if self._kdf_rounds is not None:
7993
raise ValueError("kdf_rounds already set")
94+
95+
if not isinstance(rounds, int) or rounds < 1:
96+
raise ValueError("kdf_rounds must be a positive integer")
97+
98+
return KeySerializationEncryptionBuilder(
99+
self._format,
100+
_kdf_rounds=rounds,
101+
_mac_algorithm=self._mac_algorithm,
102+
_cert_encryption_algorithm=self._cert_encryption_algorithm,
103+
_key_encryption_algorithm=self._key_encryption_algorithm,
104+
)
105+
106+
def mac_algorithm(
107+
self, algorithm: HashAlgorithm
108+
) -> "KeySerializationEncryptionBuilder":
109+
if self._format is not PrivateFormat.PKCS12:
110+
raise TypeError(
111+
"mac_algorithm only supported with PrivateFormat.PKCS12"
112+
)
113+
114+
if self._mac_algorithm is not None:
115+
raise ValueError("mac_algorithm already set")
116+
return KeySerializationEncryptionBuilder(
117+
self._format,
118+
_kdf_rounds=self._kdf_rounds,
119+
_mac_algorithm=algorithm,
120+
_cert_encryption_algorithm=self._cert_encryption_algorithm,
121+
_key_encryption_algorithm=self._key_encryption_algorithm,
122+
)
123+
124+
def cert_encryption_algorithm(
125+
self, algorithm: PBES
126+
) -> "KeySerializationEncryptionBuilder":
127+
if self._format is not PrivateFormat.PKCS12:
128+
raise TypeError(
129+
"cert_encryption_algorithm only supported with "
130+
"PrivateFormat.PKCS12"
131+
)
132+
133+
if self._cert_encryption_algorithm is not None:
134+
raise ValueError("cert_encryption_algorithm already set")
135+
return KeySerializationEncryptionBuilder(
136+
self._format,
137+
_kdf_rounds=self._kdf_rounds,
138+
_mac_algorithm=self._mac_algorithm,
139+
_cert_encryption_algorithm=algorithm,
140+
_key_encryption_algorithm=self._key_encryption_algorithm,
141+
)
142+
143+
def key_encryption_algorithm(
144+
self, algorithm: PBES
145+
) -> "KeySerializationEncryptionBuilder":
146+
if self._format is not PrivateFormat.PKCS12:
147+
raise TypeError(
148+
"key_encryption_algorithm only supported with "
149+
"PrivateFormat.PKCS12"
150+
)
151+
if self._key_encryption_algorithm is not None:
152+
raise ValueError("key_encryption_algorithm already set")
80153
return KeySerializationEncryptionBuilder(
81-
self._format, _kdf_rounds=rounds
154+
self._format,
155+
_kdf_rounds=self._kdf_rounds,
156+
_mac_algorithm=self._mac_algorithm,
157+
_cert_encryption_algorithm=self._cert_encryption_algorithm,
158+
_key_encryption_algorithm=algorithm,
82159
)
83160

84161
def build(self, password: bytes) -> KeySerializationEncryption:
85162
if not isinstance(password, bytes) or len(password) == 0:
86163
raise ValueError("Password must be 1 or more bytes.")
87164

88165
return _KeySerializationEncryption(
89-
self._format, password, kdf_rounds=self._kdf_rounds
166+
self._format,
167+
password,
168+
kdf_rounds=self._kdf_rounds,
169+
mac_algorithm=self._mac_algorithm,
170+
cert_encryption_algorithm=self._cert_encryption_algorithm,
171+
key_encryption_algorithm=self._key_encryption_algorithm,
90172
)
91173

92174

@@ -97,8 +179,14 @@ def __init__(
97179
password: bytes,
98180
*,
99181
kdf_rounds: typing.Optional[int],
182+
mac_algorithm: typing.Optional[HashAlgorithm],
183+
cert_encryption_algorithm: typing.Optional[PBES],
184+
key_encryption_algorithm: typing.Optional[PBES],
100185
):
101186
self._format = format
102187
self.password = password
103188

104189
self._kdf_rounds = kdf_rounds
190+
self._mac_algorithm = mac_algorithm
191+
self._cert_encryption_algorithm = cert_encryption_algorithm
192+
self._key_encryption_algorithm = key_encryption_algorithm

src/cryptography/hazmat/primitives/serialization/pkcs12.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from cryptography import x509
88
from cryptography.hazmat.primitives import serialization
9+
from cryptography.hazmat.primitives._serialization import PBES as PBES
910
from cryptography.hazmat.primitives.asymmetric import (
1011
dsa,
1112
ec,
@@ -17,6 +18,10 @@
1718
PRIVATE_KEY_TYPES,
1819
)
1920

21+
# PBES is a PKCS12 specific item, but it lives in _serialization to avoid an
22+
# import cycle. We re-export it here for the public API.
23+
__all__ = ["PBES"]
24+
2025

2126
_ALLOWED_PKCS12_TYPES = typing.Union[
2227
rsa.RSAPrivateKey,

0 commit comments

Comments
 (0)