Source code for cert_chain_resolver.models

from cert_chain_resolver.exceptions import MissingCertProperty
from cert_chain_resolver.utils import load_ascii_to_x509, load_bytes_to_x509
from cryptography import x509
from cryptography.x509.oid import ExtensionOID, AuthorityInformationAccessOID, NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import Encoding
import binascii


try:
    from typing import List, Union, Optional, Type, Iterator, TYPE_CHECKING

    if TYPE_CHECKING:
        import datetime
except ImportError:
    pass

try:
    unicode  # type: ignore
except NameError:
    unicode = str


[docs] class Cert: """The :class:`Cert <Cert>` object, which is a convenience wrapper for interacting with the underlying :py:class:`cryptography.x509.Certificate` object Args: x509_obj: An instance of :py:class:`cryptography.x509.Certificate` Raises: TypeError: given type is not an instance of :py:class:`cryptography.x509.Certificate` """ def __init__(self, x509_obj): # type: (x509.Certificate) -> None if not isinstance(x509_obj, x509.Certificate): raise TypeError("Argument must be a x509 Certificate object") self._x509 = x509_obj def __repr__(self): # type: () -> str return '<Cert common_name="{0}" subject="{1}" issuer="{2}">'.format( self.common_name, self.subject, self.issuer ) def __eq__(self, other): # type: (object) -> bool if not isinstance(other, Cert): return NotImplemented return self.fingerprint == other.fingerprint @property def issuer(self): # type: () -> str """RFC4515 formatted string of the issuer field from the underlying :py:class:`cryptography.x509.Certificate` object""" return self._x509.issuer.rfc4514_string() @property def subject(self): # type: () -> str """RFC4515 formatted string of the subject field from the underlying :py:class:`cryptography.x509.Certificate` object""" return self._x509.subject.rfc4514_string() @property def common_name(self): # type: () -> str """Extracted common name from the underlying :py:class:`cryptography.x509.Certificate` object""" for attr in self._x509.subject.get_attributes_for_oid(NameOID.COMMON_NAME): if isinstance(attr.value, unicode): return attr.value elif isinstance(attr, bytes): return bytes(attr.value).decode("utf-8") else: raise ValueError("Unexpected type for attr") raise MissingCertProperty("No COMMON_NAME attribute found") @property def subject_alternative_names(self): # type: () -> List[str] """list(str): Extracted x509 Extensions from the :py:class:`cryptography.x509.Certificate` object""" try: ext = self._x509.extensions.get_extension_for_oid( ExtensionOID.SUBJECT_ALTERNATIVE_NAME ) if isinstance(ext.value, x509.SubjectAlternativeName): # Runtime check needed to ensure proper type hinting return ext.value.get_values_for_type(x509.DNSName) else: raise MissingCertProperty("Could not get SubjectAlternativeNames") except x509.extensions.ExtensionNotFound: pass return [] @property def is_ca(self): # type: () -> bool """Checks whether the Certificate Authority bit has been set""" try: ext = self._x509.extensions.get_extension_for_oid( ExtensionOID.BASIC_CONSTRAINTS ) if isinstance(ext.value, x509.BasicConstraints): # Runtime check needed to ensure proper type hinting return ext.value.ca raise MissingCertProperty("Could not extract CA bit from BasicConstraints") except x509.extensions.ExtensionNotFound: pass return False @property def is_root(self): # type: () -> bool """Checks whether the certificate is a root""" return self.subject == self.issuer @property def serial(self): # type: () -> int """gets the serial from the underlying :py:class:`cryptography.x509.Certificate` object""" return self._x509.serial_number @property def signature_hash_algorithm(self): # type: () -> str """gets the signature hashing algorithm name from the underlying :py:class:`cryptography.x509.Certificate` object""" if not self._x509.signature_hash_algorithm: raise MissingCertProperty( "X509 object does not have a signature hash algorithm" ) return self._x509.signature_hash_algorithm.name @property def not_valid_before(self): # type: () -> datetime.datetime """Date from the underlying :py:class:`cryptography.x509.Certificate` object""" return self._x509.not_valid_before @property def not_valid_after(self): # type: () -> datetime.datetime """Date from the underlying :py:class:`cryptography.x509.Certificate` object""" return self._x509.not_valid_after @property def fingerprint(self): # type: () -> str """ascii encoded sha256 fingerprint by calling :py:func:`get_fingerprint`""" return self.get_fingerprint(hashes.SHA256) @property def ca_issuer_access_location(self): # type: () -> Union[str, None] """URL that contains the CA issuer certificate""" try: aias = self._x509.extensions.get_extension_for_oid( ExtensionOID.AUTHORITY_INFORMATION_ACCESS ) if not isinstance(aias.value, x509.AuthorityInformationAccess): # Runtime check needed to ensure proper type hinting raise MissingCertProperty( "Extracted AuthorityInformationAccess but couldnt determine instance" ) for aia in aias.value: if AuthorityInformationAccessOID.CA_ISSUERS == aia.access_method: access_location = aia.access_location.value # type: str return access_location except x509.extensions.ExtensionNotFound: pass return None
[docs] def get_fingerprint(self, _hash=hashes.SHA256): # type: (Type[hashes.HashAlgorithm]) -> str """Get fingerprint of the certificate Args: _hash (:py:class:`cryptography.hazmat.primitives.hashes`, optional): Hasher to use. Defaults to hashes.SHA256. Returns: hex representation of the fingerprint """ binary = self._x509.fingerprint(_hash()) txt = binascii.hexlify(binary).decode("ascii") return txt
[docs] def export(self, encoding=Encoding.PEM): # type: (Encoding) -> str """Export the :py:class:`cryptography.x509.Certificate` object" as text Args: encoding (:py:class:`cryptography.hazmat.primitives.serialization.Encoding`, optional): The output format. Defaults to Encoding.PEM. Returns: ascii formatted """ encoded = self._x509.public_bytes(encoding) return encoded.decode(encoding="ascii")
[docs] @classmethod def load(cls, bytes_input): # type: (bytes) -> Cert """ Create a :class:`Cert <Cert>` object Args: bytes_input :py:class:`bytes` PEM or DER Raises: :class:`ImproperlyFormattedCert <ImproperlyFormattedCert>` """ x509 = load_bytes_to_x509(bytes_input) return cls(x509)
[docs] class CertificateChain: """Creates an iterable that contains a list of :class:`Cert <Cert>` objects. Args: chain: Create a new CertificateChain based on this chain. Defaults to None. """ def __init__(self, chain=None): # type: (Union[Optional[CertificateChain], List[Cert]]) -> None self._chain = [] if not chain else list(chain) # type: List[Cert] def __iter__(self): # type: () -> Iterator[Cert] for cert in self._chain: yield cert def __iadd__(self, x509_obj): # type: (Cert) -> CertificateChain self._chain.append(x509_obj) return self def __len__(self): # type: () -> int return self._chain.__len__() @property def leaf(self): # type: () -> Cert """First :class:`Cert <Cert>`: in the chain. Also known as the 'leaf'""" return self._chain[0] @property def intermediates(self): # type: () -> CertificateChain """A new :class:`CertificateChain <CertificateChain>` object with only intermediate certificates""" new_chain = [x for x in self._chain if (x.is_ca and not x.is_root)] return self.__class__(chain=new_chain) @property def root(self): # type: () -> Optional[Cert] """Last :class:`Cert <Cert>`: in the chain that can be identified as root or None if no root is present""" if self._chain[-1].is_root: return self._chain[-1] return None
[docs] @classmethod def load_from_pem(cls, input_bytes): # type: (bytes) -> CertificateChain """Create a :py:class:`CertificateChain <CertificateChain>` object from a PEM formatted file""" begin = b"-----BEGIN CERTIFICATE-----\n" chain = cls() for strip_pem in filter(len, input_bytes.split(begin)): pem = begin + strip_pem chain += Cert(load_ascii_to_x509(pem)) if chain.leaf.is_ca and not chain._chain[-1].is_ca: # if CA bit is not set on the last certificate, reverse the chain chain._chain.reverse() return chain