# 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/>.
"""Some extensions use a more complex datastructure, so attributes are represented as nested models."""
import base64
from datetime import datetime
from typing import TYPE_CHECKING, Annotated, Any, Literal, NoReturn, Self
from annotated_types import MaxLen, MinLen
from pydantic import AfterValidator, BeforeValidator, ConfigDict, Field, model_validator
from cryptography import x509
from cryptography.x509 import certificate_transparency
from cryptography.x509.oid import CertificatePoliciesOID
from django_ca import constants
from django_ca.pydantic import validators
from django_ca.pydantic.base import CryptographyModel
from django_ca.pydantic.general_name import GeneralNameModel
from django_ca.pydantic.name import NameModel
from django_ca.pydantic.type_aliases import Base64EncodedBytes, NonEmptyOrderedSet, OIDType
from django_ca.typehints import DistributionPointReason, LogEntryTypeName
if TYPE_CHECKING:
PrivateKeyUsagePeriodValueModelBase = CryptographyModel[x509.PrivateKeyUsagePeriod]
else:
PrivateKeyUsagePeriodValueModelBase = CryptographyModel
_NOTICE_REFERENCE_DESCRIPTION = (
"A NoticeReferenceModel consists of an optional *organization* and an optional list of *notice_numbers*."
)
[docs]
class NamingAuthorityModel(CryptographyModel[x509.NamingAuthority]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.NamingAuthority`.
.. versionadded:: 2.1.0
"""
model_config = ConfigDict(from_attributes=True)
id: OIDType | None = None
url: Annotated[str, MaxLen(128)] | None = None
text: Annotated[str, MaxLen(128)] | None = None
@property
def cryptography(self) -> "x509.NamingAuthority":
"""Convert to a :py:class:`~cg:cryptography.x509.NamingAuthority` instance."""
oid = None
if self.id is not None:
oid = x509.ObjectIdentifier(self.id)
return x509.NamingAuthority(id=oid, url=self.url, text=self.text)
[docs]
class ProfessionInfoModel(CryptographyModel[x509.ProfessionInfo]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.ProfessionInfo`.
.. versionadded:: 2.1.0
"""
model_config = ConfigDict(from_attributes=True)
naming_authority: NamingAuthorityModel | None = None
profession_items: Annotated[list[Annotated[str, MaxLen(128)]], MinLen(1)]
profession_oids: list[OIDType] | None = None
registration_number: Annotated[str, MaxLen(128)] | None = None
add_profession_info: Base64EncodedBytes | None = None
@property
def cryptography(self) -> x509.ProfessionInfo:
"""Convert to a :py:class:`~cg:cryptography.x509.ProfessionInfo` instance."""
naming_authority = profession_oids = add_profession_info = None
if self.naming_authority is not None:
naming_authority = self.naming_authority.cryptography
if self.profession_oids is not None:
profession_oids = [x509.ObjectIdentifier(oid) for oid in self.profession_oids]
if self.add_profession_info is not None:
add_profession_info = base64.b64decode(self.add_profession_info)
return x509.ProfessionInfo(
naming_authority=naming_authority,
profession_items=self.profession_items,
profession_oids=profession_oids,
registration_number=self.registration_number,
add_profession_info=add_profession_info,
)
[docs]
@model_validator(mode="after")
def check_consistency(self) -> "ProfessionInfoModel":
"""Verify that `profession_oids` has the same length as `profession_items` if set."""
if self.profession_oids is not None and len(self.profession_items) != len(self.profession_oids):
raise ValueError("if present, profession_oids must have the same length as profession_items.")
return self
[docs]
class AdmissionModel(CryptographyModel[x509.Admission]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.Admission`.
.. versionadded:: 2.1.0
"""
model_config = ConfigDict(from_attributes=True)
admission_authority: GeneralNameModel | None = None
naming_authority: NamingAuthorityModel | None = None
profession_infos: Annotated[list[ProfessionInfoModel], MinLen(1)]
@property
def cryptography(self) -> x509.Admission:
"""Convert to a :py:class:`~cg:cryptography.x509.Admission` instance."""
admission_authority = naming_authority = None
if self.admission_authority is not None:
admission_authority = self.admission_authority.cryptography
if self.naming_authority is not None:
naming_authority = self.naming_authority.cryptography
return x509.Admission(
admission_authority=admission_authority,
naming_authority=naming_authority,
profession_infos=[pi.cryptography for pi in self.profession_infos],
)
[docs]
class AdmissionsValueModel(CryptographyModel[x509.Admissions]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.Admissions`.
.. versionadded:: 2.1.0
"""
model_config = ConfigDict(from_attributes=True)
authority: GeneralNameModel | None = None
admissions: list[AdmissionModel] = Field(default_factory=list)
@property
def cryptography(self) -> x509.Admissions:
"""Convert to a :py:class:`~cg:cryptography.x509.Admissions` instance."""
authority = None
if self.authority is not None:
authority = self.authority.cryptography
return x509.Admissions(
authority=authority, admissions=[admission.cryptography for admission in self.admissions]
)
@model_validator(mode="before")
@classmethod
def parse_cryptography(cls, data: Any) -> Any:
"""Parse cryptography instance."""
if isinstance(data, x509.Admissions):
return {"authority": data.authority, "admissions": data._admissions} # pylint: disable=protected-access
return data
[docs]
class AccessDescriptionModel(CryptographyModel[x509.AccessDescription]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.AccessDescription`.
The `access_method` may be a dotted string OID or one of the aliases listed in
:py:attr:`~django_ca.constants.ACCESS_METHOD_TYPES`. The `access_location` is a
:py:class:`~django_ca.pydantic.general_name.GeneralNameModel`:
.. pydantic-model:: access_description_ocsp
The syntax is identical for CA issuers:
.. pydantic-model:: access_description_ca_issuers
"""
model_config = ConfigDict(from_attributes=True)
access_method: Annotated[
str,
BeforeValidator(validators.oid_parser),
BeforeValidator(validators.access_method_parser),
AfterValidator(validators.oid_validator),
]
access_location: GeneralNameModel
@property
def cryptography(self) -> x509.AccessDescription:
"""Convert to a :py:class:`~cg:cryptography.x509.AccessDescription` instance."""
return x509.AccessDescription(
access_method=x509.ObjectIdentifier(self.access_method),
access_location=self.access_location.cryptography,
)
[docs]
class AuthorityKeyIdentifierValueModel(CryptographyModel[x509.AuthorityKeyIdentifier]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.AuthorityKeyIdentifier`.
In its by far most common form, this model will just set the `key_identifier` attribute, with a value
based on the certificate authority. This is a base64-encoded string:
>>> AuthorityKeyIdentifierValueModel(key_identifier="MTIz") # doctest: +STRIP_WHITESPACE
AuthorityKeyIdentifierValueModel(
key_identifier='MTIz', authority_cert_issuer=None, authority_cert_serial_number=None
)
You may also pass bytes instead, in which case the value will be base64 encoded:
>>> AuthorityKeyIdentifierValueModel(key_identifier=b"123") # doctest: +STRIP_WHITESPACE
AuthorityKeyIdentifierValueModel(
key_identifier='MTIz', authority_cert_issuer=None, authority_cert_serial_number=None
)
You can also give a `authority_cert_issuer` (a list of
:py:class:`~django_ca.pydantic.general_name.GeneralNameModel`) and an `authority_cert_serial_number`:
>>> AuthorityKeyIdentifierValueModel(
... key_identifier=None,
... authority_cert_issuer=[{'type': 'URI', 'value': 'http://example.com'}],
... authority_cert_serial_number=123
... ) # doctest: +STRIP_WHITESPACE
AuthorityKeyIdentifierValueModel(
key_identifier=None,
authority_cert_issuer=[GeneralNameModel(type='URI', value='http://example.com')],
authority_cert_serial_number=123
)
The restrictions defined in RFC 5280 apply, so `authority_cert_issuer` and `authority_cert_serial_number`
must either be both present or ``None``, and either `key_identifier` and/or `authority_cert_issuer` must
be given.
"""
key_identifier: Base64EncodedBytes | None
authority_cert_issuer: list[GeneralNameModel] | None = None
authority_cert_serial_number: int | None = None
@model_validator(mode="before")
@classmethod
def parse_cryptography(cls, data: Any) -> Any:
"""Parse cryptography instance."""
if isinstance(data, x509.AuthorityKeyIdentifier):
return {
"key_identifier": data.key_identifier,
"authority_cert_issuer": data.authority_cert_issuer,
"authority_cert_serial_number": data.authority_cert_serial_number,
}
return data
@model_validator(mode="after")
# pylint: disable-next=missing-function-docstring
def check_consistency(self) -> "AuthorityKeyIdentifierValueModel":
if (self.authority_cert_issuer is None) != (self.authority_cert_serial_number is None):
raise ValueError(
"authority_cert_issuer and authority_cert_serial_number must both be present or both None"
)
if self.key_identifier is None and self.authority_cert_issuer is None:
raise ValueError(
"At least one of key_identifier or "
"authority_cert_issuer/authority_cert_serial_number must be given."
)
return self
@property
def cryptography(self) -> x509.AuthorityKeyIdentifier:
"""Convert to a :py:class:`~cg:cryptography.x509.AuthorityKeyIdentifier` instance."""
authority_cert_issuer = key_identifier = None
if self.authority_cert_issuer is not None:
authority_cert_issuer = [general_name.cryptography for general_name in self.authority_cert_issuer]
if self.key_identifier is not None:
key_identifier = base64.b64decode(self.key_identifier)
return x509.AuthorityKeyIdentifier(
key_identifier=key_identifier,
authority_cert_issuer=authority_cert_issuer,
authority_cert_serial_number=self.authority_cert_serial_number,
)
[docs]
class BasicConstraintsValueModel(CryptographyModel[x509.BasicConstraints]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.BasicConstraints`.
For a certificate representing a certificate authority, this extension sets `ca` to ``True`` and a path
length, which may be None:
>>> BasicConstraintsValueModel(ca=True, path_length=0)
BasicConstraintsValueModel(ca=True, path_length=0)
For end-entity certificates, this extension sets `ca` to ``False`` and must set `path_length` to
``None``:
>>> BasicConstraintsValueModel(ca=False, path_length=None)
BasicConstraintsValueModel(ca=False, path_length=None)
"""
model_config = ConfigDict(from_attributes=True)
ca: bool
path_length: int | None = Field(ge=0)
@model_validator(mode="after")
# pylint: disable-next=missing-function-docstring
def check_path_length(self) -> "BasicConstraintsValueModel":
if self.ca is False and self.path_length is not None:
raise ValueError("path_length must be None when ca is False")
return self
@property
def cryptography(self) -> x509.BasicConstraints:
"""Convert to a :py:class:`~cg:cryptography.x509.BasicConstraints` instance."""
return x509.BasicConstraints(ca=self.ca, path_length=self.path_length)
[docs]
class DistributionPointModel(CryptographyModel[x509.DistributionPoint]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.DistributionPoint`.
In its by far most common form, this model only has a `full_name` containing a URI:
>>> DistributionPointModel(
... full_name=[{"type": "URI", "value": "https://ca.example.com/crl"}]
... ) # doctest: +STRIP_WHITESPACE
DistributionPointModel(
full_name=[GeneralNameModel(type='URI', value='https://ca.example.com/crl')],
relative_name=None, crl_issuer=None, reasons=None
)
Of course, other fields are also supported:
>>> DistributionPointModel(
... relative_name=[{"oid": "2.5.4.3", "value": "example.com"}],
... crl_issuer=[{"type": "URI", "value": "https://ca.example.com/issuer"}],
... reasons={"key_compromise",}
... ) # doctest: +STRIP_WHITESPACE
DistributionPointModel(
full_name=None,
relative_name=NameModel(root=[NameAttributeModel(oid='2.5.4.3', value='example.com')]),
crl_issuer=[GeneralNameModel(type='URI', value='https://ca.example.com/issuer')],
reasons={'key_compromise'}
)
"""
model_config = ConfigDict(from_attributes=True)
full_name: list[GeneralNameModel] | None = None
relative_name: NameModel | None = None
crl_issuer: list[GeneralNameModel] | None = None
reasons: set[DistributionPointReason] | None = None
@model_validator(mode="before")
@classmethod
def parse_cryptography(cls, data: Any) -> Any: # pylint: disable=missing-function-docstring
if isinstance(data, x509.DistributionPoint):
reasons = None
if data.reasons:
reasons = [reason.name for reason in data.reasons]
return DistributionPointModel(
full_name=data.full_name,
relative_name=data.relative_name,
crl_issuer=data.crl_issuer,
reasons=reasons,
)
return data
@model_validator(mode="after")
# pylint: disable-next=missing-function-docstring
def check_consistency(self) -> "DistributionPointModel":
if self.full_name and self.relative_name:
raise ValueError("must give exactly one of full_name or relative_name.")
if not self.full_name and not self.relative_name and not self.crl_issuer:
raise ValueError("either full_name, relative_name or crl_issuer must be provided.")
return self
@property
def cryptography(self) -> x509.DistributionPoint:
"""Convert to a :py:class:`~cg:cryptography.x509.DistributionPoint` instance."""
full_name = relative_name = crl_issuer = reasons = None
if self.full_name:
full_name = [name.cryptography for name in self.full_name]
elif self.relative_name is not None: # pragma: no branch
relative_name = x509.RelativeDistinguishedName(self.relative_name.cryptography)
if self.crl_issuer:
crl_issuer = [name.cryptography for name in self.crl_issuer]
if self.reasons:
reasons = frozenset(x509.ReasonFlags[reason] for reason in self.reasons)
return x509.DistributionPoint(
full_name=full_name, relative_name=relative_name, crl_issuer=crl_issuer, reasons=reasons
)
[docs]
class IssuingDistributionPointValueModel(CryptographyModel[x509.IssuingDistributionPoint]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.IssuingDistributionPoint`.
>>> full_name = [{"type": "URI", "value": "https://ca.example.com/crl"}]
>>> IssuingDistributionPointValueModel(full_name=full_name) # doctest: +STRIP_WHITESPACE
IssuingDistributionPointValueModel(
only_contains_user_certs=False,
only_contains_ca_certs=False,
indirect_crl=False,
only_contains_attribute_certs=False,
only_some_reasons=None,
full_name=[GeneralNameModel(type='URI', value='https://ca.example.com/crl')],
relative_name=None
)
Note that all attributes default to False or None and can thus be omitted, but at least one parameter
needs to be given.
"""
model_config = ConfigDict(from_attributes=True)
only_contains_user_certs: bool = False
only_contains_ca_certs: bool = False
indirect_crl: bool = False
only_contains_attribute_certs: bool = False
only_some_reasons: set[DistributionPointReason] | None = None
full_name: NonEmptyOrderedSet[list[GeneralNameModel]] | None = None
relative_name: NameModel | None = None
@model_validator(mode="before")
@classmethod
def parse_cryptography(cls, data: Any) -> Any: # pylint: disable=missing-function-docstring
if isinstance(data, x509.IssuingDistributionPoint):
reasons = None
if data.only_some_reasons:
reasons = [reason.name for reason in data.only_some_reasons]
return IssuingDistributionPointValueModel(
only_contains_user_certs=data.only_contains_user_certs,
only_contains_ca_certs=data.only_contains_ca_certs,
indirect_crl=data.indirect_crl,
only_contains_attribute_certs=data.only_contains_attribute_certs,
only_some_reasons=reasons,
full_name=data.full_name,
relative_name=data.relative_name,
)
return data
@model_validator(mode="after")
# pylint: disable-next=missing-function-docstring
def check_consistency(self) -> "IssuingDistributionPointValueModel":
if self.full_name and self.relative_name:
raise ValueError("only one of full_name or relative_name may be True")
crl_constraints = [
self.only_contains_user_certs,
self.only_contains_ca_certs,
self.indirect_crl,
self.only_contains_attribute_certs,
]
if len([x for x in crl_constraints if x]) > 1:
raise ValueError(
"only one can be set: only_contains_user_certs, only_contains_ca_certs, indirect_crl, "
"only_contains_attribute_certs"
)
if not any((*crl_constraints, self.only_some_reasons, self.full_name, self.relative_name)):
raise ValueError("cannot create empty extension")
return self
@property
def cryptography(self) -> x509.IssuingDistributionPoint:
"""Convert to a :py:class:`~cg:cryptography.x509.IssuingDistributionPoint` instance."""
full_name = relative_name = reasons = None
if self.full_name is not None:
full_name = [general_name.cryptography for general_name in self.full_name]
if self.relative_name is not None:
relative_name = x509.RelativeDistinguishedName(self.relative_name.cryptography)
if self.only_some_reasons:
reasons = frozenset(x509.ReasonFlags[reason] for reason in self.only_some_reasons)
return x509.IssuingDistributionPoint(
only_contains_user_certs=self.only_contains_user_certs,
only_contains_ca_certs=self.only_contains_ca_certs,
indirect_crl=self.indirect_crl,
only_contains_attribute_certs=self.only_contains_attribute_certs,
only_some_reasons=reasons,
full_name=full_name,
relative_name=relative_name,
)
[docs]
class PrivateKeyUsagePeriodValueModel(PrivateKeyUsagePeriodValueModelBase):
"""Pydantic model wrapping ``PrivateKeyUsagePeriod``."""
model_config = ConfigDict(from_attributes=True)
not_before: datetime | None = None
not_after: datetime | None = None
[docs]
@model_validator(mode="after")
def validate_values(self) -> Self:
"""Validate that at least one value is set, and order of values."""
if self.not_before is None and self.not_after is None:
raise ValueError("At least one of not_before and not_after must be set.")
if self.not_before and self.not_after and self.not_after <= self.not_before:
raise ValueError("not_after cannot be before not_before.")
return self
@property
def cryptography(self) -> "x509.PrivateKeyUsagePeriod":
return x509.PrivateKeyUsagePeriod(not_before=self.not_before, not_after=self.not_after)
[docs]
class SignedCertificateTimestampModel(CryptographyModel[certificate_transparency.SignedCertificateTimestamp]):
"""Pydantic model wrapping ``SignedCertificateTimestamp``.
.. NOTE::
Due to library limitations, this model cannot be converted to a cryptography class.
>>> SignedCertificateTimestampModel(
... log_id="MTIz", timestamp=datetime(2023, 12, 10), entry_type="precertificate"
... ) # doctest: +STRIP_WHITESPACE
SignedCertificateTimestampModel(
version='v1',
log_id='MTIz',
timestamp=datetime.datetime(2023, 12, 10, 0, 0),
entry_type='precertificate'
)
"""
model_config = ConfigDict(from_attributes=True)
version: Literal["v1"] = "v1"
log_id: Base64EncodedBytes
timestamp: datetime
entry_type: LogEntryTypeName
@model_validator(mode="before")
@classmethod
def parse_cryptography(cls, data: Any) -> Any: # pylint: disable=missing-function-docstring
if isinstance(data, certificate_transparency.SignedCertificateTimestamp):
return {
"version": data.version.name,
"log_id": data.log_id,
"timestamp": data.timestamp,
"entry_type": constants.LOG_ENTRY_TYPE_KEYS[data.entry_type],
}
return data # pragma: no cover
@property
def cryptography(self) -> NoReturn: # # pragma: no cover
"""Will always raise an exception for this class."""
raise ValueError("SignedCertificateTimestamps cannot be loaded as cryptography instances.")
[docs]
class MSCertificateTemplateValueModel(CryptographyModel[x509.MSCertificateTemplate]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.MSCertificateTemplate`.
The `template_id` parameter is a dotted-string object identifier, while `major_version` and
`minor_version` are optional integers:
>>> MSCertificateTemplateValueModel(template_id="1.2.3", major_version=1)
MSCertificateTemplateValueModel(template_id='1.2.3', major_version=1, minor_version=None)
"""
model_config = ConfigDict(from_attributes=True)
template_id: OIDType
major_version: int | None = None
minor_version: int | None = None
@property
def cryptography(self) -> x509.MSCertificateTemplate:
"""Convert to a :py:class:`~cg:cryptography.x509.MSCertificateTemplate` instance."""
return x509.MSCertificateTemplate(
template_id=x509.ObjectIdentifier(self.template_id),
major_version=self.major_version,
minor_version=self.minor_version,
)
[docs]
class NameConstraintsValueModel(CryptographyModel[x509.NameConstraints]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.NameConstraints`.
Both `permitted_subtrees` and `excluded_subtrees` are optional, but at least one of them must be given.
They are a list of :py:class:`~django_ca.pydantic.general_name.GeneralNameModel` instances:
>>> NameConstraintsValueModel(
... permitted_subtrees=[{"type": "DNS", "value": ".com"}]
... ) # doctest: +STRIP_WHITESPACE
NameConstraintsValueModel(
permitted_subtrees=[GeneralNameModel(type='DNS', value='.com')], excluded_subtrees=None
)
"""
model_config = ConfigDict(from_attributes=True)
permitted_subtrees: NonEmptyOrderedSet[list[GeneralNameModel]] | None = None
excluded_subtrees: NonEmptyOrderedSet[list[GeneralNameModel]] | None = None
@model_validator(mode="after")
# pylint: disable-next=missing-function-docstring
def check_at_least_one_given(self) -> "NameConstraintsValueModel":
if self.permitted_subtrees is None and self.excluded_subtrees is None:
raise ValueError("At least one of permitted_subtrees and excluded_subtrees must not be None")
return self
@property
def cryptography(self) -> x509.NameConstraints:
"""Convert to a :py:class:`~cg:cryptography.x509.NameConstraints` instance."""
permitted = excluded = None
if self.permitted_subtrees is not None:
permitted = [name.cryptography for name in self.permitted_subtrees]
if self.excluded_subtrees is not None:
excluded = [name.cryptography for name in self.excluded_subtrees]
return x509.NameConstraints(permitted_subtrees=permitted, excluded_subtrees=excluded)
[docs]
class NoticeReferenceModel(CryptographyModel[x509.NoticeReference]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.NoticeReference`.
>>> NoticeReferenceModel(organization="MyOrg", notice_numbers=[1, 2, 3])
NoticeReferenceModel(organization='MyOrg', notice_numbers=[1, 2, 3])
Note that `organization` is optional.
"""
model_config = ConfigDict(
from_attributes=True, json_schema_extra={"description": _NOTICE_REFERENCE_DESCRIPTION}
)
organization: str | None = None
notice_numbers: list[int]
@property
def cryptography(self) -> x509.NoticeReference:
"""Convert to a :py:class:`~cg:cryptography.x509.NoticeReference` instance."""
return x509.NoticeReference(organization=self.organization, notice_numbers=self.notice_numbers)
[docs]
class PolicyConstraintsValueModel(CryptographyModel[x509.PolicyConstraints]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.PolicyConstraints`.
The `require_explicit_policy` and `inhibit_policy_mapping` are both optional and must be integers if set.
At least one value must be given.
>>> PolicyConstraintsValueModel(require_explicit_policy=0, inhibit_policy_mapping=1)
PolicyConstraintsValueModel(require_explicit_policy=0, inhibit_policy_mapping=1)
"""
model_config = ConfigDict(from_attributes=True)
require_explicit_policy: int | None = Field(ge=0)
inhibit_policy_mapping: int | None = Field(ge=0)
@model_validator(mode="after")
# pylint: disable-next=missing-function-docstring
def check_consistency(self) -> "PolicyConstraintsValueModel":
if self.require_explicit_policy is None and self.inhibit_policy_mapping is None:
raise ValueError(
"At least one of require_explicit_policy and inhibit_policy_mapping must not be None"
)
return self
@property
def cryptography(self) -> x509.PolicyConstraints:
"""Convert to a :py:class:`~cg:cryptography.x509.PolicyConstraints` instance."""
return x509.PolicyConstraints(
require_explicit_policy=self.require_explicit_policy,
inhibit_policy_mapping=self.inhibit_policy_mapping,
)
[docs]
class UserNoticeModel(CryptographyModel[x509.UserNotice]):
"""Pydantic model wrapping :py:class:`~cg:cryptography.x509.UserNotice`.
In its simplest form, the model can just take an explicit text:
>>> UserNoticeModel(explicit_text="my text")
UserNoticeModel(notice_reference=None, explicit_text='my text')
But it may also take notice reference:
>>> ref = NoticeReferenceModel(notice_numbers=[1, 2, 3])
>>> UserNoticeModel(notice_reference=ref, explicit_text="my text") # doctest: +NORMALIZE_WHITESPACE
UserNoticeModel(notice_reference=NoticeReferenceModel(organization=None,
notice_numbers=[1, 2, 3]),
explicit_text='my text')
"""
model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"description": "A UserNoticeModel consists of an *explicit_text* and an optional "
"*notice_reference*."
},
)
notice_reference: NoticeReferenceModel | None = None
explicit_text: str | None
@property
def cryptography(self) -> x509.UserNotice:
"""Convert to a :py:class:`~cg:cryptography.x509.UserNotice` instance."""
notice_reference = None
if self.notice_reference is not None:
notice_reference = self.notice_reference.cryptography
return x509.UserNotice(notice_reference=notice_reference, explicit_text=self.explicit_text)
[docs]
class UnrecognizedExtensionValueModel(CryptographyModel[x509.UnrecognizedExtension]):
"""Pydantic model for a :py:class:`~cg:cryptography.x509.UnrecognizedExtension` extension.
The `value` a base64 encoded string, and the `oid` is any dotted string:
>>> UnrecognizedExtensionValueModel(value="MTIz", oid="1.2.3")
UnrecognizedExtensionValueModel(oid='1.2.3', value='MTIz')
You can also pass bytes to `value`, in which case it will base base64 encoded:
>>> UnrecognizedExtensionValueModel(value=b"123", oid="1.2.3")
UnrecognizedExtensionValueModel(oid='1.2.3', value='MTIz')
"""
oid: OIDType
value: Base64EncodedBytes
@model_validator(mode="before")
@classmethod
def parse_cryptography(cls, data: Any) -> Any:
"""Parse cryptography instances."""
if isinstance(data, x509.UnrecognizedExtension):
return {"oid": data.oid, "value": data.value}
return data
@property
def cryptography(self) -> x509.UnrecognizedExtension:
"""The :py:class:`~cg:cryptography.x509.UnrecognizedExtension` instance."""
value = base64.b64decode(self.value)
return x509.UnrecognizedExtension(value=value, oid=x509.ObjectIdentifier(self.oid))