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 absolute_import, division, print_function 6 7from enum import Enum 8 9import six 10 11from cryptography import utils 12from cryptography.hazmat.backends import _get_backend 13from cryptography.x509.oid import NameOID, ObjectIdentifier 14 15 16class _ASN1Type(Enum): 17 UTF8String = 12 18 NumericString = 18 19 PrintableString = 19 20 T61String = 20 21 IA5String = 22 22 UTCTime = 23 23 GeneralizedTime = 24 24 VisibleString = 26 25 UniversalString = 28 26 BMPString = 30 27 28 29_ASN1_TYPE_TO_ENUM = {i.value: i for i in _ASN1Type} 30_SENTINEL = object() 31_NAMEOID_DEFAULT_TYPE = { 32 NameOID.COUNTRY_NAME: _ASN1Type.PrintableString, 33 NameOID.JURISDICTION_COUNTRY_NAME: _ASN1Type.PrintableString, 34 NameOID.SERIAL_NUMBER: _ASN1Type.PrintableString, 35 NameOID.DN_QUALIFIER: _ASN1Type.PrintableString, 36 NameOID.EMAIL_ADDRESS: _ASN1Type.IA5String, 37 NameOID.DOMAIN_COMPONENT: _ASN1Type.IA5String, 38} 39 40#: Short attribute names from RFC 4514: 41#: https://tools.ietf.org/html/rfc4514#page-7 42_NAMEOID_TO_NAME = { 43 NameOID.COMMON_NAME: "CN", 44 NameOID.LOCALITY_NAME: "L", 45 NameOID.STATE_OR_PROVINCE_NAME: "ST", 46 NameOID.ORGANIZATION_NAME: "O", 47 NameOID.ORGANIZATIONAL_UNIT_NAME: "OU", 48 NameOID.COUNTRY_NAME: "C", 49 NameOID.STREET_ADDRESS: "STREET", 50 NameOID.DOMAIN_COMPONENT: "DC", 51 NameOID.USER_ID: "UID", 52} 53 54 55def _escape_dn_value(val): 56 """Escape special characters in RFC4514 Distinguished Name value.""" 57 58 if not val: 59 return "" 60 61 # See https://tools.ietf.org/html/rfc4514#section-2.4 62 val = val.replace("\\", "\\\\") 63 val = val.replace('"', '\\"') 64 val = val.replace("+", "\\+") 65 val = val.replace(",", "\\,") 66 val = val.replace(";", "\\;") 67 val = val.replace("<", "\\<") 68 val = val.replace(">", "\\>") 69 val = val.replace("\0", "\\00") 70 71 if val[0] in ("#", " "): 72 val = "\\" + val 73 if val[-1] == " ": 74 val = val[:-1] + "\\ " 75 76 return val 77 78 79class NameAttribute(object): 80 def __init__(self, oid, value, _type=_SENTINEL): 81 if not isinstance(oid, ObjectIdentifier): 82 raise TypeError( 83 "oid argument must be an ObjectIdentifier instance." 84 ) 85 86 if not isinstance(value, six.text_type): 87 raise TypeError("value argument must be a text type.") 88 89 if ( 90 oid == NameOID.COUNTRY_NAME 91 or oid == NameOID.JURISDICTION_COUNTRY_NAME 92 ): 93 if len(value.encode("utf8")) != 2: 94 raise ValueError( 95 "Country name must be a 2 character country code" 96 ) 97 98 # The appropriate ASN1 string type varies by OID and is defined across 99 # multiple RFCs including 2459, 3280, and 5280. In general UTF8String 100 # is preferred (2459), but 3280 and 5280 specify several OIDs with 101 # alternate types. This means when we see the sentinel value we need 102 # to look up whether the OID has a non-UTF8 type. If it does, set it 103 # to that. Otherwise, UTF8! 104 if _type == _SENTINEL: 105 _type = _NAMEOID_DEFAULT_TYPE.get(oid, _ASN1Type.UTF8String) 106 107 if not isinstance(_type, _ASN1Type): 108 raise TypeError("_type must be from the _ASN1Type enum") 109 110 self._oid = oid 111 self._value = value 112 self._type = _type 113 114 oid = utils.read_only_property("_oid") 115 value = utils.read_only_property("_value") 116 117 def rfc4514_string(self): 118 """ 119 Format as RFC4514 Distinguished Name string. 120 121 Use short attribute name if available, otherwise fall back to OID 122 dotted string. 123 """ 124 key = _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string) 125 return "%s=%s" % (key, _escape_dn_value(self.value)) 126 127 def __eq__(self, other): 128 if not isinstance(other, NameAttribute): 129 return NotImplemented 130 131 return self.oid == other.oid and self.value == other.value 132 133 def __ne__(self, other): 134 return not self == other 135 136 def __hash__(self): 137 return hash((self.oid, self.value)) 138 139 def __repr__(self): 140 return "<NameAttribute(oid={0.oid}, value={0.value!r})>".format(self) 141 142 143class RelativeDistinguishedName(object): 144 def __init__(self, attributes): 145 attributes = list(attributes) 146 if not attributes: 147 raise ValueError("a relative distinguished name cannot be empty") 148 if not all(isinstance(x, NameAttribute) for x in attributes): 149 raise TypeError("attributes must be an iterable of NameAttribute") 150 151 # Keep list and frozenset to preserve attribute order where it matters 152 self._attributes = attributes 153 self._attribute_set = frozenset(attributes) 154 155 if len(self._attribute_set) != len(attributes): 156 raise ValueError("duplicate attributes are not allowed") 157 158 def get_attributes_for_oid(self, oid): 159 return [i for i in self if i.oid == oid] 160 161 def rfc4514_string(self): 162 """ 163 Format as RFC4514 Distinguished Name string. 164 165 Within each RDN, attributes are joined by '+', although that is rarely 166 used in certificates. 167 """ 168 return "+".join(attr.rfc4514_string() for attr in self._attributes) 169 170 def __eq__(self, other): 171 if not isinstance(other, RelativeDistinguishedName): 172 return NotImplemented 173 174 return self._attribute_set == other._attribute_set 175 176 def __ne__(self, other): 177 return not self == other 178 179 def __hash__(self): 180 return hash(self._attribute_set) 181 182 def __iter__(self): 183 return iter(self._attributes) 184 185 def __len__(self): 186 return len(self._attributes) 187 188 def __repr__(self): 189 return "<RelativeDistinguishedName({})>".format(self.rfc4514_string()) 190 191 192class Name(object): 193 def __init__(self, attributes): 194 attributes = list(attributes) 195 if all(isinstance(x, NameAttribute) for x in attributes): 196 self._attributes = [ 197 RelativeDistinguishedName([x]) for x in attributes 198 ] 199 elif all(isinstance(x, RelativeDistinguishedName) for x in attributes): 200 self._attributes = attributes 201 else: 202 raise TypeError( 203 "attributes must be a list of NameAttribute" 204 " or a list RelativeDistinguishedName" 205 ) 206 207 def rfc4514_string(self): 208 """ 209 Format as RFC4514 Distinguished Name string. 210 For example 'CN=foobar.com,O=Foo Corp,C=US' 211 212 An X.509 name is a two-level structure: a list of sets of attributes. 213 Each list element is separated by ',' and within each list element, set 214 elements are separated by '+'. The latter is almost never used in 215 real world certificates. According to RFC4514 section 2.1 the 216 RDNSequence must be reversed when converting to string representation. 217 """ 218 return ",".join( 219 attr.rfc4514_string() for attr in reversed(self._attributes) 220 ) 221 222 def get_attributes_for_oid(self, oid): 223 return [i for i in self if i.oid == oid] 224 225 @property 226 def rdns(self): 227 return self._attributes 228 229 def public_bytes(self, backend=None): 230 backend = _get_backend(backend) 231 return backend.x509_name_bytes(self) 232 233 def __eq__(self, other): 234 if not isinstance(other, Name): 235 return NotImplemented 236 237 return self._attributes == other._attributes 238 239 def __ne__(self, other): 240 return not self == other 241 242 def __hash__(self): 243 # TODO: this is relatively expensive, if this looks like a bottleneck 244 # for you, consider optimizing! 245 return hash(tuple(self._attributes)) 246 247 def __iter__(self): 248 for rdn in self._attributes: 249 for ava in rdn: 250 yield ava 251 252 def __len__(self): 253 return sum(len(rdn) for rdn in self._attributes) 254 255 def __repr__(self): 256 rdns = ",".join(attr.rfc4514_string() for attr in self._attributes) 257 258 if six.PY2: 259 return "<Name({})>".format(rdns.encode("utf8")) 260 else: 261 return "<Name({})>".format(rdns) 262