Source code for django_ca.pydantic.extensions

# 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/>.

"""Pydantic models for x509 extensions.

Every extension model has exactly three parameters, `critical`, `value` and `type`.

The `critical` parameter is a boolean value. It usually defaults to the recommended value (usually in
`RFC 5280`_), but is mandatory in some extensions where no value is defined. Default values are defined in
:py:attr:`~django_ca.constants.EXTENSION_DEFAULT_CRITICAL`.

The `value` parameter represents the actual value of the extension, and its format is different for every
extension. In trivial extensions (for example in the
:py:class:`~django_ca.pydantic.extensions.InhibitAnyPolicyModel`), this is usually a basic type:

    >>> InhibitAnyPolicyModel(value=1)
    InhibitAnyPolicyModel(critical=True, value=1)

More complex extensions require a nested model:

    >>> dpoint = DistributionPointModel(
    ...     full_name=[{"type": "URI", "value": "https://ca.example.com/crl"}]
    ... )
    >>> CRLDistributionPointsModel(value=[dpoint])  # doctest: +STRIP_WHITESPACE
    CRLDistributionPointsModel(
        critical=False,
        value=[DistributionPointModel(
            full_name=[GeneralNameModel(type='URI', value='https://ca.example.com/crl')],
            relative_name=None, crl_issuer=None, reasons=None
        )]
    )

Nested models are described in more details under :ref:`pydantic_extension_attributes`.

Every model has a `cryptography` property returning the |Extension| instance and an `extension_type` property
returning the |ExtensionType| instance:

    >>> ext_model = InhibitAnyPolicyModel(value=1)
    >>> ext_model.extension_type
    <InhibitAnyPolicy(skip_certs=1)>
    >>> ext_model.cryptography  # doctest: +STRIP_WHITESPACE
    <Extension(
        oid=<ObjectIdentifier(oid=2.5.29.54, name=inhibitAnyPolicy)>,
        critical=True,
        value=<InhibitAnyPolicy(skip_certs=1)>
    )>

Finally, the `type` parameter is a literal string used to identify the type of extension. It is mandatory in
serialized versions of a model (e.g. as JSON), but does not have to be given when instantiating a model
directly.
"""

import abc
import base64
import typing
from types import MappingProxyType
from typing import Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union

from pydantic import (
    AfterValidator,
    Base64Bytes,
    BeforeValidator,
    ConfigDict,
    Field,
    TypeAdapter,
    field_validator,
    model_validator,
)
from pydantic.fields import ModelPrivateAttr
from pydantic_core.core_schema import ValidationInfo

from cryptography import x509
from cryptography.x509.oid import AuthorityInformationAccessOID, ExtensionOID, SubjectInformationAccessOID

from django_ca.constants import EXTENSION_DEFAULT_CRITICAL, KEY_USAGE_NAMES
from django_ca.pydantic import validators
from django_ca.pydantic.base import CryptographyModel
from django_ca.pydantic.extension_attributes import (
    AccessDescriptionModel,
    AuthorityKeyIdentifierValueModel,
    BasicConstraintsValueModel,
    DistributionPointModel,
    IssuingDistributionPointValueModel,
    MSCertificateTemplateValueModel,
    NameConstraintsValueModel,
    PolicyConstraintsValueModel,
    PolicyInformationModel,
    SignedCertificateTimestampModel,
    UnrecognizedExtensionValueModel,
)
from django_ca.pydantic.general_name import GeneralNameModel
from django_ca.pydantic.type_aliases import NonEmptyOrderedSet
from django_ca.typehints import (
    AlternativeNameTypeVar,
    Annotated,
    CRLExtensionTypeTypeVar,
    ExtensionTypeTypeVar,
    InformationAccessTypeVar,
    KeyUsages,
    NoValueExtensionTypeVar,
    SerializedPydanticExtension,
    SignedCertificateTimestampTypeVar,
)

if typing.TYPE_CHECKING:
    from pydantic.main import IncEx

###############
# Base models #
###############


class ExtensionModel(CryptographyModel[ExtensionTypeTypeVar], metaclass=abc.ABCMeta):
    """Base class for all extension models."""

    type: str = Field(repr=False)
    critical: bool
    value: Any
    requires_critical: typing.ClassVar[Optional[bool]] = None

    @field_validator("critical", mode="after")
    @classmethod
    def validate_critical(cls, critical: bool, info: ValidationInfo) -> bool:
        """Validate that the critical flag is correct if the defining RFC mandates a value."""
        if info.context is not None and info.context.get("validate_required_critical", True) is False:
            return critical

        if cls.requires_critical is True and cls.requires_critical != critical:
            raise ValueError("this extension must be marked as critical")
        if cls.requires_critical is False and cls.requires_critical != critical:
            raise ValueError("this extension must be marked as non-critical")
        return critical

    @property
    def cryptography(self) -> x509.Extension[ExtensionTypeTypeVar]:  # type: ignore[override]
        """Convert to a :py:class:`~cg:cryptography.x509.Extension` instance."""
        value = self.extension_type
        return x509.Extension(oid=value.oid, critical=self.critical, value=value)

    @property
    @abc.abstractmethod
    def extension_type(self) -> ExtensionTypeTypeVar:
        """ExtensionType subclass for this extension."""

    if typing.TYPE_CHECKING:
        # pylint: disable=unused-argument,missing-function-docstring

        def model_dump(  # type: ignore[override]
            self,
            *,
            mode: Literal["json", "python"] | str = "python",
            include: IncEx = None,
            exclude: IncEx = None,
            by_alias: bool = False,
            exclude_unset: bool = False,
            exclude_defaults: bool = False,
            exclude_none: bool = False,
            round_trip: bool = False,
            warnings: bool = True,
        ) -> SerializedPydanticExtension: ...


