aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst8
-rw-r--r--docs/hazmat/primitives/asymmetric/rsa.rst62
-rw-r--r--docs/hazmat/primitives/asymmetric/serialization.rst70
-rw-r--r--docs/spelling_wordlist.txt2
-rw-r--r--src/cryptography/hazmat/backends/openssl/rsa.py61
-rw-r--r--src/cryptography/hazmat/primitives/asymmetric/rsa.py12
-rw-r--r--src/cryptography/hazmat/primitives/serialization.py47
-rw-r--r--src/cryptography/utils.py1
-rw-r--r--tests/hazmat/backends/test_openssl.py29
-rw-r--r--tests/hazmat/primitives/test_rsa.py99
-rw-r--r--tests/hazmat/primitives/test_serialization.py29
11 files changed, 410 insertions, 10 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index da529f68..66a308a5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -78,6 +78,14 @@ Changelog
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 fd97d75b..66bb37c9 100644
--- a/docs/hazmat/primitives/asymmetric/rsa.rst
+++ b/docs/hazmat/primitives/asymmetric/rsa.rst
@@ -80,6 +80,37 @@ password. If the key is encrypted we can pass a ``bytes`` object as the
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 @@ Key interfaces
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 87f3c0b0..68eaf021 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 @@ DSA keys look almost identical but begin with ``ssh-dss`` rather than
: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 ddd37897..6e545370 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -44,6 +44,8 @@ pseudorandom
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 00ddcda3..1357889f 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 import (
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 @@ class _RSAVerificationContext(object):
@utils.register_interface(RSAPrivateKeyWithNumbers)
+@utils.register_interface(RSAPrivateKeyWithSerialization)
class _RSAPrivateKey(object):
def __init__(self, backend, rsa_cdata):
self._backend = backend
@@ -559,6 +565,57 @@ class _RSAPrivateKey(object):
)
)
+ 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 332ad2c3..e994a9cc 100644
--- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py
@@ -42,13 +42,23 @@ class RSAPrivateKey(object):
@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 0f9506e1..9bfbc6b7 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 @@ else:
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 78dcc1ca..77b6d253 100644
--- a/src/cryptography/utils.py
+++ b/src/cryptography/utils.py
@@ -12,6 +12,7 @@ import warnings
# 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 0e4d75ed..35b7c5c3 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -15,11 +15,12 @@ import pytest
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 import (
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 @@ class TestOpenSSLEllipticCurve(object):
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 74183010..72bc08ad 100644
--- a/tests/hazmat/primitives/test_rsa.py
+++ b/tests/hazmat/primitives/test_rsa.py
@@ -15,8 +15,10 @@ from cryptography import utils
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 @@ class DummyMGF(object):
_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 test_modular_inverse():
)
+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 @@ class TestRSAPrimeFactorRecovery(object):
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 a17aac4b..2a5fb21d 100644
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -18,8 +18,9 @@ from cryptography.hazmat.backends.interfaces import (
)
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 @@ class TestECDSASSHSerialization(object):
)
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"")