Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/cryptography/x509/name.py: 39%
232 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:36 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:36 +0000
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
5import binascii
6import re
7import sys
8import typing
9import warnings
11from cryptography import utils
12from cryptography.hazmat.bindings._rust import x509 as rust_x509
13from cryptography.x509.oid import NameOID, ObjectIdentifier
16class _ASN1Type(utils.Enum):
17 BitString = 3
18 OctetString = 4
19 UTF8String = 12
20 NumericString = 18
21 PrintableString = 19
22 T61String = 20
23 IA5String = 22
24 UTCTime = 23
25 GeneralizedTime = 24
26 VisibleString = 26
27 UniversalString = 28
28 BMPString = 30
31_ASN1_TYPE_TO_ENUM = {i.value: i for i in _ASN1Type}
32_NAMEOID_DEFAULT_TYPE: typing.Dict[ObjectIdentifier, _ASN1Type] = {
33 NameOID.COUNTRY_NAME: _ASN1Type.PrintableString,
34 NameOID.JURISDICTION_COUNTRY_NAME: _ASN1Type.PrintableString,
35 NameOID.SERIAL_NUMBER: _ASN1Type.PrintableString,
36 NameOID.DN_QUALIFIER: _ASN1Type.PrintableString,
37 NameOID.EMAIL_ADDRESS: _ASN1Type.IA5String,
38 NameOID.DOMAIN_COMPONENT: _ASN1Type.IA5String,
39}
41# Type alias
42_OidNameMap = typing.Mapping[ObjectIdentifier, str]
43_NameOidMap = typing.Mapping[str, ObjectIdentifier]
45#: Short attribute names from RFC 4514:
46#: https://tools.ietf.org/html/rfc4514#page-7
47_NAMEOID_TO_NAME: _OidNameMap = {
48 NameOID.COMMON_NAME: "CN",
49 NameOID.LOCALITY_NAME: "L",
50 NameOID.STATE_OR_PROVINCE_NAME: "ST",
51 NameOID.ORGANIZATION_NAME: "O",
52 NameOID.ORGANIZATIONAL_UNIT_NAME: "OU",
53 NameOID.COUNTRY_NAME: "C",
54 NameOID.STREET_ADDRESS: "STREET",
55 NameOID.DOMAIN_COMPONENT: "DC",
56 NameOID.USER_ID: "UID",
57}
58_NAME_TO_NAMEOID = {v: k for k, v in _NAMEOID_TO_NAME.items()}
61def _escape_dn_value(val: typing.Union[str, bytes]) -> str:
62 """Escape special characters in RFC4514 Distinguished Name value."""
64 if not val:
65 return ""
67 # RFC 4514 Section 2.4 defines the value as being the # (U+0023) character
68 # followed by the hexadecimal encoding of the octets.
69 if isinstance(val, bytes):
70 return "#" + binascii.hexlify(val).decode("utf8")
72 # See https://tools.ietf.org/html/rfc4514#section-2.4
73 val = val.replace("\\", "\\\\")
74 val = val.replace('"', '\\"')
75 val = val.replace("+", "\\+")
76 val = val.replace(",", "\\,")
77 val = val.replace(";", "\\;")
78 val = val.replace("<", "\\<")
79 val = val.replace(">", "\\>")
80 val = val.replace("\0", "\\00")
82 if val[0] in ("#", " "):
83 val = "\\" + val
84 if val[-1] == " ":
85 val = val[:-1] + "\\ "
87 return val
90def _unescape_dn_value(val: str) -> str:
91 if not val:
92 return ""
94 # See https://tools.ietf.org/html/rfc4514#section-3
96 # special = escaped / SPACE / SHARP / EQUALS
97 # escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE
98 def sub(m):
99 val = m.group(1)
100 # Regular escape
101 if len(val) == 1:
102 return val
103 # Hex-value scape
104 return chr(int(val, 16))
106 return _RFC4514NameParser._PAIR_RE.sub(sub, val)
109class NameAttribute:
110 def __init__(
111 self,
112 oid: ObjectIdentifier,
113 value: typing.Union[str, bytes],
114 _type: typing.Optional[_ASN1Type] = None,
115 *,
116 _validate: bool = True,
117 ) -> None:
118 if not isinstance(oid, ObjectIdentifier):
119 raise TypeError(
120 "oid argument must be an ObjectIdentifier instance."
121 )
122 if _type == _ASN1Type.BitString:
123 if oid != NameOID.X500_UNIQUE_IDENTIFIER:
124 raise TypeError(
125 "oid must be X500_UNIQUE_IDENTIFIER for BitString type."
126 )
127 if not isinstance(value, bytes):
128 raise TypeError("value must be bytes for BitString")
129 else:
130 if not isinstance(value, str):
131 raise TypeError("value argument must be a str")
133 if (
134 oid == NameOID.COUNTRY_NAME
135 or oid == NameOID.JURISDICTION_COUNTRY_NAME
136 ):
137 assert isinstance(value, str)
138 c_len = len(value.encode("utf8"))
139 if c_len != 2 and _validate is True:
140 raise ValueError(
141 "Country name must be a 2 character country code"
142 )
143 elif c_len != 2:
144 warnings.warn(
145 "Country names should be two characters, but the "
146 "attribute is {} characters in length.".format(c_len),
147 stacklevel=2,
148 )
150 # The appropriate ASN1 string type varies by OID and is defined across
151 # multiple RFCs including 2459, 3280, and 5280. In general UTF8String
152 # is preferred (2459), but 3280 and 5280 specify several OIDs with
153 # alternate types. This means when we see the sentinel value we need
154 # to look up whether the OID has a non-UTF8 type. If it does, set it
155 # to that. Otherwise, UTF8!
156 if _type is None:
157 _type = _NAMEOID_DEFAULT_TYPE.get(oid, _ASN1Type.UTF8String)
159 if not isinstance(_type, _ASN1Type):
160 raise TypeError("_type must be from the _ASN1Type enum")
162 self._oid = oid
163 self._value = value
164 self._type = _type
166 @property
167 def oid(self) -> ObjectIdentifier:
168 return self._oid
170 @property
171 def value(self) -> typing.Union[str, bytes]:
172 return self._value
174 @property
175 def rfc4514_attribute_name(self) -> str:
176 """
177 The short attribute name (for example "CN") if available,
178 otherwise the OID dotted string.
179 """
180 return _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string)
182 def rfc4514_string(
183 self, attr_name_overrides: typing.Optional[_OidNameMap] = None
184 ) -> str:
185 """
186 Format as RFC4514 Distinguished Name string.
188 Use short attribute name if available, otherwise fall back to OID
189 dotted string.
190 """
191 attr_name = (
192 attr_name_overrides.get(self.oid) if attr_name_overrides else None
193 )
194 if attr_name is None:
195 attr_name = self.rfc4514_attribute_name
197 return f"{attr_name}={_escape_dn_value(self.value)}"
199 def __eq__(self, other: object) -> bool:
200 if not isinstance(other, NameAttribute):
201 return NotImplemented
203 return self.oid == other.oid and self.value == other.value
205 def __hash__(self) -> int:
206 return hash((self.oid, self.value))
208 def __repr__(self) -> str:
209 return "<NameAttribute(oid={0.oid}, value={0.value!r})>".format(self)
212class RelativeDistinguishedName:
213 def __init__(self, attributes: typing.Iterable[NameAttribute]):
214 attributes = list(attributes)
215 if not attributes:
216 raise ValueError("a relative distinguished name cannot be empty")
217 if not all(isinstance(x, NameAttribute) for x in attributes):
218 raise TypeError("attributes must be an iterable of NameAttribute")
220 # Keep list and frozenset to preserve attribute order where it matters
221 self._attributes = attributes
222 self._attribute_set = frozenset(attributes)
224 if len(self._attribute_set) != len(attributes):
225 raise ValueError("duplicate attributes are not allowed")
227 def get_attributes_for_oid(
228 self, oid: ObjectIdentifier
229 ) -> typing.List[NameAttribute]:
230 return [i for i in self if i.oid == oid]
232 def rfc4514_string(
233 self, attr_name_overrides: typing.Optional[_OidNameMap] = None
234 ) -> str:
235 """
236 Format as RFC4514 Distinguished Name string.
238 Within each RDN, attributes are joined by '+', although that is rarely
239 used in certificates.
240 """
241 return "+".join(
242 attr.rfc4514_string(attr_name_overrides)
243 for attr in self._attributes
244 )
246 def __eq__(self, other: object) -> bool:
247 if not isinstance(other, RelativeDistinguishedName):
248 return NotImplemented
250 return self._attribute_set == other._attribute_set
252 def __hash__(self) -> int:
253 return hash(self._attribute_set)
255 def __iter__(self) -> typing.Iterator[NameAttribute]:
256 return iter(self._attributes)
258 def __len__(self) -> int:
259 return len(self._attributes)
261 def __repr__(self) -> str:
262 return f"<RelativeDistinguishedName({self.rfc4514_string()})>"
265class Name:
266 @typing.overload
267 def __init__(self, attributes: typing.Iterable[NameAttribute]) -> None:
268 ...
270 @typing.overload
271 def __init__(
272 self, attributes: typing.Iterable[RelativeDistinguishedName]
273 ) -> None:
274 ...
276 def __init__(
277 self,
278 attributes: typing.Iterable[
279 typing.Union[NameAttribute, RelativeDistinguishedName]
280 ],
281 ) -> None:
282 attributes = list(attributes)
283 if all(isinstance(x, NameAttribute) for x in attributes):
284 self._attributes = [
285 RelativeDistinguishedName([typing.cast(NameAttribute, x)])
286 for x in attributes
287 ]
288 elif all(isinstance(x, RelativeDistinguishedName) for x in attributes):
289 self._attributes = typing.cast(
290 typing.List[RelativeDistinguishedName], attributes
291 )
292 else:
293 raise TypeError(
294 "attributes must be a list of NameAttribute"
295 " or a list RelativeDistinguishedName"
296 )
298 @classmethod
299 def from_rfc4514_string(
300 cls,
301 data: str,
302 attr_name_overrides: typing.Optional[_NameOidMap] = None,
303 ) -> "Name":
304 return _RFC4514NameParser(data, attr_name_overrides or {}).parse()
306 def rfc4514_string(
307 self, attr_name_overrides: typing.Optional[_OidNameMap] = None
308 ) -> str:
309 """
310 Format as RFC4514 Distinguished Name string.
311 For example 'CN=foobar.com,O=Foo Corp,C=US'
313 An X.509 name is a two-level structure: a list of sets of attributes.
314 Each list element is separated by ',' and within each list element, set
315 elements are separated by '+'. The latter is almost never used in
316 real world certificates. According to RFC4514 section 2.1 the
317 RDNSequence must be reversed when converting to string representation.
318 """
319 return ",".join(
320 attr.rfc4514_string(attr_name_overrides)
321 for attr in reversed(self._attributes)
322 )
324 def get_attributes_for_oid(
325 self, oid: ObjectIdentifier
326 ) -> typing.List[NameAttribute]:
327 return [i for i in self if i.oid == oid]
329 @property
330 def rdns(self) -> typing.List[RelativeDistinguishedName]:
331 return self._attributes
333 def public_bytes(self, backend: typing.Any = None) -> bytes:
334 return rust_x509.encode_name_bytes(self)
336 def __eq__(self, other: object) -> bool:
337 if not isinstance(other, Name):
338 return NotImplemented
340 return self._attributes == other._attributes
342 def __hash__(self) -> int:
343 # TODO: this is relatively expensive, if this looks like a bottleneck
344 # for you, consider optimizing!
345 return hash(tuple(self._attributes))
347 def __iter__(self) -> typing.Iterator[NameAttribute]:
348 for rdn in self._attributes:
349 for ava in rdn:
350 yield ava
352 def __len__(self) -> int:
353 return sum(len(rdn) for rdn in self._attributes)
355 def __repr__(self) -> str:
356 rdns = ",".join(attr.rfc4514_string() for attr in self._attributes)
357 return f"<Name({rdns})>"
360class _RFC4514NameParser:
361 _OID_RE = re.compile(r"(0|([1-9]\d*))(\.(0|([1-9]\d*)))+")
362 _DESCR_RE = re.compile(r"[a-zA-Z][a-zA-Z\d-]*")
364 _PAIR = r"\\([\\ #=\"\+,;<>]|[\da-zA-Z]{2})"
365 _PAIR_RE = re.compile(_PAIR)
366 _LUTF1 = r"[\x01-\x1f\x21\x24-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"
367 _SUTF1 = r"[\x01-\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"
368 _TUTF1 = r"[\x01-\x1F\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"
369 _UTFMB = rf"[\x80-{chr(sys.maxunicode)}]"
370 _LEADCHAR = rf"{_LUTF1}|{_UTFMB}"
371 _STRINGCHAR = rf"{_SUTF1}|{_UTFMB}"
372 _TRAILCHAR = rf"{_TUTF1}|{_UTFMB}"
373 _STRING_RE = re.compile(
374 rf"""
375 (
376 ({_LEADCHAR}|{_PAIR})
377 (
378 ({_STRINGCHAR}|{_PAIR})*
379 ({_TRAILCHAR}|{_PAIR})
380 )?
381 )?
382 """,
383 re.VERBOSE,
384 )
385 _HEXSTRING_RE = re.compile(r"#([\da-zA-Z]{2})+")
387 def __init__(self, data: str, attr_name_overrides: _NameOidMap) -> None:
388 self._data = data
389 self._idx = 0
391 self._attr_name_overrides = attr_name_overrides
393 def _has_data(self) -> bool:
394 return self._idx < len(self._data)
396 def _peek(self) -> typing.Optional[str]:
397 if self._has_data():
398 return self._data[self._idx]
399 return None
401 def _read_char(self, ch: str) -> None:
402 if self._peek() != ch:
403 raise ValueError
404 self._idx += 1
406 def _read_re(self, pat) -> str:
407 match = pat.match(self._data, pos=self._idx)
408 if match is None:
409 raise ValueError
410 val = match.group()
411 self._idx += len(val)
412 return val
414 def parse(self) -> Name:
415 """
416 Parses the `data` string and converts it to a Name.
418 According to RFC4514 section 2.1 the RDNSequence must be
419 reversed when converting to string representation. So, when
420 we parse it, we need to reverse again to get the RDNs on the
421 correct order.
422 """
423 rdns = [self._parse_rdn()]
425 while self._has_data():
426 self._read_char(",")
427 rdns.append(self._parse_rdn())
429 return Name(reversed(rdns))
431 def _parse_rdn(self) -> RelativeDistinguishedName:
432 nas = [self._parse_na()]
433 while self._peek() == "+":
434 self._read_char("+")
435 nas.append(self._parse_na())
437 return RelativeDistinguishedName(nas)
439 def _parse_na(self) -> NameAttribute:
440 try:
441 oid_value = self._read_re(self._OID_RE)
442 except ValueError:
443 name = self._read_re(self._DESCR_RE)
444 oid = self._attr_name_overrides.get(
445 name, _NAME_TO_NAMEOID.get(name)
446 )
447 if oid is None:
448 raise ValueError
449 else:
450 oid = ObjectIdentifier(oid_value)
452 self._read_char("=")
453 if self._peek() == "#":
454 value = self._read_re(self._HEXSTRING_RE)
455 value = binascii.unhexlify(value[1:]).decode()
456 else:
457 raw_value = self._read_re(self._STRING_RE)
458 value = _unescape_dn_value(raw_value)
460 return NameAttribute(oid, value)