Support for traditional OpenSSL and PKCS8 RSA private key serialization
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index da529f6..66a308a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -78,6 +78,14 @@
   support loading DER encoded public keys.
 * Fixed building against LibreSSL, a compile-time substitute for OpenSSL.
 * FreeBSD 9.2 was removed from the continuous integration system.
+* Added
+  :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`
+  and deprecated
+  :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithNumbers`.
+* Added
+  :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.dump`
+  to
+  :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`.
 
 0.7.2 - 2015-01-16
 ~~~~~~~~~~~~~~~~~~
diff --git a/docs/hazmat/primitives/asymmetric/rsa.rst b/docs/hazmat/primitives/asymmetric/rsa.rst
index fd97d75..66bb37c 100644
--- a/docs/hazmat/primitives/asymmetric/rsa.rst
+++ b/docs/hazmat/primitives/asymmetric/rsa.rst
@@ -80,6 +80,37 @@
 There is also support for :func:`loading public keys in the SSH format
 <cryptography.hazmat.primitives.serialization.load_ssh_public_key>`.
 
+Key serialization
+~~~~~~~~~~~~~~~~~
+
+If you have a previously loaded or generated key that has the
+:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`
+interface you can use
+:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.dump`
+to serialize the key.
+
+.. doctest::
+
+    >>> from cryptography.hazmat.primitives import serialization
+    >>> pem = private_key.dump(
+    ...    serialization.PKCS8(serialization.Encoding.PEM),
+    ...    serialization.BestAvailable(b'passwordgoeshere')
+    ... )
+    >>> pem.splitlines()[0]
+    '-----BEGIN ENCRYPTED PRIVATE KEY-----'
+
+It is also possible to serialize without encryption using
+:class:`~cryptography.hazmat.primitives.serialization.NoEncryption`.
+
+.. doctest::
+
+    >>> pem = private_key.dump(
+    ...    serialization.TraditionalOpenSSL(serialization.Encoding.PEM),
+    ...    serialization.NoEncryption()
+    ... )
+    >>> pem.splitlines()[0]
+    '-----BEGIN RSA PRIVATE KEY-----'
+
 Signing
 ~~~~~~~
 
@@ -485,6 +516,37 @@
             instance.
 
 
