From e0017be396df1a506b92ec1b669086dd02ca25b8 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Sun, 17 May 2015 20:39:40 -0600 Subject: add nameconstraints classes --- docs/x509.rst | 31 ++++++++++++++++++++++ src/cryptography/x509.py | 52 ++++++++++++++++++++++++++++++++++++ tests/test_x509_ext.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/docs/x509.rst b/docs/x509.rst index ed7b8716..1e4efb4c 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -814,6 +814,32 @@ X.509 Extensions extension is only relevant when the certificate is an authorized OCSP responder. +.. class:: NameConstraints + + .. versionadded:: 1.0 + + The name constraints extension, which only has meaning in a CA certificate, + defines a name space within which all subject names in certificates issued + beneath the CA certificate must (or must not) be in. For specific details + on the way this extension should be processed see :rfc:`5280`. + + .. attribute:: permitted_subtrees + + :type: list of :class:`GeneralName` objects or None + + The set of permitted name patterns. If a name matches this and an + element in ``excluded_subtrees`` it is invalid. At least one of + ``permitted_subtrees`` and ``excluded_subtrees`` will be non-None. + + .. attribute:: excluded_subtrees + + :type: list of :class:`GeneralName` objects or None + + Any name matching a restriction in the ``excluded_subtrees`` field is + invalid regardless of information appearing in the + ``permitted_subtrees``. At least one of ``permitted_subtrees`` and + ``excluded_subtrees`` will be non-None. + .. class:: AuthorityKeyIdentifier .. versionadded:: 0.9 @@ -1369,6 +1395,11 @@ Extension OIDs Corresponds to the dotted string ``"2.5.29.14"``. The identifier for the :class:`SubjectKeyIdentifier` extension type. +.. data:: OID_NAME_CONSTRAINTS + + Corresponds to the dotted string ``"2.5.29.30"``. The identifier for the + :class:`NameConstraints` extension type. + .. data:: OID_CRL_DISTRIBUTION_POINTS Corresponds to the dotted string ``"2.5.29.31"``. The identifier for the diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index 2e2e8512..4dbe3da1 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -676,6 +676,58 @@ class SubjectKeyIdentifier(object): return not self == other +class NameConstraints(object): + def __init__(self, permitted_subtrees, excluded_subtrees): + if permitted_subtrees is not None: + if not all( + isinstance(x, GeneralName) for x in permitted_subtrees + ): + raise TypeError( + "permitted_subtrees must be a list of GeneralName objects " + "or None" + ) + + self._validate_ip_name(permitted_subtrees) + + if excluded_subtrees is not None: + if not all( + isinstance(x, GeneralName) for x in excluded_subtrees + ): + raise TypeError( + "excluded_subtrees must be a list of GeneralName objects " + "or None" + ) + + self._validate_ip_name(excluded_subtrees) + + if permitted_subtrees is None and excluded_subtrees is None: + raise ValueError( + "At least one of permitted_subtrees and excluded_subtrees " + "must not be None" + ) + + self._permitted_subtrees = permitted_subtrees + self._excluded_subtrees = excluded_subtrees + + def _validate_ip_name(self, tree): + if any(isinstance(name, IPAddress) and not isinstance( + name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network) + ) for name in tree): + raise TypeError( + "IPAddress name constraints must be an IPv4Network or" + " IPv6Network object" + ) + + def __repr__(self): + return ( + u"".format(self) + ) + + permitted_subtrees = utils.read_only_property("_permitted_subtrees") + excluded_subtrees = utils.read_only_property("_excluded_subtrees") + + class CRLDistributionPoints(object): def __init__(self, distribution_points): if not all( diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index 62d9f83d..a5747c37 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -1905,6 +1905,74 @@ class TestAuthorityKeyIdentifierExtension(object): assert ext.value.authority_cert_serial_number == 3 +class TestNameConstraints(object): + def test_ipaddress_wrong_type(self): + with pytest.raises(TypeError): + x509.NameConstraints( + permitted_subtrees=[ + x509.IPAddress(ipaddress.IPv4Address(u"127.0.0.1")) + ], + excluded_subtrees=None + ) + + with pytest.raises(TypeError): + x509.NameConstraints( + permitted_subtrees=None, + excluded_subtrees=[ + x509.IPAddress(ipaddress.IPv4Address(u"127.0.0.1")) + ] + ) + + def test_ipaddress_allowed_type(self): + permitted = [x509.IPAddress(ipaddress.IPv4Network(u"192.168.0.0/29"))] + excluded = [x509.IPAddress(ipaddress.IPv4Network(u"10.10.0.0/24"))] + nc = x509.NameConstraints( + permitted_subtrees=permitted, + excluded_subtrees=excluded + ) + assert nc.permitted_subtrees == permitted + assert nc.excluded_subtrees == excluded + + def test_invalid_permitted_subtrees(self): + with pytest.raises(TypeError): + x509.NameConstraints("badpermitted", None) + + def test_invalid_excluded_subtrees(self): + with pytest.raises(TypeError): + x509.NameConstraints(None, "badexcluded") + + def test_no_subtrees(self): + with pytest.raises(ValueError): + x509.NameConstraints(None, None) + + def test_permitted_none(self): + excluded = [x509.DNSName(u"name.local")] + nc = x509.NameConstraints( + permitted_subtrees=None, excluded_subtrees=excluded + ) + assert nc.permitted_subtrees is None + assert nc.excluded_subtrees is not None + + def test_excluded_none(self): + permitted = [x509.DNSName(u"name.local")] + nc = x509.NameConstraints( + permitted_subtrees=permitted, excluded_subtrees=None + ) + assert nc.permitted_subtrees is not None + assert nc.excluded_subtrees is None + + def test_repr(self): + permitted = [x509.DNSName(u"name.local"), x509.DNSName(u"name2.local")] + nc = x509.NameConstraints( + permitted_subtrees=permitted, + excluded_subtrees=None + ) + assert repr(nc) == ( + "" + ", ], excluded_subtrees=None)>" + ) + + class TestDistributionPoint(object): def test_distribution_point_full_name_not_general_names(self): with pytest.raises(TypeError): -- cgit v1.2.3