Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api updates corresponding to post-0.2 rust-umbral PRs #272

Merged
merged 6 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be by_label or from_label?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would also be my guess, but I think from prefix is widely accepted as a naming convention for From<> trait implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"by_label" is a leftover from PyUmbral 0.1. from is indeed used generally for constructors.

Come to think of it, perhaps make_secret_key()/make_secret_key_factory() (or derive_, borrowing from PyUmbral 0.1) would be a better name for these methods. by_label in my mind implies that there is some lookup going on and not creation.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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:

Copy link
Member

@cygnusv cygnusv Jul 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR but we discussed this extra hoop before in discord (https://discord.com/channels/411401661714792449/411401661714792451/860193967923527730)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I remember - see my concerns in nucypher/rust-umbral#64

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
Original file line number Diff line number Diff line change
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