Merge pull request #2045 from sigmavirus24/csr-builder

Adds CSR Builder (Redux of #1927)
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5c9d08e..bc95cf7 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -24,6 +24,8 @@
   and :class:`~cryptography.hazmat.primitives.kdf.concatkdf.ConcatKDFHMAC`.
 * Raise a ``TypeError`` when passing objects that are not text as the value to
   :class:`~cryptography.x509.NameAttribute`.
+* Add support for creating certificate signing requests with
+  :class:`~cryptography.x509.CertificateSigningRequestBuilder`.
 
 0.9.1 - 2015-06-06
 ~~~~~~~~~~~~~~~~~~
diff --git a/docs/x509.rst b/docs/x509.rst
index b8e3c8e..c4c441e 100644
--- a/docs/x509.rst
+++ b/docs/x509.rst
@@ -468,6 +468,76 @@
 
         The extensions encoded in the revoked certificate.
 
+X.509 CSR (Certificate Signing Request) Builder Object
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: CertificateSigningRequestBuilder
+
+    .. versionadded:: 1.0
+
+    .. doctest::
+
+        >>> from cryptography import x509
+        >>> from cryptography.hazmat.backends import default_backend
+        >>> from cryptography.hazmat.primitives import hashes
+        >>> from cryptography.hazmat.primitives.asymmetric import rsa
+        >>> private_key = rsa.generate_private_key(
+        ...     public_exponent=65537,
+        ...     key_size=2048,
+        ...     backend=default_backend()
+        ... )
+        >>> builder = x509.CertificateSigningRequestBuilder()
+        >>> builder = builder.subject_name(x509.Name([
+        ...     x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+        ... ]))
+        >>> builder = builder.add_extension(
+        ...     x509.BasicConstraints(ca=False, path_length=None), critical=True,
+        ... )
+        >>> request = builder.sign(
+        ...     default_backend(), private_key, hashes.SHA256()
+        ... )
+        >>> isinstance(request, x509.CertificateSigningRequest)
+        True
+
+    .. method:: subject_name(name)
+
+        :param name: The :class:`~cryptography.x509.Name` of the certificate
+            subject.
+        :returns: A new
+            :class:`~cryptography.x509.CertificateSigningRequestBuilder`.
+
+    .. method:: add_extension(extension, critical)
+
+        :param extension: The :class:`~cryptography.x509.Extension` to add to
+            the request.
+        :param critical: Set to `True` if the extension must be understood and
+             handled by whoever reads the certificate.
+        :returns: A new
+            :class:`~cryptography.x509.CertificateSigningRequestBuilder`.
+
+    .. method:: sign(backend, private_key, algorithm)
+
+        :param backend: Backend that will be used to sign the request.
+            Must support the
+            :class:`~cryptography.hazmat.backends.interfaces.X509Backend`
+            interface.
+
+        :param private_key: The
+            :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`,
+            :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey` or
+            :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey`
+            that will be used to sign the request.  When the request is
+            signed by a certificate authority, the private key's associated
+            public key will be stored in the resulting certificate.
+
+        :param algorithm: The
+            :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
+            that will be used to generate the request signature.
+
+        :returns: A new
+            :class:`~cryptography.x509.CertificateSigningRequest`.
+
+
 .. class:: Name
 
     .. versionadded:: 0.8
diff --git a/src/cryptography/hazmat/backends/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py
index eca7ddf..4d378e6 100644
--- a/src/cryptography/hazmat/backends/interfaces.py
+++ b/src/cryptography/hazmat/backends/interfaces.py
@@ -274,6 +274,12 @@
         Load an X.509 CSR from PEM encoded data.
         """
 
+    @abc.abstractmethod
+    def create_x509_csr(self, builder, private_key, algorithm):
+        """
+        Create and sign an X.509 CSR from a CSR builder object.
+        """
+
 
 @six.add_metaclass(abc.ABCMeta)
 class DHBackend(object):
diff --git a/src/cryptography/hazmat/backends/multibackend.py b/src/cryptography/hazmat/backends/multibackend.py
index 784ab84..6e911fd 100644
--- a/src/cryptography/hazmat/backends/multibackend.py
+++ b/src/cryptography/hazmat/backends/multibackend.py
@@ -342,3 +342,12 @@
             "This backend does not support X.509.",
             _Reasons.UNSUPPORTED_X509
         )
+
+    def create_x509_csr(self, builder, private_key, algorithm):
+        for b in self._filtered_backends(X509Backend):
+            return b.create_x509_csr(builder, private_key, algorithm)
+
+        raise UnsupportedAlgorithm(
+            "This backend does not support X.509.",
+            _Reasons.UNSUPPORTED_X509
+        )
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 4d469c4..78de79d 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -10,7 +10,7 @@
 
 import six
 
-from cryptography import utils
+from cryptography import utils, x509
 from cryptography.exceptions import (
     InternalError, UnsupportedAlgorithm, _Reasons
 )
@@ -56,6 +56,96 @@
                                        ["code", "lib", "func", "reason"])
 
 
+def _encode_asn1_int(backend, x):
+    """
+    Converts a python integer to a ASN1_INTEGER. The returned ASN1_INTEGER will
+    not be garbage collected (to support adding them to structs that take
+    ownership of the object). Be sure to register it for GC if it will be
+    discarded after use.
+
+    """
+    # Convert Python integer to OpenSSL "bignum" in case value exceeds
+    # machine's native integer limits (note: `int_to_bn` doesn't automatically
+    # GC).
+    i = backend._int_to_bn(x)
+    i = backend._ffi.gc(i, backend._lib.BN_free)
+
+    # Wrap in a ASN.1 integer.  Don't GC -- as documented.
+    i = backend._lib.BN_to_ASN1_INTEGER(i, backend._ffi.NULL)
+    assert i != backend._ffi.NULL
+    return i
+
+
+def _encode_asn1_str(backend, data, length):
+    """
+    Create an ASN1_OCTET_STRING from a Python byte string.
+    """
+    s = backend._lib.ASN1_OCTET_STRING_new()
+    s = backend._ffi.gc(s, backend._lib.ASN1_OCTET_STRING_free)
+    backend._lib.ASN1_OCTET_STRING_set(s, data, length)
+    return s
+
+
+def _encode_name(backend, attributes):
+    subject = backend._lib.X509_NAME_new()
+    subject = backend._ffi.gc(subject, backend._lib.X509_NAME_free)
+    for attribute in attributes:
+        value = attribute.value.encode('utf8')
+        obj = _txt2obj(backend, attribute.oid.dotted_string)
+        res = backend._lib.X509_NAME_add_entry_by_OBJ(
+            subject,
+            obj,
+            backend._lib.MBSTRING_UTF8,
+            value,
+            -1, -1, 0,
+        )
+        assert res == 1
+    return subject
+
+
+def _txt2obj(backend, name):
+    """
+    Converts a Python string with an ASN.1 object ID in dotted form to a
+    ASN1_OBJECT.
+    """
+    name = name.encode('ascii')
+    obj = backend._lib.OBJ_txt2obj(name, 1)
+    assert obj != backend._ffi.NULL
+    obj = backend._ffi.gc(obj, backend._lib.ASN1_OBJECT_free)
+    return obj
+
+
+def _encode_basic_constraints(backend, basic_constraints, critical):
+    obj = _txt2obj(backend, x509.OID_BASIC_CONSTRAINTS.dotted_string)
+    assert obj is not None
+    constraints = backend._lib.BASIC_CONSTRAINTS_new()
+    constraints.ca = 255 if basic_constraints.ca else 0
+    if basic_constraints.ca:
+        constraints.pathlen = _encode_asn1_int(
+            backend, basic_constraints.path_length
+        )
+
+    # Fetch the encoded payload.
+    pp = backend._ffi.new('unsigned char **')
+    r = backend._lib.i2d_BASIC_CONSTRAINTS(constraints, pp)
+    assert r > 0
+    pp = backend._ffi.gc(
+        pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0])
+    )
+
+    # Wrap that in an X509 extension object.
+    extension = backend._lib.X509_EXTENSION_create_by_OBJ(
+        backend._ffi.NULL,
+        obj,
+        1 if critical else 0,
+        _encode_asn1_str(backend, pp[0], r),
+    )
+    assert extension != backend._ffi.NULL
+
+    # Return the wrapped extension.
+    return extension
+
+
 @utils.register_interface(CipherBackend)
 @utils.register_interface(CMACBackend)
 @utils.register_interface(DERSerializationBackend)
@@ -710,6 +800,79 @@
     def create_cmac_ctx(self, algorithm):
         return _CMACContext(self, algorithm)
 
+    def create_x509_csr(self, builder, private_key, algorithm):
+        if not isinstance(algorithm, hashes.HashAlgorithm):
+            raise TypeError('Algorithm must be a registered hash algorithm.')
+
+        if self._lib.OPENSSL_VERSION_NUMBER <= 0x10001000:
+            if isinstance(private_key, _DSAPrivateKey):
+                raise NotImplementedError(
+                    "Certificate signing requests aren't implemented for DSA"
+                    " keys on OpenSSL versions less than 1.0.1."
+                )
+            if isinstance(private_key, _EllipticCurvePrivateKey):
+                raise NotImplementedError(
+                    "Certificate signing requests aren't implemented for EC"
+                    " keys on OpenSSL versions less than 1.0.1."
+                )
+
+        # Resolve the signature algorithm.
+        evp_md = self._lib.EVP_get_digestbyname(
+            algorithm.name.encode('ascii')
+        )
+        assert evp_md != self._ffi.NULL
+
+        # Create an empty request.
+        x509_req = self._lib.X509_REQ_new()
+        assert x509_req != self._ffi.NULL
+        x509_req = self._ffi.gc(x509_req, self._lib.X509_REQ_free)
+
+        # Set x509 version.
+        res = self._lib.X509_REQ_set_version(x509_req, x509.Version.v1.value)
+        assert res == 1
+
+        # Set subject name.
+        res = self._lib.X509_REQ_set_subject_name(
+            x509_req, _encode_name(self, list(builder._subject_name))
+        )
+        assert res == 1
+
+        # Set subject public key.
+        public_key = private_key.public_key()
+        res = self._lib.X509_REQ_set_pubkey(
+            x509_req, public_key._evp_pkey
+        )
+        assert res == 1
+
+        # Add extensions.
+        extensions = self._lib.sk_X509_EXTENSION_new_null()
+        assert extensions != self._ffi.NULL
+        extensions = self._ffi.gc(
+            extensions,
+            self._lib.sk_X509_EXTENSION_free,
+        )
+        for extension in builder._extensions:
+            if isinstance(extension.value, x509.BasicConstraints):
+                extension = _encode_basic_constraints(
+                    self,
+                    extension.value,
+                    extension.critical
+                )
+            else:
+                raise NotImplementedError('Extension not yet supported.')
+            res = self._lib.sk_X509_EXTENSION_push(extensions, extension)
+            assert res == 1
+        res = self._lib.X509_REQ_add_extensions(x509_req, extensions)
+        assert res == 1
+
+        # Sign the request using the requester's private key.
+        res = self._lib.X509_REQ_sign(
+            x509_req, private_key._evp_pkey, evp_md
+        )
+        assert res > 0
+
+        return _CertificateSigningRequest(self, x509_req)
+
     def load_pem_private_key(self, data, password):
         return self._load_key(
             self._lib.PEM_read_bio_PrivateKey,
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
index 4b030ca..21e18dd 100644
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -1442,3 +1442,44 @@
         """
         Returns an Extensions object containing a list of Revoked extensions.
         """
+
+
+class CertificateSigningRequestBuilder(object):
+    def __init__(self, subject_name=None, extensions=[]):
+        """
+        Creates an empty X.509 certificate request (v1).
+        """
+        self._subject_name = subject_name
+        self._extensions = extensions
+
+    def subject_name(self, name):
+        """
+        Sets the certificate requestor's distinguished name.
+        """
+        if not isinstance(name, Name):
+            raise TypeError('Expecting x509.Name object.')
+        if self._subject_name is not None:
+            raise ValueError('The subject name may only be set once.')
+        return CertificateSigningRequestBuilder(name, self._extensions)
+
+    def add_extension(self, extension, critical):
+        """
+        Adds an X.509 extension to the certificate request.
+        """
+        if isinstance(extension, BasicConstraints):
+            extension = Extension(OID_BASIC_CONSTRAINTS, critical, extension)
+        else:
+            raise NotImplementedError('Unsupported X.509 extension.')
+        # TODO: This is quadratic in the number of extensions
+        for e in self._extensions:
+            if e.oid == extension.oid:
+                raise ValueError('This extension has already been set.')
+        return CertificateSigningRequestBuilder(
+            self._subject_name, self._extensions + [extension]
+        )
+
+    def sign(self, backend, private_key, algorithm):
+        """
+        Signs the request using the requestor's private key.
+        """
+        return backend.create_x509_csr(self, private_key, algorithm)
diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py
index 5871e6c..3c05cdf 100644
--- a/tests/hazmat/backends/test_multibackend.py
+++ b/tests/hazmat/backends/test_multibackend.py
@@ -203,6 +203,9 @@
     def load_der_x509_csr(self, data):
         pass
 
+    def create_x509_csr(self, builder, private_key, algorithm):
+        pass
+
 
 class TestMultiBackend(object):
     def test_ciphers(self):
@@ -480,6 +483,7 @@
         backend.load_der_x509_certificate(b"certdata")
         backend.load_pem_x509_csr(b"reqdata")
         backend.load_der_x509_csr(b"reqdata")
+        backend.create_x509_csr(object(), b"privatekey", hashes.SHA1())
 
         backend = MultiBackend([])
         with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509):
@@ -490,3 +494,5 @@
             backend.load_pem_x509_csr(b"reqdata")
         with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509):
             backend.load_der_x509_csr(b"reqdata")
+        with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509):
+            backend.create_x509_csr(object(), b"privatekey", hashes.SHA1())
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index b35e767..34fff27 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -21,14 +21,16 @@
 )
 from cryptography.hazmat.backends.openssl.ec import _sn_to_elliptic_curve
 from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import dsa, padding
+from cryptography.hazmat.primitives.asymmetric import dsa, ec, padding
 from cryptography.hazmat.primitives.ciphers import (
     BlockCipherAlgorithm, Cipher, CipherAlgorithm
 )
 from cryptography.hazmat.primitives.ciphers.algorithms import AES
 from cryptography.hazmat.primitives.ciphers.modes import CBC, CTR, Mode
 
+from ..primitives.fixtures_dsa import DSA_KEY_2048
 from ..primitives.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512
+from ..primitives.test_ec import _skip_curve_unsupported
 from ...utils import load_vectors_from_file, raises_unsupported_algorithm
 
 
@@ -453,6 +455,29 @@
             backend.create_cmac_ctx(FakeAlgorithm())
 
 
+class TestOpenSSLCreateX509CSR(object):
+    @pytest.mark.skipif(
+        backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000,
+        reason="Requires an older OpenSSL. Must be < 1.0.1"
+    )
+    def test_unsupported_dsa_keys(self):
+        private_key = DSA_KEY_2048.private_key(backend)
+
+        with pytest.raises(NotImplementedError):
+            backend.create_x509_csr(object(), private_key, hashes.SHA1())
+
+    @pytest.mark.skipif(
+        backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000,
+        reason="Requires an older OpenSSL. Must be < 1.0.1"
+    )
+    def test_unsupported_ec_keys(self):
+        _skip_curve_unsupported(backend, ec.SECP256R1())
+        private_key = ec.generate_private_key(ec.SECP256R1(), backend)
+
+        with pytest.raises(NotImplementedError):
+            backend.create_x509_csr(object(), private_key, hashes.SHA1())
+
+
 class TestOpenSSLSerialisationWithOpenSSL(object):
     def test_pem_password_cb_buffer_too_small(self):
         ffi_cb, cb = backend._pem_password_cb(b"aa")
diff --git a/tests/test_x509.py b/tests/test_x509.py
index cf3499b..429f2d2 100644
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -20,6 +20,8 @@
 from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
 
+from .hazmat.primitives.fixtures_dsa import DSA_KEY_2048
+from .hazmat.primitives.fixtures_rsa import RSA_KEY_2048
 from .hazmat.primitives.test_ec import _skip_curve_unsupported
 from .utils import load_vectors_from_file
 
@@ -586,7 +588,7 @@
             x509.Extension(
                 x509.OID_BASIC_CONSTRAINTS,
                 True,
-                x509.BasicConstraints(True, 1),
+                x509.BasicConstraints(ca=True, path_length=1),
             ),
         ]
 
@@ -679,6 +681,222 @@
         assert serialized == request_bytes
 
 
+@pytest.mark.requires_backend_interface(interface=X509Backend)
+class TestCertificateSigningRequestBuilder(object):
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    def test_sign_invalid_hash_algorithm(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        builder = x509.CertificateSigningRequestBuilder()
+        with pytest.raises(TypeError):
+            builder.sign(backend, private_key, 'NotAHash')
+
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    def test_build_ca_request_with_rsa(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        request = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+                x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+                x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+                x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+                x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+            ])
+        ).add_extension(
+            x509.BasicConstraints(ca=True, path_length=2), critical=True
+        ).sign(
+            backend, private_key, hashes.SHA1()
+        )
+
+        assert isinstance(request.signature_hash_algorithm, hashes.SHA1)
+        public_key = request.public_key()
+        assert isinstance(public_key, rsa.RSAPublicKey)
+        subject = request.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+        ]
+        basic_constraints = request.extensions.get_extension_for_oid(
+            x509.OID_BASIC_CONSTRAINTS
+        )
+        assert basic_constraints.value.ca is True
+        assert basic_constraints.value.path_length == 2
+
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    def test_build_ca_request_with_unicode(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        request = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+                x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+                x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+                x509.NameAttribute(x509.OID_ORGANIZATION_NAME,
+                                   u'PyCA\U0001f37a'),
+                x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+            ])
+        ).add_extension(
+            x509.BasicConstraints(ca=True, path_length=2), critical=True
+        ).sign(
+            backend, private_key, hashes.SHA1()
+        )
+
+        loaded_request = x509.load_pem_x509_csr(
+            request.public_bytes(encoding=serialization.Encoding.PEM), backend
+        )
+        subject = loaded_request.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA\U0001f37a'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+        ]
+
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    def test_build_nonca_request_with_rsa(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        request = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+                x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+                x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+                x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+                x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+            ])
+        ).add_extension(
+            x509.BasicConstraints(ca=False, path_length=None), critical=True,
+        ).sign(
+            backend, private_key, hashes.SHA1()
+        )
+
+        assert isinstance(request.signature_hash_algorithm, hashes.SHA1)
+        public_key = request.public_key()
+        assert isinstance(public_key, rsa.RSAPublicKey)
+        subject = request.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+        ]
+        basic_constraints = request.extensions.get_extension_for_oid(
+            x509.OID_BASIC_CONSTRAINTS
+        )
+        assert basic_constraints.value.ca is False
+        assert basic_constraints.value.path_length is None
+
+    @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+    def test_build_ca_request_with_ec(self, backend):
+        if backend._lib.OPENSSL_VERSION_NUMBER < 0x10001000:
+            pytest.skip("Requires a newer OpenSSL. Must be >= 1.0.1")
+
+        _skip_curve_unsupported(backend, ec.SECP256R1())
+        private_key = ec.generate_private_key(ec.SECP256R1(), backend)
+
+        request = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+                x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+                x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+                x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+                x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+            ])
+        ).add_extension(
+            x509.BasicConstraints(ca=True, path_length=2), critical=True
+        ).sign(
+            backend, private_key, hashes.SHA1()
+        )
+
+        assert isinstance(request.signature_hash_algorithm, hashes.SHA1)
+        public_key = request.public_key()
+        assert isinstance(public_key, ec.EllipticCurvePublicKey)
+        subject = request.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+        ]
+        basic_constraints = request.extensions.get_extension_for_oid(
+            x509.OID_BASIC_CONSTRAINTS
+        )
+        assert basic_constraints.value.ca is True
+        assert basic_constraints.value.path_length == 2
+
+    @pytest.mark.requires_backend_interface(interface=DSABackend)
+    def test_build_ca_request_with_dsa(self, backend):
+        if backend._lib.OPENSSL_VERSION_NUMBER < 0x10001000:
+            pytest.skip("Requires a newer OpenSSL. Must be >= 1.0.1")
+
+        private_key = DSA_KEY_2048.private_key(backend)
+
+        request = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+                x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+                x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+                x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+                x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+            ])
+        ).add_extension(
+            x509.BasicConstraints(ca=True, path_length=2), critical=True
+        ).sign(
+            backend, private_key, hashes.SHA1()
+        )
+
+        assert isinstance(request.signature_hash_algorithm, hashes.SHA1)
+        public_key = request.public_key()
+        assert isinstance(public_key, dsa.DSAPublicKey)
+        subject = request.subject
+        assert isinstance(subject, x509.Name)
+        assert list(subject) == [
+            x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+        ]
+        basic_constraints = request.extensions.get_extension_for_oid(
+            x509.OID_BASIC_CONSTRAINTS
+        )
+        assert basic_constraints.value.ca is True
+        assert basic_constraints.value.path_length == 2
+
+    def test_add_duplicate_extension(self, backend):
+        builder = x509.CertificateSigningRequestBuilder().add_extension(
+            x509.BasicConstraints(True, 2), critical=True,
+        )
+        with pytest.raises(ValueError):
+            builder.add_extension(
+                x509.BasicConstraints(True, 2), critical=True,
+            )
+
+    def test_set_invalid_subject(self, backend):
+        builder = x509.CertificateSigningRequestBuilder()
+        with pytest.raises(TypeError):
+            builder.subject_name('NotAName')
+
+    def test_add_unsupported_extension(self, backend):
+        builder = x509.CertificateSigningRequestBuilder()
+        with pytest.raises(NotImplementedError):
+            builder.add_extension(
+                x509.AuthorityKeyIdentifier('keyid', None, None),
+                critical=False,
+            )
+
+
 @pytest.mark.requires_backend_interface(interface=DSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)
 class TestDSACertificate(object):