diff options
-rw-r--r-- | docs/x509.rst | 90 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | src/cryptography/x509.py | 170 | ||||
-rw-r--r-- | tests/test_x509_ext.py | 156 |
4 files changed, 419 insertions, 0 deletions
diff --git a/docs/x509.rst b/docs/x509.rst index 512d940a..e0e05b6b 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -399,6 +399,80 @@ X.509 CSR (Certificate Signing Request) Object The dotted string value of the OID (e.g. ``"2.5.4.3"``) +.. _general_name_classes: + +General Name Classes +~~~~~~~~~~~~~~~~~~~~ + +.. class:: GeneralName + + .. versionadded:: 0.9 + + This is the generic interface that all the following classes are registered + against. + +.. class:: RFC822Name + + .. versionadded:: 0.9 + + This corresponds to an email address. For example, ``user@example.com``. + + .. attribute:: value + + :type: :term:`text` + +.. class:: DNSName + + .. versionadded:: 0.9 + + This corresponds to a domain name. For example, ``cryptography.io``. + + .. attribute:: value + + :type: :term:`text` + +.. class:: DirectoryName + + .. versionadded:: 0.9 + + This corresponds to a directory name. + + .. attribute:: value + + :type: :class:`Name` + +.. class:: UniformResourceIdentifier + + .. versionadded:: 0.9 + + This corresponds to a uniform resource identifier. For example, + ``https://cryptography.io``. + + .. attribute:: value + + :type: :term:`text` + +.. class:: IPAddress + + .. versionadded:: 0.9 + + This corresponds to an IP address. + + .. attribute:: value + + :type: :class:`~ipaddress.IPv4Address` or + :class:`~ipaddress.IPv6Address`. + +.. class:: RegisteredID + + .. versionadded:: 0.9 + + This corresponds to a registered ID. + + .. attribute:: value + + :type: :class:`ObjectIdentifier` + X.509 Extensions ~~~~~~~~~~~~~~~~ @@ -591,6 +665,22 @@ X.509 Extensions The binary value of the identifier. +.. class:: SubjectAlternativeName + + .. versionadded:: 0.9 + + Subject alternative name is an X.509 extension that provides a list of + :ref:`general name <general_name_classes>` instances that provide a set + of identities for which the certificate is valid. The object is iterable to + get every element. + + .. method:: get_values_for_type(type) + + :param type: A :class:`GeneralName` provider. This is one of the + :ref:`general name classes <general_name_classes>`. + + :returns: A list of values extracted from the matched general names. + Object Identifiers ~~~~~~~~~~~~~~~~~~ @@ -40,6 +40,9 @@ requirements = [ if sys.version_info < (3, 4): requirements.append("enum34") +if sys.version_info < (3, 3): + requirements.append("ipaddress") + if platform.python_implementation() != "PyPy": requirements.append("cffi>=0.8") diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index b533b434..55b17460 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function import abc +import ipaddress from enum import Enum import six @@ -387,6 +388,175 @@ class SubjectKeyIdentifier(object): return not self == other +@six.add_metaclass(abc.ABCMeta) +class GeneralName(object): + @abc.abstractproperty + def value(self): + """ + Return the value of the object + """ + + +@utils.register_interface(GeneralName) +class RFC822Name(object): + def __init__(self, value): + if not isinstance(value, six.text_type): + raise TypeError("value must be a unicode string") + + self._value = value + + value = utils.read_only_property("_value") + + def __repr__(self): + return "<RFC822Name(value={0})>".format(self.value) + + def __eq__(self, other): + if not isinstance(other, RFC822Name): + return NotImplemented + + return self.value == other.value + + def __ne__(self, other): + return not self == other + + +@utils.register_interface(GeneralName) +class DNSName(object): + def __init__(self, value): + if not isinstance(value, six.text_type): + raise TypeError("value must be a unicode string") + + self._value = value + + value = utils.read_only_property("_value") + + def __repr__(self): + return "<DNSName(value={0})>".format(self.value) + + def __eq__(self, other): + if not isinstance(other, DNSName): + return NotImplemented + + return self.value == other.value + + def __ne__(self, other): + return not self == other + + +@utils.register_interface(GeneralName) +class UniformResourceIdentifier(object): + def __init__(self, value): + if not isinstance(value, six.text_type): + raise TypeError("value must be a unicode string") + + self._value = value + + value = utils.read_only_property("_value") + + def __repr__(self): + return "<UniformResourceIdentifier(value={0})>".format(self.value) + + def __eq__(self, other): + if not isinstance(other, UniformResourceIdentifier): + return NotImplemented + + return self.value == other.value + + def __ne__(self, other): + return not self == other + + +@utils.register_interface(GeneralName) +class DirectoryName(object): + def __init__(self, value): + if not isinstance(value, Name): + raise TypeError("value must be a Name") + + self._value = value + + value = utils.read_only_property("_value") + + def __repr__(self): + return "<DirectoryName(value={0})>".format(self.value) + + def __eq__(self, other): + if not isinstance(other, DirectoryName): + return NotImplemented + + return self.value == other.value + + def __ne__(self, other): + return not self == other + + +@utils.register_interface(GeneralName) +class RegisteredID(object): + def __init__(self, value): + if not isinstance(value, ObjectIdentifier): + raise TypeError("value must be an ObjectIdentifier") + + self._value = value + + value = utils.read_only_property("_value") + + def __repr__(self): + return "<RegisteredID(value={0})>".format(self.value) + + def __eq__(self, other): + if not isinstance(other, RegisteredID): + return NotImplemented + + return self.value == other.value + + def __ne__(self, other): + return not self == other + + +@utils.register_interface(GeneralName) +class IPAddress(object): + def __init__(self, value): + if not isinstance( + value, (ipaddress.IPv4Address, ipaddress.IPv6Address) + ): + raise TypeError( + "value must be an instance of ipaddress.IPv4Address or " + "ipaddress.IPv6Address" + ) + + self._value = value + + value = utils.read_only_property("_value") + + def __repr__(self): + return "<IPAddress(value={0})>".format(self.value) + + def __eq__(self, other): + if not isinstance(other, IPAddress): + return NotImplemented + + return self.value == other.value + + def __ne__(self, other): + return not self == other + + +class SubjectAlternativeName(object): + def __init__(self, general_names): + self._general_names = general_names + + def __iter__(self): + return iter(self._general_names) + + def __len__(self): + return len(self._general_names) + + def get_values_for_type(self, type): + return [i.value for i in self if isinstance(i, type)] + + def __repr__(self): + return "<SubjectAlternativeName({0})>".format(self._general_names) + + OID_COMMON_NAME = ObjectIdentifier("2.5.4.3") OID_COUNTRY_NAME = ObjectIdentifier("2.5.4.6") OID_LOCALITY_NAME = ObjectIdentifier("2.5.4.7") diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index 9f98bce1..4811541f 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function import binascii +import ipaddress import os import pytest @@ -523,3 +524,158 @@ class TestKeyUsageExtension(object): assert ku.key_agreement is False assert ku.key_cert_sign is True assert ku.crl_sign is True + + +@pytest.mark.parametrize( + "name", [ + x509.RFC822Name, + x509.DNSName, + x509.UniformResourceIdentifier + ] +) +class TestTextGeneralNames(object): + def test_not_text(self, name): + with pytest.raises(TypeError): + name(b"notaunicodestring") + + with pytest.raises(TypeError): + name(1.3) + + def test_repr(self, name): + gn = name(six.u("string")) + assert repr(gn) == "<{0}(value=string)>".format(name.__name__) + + def test_eq(self, name): + gn = name(six.u("string")) + gn2 = name(six.u("string")) + assert gn == gn2 + + def test_ne(self, name): + gn = name(six.u("string")) + gn2 = name(six.u("string2")) + assert gn != gn2 + assert gn != object() + + +class TestDirectoryName(object): + def test_not_name(self): + with pytest.raises(TypeError): + x509.DirectoryName(b"notaname") + + with pytest.raises(TypeError): + x509.DirectoryName(1.3) + + def test_repr(self): + name = x509.Name([x509.NameAttribute(x509.OID_COMMON_NAME, 'value1')]) + gn = x509.DirectoryName(x509.Name([name])) + assert repr(gn) == ( + "<DirectoryName(value=<Name([<Name([<NameAttribute(oid=<ObjectIden" + "tifier(oid=2.5.4.3, name=commonName)>, value='value1')>])>])>)>" + ) + + def test_eq(self): + name = x509.Name([ + x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1') + ]) + name2 = x509.Name([ + x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1') + ]) + gn = x509.DirectoryName(x509.Name([name])) + gn2 = x509.DirectoryName(x509.Name([name2])) + assert gn == gn2 + + def test_ne(self): + name = x509.Name([ + x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value1') + ]) + name2 = x509.Name([ + x509.NameAttribute(x509.ObjectIdentifier('oid'), 'value2') + ]) + gn = x509.DirectoryName(x509.Name([name])) + gn2 = x509.DirectoryName(x509.Name([name2])) + assert gn != gn2 + assert gn != object() + + +class TestRegisteredID(object): + def test_not_oid(self): + with pytest.raises(TypeError): + x509.RegisteredID(b"notanoid") + + with pytest.raises(TypeError): + x509.RegisteredID(1.3) + + def test_repr(self): + gn = x509.RegisteredID(x509.OID_COMMON_NAME) + assert repr(gn) == ( + "<RegisteredID(value=<ObjectIdentifier(oid=2.5.4.3, name=commonNam" + "e)>)>" + ) + + def test_eq(self): + gn = x509.RegisteredID(x509.OID_COMMON_NAME) + gn2 = x509.RegisteredID(x509.OID_COMMON_NAME) + assert gn == gn2 + + def test_ne(self): + gn = x509.RegisteredID(x509.OID_COMMON_NAME) + gn2 = x509.RegisteredID(x509.OID_BASIC_CONSTRAINTS) + assert gn != gn2 + assert gn != object() + + +class TestIPAddress(object): + def test_not_ipaddress(self): + with pytest.raises(TypeError): + x509.IPAddress(b"notanipaddress") + + with pytest.raises(TypeError): + x509.IPAddress(1.3) + + def test_repr(self): + gn = x509.IPAddress(ipaddress.IPv4Address(six.u("127.0.0.1"))) + assert repr(gn) == "<IPAddress(value=127.0.0.1)>" + + gn2 = x509.IPAddress(ipaddress.IPv6Address(six.u("ff::"))) + assert repr(gn2) == "<IPAddress(value=ff::)>" + + def test_eq(self): + gn = x509.IPAddress(ipaddress.IPv4Address(six.u("127.0.0.1"))) + gn2 = x509.IPAddress(ipaddress.IPv4Address(six.u("127.0.0.1"))) + assert gn == gn2 + + def test_ne(self): + gn = x509.IPAddress(ipaddress.IPv4Address(six.u("127.0.0.1"))) + gn2 = x509.IPAddress(ipaddress.IPv4Address(six.u("127.0.0.2"))) + assert gn != gn2 + assert gn != object() + + +class TestSubjectAlternativeName(object): + def test_get_values_for_type(self): + san = x509.SubjectAlternativeName( + [x509.DNSName(six.u("cryptography.io"))] + ) + names = san.get_values_for_type(x509.DNSName) + assert names == [six.u("cryptography.io")] + + def test_iter_names(self): + san = x509.SubjectAlternativeName([ + x509.DNSName(six.u("cryptography.io")), + x509.DNSName(six.u("crypto.local")), + ]) + assert len(san) == 2 + assert list(san) == [ + x509.DNSName(six.u("cryptography.io")), + x509.DNSName(six.u("crypto.local")), + ] + + def test_repr(self): + san = x509.SubjectAlternativeName( + [ + x509.DNSName(six.u("cryptography.io")) + ] + ) + assert repr(san) == ( + "<SubjectAlternativeName([<DNSName(value=cryptography.io)>])>" + ) |