Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/cryptography/x509/name.py: 39%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

232 statements  

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. 

4 

5from __future__ import annotations 

6 

7import binascii 

8import re 

9import sys 

10import typing 

11import warnings 

12 

13from cryptography import utils 

14from cryptography.hazmat.bindings._rust import x509 as rust_x509 

15from cryptography.x509.oid import NameOID, ObjectIdentifier 

16 

17 

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 

31 

32 

33_ASN1_TYPE_TO_ENUM = {i.value: i for i in _ASN1Type} 

34_NAMEOID_DEFAULT_TYPE: 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} 

42 

43# Type alias 

44_OidNameMap = typing.Mapping[ObjectIdentifier, str] 

45_NameOidMap = typing.Mapping[str, ObjectIdentifier] 

46 

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()} 

61 

62 

63def _escape_dn_value(val: str | bytes) -> str: 

64 """Escape special characters in RFC4514 Distinguished Name value.""" 

65 

66 if not val: 

67 return "" 

68 

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") 

73 

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") 

83 

84 if val[0] in ("#", " "): 

85 val = "\\" + val 

86 if val[-1] == " ": 

87 val = val[:-1] + "\\ " 

88 

89 return val 

90 

91 

92def _unescape_dn_value(val: str) -> str: 

93 if not val: 

94 return "" 

95 

96 # See https://tools.ietf.org/html/rfc4514#section-3 

97 

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)) 

107 

108 return _RFC4514NameParser._PAIR_RE.sub(sub, val) 

109 

110 

111class NameAttribute: 

112 def __init__( 

113 self, 

114 oid: ObjectIdentifier, 

115 value: str | bytes, 

116 _type: _ASN1Type | None = 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") 

134 

135 if oid in (NameOID.COUNTRY_NAME, NameOID.JURISDICTION_COUNTRY_NAME): 

136 assert isinstance(value, str) 

137 c_len = len(value.encode("utf8")) 

138 if c_len != 2 and _validate is True: 

139 raise ValueError( 

140 "Country name must be a 2 character country code" 

141 ) 

142 elif c_len != 2: 

143 warnings.warn( 

144 "Country names should be two characters, but the " 

145 f"attribute is {c_len} characters in length.", 

146 stacklevel=2, 

147 ) 

148 

149 # The appropriate ASN1 string type varies by OID and is defined across 

150 # multiple RFCs including 2459, 3280, and 5280. In general UTF8String 

151 # is preferred (2459), but 3280 and 5280 specify several OIDs with 

152 # alternate types. This means when we see the sentinel value we need 

153 # to look up whether the OID has a non-UTF8 type. If it does, set it 

154 # to that. Otherwise, UTF8! 

155 if _type is None: 

156 _type = _NAMEOID_DEFAULT_TYPE.get(oid, _ASN1Type.UTF8String) 

157 

158 if not isinstance(_type, _ASN1Type): 

159 raise TypeError("_type must be from the _ASN1Type enum") 

160 

161 self._oid = oid 

162 self._value = value 

163 self._type = _type 

164 

165 @property 

166 def oid(self) -> ObjectIdentifier: 

167 return self._oid 

168 

169 @property 

170 def value(self) -> str | bytes: 

171 return self._value 

172 

173 @property 

174 def rfc4514_attribute_name(self) -> str: 

175 """ 

176 The short attribute name (for example "CN") if available, 

177 otherwise the OID dotted string. 

178 """ 

179 return _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string) 

180 

181 def rfc4514_string( 

182 self, attr_name_overrides: _OidNameMap | None = None 

183 ) -> str: 

184 """ 

185 Format as RFC4514 Distinguished Name string. 

186 

187 Use short attribute name if available, otherwise fall back to OID 

188 dotted string. 

189 """ 

190 attr_name = ( 

191 attr_name_overrides.get(self.oid) if attr_name_overrides else None 

192 ) 

193 if attr_name is None: 

194 attr_name = self.rfc4514_attribute_name 

195 

196 return f"{attr_name}={_escape_dn_value(self.value)}" 

197 

198 def __eq__(self, other: object) -> bool: 

199 if not isinstance(other, NameAttribute): 

200 return NotImplemented 

201 

202 return self.oid == other.oid and self.value == other.value 

203 

204 def __hash__(self) -> int: 

205 return hash((self.oid, self.value)) 

206 

207 def __repr__(self) -> str: 

208 return f"<NameAttribute(oid={self.oid}, value={self.value!r})>" 

209 

210 

211class RelativeDistinguishedName: 

212 def __init__(self, attributes: typing.Iterable[NameAttribute]): 

