Repository: pyca/cryptography

Issue:
Being able to read PKCS12 'friendly name'
Right now, the interface for loading PKCS12 files (https://cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization.html?highlight=pkcs12#cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates) returns a tuple `(private_key, certificate, additional_certificates)`. This tuple does not contain the 'friendly name', and assuming you don't want to have a different tuple length depending on some function argument, this cannot be properly extended.

Goal: make it possible to load the PKCS12 friendly name.

Idea: how about creating a small immutable object for PKCS12 content (i.e. it contains private key, certificate, list of additional certificates, friendly name - and maybe more in the future?) that can be serialized and deserialized by a new set of functions?

Does this sounds good? Does anyone have a better idea? I'm hesistant of suggestion a completely new interface, now that we "just" got PKCS12 support a few versions ago. But I can't think of a much better way to be able to return more data (friendly name now) while keeping backwards compatibility.



Discussion:
@reaperhulk I guess this would be what you originally proposed in https://github.com/pyca/cryptography/issues/2860#issuecomment-433680572 and what you implemented in #4553 until https://github.com/pyca/cryptography/compare/b2b793f01ba187ad887b1868898848cb43a43053..b6e8f170b0c3a4764017d4bf6f7f060c9ba08003 / https://github.com/pyca/cryptography/pull/4612#issuecomment-441614062.
The existence of the current API doesn't preclude adding a more fully-featured API for people who need more complexity. In a parsed object how do you envision the friendly name being accessed though? e.g. if I have a `PKCS12` object that contains two certificates, how do I get the associated value? Can friendly names be associated with a private key?
That's a very good question. It depends a lot on how fully-featured the API should be. For me, it would be enough if the API allows to extract the same information that were used during serialization with cryptography, i.e. what OpenSSL's `PKCS12_create()` does: the friendly name of the parsed PKCS12 would be the friendly name associated with the serialized key+certificate pair (or more precisely, of the certificate that's part of that pair). (And if there is no key, there's no friendly name.)

That would be essentially the same what PyOpenSSL's `PKCS12` offers.

(I guess everything more complicated would make parsing / serializing more complicated, since `PKCS12_parse()` and `PKCS12_create()` can no longer be used.)
I think this is another situation where perfect is the enemy of good. We will paralyze ourselves forever if we attempt to design an API that handles all the complexity of PKCS12 (because it is a fractal of insanity). What's the minimum API we can do here? Is it safe to do `load_key_and_certificates_with_friendly_name` and return a four element tuple? Can there be more than one friendly name?
I don't think it's a matter of changing the interface.

`PKCS12_parse()` is already mapping the `friendlyName` onto each certificate (See [here](https://www.openssl.org/docs/man1.1.1/man3/PKCS12_parse.html))

> NOTES
> The parameters pkey and cert cannot be NULL. ca can be <NULL> in which case additional certificates will be discarded. *ca can 
> 
> also be a valid STACK in which case additional certificates are appended to *ca. If *ca is NULL a new STACK will be allocated.
> 
> The friendlyName and localKeyID attributes (if present) on each certificate will be stored in the alias and keyid attributes of the X509 structure.

However `alias` is not a part of the certificate, but the greater x509 structure within the `trust settings` which are not accessible by any convenient means and are excluded in the default PEM representation (See [here](https://www.openssl.org/docs/man1.0.2/man3/PEM_write_bio_X509.html))

> The X509 functions process an X509 certificate using an X509 structure. They will also process a trusted X509 certificate but any trust settings are discarded.

There looks to be an alternative function which can handle `trust settings` however. (See [here](https://www.openssl.org/docs/man1.0.2/man3/PEM_write_bio_X509_AUX.html))

> The X509_AUX functions process a trusted X509 certificate using an X509 structure.

But I think making the `alias` accessible from each `certificate` object is the correct approach. (Or maybe that should be reserved for a new `TrustedCertificate` class assuming OpenSSL's trusted certificates can be consistently identified)
I did a little more digging and there are in fact specific functions which can retrieve the trusted settings. (See [here](https://github.com/openssl/openssl/blob/a227ff336fc934b4a52c4659ae423dfa10efd5f3/crypto/x509/x_x509a.c#L85-L101))

alias (friendlyName) -> X509_alias_get0
keyid (localKeyID) -> X509_keyid_get0

In particular X509_alias_get0 is already included in `cryptography` so as a temporary hack @felixfontein it would be possible to do the following:

```python
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates
from cryptography.hazmat.primitives.serialization import Encoding

with open('./any_pkcs12_file', 'rb') as p12:
    pkcs12 = load_key_and_certificates(
        p12.read(),
        password='password'.encode('UTF-8'),
    )

_, cert, _ = pkcs12

print(
    cert._backend._ffi.string(
        cert._backend._lib.X509_alias_get0(
            cert._x509, cert._backend._ffi.new("int *")
        )
    )
)
```
This workaround unfortunately stopped working (the most obvious reason why it stopped is that `cert._backend` no longer exists, but replacing it by a new instance of `default_backend()` doesn't work either - it looks like that information has been stripped from `cert._x509`).
Yeah we need a public API here now because we no longer use OpenSSL for our x509.Certificate structures.

Resolving patch:
diff --git a/src/cryptography/hazmat/backends/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py
--- a/src/cryptography/hazmat/backends/interfaces.py
+++ b/src/cryptography/hazmat/backends/interfaces.py
@@ -440,6 +440,12 @@ def load_key_and_certificates_from_pkcs12(self, data, password):
         Returns a tuple of (key, cert, [certs])
         """
 
+    @abc.abstractmethod
+    def load_pkcs12(self, data, password):
+        """
+        Returns a PKCS12KeyAndCertificates object
+        """
+
     @abc.abstractmethod
     def serialize_key_and_certificates_to_pkcs12(
         self, name, key, cert, cas, encryption_algorithm
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -118,6 +118,10 @@
 )
 from cryptography.hazmat.primitives.kdf import scrypt
 from cryptography.hazmat.primitives.serialization import pkcs7, ssh
+from cryptography.hazmat.primitives.serialization.pkcs12 import (
+    PKCS12Certificate,
+    PKCS12KeyAndCertificates,
+)
 from cryptography.x509 import ocsp
 from cryptography.x509.base import PUBLIC_KEY_TYPES
 from cryptography.x509.name import Name
@@ -2499,6 +2503,14 @@ def _zeroed_null_terminated_buf(self, data):
                 self._zero_data(self._ffi.cast("uint8_t *", buf), data_len)
 
     def load_key_and_certificates_from_pkcs12(self, data, password):
+        pkcs12 = self.load_pkcs12(data, password)
+        return (
+            pkcs12.key,
+            pkcs12.cert.certificate if pkcs12.cert else None,
+            [cert.certificate for cert in pkcs12.additional_certs],
+        )
+
+    def load_pkcs12(self, data, password):
         if password is not None:
             utils._check_byteslike("password", password)
 
@@ -2537,7 +2549,12 @@ def load_key_and_certificates_from_pkcs12(self, data, password):
 
         if x509_ptr[0] != self._ffi.NULL:
             x509 = self._ffi.gc(x509_ptr[0], self._lib.X509_free)
-            cert = self._ossl2cert(x509)
+            cert_obj = self._ossl2cert(x509)
+            name = None
+            maybe_name = self._lib.X509_alias_get0(x509, self._ffi.NULL)
+            if maybe_name != self._ffi.NULL:
+                name = self._ffi.string(maybe_name)
+            cert = PKCS12Certificate(cert_obj, name)
 
         if sk_x509_ptr[0] != self._ffi.NULL:
             sk_x509 = self._ffi.gc(sk_x509_ptr[0], self._lib.sk_X509_free)
@@ -2556,9 +2573,15 @@ def load_key_and_certificates_from_pkcs12(self, data, password):
                 self.openssl_assert(x509 != self._ffi.NULL)
                 x509 = self._ffi.gc(x509, self._lib.X509_free)
                 addl_cert = self._ossl2cert(x509)
-                additional_certificates.append(addl_cert)
+                addl_name = None
+                maybe_name = self._lib.X509_alias_get0(x509, self._ffi.NULL)
+                if maybe_name != self._ffi.NULL:
+                    addl_name = self._ffi.string(maybe_name)
+                additional_certificates.append(
+                    PKCS12Certificate(addl_cert, addl_name)
+                )
 
-        return (key, cert, additional_certificates)
+        return PKCS12KeyAndCertificates(key, cert, additional_certificates)
 
     def serialize_key_and_certificates_to_pkcs12(
         self, name, key, cert, cas, encryption_algorithm
diff --git a/src/cryptography/hazmat/primitives/serialization/pkcs12.py b/src/cryptography/hazmat/primitives/serialization/pkcs12.py
--- a/src/cryptography/hazmat/primitives/serialization/pkcs12.py
+++ b/src/cryptography/hazmat/primitives/serialization/pkcs12.py
@@ -8,7 +8,16 @@
 from cryptography.hazmat.backends import _get_backend
 from cryptography.hazmat.backends.interfaces import Backend
 from cryptography.hazmat.primitives import serialization
-from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
+from cryptography.hazmat.primitives.asymmetric import (
+    dsa,
+    ec,
+    ed25519,
+    ed448,
+    rsa,
+)
+from cryptography.hazmat.primitives.asymmetric.types import (
+    PRIVATE_KEY_TYPES,
+)
 
 
 _ALLOWED_PKCS12_TYPES = typing.Union[
@@ -18,6 +27,118 @@
 ]
 
 
+class PKCS12Certificate:
+    def __init__(
+        self,
+        cert: x509.Certificate,
+        friendly_name: typing.Optional[bytes],
+    ):
+        if not isinstance(cert, x509.Certificate):
+            raise TypeError("Expecting x509.Certificate object")
+        if friendly_name is not None and not isinstance(friendly_name, bytes):
+            raise TypeError("friendly_name must be bytes or None")
+        self._cert = cert
+        self._friendly_name = friendly_name
+
+    @property
+    def friendly_name(self) -> typing.Optional[bytes]:
+        return self._friendly_name
+
+    @property
+    def certificate(self) -> x509.Certificate:
+        return self._cert
+
+    def __eq__(self, other: typing.Any) -> bool:
+        if not isinstance(other, PKCS12Certificate):
+            return NotImplemented
+
+        return (
+            self.certificate == other.certificate
+            and self.friendly_name == other.friendly_name
+        )
+
+    def __ne__(self, other: typing.Any) -> bool:
+        return not self == other
+
+    def __hash__(self) -> int:
+        return hash((self.certificate, self.friendly_name))
+
+    def __repr__(self) -> str:
+        return "<PKCS12Certificate({}, friendly_name={!r})>".format(
+            self.certificate, self.friendly_name
+        )
+
+
+class PKCS12KeyAndCertificates:
+    def __init__(
+        self,
+        key: typing.Optional[PRIVATE_KEY_TYPES],
+        cert: typing.Optional[PKCS12Certificate],
+        additional_certs: typing.List[PKCS12Certificate],
+    ):
+        if key is not None and not isinstance(
+            key,
+            (
+                rsa.RSAPrivateKey,
+                dsa.DSAPrivateKey,
+                ec.EllipticCurvePrivateKey,
+                ed25519.Ed25519PrivateKey,
+                ed448.Ed448PrivateKey,
+            ),
+        ):
+            raise TypeError(
+                "Key must be RSA, DSA, EllipticCurve, ED25519, or ED448"
+                " private key, or None."
+            )
+        if cert is not None and not isinstance(cert, PKCS12Certificate):
+            raise TypeError("cert must be a PKCS12Certificate object or None")
+        if not all(
+            isinstance(add_cert, PKCS12Certificate)
+            for add_cert in additional_certs
+        ):
+            raise TypeError(
+                "all values in additional_certs must be PKCS12Certificate"
+                " objects"
+            )
+        self._key = key
+        self._cert = cert
+        self._additional_certs = additional_certs
+
+    @property
+    def key(self) -> typing.Optional[PRIVATE_KEY_TYPES]:
+        return self._key
+
+    @property
+    def cert(self) -> typing.Optional[PKCS12Certificate]:
+        return self._cert
+
+    @property
+    def additional_certs(self) -> typing.List[PKCS12Certificate]:
+        return self._additional_certs
+
+    def __eq__(self, other: typing.Any) -> bool:
+        if not isinstance(other, PKCS12KeyAndCertificates):
+            return NotImplemented
+
+        return (
+            self.key == other.key
+            and self.cert == other.cert
+            and self.additional_certs == other.additional_certs
+        )
+
+    def __ne__(self, other: typing.Any) -> bool:
+        return not self == other
+
+    def __hash__(self) -> int:
+        return hash((self.key, self.cert, tuple(self.additional_certs)))
+
+    def __repr__(self) -> str:
+        fmt = (
+            "<PKCS12KeyAndCertificates(key={}, cert={}, additional_certs={})>"
+        )
+        return fmt.format(self.key, self.cert, self.additional_certs)
+
+
 def load_key_and_certificates(
     data: bytes,
     password: typing.Optional[bytes],
@@ -31,6 +152,15 @@ def load_key_and_certificates(
     return backend.load_key_and_certificates_from_pkcs12(data, password)
 
 
+def load_pkcs12(
+    data: bytes,
+    password: typing.Optional[bytes],
+    backend: typing.Optional[Backend] = None,
+) -> PKCS12KeyAndCertificates:
+    backend = _get_backend(backend)
+    return backend.load_pkcs12(data, password)
+
+
 def serialize_key_and_certificates(
     name: typing.Optional[bytes],
     key: typing.Optional[_ALLOWED_PKCS12_TYPES],
@@ -46,9 +176,11 @@ def serialize_key_and_certificates(
             ec.EllipticCurvePrivateKey,
         ),
     ):
-        raise TypeError("Key must be RSA, DSA, or EllipticCurve private key.")
+        raise TypeError(
+            "Key must be RSA, DSA, or EllipticCurve private key or None."
+        )
     if cert is not None and not isinstance(cert, x509.Certificate):
-        raise TypeError("cert must be a certificate")
+        raise TypeError("cert must be a certificate or None")
 
     if cas is not None:
         cas = list(cas)


Test patch:
diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py
--- a/tests/hazmat/primitives/test_pkcs12.py
+++ b/tests/hazmat/primitives/test_pkcs12.py
@@ -14,7 +14,10 @@
 from cryptography.hazmat.primitives.asymmetric import ec
 from cryptography.hazmat.primitives.serialization import load_pem_private_key
 from cryptography.hazmat.primitives.serialization.pkcs12 import (
+    PKCS12Certificate,
+    PKCS12KeyAndCertificates,
     load_key_and_certificates,
+    load_pkcs12,
     serialize_key_and_certificates,
 )
 
@@ -148,6 +151,108 @@ def test_buffer_protocol(self, backend):
         assert parsed_cert is not None
         assert parsed_more_certs == []
 
+    @pytest.mark.parametrize(
+        ("name", "name2", "name3", "filename", "password"),
+        [
+            (None, None, None, "no-name-no-pwd.p12", None),
+            (b"name", b"name2", b"name3", "name-all-no-pwd.p12", None),
+            (b"name", None, None, "name-1-no-pwd.p12", None),
+            (None, b"name2", b"name3", "name-2-3-no-pwd.p12", None),
+            (None, b"name2", None, "name-2-no-pwd.p12", None),
+            (None, None, b"name3", "name-3-no-pwd.p12", None),
+            (
+                "☺".encode("utf-8"),
+                "ä".encode("utf-8"),
+                "ç".encode("utf-8"),
+                "name-unicode-no-pwd.p12",
+                None,
+            ),
+            (None, None, None, "no-name-pwd.p12", b"password"),
+            (b"name", b"name2", b"name3", "name-all-pwd.p12", b"password"),
+            (b"name", None, None, "name-1-pwd.p12", b"password"),
+            (None, b"name2", b"name3", "name-2-3-pwd.p12", b"password"),
+            (None, b"name2", None, "name-2-pwd.p12", b"password"),
+            (None, None, b"name3", "name-3-pwd.p12", b"password"),
+            (
+                "☺".encode("utf-8"),
+                "ä".encode("utf-8"),
+                "ç".encode("utf-8"),
+                "name-unicode-pwd.p12",
+                b"password",
+            ),
+        ],
+    )
+    def test_load_object(
+        self, filename, name, name2, name3, password, backend
+    ):
+        cert, key = _load_ca(backend)
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "cryptography.io.pem")
+        )
+        cert3 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem"))
+
+        pkcs12 = load_vectors_from_file(
+            os.path.join("pkcs12", filename),
+            lambda derfile: load_pkcs12(derfile.read(), password, backend),
+            mode="rb",
+        )
+        assert pkcs12.cert is not None
+        assert pkcs12.cert.certificate == cert
+        assert pkcs12.cert.friendly_name == name
+        assert pkcs12.key is not None
+        assert pkcs12.key.private_numbers() == key.private_numbers()
+        assert len(pkcs12.additional_certs) == 2
+        assert pkcs12.additional_certs[0].certificate == cert2
+        assert pkcs12.additional_certs[0].friendly_name == name2
+        assert pkcs12.additional_certs[1].certificate == cert3
+        assert pkcs12.additional_certs[1].friendly_name == name3
+
+    @pytest.mark.parametrize(
+        ("name2", "name3", "filename", "password"),
+        [
+            (None, None, "no-cert-no-name-no-pwd.p12", None),
+            (b"name2", b"name3", "no-cert-name-all-no-pwd.p12", None),
+            (b"name2", None, "no-cert-name-2-no-pwd.p12", None),
+            (None, b"name3", "no-cert-name-3-no-pwd.p12", None),
+            (
+                "☹".encode("utf-8"),
+                "ï".encode("utf-8"),
+                "no-cert-name-unicode-no-pwd.p12",
+                None,
+            ),
+            (None, None, "no-cert-no-name-pwd.p12", b"password"),
+            (b"name2", b"name3", "no-cert-name-all-pwd.p12", b"password"),
+            (b"name2", None, "no-cert-name-2-pwd.p12", b"password"),
+            (None, b"name3", "no-cert-name-3-pwd.p12", b"password"),
+            (
+                "☹".encode("utf-8"),
+                "ï".encode("utf-8"),
+                "no-cert-name-unicode-pwd.p12",
+                b"password",
+            ),
+        ],
+    )
+    def test_load_object_no_cert_key(
+        self, filename, name2, name3, password, backend
+    ):
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "cryptography.io.pem")
+        )
+        cert3 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem"))
+
+        pkcs12 = load_vectors_from_file(
+            os.path.join("pkcs12", filename),
+            lambda derfile: load_pkcs12(derfile.read(), password, backend),
+            mode="rb",
+        )
+        assert pkcs12.cert is None
+        assert pkcs12.key is None
+        assert len(pkcs12.additional_certs) == 2
+        assert pkcs12.additional_certs[0].certificate == cert2
+        assert pkcs12.additional_certs[0].friendly_name == name2
+        assert pkcs12.additional_certs[1].certificate == cert3
+        assert pkcs12.additional_certs[1].friendly_name == name3
+
 
 def _load_cert(backend, path):
     return load_vectors_from_file(
@@ -224,12 +329,12 @@ def test_generate_wrong_types(self, backend):
             )
         assert (
             str(exc.value)
-            == "Key must be RSA, DSA, or EllipticCurve private key."
+            == "Key must be RSA, DSA, or EllipticCurve private key or None."
         )
 
         with pytest.raises(TypeError) as exc:
             serialize_key_and_certificates(b"name", key, key, None, encryption)
-        assert str(exc.value) == "cert must be a certificate"
+        assert str(exc.value) == "cert must be a certificate or None"
 
         with pytest.raises(TypeError) as exc:
             serialize_key_and_certificates(b"name", key, cert, None, key)
@@ -335,3 +440,217 @@ def make_cert(name):
     c_idx = p12.index(c_name.encode("utf-8"))
 
     assert a_idx < b_idx < c_idx
+
+
+class TestPKCS12Objects(object):
+    def test_certificate_constructor(self, backend):
+        with pytest.raises(TypeError):
+            PKCS12Certificate(None, None)  # type:ignore[arg-type]
+        with pytest.raises(TypeError):
+            PKCS12Certificate("hello", None)  # type:ignore[arg-type]
+        cert = _load_cert(backend, os.path.join("x509", "cryptography.io.pem"))
+        with pytest.raises(TypeError):
+            PKCS12Certificate(cert, "hello")  # type:ignore[arg-type]
+        with pytest.raises(TypeError):
+            PKCS12Certificate(cert, 42)  # type:ignore[arg-type]
+
+    def test_certificate_equality(self, backend):
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem")
+        )
+        cert3 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem"))
+
+        c2n = PKCS12Certificate(cert2, None)
+        c2a = PKCS12Certificate(cert2, b"a")
+        c2b = PKCS12Certificate(cert2, b"b")
+        c3n = PKCS12Certificate(cert3, None)
+        c3a = PKCS12Certificate(cert3, b"a")
+
+        assert c2n == c2n
+        assert c2a == c2a
+        assert c2n != c2a
+        assert c2n != c3n
+        assert c2a != c2b
+        assert c2a != c3a
+
+        assert c2n != "test"
+
+    def test_certificate_hash(self, backend):
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem")
+        )
+        cert3 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem"))
+
+        c2n = PKCS12Certificate(cert2, None)
+        c2a = PKCS12Certificate(cert2, b"a")
+        c2b = PKCS12Certificate(cert2, b"b")
+        c3n = PKCS12Certificate(cert3, None)
+        c3a = PKCS12Certificate(cert3, b"a")
+
+        assert hash(c2n) == hash(c2n)
+        assert hash(c2a) == hash(c2a)
+        assert hash(c2n) != hash(c2a)
+        assert hash(c2n) != hash(c3n)
+        assert hash(c2a) != hash(c2b)
+        assert hash(c2a) != hash(c3a)
+
+    def test_certificate_repr(self, backend):
+        cert = _load_cert(backend, os.path.join("x509", "cryptography.io.pem"))
+        assert repr(
+            PKCS12Certificate(cert, None)
+        ) == "<PKCS12Certificate({0}, friendly_name=None)>".format(repr(cert))
+        assert repr(
+            PKCS12Certificate(cert, b"a")
+        ) == "<PKCS12Certificate({0}, friendly_name=b'a')>".format(repr(cert))
+
+    def test_key_and_certificates_constructor(self, backend):
+        with pytest.raises(TypeError):
+            PKCS12KeyAndCertificates(
+                "hello", None, []  # type:ignore[arg-type]
+            )
+        with pytest.raises(TypeError):
+            PKCS12KeyAndCertificates(
+                None, "hello", []  # type:ignore[arg-type]
+            )
+        with pytest.raises(TypeError):
+            PKCS12KeyAndCertificates(
+                None, None, ["hello"]  # type:ignore[list-item]
+            )
+
+    def test_key_and_certificates_equality(self, backend):
+        cert, key = _load_ca(backend)
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem")
+        )
+        cert3 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem"))
+
+        p12a = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12b = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert, b"name"),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12c = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert2, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12d = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert, None),
+            [PKCS12Certificate(cert3, None), PKCS12Certificate(cert2, None)],
+        )
+        p12e = PKCS12KeyAndCertificates(
+            None,
+            PKCS12Certificate(cert, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12f = PKCS12KeyAndCertificates(
+            None,
+            PKCS12Certificate(cert2, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12g = PKCS12KeyAndCertificates(
+            key,
+            None,
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12h = PKCS12KeyAndCertificates(None, None, [])
+
+        assert p12a == p12a
+        assert p12h == p12h
+
+        assert p12a != p12b
+        assert p12a != p12c
+        assert p12a != p12d
+        assert p12a != p12e
+        assert p12a != p12g
+        assert p12a != p12h
+        assert p12e != p12f
+        assert p12e != p12g
+        assert p12e != p12h
+
+        assert p12e != "test"
+
+    def test_key_and_certificates_hash(self, backend):
+        cert, key = _load_ca(backend)
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "custom", "dsa_selfsigned_ca.pem")
+        )
+        cert3 = _load_cert(backend, os.path.join("x509", "letsencryptx3.pem"))
+
+        p12a = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12b = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert, b"name"),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12c = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert2, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12d = PKCS12KeyAndCertificates(
+            key,
+            PKCS12Certificate(cert, None),
+            [PKCS12Certificate(cert3, None), PKCS12Certificate(cert2, None)],
+        )
+        p12e = PKCS12KeyAndCertificates(
+            None,
+            PKCS12Certificate(cert, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12f = PKCS12KeyAndCertificates(
+            None,
+            PKCS12Certificate(cert2, None),
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12g = PKCS12KeyAndCertificates(
+            key,
+            None,
+            [PKCS12Certificate(cert2, None), PKCS12Certificate(cert3, None)],
+        )
+        p12h = PKCS12KeyAndCertificates(None, None, [])
+
+        assert hash(p12a) == hash(p12a)
+        assert hash(p12h) == hash(p12h)
+
+        assert hash(p12a) != hash(p12b)
+        assert hash(p12a) != hash(p12c)
+        assert hash(p12a) != hash(p12d)
+        assert hash(p12a) != hash(p12e)
+        assert hash(p12a) != hash(p12g)
+        assert hash(p12a) != hash(p12h)
+        assert hash(p12e) != hash(p12f)
+        assert hash(p12e) != hash(p12g)
+        assert hash(p12e) != hash(p12h)
+
+    def test_key_and_certificates_repr(self, backend):
+        cert, key = _load_ca(backend)
+        cert2 = _load_cert(
+            backend, os.path.join("x509", "cryptography.io.pem")
+        )
+        assert (
+            repr(
+                PKCS12KeyAndCertificates(
+                    key,
+                    PKCS12Certificate(cert, None),
+                    [PKCS12Certificate(cert2, b"name2")],
+                )
+            )
+            == "<PKCS12KeyAndCertificates(key={0}, cert=<PKCS12Certificate("
+            "{1}, friendly_name=None)>, additional_certs=[<PKCS12Certificate"
+            "({2}, friendly_name=b'name2')>])>".format(
+                key,
+                cert,
+                cert2,
+            )
+        )