diff options
author | Alex Gaynor <alex.gaynor@gmail.com> | 2014-10-22 09:51:46 -0700 |
---|---|---|
committer | Alex Gaynor <alex.gaynor@gmail.com> | 2014-10-22 09:51:46 -0700 |
commit | 633caacfe7e86f9098bb8cb64cfc12a9fe5cc35c (patch) | |
tree | 007cfe0a59150d2681c153323b15d1f8d00ab4c2 | |
parent | 67a4dd1a2227af1a3461c92edc5316ab9fe7a942 (diff) | |
parent | 60d8640bc3aa8eb48273e1df6de94607a38f8ec3 (diff) | |
download | cryptography-633caacfe7e86f9098bb8cb64cfc12a9fe5cc35c.tar.gz cryptography-633caacfe7e86f9098bb8cb64cfc12a9fe5cc35c.tar.bz2 cryptography-633caacfe7e86f9098bb8cb64cfc12a9fe5cc35c.zip |
Merge branch 'master' into verify-interfaces
Conflicts:
cryptography/utils.py
-rw-r--r-- | CHANGELOG.rst | 2 | ||||
-rw-r--r-- | cryptography/fernet.py | 21 | ||||
-rw-r--r-- | cryptography/hazmat/backends/commoncrypto/ciphers.py | 4 | ||||
-rw-r--r-- | cryptography/hazmat/backends/multibackend.py | 6 | ||||
-rw-r--r-- | cryptography/hazmat/backends/openssl/ciphers.py | 4 | ||||
-rw-r--r-- | cryptography/hazmat/backends/openssl/dsa.py | 8 | ||||
-rw-r--r-- | cryptography/hazmat/backends/openssl/ec.py | 8 | ||||
-rw-r--r-- | cryptography/hazmat/backends/openssl/rsa.py | 8 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/asymmetric/dsa.py | 32 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/asymmetric/ec.py | 27 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/asymmetric/rsa.py | 44 | ||||
-rw-r--r-- | cryptography/utils.py | 4 | ||||
-rw-r--r-- | docs/fernet.rst | 36 | ||||
-rw-r--r-- | tests/hazmat/backends/test_multibackend.py | 9 | ||||
-rw-r--r-- | tests/test_fernet.py | 36 |
15 files changed, 141 insertions, 108 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c8cec58d..1d69d9cb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changelog .. note:: This version is not yet released and is under active development. +* Added key-rotation support to :doc:`Fernet </fernet>` with + :class:`~cryptography.fernet.MultiFernet`. * More bit-lengths are now support for ``p`` and ``q`` when loading DSA keys from numbers. * Added :class:`~cryptography.hazmat.primitives.interfaces.MACContext` as a diff --git a/cryptography/fernet.py b/cryptography/fernet.py index a8e0330e..4f98feec 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -127,3 +127,24 @@ class Fernet(object): except ValueError: raise InvalidToken return unpadded + + +class MultiFernet(object): + def __init__(self, fernets): + fernets = list(fernets) + if not fernets: + raise ValueError( + "MultiFernet requires at least one Fernet instance" + ) + self._fernets = fernets + + def encrypt(self, msg): + return self._fernets[0].encrypt(msg) + + def decrypt(self, msg, ttl=None): + for f in self._fernets: + try: + return f.decrypt(msg, ttl) + except InvalidToken: + pass + raise InvalidToken diff --git a/cryptography/hazmat/backends/commoncrypto/ciphers.py b/cryptography/hazmat/backends/commoncrypto/ciphers.py index 6d3ba863..d94746c6 100644 --- a/cryptography/hazmat/backends/commoncrypto/ciphers.py +++ b/cryptography/hazmat/backends/commoncrypto/ciphers.py @@ -198,6 +198,4 @@ class _GCMCipherContext(object): ) self._backend._check_cipher_response(res) - @property - def tag(self): - return self._tag + tag = utils.read_only_property("_tag") diff --git a/cryptography/hazmat/backends/multibackend.py b/cryptography/hazmat/backends/multibackend.py index db189787..e873f504 100644 --- a/cryptography/hazmat/backends/multibackend.py +++ b/cryptography/hazmat/backends/multibackend.py @@ -210,6 +210,12 @@ class MultiBackend(object): raise UnsupportedAlgorithm("DSA is not supported by the backend.", _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM) + def load_dsa_parameter_numbers(self, numbers): + for b in self._filtered_backends(DSABackend): + return b.load_dsa_parameter_numbers(numbers) + raise UnsupportedAlgorithm("DSA is not supported by the backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM) + def cmac_algorithm_supported(self, algorithm): return any( b.cmac_algorithm_supported(algorithm) diff --git a/cryptography/hazmat/backends/openssl/ciphers.py b/cryptography/hazmat/backends/openssl/ciphers.py index d37bb014..4ec2ac89 100644 --- a/cryptography/hazmat/backends/openssl/ciphers.py +++ b/cryptography/hazmat/backends/openssl/ciphers.py @@ -187,9 +187,7 @@ class _CipherContext(object): ) assert res != 0 - @property - def tag(self): - return self._tag + tag = utils.read_only_property("_tag") @utils.register_interface(interfaces.CipherContext) diff --git a/cryptography/hazmat/backends/openssl/dsa.py b/cryptography/hazmat/backends/openssl/dsa.py index 3fb67a5d..2298e7d9 100644 --- a/cryptography/hazmat/backends/openssl/dsa.py +++ b/cryptography/hazmat/backends/openssl/dsa.py @@ -129,9 +129,7 @@ class _DSAPrivateKey(object): self._dsa_cdata = dsa_cdata self._key_size = self._backend._lib.BN_num_bits(self._dsa_cdata.p) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def signer(self, algorithm): return _DSASignatureContext(self._backend, self, algorithm) @@ -180,9 +178,7 @@ class _DSAPublicKey(object): self._dsa_cdata = dsa_cdata self._key_size = self._backend._lib.BN_num_bits(self._dsa_cdata.p) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def verifier(self, signature, algorithm): return _DSAVerificationContext( diff --git a/cryptography/hazmat/backends/openssl/ec.py b/cryptography/hazmat/backends/openssl/ec.py index 7798c3dc..13b0ddbb 100644 --- a/cryptography/hazmat/backends/openssl/ec.py +++ b/cryptography/hazmat/backends/openssl/ec.py @@ -146,9 +146,7 @@ class _EllipticCurvePrivateKey(object): sn = _ec_key_curve_sn(backend, ec_key_cdata) self._curve = _sn_to_elliptic_curve(backend, sn) - @property - def curve(self): - return self._curve + curve = utils.read_only_property("_curve") def signer(self, signature_algorithm): if isinstance(signature_algorithm, ec.ECDSA): @@ -200,9 +198,7 @@ class _EllipticCurvePublicKey(object): sn = _ec_key_curve_sn(backend, ec_key_cdata) self._curve = _sn_to_elliptic_curve(backend, sn) - @property - def curve(self): - return self._curve + curve = utils.read_only_property("_curve") def verifier(self, signature, signature_algorithm): if isinstance(signature_algorithm, ec.ECDSA): diff --git a/cryptography/hazmat/backends/openssl/rsa.py b/cryptography/hazmat/backends/openssl/rsa.py index 7312fcb2..0a2a7f96 100644 --- a/cryptography/hazmat/backends/openssl/rsa.py +++ b/cryptography/hazmat/backends/openssl/rsa.py @@ -532,9 +532,7 @@ class _RSAPrivateKey(object): self._key_size = self._backend._lib.BN_num_bits(self._rsa_cdata.n) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def signer(self, padding, algorithm): return _RSASignatureContext(self._backend, self, padding, algorithm) @@ -588,9 +586,7 @@ class _RSAPublicKey(object): self._key_size = self._backend._lib.BN_num_bits(self._rsa_cdata.n) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def verifier(self, signature, padding, algorithm): return _RSAVerificationContext( diff --git a/cryptography/hazmat/primitives/asymmetric/dsa.py b/cryptography/hazmat/primitives/asymmetric/dsa.py index 97265868..83e01377 100644 --- a/cryptography/hazmat/primitives/asymmetric/dsa.py +++ b/cryptography/hazmat/primitives/asymmetric/dsa.py @@ -61,17 +61,9 @@ class DSAParameterNumbers(object): self._q = q self._g = g - @property - def p(self): - return self._p - - @property - def q(self): - return self._q - - @property - def g(self): - return self._g + p = utils.read_only_property("_p") + q = utils.read_only_property("_q") + g = utils.read_only_property("_g") def parameters(self, backend): return backend.load_dsa_parameter_numbers(self) @@ -90,13 +82,8 @@ class DSAPublicNumbers(object): self._y = y self._parameter_numbers = parameter_numbers - @property - def y(self): - return self._y - - @property - def parameter_numbers(self): - return self._parameter_numbers + y = utils.read_only_property("_y") + parameter_numbers = utils.read_only_property("_parameter_numbers") def public_key(self, backend): return backend.load_dsa_public_numbers(self) @@ -114,13 +101,8 @@ class DSAPrivateNumbers(object): self._public_numbers = public_numbers self._x = x - @property - def x(self): - return self._x - - @property - def public_numbers(self): - return self._public_numbers + x = utils.read_only_property("_x") + public_numbers = utils.read_only_property("_public_numbers") def private_key(self, backend): return backend.load_dsa_private_numbers(self) diff --git a/cryptography/hazmat/primitives/asymmetric/ec.py b/cryptography/hazmat/primitives/asymmetric/ec.py index 6dcf39cf..b27d0458 100644 --- a/cryptography/hazmat/primitives/asymmetric/ec.py +++ b/cryptography/hazmat/primitives/asymmetric/ec.py @@ -213,9 +213,7 @@ class ECDSA(object): def __init__(self, algorithm): self._algorithm = algorithm - @property - def algorithm(self): - return self._algorithm + algorithm = utils.read_only_property("_algorithm") def generate_private_key(curve, backend): @@ -243,17 +241,9 @@ class EllipticCurvePublicNumbers(object): except AttributeError: return backend.elliptic_curve_public_key_from_numbers(self) - @property - def curve(self): - return self._curve - - @property - def x(self): - return self._x - - @property - def y(self): - return self._y + curve = utils.read_only_property("_curve") + x = utils.read_only_property("_x") + y = utils.read_only_property("_y") class EllipticCurvePrivateNumbers(object): @@ -276,10 +266,5 @@ class EllipticCurvePrivateNumbers(object): except AttributeError: return backend.elliptic_curve_private_key_from_numbers(self) - @property - def private_value(self): - return self._private_value - - @property - def public_numbers(self): - return self._public_numbers + private_value = utils.read_only_property("_private_value") + public_numbers = utils.read_only_property("_public_numbers") diff --git a/cryptography/hazmat/primitives/asymmetric/rsa.py b/cryptography/hazmat/primitives/asymmetric/rsa.py index c192811d..db38ed55 100644 --- a/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -15,6 +15,7 @@ from __future__ import absolute_import, division, print_function import six +from cryptography import utils from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat.backends.interfaces import RSABackend @@ -157,33 +158,13 @@ class RSAPrivateNumbers(object): self._iqmp = iqmp self._public_numbers = public_numbers - @property - def p(self): - return self._p - - @property - def q(self): - return self._q - - @property - def d(self): - return self._d - - @property - def dmp1(self): - return self._dmp1 - - @property - def dmq1(self): - return self._dmq1 - - @property - def iqmp(self): - return self._iqmp - - @property - def public_numbers(self): - return self._public_numbers + p = utils.read_only_property("_p") + q = utils.read_only_property("_q") + d = utils.read_only_property("_d") + dmp1 = utils.read_only_property("_dmp1") + dmq1 = utils.read_only_property("_dmq1") + iqmp = utils.read_only_property("_iqmp") + public_numbers = utils.read_only_property("_public_numbers") def private_key(self, backend): return backend.load_rsa_private_numbers(self) @@ -200,13 +181,8 @@ class RSAPublicNumbers(object): self._e = e self._n = n - @property - def e(self): - return self._e - - @property - def n(self): - return self._n + e = utils.read_only_property("_e") + n = utils.read_only_property("_n") def public_key(self, backend): return backend.load_rsa_public_numbers(self) diff --git a/cryptography/utils.py b/cryptography/utils.py index e4ec6ccf..8fbcabc5 100644 --- a/cryptography/utils.py +++ b/cryptography/utils.py @@ -28,6 +28,10 @@ def register_interface(iface): return register_decorator +def read_only_property(name): + return property(lambda self: getattr(self, name)) + + class InterfaceNotImplemented(Exception): pass diff --git a/docs/fernet.rst b/docs/fernet.rst index 4b713a54..f1a4c748 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -5,7 +5,8 @@ Fernet (symmetric encryption) Fernet provides guarantees that a message encrypted using it cannot be manipulated or read without the key. `Fernet`_ is an implementation of -symmetric (also known as "secret key") authenticated cryptography. +symmetric (also known as "secret key") authenticated cryptography. Fernet also +has support for implementing key rotation via :class:`MultiFernet`. .. class:: Fernet(key) @@ -40,7 +41,8 @@ symmetric (also known as "secret key") authenticated cryptography. :returns bytes: A secure message that cannot be read or altered without the key. It is URL-safe base64-encoded. This is referred to as a "Fernet token". - :raises TypeError: This exception is raised if ``data`` is not ``bytes``. + :raises TypeError: This exception is raised if ``data`` is not + ``bytes``. .. note:: @@ -67,7 +69,35 @@ symmetric (also known as "secret key") authenticated cryptography. ``ttl``, it is malformed, or it does not have a valid signature. - :raises TypeError: This exception is raised if ``token`` is not ``bytes``. + :raises TypeError: This exception is raised if ``token`` is not + ``bytes``. + + +.. class:: MultiFernet(fernets) + + .. versionadded:: 0.7 + + This class implements key rotation for Fernet. It takes a ``list`` of + :class:`Fernet` instances, and implements the same API: + + .. doctest:: + + >>> from cryptography.fernet import Fernet, MultiFernet + >>> key1 = Fernet(Fernet.generate_key()) + >>> key2 = Fernet(Fernet.generate_key()) + >>> f = MultiFernet([key1, key2]) + >>> token = f.encrypt(b"Secret message!") + >>> token + '...' + >>> f.decrypt(token) + 'Secret message!' + + Fernet performs all encryption options using the *first* key in the + ``list`` provided. Decryption supports using *any* of constituent keys. + + Key rotation makes it easy to replace old keys. You can add your new key at + the front of the list to start encrypting new messages, and remove old keys + as they are no longer needed. .. class:: InvalidToken diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py index 93934ad6..c50b6cf6 100644 --- a/tests/hazmat/backends/test_multibackend.py +++ b/tests/hazmat/backends/test_multibackend.py @@ -131,6 +131,9 @@ class DummyDSABackend(object): def load_dsa_public_numbers(self, numbers): pass + def load_dsa_parameter_numbers(self, numbers): + pass + @utils.register_interface(CMACBackend) class DummyCMACBackend(object): @@ -330,6 +333,7 @@ class TestMultiBackend(object): backend.dsa_parameters_supported(1, 2, 3) backend.load_dsa_private_numbers("numbers") backend.load_dsa_public_numbers("numbers") + backend.load_dsa_parameter_numbers("numbers") backend = MultiBackend([]) with raises_unsupported_algorithm( @@ -367,6 +371,11 @@ class TestMultiBackend(object): ): backend.load_dsa_public_numbers("numbers") + with raises_unsupported_algorithm( + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM + ): + backend.load_dsa_parameter_numbers("numbers") + def test_cmac(self): backend = MultiBackend([ DummyCMACBackend([algorithms.AES]) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 0b4e3e87..5c630b9e 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -24,7 +24,7 @@ import pytest import six -from cryptography.fernet import Fernet, InvalidToken +from cryptography.fernet import Fernet, InvalidToken, MultiFernet from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import algorithms, modes @@ -115,3 +115,37 @@ class TestFernet(object): def test_bad_key(self, backend): with pytest.raises(ValueError): Fernet(base64.urlsafe_b64encode(b"abc"), backend=backend) + + +@pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", +) +class TestMultiFernet(object): + def test_encrypt(self, backend): + f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend) + f = MultiFernet([f1, f2]) + + assert f1.decrypt(f.encrypt(b"abc")) == b"abc" + + def test_decrypt(self, backend): + f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend) + f = MultiFernet([f1, f2]) + + assert f.decrypt(f1.encrypt(b"abc")) == b"abc" + assert f.decrypt(f2.encrypt(b"abc")) == b"abc" + + with pytest.raises(InvalidToken): + f.decrypt(b"\x00" * 16) + + def test_no_fernets(self, backend): + with pytest.raises(ValueError): + MultiFernet([]) + + def test_non_iterable_argument(self, backend): + with pytest.raises(TypeError): + MultiFernet(None) |