diff options
-rw-r--r-- | CHANGELOG.rst | 8 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/rsa.rst | 76 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 66 | ||||
-rw-r--r-- | docs/spelling_wordlist.txt | 2 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/rsa.py | 66 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/asymmetric/rsa.py | 19 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/interfaces/__init__.py | 5 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/serialization.py | 32 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl.py | 27 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_rsa.py | 133 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_serialization.py | 14 |
11 files changed, 436 insertions, 12 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da529f68..f0e373be 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.private_bytes` + 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..924696db 100644 --- a/docs/hazmat/primitives/asymmetric/rsa.rst +++ b/docs/hazmat/primitives/asymmetric/rsa.rst @@ -80,6 +80,39 @@ 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 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` +to serialize the key. + +.. doctest:: + + >>> from cryptography.hazmat.primitives import serialization + >>> pem = private_key.private_bytes( + ... encoding=serialization.Encoding.PEM, + ... format=serialization.Format.PKCS8, + ... encryption_algorithm=serialization.BestAvailableEncryption(b'mypassword') + ... ) + >>> 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.private_bytes( + ... encoding=serialization.Encoding.PEM, + ... format=serialization.Format.TraditionalOpenSSL, + ... encryption_algorithm=serialization.NoEncryption() + ... ) + >>> pem.splitlines()[0] + '-----BEGIN RSA PRIVATE KEY-----' + Signing ~~~~~~~ @@ -485,6 +518,49 @@ 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:: private_bytes(encoding, format, encryption_algorithm) + + Allows serialization of the key to bytes. Encoding ( + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM` or + :attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`), + format ( + :attr:`~cryptography.hazmat.primitives.serialization.Format.TraditionalOpenSSL` + or + :attr:`~cryptography.hazmat.primitives.serialization.Format.PKCS8`) and + encryption algorithm (such as + :class:`~cryptography.hazmat.primitives.serialization.BestAvailableEncryption` + or :class:`~cryptography.hazmat.primitives.serialization.NoEncryption`) + 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.Format` enum. + + :param encryption_algorithm: 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..4940ebd4 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,67 @@ 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. + +Serialization Formats +~~~~~~~~~~~~~~~~~~~~~ + +.. class:: Format + + .. versionadded:: 0.8 + + An enumeration for key formats. Used with + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`. + + .. attribute:: TraditionalOpenSSL + + Frequently known as PKCS#1 format. Still a widely used format, but + generally considered legacy. + + .. attribute:: PKCS8 + + A more modern format for serializing keys which allows for better + encryption. Choose this unless you have explicit legacy compatibility + requirements. + +Serialization Encodings +~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: Encoding + + .. versionadded:: 0.8 + + An enumeration for encoding types. Used with + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`. + + .. attribute:: PEM + + For PEM format. This is a base64 format with delimiters. + + .. attribute:: DER + + For DER format. This is a binary format. + + +Serialization Encryption Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: KeySerializationEncryption + + Objects with this interface are usable as encryption types with methods + like + :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`. + All other classes in this section represent the available choices for + encryption and have this interface. They are used with + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`. + +.. class:: BestAvailableEncryption(password) + + 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. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 9434a0b7..81310d2d 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -45,6 +45,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..e7365c11 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 ( + BestAvailableEncryption, Encoding, Format, KeySerializationEncryption, + NoEncryption ) @@ -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,62 @@ class _RSAPrivateKey(object): ) ) + def private_bytes(self, encoding, format, encryption_algorithm): + if not isinstance(encoding, Encoding): + raise TypeError("encoding must be an item from the Encoding enum") + + if not isinstance(format, Format): + raise TypeError("format must be an item from the Format enum") + + # This is a temporary check until we land DER serialization. + if encoding is not Encoding.PEM: + raise ValueError("Only PEM encoding is supported by this backend") + + if format is Format.PKCS8: + write_bio = self._backend._lib.PEM_write_bio_PKCS8PrivateKey + key = self._evp_pkey + elif format is Format.TraditionalOpenSSL: + write_bio = self._backend._lib.PEM_write_bio_RSAPrivateKey + key = self._rsa_cdata + + 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, BestAvailableEncryption): + # 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..4963d85c 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -42,13 +42,30 @@ 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 private_bytes(self, encoding, format, encryption_algorithm): + """ + Returns the key serialized as bytes. + """ + + +RSAPrivateKeyWithNumbers = utils.deprecated( + RSAPrivateKeyWithSerialization, + __name__, + ( + "The RSAPrivateKeyWithNumbers interface has been renamed to " + "RSAPrivateKeyWithSerialization" + ), + utils.DeprecatedIn08 +) + @six.add_metaclass(abc.ABCMeta) class RSAPublicKey(object): diff --git a/src/cryptography/hazmat/primitives/interfaces/__init__.py b/src/cryptography/hazmat/primitives/interfaces/__init__.py index 6b4241bd..f9ffae06 100644 --- a/src/cryptography/hazmat/primitives/interfaces/__init__.py +++ b/src/cryptography/hazmat/primitives/interfaces/__init__.py @@ -289,11 +289,12 @@ RSAPrivateKey = utils.deprecated( ) RSAPrivateKeyWithNumbers = utils.deprecated( - rsa.RSAPrivateKeyWithNumbers, + rsa.RSAPrivateKeyWithSerialization, __name__, ( "The RSAPrivateKeyWithNumbers interface has moved to the " - "cryptography.hazmat.primitives.asymmetric.rsa module" + "cryptography.hazmat.primitives.asymmetric.rsa module and has been " + "renamed RSAPrivateKeyWithSerialization" ), utils.DeprecatedIn08 ) diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py index 0f9506e1..0d564221 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,32 @@ else: data = data[4:] return result + + +class Encoding(Enum): + PEM = "PEM" + DER = "DER" + + +class Format(Enum): + PKCS8 = "PKCS8" + TraditionalOpenSSL = "TraditionalOpenSSL" + + +@six.add_metaclass(abc.ABCMeta) +class KeySerializationEncryption(object): + pass + + +@utils.register_interface(KeySerializationEncryption) +class BestAvailableEncryption(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/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index 0e4d75ed..4f44f686 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,25 @@ 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.private_bytes( + serialization.Encoding.PEM, + serialization.Format.PKCS8, + serialization.BestAvailableEncryption(password) + ) + + def test_unsupported_key_encoding(self): + key = RSA_KEY_2048.private_key(backend) + with pytest.raises(ValueError): + key.private_bytes( + serialization.Encoding.DER, + serialization.Format.PKCS8, + serialization.NoEncryption() + ) diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index 74183010..0cf94afe 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,113 @@ 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( + ("fmt", "password"), + itertools.product( + [ + serialization.Format.TraditionalOpenSSL, + serialization.Format.PKCS8 + ], + [ + b"s", + b"longerpassword", + b"!*$&(@#$*&($T@%_somesymbols", + b"\x01" * 1000, + ] + ) + ) + def test_private_bytes_encrypted_pem(self, backend, fmt, password): + key = RSA_KEY_2048.private_key(backend) + _skip_if_no_serialization(key, backend) + serialized = key.private_bytes( + serialization.Encoding.PEM, + fmt, + serialization.BestAvailableEncryption(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( + "fmt", + [serialization.Format.TraditionalOpenSSL, serialization.Format.PKCS8], + ) + def test_private_bytes_unencrypted_pem(self, backend, fmt): + key = RSA_KEY_2048.private_key(backend) + _skip_if_no_serialization(key, backend) + serialized = key.private_bytes( + serialization.Encoding.PEM, + fmt, + 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_private_bytes_traditional_openssl_unencrypted_pem(self, backend): + key_bytes = load_vectors_from_file( + os.path.join( + "asymmetric", + "Traditional_OpenSSL_Serialization", + "testrsa.pem" + ), + lambda pemfile: pemfile.read().encode() + ) + key = serialization.load_pem_private_key(key_bytes, None, backend) + serialized = key.private_bytes( + serialization.Encoding.PEM, + serialization.Format.TraditionalOpenSSL, + serialization.NoEncryption() + ) + assert serialized == key_bytes + + def test_private_bytes_invalid_encoding(self, backend): + key = RSA_KEY_2048.private_key(backend) + _skip_if_no_serialization(key, backend) + with pytest.raises(TypeError): + key.private_bytes( + "notencoding", + serialization.Format.PKCS8, + serialization.NoEncryption() + ) + + def test_private_bytes_invalid_format(self, backend): + key = RSA_KEY_2048.private_key(backend) + _skip_if_no_serialization(key, backend) + with pytest.raises(TypeError): + key.private_bytes( + serialization.Encoding.PEM, + "invalidformat", + serialization.NoEncryption() + ) + + def test_private_bytes_invalid_encryption_algorithm(self, backend): + key = RSA_KEY_2048.private_key(backend) + _skip_if_no_serialization(key, backend) + with pytest.raises(TypeError): + key.private_bytes( + serialization.Encoding.PEM, + serialization.Format.TraditionalOpenSSL, + "notanencalg" + ) + + def test_private_bytes_unsupported_encryption_type(self, backend): + key = RSA_KEY_2048.private_key(backend) + _skip_if_no_serialization(key, backend) + with pytest.raises(ValueError): + key.private_bytes( + serialization.Encoding.PEM, + serialization.Format.TraditionalOpenSSL, + DummyKeyEncryption() + ) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index a17aac4b..fc8f8664 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -18,8 +18,8 @@ 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 + BestAvailableEncryption, load_der_private_key, load_der_public_key, + load_pem_private_key, load_pem_public_key, load_ssh_public_key ) @@ -1159,3 +1159,13 @@ class TestECDSASSHSerialization(object): ) with pytest.raises(ValueError): load_ssh_public_key(ssh_key, backend) + + +class TestKeySerializationEncryptionTypes(object): + def test_non_bytes_password(self): + with pytest.raises(ValueError): + BestAvailableEncryption(object()) + + def test_encryption_with_zero_length_password(self): + with pytest.raises(ValueError): + BestAvailableEncryption(b"") |