Source code for django_ca.pydantic.name

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

"""Model for x509.Name."""

import base64
import typing
from typing import Any, List

from pydantic import BeforeValidator, ConfigDict, Field, model_validator

from cryptography import x509
from cryptography.x509.name import _ASN1Type
from cryptography.x509.oid import NameOID

from django_ca import constants
from django_ca.pydantic import validators
from django_ca.pydantic.base import CryptographyModel, CryptographyRootModel
from django_ca.pydantic.type_aliases import OIDType
from django_ca.typehints import Annotated
from django_ca.utils import MULTIPLE_OIDS

_NAME_ATTRIBUTE_OID_DESCRIPTION = (
    "A dotted string representing the OID or a known alias as described in "
    "[NAME_OID_TYPES]"
    "(https://django-ca.readthedocs.io/en/latest/python/constants.html#django_ca.constants.NAME_OID_TYPES)."
)
_NAME_ATTRIBUTE_VALUE_DESCRIPTION = (
    "Actual value of the attribute. For x500 unique identifiers (OID "
    f"{NameOID.X500_UNIQUE_IDENTIFIER.dotted_string}) the value must be the base64 encoded."
)


[docs] class NameAttributeModel(CryptographyModel[x509.NameAttribute]): """Pydantic model wrapping :py:class:`~cg:cryptography.x509.NameAttribute`. >>> from cryptography.x509.oid import NameOID >>> NameAttributeModel(oid=NameOID.COMMON_NAME.dotted_string, value="example.com") NameAttributeModel(oid='2.5.4.3', value='example.com') When processing a x500 unique identifier attribute, the value is expected to be base64 encoded: >>> import base64 >>> value = base64.b64encode(b"example.com") >>> NameAttributeModel(oid=NameOID.X500_UNIQUE_IDENTIFIER.dotted_string, value=value) NameAttributeModel(oid='2.5.4.45', value='ZXhhbXBsZS5jb20=') """ model_config = ConfigDict( from_attributes=True, json_schema_extra={ "description": "A NameAttribute is defined by an object identifier (OID) and a value." }, ) oid: Annotated[OIDType, BeforeValidator(validators.name_oid_parser)] = Field( title="Object identifier", description=_NAME_ATTRIBUTE_OID_DESCRIPTION, json_schema_extra={"example": NameOID.COMMON_NAME.dotted_string}, ) value: str = Field( description=_NAME_ATTRIBUTE_VALUE_DESCRIPTION, json_schema_extra={"example": "example.com"}, )
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Validator to handle x500 unique identifiers.""" if isinstance(data, x509.NameAttribute) and data.oid == NameOID.X500_UNIQUE_IDENTIFIER: value = typing.cast(bytes, data.value) return {"oid": data.oid.dotted_string, "value": base64.b64encode(value).decode("ascii")} return data
[docs] @model_validator(mode="after") def validate_name_attribute(self) -> "NameAttributeModel": """Validate that country code OIDs have exactly two characters.""" country_code_oids = ( NameOID.COUNTRY_NAME.dotted_string, NameOID.JURISDICTION_COUNTRY_NAME.dotted_string, ) if self.oid in country_code_oids and len(self.value) != 2: raise ValueError(f"{self.value}: Must have exactly two characters") if self.oid == NameOID.COMMON_NAME.dotted_string and not self.value: name = constants.NAME_OID_NAMES[NameOID.COMMON_NAME] raise ValueError(f"{name} must not be an empty value") return self
@property def cryptography(self) -> x509.NameAttribute: """The :py:class:`~cg:cryptography.x509.NameAttribute` instance for this model. >>> NameAttributeModel(oid='2.5.4.3', value="example.com").cryptography <NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='example.com')> """ oid = x509.ObjectIdentifier(self.oid) if oid == NameOID.X500_UNIQUE_IDENTIFIER: value = base64.b64decode(self.value) return x509.NameAttribute(oid=oid, value=value, _type=_ASN1Type.BitString) return x509.NameAttribute(oid=oid, value=self.value)
[docs] class NameModel(CryptographyRootModel[List[NameAttributeModel], x509.Name]): """Pydantic model wrapping :py:class:`~cg:cryptography.x509.Name`. This model is a Pydantic :py:class:`~pydantic.root_model.RootModel` that takes a list of :py:class:`~django_ca.pydantic.name.NameAttributeModel` instances: >>> NameModel( ... [ ... {'oid': '2.5.4.6', 'value': 'AT'}, ... {'oid': '2.5.4.3', 'value': 'example.com'} ... ] ... ) # doctest: +STRIP_WHITESPACE NameModel(root=[ NameAttributeModel(oid='2.5.4.6', value='AT'), NameAttributeModel(oid='2.5.4.3', value='example.com') ]) """ root: List[NameAttributeModel] = Field( json_schema_extra={ "format": "X.501 Name", "example": [ {"oid": NameOID.COUNTRY_NAME.dotted_string, "value": "AT"}, {"oid": NameOID.COMMON_NAME.dotted_string, "value": "example.com"}, ], "description": "A Name is composed of a list of name attributes.", }, )
[docs] @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any) -> Any: """Validator for parsing :py:class:`~cg:cryptography.x509.Name`.""" if isinstance(data, x509.Name): return list(data) return data
[docs] @model_validator(mode="after") def validate_duplicates(self) -> "NameModel": """Validator to make sure that OIDs do not occur multiple times.""" seen = set() # for oid in set(oids): for attr in self.root: oid = x509.ObjectIdentifier(attr.oid) # Check if any fields are duplicate where this is not allowed (e.g. multiple CommonName fields) if oid in seen and oid not in MULTIPLE_OIDS: name = constants.NAME_OID_NAMES.get(oid, oid.dotted_string) raise ValueError(f"attribute of type {name} must not occur more then once in a name.") seen.add(oid) return self
@property def cryptography(self) -> x509.Name: """The :py:class:`~cg:cryptography.x509.Name` instance for this model.""" return x509.Name([attr.cryptography for attr in self.root])