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

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 

5import binascii 

6import re 

7import sys 

8import typing 

9import warnings 

10 

11from cryptography import utils 

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

13from cryptography.x509.oid import NameOID, ObjectIdentifier 

14 

15 

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 

29 

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} 

40 

41# Type alias 

42_OidNameMap = typing.Mapping[ObjectIdentifier, str] 

43_NameOidMap = typing.Mapping[str, ObjectIdentifier] 

44 

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

59 

60 

61def _escape_dn_value(val: typing.Union[str, bytes]) -> str: 

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

63 

64 if not val: 

65 return "" 

66 

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

71 

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

81 

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

83 val = "\\" + val 

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

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

86 

87 return val 

88 

89 

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

91 if not val: 

92 return "" 

93 

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

95 

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

105 

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

107 

108 

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

132 

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 ) 

149 

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) 

158 

159 if not isinstance(_type, _ASN1Type): 

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

161 

162 self._oid = oid 

163 self._value = value 

164 self._type = _type 

165 

166 @property 

167 def oid(self) -> ObjectIdentifier: 

168 return self._oid 

169 

170 @property 

171 def value(self) -> typing.Union[str, bytes]: 

172 return self._value 

173 

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) 

181 

182 def rfc4514_string( 

183 self, attr_name_overrides: typing.Optional[_OidNameMap] = None 

184 ) -> str: 

185 """ 

186 Format as RFC4514 Distinguished Name string. 

187 

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 

196 

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

198 

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

200 if not isinstance(other, NameAttribute): 

201 return NotImplemented 

202 

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

204 

205 def __hash__(self) -> int: 

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

207 

208 def __repr__(self) -> str: 

209 return "<NameAttribute(oid={0.oid}, value={0.value!r})>".format(self) 

210 

211 

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

219 

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

221 self._attributes = attributes 

222 self._attribute_set = frozenset(attributes) 

223 

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

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

226 

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] 

231 

232 def rfc4514_string( 

233 self, attr_name_overrides: typing.Optional[_OidNameMap] = None 

234 ) -> str: 

235 """ 

236 Format as RFC4514 Distinguished Name string. 

237 

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 ) 

245 

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

247 if not isinstance(other, RelativeDistinguishedName): 

248 return NotImplemented 

249 

250 return self._attribute_set == other._attribute_set 

251 

252 def __hash__(self) -> int: 

253 return hash(self._attribute_set) 

254 

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

256 return iter(self._attributes) 

257 

258 def __len__(self) -> int: 

259 return len(self._attributes) 

260 

261 def __repr__(self) -> str: 

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

263 

264 

265class Name: 

266 @typing.overload 

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

268 ... 

269 

270 @typing.overload 

271 def __init__( 

272 self, attributes: typing.Iterable[RelativeDistinguishedName] 

273 ) -> None: 

274 ... 

275 

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 ) 

297 

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

305 

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' 

312 

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 ) 

323 

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] 

328 

329 @property 

330 def rdns(self) -> typing.List[RelativeDistinguishedName]: 

331 return self._attributes 

332 

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

334 return rust_x509.encode_name_bytes(self) 

335 

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

337 if not isinstance(other, Name): 

338 return NotImplemented 

339 

340 return self._attributes == other._attributes 

341 

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

346 

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

348 for rdn in self._attributes: 

349 for ava in rdn: 

350 yield ava 

351 

352 def __len__(self) -> int: 

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

354 

355 def __repr__(self) -> str: 

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

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

358 

359 

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

363 

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

386 

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

388 self._data = data 

389 self._idx = 0 

390 

391 self._attr_name_overrides = attr_name_overrides 

392 

393 def _has_data(self) -> bool: 

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

395 

396 def _peek(self) -> typing.Optional[str]: 

397 if self._has_data(): 

398 return self._data[self._idx] 

399 return None 

400 

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

402 if self._peek() != ch: 

403 raise ValueError 

404 self._idx += 1 

405 

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 

413 

414 def parse(self) -> Name: 

415 """ 

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

417 

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

424 

425 while self._has_data(): 

426 self._read_char(",") 

427 rdns.append(self._parse_rdn()) 

428 

429 return Name(reversed(rdns)) 

430 

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

436 

437 return RelativeDistinguishedName(nas) 

438 

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) 

451 

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) 

459 

460 return NameAttribute(oid, value)