import enum import hashlib from dataclasses import dataclass from datetime import datetime, timezone from typing import Any, Dict, Iterator, Optional, Union from asn1crypto import core, x509 from pyhanko_certvalidator.revinfo.archival import CRLContainer, OCSPContainer __all__ = [ 'ValidationObjectType', 'ValidationObject', 'POEType', 'KnownPOE', 'POEManager', 'digest_for_poe', ] @enum.unique class ValidationObjectType(enum.Enum): """ Types of validation objects recognised by ETSI TS 119 102-2. """ CERTIFICATE = 'certificate' CRL = 'CRL' OCSP_RESPONSE = 'OCSPResponse' TIMESTAMP = 'timestamp' EVIDENCE_RECORD = 'evidencerecord' PUBLIC_KEY = 'publicKey' SIGNED_DATA = 'signedData' OTHER = 'other' def urn(self): return f'urn:etsi:019102:validationObject:{self.value}' KnownObjectType = Union[bytes, CRLContainer, OCSPContainer, x509.Certificate] def guess_validation_object_type( thing: object, ) -> Optional[ValidationObjectType]: if isinstance(thing, CRLContainer): return ValidationObjectType.CRL elif isinstance(thing, OCSPContainer): return ValidationObjectType.OCSP_RESPONSE elif isinstance(thing, x509.Certificate): return ValidationObjectType.CERTIFICATE return None @dataclass(frozen=True) class ValidationObject: """ A validation object used in the course of a validation operation for which proofs of existence can potentially be gathered. """ object_type: ValidationObjectType """ The type of validation object. """ value: Any """ The actual object. Currently, the following types are supported explicitly. Others must currently be supplied as :class:`bytes`. - :class:`.CRLContainer`: :attr:`.ValidationObjectType.CRL` - :class:`.OCSPContainer`: :attr:`.ValidationObjectType.OCSP_RESPONSE` - :class:`x509.Certificate`: :attr:`.ValidationObjectType.CERTIFICATE` """ @enum.unique class POEType(enum.Enum): PROVIDED = 'provided' VALIDATION = 'validation' POLICY = 'policy' @property def urn(self) -> str: return f'urn:etsi:019102:poetype:{self.value}' @dataclass(frozen=True) class KnownPOE: poe_type: POEType digest: bytes poe_time: datetime validation_object: Optional[ValidationObject] = None def digest_for_poe(data: bytes) -> bytes: return hashlib.sha256(data).digest() class POEManager: """ Class to manage proof-of-existence (POE) claims. :param current_dt_override: Override the current time. """ def __init__(self, current_dt_override: Optional[datetime] = None): self._poes: Dict[bytes, KnownPOE] = {} self._current_dt_override = current_dt_override def register( self, data: KnownObjectType, poe_type: POEType, dt: Optional[datetime] = None, ) -> KnownPOE: """ Register a new POE claim if no POE for an earlier time is available. :param data: Data to register a POE claim for. :param poe_type: The type of POE. :param dt: The POE time to register. If ``None``, assume the current time. :return: The oldest POE datetime available. """ if isinstance(data, bytes): b_data = data elif isinstance(data, core.Asn1Value): b_data = data.dump() elif isinstance(data, CRLContainer): b_data = data.crl_data.dump() elif isinstance(data, OCSPContainer): b_data = data.ocsp_response_data.dump() else: raise NotImplementedError digest = digest_for_poe(b_data) dt = dt or self._current_dt_override or datetime.now(timezone.utc) vo_type = guess_validation_object_type(data) vo = None if vo_type: vo = ValidationObject(object_type=vo_type, value=data) return self.register_known_poe( KnownPOE( poe_type=poe_type, digest=digest, poe_time=dt, validation_object=vo, ) ) def register_by_digest( self, digest: bytes, poe_type: POEType, dt: Optional[datetime] = None, ) -> KnownPOE: """ Register a new POE claim if no POE for an earlier time is available. :param digest: SHA-256 digest of the data to register a POE claim for. :param dt: The POE time to register. If ``None``, assume the current time. :param poe_type: The type of POE. :return: The oldest POE datetime available. """ dt = dt or self._current_dt_override or datetime.now(timezone.utc) return self.register_known_poe( KnownPOE( poe_type=poe_type, digest=digest, poe_time=dt, validation_object=None, ) ) def register_known_poe(self, known_poe: KnownPOE) -> KnownPOE: """ Register a new POE claim if no POE for an earlier time is available. :param known_poe: The POE object to register. :return: The oldest POE for the given digest. """ dt = known_poe.poe_time digest = known_poe.digest try: cur_poe = self._poes[digest] if cur_poe.poe_time <= dt: return cur_poe except KeyError: pass self._poes[digest] = known_poe return known_poe def __iter__(self) -> Iterator[KnownPOE]: """ Iterate over the current earliest known POE for all items currently being managed. Returns an iterator with :class:`KnownPOE` objects. """ return iter(self._poes.values()) def __getitem__(self, item: KnownObjectType) -> datetime: """ Return the earliest available POE for an item. .. note:: This is a wrapper around :meth:`register` with `dt=None`, and hence will register the current time as the POE time for the given item. This side effect is intentional. :param item: Item to get the current POE time for. :return: A datetime object representing the earliest available POE for the item. """ return self.register( item, poe_type=POEType.VALIDATION, dt=None ).poe_time def __ior__(self, other): """ Combine data in another POE manager with the POEs managed by this instance. """ if not isinstance(other, POEManager): raise TypeError for poe in iter(other): self.register_known_poe(poe) def __copy__(self): new_instance = POEManager(current_dt_override=self._current_dt_override) new_instance._poes = dict(self._poes) return new_instance