Merge pull request #1706 from reaperhulk/serialize-rsa-public-key

Serialize RSA public keys
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 70fd7a5..8662894 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -107,6 +107,14 @@
   :meth:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization.private_bytes`
   to
   :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization`.
+* Added
+  :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`
+  and deprecated
+  :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithNumbers`.
+* Added
+  :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization.public_bytes`
+  to
+  :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`.
 
 0.7.2 - 2015-01-16
 ~~~~~~~~~~~~~~~~~~
diff --git a/docs/hazmat/primitives/asymmetric/rsa.rst b/docs/hazmat/primitives/asymmetric/rsa.rst
index a8d7bfc..e703310 100644
--- a/docs/hazmat/primitives/asymmetric/rsa.rst
+++ b/docs/hazmat/primitives/asymmetric/rsa.rst
@@ -83,7 +83,7 @@
 Key serialization
 ~~~~~~~~~~~~~~~~~
 
-If you have a key that you've loaded or generated which implements the
+If you have a private key that you've loaded or generated which implements the
 :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`
 interface you can use
 :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`
@@ -113,6 +113,23 @@
     >>> pem.splitlines()[0]
     '-----BEGIN RSA PRIVATE KEY-----'
 
+Similarly, if your public key implements
+:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`
+interface you can use
+:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization.public_bytes`
+to serialize the key.
+
+.. doctest::
+
+    >>> from cryptography.hazmat.primitives import serialization
+    >>> public_key = private_key.public_key()
+    >>> pem = public_key.public_bytes(
+    ...    encoding=serialization.Encoding.PEM,
+    ...    format=serialization.PublicFormat.SubjectPublicKeyInfo
+    ... )
+    >>> pem.splitlines()[0]
+    '-----BEGIN PUBLIC KEY-----'
+
 Signing
 ~~~~~~~
 
@@ -626,6 +643,42 @@
             instance.
 
 
+.. class:: RSAPublicKeyWithSerialization
+
+    .. versionadded:: 0.8
+
+    Extends :class:`RSAPublicKey`.
+
+    .. method:: public_numbers()
+
+        Create a
+        :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicNumbers`
+        object.
+
+        :returns: An
+            :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicNumbers`
+            instance.
+
+    .. method:: public_bytes(encoding, format)
+
+        Allows serialization of the key to bytes. Encoding (
+        :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM` or
+        :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`) and
+        format (
+        :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`
+        or
+        :attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.PKCS1`)
+        are chosen to define the exact serialization.
+
+        :param encoding: A value from the
+            :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
+
+        :param format: A value from the
+            :class:`~cryptography.hazmat.primitives.serialization.PublicFormat` enum.
+
+        :return bytes: Serialized key.
+
+
 .. _`RSA`: https://en.wikipedia.org/wiki/RSA_(cryptosystem)
 .. _`public-key`: https://en.wikipedia.org/wiki/Public-key_cryptography
 .. _`specific mathematical properties`: https://en.wikipedia.org/wiki/RSA_(cryptosystem)#Key_generation
diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst
index 4a2aedc..fb8c93a 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -309,6 +309,25 @@
         encryption. Choose this unless you have explicit legacy compatibility
         requirements.
 
