Merge pull request #1963 from AndreLouisCaron/csr-encoding

Adds support for writing CSRs.
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e2f1890..4d7a9a8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,10 @@
 
 .. note:: This version is not yet released and is under active development.
 
+* Support serialization of certificate signing requests using the
+  ``public_bytes`` method of
+  :class:`~cryptography.x509.CertificateSigningRequest`.
+
 0.9 - 2015-05-13
 ~~~~~~~~~~~~~~~~
 
diff --git a/docs/x509.rst b/docs/x509.rst
index 3f1af86..c8505a8 100644
--- a/docs/x509.rst
+++ b/docs/x509.rst
@@ -366,6 +366,17 @@
             >>> isinstance(csr.signature_hash_algorithm, hashes.SHA1)
             True
 
+    .. method:: public_bytes(encoding)
+
+        :param encoding: The
+            :class:`~cryptography.hazmat.primitives.serialization.Encoding`
+            that will be used to serialize the certificate request.
+
+        :return bytes: The data that can be written to a file or sent
+            over the network to be signed by the certificate
+            authority.
+
+
 .. class:: Name
 
     .. versionadded:: 0.8
diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py
index 6db6fc9..7204136 100644
--- a/src/cryptography/hazmat/backends/openssl/x509.py
+++ b/src/cryptography/hazmat/backends/openssl/x509.py
@@ -25,7 +25,7 @@
 
 from cryptography import utils, x509
 from cryptography.exceptions import UnsupportedAlgorithm
-from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import hashes, serialization
 
 
 def _obj2txt(backend, obj):
@@ -689,3 +689,17 @@
             extensions.append(x509.Extension(oid, critical, value))
 
         return x509.Extensions(extensions)
+
+    def public_bytes(self, encoding):
+        if not isinstance(encoding, serialization.Encoding):
+            raise TypeError("encoding must be an item from the Encoding enum")
+
+        bio = self._backend._create_mem_bio()
+        if encoding is serialization.Encoding.PEM:
+            res = self._backend._lib.PEM_write_bio_X509_REQ(
+                bio, self._x509_req
+            )
+        elif encoding is serialization.Encoding.DER:
+            res = self._backend._lib.i2d_X509_REQ_bio(bio, self._x509_req)
+        assert res == 1
+        return self._backend._read_mem_bio(bio)
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
index 7ac0662..9a3295c 100644
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -1194,3 +1194,9 @@
         """
         Returns the extensions in the signing request.
         """
+
+    @abc.abstractmethod
+    def public_bytes(self, encoding):
+        """
+        Encodes the request to PEM or DER format.
+        """
diff --git a/tests/test_x509.py b/tests/test_x509.py
index 47c1c64..72fc9d4 100644
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -15,7 +15,7 @@
 from cryptography.hazmat.backends.interfaces import (
     DSABackend, EllipticCurveBackend, RSABackend, X509Backend
 )
-from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
 
 from .hazmat.primitives.test_ec import _skip_curve_unsupported
@@ -471,6 +471,94 @@
             ),
         ]
 
+    def test_public_bytes_pem(self, backend):
+        # Load an existing CSR.
+        request = _load_cert(
+            os.path.join("x509", "requests", "rsa_sha1.pem"),
+            x509.load_pem_x509_csr,
+            backend
+        )
+
+        # Encode it to PEM and load it back.
+        request = x509.load_pem_x509_csr(request.public_bytes(
+            encoding=serialization.Encoding.PEM,
+        ), backend)
+
+        # We should recover what we had to start with.
+        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, 'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'PyCA'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'cryptography.io'),
+        ]
+
+    def test_public_bytes_der(self, backend):
+        # Load an existing CSR.
+        request = _load_cert(
+            os.path.join("x509", "requests", "rsa_sha1.pem"),
+            x509.load_pem_x509_csr,
+            backend
+        )
+
+        # Encode it to DER and load it back.
+        request = x509.load_der_x509_csr(request.public_bytes(
+            encoding=serialization.Encoding.DER,
+        ), backend)
+
+        # We should recover what we had to start with.
+        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, 'US'),
+            x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'Texas'),
+            x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Austin'),
+            x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'PyCA'),
+            x509.NameAttribute(x509.OID_COMMON_NAME, 'cryptography.io'),
+        ]
+
+    def test_public_bytes_invalid_encoding(self, backend):
+        request = _load_cert(
+            os.path.join("x509", "requests", "rsa_sha1.pem"),
+            x509.load_pem_x509_csr,
+            backend
+        )
+
+        with pytest.raises(TypeError):
+            request.public_bytes('NotAnEncoding')
+
+    @pytest.mark.parametrize(
+        ("request_path", "loader_func", "encoding"),
+        [
+            (
+                os.path.join("x509", "requests", "rsa_sha1.pem"),
+                x509.load_pem_x509_csr,
+                serialization.Encoding.PEM,
+            ),
+            (
+                os.path.join("x509", "requests", "rsa_sha1.der"),
+                x509.load_der_x509_csr,
+                serialization.Encoding.DER,
+            ),
+        ]
+    )
+    def test_public_bytes_match(self, request_path, loader_func, encoding,
+                                backend):
+        request_bytes = load_vectors_from_file(
+            request_path, lambda pemfile: pemfile.read(), mode="rb"
+        )
+        request = loader_func(request_bytes, backend)
+        serialized = request.public_bytes(encoding)
+        assert serialized == request_bytes
+
 
 @pytest.mark.requires_backend_interface(interface=DSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)