diff options
-rw-r--r-- | docs/hazmat/primitives/asymmetric/ec.rst | 39 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/serialization.rst | 20 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/ec.py | 19 | ||||
-rw-r--r-- | src/cryptography/hazmat/bindings/openssl/pem.py | 16 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/asymmetric/ec.py | 19 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_ec.py | 164 |
6 files changed, 266 insertions, 11 deletions
diff --git a/docs/hazmat/primitives/asymmetric/ec.rst b/docs/hazmat/primitives/asymmetric/ec.rst index 8b9a584b..cf616a0d 100644 --- a/docs/hazmat/primitives/asymmetric/ec.rst +++ b/docs/hazmat/primitives/asymmetric/ec.rst @@ -326,6 +326,45 @@ Key Interfaces :returns: An :class:`EllipticCurvePrivateNumbers` instance. +.. class:: EllipticCurvePrivateKeyWithSerialization + + .. versionadded:: 0.8 + + Extends :class:`EllipticCurvePrivateKey`. + + .. method:: private_numbers() + + Create a :class:`EllipticCurvePrivateNumbers` object. + + :returns: An :class:`EllipticCurvePrivateNumbers` 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:: EllipticCurvePublicKey .. versionadded:: 0.5 diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index e11b02ab..49a0e36e 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -290,8 +290,11 @@ Serialization Formats .. versionadded:: 0.8 - An enumeration for private key formats. Used with - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`. + An enumeration for private key formats. Used with the ``private_bytes`` + method available on + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization` + and + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization`. .. attribute:: TraditionalOpenSSL @@ -311,8 +314,11 @@ Serialization Encodings .. versionadded:: 0.8 - An enumeration for encoding types. Used with - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`. + An enumeration for encoding types. Used with the ``private_bytes`` method + available on + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization` + and + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization`. .. attribute:: PEM @@ -329,8 +335,10 @@ 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`. + like ``private_bytes`` available on + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization` + and + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization`. 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`. diff --git a/src/cryptography/hazmat/backends/openssl/ec.py b/src/cryptography/hazmat/backends/openssl/ec.py index 52c93da9..19d646e8 100644 --- a/src/cryptography/hazmat/backends/openssl/ec.py +++ b/src/cryptography/hazmat/backends/openssl/ec.py @@ -148,7 +148,7 @@ class _ECDSAVerificationContext(object): return True -@utils.register_interface(ec.EllipticCurvePrivateKeyWithNumbers) +@utils.register_interface(ec.EllipticCurvePrivateKeyWithSerialization) class _EllipticCurvePrivateKey(object): def __init__(self, backend, ec_key_cdata): self._backend = backend @@ -200,6 +200,23 @@ class _EllipticCurvePrivateKey(object): public_numbers=self.public_key().public_numbers() ) + def private_bytes(self, encoding, format, encryption_algorithm): + evp_pkey = self._backend._lib.EVP_PKEY_new() + assert evp_pkey != self._backend._ffi.NULL + evp_pkey = self._backend._ffi.gc( + evp_pkey, self._backend._lib.EVP_PKEY_free + ) + res = self._backend._lib.EVP_PKEY_set1_EC_KEY(evp_pkey, self._ec_key) + assert res == 1 + return self._backend._private_key_bytes( + encoding, + format, + encryption_algorithm, + self._backend._lib.PEM_write_bio_ECPrivateKey, + evp_pkey, + self._ec_key + ) + @utils.register_interface(ec.EllipticCurvePublicKeyWithNumbers) class _EllipticCurvePublicKey(object): diff --git a/src/cryptography/hazmat/bindings/openssl/pem.py b/src/cryptography/hazmat/bindings/openssl/pem.py index d0c70f5d..98c7648f 100644 --- a/src/cryptography/hazmat/bindings/openssl/pem.py +++ b/src/cryptography/hazmat/bindings/openssl/pem.py @@ -72,9 +72,23 @@ int PEM_write_bio_PUBKEY(BIO *, EVP_PKEY *); """ MACROS = """ +int PEM_write_bio_ECPrivateKey(BIO *, EC_KEY *, const EVP_CIPHER *, + unsigned char *, int, pem_password_cb *, + void *); """ CUSTOMIZATIONS = """ +// Cryptography_HAS_EC is provided by ec.py so we don't need to define it here +#ifdef OPENSSL_NO_EC +int (*PEM_write_bio_ECPrivateKey)(BIO *, EC_KEY *, const EVP_CIPHER *, + unsigned char *, int, pem_password_cb *, + void *) = NULL; +#endif + """ -CONDITIONAL_NAMES = {} +CONDITIONAL_NAMES = { + "Cryptography_HAS_EC": [ + "PEM_write_bio_ECPrivateKey" + ] +} diff --git a/src/cryptography/hazmat/primitives/asymmetric/ec.py b/src/cryptography/hazmat/primitives/asymmetric/ec.py index c7749ca5..52e14816 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/ec.py +++ b/src/cryptography/hazmat/primitives/asymmetric/ec.py @@ -57,13 +57,30 @@ class EllipticCurvePrivateKey(object): @six.add_metaclass(abc.ABCMeta) -class EllipticCurvePrivateKeyWithNumbers(EllipticCurvePrivateKey): +class EllipticCurvePrivateKeyWithSerialization(EllipticCurvePrivateKey): @abc.abstractmethod def private_numbers(self): """ Returns an EllipticCurvePrivateNumbers. """ + @abc.abstractmethod + def private_bytes(self, encoding, format, encryption_algorithm): + """ + Returns the key serialized as bytes. + """ + + +EllipticCurvePrivateKeyWithNumbers = utils.deprecated( + EllipticCurvePrivateKeyWithSerialization, + __name__, + ( + "The EllipticCurvePrivateKeyWithNumbers interface has been renamed to " + "EllipticCurvePrivateKeyWithSerialization" + ), + utils.DeprecatedIn08 +) + @six.add_metaclass(abc.ABCMeta) class EllipticCurvePublicKey(object): diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py index ea621ad6..f1c49cf7 100644 --- a/tests/hazmat/primitives/test_ec.py +++ b/tests/hazmat/primitives/test_ec.py @@ -10,8 +10,10 @@ import os import pytest from cryptography import exceptions, utils -from cryptography.hazmat.backends.interfaces import EllipticCurveBackend -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends.interfaces import ( + EllipticCurveBackend, PEMSerializationBackend +) +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import ( encode_rfc6979_signature @@ -31,6 +33,13 @@ _HASH_TYPES = { } +def _skip_if_no_serialization(key, backend): + if not isinstance(key, ec.EllipticCurvePrivateKeyWithSerialization): + pytest.skip( + "{0} does not support EC key serialization".format(backend) + ) + + def _skip_ecdsa_vector(backend, curve_type, hash_type): if not backend.elliptic_curve_signature_algorithm_supported( ec.ECDSA(hash_type()), @@ -63,12 +72,22 @@ class DummySignatureAlgorithm(object): algorithm = None +@utils.register_interface(serialization.KeySerializationEncryption) +class DummyKeyEncryption(object): + pass + + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) def test_skip_curve_unsupported(backend): with pytest.raises(pytest.skip.Exception): _skip_curve_unsupported(backend, DummyCurve()) +def test_skip_no_serialization(): + with pytest.raises(pytest.skip.Exception): + _skip_if_no_serialization("fakebackend", "fakekey") + + def test_ec_numbers(): numbers = ec.EllipticCurvePrivateNumbers( 1, @@ -378,3 +397,144 @@ class TestECNumbersEquality(object): 1, ec.EllipticCurvePublicNumbers(1, 2, ec.SECP521R1()) ) assert priv != object() + + +@pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) +@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend) +class TestECSerialization(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_bytes = load_vectors_from_file( + os.path.join( + "asymmetric", "PKCS8", "ec_private_key.pem"), + lambda pemfile: pemfile.read().encode() + ) + key = serialization.load_pem_private_key(key_bytes, None, 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_bytes = load_vectors_from_file( + os.path.join( + "asymmetric", "PKCS8", "ec_private_key.pem"), + lambda pemfile: pemfile.read().encode() + ) + key = serialization.load_pem_private_key(key_bytes, None, 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", "PEM_Serialization", "ec_private_key.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 = load_vectors_from_file( + os.path.join( + "asymmetric", "PKCS8", "ec_private_key.pem"), + lambda pemfile: serialization.load_pem_private_key( + pemfile.read().encode(), None, 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 = load_vectors_from_file( + os.path.join( + "asymmetric", "PKCS8", "ec_private_key.pem"), + lambda pemfile: serialization.load_pem_private_key( + pemfile.read().encode(), None, 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 = load_vectors_from_file( + os.path.join( + "asymmetric", "PKCS8", "ec_private_key.pem"), + lambda pemfile: serialization.load_pem_private_key( + pemfile.read().encode(), None, 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 = load_vectors_from_file( + os.path.join( + "asymmetric", "PKCS8", "ec_private_key.pem"), + lambda pemfile: serialization.load_pem_private_key( + pemfile.read().encode(), None, backend + ) + ) + _skip_if_no_serialization(key, backend) + with pytest.raises(ValueError): + key.private_bytes( + serialization.Encoding.PEM, + serialization.Format.TraditionalOpenSSL, + DummyKeyEncryption() + ) |