+.. class:: PublicFormat
+
+    .. versionadded:: 0.8
+
+    An enumeration for public key formats. Used with the ``public_bytes``
+    method available on
+    :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`.
+
+    .. attribute:: SubjectPublicKeyInfo
+
+        This is the typical public key format. It consists of an algorithm
+        identifier and the public key as a bit string. Choose this unless
+        you have specific needs.
+
+    .. attribute:: PKCS1
+
+        Just the public key elements (without the algorithm identifier). This
+        format is RSA only, but is used by some older systems.
+
 Serialization Encodings
 ~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 42dcc0f..f33aba9 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -1184,6 +1184,32 @@
         assert res == 1
         return self._read_mem_bio(bio)
 
+    def _public_key_bytes(self, encoding, format, pkcs1_write_func, evp_pkey,
+                          cdata):
+        if not isinstance(encoding, serialization.Encoding):
+            raise TypeError("encoding must be an item from the Encoding enum")
+
+        if not isinstance(format, serialization.PublicFormat):
+            raise TypeError(
+                "format must be an item from the PublicFormat enum"
+            )
+
+        # This is a temporary check until we land DER serialization.
+        if encoding is not serialization.Encoding.PEM:
+            raise ValueError("Only PEM encoding is supported by this backend")
+
+        if format is serialization.PublicFormat.SubjectPublicKeyInfo:
+            write_bio = self._lib.PEM_write_bio_PUBKEY
+            key = evp_pkey
+        elif format is serialization.PublicFormat.PKCS1:
+            write_bio = pkcs1_write_func
+            key = cdata
+
+        bio = self._create_mem_bio()
+        res = write_bio(bio, key)
+        assert res == 1
+        return self._read_mem_bio(bio)
+
 
 class GetCipherByName(object):
     def __init__(self, fmt):
diff --git a/src/cryptography/hazmat/backends/openssl/rsa.py b/src/cryptography/hazmat/backends/openssl/rsa.py
index 0470c3f..25168c2 100644
--- a/src/cryptography/hazmat/backends/openssl/rsa.py
+++ b/src/cryptography/hazmat/backends/openssl/rsa.py
@@ -19,7 +19,7 @@
 )
 from cryptography.hazmat.primitives.asymmetric.rsa import (
     RSAPrivateKeyWithNumbers, RSAPrivateKeyWithSerialization,
-    RSAPublicKeyWithNumbers
+    RSAPublicKeyWithSerialization
 )
 
 
@@ -572,7 +572,7 @@
         )
 
 
-@utils.register_interface(RSAPublicKeyWithNumbers)
+@utils.register_interface(RSAPublicKeyWithSerialization)
 class _RSAPublicKey(object):
     def __init__(self, backend, rsa_cdata):
         self._backend = backend
@@ -604,3 +604,12 @@
             e=self._backend._bn_to_int(self._rsa_cdata.e),
             n=self._backend._bn_to_int(self._rsa_cdata.n),
         )
+
+    def public_bytes(self, encoding, format):
+        return self._backend._public_key_bytes(
+            encoding,
+            format,
+            self._backend._lib.PEM_write_bio_RSAPublicKey,
+            self._evp_pkey,
+            self._rsa_cdata
+        )
diff --git a/src/cryptography/hazmat/primitives/asymmetric/rsa.py b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
index 4963d85..8adc745 100644
--- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
@@ -89,13 +89,30 @@
 
 
 @six.add_metaclass(abc.ABCMeta)
-class RSAPublicKeyWithNumbers(RSAPublicKey):
+class RSAPublicKeyWithSerialization(RSAPublicKey):
     @abc.abstractmethod
     def public_numbers(self):
         """
         Returns an RSAPublicNumbers
         """
 
+    @abc.abstractmethod
+    def public_bytes(self, encoding, format):
+        """
+        Returns the key serialized as bytes.
+        """
+
+
+RSAPublicKeyWithNumbers = utils.deprecated(
+    RSAPublicKeyWithSerialization,
+    __name__,
+    (
+        "The RSAPublicKeyWithNumbers interface has been renamed to "
+        "RSAPublicKeyWithSerialization"
+    ),
+    utils.DeprecatedIn08
+)
+
 
 def generate_private_key(public_exponent, key_size, backend):
     if not isinstance(backend, RSABackend):
diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py
index 7e36319..8699fa9 100644
--- a/src/cryptography/hazmat/primitives/serialization.py
+++ b/src/cryptography/hazmat/primitives/serialization.py
@@ -179,6 +179,11 @@
     TraditionalOpenSSL = "TraditionalOpenSSL"
 
 
+class PublicFormat(Enum):
+    SubjectPublicKeyInfo = "X.509 subjectPublicKeyInfo with PKCS#1"
+    PKCS1 = "Raw PKCS#1"
+
+
 @six.add_metaclass(abc.ABCMeta)
 class KeySerializationEncryption(object):
     pass
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index 8ee9d24..ba0a2ba 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -508,7 +508,7 @@
                 serialization.BestAvailableEncryption(password)
             )
 
-    def test_unsupported_key_encoding(self):
+    def test_unsupported_private_key_encoding(self):
         key = RSA_KEY_2048.private_key(backend)
         with pytest.raises(ValueError):
             key.private_bytes(
@@ -516,3 +516,11 @@
                 serialization.PrivateFormat.PKCS8,
                 serialization.NoEncryption()
             )
+
+    def test_unsupported_public_key_encoding(self):
+        key = RSA_KEY_2048.private_key(backend).public_key()
+        with pytest.raises(ValueError):
+            key.public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo
+            )
diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py
index 890a1d4..e6d0ac2 100644
--- a/tests/hazmat/primitives/test_rsa.py
+++ b/tests/hazmat/primitives/test_rsa.py
@@ -86,7 +86,10 @@
 
 
 def _skip_if_no_serialization(key, backend):
-    if not isinstance(key, rsa.RSAPrivateKeyWithSerialization):
+    if not isinstance(
+        key,
+        (rsa.RSAPrivateKeyWithSerialization, rsa.RSAPublicKeyWithSerialization)
+    ):
         pytest.skip(
             "{0} does not support RSA key serialization".format(backend)
         )
@@ -1748,7 +1751,7 @@
 
 @pytest.mark.requires_backend_interface(interface=RSABackend)
 @pytest.mark.requires_backend_interface(interface=PEMSerializationBackend)
-class TestRSAPEMWriter(object):
+class TestRSAPEMPrivateKeySerialization(object):
     @pytest.mark.parametrize(
         ("fmt", "password"),
         itertools.product(
@@ -1857,3 +1860,45 @@
                 serialization.PrivateFormat.TraditionalOpenSSL,
                 DummyKeyEncryption()
             )
+
+
+@pytest.mark.requires_backend_interface(interface=RSABackend)
+@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend)
+class TestRSAPEMPublicKeySerialization(object):
+    def test_public_bytes_unencrypted_pem(self, backend):
+        key_bytes = load_vectors_from_file(
+            os.path.join("asymmetric", "PKCS8", "unenc-rsa-pkcs8.pub.pem"),
+            lambda pemfile: pemfile.read().encode()
+        )
+        key = serialization.load_pem_public_key(key_bytes, backend)
+        _skip_if_no_serialization(key, backend)
+        serialized = key.public_bytes(
+            serialization.Encoding.PEM,
+            serialization.PublicFormat.SubjectPublicKeyInfo,
+        )
+        assert serialized == key_bytes
+
+    def test_public_bytes_pkcs1_unencrypted_pem(self, backend):
+        key_bytes = load_vectors_from_file(
+            os.path.join("asymmetric", "public", "PKCS1", "rsa.pub.pem"),
+            lambda pemfile: pemfile.read().encode()
+        )
+        key = serialization.load_pem_public_key(key_bytes, backend)
+        _skip_if_no_serialization(key, backend)
+        serialized = key.public_bytes(
+            serialization.Encoding.PEM,
+            serialization.PublicFormat.PKCS1,
+        )
+        assert serialized == key_bytes
+
+    def test_public_bytes_invalid_encoding(self, backend):
+        key = RSA_KEY_2048.private_key(backend).public_key()
+        _skip_if_no_serialization(key, backend)
+        with pytest.raises(TypeError):
+            key.public_bytes("notencoding", serialization.PublicFormat.PKCS1)
+
+    def test_public_bytes_invalid_format(self, backend):
+        key = RSA_KEY_2048.private_key(backend).public_key()
+        _skip_if_no_serialization(key, backend)
+        with pytest.raises(TypeError):
+            key.public_bytes(serialization.Encoding.PEM, "invalidformat")