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