Merge pull request #1768 from reaperhulk/basic-constraints

basic constraints class & extensions interface
diff --git a/docs/x509.rst b/docs/x509.rst
index 27f1d54..1321891 100644
--- a/docs/x509.rst
+++ b/docs/x509.rst
@@ -273,6 +273,61 @@
 
         The dotted string value of the OID (e.g. ``"2.5.4.3"``)
 
+X.509 Extensions
+~~~~~~~~~~~~~~~~
+
+.. class:: Extension
+
+    .. versionadded:: 0.9
+
+    .. attribute:: oid
+
+        :type: :class:`ObjectIdentifier`
+
+        The :ref:`extension OID <extension_oids>`.
+
+    .. attribute:: critical
+
+        :type: bool
+
+        Determines whether a given extension is critical or not. :rfc:`5280`
+        requires that "A certificate-using system MUST reject the certificate
+        if it encounters a critical extension it does not recognize or a
+        critical extension that contains information that it cannot process".
+
+    .. attribute:: value
+
+        Returns an instance of the extension type corresponding to the OID.
+
+.. class:: BasicConstraints
+
+    .. versionadded:: 0.9
+
+    Basic constraints is an X.509 extension type that defines whether a given
+    certificate is allowed to sign additional certificates and what path
+    length restrictions may exist. It corresponds to
+    :data:`OID_BASIC_CONSTRAINTS`.
+
+    .. attribute:: ca
+
+        :type: bool
+
+        Whether the certificate can sign certificates.
+
+    .. attribute:: path_length
+
+        :type: int or None
+
+        The maximum path length for certificates subordinate to this
+        certificate. This attribute only has meaning if ``ca`` is true.
+        If ``ca`` is true then a path length of None means there's no
+        restriction on the number of subordinate CAs in the certificate chain.
+        If it is zero or greater then that number defines the maximum length.
+        For example, a ``path_length`` of 1 means the certificate can sign a
+        subordinate CA, but the subordinate CA is not allowed to create
+        subordinates with ``ca`` set to true.
+
+
 Object Identifiers
 ~~~~~~~~~~~~~~~~~~
 
@@ -430,6 +485,16 @@
     Corresponds to the dotted string ``2.16.840.1.101.3.4.3.2"``. This is
     a SHA256 digest signed by a DSA key.
 
+.. _extension_oids:
+
+Extension OIDs
+~~~~~~~~~~~~~~
+
+.. data:: OID_BASIC_CONSTRAINTS
+
+    Corresponds to the dotted string ``"2.5.29.19"``. The identifier for the
+    :class:`BasicConstraints` extension type.
+
 
 Exceptions
 ~~~~~~~~~~
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
index 1d2a948..1ad7028 100644
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -42,6 +42,7 @@
     "1.2.840.10040.4.3": "dsa-with-sha1",
     "2.16.840.1.101.3.4.3.1": "dsa-with-sha224",
     "2.16.840.1.101.3.4.3.2": "dsa-with-sha256",
+    "2.5.29.19": "basicConstraints",
 }
 
 
@@ -138,6 +139,59 @@
         return len(self._attributes)
 
 
+OID_BASIC_CONSTRAINTS = ObjectIdentifier("2.5.29.19")
+
+
+class Extension(object):
+    def __init__(self, oid, critical, value):
+        if not isinstance(oid, ObjectIdentifier):
+            raise TypeError(
+                "oid argument must be an ObjectIdentifier instance."
+            )
+
+        if not isinstance(critical, bool):
+            raise TypeError("critical must be a boolean value")
+
+        self._oid = oid
+        self._critical = critical
+        self._value = value
+
+    oid = utils.read_only_property("_oid")
+    critical = utils.read_only_property("_critical")
+    value = utils.read_only_property("_value")
+
+    def __repr__(self):
+        return ("<Extension(oid={0.oid}, critical={0.critical}, "
+                "value={0.value})>").format(self)
+
+
+class BasicConstraints(object):
+    def __init__(self, ca, path_length):
+        if not isinstance(ca, bool):
+            raise TypeError("ca must be a boolean value")
+
+        if path_length is not None and not ca:
+            raise ValueError("path_length must be None when ca is False")
+
+        if (
+            path_length is not None and
+            (not isinstance(path_length, six.integer_types) or path_length < 0)
+        ):
+            raise TypeError(
+                "path_length must be a non-negative integer or None"
+            )
+
+        self._ca = ca
+        self._path_length = path_length
+
+    ca = utils.read_only_property("_ca")
+    path_length = utils.read_only_property("_path_length")
+
+    def __repr__(self):
+        return ("<BasicConstraints(ca={0.ca}, "
+                "path_length={0.path_length})>").format(self)
+
+
 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
new file mode 100644
index 0000000..74d14c5
--- /dev/null
+++ b/tests/test_x509_ext.py
@@ -0,0 +1,57 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+from cryptography import x509
+
+
+class TestExtension(object):
+    def test_not_an_oid(self):
+        bc = x509.BasicConstraints(ca=False, path_length=None)
+        with pytest.raises(TypeError):
+            x509.Extension("notanoid", True, bc)
+
+    def test_critical_not_a_bool(self):
+        bc = x509.BasicConstraints(ca=False, path_length=None)
+        with pytest.raises(TypeError):
+            x509.Extension(x509.OID_BASIC_CONSTRAINTS, "notabool", bc)
+
+    def test_repr(self):
+        bc = x509.BasicConstraints(ca=False, path_length=None)
+        ext = x509.Extension(x509.OID_BASIC_CONSTRAINTS, True, bc)
+        assert repr(ext) == (
+            "<Extension(oid=<ObjectIdentifier(oid=2.5.29.19, name=basicConst"
+            "raints)>, critical=True, value=<BasicConstraints(ca=False, path"
+            "_length=None)>)>"
+        )
+
+
+class TestBasicConstraints(object):
+    def test_ca_not_boolean(self):
+        with pytest.raises(TypeError):
+            x509.BasicConstraints(ca="notbool", path_length=None)
+
+    def test_path_length_not_ca(self):
+        with pytest.raises(ValueError):
+            x509.BasicConstraints(ca=False, path_length=0)
+
+    def test_path_length_not_int(self):
+        with pytest.raises(TypeError):
+            x509.BasicConstraints(ca=True, path_length=1.1)
+
+        with pytest.raises(TypeError):
+            x509.BasicConstraints(ca=True, path_length="notint")
+
+    def test_path_length_negative(self):
+        with pytest.raises(TypeError):
+            x509.BasicConstraints(ca=True, path_length=-1)
+
+    def test_repr(self):
+        na = x509.BasicConstraints(ca=True, path_length=None)
+        assert repr(na) == (
+            "<BasicConstraints(ca=True, path_length=None)>"
+        )