Skip to content

Commit

Permalink
PKCS12: return 'friendly name' with PKCS12KeyAndCertificates API (#6348)
Browse files Browse the repository at this point in the history
* Propose a new load_key_and_certificates_with_name API to return the PKCS12 'friendly name' as well.

* Extend load_key_and_certificates_with_name to return friendly names for all certificates; add serialize_key_and_certificates_with_names; add X509_alias_set1 to cffi; add basic tests for all these.

* Add changelog entry and documentation.

* Revert "Extend load_key_and_certificates_with_name to return friendly names for all certificates; add serialize_key_and_certificates_with_names; add X509_alias_set1 to cffi; add basic tests for all these."

This reverts commit 125935e.

* Create new interface.

* Rename load_key_and_certificates_object -> load_pkcs12.

* Add constructor validation, improve repr tests.

* Mention '... or None'.

* Allow all private key types.

* Fix/improve tests.

* Ignore type errors when intentionally passing wrong types.

* Fix type; linting.

* Use correct ignore.
  • Loading branch information
felixfontein committed Oct 6, 2021
1 parent 667e7a5 commit 17aeaa6
Show file tree
Hide file tree
Showing 6 changed files with 554 additions and 8 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Changelog

.. note:: This version is not yet released and is under active development.

* Added support for parsing PKCS12 files with friendly names for all
certificates with
:func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_pkcs12`,
which will return an object of type
:class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12KeyAndCertificates`.

.. _v35-0-0:

35.0.0 - 2021-09-29
Expand Down
60 changes: 60 additions & 0 deletions docs/hazmat/primitives/asymmetric/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,27 @@ file suffix.
``additional_certificates`` is a list of all other
:class:`~cryptography.x509.Certificate` instances in the PKCS12 object.

.. function:: load_pkcs12(data, password, backend=None)

.. versionadded:: 36.0

Deserialize a PKCS12 blob, and return a
:class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12KeyAndCertificates`
instance.

:param data: The binary data.
:type data: :term:`bytes-like`

:param password: The password to use to decrypt the data. ``None``
if the PKCS12 is not encrypted.
:type password: :term:`bytes-like`

:param backend: An optional backend instance.

:returns: A
:class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12KeyAndCertificates`
instance.

.. function:: serialize_key_and_certificates(name, key, cert, cas, encryption_algorithm)

.. versionadded:: 3.0
Expand Down Expand Up @@ -543,6 +564,45 @@ file suffix.

:return bytes: Serialized PKCS12.

.. class:: PKCS12Certificate

.. versionadded:: 36.0

Represents additional data provided for a certificate in a PKCS12 file.

.. attribute:: certificate

A :class:`~cryptography.x509.Certificate` instance.

.. attribute:: friendly_name

:type: bytes or None

An optional byte string containing the friendly name of the certificate.

.. class:: PKCS12KeyAndCertificates

.. versionadded:: 36.0

A simplified representation of a PKCS12 file.

.. attribute:: key

An optional private key belonging to
:attr:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12KeyAndCertificates.cert`.

.. attribute:: cert

An optional
:class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12Certificate`
instance belonging to the private key
:attr:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12KeyAndCertificates.key`.

.. attribute:: additional_certs

A list of :class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12Certificate`
instances.

PKCS7
~~~~~

Expand Down
6 changes: 6 additions & 0 deletions src/cryptography/hazmat/backends/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 26 additions & 3 deletions src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
138 changes: 135 additions & 3 deletions src/cryptography/hazmat/primitives/serialization/pkcs12.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand All @@ -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],
Expand All @@ -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],
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 17aeaa6

Please sign in to comment.