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