+.. class:: RSAPrivateKeyWithSerialization
+
+    .. versionadded:: 0.8
+
+    Extends :class:`RSAPrivateKey`.
+
+    .. method:: private_numbers()
+
+        Create a
+        :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateNumbers`
+        object.
+
+        :returns: An
+            :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateNumbers`
+            instance.
+
+    .. method:: dump(serializer, encryption_type)
+
+        Dump the key to PEM encoded bytes using the serializer provided.
+
+        :param serializer: An instance of
+            :class:`~cryptography.hazmat.primitives.serialization.TraditionalOpenSSL`
+            or :class:`~cryptography.hazmat.primitives.serialization.PKCS8`
+
+        :param encryption_type: An instance of an object conforming to the
+            :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption`
+            interface.
+
+        :return bytes: Serialized key.
+
+
 .. class:: RSAPublicKey
 
     .. versionadded:: 0.2
diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst
index 87f3c0b..68eaf02 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -3,7 +3,7 @@
 Key Serialization
 =================
 
-.. currentmodule:: cryptography.hazmat.primitives.serialization
+.. module:: cryptography.hazmat.primitives.serialization
 
 .. testsetup::
 
@@ -282,3 +282,71 @@
 
     :raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized
         key is of a type that is not supported.
+
+Serializers
+~~~~~~~~~~~
+
+Instances of these classes can be passed to methods like
+:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.dump`.
+
+.. class:: PKCS8(encoding)
+
+    .. versionadded:: 0.8
+
+    A serializer for the PKCS #8 format.
+
+    :param encoding: A value from the
+        :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
+
+.. class:: TraditionalOpenSSL(encoding)
+
+    .. versionadded:: 0.8
+
+    A serializer for the traditional OpenSSL (sometimes known as PKCS #1)
+    format.
+
+    :param encoding: A value from the
+        :class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.
+
+
+Serialization Encryption Types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: KeySerializationEncryption
+
+    Objects with this interface are usable as encryption types with methods
+    like
+    :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.dump`.
+    All other classes in this section represent the available choices for
+    encryption and have this interface.
+
+.. class:: BestAvailable
+
+    Encrypt using the best available encryption for a given key's backend.
+    This is a curated encryption choice and the algorithm may change over
+    time.
+
+    :param bytes password: The password to use for encryption.
+
+.. class:: NoEncryption
+
+    Do not encrypt.
+
+
+Utility Classes
+~~~~~~~~~~~~~~~
+
+.. class:: Encoding
+
+    .. versionadded:: 0.8
+
+    An enumeration for encoding types. Used by :class:`PKCS8` and
+    :class:`TraditionalOpenSSL`.
+
+    .. attribute:: PEM
+
+        For PEM format. This is a base64 format with delimiters.
+
+    .. attribute:: DER
+
+        For DER format. This is a binary format.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index ddd3789..6e54537 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -44,6 +44,8 @@
 pyOpenSSL
 Schneier
 scrypt
+Serializers
+serializer
 Solaris
 Tanja
 testability
diff --git a/src/cryptography/hazmat/backends/openssl/rsa.py b/src/cryptography/hazmat/backends/openssl/rsa.py
index 00ddcda..1357889 100644
--- a/src/cryptography/hazmat/backends/openssl/rsa.py
+++ b/src/cryptography/hazmat/backends/openssl/rsa.py
@@ -17,8 +17,13 @@
 from cryptography.hazmat.primitives.asymmetric.padding import (
     AsymmetricPadding, MGF1, OAEP, PKCS1v15, PSS
 )
-from cryptography.hazmat.primitives.interfaces import (
-    RSAPrivateKeyWithNumbers, RSAPublicKeyWithNumbers
+from cryptography.hazmat.primitives.asymmetric.rsa import (
+    RSAPrivateKeyWithNumbers, RSAPrivateKeyWithSerialization,
+    RSAPublicKeyWithNumbers
+)
+from cryptography.hazmat.primitives.serialization import (
+    BestAvailable, Encoding, KeySerializationEncryption, NoEncryption, PKCS8,
+    TraditionalOpenSSL
 )
 
 
@@ -507,6 +512,7 @@
 
 
 @utils.register_interface(RSAPrivateKeyWithNumbers)
+@utils.register_interface(RSAPrivateKeyWithSerialization)
 class _RSAPrivateKey(object):
     def __init__(self, backend, rsa_cdata):
         self._backend = backend
@@ -559,6 +565,57 @@
             )
         )
 
+    def dump(self, serializer, encryption_algorithm):
+        if isinstance(serializer, PKCS8):
+            write_bio = self._backend._lib.PEM_write_bio_PKCS8PrivateKey
+            key = self._evp_pkey
+        elif isinstance(serializer, TraditionalOpenSSL):
+            write_bio = self._backend._lib.PEM_write_bio_RSAPrivateKey
+            key = self._rsa_cdata
+        else:
+            raise TypeError("serializer must be PKCS8 or TraditionalOpenSSL")
+
+        if serializer.encoding != Encoding.PEM:
+            raise ValueError("Only PEM encoding is supported by this backend")
+
+        if not isinstance(encryption_algorithm, KeySerializationEncryption):
+            raise TypeError(
+                "Encryption algorithm must be a KeySerializationEncryption "
+                "instance"
+            )
+
+        if isinstance(encryption_algorithm, NoEncryption):
+            password = b""
+            passlen = 0
+            evp_cipher = self._backend._ffi.NULL
+        elif isinstance(encryption_algorithm, BestAvailable):
+            # This is a curated value that we will update over time.
+            evp_cipher = self._backend._lib.EVP_get_cipherbyname(
+                b"aes-256-cbc"
+            )
+            password = encryption_algorithm.password
+            passlen = len(password)
+            if passlen > 1023:
+                raise ValueError(
+                    "Passwords longer than 1023 bytes are not supported by "
+                    "this backend"
+                )
+        else:
+            raise ValueError("Unsupported encryption type")
+
+        bio = self._backend._create_mem_bio()
+        res = write_bio(
+            bio,
+            key,
+            evp_cipher,
+            password,
+            passlen,
+            self._backend._ffi.NULL,
+            self._backend._ffi.NULL
+        )
+        assert res == 1
+        return self._backend._read_mem_bio(bio)
+
 
 @utils.register_interface(RSAPublicKeyWithNumbers)
 class _RSAPublicKey(object):
diff --git a/src/cryptography/hazmat/primitives/asymmetric/rsa.py b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
index 332ad2c..e994a9c 100644
--- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
@@ -42,13 +42,23 @@
 
 
 @six.add_metaclass(abc.ABCMeta)
-class RSAPrivateKeyWithNumbers(RSAPrivateKey):
+class RSAPrivateKeyWithSerialization(RSAPrivateKey):
     @abc.abstractmethod
     def private_numbers(self):
         """
         Returns an RSAPrivateNumbers.
         """
 
+    @abc.abstractmethod
+    def dump(self, serializer, encryption_algorithm):
+        """
+        Returns the PEM encoded key.
+        """
+
+
+# DeprecatedIn08
+RSAPrivateKeyWithNumbers = RSAPrivateKeyWithSerialization
+
 
 @six.add_metaclass(abc.ABCMeta)
 class RSAPublicKey(object):
diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py
index 0f9506e..9bfbc6b 100644
--- a/src/cryptography/hazmat/primitives/serialization.py
+++ b/src/cryptography/hazmat/primitives/serialization.py
@@ -4,11 +4,14 @@
 
 from __future__ import absolute_import, division, print_function
 
+import abc
 import base64
 import struct
+from enum import Enum
 
 import six
 
+from cryptography import utils
 from cryptography.exceptions import UnsupportedAlgorithm
 from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
 
@@ -164,3 +167,47 @@
             data = data[4:]
 
         return result
+
+
+class Encoding(Enum):
+    PEM = "PEM"
+    DER = "DER"
+
+
+class PKCS8(object):
+    def __init__(self, encoding):
+        if not isinstance(encoding, Encoding):
+            raise TypeError(
+                "Encoding must be an element from the Encoding enum"
+            )
+
+        self.encoding = encoding
+
+
+class TraditionalOpenSSL(object):
+    def __init__(self, encoding):
+        if not isinstance(encoding, Encoding):
+            raise TypeError(
+                "Encoding must be an element from the Encoding enum"
+            )
+
+        self.encoding = encoding
+
+
+@six.add_metaclass(abc.ABCMeta)
+class KeySerializationEncryption(object):
+    pass
+
+
+@utils.register_interface(KeySerializationEncryption)
+class BestAvailable(object):
+    def __init__(self, password):
+        if not isinstance(password, bytes) or len(password) == 0:
+            raise ValueError("Password must be 1 or more bytes.")
+
+        self.password = password
+
+
+@utils.register_interface(KeySerializationEncryption)
+class NoEncryption(object):
+    pass
diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py
index 78dcc1c..77b6d25 100644
--- a/src/cryptography/utils.py
+++ b/src/cryptography/utils.py
@@ -12,6 +12,7 @@
 
 # DeprecatedIn07 objects exist. This comment exists to remind developers to
 # look for them when it's time for the ninth release cycle deprecation dance.
+# DeprecatedIn08 objects also exist.
 
 DeprecatedIn08 = PendingDeprecationWarning
 
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index 0e4d75e..35b7c5c 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -15,11 +15,12 @@
 
 from cryptography import utils
 from cryptography.exceptions import InternalError, _Reasons
+from cryptography.hazmat.backends.interfaces import RSABackend
 from cryptography.hazmat.backends.openssl.backend import (
     Backend, backend
 )
 from cryptography.hazmat.backends.openssl.ec import _sn_to_elliptic_curve
-from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import dsa, padding
 from cryptography.hazmat.primitives.ciphers import (
     BlockCipherAlgorithm, Cipher, CipherAlgorithm
@@ -27,7 +28,7 @@
 from cryptography.hazmat.primitives.ciphers.algorithms import AES
 from cryptography.hazmat.primitives.ciphers.modes import CBC, CTR, Mode
 
-from ..primitives.fixtures_rsa import RSA_KEY_512
+from ..primitives.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512
 from ...utils import load_vectors_from_file, raises_unsupported_algorithm
 
 
@@ -493,3 +494,27 @@
     def test_sn_to_elliptic_curve_not_supported(self):
         with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_ELLIPTIC_CURVE):
             _sn_to_elliptic_curve(backend, b"fake")
+
+
+@pytest.mark.requires_backend_interface(interface=RSABackend)
+class TestRSAPEMSerialization(object):
+    def test_password_length_limit(self):
+        password = b"x" * 1024
+        key = RSA_KEY_2048.private_key(backend)
+        with pytest.raises(ValueError):
+            key.dump(
+                serialization.PKCS8(
+                    serialization.Encoding.PEM
+                ),
+                serialization.BestAvailable(password)
+            )
+
+    def test_unsupported_key_encoding(self):
+        key = RSA_KEY_2048.private_key(backend)
+        with pytest.raises(ValueError):
+            key.dump(
+                serialization.PKCS8(
+                    serialization.Encoding.DER
+                ),
+                serialization.NoEncryption()
+            )
diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py
index 7418301..72bc08a 100644
--- a/tests/hazmat/primitives/test_rsa.py
+++ b/tests/hazmat/primitives/test_rsa.py
@@ -15,8 +15,10 @@
 from cryptography.exceptions import (
     AlreadyFinalized, InvalidSignature, _Reasons
 )
-from cryptography.hazmat.backends.interfaces import RSABackend
-from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.backends.interfaces import (
+    PEMSerializationBackend, RSABackend
+)
+from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import padding, rsa
 from cryptography.hazmat.primitives.asymmetric.rsa import (
     RSAPrivateNumbers, RSAPublicNumbers
@@ -46,6 +48,11 @@
     _salt_length = 0
 
 
+@utils.register_interface(serialization.KeySerializationEncryption)
+class DummyKeyEncryption(object):
+    pass
+
+
 def _flatten_pkcs1_examples(vectors):
     flattened_vectors = []
     for vector in vectors:
@@ -78,6 +85,18 @@
     )
 
 
+def _skip_if_no_serialization(key, backend):
+    if not isinstance(key, rsa.RSAPrivateKeyWithSerialization):
+        pytest.skip(
+            "{0} does not support RSA key serialization".format(backend)
+        )
+
+
+def test_skip_if_no_serialization():
+    with pytest.raises(pytest.skip.Exception):
+        _skip_if_no_serialization("notakeywithserialization", "backend")
+
+
 @pytest.mark.requires_backend_interface(interface=RSABackend)
 class TestRSA(object):
     @pytest.mark.parametrize(
@@ -1725,3 +1744,79 @@
     def test_invalid_recover_prime_factors(self):
         with pytest.raises(ValueError):
             rsa.rsa_recover_prime_factors(34, 3, 7)
+
+
+@pytest.mark.requires_backend_interface(interface=RSABackend)
+@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend)
+class TestRSAPEMWriter(object):
+    @pytest.mark.parametrize(
+        ("serializer", "password"),
+        itertools.product(
+            [serialization.TraditionalOpenSSL, serialization.PKCS8],
+            [
+                b"s",
+                b"longerpassword",
+                b"!*$&(@#$*&($T@%_somesymbols",
+                b"\x01" * 1000,
+            ]
+        )
+    )
+    def test_dump_encrypted_pem(self, backend, serializer, password):
+        key = RSA_KEY_2048.private_key(backend)
+        _skip_if_no_serialization(key, backend)
+        serialized = key.dump(
+            serializer(serialization.Encoding.PEM),
+            serialization.BestAvailable(password)
+        )
+        loaded_key = serialization.load_pem_private_key(
+            serialized, password, backend
+        )
+        loaded_priv_num = loaded_key.private_numbers()
+        priv_num = key.private_numbers()
+        assert loaded_priv_num == priv_num
+
+    @pytest.mark.parametrize(
+        "serializer",
+        (serialization.TraditionalOpenSSL, serialization.PKCS8),
+    )
+    def test_dump_unencrypted_pem(self, backend, serializer):
+        key = RSA_KEY_2048.private_key(backend)
+        _skip_if_no_serialization(key, backend)
+        serialized = key.dump(
+            serializer(serialization.Encoding.PEM),
+            serialization.NoEncryption()
+        )
+        loaded_key = serialization.load_pem_private_key(
+            serialized, None, backend
+        )
+        loaded_priv_num = loaded_key.private_numbers()
+        priv_num = key.private_numbers()
+        assert loaded_priv_num == priv_num
+
+    def test_dump_invalid_serializer(self, backend):
+        key = RSA_KEY_2048.private_key(backend)
+        _skip_if_no_serialization(key, backend)
+        with pytest.raises(TypeError):
+            key.dump("notaserializer", serialization.NoEncryption())
+
+    def test_dump_invalid_encryption_algorithm(self, backend):
+        key = RSA_KEY_2048.private_key(backend)
+        _skip_if_no_serialization(key, backend)
+        with pytest.raises(TypeError):
+            key.dump(
+                serialization.TraditionalOpenSSL(
+                    serialization.Encoding.PEM
+                ),
+                "notanencalg"
+            )
+
+    def test_dump_unsupported_encryption_type(self, backend):
+        key = RSA_KEY_2048.private_key(backend)
+        _skip_if_no_serialization(key, backend)
+        with pytest.raises(ValueError):
+            key.dump(
+                serialization.TraditionalOpenSSL(
+                    serialization.Encoding.PEM
+                ),
+                DummyKeyEncryption()
+            )
diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py
index a17aac4..2a5fb21 100644
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -18,8 +18,9 @@
 )
 from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
 from cryptography.hazmat.primitives.serialization import (
-    load_der_private_key, load_der_public_key, load_pem_private_key,
-    load_pem_public_key, load_ssh_public_key
+    BestAvailable, Encoding, PKCS8, TraditionalOpenSSL, load_der_private_key,
+    load_der_public_key, load_pem_private_key, load_pem_public_key,
+    load_ssh_public_key
 )
 
 
@@ -1159,3 +1160,27 @@
         )
         with pytest.raises(ValueError):
             load_ssh_public_key(ssh_key, backend)
+
+
+@pytest.mark.parametrize(
+    "serializer",
+    [PKCS8, TraditionalOpenSSL]
+)
+class TestSerializers(object):
+    def test_invalid_encoding(self, serializer):
+        with pytest.raises(TypeError):
+            serializer("thing")
+
+    def test_valid_params(self, serializer):
+        fmt = serializer(Encoding.PEM)
+        assert isinstance(fmt, (PKCS8, TraditionalOpenSSL))
+
+
+class TestKeySerializationEncryptionTypes(object):
+    def test_non_bytes_password(self):
+        with pytest.raises(ValueError):
+            BestAvailable(object())
+
+    def test_encryption_with_zero_length_password(self):
+        with pytest.raises(ValueError):
+            BestAvailable(b"")