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
7import abc
8import ipaddress
9import warnings
10from email.utils import parseaddr
11
12import six
13from six.moves import urllib_parse
14
15from cryptography import utils
16from cryptography.x509.name import Name
17from cryptography.x509.oid import ObjectIdentifier
18
19
20_GENERAL_NAMES = {
21    0: "otherName",
22    1: "rfc822Name",
23    2: "dNSName",
24    3: "x400Address",
25    4: "directoryName",
26    5: "ediPartyName",
27    6: "uniformResourceIdentifier",
28    7: "iPAddress",
29    8: "registeredID",
30}
31
32
33def _lazy_import_idna():
34    # Import idna lazily becase it allocates a decent amount of memory, and
35    # we're only using it in deprecated paths.
36    try:
37        import idna
38        return idna
39    except ImportError:
40        raise ImportError(
41            "idna is not installed, but a deprecated feature that requires it"
42            " was used. See: https://cryptography.io/en/latest/faq/#importe"
43            "rror-idna-is-not-installed"
44        )
45
46
47class UnsupportedGeneralNameType(Exception):
48    def __init__(self, msg, type):
49        super(UnsupportedGeneralNameType, self).__init__(msg)
50        self.type = type
51
52
53@six.add_metaclass(abc.ABCMeta)
54class GeneralName(object):
55    @abc.abstractproperty
56    def value(self):
57        """
58        Return the value of the object
59        """
60
61
62@utils.register_interface(GeneralName)
63class RFC822Name(object):
64    def __init__(self, value):
65        if isinstance(value, six.text_type):
66            try:
67                value.encode("ascii")
68            except UnicodeEncodeError:
69                value = self._idna_encode(value)
70                warnings.warn(
71                    "RFC822Name values should be passed as an A-label string. "
72                    "This means unicode characters should be encoded via "
73                    "idna. Support for passing unicode strings (aka U-label) "
74                    "will be removed in a future version.",
75                    utils.DeprecatedIn21,
76                    stacklevel=2,
77                )
78        else:
79            raise TypeError("value must be string")
80
81        name, address = parseaddr(value)
82        if name or not address:
83            # parseaddr has found a name (e.g. Name <email>) or the entire
84            # value is an empty string.
85            raise ValueError("Invalid rfc822name value")
86
87        self._value = value
88
89    value = utils.read_only_property("_value")
90
91    @classmethod
92    def _init_without_validation(cls, value):
93        instance = cls.__new__(cls)
94        instance._value = value
95        return instance
96
97    def _idna_encode(self, value):
98        idna = _lazy_import_idna()
99        _, address = parseaddr(value)
100        parts = address.split(u"@")
101        return parts[0] + "@" + idna.encode(parts[1]).decode("ascii")
102
103    def __repr__(self):
104        return "<RFC822Name(value={0!r})>".format(self.value)
105
106    def __eq__(self, other):
107        if not isinstance(other, RFC822Name):
108            return NotImplemented
109
110        return self.value == other.value
111
112    def __ne__(self, other):
113        return not self == other
114
115    def __hash__(self):
116        return hash(self.value)
117
118
119def _idna_encode(value):
120    idna = _lazy_import_idna()
121    # Retain prefixes '*.' for common/alt names and '.' for name constraints
122    for prefix in ['*.', '.']:
123        if value.startswith(prefix):
124            value = value[len(prefix):]
125            return prefix + idna.encode(value).decode("ascii")
126    return idna.encode(value).decode("ascii")
127
128
129@utils.register_interface(GeneralName)
130class DNSName(object):
131    def __init__(self, value):
132        if isinstance(value, six.text_type):
133            try:
134                value.encode("ascii")
135            except UnicodeEncodeError:
136                value = _idna_encode(value)
137                warnings.warn(
138                    "DNSName values should be passed as an A-label string. "
139                    "This means unicode characters should be encoded via "
140                    "idna. Support for passing unicode strings (aka U-label) "
141                    "will be removed in a future version.",
142                    utils.DeprecatedIn21,
143                    stacklevel=2,
144                )
145        else:
146            raise TypeError("value must be string")
147
148        self._value = value
149
150    value = utils.read_only_property("_value")
151
152    @classmethod
153    def _init_without_validation(cls, value):
154        instance = cls.__new__(cls)
155        instance._value = value
156        return instance
157
158    def __repr__(self):
159        return "<DNSName(value={0!r})>".format(self.value)
160
161    def __eq__(self, other):
162        if not isinstance(other, DNSName):
163            return NotImplemented
164
165        return self.value == other.value
166
167    def __ne__(self, other):
168        return not self == other
169
170    def __hash__(self):
171        return hash(self.value)
172
173
174@utils.register_interface(GeneralName)
175class UniformResourceIdentifier(object):
176    def __init__(self, value):
177        if isinstance(value, six.text_type):
178            try:
179                value.encode("ascii")
180            except UnicodeEncodeError:
181                value = self._idna_encode(value)
182                warnings.warn(
183                    "URI values should be passed as an A-label string. "
184                    "This means unicode characters should be encoded via "
185                    "idna. Support for passing unicode strings (aka U-label) "
186                    " will be removed in a future version.",
187                    utils.DeprecatedIn21,
188                    stacklevel=2,
189                )
190        else:
191            raise TypeError("value must be string")
192
193        self._value = value
194
195    value = utils.read_only_property("_value")
196
197    @classmethod
198    def _init_without_validation(cls, value):
199        instance = cls.__new__(cls)
200        instance._value = value
201        return instance
202
203    def _idna_encode(self, value):
204        idna = _lazy_import_idna()
205        parsed = urllib_parse.urlparse(value)
206        if parsed.port:
207            netloc = (
208                idna.encode(parsed.hostname) +
209                ":{0}".format(parsed.port).encode("ascii")
210            ).decode("ascii")
211        else:
212            netloc = idna.encode(parsed.hostname).decode("ascii")
213
214        # Note that building a URL in this fashion means it should be
215        # semantically indistinguishable from the original but is not
216        # guaranteed to be exactly the same.
217        return urllib_parse.urlunparse((
218            parsed.scheme,
219            netloc,
220            parsed.path,
221            parsed.params,
222            parsed.query,
223            parsed.fragment
224        ))
225
226    def __repr__(self):
227        return "<UniformResourceIdentifier(value={0!r})>".format(self.value)
228
229    def __eq__(self, other):
230        if not isinstance(other, UniformResourceIdentifier):
231            return NotImplemented
232
233        return self.value == other.value
234
235    def __ne__(self, other):
236        return not self == other
237
238    def __hash__(self):
239        return hash(self.value)
240
241
242@utils.register_interface(GeneralName)
243class DirectoryName(object):
244    def __init__(self, value):
245        if not isinstance(value, Name):
246            raise TypeError("value must be a Name")
247
248        self._value = value
249
250    value = utils.read_only_property("_value")
251
252    def __repr__(self):
253        return "<DirectoryName(value={0})>".format(self.value)
254
255    def __eq__(self, other):
256        if not isinstance(other, DirectoryName):
257            return NotImplemented
258
259        return self.value == other.value
260
261    def __ne__(self, other):
262        return not self == other
263
264    def __hash__(self):
265        return hash(self.value)
266
267
268@utils.register_interface(GeneralName)
269class RegisteredID(object):
270    def __init__(self, value):
271        if not isinstance(value, ObjectIdentifier):
272            raise TypeError("value must be an ObjectIdentifier")
273
274        self._value = value
275
276    value = utils.read_only_property("_value")
277
278    def __repr__(self):
279        return "<RegisteredID(value={0})>".format(self.value)
280
281    def __eq__(self, other):
282        if not isinstance(other, RegisteredID):
283            return NotImplemented
284
285        return self.value == other.value
286
287    def __ne__(self, other):
288        return not self == other
289
290    def __hash__(self):
291        return hash(self.value)
292
293
294@utils.register_interface(GeneralName)
295class IPAddress(object):
296    def __init__(self, value):
297        if not isinstance(
298            value,
299            (
300                ipaddress.IPv4Address,
301                ipaddress.IPv6Address,
302                ipaddress.IPv4Network,
303                ipaddress.IPv6Network
304            )
305        ):
306            raise TypeError(
307                "value must be an instance of ipaddress.IPv4Address, "
308                "ipaddress.IPv6Address, ipaddress.IPv4Network, or "
309                "ipaddress.IPv6Network"
310            )
311
312        self._value = value
313
314    value = utils.read_only_property("_value")
315
316    def __repr__(self):
317        return "<IPAddress(value={0})>".format(self.value)
318
319    def __eq__(self, other):
320        if not isinstance(other, IPAddress):
321            return NotImplemented
322
323        return self.value == other.value
324
325    def __ne__(self, other):
326        return not self == other
327
328    def __hash__(self):
329        return hash(self.value)
330
331
332@utils.register_interface(GeneralName)
333class OtherName(object):
334    def __init__(self, type_id, value):
335        if not isinstance(type_id, ObjectIdentifier):
336            raise TypeError("type_id must be an ObjectIdentifier")
337        if not isinstance(value, bytes):
338            raise TypeError("value must be a binary string")
339
340        self._type_id = type_id
341        self._value = value
342
343    type_id = utils.read_only_property("_type_id")
344    value = utils.read_only_property("_value")
345
346    def __repr__(self):
347        return "<OtherName(type_id={0}, value={1!r})>".format(
348            self.type_id, self.value)
349
350    def __eq__(self, other):
351        if not isinstance(other, OtherName):
352            return NotImplemented
353
354        return self.type_id == other.type_id and self.value == other.value
355
356    def __ne__(self, other):
357        return not self == other
358
359    def __hash__(self):
360        return hash((self.type_id, self.value))
361