class BaseExtensionModel(ExtensionModel[ExtensionTypeTypeVar], metaclass=abc.ABCMeta):
    """Abstract base class for extension base classes.

    This base class adds the ``_extension_type`` attribute and the ``get_extension_type_class`` classmethod.
    """

    _extension_type: Type[ExtensionTypeTypeVar]

    @classmethod
    def get_extension_type_class(cls) -> Type[ExtensionTypeTypeVar]:
        """Get the :py:class:`~cg:cryptography.x509.ExtensionType` class configured via ``_extension_type``.

        Pydantic models will have a ``ModelPrivateAttr`` in class methods, instead of the actual configured
        class. The "good" solution would be to configure ``_extension_type`` as ``ClassVar``, but MyPy
        currently forbids this:

            https://github.com/python/mypy/issues/5144
        """
        private_attr = typing.cast(ModelPrivateAttr, cls._extension_type)
        return private_attr.default  # type: ignore[no-any-return]


class NoValueExtensionModel(BaseExtensionModel[NoValueExtensionTypeVar]):
    """Base model for extensions that do not have a value."""

    _extension_type: Type[NoValueExtensionTypeVar]
    value: None = Field(default=None, repr=False)

    @model_validator(mode="before")
    @classmethod
    def parse_cryptography(cls, data: Any) -> Any:
        """Parse cryptography instances."""
        extension_type_class = cls.get_extension_type_class()
        if isinstance(data, x509.Extension) and isinstance(data.value, extension_type_class):
            return {"critical": data.critical}
        return data

    @property
    def extension_type(self) -> NoValueExtensionTypeVar:
        """Convert to the respective cryptography extension type instance."""
        return self._extension_type()


class AlternativeNameBaseModel(BaseExtensionModel[AlternativeNameTypeVar]):
    """Base model for extensions with a list of general names as value."""

    _extension_type: Type[AlternativeNameTypeVar]

    value: NonEmptyOrderedSet[List[GeneralNameModel]]

    @model_validator(mode="before")
    @classmethod
    def parse_cryptography(cls, data: Any) -> Any:
        """Parse cryptography instances."""
        extension_type_class = cls.get_extension_type_class()
        if isinstance(data, x509.Extension) and isinstance(data.value, extension_type_class):
            return {"critical": data.critical, "value": list(data.value)}
        return data

    @property
    def extension_type(self) -> AlternativeNameTypeVar:
        """Convert to the respective cryptography extension type instance."""
        return self._extension_type(general_names=[name.cryptography for name in self.value])


class CRLExtensionBaseModel(BaseExtensionModel[CRLExtensionTypeTypeVar]):
    """Base model for extensions with a list of distribution points."""

    _extension_type: Type[CRLExtensionTypeTypeVar]

    value: NonEmptyOrderedSet[List[DistributionPointModel]]

    @model_validator(mode="before")
    @classmethod
    def parse_cryptography(cls, data: Any) -> Any:
        """Parse cryptography instances."""
        extension_type_class = cls.get_extension_type_class()
        if isinstance(data, x509.Extension) and isinstance(data.value, extension_type_class):
            return {"critical": data.critical, "value": list(data.value)}
        return data

    @property
    def extension_type(self) -> CRLExtensionTypeTypeVar:
        """Convert to the respective cryptography extension instance."""
        return self._extension_type(distribution_points=[dpoint.cryptography for dpoint in self.value])


class InformationAccessBaseModel(BaseExtensionModel[InformationAccessTypeVar]):
    """Base model for extensions with a list of access descriptions."""

    _extension_type: Type[InformationAccessTypeVar]
    _acceptable_oids: typing.ClassVar[Tuple[str, ...]]

    value: NonEmptyOrderedSet[List[AccessDescriptionModel]]

    @model_validator(mode="before")
    @classmethod
    def parse_cryptography(cls, data: Any) -> Any:
        """Parse cryptography instances."""
        extension_type_class = cls.get_extension_type_class()
        if isinstance(data, x509.Extension) and isinstance(data.value, extension_type_class):
            return {"critical": data.critical, "value": list(data.value)}
        return data

    @model_validator(mode="after")
    # pylint: disable-next=missing-function-docstring
    def check_consistency(self) -> "typing.Self":
        for desc in self.value:
            if desc.access_method not in self._acceptable_oids:
                raise ValueError(f"{desc.access_method}: access_method not acceptable for this extension.")
        return self

    @property
    def extension_type(self) -> InformationAccessTypeVar:
        """Convert to the respective cryptography extension instance."""
        return self._extension_type(descriptions=[ad.cryptography for ad in self.value])