213 attributes = list(attributes) 

214 if not attributes: 

215 raise ValueError("a relative distinguished name cannot be empty") 

216 if not all(isinstance(x, NameAttribute) for x in attributes): 

217 raise TypeError("attributes must be an iterable of NameAttribute") 

218 

219 # Keep list and frozenset to preserve attribute order where it matters 

220 self._attributes = attributes 

221 self._attribute_set = frozenset(attributes) 

222 

223 if len(self._attribute_set) != len(attributes): 

224 raise ValueError("duplicate attributes are not allowed") 

225 

226 def get_attributes_for_oid( 

227 self, oid: ObjectIdentifier 

228 ) -> list[NameAttribute]: 

229 return [i for i in self if i.oid == oid] 

230 

231 def rfc4514_string( 

232 self, attr_name_overrides: _OidNameMap | None = None 

233 ) -> str: 

234 """ 

235 Format as RFC4514 Distinguished Name string. 

236 

237 Within each RDN, attributes are joined by '+', although that is rarely 

238 used in certificates. 

239 """ 

240 return "+".join( 

241 attr.rfc4514_string(attr_name_overrides) 

242 for attr in self._attributes 

243 ) 

244 

245 def __eq__(self, other: object) -> bool: 

246 if not isinstance(other, RelativeDistinguishedName): 

247 return NotImplemented 

248 

249 return self._attribute_set == other._attribute_set 

250 

251 def __hash__(self) -> int: 

252 return hash(self._attribute_set) 

253 

254 def __iter__(self) -> typing.Iterator[NameAttribute]: 

255 return iter(self._attributes) 

256 

257 def __len__(self) -> int: 

258 return len(self._attributes) 

259 

260 def __repr__(self) -> str: 

261 return f"<RelativeDistinguishedName({self.rfc4514_string()})>" 

262 

263 

264class Name: 

265 @typing.overload 

266 def __init__(self, attributes: typing.Iterable[NameAttribute]) -> None: 

267 ... 

268 

269 @typing.overload 

270 def __init__( 

271 self, attributes: typing.Iterable[RelativeDistinguishedName] 

272 ) -> None: 

273 ... 

274 

275 def __init__( 

276 self, 

277 attributes: typing.Iterable[NameAttribute | RelativeDistinguishedName], 

278 ) -> None: 

279 attributes = list(attributes) 

280 if all(isinstance(x, NameAttribute) for x in attributes): 

281 self._attributes = [ 

282 RelativeDistinguishedName([typing.cast(NameAttribute, x)]) 

283 for x in attributes 

284 ] 

285 elif all(isinstance(x, RelativeDistinguishedName) for x in attributes): 

286 self._attributes = typing.cast( 

287 typing.List[RelativeDistinguishedName], attributes 

288 ) 

289 else: 

290 raise TypeError( 

291 "attributes must be a list of NameAttribute" 

292 " or a list RelativeDistinguishedName" 

293 ) 

294 

295 @classmethod 

296 def from_rfc4514_string( 

297 cls, 

298 data: str, 

299 attr_name_overrides: _NameOidMap | None = None, 

300 ) -> Name: 

301 return _RFC4514NameParser(data, attr_name_overrides or {}).parse() 

302 

303 def rfc4514_string( 

304 self, attr_name_overrides: _OidNameMap | None = None 

305 ) -> str: 

306 """ 

307 Format as RFC4514 Distinguished Name string. 

308 For example 'CN=foobar.com,O=Foo Corp,C=US' 

309 

310 An X.509 name is a two-level structure: a list of sets of attributes. 

311 Each list element is separated by ',' and within each list element, set 

312 elements are separated by '+'. The latter is almost never used in 

313 real world certificates. According to RFC4514 section 2.1 the 

314 RDNSequence must be reversed when converting to string representation. 

315 """ 

316 return ",".join( 

317 attr.rfc4514_string(attr_name_overrides) 

318 for attr in reversed(self._attributes) 

319 ) 

320 

321 def get_attributes_for_oid( 

322 self, oid: ObjectIdentifier 

323 ) -> list[NameAttribute]: 

324 return [i for i in self if i.oid == oid] 

325 

326 @property 

327 def rdns(self) -> list[RelativeDistinguishedName]: 

328 return self._attributes 

329 

330 def public_bytes(self, backend: typing.Any = None) -> bytes: 

331 return rust_x509.encode_name_bytes(self) 

332 

333 def __eq__(self, other: object) -> bool: 

334 if not isinstance(other, Name): 

335 return NotImplemented 

336 

337 return self._attributes == other._attributes 

338 

