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