Skip to content

Commit e4e7b89

Browse files
reaperhulkalex
authored andcommitted
PKCS12 Basic Parsing (pyca#4553)
* PKCS12 parsing support * running all the tests is so gauche * rename func * various significant fixes * dangerous idiot here * move pkcs12 * docs updates * a bit more prose
1 parent 2f2f3d2 commit e4e7b89

6 files changed

Lines changed: 222 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Changelog
1616
:class:`~cryptography.hazmat.primitives.hashes.SHA3_384`, and
1717
:class:`~cryptography.hazmat.primitives.hashes.SHA3_512` when using OpenSSL
1818
1.1.1.
19+
* Added initial support for parsing PKCS12 files with
20+
:func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates`.
1921

2022
.. _v2-4-2:
2123

docs/hazmat/primitives/asymmetric/serialization.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,46 @@ DSA keys look almost identical but begin with ``ssh-dss`` rather than
397397
:raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized
398398
key is of a type that is not supported.
399399

400+
PKCS12
401+
~~~~~~
402+
403+
.. currentmodule:: cryptography.hazmat.primitives.serialization.pkcs12
404+
405+
PKCS12 is a binary format described in :rfc:`7292`. It can contain
406+
certificates, keys, and more. PKCS12 files commonly have a ``pfx`` or ``p12``
407+
file suffix.
408+
409+
.. note::
410+
411+
``cryptography`` only supports a single private key and associated
412+
certificates when parsing PKCS12 files at this time.
413+
414+
.. function:: load_key_and_certificates(data, password, backend)
415+
416+
.. versionadded:: 2.5
417+
418+
Deserialize a PKCS12 blob.
419+
420+
:param bytes data: The binary data.
421+
422+
:param bytes password: The password to use to decrypt the data. ``None``
423+
if the PKCS12 is not encrypted.
424+
425+
:param backend: A backend instance.
426+
427+
:returns: A tuple of
428+
``(private_key, certificate, additional_certificates)``.
429+
``private_key`` is a private key type or ``None``, ``certificate``
430+
is either the :class:`~cryptography.x509.Certificate` whose public key
431+
matches the private key in the PKCS 12 object or ``None``, and
432+
``additional_certificates`` is a list of all other
433+
:class:`~cryptography.x509.Certificate` instances in the PKCS12 object.
434+
400435
Serialization Formats
401436
~~~~~~~~~~~~~~~~~~~~~
402437

438+
.. currentmodule:: cryptography.hazmat.primitives.serialization
439+
403440
.. class:: PrivateFormat
404441

405442
.. versionadded:: 0.8

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,6 +2129,52 @@ def aead_cipher_supported(self, cipher):
21292129
self._lib.EVP_get_cipherbyname(cipher_name) != self._ffi.NULL
21302130
)
21312131

2132+
def load_key_and_certificates_from_pkcs12(self, data, password):
2133+
if password is None:
2134+
password = self._ffi.NULL
2135+
elif not isinstance(password, bytes):
2136+
raise TypeError("Password must be a byte string or None")
2137+
2138+
bio = self._bytes_to_bio(data)
2139+
p12 = self._lib.d2i_PKCS12_bio(bio.bio, self._ffi.NULL)
2140+
if p12 == self._ffi.NULL:
2141+
self._consume_errors()
2142+
raise ValueError("Could not deserialize PKCS12 data")
2143+
2144+
p12 = self._ffi.gc(p12, self._lib.PKCS12_free)
2145+
evp_pkey_ptr = self._ffi.new("EVP_PKEY **")
2146+
x509_ptr = self._ffi.new("X509 **")
2147+
sk_x509_ptr = self._ffi.new("Cryptography_STACK_OF_X509 **")
2148+
res = self._lib.PKCS12_parse(
2149+
p12, password, evp_pkey_ptr, x509_ptr, sk_x509_ptr
2150+
)
2151+
if res == 0:
2152+
self._consume_errors()
2153+
raise ValueError("Invalid password or PKCS12 data")
2154+
2155+
cert = None
2156+
key = None
2157+
additional_certificates = []
2158+
2159+
if evp_pkey_ptr[0] != self._ffi.NULL:
2160+
evp_pkey = self._ffi.gc(evp_pkey_ptr[0], self._lib.EVP_PKEY_free)
2161+
key = self._evp_pkey_to_private_key(evp_pkey)
2162+
2163+
if x509_ptr[0] != self._ffi.NULL:
2164+
x509 = self._ffi.gc(x509_ptr[0], self._lib.X509_free)
2165+
cert = _Certificate(self, x509)
2166+
2167+
if sk_x509_ptr[0] != self._ffi.NULL:
2168+
sk_x509 = self._ffi.gc(sk_x509_ptr[0], self._lib.sk_X509_free)
2169+
num = self._lib.sk_X509_num(sk_x509_ptr[0])
2170+
for i in range(num):
2171+
x509 = self._lib.sk_X509_value(sk_x509, i)
2172+
x509 = self._ffi.gc(x509, self._lib.X509_free)
2173+
self.openssl_assert(x509 != self._ffi.NULL)
2174+
additional_certificates.append(_Certificate(self, x509))
2175+
2176+
return (key, cert, additional_certificates)
2177+
21322178

21332179
class GetCipherByName(object):
21342180
def __init__(self, fmt):
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
from __future__ import absolute_import, division, print_function
6+
7+
8+
def load_key_and_certificates(data, password, backend):
9+
return backend.load_key_and_certificates_from_pkcs12(data, password)

tests/hazmat/backends/test_openssl_memleak.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,21 @@ def func():
307307
).add_extension(x509.OCSPNonce(b"0000"), False)
308308
req = builder.build()
309309
"""))
310+
311+
@pytest.mark.parametrize("path", [
312+
"pkcs12/cert-aes256cbc-no-key.p12",
313+
"pkcs12/cert-key-aes256cbc.p12",
314+
])
315+
def test_load_pkcs12_key_and_certificates(self, path):
316+
assert_no_memory_leaks(textwrap.dedent("""
317+
def func(path):
318+
from cryptography import x509
319+
from cryptography.hazmat.backends.openssl import backend
320+
from cryptography.hazmat.primitives.serialization import pkcs12
321+
import cryptography_vectors
322+
323+
with cryptography_vectors.open_vector_file(path, "rb") as f:
324+
pkcs12.load_key_and_certificates(
325+
f.read(), b"cryptography", backend
326+
)
327+
"""), [path])
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
from __future__ import absolute_import, division, print_function
6+
7+
import os
8+
9+
import pytest
10+
11+
from cryptography import x509
12+
from cryptography.hazmat.backends.interfaces import DERSerializationBackend
13+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
14+
from cryptography.hazmat.primitives.serialization.pkcs12 import (
15+
load_key_and_certificates
16+
)
17+
18+
from .utils import load_vectors_from_file
19+
20+
21+
@pytest.mark.requires_backend_interface(interface=DERSerializationBackend)
22+
class TestPKCS12(object):
23+
@pytest.mark.parametrize(
24+
("filename", "password"),
25+
[
26+
("cert-key-aes256cbc.p12", b"cryptography"),
27+
("cert-none-key-none.p12", b"cryptography"),
28+
("cert-rc2-key-3des.p12", b"cryptography"),
29+
("no-password.p12", None),
30+
]
31+
)
32+
def test_load_pkcs12_ec_keys(self, filename, password, backend):
33+
cert = load_vectors_from_file(
34+
os.path.join("x509", "custom", "ca", "ca.pem"),
35+
lambda pemfile: x509.load_pem_x509_certificate(
36+
pemfile.read(), backend
37+
), mode="rb"
38+
)
39+
key = load_vectors_from_file(
40+
os.path.join("x509", "custom", "ca", "ca_key.pem"),
41+
lambda pemfile: load_pem_private_key(
42+
pemfile.read(), None, backend
43+
), mode="rb"
44+
)
45+
parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file(
46+
os.path.join("pkcs12", filename),
47+
lambda derfile: load_key_and_certificates(
48+
derfile.read(), password, backend
49+
), mode="rb"
50+
)
51+
assert parsed_cert == cert
52+
assert parsed_key.private_numbers() == key.private_numbers()
53+
assert parsed_more_certs == []
54+
55+
def test_load_pkcs12_cert_only(self, backend):
56+
cert = load_vectors_from_file(
57+
os.path.join("x509", "custom", "ca", "ca.pem"),
58+
lambda pemfile: x509.load_pem_x509_certificate(
59+
pemfile.read(), backend
60+
), mode="rb"
61+
)
62+
parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file(
63+
os.path.join("pkcs12", "cert-aes256cbc-no-key.p12"),
64+
lambda data: load_key_and_certificates(
65+
data.read(), b"cryptography", backend
66+
),
67+
mode="rb"
68+
)
69+
assert parsed_cert is None
70+
assert parsed_key is None
71+
assert parsed_more_certs == [cert]
72+
73+
def test_load_pkcs12_key_only(self, backend):
74+
key = load_vectors_from_file(
75+
os.path.join("x509", "custom", "ca", "ca_key.pem"),
76+
lambda pemfile: load_pem_private_key(
77+
pemfile.read(), None, backend
78+
), mode="rb"
79+
)
80+
parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file(
81+
os.path.join("pkcs12", "no-cert-key-aes256cbc.p12"),
82+
lambda data: load_key_and_certificates(
83+
data.read(), b"cryptography", backend
84+
),
85+
mode="rb"
86+
)
87+
assert parsed_key.private_numbers() == key.private_numbers()
88+
assert parsed_cert is None
89+
assert parsed_more_certs == []
90+
91+
def test_non_bytes(self, backend):
92+
with pytest.raises(TypeError):
93+
load_key_and_certificates(
94+
b"irrelevant", object(), backend
95+
)
96+
97+
def test_not_a_pkcs12(self, backend):
98+
with pytest.raises(ValueError):
99+
load_key_and_certificates(
100+
b"invalid", b"pass", backend
101+
)
102+
103+
def test_invalid_password(self, backend):
104+
with pytest.raises(ValueError):
105+
load_vectors_from_file(
106+
os.path.join("pkcs12", "cert-key-aes256cbc.p12"),
107+
lambda derfile: load_key_and_certificates(
108+
derfile.read(), b"invalid", backend
109+
), mode="rb"
110+
)

0 commit comments

Comments
 (0)