From 7abd5c09d09f1d371a06ef02abbad7a97ee68938 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 19 Jul 2021 15:08:29 -0700 Subject: [PATCH 1/6] Make SecretKey and SecretKeyFactory serializable via `to_secret_bytes()` instead of `bytes()` --- docs/source/api.rst | 10 +++++++--- tests/test_compatibility.py | 4 ++-- tests/test_keys.py | 4 ++-- umbral/keys.py | 10 +++++----- umbral/serializable.py | 16 +++++++++++++++- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index ee4349d..9dd7c27 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -73,12 +73,16 @@ Utilities :show-inheritance: .. autoclass:: umbral.serializable.HasSerializedSize - :members: serialized_size + :members: .. autoclass:: umbral.serializable.Serializable :special-members: __bytes__ :show-inheritance: -.. autoclass:: umbral.serializable.Deserializable - :members: from_bytes +.. autoclass:: umbral.serializable.SerializableSecret + :members: + :show-inheritance: + +.. autoclass:: umbral.serializable.Deserializable + :members: :show-inheritance: diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 7c7d3e3..d8149f2 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -22,7 +22,7 @@ def pytest_generate_tests(metafunc): def _create_keypair(umbral): sk = umbral.SecretKey.random() pk = sk.public_key() - return bytes(sk), bytes(pk) + return sk.to_secret_bytes(), bytes(pk) def _restore_keys(umbral, sk_bytes, pk_bytes): @@ -45,7 +45,7 @@ def test_keys(implementations): def _create_sk_factory_and_sk(umbral, label): skf = umbral.SecretKeyFactory.random() sk = skf.secret_key_by_label(label) - return bytes(skf), bytes(sk) + return skf.to_secret_bytes(), sk.to_secret_bytes() def _check_sk_is_same(umbral, label, skf_bytes, sk_bytes): diff --git a/tests/test_keys.py b/tests/test_keys.py index 9411f71..0005ed9 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -48,7 +48,7 @@ def test_derive_key_from_label(): def test_secret_key_serialization(): sk = SecretKey.random() - encoded_key = bytes(sk) + encoded_key = sk.to_secret_bytes() decoded_key = SecretKey.from_bytes(encoded_key) assert sk == decoded_key @@ -102,7 +102,7 @@ def test_public_key_str(): def test_secret_key_factory_serialization(): factory = SecretKeyFactory.random() - encoded_factory = bytes(factory) + encoded_factory = factory.to_secret_bytes() decoded_factory = SecretKeyFactory.from_bytes(encoded_factory) label = os.urandom(32) diff --git a/umbral/keys.py b/umbral/keys.py index 17645b3..e9854e4 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -5,10 +5,10 @@ from .curve_scalar import CurveScalar from .curve_point import CurvePoint from .dem import kdf from .hashing import Hash -from .serializable import Serializable, Deserializable +from .serializable import Serializable, SerializableSecret, Deserializable -class SecretKey(Serializable, Deserializable): +class SecretKey(SerializableSecret, Deserializable): """ Umbral secret (private) key. """ @@ -53,7 +53,7 @@ class SecretKey(Serializable, Deserializable): def _from_exact_bytes(cls, data: bytes): return cls(CurveScalar._from_exact_bytes(data)) - def __bytes__(self) -> bytes: + def to_secret_bytes(self) -> bytes: return bytes(self._scalar_key) @@ -91,7 +91,7 @@ class PublicKey(Serializable, Deserializable): return hash((self.__class__, bytes(self))) -class SecretKeyFactory(Serializable, Deserializable): +class SecretKeyFactory(SerializableSecret, Deserializable): """ This class handles keyring material for Umbral, by allowing deterministic derivation of :py:class:`SecretKey` objects based on labels. @@ -133,7 +133,7 @@ class SecretKeyFactory(Serializable, Deserializable): def _from_exact_bytes(cls, data: bytes): return cls(data) - def __bytes__(self) -> bytes: + def to_secret_bytes(self) -> bytes: return bytes(self.__key_seed) def __str__(self): diff --git a/umbral/serializable.py b/umbral/serializable.py index f253d4f..ae8eb54 100644 --- a/umbral/serializable.py +++ b/umbral/serializable.py @@ -12,7 +12,7 @@ class HasSerializedSize(ABC): def serialized_size(cls) -> int: """ Returns the size in bytes of the serialized representation of this object - (obtained with ``bytes()``). + (obtained with ``bytes()`` or ``to_secret_bytes()``). """ raise NotImplementedError @@ -85,6 +85,20 @@ class Serializable(HasSerializedSize): raise NotImplementedError +class SerializableSecret(HasSerializedSize): + """ + A mixin for composable serialization of objects containing secret data. + """ + + @abstractmethod + def to_secret_bytes(self): + """ + Serializes the object into bytes. + This bytestring is secret, handle with care! + """ + raise NotImplementedError + + def bool_serialized_size() -> int: return 1 From 2e046fd181225817a208fe53164508dd9cb0ac95 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 19 Jul 2021 14:11:16 -0700 Subject: [PATCH 2/6] Remove `SecretKey.__eq__()` --- tests/test_compatibility.py | 2 +- tests/test_keys.py | 6 +++--- umbral/keys.py | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index d8149f2..6fbadd8 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -52,7 +52,7 @@ def _check_sk_is_same(umbral, label, skf_bytes, sk_bytes): skf = umbral.SecretKeyFactory.from_bytes(skf_bytes) sk_restored = umbral.SecretKey.from_bytes(sk_bytes) sk_generated = skf.secret_key_by_label(label) - assert sk_restored == sk_generated + assert sk_restored.to_secret_bytes() == sk_generated.to_secret_bytes() def test_secret_key_factory(implementations): diff --git a/tests/test_keys.py b/tests/test_keys.py index 0005ed9..a854c3e 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -36,7 +36,7 @@ def test_derive_key_from_label(): # Check that key derivation is reproducible sk2 = factory.secret_key_by_label(label) pk2 = sk2.public_key() - assert sk1 == sk2 + assert sk1.to_secret_bytes() == sk2.to_secret_bytes() assert pk1 == pk2 # Different labels on the same master secret create different keys @@ -50,7 +50,7 @@ def test_secret_key_serialization(): sk = SecretKey.random() encoded_key = sk.to_secret_bytes() decoded_key = SecretKey.from_bytes(encoded_key) - assert sk == decoded_key + assert sk.to_secret_bytes() == decoded_key.to_secret_bytes() def test_secret_key_str(): @@ -108,7 +108,7 @@ def test_secret_key_factory_serialization(): label = os.urandom(32) sk1 = factory.secret_key_by_label(label) sk2 = decoded_factory.secret_key_by_label(label) - assert sk1 == sk2 + assert sk1.to_secret_bytes() == sk2.to_secret_bytes() def test_public_key_is_hashable(): diff --git a/umbral/keys.py b/umbral/keys.py index e9854e4..4cb829c 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -33,9 +33,6 @@ class SecretKey(SerializableSecret, Deserializable): """ return self._public_key - def __eq__(self, other): - return self._scalar_key == other._scalar_key - def __str__(self): return f"{self.__class__.__name__}:..." From d6d3a272a7a310dac83ce9d6e00f5fb74a03ff2c Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 19 Jul 2021 14:15:15 -0700 Subject: [PATCH 3/6] Change the seed size in SecretKeyFactory from 64 to 32 bytes --- umbral/keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umbral/keys.py b/umbral/keys.py index 4cb829c..ff705d1 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -96,7 +96,7 @@ class SecretKeyFactory(SerializableSecret, Deserializable): Don't use this key material directly as a key. """ - _KEY_SEED_SIZE = 64 + _KEY_SEED_SIZE = 32 _DERIVED_KEY_SIZE = 64 def __init__(self, key_seed: bytes): From 7e75ecb2aaf3119ede56a6f0584520d68f4d4c81 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 19 Jul 2021 14:24:58 -0700 Subject: [PATCH 4/6] Add `VerifiedCapsuleFrag.from_verified_bytes()` --- docs/source/api.rst | 1 + tests/test_capsule_frag.py | 9 ++++++++- umbral/capsule_frag.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 9dd7c27..04e55e6 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -50,6 +50,7 @@ Intermediate objects :show-inheritance: .. autoclass:: VerifiedCapsuleFrag() + :members: :special-members: __eq__, __hash__ :show-inheritance: diff --git a/tests/test_capsule_frag.py b/tests/test_capsule_frag.py index b6669b5..5cf685f 100644 --- a/tests/test_capsule_frag.py +++ b/tests/test_capsule_frag.py @@ -1,6 +1,6 @@ import pytest -from umbral import encrypt, reencrypt, CapsuleFrag, Capsule, VerificationError +from umbral import encrypt, reencrypt, CapsuleFrag, VerifiedCapsuleFrag, Capsule, VerificationError from umbral.curve_point import CurvePoint @@ -116,6 +116,13 @@ def test_cfrag_str(capsule, kfrags): assert "CapsuleFrag" in s +def test_from_verified_bytes(capsule, kfrags): + verified_cfrag = reencrypt(capsule, kfrags[0]) + cfrag_bytes = bytes(verified_cfrag) + verified_cfrag_back = VerifiedCapsuleFrag.from_verified_bytes(cfrag_bytes) + assert verified_cfrag == verified_cfrag_back + + def test_serialized_size(capsule, kfrags): verified_cfrag = reencrypt(capsule, kfrags[0]) cfrag = CapsuleFrag.from_bytes(bytes(verified_cfrag)) diff --git a/umbral/capsule_frag.py b/umbral/capsule_frag.py index 44a2e70..b7045bb 100644 --- a/umbral/capsule_frag.py +++ b/umbral/capsule_frag.py @@ -232,6 +232,18 @@ class VerifiedCapsuleFrag(Serializable): def serialized_size(cls): return CapsuleFrag.serialized_size() + @classmethod + def from_verified_bytes(cls, data) -> 'VerifiedCapsuleFrag': + """ + Restores a verified capsule frag directly from serialized bytes, + skipping :py:meth:`CapsuleFrag.verify` call. + + Intended for internal storage; + make sure that the bytes come from a trusted source. + """ + cfrag = CapsuleFrag.from_bytes(data) + return cls(cfrag) + def __eq__(self, other): return self.cfrag == other.cfrag From a9050f46787e997ad0a2d26f829f8411ecd00b4a Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 19 Jul 2021 14:52:39 -0700 Subject: [PATCH 5/6] Add `SecretKeyFactory.secret_key_factory_by_label()` --- tests/test_compatibility.py | 23 +++++++++++++++-------- tests/test_keys.py | 22 ++++++++++++++++++++++ umbral/keys.py | 10 +++++++++- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 6fbadd8..11e1278 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -42,25 +42,32 @@ def test_keys(implementations): _restore_keys(umbral2, sk_bytes, pk_bytes) -def _create_sk_factory_and_sk(umbral, label): +def _create_sk_factory_and_sk(umbral, skf_label, key_label): skf = umbral.SecretKeyFactory.random() - sk = skf.secret_key_by_label(label) - return skf.to_secret_bytes(), sk.to_secret_bytes() + derived_skf = skf.secret_key_factory_by_label(skf_label) + sk = derived_skf.secret_key_by_label(key_label) + return skf.to_secret_bytes(), derived_skf.to_secret_bytes(), sk.to_secret_bytes() -def _check_sk_is_same(umbral, label, skf_bytes, sk_bytes): +def _check_sk_is_same(umbral, skf_label, key_label, skf_bytes, derived_skf_bytes, sk_bytes): skf = umbral.SecretKeyFactory.from_bytes(skf_bytes) + + derived_skf_restored = umbral.SecretKeyFactory.from_bytes(derived_skf_bytes) + derived_skf_generated = skf.secret_key_factory_by_label(skf_label) + assert derived_skf_generated.to_secret_bytes() == derived_skf_restored.to_secret_bytes() + sk_restored = umbral.SecretKey.from_bytes(sk_bytes) - sk_generated = skf.secret_key_by_label(label) + sk_generated = derived_skf_generated.secret_key_by_label(key_label) assert sk_restored.to_secret_bytes() == sk_generated.to_secret_bytes() def test_secret_key_factory(implementations): umbral1, umbral2 = implementations - label = b'label' + skf_label = b'skf label' + key_label = b'key label' - skf_bytes, sk_bytes = _create_sk_factory_and_sk(umbral1, label) - _check_sk_is_same(umbral2, label, skf_bytes, sk_bytes) + skf_bytes, derived_skf_bytes, sk_bytes = _create_sk_factory_and_sk(umbral1, skf_label, key_label) + _check_sk_is_same(umbral2, skf_label, key_label, skf_bytes, derived_skf_bytes, sk_bytes) def _encrypt(umbral, plaintext, pk_bytes): diff --git a/tests/test_keys.py b/tests/test_keys.py index a854c3e..fee7054 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -46,6 +46,28 @@ def test_derive_key_from_label(): assert sk1 != sk3 +def test_derive_skf_from_label(): + root = SecretKeyFactory.random() + + skf_label = b"Alice" + + skf = root.secret_key_factory_by_label(skf_label) + assert type(skf) == SecretKeyFactory + + skf_same = root.secret_key_factory_by_label(skf_label) + assert skf.to_secret_bytes() == skf_same.to_secret_bytes() + + # Just in case, check that they produce the same secret keys too. + key_label = b"my_healthcare_information" + key = skf.secret_key_by_label(key_label) + key_same = skf_same.secret_key_by_label(key_label) + assert key.to_secret_bytes() == key_same.to_secret_bytes() + + # Different label produces a different factory + skf_different = root.secret_key_factory_by_label(b"Bob") + assert skf.to_secret_bytes() != skf_different.to_secret_bytes() + + def test_secret_key_serialization(): sk = SecretKey.random() encoded_key = sk.to_secret_bytes() diff --git a/umbral/keys.py b/umbral/keys.py index ff705d1..57fd564 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -111,7 +111,7 @@ class SecretKeyFactory(SerializableSecret, Deserializable): def secret_key_by_label(self, label: bytes) -> SecretKey: """ - Creates a :py:class:`SecretKey` from the given label. + Creates a :py:class:`SecretKey` deterministically from the given label. """ tag = b"KEY_DERIVATION/" + label key = kdf(self.__key_seed, self._DERIVED_KEY_SIZE, info=tag) @@ -122,6 +122,14 @@ class SecretKeyFactory(SerializableSecret, Deserializable): return SecretKey(scalar_key) + def secret_key_factory_by_label(self, label: bytes) -> 'SecretKeyFactory': + """ + Creates a :py:class:`SecretKeyFactory` deterministically from the given label. + """ + tag = b"FACTORY_DERIVATION/" + label + key_seed = kdf(self.__key_seed, self._KEY_SEED_SIZE, info=tag) + return SecretKeyFactory(key_seed) + @classmethod def serialized_size(cls): return cls._KEY_SEED_SIZE From a63b1ece589e34a70363b91d9e28a368294c5557 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 19 Jul 2021 15:05:13 -0700 Subject: [PATCH 6/6] Add `SecretKeyFactory.from_secure_randomness()` and `.seed_size()` --- tests/test_keys.py | 18 ++++++++++++++++++ umbral/keys.py | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/tests/test_keys.py b/tests/test_keys.py index fee7054..10b1722 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -68,6 +68,24 @@ def test_derive_skf_from_label(): assert skf.to_secret_bytes() != skf_different.to_secret_bytes() +def test_from_secure_randomness(): + + seed = os.urandom(SecretKeyFactory.seed_size()) + skf = SecretKeyFactory.from_secure_randomness(seed) + assert type(skf) == SecretKeyFactory + + # Check that it can produce keys + sk = skf.secret_key_by_label(b"key label") + + # Wrong seed size + + with pytest.raises(ValueError, match=f"Expected {len(seed)} bytes, got {len(seed) + 1}"): + SecretKeyFactory.from_secure_randomness(seed + b'a') + + with pytest.raises(ValueError, match=f"Expected {len(seed)} bytes, got {len(seed) - 1}"): + SecretKeyFactory.from_secure_randomness(seed[:-1]) + + def test_secret_key_serialization(): sk = SecretKey.random() encoded_key = sk.to_secret_bytes() diff --git a/umbral/keys.py b/umbral/keys.py index 57fd564..2b0476f 100644 --- a/umbral/keys.py +++ b/umbral/keys.py @@ -109,6 +109,29 @@ class SecretKeyFactory(SerializableSecret, Deserializable): """ return cls(os.urandom(cls._KEY_SEED_SIZE)) + @classmethod + def seed_size(cls): + """ + Returns the seed size required by + :py:meth:`~SecretKeyFactory.from_secure_randomness`. + """ + return cls._KEY_SEED_SIZE + + @classmethod + def from_secure_randomness(cls, seed: bytes) -> 'SecretKeyFactory': + """ + Creates a secret key factory using the given random bytes + (of size :py:meth:`~SecretKeyFactory.seed_size`). + + .. warning:: + + Make sure the given seed has been obtained + from a cryptographically secure source of randomness! + """ + if len(seed) != cls.seed_size(): + raise ValueError(f"Expected {cls.seed_size()} bytes, got {len(seed)}") + return cls(seed) + def secret_key_by_label(self, label: bytes) -> SecretKey: """ Creates a :py:class:`SecretKey` deterministically from the given label.