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

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 ( 

13 x509 as rust_x509, 

14) 

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

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: typing.Union[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: 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") 

134 

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 ) 

151 

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) 

160 

161 if not isinstance(_type, _ASN1Type): 

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

163 

164 self._oid = oid 

165 self._value = value 

166 self._type = _type 

167 

168 @property 

169 def oid(self) -> ObjectIdentifier: 

170 return self._oid 

171 

172 @property 

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

174 return self._value 

175 

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) 

183 

184 def rfc4514_string( 

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

186 ) -> str: 

187 """ 

188 Format as RFC4514 Distinguished Name string. 

189 

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 

198 

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

200 

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

202 if not isinstance(other, NameAttribute): 

203 return NotImplemented 

204 

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

206 

207 def __hash__(self) -> int: 

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

209 

210 def __repr__(self) -> str: 

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

212 

213 

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

221 

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

223 self._attributes = attributes 

224 self._attribute_set = frozenset(attributes) 

225 

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

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

228 

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] 

233 

234 def rfc4514_string( 

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

236 ) -> str: 

237 """ 

238 Format as RFC4514 Distinguished Name string. 

239 

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 ) 

247 

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

249 if not isinstance(other, RelativeDistinguishedName): 

250 return NotImplemented 

251 

252 return self._attribute_set == other._attribute_set 

253 

254 def __hash__(self) -> int: 

255 return hash(self._attribute_set) 

256 

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

258 return iter(self._attributes) 

259 

260 def __len__(self) -> int: 

261 return len(self._attributes) 

262 

263 def __repr__(self) -> str: 

264 return "<RelativeDistinguishedName({})>".format(self.rfc4514_string()) 

265 

266 

267class Name: 

268 @typing.overload 

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

270 ... 

271 

272 @typing.overload 

273 def __init__( 

274 self, attributes: typing.Iterable[RelativeDistinguishedName] 

275 ) -> None: 

276 ... 

277 

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 ) 

299 

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

307 

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' 

314 

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 ) 

325 

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] 

330 

331 @property 

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

333 return self._attributes 

334 

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

336 return rust_x509.encode_name_bytes(self) 

337 

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

339 if not isinstance(other, Name): 

340 return NotImplemented 

341 

342 return self._attributes == other._attributes 

343 

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

348 

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

350 for rdn in self._attributes: 

351 for ava in rdn: 

352 yield ava 

353 

354 def __len__(self) -> int: 

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

356 

357 def __repr__(self) -> str: 

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

359 return "<Name({})>".format(rdns) 

360 

361 

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

365 

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

388 

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

390 self._data = data 

391 self._idx = 0 

392 

393 self._attr_name_overrides = attr_name_overrides 

394 

395 def _has_data(self) -> bool: 

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

397 

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

399 if self._has_data(): 

400 return self._data[self._idx] 

401 return None 

402 

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

404 if self._peek() != ch: 

405 raise ValueError 

406 self._idx += 1 

407 

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 

415 

416 def parse(self) -> Name: 

417 """ 

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

419 

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

426 

427 while self._has_data(): 

428 self._read_char(",") 

429 rdns.append(self._parse_rdn()) 

430 

431 return Name(reversed(rdns)) 

432 

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

438 

439 return RelativeDistinguishedName(nas) 

440 

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) 

453 

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) 

461 

462 return NameAttribute(oid, value)