# coding: utf-8 import itertools from dataclasses import dataclass from typing import FrozenSet, Iterable, Iterator, Optional, Union from asn1crypto import cms, x509 from .asn1_types import AAControls from .authority import ( Authority, AuthorityWithCert, CertTrustAnchor, TrustAnchor, ) from .util import get_ac_extension_value, get_issuer_dn @dataclass(frozen=True) class QualifiedPolicy: issuer_domain_policy_id: str """ Policy OID in the issuer domain (i.e. as listed on the certificate). """ user_domain_policy_id: str """ Policy OID of the equivalent policy in the user domain. """ qualifiers: frozenset """ Set of x509.PolicyQualifierInfo objects. """ Leaf = Union[x509.Certificate, cms.AttributeCertificateV2] class ValidationPath: """ Represents a path going towards an end-entity certificate or attribute certificate. """ _qualified_policies: Optional[FrozenSet[QualifiedPolicy]] = None _path_aa_controls = None def __init__( self, trust_anchor: TrustAnchor, interm: Iterable[x509.Certificate], leaf: Optional[Leaf], ): if interm and not leaf: raise ValueError("Leafless paths cannot have intermediate certs") self._interm = list(interm) self._root = trust_anchor self._leaf = leaf @property def trust_anchor(self) -> TrustAnchor: return self._root @property def first(self): """ Returns the current beginning of the path - for a path to be complete, this certificate should be a trust root .. warning:: This is a compatibility property, and will return the first non-root certificate if the trust root is not provisioned as a certificate. If you want the trust root itself (even when it doesn't have a certificate), use :attr:`trust_anchor`. :return: The first asn1crypto.x509.Certificate object in the path """ root = self._root.authority if isinstance(root, AuthorityWithCert): return root.certificate elif self._interm: return self._interm[0] elif isinstance(self._leaf, x509.Certificate): return self._leaf @property def leaf(self) -> Optional[Leaf]: """ Returns the current leaf certificate (AC or public-key). The trust root's certificate will be returned if there is one and there are no other certificates in the path. If the trust root is certificate-less and there are no certificates, the result will be ``None``. """ if self._leaf is not None: return self._leaf elif not self._interm and isinstance(self._root, CertTrustAnchor): return self._root.certificate # __init__ ensures that leaf None -> there are no intermediate certs return None def describe_leaf(self) -> Optional[str]: leaf = self.leaf if isinstance(leaf, x509.Certificate): return leaf.subject.human_friendly elif isinstance(leaf, cms.AttributeCertificateV2): return '' else: return None def get_ee_cert_safe(self) -> Optional[x509.Certificate]: """ Returns the current leaf certificate if it is an X.509 public-key certificate, and ``None`` otherwise. :return: """ leaf = self.leaf if isinstance(leaf, x509.Certificate): return leaf else: return None @property def last(self) -> x509.Certificate: """ Returns the last certificate in the path if it is an X.509 public-key certificate, and throws an error otherwise. :return: The last asn1crypto.x509.Certificate object in the path """ cert = self.get_ee_cert_safe() if cert: return cert else: raise LookupError def iter_authorities(self) -> Iterable[Authority]: """ Iterate over all authorities in the path, including the trust root. """ yield self._root.authority for cert in self._interm: yield AuthorityWithCert(cert) def find_issuing_authority(self, cert: Leaf): """ Return the issuer of the cert specified, as defined by this path :param cert: A certificate to get the issuer of :raises: LookupError - when the issuer of the certificate could not be found :return: An asn1crypto.x509.Certificate object of the issuer """ issuer_name = get_issuer_dn(cert) if isinstance(cert, x509.Certificate): aki = cert.authority_key_identifier else: aki_ext = get_ac_extension_value(cert, 'authority_key_identifier') aki = aki_ext['key_identifier'].native if aki_ext else None for authority in self.iter_authorities(): if authority.name == issuer_name: keyid = authority.key_id if keyid and aki and keyid != aki: continue return authority raise LookupError( 'Unable to find the issuer of the certificate specified' ) def truncate_to_and_append(self, cert: x509.Certificate, new_leaf: Leaf): """ Remove all certificates in the path after the cert specified and return them in a new path. Internal API. :param cert: An asn1crypto.x509.Certificate object to find :param new_leaf: A new leaf certificate to append. :raises: LookupError - when the certificate could not be found :return: The current ValidationPath object, for chaining """ if isinstance(self._root, CertTrustAnchor): if self._root.certificate.issuer_serial == cert.issuer_serial: return ValidationPath(self._root, interm=[], leaf=new_leaf) certs = self._interm cert_index = None for index, entry in enumerate(certs): if entry.issuer_serial == cert.issuer_serial: cert_index = index break if cert_index is None: raise LookupError('Unable to find the certificate specified') return ValidationPath( self._root, interm=certs[: cert_index + 1], leaf=new_leaf ) # TODO generalise this to ACs as well? def truncate_to_issuer_and_append(self, cert: x509.Certificate): """ Remove all certificates in the path after the issuer of the cert specified, as defined by this path, and append a new one. Internal API. :param cert: A new leaf certificate to append. :raises: LookupError - when the issuer of the certificate could not be found :return: The current ValidationPath object, for chaining """ issuer_index = None # check the trust root separately if self.trust_anchor.authority.is_potential_issuer_of(cert): # in case of a match, truncate everything if cert.self_signed == 'maybe': # if the candidate leaf is self-signed (according to metadata), # then it's actually the authority itself -> no need to append. return ValidationPath(self._root, interm=[], leaf=None) else: return ValidationPath(self._root, interm=[], leaf=cert) # now run through the rest of the path certs = self._interm for index, entry in enumerate(certs): if entry.subject == cert.issuer: if entry.key_identifier and cert.authority_key_identifier: if entry.key_identifier == cert.authority_key_identifier: issuer_index = index break else: issuer_index = index break if issuer_index is None: raise LookupError( 'Unable to find the issuer of the certificate specified' ) return ValidationPath(self._root, certs[: issuer_index + 1], leaf=cert) def copy_and_append(self, cert: Leaf): new_certs = self._interm[:] if self._leaf: new_certs.append(self._leaf) return ValidationPath( trust_anchor=self._root, interm=new_certs, leaf=cert ) def copy_and_drop_leaf(self) -> 'ValidationPath': """ Drop the leaf cert from this path and return a new path with the last intermediate certificate set as the leaf. """ if len(self._interm) == 0: raise IndexError new_interm, new_leaf = self._interm[:-1], self._interm[-1] return ValidationPath( trust_anchor=self._root, interm=new_interm, leaf=new_leaf ) def _set_qualified_policies(self, policies): self._qualified_policies = policies def qualified_policies(self) -> Optional[FrozenSet[QualifiedPolicy]]: return self._qualified_policies def aa_attr_in_scope(self, attr_id: cms.AttCertAttributeType) -> bool: aa_controls_extensions = [ AAControls.read_extension_value(cert) for cert in self ] aa_controls_used = any(x is not None for x in aa_controls_extensions) if not aa_controls_used: return True else: # the path validation code ensures that all non-anchor certs # have an AAControls extension, but we still enforce the root's # AAControls if there is one (since we might as well treat it # as a configuration setting/failsafe at that point) # This is appropriate in PKIX-land (see RFC 5280, § 6.2 as # updated in RFC 6818, § 4) return all( ctrl.accept(attr_id) for ctrl in aa_controls_extensions # None check for defensiveness (already enforced by validation # algorithm), and to (potentially) skip the root if ctrl is not None ) @property def pkix_len(self): return len(self._interm) + (1 if self._leaf else 0) def __len__(self): # backwards compat return 1 + self.pkix_len def __getitem__(self, key): # convoluted because of compatibility issues... if key > 0: leaf_ix = len(self._interm) + 1 if key == leaf_ix and self._leaf is not None: return self._leaf return self._interm[key - 1] elif isinstance(self._root, CertTrustAnchor): # backwards compat return self._root.certificate else: # Throw an error instead of returning None, because we want this # to fail loudly. raise LookupError("Root has no certificate") def iter_certs(self, include_root: bool) -> Iterator[x509.Certificate]: """ Iterate over the certificates in the path. :param include_root: Include the root (if it is supplied as a certificate) :return: An iterator. """ root = self._root.authority from_root = ( (root.certificate,) if include_root and isinstance(root, AuthorityWithCert) else () ) leaf = self._leaf from_leaf = (leaf,) if isinstance(leaf, x509.Certificate) else () return itertools.chain(from_root, self._interm, from_leaf) def __iter__(self): # backwards compat, we iterate over all certs _including_ the root # if it is supplied as a cert return self.iter_certs(include_root=True) def __eq__(self, other): if not isinstance(other, ValidationPath): return False return ( self.trust_anchor == other.trust_anchor and self._interm == other._interm and self._leaf == other._leaf )