Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
name.py483 linesDownload Raw Back to x509
1# This file is dual licensed under the terms of the Apache License, Version2# 2.0, and the BSD License. See the LICENSE file in the root of this repository3# for complete details.4 5from __future__ import annotations6 7import binascii8import re9import sys10import typing11import warnings12from collections.abc import Iterable, Iterator13 14from cryptography import utils15from cryptography.hazmat.bindings._rust import x509 as rust_x50916from cryptography.x509.oid import NameOID, ObjectIdentifier17 18 19class _ASN1Type(utils.Enum):20    BitString = 321    OctetString = 422    UTF8String = 1223    NumericString = 1824    PrintableString = 1925    T61String = 2026    IA5String = 2227    UTCTime = 2328    GeneralizedTime = 2429    VisibleString = 2630    UniversalString = 2831    BMPString = 3032 33 34_ASN1_TYPE_TO_ENUM = {i.value: i for i in _ASN1Type}35_NAMEOID_DEFAULT_TYPE: dict[ObjectIdentifier, _ASN1Type] = {36    NameOID.COUNTRY_NAME: _ASN1Type.PrintableString,37    NameOID.JURISDICTION_COUNTRY_NAME: _ASN1Type.PrintableString,38    NameOID.SERIAL_NUMBER: _ASN1Type.PrintableString,39    NameOID.DN_QUALIFIER: _ASN1Type.PrintableString,40    NameOID.EMAIL_ADDRESS: _ASN1Type.IA5String,41    NameOID.DOMAIN_COMPONENT: _ASN1Type.IA5String,42}43 44# Type alias45_OidNameMap = typing.Mapping[ObjectIdentifier, str]46_NameOidMap = typing.Mapping[str, ObjectIdentifier]47 48#: Short attribute names from RFC 4514:49#: https://tools.ietf.org/html/rfc4514#page-750_NAMEOID_TO_NAME: _OidNameMap = {51    NameOID.COMMON_NAME: "CN",52    NameOID.LOCALITY_NAME: "L",53    NameOID.STATE_OR_PROVINCE_NAME: "ST",54    NameOID.ORGANIZATION_NAME: "O",55    NameOID.ORGANIZATIONAL_UNIT_NAME: "OU",56    NameOID.COUNTRY_NAME: "C",57    NameOID.STREET_ADDRESS: "STREET",58    NameOID.DOMAIN_COMPONENT: "DC",59    NameOID.USER_ID: "UID",60}61_NAME_TO_NAMEOID = {v: k for k, v in _NAMEOID_TO_NAME.items()}62 63_NAMEOID_LENGTH_LIMIT = {64    NameOID.COUNTRY_NAME: (2, 2),65    NameOID.JURISDICTION_COUNTRY_NAME: (2, 2),66    NameOID.COMMON_NAME: (1, 64),67}68 69 70def _escape_dn_value(val: str | bytes) -> str:71    """Escape special characters in RFC4514 Distinguished Name value."""72 73    if not val:74        return ""75 76    # RFC 4514 Section 2.4 defines the value as being the # (U+0023) character77    # followed by the hexadecimal encoding of the octets.78    if isinstance(val, bytes):79        return "#" + binascii.hexlify(val).decode("utf8")80 81    # See https://tools.ietf.org/html/rfc4514#section-2.482    val = val.replace("\\", "\\\\")83    val = val.replace('"', '\\"')84    val = val.replace("+", "\\+")85    val = val.replace(",", "\\,")86    val = val.replace(";", "\\;")87    val = val.replace("<", "\\<")88    val = val.replace(">", "\\>")89    val = val.replace("\0", "\\00")90 91    if val[0] == "#" or (val[0] == " " and len(val) > 1):92        val = "\\" + val93    if val[-1] == " ":94        val = val[:-1] + "\\ "95 96    return val97 98 99def _unescape_dn_value(val: str) -> str:100    if not val:101        return ""102 103    # See https://tools.ietf.org/html/rfc4514#section-3104 105    # special = escaped / SPACE / SHARP / EQUALS106    # escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE107    def sub(m):108        val = m.group(0)109        # Special character escape110        if len(val) == 2:111            return val[1:]112 113        # Unicode string of hex114        return binascii.unhexlify(val.replace("\\", "")).decode()115 116    return _RFC4514NameParser._PAIR_MULTI_RE.sub(sub, val)117 118 119NameAttributeValueType = typing.TypeVar(120    "NameAttributeValueType",121    typing.Union[str, bytes],122    str,123    bytes,124    covariant=True,125)126 127 128class NameAttribute(typing.Generic[NameAttributeValueType]):129    def __init__(130        self,131        oid: ObjectIdentifier,132        value: NameAttributeValueType,133        _type: _ASN1Type | None = None,134        *,135        _validate: bool = True,136    ) -> None:137        if not isinstance(oid, ObjectIdentifier):138            raise TypeError(139                "oid argument must be an ObjectIdentifier instance."140            )141        if _type == _ASN1Type.BitString:142            if oid != NameOID.X500_UNIQUE_IDENTIFIER:143                raise TypeError(144                    "oid must be X500_UNIQUE_IDENTIFIER for BitString type."145                )146            if not isinstance(value, bytes):147                raise TypeError("value must be bytes for BitString")148        elif not isinstance(value, str):149            raise TypeError("value argument must be a str")150 151        length_limits = _NAMEOID_LENGTH_LIMIT.get(oid)152        if length_limits is not None:153            min_length, max_length = length_limits154            assert isinstance(value, str)155            c_len = len(value.encode("utf8"))156            if c_len < min_length or c_len > max_length:157                msg = (158                    f"Attribute's length must be >= {min_length} and "159                    f"<= {max_length}, but it was {c_len}"160                )161                if _validate is True:162                    raise ValueError(msg)163                else:164                    warnings.warn(msg, stacklevel=2)165 166        # The appropriate ASN1 string type varies by OID and is defined across167        # multiple RFCs including 2459, 3280, and 5280. In general UTF8String168        # is preferred (2459), but 3280 and 5280 specify several OIDs with169        # alternate types. This means when we see the sentinel value we need170        # to look up whether the OID has a non-UTF8 type. If it does, set it171        # to that. Otherwise, UTF8!172        if _type is None:173            _type = _NAMEOID_DEFAULT_TYPE.get(oid, _ASN1Type.UTF8String)174 175        if not isinstance(_type, _ASN1Type):176            raise TypeError("_type must be from the _ASN1Type enum")177 178        self._oid = oid179        self._value: NameAttributeValueType = value180        self._type: _ASN1Type = _type181 182    @property183    def oid(self) -> ObjectIdentifier:184        return self._oid185 186    @property187    def value(self) -> NameAttributeValueType:188        return self._value189 190    @property191    def rfc4514_attribute_name(self) -> str:192        """193        The short attribute name (for example "CN") if available,194        otherwise the OID dotted string.195        """196        return _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string)197 198    def rfc4514_string(199        self, attr_name_overrides: _OidNameMap | None = None200    ) -> str:201        """202        Format as RFC4514 Distinguished Name string.203 204        Use short attribute name if available, otherwise fall back to OID205        dotted string.206        """207        attr_name = (208            attr_name_overrides.get(self.oid) if attr_name_overrides else None209        )210        if attr_name is None:211            attr_name = self.rfc4514_attribute_name212 213        return f"{attr_name}={_escape_dn_value(self.value)}"214 215    def __eq__(self, other: object) -> bool:216        if not isinstance(other, NameAttribute):217            return NotImplemented218 219        return self.oid == other.oid and self.value == other.value220 221    def __hash__(self) -> int:222        return hash((self.oid, self.value))223 224    def __repr__(self) -> str:225        return f"<NameAttribute(oid={self.oid}, value={self.value!r})>"226 227 228class RelativeDistinguishedName:229    def __init__(self, attributes: Iterable[NameAttribute[str | bytes]]):230        attributes = list(attributes)231        if not attributes:232            raise ValueError("a relative distinguished name cannot be empty")233        if not all(isinstance(x, NameAttribute) for x in attributes):234            raise TypeError("attributes must be an iterable of NameAttribute")235 236        # Keep list and frozenset to preserve attribute order where it matters237        self._attributes = attributes238        self._attribute_set = frozenset(attributes)239 240        if len(self._attribute_set) != len(attributes):241            raise ValueError("duplicate attributes are not allowed")242 243    def get_attributes_for_oid(244        self,245        oid: ObjectIdentifier,246    ) -> list[NameAttribute[str | bytes]]:247        return [i for i in self if i.oid == oid]248 249    def rfc4514_string(250        self, attr_name_overrides: _OidNameMap | None = None251    ) -> str:252        """253        Format as RFC4514 Distinguished Name string.254 255        Within each RDN, attributes are joined by '+', although that is rarely256        used in certificates.257        """258        return "+".join(259            attr.rfc4514_string(attr_name_overrides)260            for attr in self._attributes261        )262 263    def __eq__(self, other: object) -> bool:264        if not isinstance(other, RelativeDistinguishedName):265            return NotImplemented266 267        return self._attribute_set == other._attribute_set268 269    def __hash__(self) -> int:270        return hash(self._attribute_set)271 272    def __iter__(self) -> Iterator[NameAttribute[str | bytes]]:273        return iter(self._attributes)274 275    def __len__(self) -> int:276        return len(self._attributes)277 278    def __repr__(self) -> str:279        return f"<RelativeDistinguishedName({self.rfc4514_string()})>"280 281 282class Name:283    @typing.overload284    def __init__(285        self, attributes: Iterable[NameAttribute[str | bytes]]286    ) -> None: ...287 288    @typing.overload289    def __init__(290        self, attributes: Iterable[RelativeDistinguishedName]291    ) -> None: ...292 293    def __init__(294        self,295        attributes: Iterable[296            NameAttribute[str | bytes] | RelativeDistinguishedName297        ],298    ) -> None:299        attributes = list(attributes)300        if all(isinstance(x, NameAttribute) for x in attributes):301            self._attributes = [302                RelativeDistinguishedName([typing.cast(NameAttribute, x)])303                for x in attributes304            ]305        elif all(isinstance(x, RelativeDistinguishedName) for x in attributes):306            self._attributes = typing.cast(307                list[RelativeDistinguishedName], attributes308            )309        else:310            raise TypeError(311                "attributes must be a list of NameAttribute"312                " or a list RelativeDistinguishedName"313            )314 315    @classmethod316    def from_rfc4514_string(317        cls,318        data: str,319        attr_name_overrides: _NameOidMap | None = None,320    ) -> Name:321        return _RFC4514NameParser(data, attr_name_overrides or {}).parse()322 323    def rfc4514_string(324        self, attr_name_overrides: _OidNameMap | None = None325    ) -> str:326        """327        Format as RFC4514 Distinguished Name string.328        For example 'CN=foobar.com,O=Foo Corp,C=US'329 330        An X.509 name is a two-level structure: a list of sets of attributes.331        Each list element is separated by ',' and within each list element, set332        elements are separated by '+'. The latter is almost never used in333        real world certificates. According to RFC4514 section 2.1 the334        RDNSequence must be reversed when converting to string representation.335        """336        return ",".join(337            attr.rfc4514_string(attr_name_overrides)338            for attr in reversed(self._attributes)339        )340 341    def get_attributes_for_oid(342        self,343        oid: ObjectIdentifier,344    ) -> list[NameAttribute[str | bytes]]:345        return [i for i in self if i.oid == oid]346 347    @property348    def rdns(self) -> list[RelativeDistinguishedName]:349        return self._attributes350 351    def public_bytes(self, backend: typing.Any = None) -> bytes:352        return rust_x509.encode_name_bytes(self)353 354    def __eq__(self, other: object) -> bool:355        if not isinstance(other, Name):356            return NotImplemented357 358        return self._attributes == other._attributes359 360    def __hash__(self) -> int:361        # TODO: this is relatively expensive, if this looks like a bottleneck362        # for you, consider optimizing!363        return hash(tuple(self._attributes))364 365    def __iter__(self) -> Iterator[NameAttribute[str | bytes]]:366        for rdn in self._attributes:367            yield from rdn368 369    def __len__(self) -> int:370        return sum(len(rdn) for rdn in self._attributes)371 372    def __repr__(self) -> str:373        return f"<Name({self.rfc4514_string()})>"374 375 376class _RFC4514NameParser:377    _OID_RE = re.compile(r"(0|([1-9]\d*))(\.(0|([1-9]\d*)))+")378    _DESCR_RE = re.compile(r"[a-zA-Z][a-zA-Z\d-]*")379 380    _ESCAPE_SPECIAL = r"[\\ #=\"\+,;<>]"381    _ESCAPE_HEX = r"[\da-zA-Z]{2}"382    _PAIR = rf"\\({_ESCAPE_SPECIAL}|{_ESCAPE_HEX})"383    _PAIR_MULTI_RE = re.compile(rf"(\\{_ESCAPE_SPECIAL})|((\\{_ESCAPE_HEX})+)")384    _LUTF1 = r"[\x01-\x1f\x21\x24-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"385    _SUTF1 = r"[\x01-\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"386    _TUTF1 = r"[\x01-\x1F\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"387    _UTFMB = rf"[\x80-{chr(sys.maxunicode)}]"388    _LEADCHAR = rf"{_LUTF1}|{_UTFMB}"389    _STRINGCHAR = rf"{_SUTF1}|{_UTFMB}"390    _TRAILCHAR = rf"{_TUTF1}|{_UTFMB}"391    _STRING_RE = re.compile(392        rf"""393        (394            ({_LEADCHAR}|{_PAIR})395            (396                ({_STRINGCHAR}|{_PAIR})*397                ({_TRAILCHAR}|{_PAIR})398            )?399        )?400        """,401        re.VERBOSE,402    )403    _HEXSTRING_RE = re.compile(r"#([\da-zA-Z]{2})+")404 405    def __init__(self, data: str, attr_name_overrides: _NameOidMap) -> None:406        self._data = data407        self._idx = 0408 409        self._attr_name_overrides = attr_name_overrides410 411    def _has_data(self) -> bool:412        return self._idx < len(self._data)413 414    def _peek(self) -> str | None:415        if self._has_data():416            return self._data[self._idx]417        return None418 419    def _read_char(self, ch: str) -> None:420        if self._peek() != ch:421            raise ValueError422        self._idx += 1423 424    def _read_re(self, pat) -> str:425        match = pat.match(self._data, pos=self._idx)426        if match is None:427            raise ValueError428        val = match.group()429        self._idx += len(val)430        return val431 432    def parse(self) -> Name:433        """434        Parses the `data` string and converts it to a Name.435 436        According to RFC4514 section 2.1 the RDNSequence must be437        reversed when converting to string representation. So, when438        we parse it, we need to reverse again to get the RDNs on the439        correct order.440        """441 442        if not self._has_data():443            return Name([])444 445        rdns = [self._parse_rdn()]446 447        while self._has_data():448            self._read_char(",")449            rdns.append(self._parse_rdn())450 451        return Name(reversed(rdns))452 453    def _parse_rdn(self) -> RelativeDistinguishedName:454        nas = [self._parse_na()]455        while self._peek() == "+":456            self._read_char("+")457            nas.append(self._parse_na())458 459        return RelativeDistinguishedName(nas)460 461    def _parse_na(self) -> NameAttribute[str]:462        try:463            oid_value = self._read_re(self._OID_RE)464        except ValueError:465            name = self._read_re(self._DESCR_RE)466            oid = self._attr_name_overrides.get(467                name, _NAME_TO_NAMEOID.get(name)468            )469            if oid is None:470                raise ValueError471        else:472            oid = ObjectIdentifier(oid_value)473 474        self._read_char("=")475        if self._peek() == "#":476            value = self._read_re(self._HEXSTRING_RE)477            value = binascii.unhexlify(value[1:]).decode()478        else:479            raw_value = self._read_re(self._STRING_RE)480            value = _unescape_dn_value(raw_value)481 482        return NameAttribute(oid, value)483 
codekingpro/portable-devtools · Team Ai