Skip to content

Commit

Permalink
Merge pull request #272 from fjarri/api-updates
Browse files Browse the repository at this point in the history
Api updates corresponding to post-0.2 `rust-umbral` PRs
  • Loading branch information
fjarri committed Aug 19, 2021
2 parents f64a129 + a63b1ec commit 5c9fb53
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 29 deletions.
9 changes: 7 additions & 2 deletions docs/source/api.rst
Expand Up @@ -50,6 +50,7 @@ Intermediate objects
:show-inheritance:

.. autoclass:: VerifiedCapsuleFrag()
:members:
:special-members: __eq__, __hash__
:show-inheritance:

Expand All @@ -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.SerializableSecret
:members:
:show-inheritance:

.. autoclass:: umbral.serializable.Deserializable
:members: from_bytes
:members:
:show-inheritance:
9 changes: 8 additions & 1 deletion 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


Expand Down Expand Up @@ -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))
Expand Down
27 changes: 17 additions & 10 deletions tests/test_compatibility.py
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
50 changes: 45 additions & 5 deletions tests/test_keys.py
Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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():
Expand Down
12 changes: 12 additions & 0 deletions umbral/capsule_frag.py
Expand Up @@ -232,6 +232,18 @@ def __bytes__(self):
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

Expand Down
48 changes: 38 additions & 10 deletions umbral/keys.py
Expand Up @@ -5,10 +5,10 @@
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.
"""
Expand All @@ -33,9 +33,6 @@ def public_key(self) -> 'PublicKey':
"""
return self._public_key

def __eq__(self, other):
return self._scalar_key == other._scalar_key

def __str__(self):
return f"{self.__class__.__name__}:..."

Expand All @@ -53,7 +50,7 @@ def serialized_size(cls):
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)


Expand Down Expand Up @@ -91,15 +88,15 @@ def __hash__(self) -> int:
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.
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):
Expand All @@ -112,9 +109,32 @@ def random(cls) -> 'SecretKeyFactory':
"""
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)
Expand All @@ -125,6 +145,14 @@ def secret_key_by_label(self, label: bytes) -> SecretKey:

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
Expand All @@ -133,7 +161,7 @@ def serialized_size(cls):
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):
Expand Down
16 changes: 15 additions & 1 deletion umbral/serializable.py
Expand Up @@ -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

Expand Down Expand Up @@ -85,6 +85,20 @@ def __bytes__(self):
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

Expand Down

0 comments on commit 5c9fb53

Please sign in to comment.