339 def __hash__(self) -> int: 

340 # TODO: this is relatively expensive, if this looks like a bottleneck 

341 # for you, consider optimizing! 

342 return hash(tuple(self._attributes)) 

343 

344 def __iter__(self) -> typing.Iterator[NameAttribute]: 

345 for rdn in self._attributes: 

346 yield from rdn 

347 

348 def __len__(self) -> int: 

349 return sum(len(rdn) for rdn in self._attributes) 

350 

351 def __repr__(self) -> str: 

352 rdns = ",".join(attr.rfc4514_string() for attr in self._attributes) 

353 return f"<Name({rdns})>" 

354 

355 

356class _RFC4514NameParser: 

357 _OID_RE = re.compile(r"(0|([1-9]\d*))(\.(0|([1-9]\d*)))+") 

358 _DESCR_RE = re.compile(r"[a-zA-Z][a-zA-Z\d-]*") 

359 

360 _PAIR = r"\\([\\ #=\"\+,;<>]|[\da-zA-Z]{2})" 

361 _PAIR_RE = re.compile(_PAIR) 

362 _LUTF1 = r"[\x01-\x1f\x21\x24-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]" 

363 _SUTF1 = r"[\x01-\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]" 

364 _TUTF1 = r"[\x01-\x1F\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]" 

365 _UTFMB = rf"[\x80-{chr(sys.maxunicode)}]" 

366 _LEADCHAR = rf"{_LUTF1}|{_UTFMB}" 

367 _STRINGCHAR = rf"{_SUTF1}|{_UTFMB}" 

368 _TRAILCHAR = rf"{_TUTF1}|{_UTFMB}" 

369 _STRING_RE = re.compile( 

370 rf""" 

371 ( 

372 ({_LEADCHAR}|{_PAIR}) 

373 ( 

374 ({_STRINGCHAR}|{_PAIR})* 

375 ({_TRAILCHAR}|{_PAIR}) 

376 )? 

377 )? 

378 """, 

379 re.VERBOSE, 

380 ) 

381 _HEXSTRING_RE = re.compile(r"#([\da-zA-Z]{2})+") 

382 

383 def __init__(self, data: str, attr_name_overrides: _NameOidMap) -> None: 

384 self._data = data 

385 self._idx = 0 

386 

387 self._attr_name_overrides = attr_name_overrides 

388 

389 def _has_data(self) -> bool: 

390 return self._idx < len(self._data) 

391 

392 def _peek(self) -> str | None: 

393 if self._has_data(): 

394 return self._data[self._idx] 

395 return None 

396 

397 def _read_char(self, ch: str) -> None: 

398 if self._peek() != ch: 

399 raise ValueError 

400 self._idx += 1 

401 

402 def _read_re(self, pat) -> str: 

403 match = pat.match(self._data, pos=self._idx) 

404 if match is None: 

405 raise ValueError 

406 val = match.group() 

407 self._idx += len(val) 

408 return val 

409 

410 def parse(self) -> Name: 

411 """ 

412 Parses the `data` string and converts it to a Name. 

413 

414 According to RFC4514 section 2.1 the RDNSequence must be 

415 reversed when converting to string representation. So, when 

416 we parse it, we need to reverse again to get the RDNs on the 

417 correct order. 

418 """ 

419 rdns = [self._parse_rdn()] 

420 

421 while self._has_data(): 

422 self._read_char(",") 

423 rdns.append(self._parse_rdn()) 

424 

425 return Name(reversed(rdns)) 

426 

427 def _parse_rdn(self) -> RelativeDistinguishedName: 

428 nas = [self._parse_na()] 

429 while self._peek() == "+": 

430 self._read_char("+") 

431 nas.append(self._parse_na()) 

432 

433 return RelativeDistinguishedName(nas) 

434 

435 def _parse_na(self) -> NameAttribute: 

436 try: 

437 oid_value = self._read_re(self._OID_RE) 

438 except ValueError: 

439 name = self._read_re(self._DESCR_RE) 

440 oid = self._attr_name_overrides.get( 

441 name, _NAME_TO_NAMEOID.get(name) 

442 ) 

443 if oid is None: 

444 raise ValueError 

445 else: 

446 oid = ObjectIdentifier(oid_value) 

447 

448 self._read_char("=") 

449 if self._peek() == "#": 

450 value = self._read_re(self._HEXSTRING_RE) 

451 value = binascii.unhexlify(value[1:]).decode() 

452 else: 

453 raw_value = self._read_re(self._STRING_RE) 

454 value = _unescape_dn_value(raw_value) 

455 

456 return NameAttribute(oid, value)