• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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