class SignedCertificateTimestampBaseModel(ExtensionModel[SignedCertificateTimestampTypeVar]):
    """Base class for a extensions with a list of signed certificate timestamps."""

    model_config = ConfigDict(from_attributes=True)
    _extension_type: Type[SignedCertificateTimestampTypeVar]

    value: NonEmptyOrderedSet[List[SignedCertificateTimestampModel]]

    @property
    def extension_type(self) -> typing.NoReturn:  # pragma: no cover
        """Convert to the respective cryptography extension instance."""
        raise ValueError(f"{self._extension_type.__name__} cannot be loaded as cryptography instances.")


########################
# ExtensionType models #
########################


[docs] class AuthorityInformationAccessModel(InformationAccessBaseModel[x509.AuthorityInformationAccess]): """Pydantic model for a :py:class:`~cg:cryptography.x509.AuthorityInformationAccess` extension. The `value` is a list of :py:class:`~django_ca.pydantic.extension_attributes.AccessDescriptionModel` instances: >>> ocsp = AccessDescriptionModel( ... access_method='ocsp', ... access_location={'type': 'URI', 'value': 'http://ocsp.example.com'} ... ) >>> issuers = AccessDescriptionModel( ... access_method='ca_issuers', ... access_location={'type': 'URI', 'value': 'http://example.com'} ... ) >>> AuthorityInformationAccessModel(value=[ocsp, issuers]) # doctest: +STRIP_WHITESPACE AuthorityInformationAccessModel( critical=False, value=[ AccessDescriptionModel( access_method='1.3.6.1.5.5.7.48.1', access_location=GeneralNameModel(type='URI', value='http://ocsp.example.com') ), AccessDescriptionModel( access_method='1.3.6.1.5.5.7.48.2', access_location=GeneralNameModel(type='URI', value='http://example.com') ) ] ) """ _extension_type = x509.AuthorityInformationAccess _acceptable_oids = ( AuthorityInformationAccessOID.OCSP.dotted_string, AuthorityInformationAccessOID.CA_ISSUERS.dotted_string, ) type: Literal["authority_information_access"] = Field(default="authority_information_access", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.AUTHORITY_INFORMATION_ACCESS] requires_critical: typing.ClassVar[bool] = False # MUST mark this extension as non-critical.
[docs] class AuthorityKeyIdentifierModel(ExtensionModel[x509.AuthorityKeyIdentifier]): """Pydantic model for a :py:class:`~cg:cryptography.x509.AuthorityKeyIdentifier` extension. The `value` is a :py:class:`~django_ca.pydantic.extension_attributes.AuthorityKeyIdentifierValueModel` instance: >>> value = AuthorityKeyIdentifierValueModel(key_identifier=b"MTIz") >>> AuthorityKeyIdentifierModel(value=value) # doctest: +STRIP_WHITESPACE AuthorityKeyIdentifierModel( critical=False, value=AuthorityKeyIdentifierValueModel( key_identifier=b'123', authority_cert_issuer=None, authority_cert_serial_number=None ) ) """ model_config = ConfigDict(from_attributes=True) type: Literal["authority_key_identifier"] = Field(default="authority_key_identifier", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.AUTHORITY_KEY_IDENTIFIER] value: AuthorityKeyIdentifierValueModel requires_critical: typing.ClassVar[bool] = False # MUST mark this extension as non-critical. @property def extension_type(self) -> x509.AuthorityKeyIdentifier: """Convert to a :py:class:`~cg:cryptography.x509.AuthorityKeyIdentifier` instance.""" return self.value.cryptography
[docs] class BasicConstraintsModel(ExtensionModel[x509.BasicConstraints]): """Pydantic model for a :py:class:`~cg:cryptography.x509.BasicConstraints` extension. The `value` is a :py:class:`~django_ca.pydantic.extension_attributes.BasicConstraintsValueModel` instance. For example, for a certificate authority: >>> value = BasicConstraintsValueModel(ca=True, path_length=0) >>> BasicConstraintsModel(value=value) BasicConstraintsModel(critical=True, value=BasicConstraintsValueModel(ca=True, path_length=0)) For end-entity certificates, sets `ca` to ``False`` and `path_length` to ``None``: >>> value = BasicConstraintsValueModel(ca=False, path_length=None) >>> BasicConstraintsModel(value=value) BasicConstraintsModel(critical=True, value=BasicConstraintsValueModel(ca=False, path_length=None)) """ model_config = ConfigDict(from_attributes=True) type: Literal["basic_constraints"] = Field(default="basic_constraints", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.BASIC_CONSTRAINTS] value: BasicConstraintsValueModel @property def extension_type(self) -> x509.BasicConstraints: """Convert to a :py:class:`~cg:cryptography.x509.BasicConstraints` instance.""" return self.value.cryptography
[docs] class CRLDistributionPointsModel(CRLExtensionBaseModel[x509.CRLDistributionPoints]): """Pydantic model for a :py:class:`~cg:cryptography.x509.CRLDistributionPoints` extension. The `value` is a list of :py:class:`~django_ca.pydantic.extension_attributes.DistributionPointModel` instances: >>> dpoint = DistributionPointModel( ... full_name=[{"type": "URI", "value": "https://ca.example.com/crl"}] ... ) >>> CRLDistributionPointsModel(value=[dpoint]) # doctest: +STRIP_WHITESPACE CRLDistributionPointsModel( critical=False, value=[DistributionPointModel( full_name=[GeneralNameModel(type='URI', value='https://ca.example.com/crl')], relative_name=None, crl_issuer=None, reasons=None )] ) Please refer to the ``DistributionPointModel`` documentation for full information on how to instantiate this model. """ _extension_type = x509.CRLDistributionPoints type: Literal["crl_distribution_points"] = Field(default="crl_distribution_points", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.CRL_DISTRIBUTION_POINTS]
[docs] class CRLNumberModel(ExtensionModel[x509.CRLNumber]): """Pydantic model for a :py:class:`~cg:cryptography.x509.CRLNumber` extension. The `value` is an integer: >>> CRLNumberModel(value=1) CRLNumberModel(critical=False, value=1) """ type: Literal["crl_number"] = Field(default="crl_number", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.CRL_NUMBER] requires_critical: typing.ClassVar[bool] = False # is a non-critical CRL extension value: int = Field(ge=0) @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.CRLNumber): return {"critical": data.critical, "value": data.value.crl_number} return data @property def extension_type(self) -> x509.CRLNumber: """Convert to a :py:class:`~cg:cryptography.x509.CRLNumber` instance.""" return x509.CRLNumber(crl_number=self.value)
[docs] class CertificatePoliciesModel(ExtensionModel[x509.CertificatePolicies]): """Pydantic model for a :py:class:`~cg:cryptography.x509.CertificatePolicies` extension. The `value` is a list of :py:class:`~django_ca.pydantic.extension_attributes.PolicyInformationModel` instances: >>> policy = PolicyInformationModel(policy_identifier="2.5.29.32.0") >>> CertificatePoliciesModel(value=[policy]) # doctest: +STRIP_WHITESPACE CertificatePoliciesModel( critical=False, value=[ PolicyInformationModel(policy_identifier='2.5.29.32.0', policy_qualifiers=None) ] ) """ model_config = ConfigDict(json_schema_extra={"description": "A CertificatePolicies extension."}) type: Literal["certificate_policies"] = Field(default="certificate_policies", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.CERTIFICATE_POLICIES] value: NonEmptyOrderedSet[List[PolicyInformationModel]] = Field( description="The value of the CertificatePolicies extension is a list of policy information objects.", json_schema_extra={"minItems": 1, "uniqueItems": True}, )
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.CertificatePolicies): return {"critical": data.critical, "value": list(data.value)} return data
@property def extension_type(self) -> x509.CertificatePolicies: """Convert to a :py:class:`~cg:cryptography.x509.CertificatePolicies` instance.""" return x509.CertificatePolicies([pol.cryptography for pol in self.value])
[docs] class DeltaCRLIndicatorModel(ExtensionModel[x509.DeltaCRLIndicator]): """Pydantic model for a :py:class:`~cg:cryptography.x509.DeltaCRLIndicator` extension. The `value` is an integer: >>> DeltaCRLIndicatorModel(value=1) DeltaCRLIndicatorModel(critical=True, value=1) """ type: Literal["delta_crl_indicator"] = Field(default="delta_crl_indicator", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.DELTA_CRL_INDICATOR] requires_critical: typing.ClassVar[bool] = True # is a critical CRL extension value: int = Field(ge=0) @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.DeltaCRLIndicator): return {"critical": data.critical, "value": data.value.crl_number} return data @property def extension_type(self) -> x509.DeltaCRLIndicator: """Convert to a :py:class:`~cg:cryptography.x509.DeltaCRLIndicator` instance.""" return x509.DeltaCRLIndicator(crl_number=self.value)
[docs] class ExtendedKeyUsageModel(ExtensionModel[x509.ExtendedKeyUsage]): """Pydantic model for a :py:class:`~cg:cryptography.x509.ExtendedKeyUsage` extension. The `value` is a list valid object identifiers as dotted strings. For convenience, any name from :py:attr:`~django_ca.constants.EXTENDED_KEY_USAGE_NAMES` can also be given: >>> ExtendedKeyUsageModel(value=["clientAuth", "1.3.6.1.5.5.7.3.1"]) ExtendedKeyUsageModel(critical=False, value=['1.3.6.1.5.5.7.3.2', '1.3.6.1.5.5.7.3.1']) """ type: Literal["extended_key_usage"] = Field(default="extended_key_usage", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.EXTENDED_KEY_USAGE] value: NonEmptyOrderedSet[ List[ Annotated[ str, BeforeValidator(validators.oid_parser), AfterValidator(validators.extended_key_usage_validator), AfterValidator(validators.oid_validator), ] ] ]
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.ExtendedKeyUsage): return {"critical": data.critical, "value": list(data.value)} return data
@property def extension_type(self) -> x509.ExtendedKeyUsage: """Convert to a :py:class:`~cg:cryptography.x509.ExtendedKeyUsage` instance.""" return x509.ExtendedKeyUsage(usages=[x509.ObjectIdentifier(usage) for usage in self.value])
[docs] class FreshestCRLModel(CRLExtensionBaseModel[x509.FreshestCRL]): """Pydantic model for a :py:class:`~cg:cryptography.x509.FreshestCRL` extension. This model behaves exactly like :py:class:`~django_ca.pydantic.extensions.CRLDistributionPointsModel`. """ _extension_type = x509.FreshestCRL type: Literal["freshest_crl"] = Field(default="freshest_crl", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.FRESHEST_CRL] requires_critical: typing.ClassVar[bool] = False # MUST be marked as non-critical
[docs] class InhibitAnyPolicyModel(ExtensionModel[x509.InhibitAnyPolicy]): """Pydantic model for a :py:class:`~cg:cryptography.x509.InhibitAnyPolicy` extension. The `value` attribute is an integer: >>> InhibitAnyPolicyModel(value=1) InhibitAnyPolicyModel(critical=True, value=1) """ type: Literal["inhibit_any_policy"] = Field(default="inhibit_any_policy", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.INHIBIT_ANY_POLICY] value: int = Field(ge=0) requires_critical: typing.ClassVar[bool] = True # MUST mark this extension as critical
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.InhibitAnyPolicy): return {"critical": data.critical, "value": data.value.skip_certs} return data
@property def extension_type(self) -> x509.InhibitAnyPolicy: """Convert to a :py:class:`~cg:cryptography.x509.InhibitAnyPolicy` instance.""" return x509.InhibitAnyPolicy(skip_certs=self.value)
[docs] class IssuerAlternativeNameModel(AlternativeNameBaseModel[x509.IssuerAlternativeName]): """Pydantic model for a :py:class:`~cg:cryptography.x509.IssuerAlternativeName` extension. This model behaves exactly like :py:class:`~django_ca.pydantic.extensions.SubjectAlternativeNameModel`. """ _extension_type = x509.IssuerAlternativeName type: Literal["issuer_alternative_name"] = Field(default="issuer_alternative_name", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.ISSUER_ALTERNATIVE_NAME]
[docs] class IssuingDistributionPointModel(ExtensionModel[x509.IssuingDistributionPoint]): """Pydantic model for a :py:class:`~cg:cryptography.x509.IssuingDistributionPoint` extension. The `value` is a :py:class:`~django_ca.pydantic.extension_attributes.IssuingDistributionPointValueModel` instances: >>> full_name = [{"type": "URI", "value": "https://ca.example.com/crl"}] >>> value = IssuingDistributionPointValueModel(full_name=full_name) >>> IssuingDistributionPointModel(value=value) # doctest: +ELLIPSIS IssuingDistributionPointModel(critical=True, value=IssuingDistributionPointValueModel(...)) """ model_config = ConfigDict(from_attributes=True) type: Literal["issuing_distribution_point"] = Field(default="issuing_distribution_point", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.ISSUING_DISTRIBUTION_POINT] value: IssuingDistributionPointValueModel requires_critical: typing.ClassVar[bool] = True # "is a critical CRL extension" @property def extension_type(self) -> x509.IssuingDistributionPoint: """Convert to a :py:class:`~cg:cryptography.x509.IssuingDistributionPoint` instance.""" return self.value.cryptography
[docs] class KeyUsageModel(ExtensionModel[x509.KeyUsage]): """Pydantic model for a :py:class:`~cg:cryptography.x509.KeyUsage` extension. All key usages default to ``False``, so you can skip giving any usages you don't care about: >>> KeyUsageModel(value=["key_agreement", "key_encipherment"]) KeyUsageModel(critical=True, value=['key_agreement', 'key_encipherment']) """ type: Literal["key_usage"] = Field(default="key_usage", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.KEY_USAGE] value: NonEmptyOrderedSet[List[Literal[KeyUsages]]]
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.KeyUsage): extension_type: x509.KeyUsage = data.value values: List[Literal[KeyUsages]] = [] for value in KEY_USAGE_NAMES: try: if getattr(extension_type, value) is True: values.append(value) except ValueError: # encipher_only and decipher_only throw ValueError if key_agreement is False pass return {"critical": data.critical, "value": values} return data
@model_validator(mode="after") # pylint: disable-next=missing-function-docstring def check_key_agreement(self) -> "KeyUsageModel": if "key_agreement" not in self.value and ( "encipher_only" in self.value or "decipher_only" in self.value ): raise ValueError("encipher_only and decipher_only can only be set when key_agreement is set") return self @property def extension_type(self) -> x509.KeyUsage: """Convert to a :py:class:`~cg:cryptography.x509.KeyUsage` instance.""" params: Dict[str, bool] = {k: k in self.value for k in KEY_USAGE_NAMES} return x509.KeyUsage(**params)
[docs] class MSCertificateTemplateModel(ExtensionModel[x509.MSCertificateTemplate]): """Pydantic model for a :py:class:`~cg:cryptography.x509.MSCertificateTemplate` extension. The `value` is a :py:class:`~django_ca.pydantic.extension_attributes.MSCertificateTemplateValueModel` instance: >>> value = MSCertificateTemplateValueModel(template_id="1.2.3", major_version=1) >>> MSCertificateTemplateModel(critical=True, value=value) # doctest: +STRIP_WHITESPACE MSCertificateTemplateModel( critical=True, value=MSCertificateTemplateValueModel(template_id='1.2.3', major_version=1, minor_version=None) ) Note that this extension does not have a default defined for the `critical` parameter, so it is mandatory. """ model_config = ConfigDict(from_attributes=True) type: Literal["ms_certificate_template"] = Field(default="ms_certificate_template", repr=False) critical: bool value: MSCertificateTemplateValueModel @property def extension_type(self) -> x509.MSCertificateTemplate: """Convert to a :py:class:`~cg:cryptography.x509.MSCertificateTemplate` instance.""" return self.value.cryptography
[docs] class NameConstraintsModel(ExtensionModel[x509.NameConstraints]): """Pydantic model for a :py:class:`~cg:cryptography.x509.NameConstraints` extension. The `value` is a :py:class:`~django_ca.pydantic.extension_attributes.NameConstraintsValueModel` instance: >>> value = NameConstraintsValueModel(permitted_subtrees=[{"type": "DNS", "value": ".com"}]) >>> NameConstraintsModel(value=value) # doctest: +STRIP_WHITESPACE NameConstraintsModel( critical=True, value=NameConstraintsValueModel( permitted_subtrees=[GeneralNameModel(type='DNS', value='.com')], excluded_subtrees=None ) ) """ model_config = ConfigDict(from_attributes=True) type: Literal["name_constraints"] = Field(default="name_constraints", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.NAME_CONSTRAINTS] value: NameConstraintsValueModel requires_critical: typing.ClassVar[bool] = True # MUST mark this extension as critical @property def extension_type(self) -> x509.NameConstraints: """The :py:class:`~cg:cryptography.x509.NameConstraints` instance.""" return self.value.cryptography
[docs] class OCSPNoCheckModel(NoValueExtensionModel[x509.OCSPNoCheck]): """Pydantic model for a :py:class:`~cg:cryptography.x509.OCSPNoCheck` extension. This extension does not have a value, and thus can be instantiated without any parameters (but ``None`` is also accepted): >>> OCSPNoCheckModel() OCSPNoCheckModel(critical=False) >>> OCSPNoCheckModel(value=None, critical=True) OCSPNoCheckModel(critical=True) """ _extension_type = x509.OCSPNoCheck type: Literal["ocsp_no_check"] = Field(default="ocsp_no_check", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.OCSP_NO_CHECK]
[docs] class PolicyConstraintsModel(ExtensionModel[x509.PolicyConstraints]): """Pydantic model for a :py:class:`~cg:cryptography.x509.PolicyConstraints` extension. The `value` is a :py:class:`~django_ca.pydantic.extension_attributes.PolicyConstraintsValueModel` instance: >>> value = PolicyConstraintsValueModel(require_explicit_policy=0, inhibit_policy_mapping=1) >>> PolicyConstraintsModel(value=value) # doctest: +STRIP_WHITESPACE PolicyConstraintsModel( critical=True, value=PolicyConstraintsValueModel(require_explicit_policy=0, inhibit_policy_mapping=1) ) """ model_config = ConfigDict(from_attributes=True) type: Literal["policy_constraints"] = Field(default="policy_constraints", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.POLICY_CONSTRAINTS] value: PolicyConstraintsValueModel requires_critical: typing.ClassVar[bool] = True # MUST mark this extension as critical @property def extension_type(self) -> x509.PolicyConstraints: """The :py:class:`~cg:cryptography.x509.PolicyConstraints` instance.""" return self.value.cryptography
[docs] class PrecertPoisonModel(NoValueExtensionModel[x509.PrecertPoison]): """Pydantic model for a :py:class:`~cg:cryptography.x509.PrecertPoison` extension. This extension does not have a value, and thus can be instantiated without any parameters (but ``None`` is also accepted): >>> PrecertPoisonModel() PrecertPoisonModel(critical=True) >>> PrecertPoisonModel(value=None, critical=True) PrecertPoisonModel(critical=True) """ _extension_type = x509.PrecertPoison type: Literal["precert_poison"] = Field(default="precert_poison", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.PRECERT_POISON] requires_critical: typing.ClassVar[bool] = True # RFC 6962: "critical poison extension"
[docs] class PrecertificateSignedCertificateTimestampsModel( SignedCertificateTimestampBaseModel[x509.PrecertificateSignedCertificateTimestamps] ): """Model for a :py:class:`~cg:cryptography.x509.PrecertificateSignedCertificateTimestamps` extension. .. NOTE:: Due to library limitations, this model cannot be converted to a cryptography class. The `value` is a list of :py:class:`~django_ca.pydantic.extension_attributes.SignedCertificateTimestampModel` instances: >>> from datetime import datetime >>> sct = SignedCertificateTimestampModel( ... log_id=b"MTIz", timestamp=datetime(2023, 12, 10), entry_type="precertificate" ... ) >>> PrecertificateSignedCertificateTimestampsModel( ... value=[sct] ... ) # doctest: +STRIP_WHITESPACE PrecertificateSignedCertificateTimestampsModel( critical=False, value=[ SignedCertificateTimestampModel( version='v1', log_id=b'123', timestamp=datetime.datetime(2023, 12, 10, 0, 0), entry_type='precertificate' ) ] ) """ _extension_type = x509.PrecertificateSignedCertificateTimestamps type: Literal["precertificate_signed_certificate_timestamps"] = Field( default="precertificate_signed_certificate_timestamps", repr=False ) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS]
[docs] class SignedCertificateTimestampsModel(SignedCertificateTimestampBaseModel[x509.SignedCertificateTimestamps]): """Pydantic model for a :py:class:`~cg:cryptography.x509.SignedCertificateTimestamps` extension. This model behaves exactly like :py:class:`~django_ca.pydantic.extensions.PrecertificateSignedCertificateTimestampsModel`. """ _extension_type = x509.SignedCertificateTimestamps type: Literal["signed_certificate_timestamps"] = Field( default="signed_certificate_timestamps", repr=False ) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS]
[docs] class SubjectAlternativeNameModel(AlternativeNameBaseModel[x509.SubjectAlternativeName]): """Pydantic model for a :py:class:`~cg:cryptography.x509.SubjectAlternativeName` extension. The `general_names` attribute is a list of :py:class:`~django_ca.pydantic.general_name.GeneralNameModel` instances: >>> name1 = {"type": "DNS", "value": "example.com"} >>> name2 = {"type": "DNS", "value": "example.net"} >>> SubjectAlternativeNameModel(value=[name1, name2]) # doctest: +STRIP_WHITESPACE SubjectAlternativeNameModel( critical=False, value=[ GeneralNameModel(type='DNS', value='example.com'), GeneralNameModel(type='DNS', value='example.net') ] ) """ _extension_type = x509.SubjectAlternativeName type: Literal["subject_alternative_name"] = Field(default="subject_alternative_name", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.SUBJECT_ALTERNATIVE_NAME]
[docs] class SubjectInformationAccessModel(InformationAccessBaseModel[x509.SubjectInformationAccess]): """Pydantic model for a :py:class:`~cg:cryptography.x509.SubjectInformationAccess` extension. This model behaves like the :py:class:`~django_ca.pydantic.extensions.AuthorityInformationAccessModel`, except that the access methods have to be `ca_repository`: >>> access_description = AccessDescriptionModel( ... access_method='ca_repository', ... access_location={'type': 'URI', 'value': 'http://example.com'} ... ) >>> SubjectInformationAccessModel(value=[access_description]) # doctest: +STRIP_WHITESPACE SubjectInformationAccessModel( critical=False, value=[ AccessDescriptionModel( access_method='1.3.6.1.5.5.7.48.5', access_location=GeneralNameModel(type='URI', value='http://example.com') ) ] ) """ _extension_type = x509.SubjectInformationAccess _acceptable_oids = (SubjectInformationAccessOID.CA_REPOSITORY.dotted_string,) type: Literal["subject_information_access"] = Field(default="subject_information_access", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.SUBJECT_INFORMATION_ACCESS] requires_critical: typing.ClassVar[bool] = False # MUST mark this extension as non-critical
[docs] class SubjectKeyIdentifierModel(ExtensionModel[x509.SubjectKeyIdentifier]): """Pydantic model for a :py:class:`~cg:cryptography.x509.SubjectKeyIdentifier` extension. The `value` is a base64-encoded ``bytes`` instance: >>> SubjectKeyIdentifierModel(value=b"kA==") SubjectKeyIdentifierModel(critical=False, value=b'\\x90') """ type: Literal["subject_key_identifier"] = Field(default="subject_key_identifier", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.SUBJECT_KEY_IDENTIFIER] value: Base64Bytes requires_critical: typing.ClassVar[bool] = False # MUST mark this extension as non-critical
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Parse cryptography instances.""" if isinstance(data, x509.Extension) and isinstance(data.value, x509.SubjectKeyIdentifier): return {"critical": data.critical, "value": base64.b64encode(data.value.digest)} return data
@property def extension_type(self) -> x509.SubjectKeyIdentifier: """The :py:class:`~cg:cryptography.x509.SubjectKeyIdentifier` instance.""" return x509.SubjectKeyIdentifier(digest=self.value)
[docs] class TLSFeatureModel(ExtensionModel[x509.TLSFeature]): """Pydantic model for a :py:class:`~cg:cryptography.x509.TLSFeature` extension. The `value` is a list of one or both of ``"status_request"`` and ``"status_request_v2"``. >>> TLSFeatureModel(value=["status_request"]) TLSFeatureModel(critical=False, value=['status_request']) For convenience, the model also accepts keys named in :py:attr:`~django_ca.constants.TLS_FEATURE_NAMES`: >>> TLSFeatureModel(value=["OCSPMustStaple"]) TLSFeatureModel(critical=False, value=['status_request']) """ model_config = ConfigDict(from_attributes=True) type: Literal["tls_feature"] = Field(default="tls_feature", repr=False) critical: bool = EXTENSION_DEFAULT_CRITICAL[ExtensionOID.TLS_FEATURE] value: NonEmptyOrderedSet[ List[ Annotated[ Literal["status_request", "status_request_v2"], BeforeValidator(validators.tls_feature_validator), ] ] ] @property def extension_type(self) -> x509.TLSFeature: """The :py:class:`~cg:cryptography.x509.TLSFeature` instance.""" features = [getattr(x509.TLSFeatureType, feature) for feature in self.value] return x509.TLSFeature(features=features)
[docs] class UnrecognizedExtensionModel(ExtensionModel[x509.UnrecognizedExtension]): """Pydantic model for a :py:class:`~cg:cryptography.x509.UnrecognizedExtension` extension. The `value` a :py:class:`~django_ca.pydantic.extension_attributes.UnrecognizedExtensionValueModel` value: >>> value = UnrecognizedExtensionValueModel(value=b"MTIz", oid="1.2.3") >>> UnrecognizedExtensionModel(critical=True, value=value) # doctest: +STRIP_WHITESPACE UnrecognizedExtensionModel( critical=True, value=UnrecognizedExtensionValueModel(oid='1.2.3', value=b'123') ) """ model_config = ConfigDict(from_attributes=True) type: Literal["unknown"] = Field(default="unknown", repr=False) critical: bool value: UnrecognizedExtensionValueModel @property def cryptography(self) -> x509.Extension[x509.UnrecognizedExtension]: # type: ignore[override] """Return the respective cryptography instance.""" value = self.extension_type return x509.Extension(critical=self.critical, oid=value.oid, value=value) @property def extension_type(self) -> x509.UnrecognizedExtension: """The :py:class:`~cg:cryptography.x509.UnrecognizedExtension` instance.""" return self.value.cryptography
EXTENSION_MODEL_OIDS: "MappingProxyType[Type[ExtensionModel[Any]], x509.ObjectIdentifier]" = MappingProxyType( { AuthorityInformationAccessModel: ExtensionOID.AUTHORITY_INFORMATION_ACCESS, AuthorityKeyIdentifierModel: ExtensionOID.AUTHORITY_KEY_IDENTIFIER, BasicConstraintsModel: ExtensionOID.BASIC_CONSTRAINTS, CRLDistributionPointsModel: ExtensionOID.CRL_DISTRIBUTION_POINTS, CRLNumberModel: ExtensionOID.CRL_NUMBER, CertificatePoliciesModel: ExtensionOID.CERTIFICATE_POLICIES, DeltaCRLIndicatorModel: ExtensionOID.DELTA_CRL_INDICATOR, ExtendedKeyUsageModel: ExtensionOID.EXTENDED_KEY_USAGE, FreshestCRLModel: ExtensionOID.FRESHEST_CRL, InhibitAnyPolicyModel: ExtensionOID.INHIBIT_ANY_POLICY, IssuerAlternativeNameModel: ExtensionOID.ISSUER_ALTERNATIVE_NAME, IssuingDistributionPointModel: ExtensionOID.ISSUING_DISTRIBUTION_POINT, KeyUsageModel: ExtensionOID.KEY_USAGE, MSCertificateTemplateModel: ExtensionOID.MS_CERTIFICATE_TEMPLATE, NameConstraintsModel: ExtensionOID.NAME_CONSTRAINTS, OCSPNoCheckModel: ExtensionOID.OCSP_NO_CHECK, PolicyConstraintsModel: ExtensionOID.POLICY_CONSTRAINTS, PrecertPoisonModel: ExtensionOID.PRECERT_POISON, PrecertificateSignedCertificateTimestampsModel: ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS, SignedCertificateTimestampsModel: ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS, SubjectAlternativeNameModel: ExtensionOID.SUBJECT_ALTERNATIVE_NAME, SubjectInformationAccessModel: ExtensionOID.SUBJECT_INFORMATION_ACCESS, SubjectKeyIdentifierModel: ExtensionOID.SUBJECT_KEY_IDENTIFIER, TLSFeatureModel: ExtensionOID.TLS_FEATURE, } ) EXTENSION_MODELS: "MappingProxyType[x509.ObjectIdentifier, Type[ExtensionModel[Any]]]" = MappingProxyType( {v: k for k, v in EXTENSION_MODEL_OIDS.items()} )
[docs] def validate_cryptograph_extensions(v: Any, info: ValidationInfo) -> Any: """Parse a cryptography extension into a Pydantic model.""" if isinstance(v, x509.Extension): if isinstance(v.value, x509.UnrecognizedExtension): model_class: type[ExtensionModel[Any]] = UnrecognizedExtensionModel else: model_class = EXTENSION_MODELS[v.oid] return model_class.model_validate(v, context=info.context) return v
#: Union type for extensions that may occur as input when signing a certificate. SignCertificateExtensions = Annotated[ Annotated[ Union[ AuthorityInformationAccessModel, CertificatePoliciesModel, CRLDistributionPointsModel, ExtendedKeyUsageModel, FreshestCRLModel, IssuerAlternativeNameModel, KeyUsageModel, OCSPNoCheckModel, SubjectAlternativeNameModel, TLSFeatureModel, ], Field(discriminator="type"), ], BeforeValidator(validate_cryptograph_extensions), ] #: Union type for all known extensions that may occur in any type of certificate. CertificateExtensions = Annotated[ Annotated[ Union[ AuthorityInformationAccessModel, AuthorityKeyIdentifierModel, BasicConstraintsModel, CRLDistributionPointsModel, CertificatePoliciesModel, ExtendedKeyUsageModel, FreshestCRLModel, InhibitAnyPolicyModel, IssuerAlternativeNameModel, KeyUsageModel, MSCertificateTemplateModel, NameConstraintsModel, OCSPNoCheckModel, PolicyConstraintsModel, PrecertPoisonModel, PrecertificateSignedCertificateTimestampsModel, SignedCertificateTimestampsModel, SubjectAlternativeNameModel, SubjectInformationAccessModel, SubjectKeyIdentifierModel, TLSFeatureModel, UnrecognizedExtensionModel, ], Field(discriminator="type"), ], BeforeValidator(validate_cryptograph_extensions), ] SignCertificateExtensionsList = TypeAdapter(List[SignCertificateExtensions]) CertificateExtensionsList = TypeAdapter(List[CertificateExtensions]) ExtensionModelTypeVar = TypeVar("ExtensionModelTypeVar", bound=ExtensionModel[Any])