Source code for django_ca.extensions.utils

# This file is part of django-ca (https://github.com/mathiasertl/django-ca).
#
# django-ca is free software: you can redistribute it and/or modify it under the terms of the GNU
# General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# django-ca is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with django-ca.  If not,
# see <http://www.gnu.org/licenses/>.

"""``django_ca.extensions.utils`` contains various utility classes used by X.509 extensions."""

import textwrap
import typing
from typing import Any
from typing import FrozenSet
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import Union

from cryptography import x509
from cryptography.x509 import ObjectIdentifier

from ..typehints import ParsableDistributionPoint
from ..typehints import ParsablePolicyIdentifier
from ..typehints import ParsablePolicyInformation
from ..typehints import ParsablePolicyQualifier
from ..typehints import PolicyQualifier
from ..typehints import SerializedDistributionPoint
from ..typehints import SerializedPolicyInformation
from ..typehints import SerializedPolicyQualifier
from ..typehints import SerializedPolicyQualifiers
from ..typehints import SerializedUserNotice
from ..utils import format_general_name
from ..utils import format_name
from ..utils import parse_general_name
from ..utils import x509_relative_name


[docs]class DistributionPoint: """Class representing a Distribution Point. This class is used internally by extensions that have a list of Distribution Points, e.g. the : :py:class:`~django_ca.extensions.CRLDistributionPoints` extension. The class accepts either a :py:class:`cg:cryptography.x509.DistributionPoint` or a ``dict``. Note that in the latter case, you can also pass a list of ``str`` as ``full_name`` or ``crl_issuer``:: >>> DistributionPoint(x509.DistributionPoint( ... full_name=[x509.UniformResourceIdentifier('http://ca.example.com/crl')], ... relative_name=None, crl_issuer=None, reasons=None ... )) <DistributionPoint: full_name=['URI:http://ca.example.com/crl']> >>> DistributionPoint({'full_name': ['http://example.com']}) <DistributionPoint: full_name=['URI:http://example.com']> >>> DistributionPoint({'full_name': ['http://example.com']}) <DistributionPoint: full_name=['URI:http://example.com']> >>> DistributionPoint({ ... 'relative_name': '/CN=example.com', ... 'crl_issuer': ['http://example.com'], ... 'reasons': ['key_compromise', 'ca_compromise'], ... }) # doctest: +NORMALIZE_WHITESPACE <DistributionPoint: relative_name='/CN=example.com', crl_issuer=['URI:http://example.com'], reasons=['ca_compromise', 'key_compromise']> .. seealso:: `RFC 5280, section 4.2.1.13 <https://tools.ietf.org/html/rfc5280#section-4.2.1.13>`_ """ full_name: Optional[typing.List[x509.GeneralName]] = None relative_name: Optional[x509.RelativeDistinguishedName] = None crl_issuer: Optional[typing.List[x509.GeneralName]] = None reasons: Optional[Set[x509.ReasonFlags]] = None def __init__( self, data: Optional[Union[x509.DistributionPoint, ParsableDistributionPoint]] = None ) -> None: if data is None: data = {} if isinstance(data, x509.DistributionPoint): if data.full_name is not None: self.full_name = data.full_name self.relative_name = data.relative_name if data.crl_issuer is not None: self.crl_issuer = data.crl_issuer if data.reasons is not None: self.reasons = set(data.reasons) elif isinstance(data, dict): full_name = data.get("full_name") if full_name is not None: self.full_name = [parse_general_name(name) for name in full_name] crl_issuer = data.get("crl_issuer") if crl_issuer is not None: self.crl_issuer = [parse_general_name(name) for name in crl_issuer] relative_name = data.get("relative_name") if isinstance(relative_name, str): self.relative_name = x509_relative_name(relative_name) else: self.relative_name = relative_name if "reasons" in data: self.reasons = {self._parse_reason(r) for r in data["reasons"]} if self.full_name and self.relative_name: raise ValueError("full_name and relative_name cannot both have a value") else: raise ValueError("data must be x509.DistributionPoint or dict") def __eq__(self, other: Any) -> bool: return ( isinstance(other, DistributionPoint) and self.full_name == other.full_name and self.relative_name == other.relative_name and self.crl_issuer == other.crl_issuer and self.reasons == other.reasons ) def __get_values(self) -> List[str]: values: List[str] = [] if self.full_name: names = [format_general_name(name) for name in self.full_name] values.append(f"full_name={names}") if self.relative_name: values.append(f"relative_name='{format_name(self.relative_name)}'") if self.crl_issuer: names = [format_general_name(name) for name in self.crl_issuer] values.append(f"crl_issuer={names}") if self.reasons: values.append(f"reasons={sorted([r.name for r in self.reasons])}") return values def __hash__(self) -> int: full_name = tuple(self.full_name) if self.full_name else None crl_issuer = tuple(self.crl_issuer) if self.crl_issuer else None reasons = tuple(self.reasons) if self.reasons else None return hash((full_name, self.relative_name, crl_issuer, reasons)) def __repr__(self) -> str: values = ", ".join(self.__get_values()) return f"<DistributionPoint: {values}>" def __str__(self) -> str: return repr(self) def _parse_reason(self, reason: Union[str, x509.ReasonFlags]) -> x509.ReasonFlags: if isinstance(reason, str): return x509.ReasonFlags[reason] return reason
[docs] def as_text(self) -> str: """Show as text.""" text = "" if self.full_name: names = "\n".join([textwrap.indent(f"* {format_general_name(s)}", " ") for s in self.full_name]) text = f"* Full Name:\n{names}" elif self.relative_name is not None: text = f"* Relative Name: {format_name(self.relative_name)}" if self.crl_issuer: names = "\n".join([textwrap.indent(f"* {format_general_name(s)}", " ") for s in self.crl_issuer]) text += f"\n* CRL Issuer:\n{names}" if self.reasons: reasons = ", ".join(sorted([r.name for r in self.reasons])) text += f"\n* Reasons: {reasons}" return text
@property def for_extension_type(self) -> x509.DistributionPoint: """Convert instance to a suitable cryptography class.""" reasons: Optional[FrozenSet[x509.ReasonFlags]] = frozenset(self.reasons) if self.reasons else None return x509.DistributionPoint( full_name=self.full_name, relative_name=self.relative_name, crl_issuer=self.crl_issuer, reasons=reasons, )
[docs] def serialize(self) -> SerializedDistributionPoint: """Serialize this distribution point.""" val: SerializedDistributionPoint = {} if self.full_name: val["full_name"] = [format_general_name(name) for name in self.full_name] if self.relative_name is not None: val["relative_name"] = format_name(self.relative_name) if self.crl_issuer: val["crl_issuer"] = [format_general_name(name) for name in self.crl_issuer] if self.reasons is not None: val["reasons"] = list(sorted([r.name for r in self.reasons])) return val
[docs]class PolicyInformation(typing.MutableSequence[PolicyQualifier]): """Class representing a PolicyInformation object. This class is internally used by the :py:class:`~django_ca.extensions.CertificatePolicies` extension. You can pass a :py:class:`~cg:cryptography.x509.PolicyInformation` instance or a dictionary representing that instance:: >>> PolicyInformation({'policy_identifier': '2.5.29.32.0', 'policy_qualifiers': ['text1']}) <PolicyInformation(oid=2.5.29.32.0, qualifiers=['text1'])> >>> PolicyInformation({ ... 'policy_identifier': '2.5.29.32.0', ... 'policy_qualifiers': [{'explicit_text': 'text2', }], ... }) <PolicyInformation(oid=2.5.29.32.0, qualifiers=[{'explicit_text': 'text2'}])> >>> PolicyInformation({ ... 'policy_identifier': '2.5', ... 'policy_qualifiers': [{ ... 'notice_reference': { ... 'organization': 't3', ... 'notice_numbers': [1, ], ... } ... }], ... }) # doctest: +ELLIPSIS <PolicyInformation(oid=2.5, qualifiers=[{'notice_reference': {...}}])> """ _policy_identifier: Optional[x509.ObjectIdentifier] policy_qualifiers: Optional[List[PolicyQualifier]] def __init__( self, data: Optional[Union[x509.PolicyInformation, ParsablePolicyInformation]] = None, ) -> None: if isinstance(data, x509.PolicyInformation): self.policy_identifier = data.policy_identifier self.policy_qualifiers = data.policy_qualifiers elif isinstance(data, dict): self.policy_identifier = data["policy_identifier"] self.policy_qualifiers = self.parse_policy_qualifiers(data.get("policy_qualifiers")) elif data is None: self.policy_identifier = None self.policy_qualifiers = None else: raise ValueError("PolicyInformation data must be either x509.PolicyInformation or dict") def __contains__(self, value: ParsablePolicyQualifier) -> bool: # type: ignore[override] if self.policy_qualifiers is None: return False try: return self._parse_policy_qualifier(value) in self.policy_qualifiers except ValueError: # not parsable return False def __delitem__(self, key: Union[int, slice]) -> None: if self.policy_qualifiers is None: raise IndexError("list assignment index out of range") del self.policy_qualifiers[key] if not self.policy_qualifiers: self.policy_qualifiers = None def __eq__(self, other: Any) -> bool: return ( isinstance(other, PolicyInformation) and self.policy_identifier == other.policy_identifier and self.policy_qualifiers == other.policy_qualifiers ) @typing.overload # type: ignore[override] # should return non-serialized version def __getitem__(self, key: int) -> SerializedPolicyQualifier: ... @typing.overload def __getitem__(self, key: slice) -> List[SerializedPolicyQualifier]: ... def __getitem__( self, key: Union[int, slice] ) -> Union[List[SerializedPolicyQualifier], SerializedPolicyQualifier]: """Implement item getter (e.g ``pi[0]`` or ``pi[0:1]``).""" if self.policy_qualifiers is None: raise IndexError("list index out of range") if isinstance(key, int): return self._serialize_policy_qualifier(self.policy_qualifiers[key]) return [self._serialize_policy_qualifier(k) for k in self.policy_qualifiers[key]] def __hash__(self) -> int: if self.policy_qualifiers is None: tup = None else: tup = tuple(self.policy_qualifiers) return hash((self.policy_identifier, tup)) def __iter__(self) -> typing.Iterator[PolicyQualifier]: if self.policy_qualifiers is None: return iter([]) return iter(self.policy_qualifiers) def __len__(self) -> int: if self.policy_qualifiers is None: return 0 return len(self.policy_qualifiers) def __repr__(self) -> str: if self.policy_identifier is None: ident = "None" else: ident = self.policy_identifier.dotted_string return f"<PolicyInformation(oid={ident}, qualifiers={self.serialize_policy_qualifiers()})>" def __setitem__( self, key: typing.Union[int, slice], value: typing.Union[ParsablePolicyQualifier, typing.Iterable[ParsablePolicyQualifier]], ) -> None: """Implement item getter (e.g ``pi[0]`` or ``pi[0:1]``).""" if isinstance(key, slice) and isinstance(value, typing.Iterable): qualifiers = [self._parse_policy_qualifier(v) for v in value] if self.policy_qualifiers is None: self.policy_qualifiers = [] self.policy_qualifiers[key] = qualifiers elif isinstance(key, int): if self.policy_qualifiers is None: # Note: same as for examle "list()[0] = 3" raise ValueError("Index out of range") # NOTE: cast() here b/c Parsable... may also be an Iterable, so we cannot use isinstance() to # narrow the scope known to mypy. qualifier = self._parse_policy_qualifier(typing.cast(ParsablePolicyQualifier, value)) self.policy_qualifiers[key] = qualifier else: raise TypeError(f"{key}/{value}: Invalid key/value type") def __str__(self) -> str: return repr(self)
[docs] def append(self, value: ParsablePolicyQualifier) -> None: """Append the given policy qualifier.""" if self.policy_qualifiers is None: self.policy_qualifiers = [] self.policy_qualifiers.append(self._parse_policy_qualifier(value))
[docs] def as_text(self, width: int = 76) -> str: """Show as text.""" if self.policy_identifier is None: text = "Policy Identifier: None\n" else: text = f"Policy Identifier: {self.policy_identifier.dotted_string}\n" if self.policy_qualifiers: text += "Policy Qualifiers:\n" for qualifier in self.policy_qualifiers: if isinstance(qualifier, str): lines = "\n".join( textwrap.wrap(qualifier, initial_indent="* ", subsequent_indent=" ", width=width) ) text += f"{lines}\n" else: text += "* UserNotice:\n" if qualifier.explicit_text: text += ( "\n".join( textwrap.wrap( f"Explicit text: {qualifier.explicit_text}\n", initial_indent=" * ", subsequent_indent=" ", width=width - 2, ) ) + "\n" ) if qualifier.notice_reference: text += " * Reference:\n" text += f" * Organiziation: {qualifier.notice_reference.organization}\n" text += f" * Notice Numbers: {qualifier.notice_reference.notice_numbers}\n" else: text += "No Policy Qualifiers" return text.strip()
[docs] def clear(self) -> None: """Clear all qualifiers from this information.""" self.policy_qualifiers = None
[docs] def count(self, value: ParsablePolicyQualifier) -> int: """Count qualifiers from this information.""" if self.policy_qualifiers is None: return 0 try: parsed_value = self._parse_policy_qualifier(value) except ValueError: return 0 return self.policy_qualifiers.count(parsed_value)
[docs] def extend(self, values: Iterable[ParsablePolicyQualifier]) -> None: """Extend qualifiers with given iterable.""" if self.policy_qualifiers is None: self.policy_qualifiers = [] self.policy_qualifiers.extend([self._parse_policy_qualifier(v) for v in values])
@property def for_extension_type(self) -> x509.PolicyInformation: """Convert instance to a suitable cryptography class.""" return x509.PolicyInformation( policy_identifier=self.policy_identifier, policy_qualifiers=self.policy_qualifiers )
[docs] def insert(self, index: int, value: ParsablePolicyQualifier) -> None: """Insert qualifier at given index.""" if self.policy_qualifiers is None: self.policy_qualifiers = [] self.policy_qualifiers.insert(index, self._parse_policy_qualifier(value))
def _parse_policy_qualifier(self, qualifier: ParsablePolicyQualifier) -> PolicyQualifier: if isinstance(qualifier, str): return qualifier if isinstance(qualifier, x509.UserNotice): return qualifier if isinstance(qualifier, dict): explicit_text = qualifier.get("explicit_text") notice_reference = qualifier.get("notice_reference") if isinstance(notice_reference, dict): notice_reference = x509.NoticeReference( organization=notice_reference.get("organization"), notice_numbers=[int(i) for i in notice_reference.get("notice_numbers", [])], ) elif notice_reference is None: pass # extra branch to ensure test coverage elif isinstance(notice_reference, x509.NoticeReference): pass # extra branch to ensure test coverage else: raise ValueError("NoticeReference must be either None, a dict or an x509.NoticeReference") return x509.UserNotice(explicit_text=explicit_text, notice_reference=notice_reference) raise ValueError("PolicyQualifier must be string, dict or x509.UserNotice")
[docs] def parse_policy_qualifiers( self, qualifiers: Optional[Iterable[ParsablePolicyQualifier]] ) -> Optional[List[PolicyQualifier]]: """Parse given list of policy qualifiers.""" if qualifiers is None: return None return [self._parse_policy_qualifier(q) for q in qualifiers]
[docs] def get_policy_identifier(self) -> Optional[x509.ObjectIdentifier]: """Property for the policy identifier. Note that you can set any parsable value, it will always be an object identifier:: >>> pi = PolicyInformation() >>> pi.policy_identifier = '1.2.3' >>> pi.policy_identifier <ObjectIdentifier(oid=1.2.3, name=Unknown OID)> """ return self._policy_identifier
def _set_policy_identifier(self, value: ParsablePolicyIdentifier) -> None: if isinstance(value, str): self._policy_identifier = ObjectIdentifier(value) else: self._policy_identifier = value policy_identifier = property(get_policy_identifier, _set_policy_identifier) # NOTE: should return non-serialized version instead
[docs] def pop(self, index: int = -1) -> SerializedPolicyQualifier: # type: ignore[override] """Pop qualifier from given index.""" if self.policy_qualifiers is None: return [].pop() val = self._serialize_policy_qualifier(self.policy_qualifiers.pop(index)) if not self.policy_qualifiers: # if list is now empty, set to none self.policy_qualifiers = None return val
[docs] def remove(self, value: ParsablePolicyQualifier) -> PolicyQualifier: # type: ignore[override] """Remove the given qualifier from this policy information. Note that unlike list.remove(), this value returns the parsed value. """ if self.policy_qualifiers is None: # Shortcut to raise the same Value error as if the element is not in the list raise ValueError(f"{value}: not in list.") parsed_value = self._parse_policy_qualifier(value) self.policy_qualifiers.remove(parsed_value) if not self.policy_qualifiers: # if list is now empty, set to none self.policy_qualifiers = None return parsed_value
def _serialize_policy_qualifier(self, qualifier: PolicyQualifier) -> SerializedPolicyQualifier: if isinstance(qualifier, str): return qualifier value: SerializedUserNotice = {} if qualifier.explicit_text: value["explicit_text"] = qualifier.explicit_text if qualifier.notice_reference: value["notice_reference"] = { "notice_numbers": qualifier.notice_reference.notice_numbers, } if qualifier.notice_reference.organization is not None: value["notice_reference"]["organization"] = qualifier.notice_reference.organization return value
[docs] def serialize_policy_qualifiers(self) -> Optional[SerializedPolicyQualifiers]: """Serialize policy qualifiers.""" if self.policy_qualifiers is None: return None return [self._serialize_policy_qualifier(q) for q in self.policy_qualifiers]
[docs] def serialize(self) -> SerializedPolicyInformation: """Serialize this policy information.""" return { "policy_identifier": self.policy_identifier.dotted_string, "policy_qualifiers": self.serialize_policy_qualifiers(), }