From ec3426383c58098a326b3568a42f298046f1b9c5 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Sun, 1 Mar 2015 16:53:58 -0600 Subject: serialize DSA private keys --- CHANGELOG.rst | 8 + docs/hazmat/primitives/asymmetric/dsa.rst | 44 ++++++ .../hazmat/primitives/asymmetric/serialization.rst | 12 +- src/cryptography/hazmat/backends/openssl/dsa.py | 26 +++- .../hazmat/primitives/asymmetric/dsa.py | 19 ++- tests/hazmat/primitives/test_dsa.py | 171 ++++++++++++++++++++- 6 files changed, 268 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1841091b..053e7552 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -94,6 +94,14 @@ Changelog :meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes` to :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`. +* Added + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization` + and deprecated + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithNumbers`. +* Added + :meth:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization.private_bytes` + to + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization`. 0.7.2 - 2015-01-16 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/hazmat/primitives/asymmetric/dsa.rst b/docs/hazmat/primitives/asymmetric/dsa.rst index 3a47da45..bd02423f 100644 --- a/docs/hazmat/primitives/asymmetric/dsa.rst +++ b/docs/hazmat/primitives/asymmetric/dsa.rst @@ -301,6 +301,50 @@ Key interfaces instance. +.. class:: DSAPrivateKeyWithSerialization + + .. versionadded:: 0.8 + + Extends :class:`DSAPrivateKey`. + + .. method:: private_numbers() + + Create a + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateNumbers` + object. + + :returns: A + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateNumbers` + 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.PrivateFormat.TraditionalOpenSSL` + or + :attr:`~cryptography.hazmat.primitives.serialization.PrivateFormat.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.PrivateFormat` + enum. + + :param encryption_algorithm: An instance of an object conforming to the + :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption` + interface. + + :return bytes: Serialized key. + + .. class:: DSAPublicKey .. versionadded:: 0.3 diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index 49a0e36e..4a2aedc9 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -293,8 +293,10 @@ Serialization Formats An enumeration for private key formats. Used with the ``private_bytes`` method available on :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization` + , + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization` and - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization`. + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization`. .. attribute:: TraditionalOpenSSL @@ -317,8 +319,10 @@ Serialization Encodings An enumeration for encoding types. Used with the ``private_bytes`` method available on :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization` + , + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization` and - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization`. + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization`. .. attribute:: PEM @@ -337,8 +341,10 @@ Serialization Encryption Types Objects with this interface are usable as encryption types with methods like ``private_bytes`` available on :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization` + , + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization` and - :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKeyWithSerialization`. + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization`. 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/dsa.py b/src/cryptography/hazmat/backends/openssl/dsa.py index d2972e4a..8d02e492 100644 --- a/src/cryptography/hazmat/backends/openssl/dsa.py +++ b/src/cryptography/hazmat/backends/openssl/dsa.py @@ -11,9 +11,6 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ( AsymmetricSignatureContext, AsymmetricVerificationContext, dsa ) -from cryptography.hazmat.primitives.interfaces import ( - DSAParametersWithNumbers, DSAPrivateKeyWithNumbers, DSAPublicKeyWithNumbers -) def _truncate_digest_for_dsa(dsa_cdata, digest, backend): @@ -94,7 +91,7 @@ class _DSASignatureContext(object): return self._backend._ffi.buffer(sig_buf)[:buflen[0]] -@utils.register_interface(DSAParametersWithNumbers) +@utils.register_interface(dsa.DSAParametersWithNumbers) class _DSAParameters(object): def __init__(self, backend, dsa_cdata): self._backend = backend @@ -111,7 +108,7 @@ class _DSAParameters(object): return self._backend.generate_dsa_private_key(self) -@utils.register_interface(DSAPrivateKeyWithNumbers) +@utils.register_interface(dsa.DSAPrivateKeyWithSerialization) class _DSAPrivateKey(object): def __init__(self, backend, dsa_cdata): self._backend = backend @@ -159,8 +156,25 @@ class _DSAPrivateKey(object): dsa_cdata.g = self._backend._lib.BN_dup(self._dsa_cdata.g) return _DSAParameters(self._backend, dsa_cdata) + 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_DSA(evp_pkey, self._dsa_cdata) + assert res == 1 + return self._backend._private_key_bytes( + encoding, + format, + encryption_algorithm, + self._backend._lib.PEM_write_bio_DSAPrivateKey, + evp_pkey, + self._dsa_cdata + ) + -@utils.register_interface(DSAPublicKeyWithNumbers) +@utils.register_interface(dsa.DSAPublicKeyWithNumbers) class _DSAPublicKey(object): def __init__(self, backend, dsa_cdata): self._backend = backend diff --git a/src/cryptography/hazmat/primitives/asymmetric/dsa.py b/src/cryptography/hazmat/primitives/asymmetric/dsa.py index 58058df9..084686e4 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/dsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/dsa.py @@ -57,13 +57,30 @@ class DSAPrivateKey(object): @six.add_metaclass(abc.ABCMeta) -class DSAPrivateKeyWithNumbers(DSAPrivateKey): +class DSAPrivateKeyWithSerialization(DSAPrivateKey): @abc.abstractmethod def private_numbers(self): """ Returns a DSAPrivateNumbers. """ + @abc.abstractmethod + def private_bytes(self, encoding, format, encryption_algorithm): + """ + Returns the key serialized as bytes. + """ + + +DSAPrivateKeyWithNumbers = utils.deprecated( + DSAPrivateKeyWithSerialization, + __name__, + ( + "The DSAPrivateKeyWithNumbers interface has been renamed to " + "DSAPrivateKeyWithSerialization" + ), + utils.DeprecatedIn08 +) + @six.add_metaclass(abc.ABCMeta) class DSAPublicKey(object): diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py index 95164923..19ca0794 100644 --- a/tests/hazmat/primitives/test_dsa.py +++ b/tests/hazmat/primitives/test_dsa.py @@ -4,13 +4,17 @@ from __future__ import absolute_import, division, print_function +import itertools import os import pytest +from cryptography import utils from cryptography.exceptions import AlreadyFinalized, InvalidSignature -from cryptography.hazmat.backends.interfaces import DSABackend -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends.interfaces import ( + DSABackend, PEMSerializationBackend +) +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import dsa from cryptography.hazmat.primitives.asymmetric.utils import ( encode_rfc6979_signature @@ -26,6 +30,23 @@ from ...utils import ( ) +def _skip_if_no_serialization(key, backend): + if not isinstance(key, dsa.DSAPrivateKeyWithSerialization): + pytest.skip( + "{0} does not support DSA key serialization".format(backend) + ) + + +def test_skip_if_no_serialization(): + with pytest.raises(pytest.skip.Exception): + _skip_if_no_serialization("notakeywithserialization", "backend") + + +@utils.register_interface(serialization.KeySerializationEncryption) +class DummyKeyEncryption(object): + pass + + @pytest.mark.requires_backend_interface(interface=DSABackend) class TestDSA(object): def test_generate_dsa_parameters(self, backend): @@ -769,3 +790,149 @@ class TestDSANumberEquality(object): ) ) assert priv != object() + + +@pytest.mark.requires_backend_interface(interface=DSABackend) +@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend) +class TestDSASerialization(object): + @pytest.mark.parametrize( + ("fmt", "password"), + itertools.product( + [ + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.PrivateFormat.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", "unenc-dsa-pkcs8.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.PrivateFormat.TraditionalOpenSSL, + serialization.PrivateFormat.PKCS8 + ], + ) + def test_private_bytes_unencrypted_pem(self, backend, fmt): + key_bytes = load_vectors_from_file( + os.path.join( + "asymmetric", + "Traditional_OpenSSL_Serialization", + "dsa.1024.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", + "Traditional_OpenSSL_Serialization", + "dsa.1024.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, + serialization.PrivateFormat.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", "unenc-dsa-pkcs8.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.PrivateFormat.PKCS8, + serialization.NoEncryption() + ) + + def test_private_bytes_invalid_format(self, backend): + key = load_vectors_from_file( + os.path.join("asymmetric", "PKCS8", "unenc-dsa-pkcs8.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", "unenc-dsa-pkcs8.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.PrivateFormat.TraditionalOpenSSL, + "notanencalg" + ) + + def test_private_bytes_unsupported_encryption_type(self, backend): + key = load_vectors_from_file( + os.path.join("asymmetric", "PKCS8", "unenc-dsa-pkcs8.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.PrivateFormat.TraditionalOpenSSL, + DummyKeyEncryption() + ) -- cgit v1.2.3