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
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6f2c964..0cc468c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,8 @@
   :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 90ec10e..7b3fb1d 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -397,9 +397,46 @@
     :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 44c2e3c..5a22a55 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -2129,6 +2129,52 @@
             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 0000000..98161d5
--- /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 483387a..6f42ed7 100644
--- a/tests/hazmat/backends/test_openssl_memleak.py
+++ b/tests/hazmat/backends/test_openssl_memleak.py
@@ -307,3 +307,21 @@
             ).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 0000000..85be3b5
--- /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"
+            )