diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2018-11-29 11:51:38 +0800 |
---|---|---|
committer | Alex Gaynor <alex.gaynor@gmail.com> | 2018-11-28 22:51:38 -0500 |
commit | e4e7b89fb627b372cde4158ceb7078d8769497cb (patch) | |
tree | 9dc87beda0cf2d1a948feea01c87361feb1a32af | |
parent | 2f2f3d2e414a0167ae3a98b9b608904b2c76a35f (diff) | |
download | cryptography-e4e7b89fb627b372cde4158ceb7078d8769497cb.tar.gz cryptography-e4e7b89fb627b372cde4158ceb7078d8769497cb.tar.bz2 cryptography-e4e7b89fb627b372cde4158ceb7078d8769497cb.zip |
PKCS12 Basic Parsing (#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
-rw-r--r-- | CHANGELOG.rst | 2 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 37 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 46 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/serialization/pkcs12.py | 9 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl_memleak.py | 18 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_pkcs12.py | 110 |
6 files changed, 222 insertions, 0 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6f2c964a..0cc468c5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,8 @@ Changelog :class:`~cryptography.hazmat.primitives.hashes.SHA3_384`, and :class:`~cryptography.hazmat.primitives.hashes.SHA3_512` when using OpenSSL 1.1.1. +* Added initial support for parsing PKCS12 files with + :func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates`. .. _v2-4-2: diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 90ec10eb..7b3fb1d6 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -397,9 +397,46 @@ DSA keys look almost identical but begin with ``ssh-dss`` rather than :raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized key is of a type that is not supported. +PKCS12 +~~~~~~ + +.. currentmodule:: cryptography.hazmat.primitives.serialization.pkcs12 + +PKCS12 is a binary format described in :rfc:`7292`. It can contain +certificates, keys, and more. PKCS12 files commonly have a ``pfx`` or ``p12`` +file suffix. + +.. note:: + + ``cryptography`` only supports a single private key and associated + certificates when parsing PKCS12 files at this time. + +.. function:: load_key_and_certificates(data, password, backend) + + .. versionadded:: 2.5 + + Deserialize a PKCS12 blob. + + :param bytes data: The binary data. + + :param bytes password: The password to use to decrypt the data. ``None`` + if the PKCS12 is not encrypted. + + :param backend: A backend instance. + + :returns: A tuple of + ``(private_key, certificate, additional_certificates)``. + ``private_key`` is a private key type or ``None``, ``certificate`` + is either the :class:`~cryptography.x509.Certificate` whose public key + matches the private key in the PKCS 12 object or ``None``, and + ``additional_certificates`` is a list of all other + :class:`~cryptography.x509.Certificate` instances in the PKCS12 object. + Serialization Formats ~~~~~~~~~~~~~~~~~~~~~ +.. currentmodule:: cryptography.hazmat.primitives.serialization + .. class:: PrivateFormat .. versionadded:: 0.8 diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 44c2e3cd..5a22a555 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -2129,6 +2129,52 @@ class Backend(object): self._lib.EVP_get_cipherbyname(cipher_name) != self._ffi.NULL ) + def load_key_and_certificates_from_pkcs12(self, data, password): + if password is None: + password = self._ffi.NULL + elif not isinstance(password, bytes): + raise TypeError("Password must be a byte string or None") + + bio = self._bytes_to_bio(data) + p12 = self._lib.d2i_PKCS12_bio(bio.bio, self._ffi.NULL) + if p12 == self._ffi.NULL: + self._consume_errors() + raise ValueError("Could not deserialize PKCS12 data") + + p12 = self._ffi.gc(p12, self._lib.PKCS12_free) + evp_pkey_ptr = self._ffi.new("EVP_PKEY **") + x509_ptr = self._ffi.new("X509 **") + sk_x509_ptr = self._ffi.new("Cryptography_STACK_OF_X509 **") + res = self._lib.PKCS12_parse( + p12, password, evp_pkey_ptr, x509_ptr, sk_x509_ptr + ) + if res == 0: + self._consume_errors() + raise ValueError("Invalid password or PKCS12 data") + + cert = None + key = None + additional_certificates = [] + + if evp_pkey_ptr[0] != self._ffi.NULL: + evp_pkey = self._ffi.gc(evp_pkey_ptr[0], self._lib.EVP_PKEY_free) + key = self._evp_pkey_to_private_key(evp_pkey) + + if x509_ptr[0] != self._ffi.NULL: + x509 = self._ffi.gc(x509_ptr[0], self._lib.X509_free) + cert = _Certificate(self, x509) + + if sk_x509_ptr[0] != self._ffi.NULL: + sk_x509 = self._ffi.gc(sk_x509_ptr[0], self._lib.sk_X509_free) + num = self._lib.sk_X509_num(sk_x509_ptr[0]) + for i in range(num): + x509 = self._lib.sk_X509_value(sk_x509, i) + x509 = self._ffi.gc(x509, self._lib.X509_free) + self.openssl_assert(x509 != self._ffi.NULL) + additional_certificates.append(_Certificate(self, x509)) + + return (key, cert, additional_certificates) + class GetCipherByName(object): def __init__(self, fmt): diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs12.py b/src/cryptography/hazmat/primitives/serialization/pkcs12.py new file mode 100644 index 00000000..98161d57 --- /dev/null +++ b/src/cryptography/hazmat/primitives/serialization/pkcs12.py @@ -0,0 +1,9 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + + +def load_key_and_certificates(data, password, backend): + return backend.load_key_and_certificates_from_pkcs12(data, password) diff --git a/tests/hazmat/backends/test_openssl_memleak.py b/tests/hazmat/backends/test_openssl_memleak.py index 483387af..6f42ed79 100644 --- a/tests/hazmat/backends/test_openssl_memleak.py +++ b/tests/hazmat/backends/test_openssl_memleak.py @@ -307,3 +307,21 @@ class TestOpenSSLMemoryLeaks(object): ).add_extension(x509.OCSPNonce(b"0000"), False) req = builder.build() """)) + + @pytest.mark.parametrize("path", [ + "pkcs12/cert-aes256cbc-no-key.p12", + "pkcs12/cert-key-aes256cbc.p12", + ]) + def test_load_pkcs12_key_and_certificates(self, path): + assert_no_memory_leaks(textwrap.dedent(""" + def func(path): + from cryptography import x509 + from cryptography.hazmat.backends.openssl import backend + from cryptography.hazmat.primitives.serialization import pkcs12 + import cryptography_vectors + + with cryptography_vectors.open_vector_file(path, "rb") as f: + pkcs12.load_key_and_certificates( + f.read(), b"cryptography", backend + ) + """), [path]) diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py new file mode 100644 index 00000000..85be3b51 --- /dev/null +++ b/tests/hazmat/primitives/test_pkcs12.py @@ -0,0 +1,110 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import os + +import pytest + +from cryptography import x509 +from cryptography.hazmat.backends.interfaces import DERSerializationBackend +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.primitives.serialization.pkcs12 import ( + load_key_and_certificates +) + +from .utils import load_vectors_from_file + + +@pytest.mark.requires_backend_interface(interface=DERSerializationBackend) +class TestPKCS12(object): + @pytest.mark.parametrize( + ("filename", "password"), + [ + ("cert-key-aes256cbc.p12", b"cryptography"), + ("cert-none-key-none.p12", b"cryptography"), + ("cert-rc2-key-3des.p12", b"cryptography"), + ("no-password.p12", None), + ] + ) + def test_load_pkcs12_ec_keys(self, filename, password, backend): + cert = load_vectors_from_file( + os.path.join("x509", "custom", "ca", "ca.pem"), + lambda pemfile: x509.load_pem_x509_certificate( + pemfile.read(), backend + ), mode="rb" + ) + key = load_vectors_from_file( + os.path.join("x509", "custom", "ca", "ca_key.pem"), + lambda pemfile: load_pem_private_key( + pemfile.read(), None, backend + ), mode="rb" + ) + parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file( + os.path.join("pkcs12", filename), + lambda derfile: load_key_and_certificates( + derfile.read(), password, backend + ), mode="rb" + ) + assert parsed_cert == cert + assert parsed_key.private_numbers() == key.private_numbers() + assert parsed_more_certs == [] + + def test_load_pkcs12_cert_only(self, backend): + cert = load_vectors_from_file( + os.path.join("x509", "custom", "ca", "ca.pem"), + lambda pemfile: x509.load_pem_x509_certificate( + pemfile.read(), backend + ), mode="rb" + ) + parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file( + os.path.join("pkcs12", "cert-aes256cbc-no-key.p12"), + lambda data: load_key_and_certificates( + data.read(), b"cryptography", backend + ), + mode="rb" + ) + assert parsed_cert is None + assert parsed_key is None + assert parsed_more_certs == [cert] + + def test_load_pkcs12_key_only(self, backend): + key = load_vectors_from_file( + os.path.join("x509", "custom", "ca", "ca_key.pem"), + lambda pemfile: load_pem_private_key( + pemfile.read(), None, backend + ), mode="rb" + ) + parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file( + os.path.join("pkcs12", "no-cert-key-aes256cbc.p12"), + lambda data: load_key_and_certificates( + data.read(), b"cryptography", backend + ), + mode="rb" + ) + assert parsed_key.private_numbers() == key.private_numbers() + assert parsed_cert is None + assert parsed_more_certs == [] + + def test_non_bytes(self, backend): + with pytest.raises(TypeError): + load_key_and_certificates( + b"irrelevant", object(), backend + ) + + def test_not_a_pkcs12(self, backend): + with pytest.raises(ValueError): + load_key_and_certificates( + b"invalid", b"pass", backend + ) + + def test_invalid_password(self, backend): + with pytest.raises(ValueError): + load_vectors_from_file( + os.path.join("pkcs12", "cert-key-aes256cbc.p12"), + lambda derfile: load_key_and_certificates( + derfile.read(), b"invalid", backend + ), mode="rb" + ) |