Source code for pydantic_cryptography.x509.name

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

import base64
from collections.abc import Iterator
from typing import Any, cast, overload

from cryptography import x509
from cryptography.x509.name import _ASN1Type
from cryptography.x509.oid import NameOID
from pydantic import ConfigDict, Field, model_validator
from pydantic_core.core_schema import ValidationInfo

from pydantic_cryptography.base.models import CryptographyModel, CryptographyRootModel
from pydantic_cryptography.base.types import ObjectIdentifierType

_NAME_ATTRIBUTE_OID_DESCRIPTION = "A dotted string representing the OID."
_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."
)

# Sources for OIDs that can be duplicate:
# * https://www.ibm.com/docs/en/ibm-mq/7.5?topic=certificates-distinguished-names - OU and DC
# * multiple_ous cert from the test suite.
#
# WARNING: sync any updates here to model_settings.SettingsModel._check_name().
#: OIDs that can occur multiple times in a certificate
MULTIPLE_OIDS = (NameOID.DOMAIN_COMPONENT, NameOID.ORGANIZATIONAL_UNIT_NAME, NameOID.STREET_ADDRESS)
MULTIPLE_OID_STRINGS = tuple(oid.dotted_string for oid in MULTIPLE_OIDS)


[docs] class NameAttributeModel(CryptographyModel["x509.NameAttribute[str | bytes]"]): """Pydantic model wrapping |NameAttributeRef|. Normal model construction is straight forward: >>> NameAttributeModel(oid="2.5.4.3", value="example.com") NameAttributeModel(oid='2.5.4.3', value='example.com') The constructor will also accept :class:`~cg:cryptography.x509.ObjectIdentifier` objects for `oid`: >>> NameAttributeModel(oid=NameOID.COMMON_NAME, value="example.com") NameAttributeModel(oid='2.5.4.3', value='example.com') For `x509UniqueIdentifier` attributes you have to base64-encode the value: >>> import base64 >>> value = base64.b64encode(b"example.com") >>> NameAttributeModel(oid=NameOID.X500_UNIQUE_IDENTIFIER, value=value) NameAttributeModel(oid='2.5.4.45', value='ZXhhbXBsZS5jb20=') :param str | ~cryptography.x509.ObjectIdentifier oid: The dotted string value of the OID (e.g. "2.5.4.3"). :param str value: The value of the attribute. """ model_config = ConfigDict( from_attributes=True, json_schema_extra={ "description": "A NameAttribute is defined by an object identifier (OID) and a value." }, ) oid: ObjectIdentifierType = 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"}, ) @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 = cast(bytes, data.value) return {"oid": data.oid.dotted_string, "value": base64.b64encode(value).decode("ascii")} return data @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") cn_oid = NameOID.COMMON_NAME.dotted_string if self.oid == cn_oid and not 1 <= len(self.value) <= 64: raise ValueError( f"{cn_oid} length must be >= 1 and <= 64, but it was {len(self.value)}" ) return self @property def cryptography(self) -> "x509.NameAttribute[str | bytes]": """The :py:class:`~cg:cryptography.x509.NameAttribute` instance for this model.""" 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:`~pydantic_cryptography.x509.NameAttributeModel` instances: >>> NameModel([ ... NameAttributeModel(oid="2.5.4.3", value="example.com"), ... ]) NameModel(root=[NameAttributeModel(oid='2.5.4.3', value='example.com')]) :param list[~pydantic_cryptography.x509.NameAttributeModel] root: The name described by this model. """ 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.", }, ) def __iter__(self) -> Iterator[NameAttributeModel]: # type: ignore[override] return iter(self.root) @overload def __getitem__(self, item: int) -> NameAttributeModel: ... @overload def __getitem__(self, item: slice) -> list[NameAttributeModel]: ... def __getitem__(self, item: int | slice) -> NameAttributeModel | list[NameAttributeModel]: return self.root[item] def __len__(self) -> int: return len(self.root) @model_validator(mode="before") @classmethod def parse_cryptography(cls, data: Any, info: ValidationInfo) -> Any: """Validator for parsing :py:class:`~cg:cryptography.x509.Name`.""" if isinstance(data, str): attr_name_overrides = {} if isinstance(info.context, dict): attr_name_overrides = info.context.get("attr_name_overrides", set()) data = x509.Name.from_rfc4514_string(data, attr_name_overrides=attr_name_overrides) if isinstance(data, x509.Name): return list(data) return data @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 = 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_OID_STRINGS: raise ValueError(f"Name attribute of type {oid} must not occur more then once.") seen.add(attr.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])