Merge pull request #272 from fjarri/api-updates

Api updates corresponding to post-0.2 `rust-umbral` PRs
pull/273/head
Bogdan Opanchuk 2021-08-19 09:07:33 -07:00 committed by GitHub
commit 5c9fb53ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 143 additions and 30 deletions

View File

@ -50,6 +50,7 @@ Intermediate objects
:show-inheritance:
.. autoclass:: VerifiedCapsuleFrag()
:members:
:special-members: __eq__, __hash__
:show-inheritance:
@ -73,12 +74,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:

View File

@ -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))

View File

@ -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):
@ -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 bytes(skf), bytes(sk)
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)
assert sk_restored == sk_generated
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):

View File

@ -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
@ -46,11 +46,51 @@ 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_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 = bytes(sk)
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():
@ -102,13 +142,13 @@ 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)
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():

View File

@ -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

View File

@ -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.
"""
@ -33,9 +33,6 @@ class SecretKey(Serializable, Deserializable):
"""
return self._public_key
def __eq__(self, other):
return self._scalar_key == other._scalar_key
def __str__(self):
return f"{self.__class__.__name__}:..."
@ -53,7 +50,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 +88,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.
@ -99,7 +96,7 @@ class SecretKeyFactory(Serializable, 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):
@ -112,9 +109,32 @@ class SecretKeyFactory(Serializable, 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` 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)
@ -125,6 +145,14 @@ class SecretKeyFactory(Serializable, 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
@ -133,7 +161,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):

View File

@ -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