diff --git a/.gitignore b/.gitignore index 0623bcc97..49abdcac5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__ /.idea .coverage _temp_test_datastore +.mypy_cache \ No newline at end of file diff --git a/nkms/characters.py b/nkms/characters.py index 39ac68a0e..115edcb2a 100644 --- a/nkms/characters.py +++ b/nkms/characters.py @@ -4,11 +4,12 @@ from binascii import hexlify from logging import getLogger import msgpack +from sqlalchemy.exc import IntegrityError + from apistar import http from apistar.core import Route from apistar.frameworks.wsgi import WSGIApp as App -from sqlalchemy.exc import IntegrityError - +from apistar.http import Response from kademlia.network import Server from kademlia.utils import digest from nkms.crypto import api as API @@ -71,6 +72,12 @@ class Character(object): else: self._seal = StrangerSeal(self) + def __eq__(self, other): + return bytes(self.seal) == bytes(other.seal) + + def __hash__(self): + return int.from_bytes(self.seal, byteorder="big") + class NotFound(KeyError): """raised when we try to interact with an actor of whom we haven't learned yet.""" @@ -224,11 +231,12 @@ class Bob(Character): _server_class = NuCypherSeedOnlyDHTServer _default_crypto_powerups = [SigningPower, EncryptingPower] - def __init__(self, alice=None): - super().__init__() + def __init__(self, alice=None, *args, **kwargs): + super().__init__(*args, **kwargs) self._ursulas = {} if alice: self.alice = alice + self._work_orders = {} @property def alice(self): @@ -265,7 +273,7 @@ class Bob(Character): _signature_for_ursula, pubkey_sig_alice, hrac, encrypted_treasure_map = dht_value_splitter( packed_encrypted_treasure_map[5::], msgpack_remainder=True) verified, cleartext = self.verify_from(self.alice, encrypted_treasure_map, - signature_is_on_cleartext=True, decrypt=True) + signature_is_on_cleartext=True, decrypt=True) alices_signature, packed_node_list = BytestringSplitter(Signature)(cleartext, return_remainder=True) if not verified: return NOT_FROM_ALICE @@ -273,6 +281,34 @@ class Bob(Character): from nkms.policy.models import TreasureMap return TreasureMap(msgpack.loads(packed_node_list)) + def generate_work_orders(self, policy_group, *pfrags, num_ursulas=None): + # TODO: Perhaps instead of taking a policy_group, it makes more sense for Bob to reconstruct one with the TreasureMap. + from nkms.policy.models import WorkOrder # Prevent circular import + + # existing_work_orders = self._work_orders.get(pfrags, {}) # TODO: lookup whether we've done this reencryption before - see #137. + existing_work_orders = {} + generated_work_orders = {} + + for ursula_dht_key, ursula in self._ursulas.items(): + if ursula_dht_key in existing_work_orders: + continue + else: + work_order = WorkOrder.constructed_by_bob(policy_group.hrac(), pfrags, ursula_dht_key, self) + existing_work_orders[ursula_dht_key] = generated_work_orders[ursula_dht_key] = work_order + + if num_ursulas is not None: + if num_ursulas == len(generated_work_orders): + break + + return generated_work_orders + + def get_reencrypted_c_frag(self, networky_stuff, work_order): + cfrags = networky_stuff.reencrypt(work_order) + return cfrags + + def get_ursula(self, ursula_id): + return self._ursulas[ursula_id] + class Ursula(Character): _server_class = NuCypherDHTServer @@ -287,6 +323,7 @@ class Ursula(Character): self.keystore = urulsas_keystore self._rest_app = None + self._work_orders = [] @property def rest_app(self): @@ -307,12 +344,13 @@ class Ursula(Character): *args, **kwargs): if not id: - id = digest(secure_random(32)) # TODO: Network-wide deterministic ID generation (ie, auction or whatever) + id = digest(secure_random(32)) # TODO: Network-wide deterministic ID generation (ie, auction or whatever) #136. super().attach_server(ksize, alpha, id, storage) routes = [ Route('/kFrag/{hrac_as_hex}', 'POST', self.set_policy), + Route('/kFrag/{hrac_as_hex}/reencrypt', 'POST', self.reencrypt_via_rest), ] self._rest_app = App(routes=routes) @@ -322,7 +360,7 @@ class Ursula(Character): self.interface = interface return self.server.listen(port, interface) - def interface_info(self): + def dht_interface_info(self): return self.port, self.interface, self.interface_ttl def interface_dht_key(self): @@ -330,10 +368,10 @@ class Ursula(Character): def interface_dht_value(self): signature = self.seal(self.interface_hrac()) - return b"uaddr" + signature + self.seal + self.interface_hrac() + msgpack.dumps(self.interface_info()) + return b"uaddr" + signature + self.seal + self.interface_hrac() + msgpack.dumps(self.dht_interface_info()) def interface_hrac(self): - return self.hash(msgpack.dumps(self.interface_info())) + return self.hash(msgpack.dumps(self.dht_interface_info())) def publish_interface_information(self): if not self.port and self.interface: @@ -362,7 +400,34 @@ class Ursula(Character): raise # Do something appropriately RESTful (ie, 4xx). - return # A 200, which whatever policy metadata. + return # A 200, with whatever policy metadata. + + def reencrypt_via_rest(self, hrac_as_hex, request: http.Request): + from nkms.policy.models import WorkOrder # Avoid circular import + hrac = binascii.unhexlify(hrac_as_hex) + work_order = WorkOrder.from_rest_payload(hrac, request.body) + kfrag = self.keystore.get_kfrag(hrac) # Careful! :-) + cfrag_byte_stream = b"" + + for pfrag in work_order.pfrags: + cfrag_byte_stream += API.ecies_reencrypt(kfrag, pfrag.encrypted_key) + + self._work_orders.append(work_order) # TODO: Put this in Ursula's datastore + + return Response(content=cfrag_byte_stream, content_type="application/octet-stream") + + def work_orders(self, bob=None): + """ + TODO: This is better written as a model method for Ursula's datastore. + """ + if not bob: + return self._work_orders + else: + work_orders_from_bob = [] + for work_order in self._work_orders: + if work_order.bob == bob: + work_orders_from_bob.append(work_order) + return work_orders_from_bob class Seal(object): diff --git a/nkms/crypto/api.py b/nkms/crypto/api.py index 2bfa1ac86..e7f345eac 100644 --- a/nkms/crypto/api.py +++ b/nkms/crypto/api.py @@ -1,11 +1,12 @@ -from random import SystemRandom from typing import Tuple, Union, List import sha3 from nacl.secret import SecretBox from py_ecc.secp256k1 import N, privtopub, ecdsa_raw_recover, ecdsa_raw_sign +from random import SystemRandom from nkms.crypto import _internal +from nkms.crypto.fragments import KFrag, PFrag, CFrag from nkms.keystore.constants import SIG_KEYPAIR_BYTE, PUB_KEY_BYTE from npre import elliptic_curve from npre import umbral @@ -378,8 +379,9 @@ def ecies_split_rekey( privkey_a = priv_bytes2ec(privkey_a) if type(privkey_b) == bytes: privkey_b = priv_bytes2ec(privkey_b) - return PRE.split_rekey(privkey_a, privkey_b, - min_shares, total_shares) + umbral_rekeys = PRE.split_rekey(privkey_a, privkey_b, + min_shares, total_shares) + return [KFrag(umbral_kfrag=u) for u in umbral_rekeys] def ecies_ephemeral_split_rekey( @@ -402,14 +404,15 @@ def ecies_ephemeral_split_rekey( :return: A tuple containing a list of rekey frags, and a tuple of the encrypted ephemeral key data (enc_symm_key, enc_eph_privkey) """ - eph_privkey, enc_eph_data = _internal._ecies_gen_ephemeral_key(pubkey_b) - frags = ecies_split_rekey(privkey_a, eph_privkey, min_shares, total_shares) + eph_privkey, (encrypted_key, encrypted_message) = _internal._ecies_gen_ephemeral_key(pubkey_b) + kfrags = ecies_split_rekey(privkey_a, eph_privkey, min_shares, total_shares) + pfrag = PFrag(ephemeral_data_as_bytes=None, encrypted_key=encrypted_key, encrypted_message=encrypted_message) - return (frags, enc_eph_data) + return (kfrags, pfrag) def ecies_combine( - encrypted_keys: List[umbral.EncryptedKey] + cfrags: List[CFrag] ) -> umbral.EncryptedKey: """ Combines the encrypted keys together to form a rekey from split_rekey. @@ -418,7 +421,7 @@ def ecies_combine( :return: The combined EncryptedKey of the rekey """ - return PRE.combine(encrypted_keys) + return PRE.combine([cfrag.encrypted_key for cfrag in cfrags]) def ecies_reencrypt( @@ -437,4 +440,5 @@ def ecies_reencrypt( rekey = umbral.RekeyFrag(None, priv_bytes2ec(rekey)) if type(enc_key) == bytes: enc_key = umbral.EncryptedKey(priv_bytes2ec(enc_key), None) - return PRE.reencrypt(rekey, enc_key) + reencrypted_data = PRE.reencrypt(rekey, enc_key) + return CFrag(reencrypted_data=reencrypted_data) diff --git a/nkms/crypto/fragments.py b/nkms/crypto/fragments.py new file mode 100644 index 000000000..1ee1129e6 --- /dev/null +++ b/nkms/crypto/fragments.py @@ -0,0 +1,119 @@ +from nkms.crypto.utils import BytestringSplitter +from npre.constants import UNKNOWN_KFRAG +from npre.umbral import RekeyFrag, EncryptedKey + + +class PFrag(object): + _key_length = 34 + _message_length = 72 + _EXPECTED_LENGTH = _key_length + _message_length + + splitter = BytestringSplitter((bytes, _key_length), (bytes, _message_length)) + + def __init__(self, ephemeral_data_as_bytes=None, encrypted_key=None, encrypted_message=None): + from nkms.crypto.api import PRE # Avoid circular import + if ephemeral_data_as_bytes and encrypted_key: + raise ValueError("Pass either the ephemeral data as bytes or the encrypted key and message. Not both.") + elif ephemeral_data_as_bytes: + encrypted_key, self.encrypted_message = self.splitter(ephemeral_data_as_bytes) + self.encrypted_key = EncryptedKey(ekey=PRE.load_key(encrypted_key), re_id=None) + elif encrypted_key and encrypted_message: + self.encrypted_key = encrypted_key + self.encrypted_message = encrypted_message + else: + assert False # What do we do if all the values were None? Perhaps have an "UNKNOWN_PFRAG" concept? + + def __bytes__(self): + from nkms.crypto.api import PRE # Avoid circular import + encrypted_key_bytes = PRE.save_key(self.encrypted_key.ekey) + return encrypted_key_bytes + self.encrypted_message + + def __len__(self): + return len(bytes(self)) + + def deserialized(self): + return self.encrypted_key, self.encrypted_message + + +class KFrag(object): + + _EXPECTED_LENGTH = 66 + _is_unknown_kfrag = False + + def __init__(self, id_plus_key_as_bytes=None, umbral_kfrag=None): + if all((id_plus_key_as_bytes, umbral_kfrag)): + raise ValueError("Pass either the id/key or an umbral_kfrag (or neither for UNKNOWN_KFRAG). Not both.") + elif id_plus_key_as_bytes: + self._umbral_kfrag = RekeyFrag.from_bytes(id_plus_key_as_bytes) + elif umbral_kfrag: + self._umbral_kfrag = umbral_kfrag + else: + self._is_unknown_kfrag = True + + def __bytes__(self): + return bytes(self._umbral_kfrag) + + def __eq__(self, other_kfrag): + if other_kfrag is UNKNOWN_KFRAG: + return bool(self._is_unknown_kfrag) + else: + return bytes(self) == bytes(other_kfrag) + + def __add__(self, other): + return bytes(self) + other + + def __radd__(self, other): + return other + bytes(self) + + def __getitem__(self, slice): + return bytes(self)[slice] + + @property + def key(self): + return self._umbral_kfrag.key + + @property + def id(self): + return self._umbral_kfrag.id + + +class CFrag(object): + _EXPECTED_LENGTH = 67 + _key_element_length = 34 + _re_id_length = 33 + + def __init__(self, encrypted_key_as_bytes=None, reencrypted_data=None): + from nkms.crypto.api import PRE # Avoid circular import + if encrypted_key_as_bytes and reencrypted_data: + raise ValueError("Pass the bytes or the EncryptedKey, not both.") + elif encrypted_key_as_bytes: + if not len(encrypted_key_as_bytes) == self._EXPECTED_LENGTH: + raise ValueError("Got {} bytes; need {} for a proper cFrag.".format(len(encrypted_key_as_bytes)), + self._EXPECTED_LENGTH) + key_element = PRE.load_key(encrypted_key_as_bytes[:self._key_element_length]) + re_id = PRE.load_key(encrypted_key_as_bytes[self._key_element_length:]) + self.encrypted_key = EncryptedKey(ekey=key_element, re_id=re_id) + elif reencrypted_data: + self.encrypted_key = reencrypted_data + else: + assert False # Again, do we want a concept of an "empty" CFrag? + + def __bytes__(self): + from nkms.crypto.api import PRE # Avoid circular import + as_bytes = PRE.save_key(self.encrypted_key.ekey) + PRE.save_key(self.encrypted_key.re_id) + if len(as_bytes) != self._EXPECTED_LENGTH: + raise TypeError("Something went crazy wrong here. This CFrag serialized to {} bytes.".format(len(as_bytes))) + else: + return as_bytes + + def __len__(self): + return len(bytes(self)) + + def __add__(self, other): + return bytes(self) + other + + def __radd__(self, other): + return other + bytes(self) + + def __eq__(self, other_cfrag): + return bytes(self) == bytes(other_cfrag) diff --git a/nkms/crypto/utils.py b/nkms/crypto/utils.py index be62a94a8..67acc8a54 100644 --- a/nkms/crypto/utils.py +++ b/nkms/crypto/utils.py @@ -45,3 +45,15 @@ class BytestringSplitter(object): @staticmethod def get_message_meta(message_type): return message_type if isinstance(message_type, tuple) else (message_type, message_type._EXPECTED_LENGTH) + + +class RepeatingBytestringSplitter(BytestringSplitter): + + def __call__(self, splittable): + remainder = True + messages = [] + while remainder: + message, remainder = super().__call__(splittable, return_remainder=True) + messages.append(message) + splittable = remainder + return messages diff --git a/nkms/keystore/keystore.py b/nkms/keystore/keystore.py index ef286b578..45ec38cdc 100644 --- a/nkms/keystore/keystore.py +++ b/nkms/keystore/keystore.py @@ -1,4 +1,6 @@ import sha3 + +from nkms.crypto.fragments import KFrag from nkms.keystore import keypairs, constants from nkms.keystore.db.models import Key, KeyFrag from nkms.crypto.utils import BytestringSplitter @@ -20,11 +22,7 @@ class KeyStore(object): A storage class of cryptographic keys. """ - kFrag_splitter = BytestringSplitter( - Signature, - (bytes, constants.REKEY_FRAG_ID_LEN), - (bytes, constants.REKEY_FRAG_KEY_LEN) - ) + kfrag_splitter = BytestringSplitter(Signature, KFrag) def __init__(self, sqlalchemy_engine=None): """ @@ -105,12 +103,11 @@ class KeyStore(object): .format(hrac) ) # TODO: Make this use a class - sig, id, key = self.kFrag_splitter(kfrag.key_frag) + sig, kfrag = self.kfrag_splitter(kfrag.key_frag) - kFrag = RekeyFrag(id=id, key=key) if get_sig: - return (kFrag, sig) - return kFrag + return (kfrag, sig) + return kfrag def add_key(self, keypair: Union[keypairs.EncryptingKeypair, diff --git a/nkms/policy/models.py b/nkms/policy/models.py index ea014b8e8..7474386a8 100644 --- a/nkms/policy/models.py +++ b/nkms/policy/models.py @@ -1,18 +1,20 @@ +import binascii + import msgpack from nkms.characters import Alice, Bob, Ursula from nkms.crypto import api from nkms.crypto.api import keccak_digest -from nkms.crypto.constants import HASH_DIGEST_LENGTH, NOT_SIGNED +from nkms.crypto.constants import NOT_SIGNED +from nkms.crypto.fragments import KFrag, PFrag from nkms.crypto.powers import EncryptingPower from nkms.crypto.signature import Signature from nkms.crypto.utils import BytestringSplitter from nkms.keystore.keypairs import PublicKey from npre.constants import UNKNOWN_KFRAG -from npre.umbral import RekeyFrag group_payload_splitter = BytestringSplitter(PublicKey) -policy_payload_splitter = BytestringSplitter((bytes, 66)) # TODO: I wish ReKeyFrag worked with this interface. +policy_payload_splitter = BytestringSplitter(KFrag) class PolicyOffer(object): @@ -59,15 +61,15 @@ class PolicyManagerForAlice(PolicyManager): re_enc_keys, encrypted_key = self.owner.generate_rekey_frags(alice_priv_enc, bob, m, n) # TODO: Access Alice's private key inside this method. policies = [] - for kfrag_id, rekey in enumerate(re_enc_keys): + for kfrag_id, kfrag in enumerate(re_enc_keys): policy = Policy.from_alice( alice=self.owner, bob=bob, - kfrag=rekey, + kfrag=kfrag, ) policies.append(policy) - return PolicyGroup(uri, self.owner, bob, policies) + return PolicyGroup(uri, self.owner, bob, encrypted_key, policies) class PolicyGroup(object): @@ -77,10 +79,11 @@ class PolicyGroup(object): _id = None - def __init__(self, uri: bytes, alice: Alice, bob: Bob, policies=None) -> None: + def __init__(self, uri: bytes, alice: Alice, bob: Bob, pfrag, policies=None) -> None: self.policies = policies or [] self.alice = alice self.bob = bob + self.pfrag = pfrag self.uri = uri self.treasure_map = TreasureMap() @@ -137,7 +140,6 @@ class PolicyGroup(object): self.hrac(), full_payload) # TODO: Parse response for confirmation. - # Assuming response is what we hope for self.treasure_map.add_ursula(policy.ursula) @@ -219,7 +221,7 @@ class Policy(object): alice = Alice.from_pubkey_sig_bytes(alice_pubkey_sig) ursula.learn_about_actor(alice) verified, cleartext = ursula.verify_from(alice, payload_encrypted_for_ursula, - decrypt=True, signature_is_on_cleartext=True) + decrypt=True, signature_is_on_cleartext=True) if not verified: # TODO: What do we do if it's not signed properly? @@ -227,8 +229,7 @@ class Policy(object): alices_signature, policy_payload = BytestringSplitter(Signature)(cleartext, return_remainder=True) - kfrag_bytes, encrypted_challenge_pack = policy_payload_splitter(policy_payload, return_remainder=True) - kfrag = RekeyFrag.from_bytes(kfrag_bytes) + kfrag, encrypted_challenge_pack = policy_payload_splitter(policy_payload, return_remainder=True) policy = Policy(alice=alice, alices_signature=alices_signature, kfrag=kfrag, encrypted_challenge_pack=encrypted_challenge_pack) @@ -288,3 +289,44 @@ class TreasureMap(object): def __iter__(self): return iter(self.ids) + + +class WorkOrder(object): + def __init__(self, bob, kfrag_hrac, pfrags, receipt_bytes, receipt_signature, ursula_id=None): + self.bob = bob + self.kfrag_hrac = kfrag_hrac + self.pfrags = pfrags + self.receipt_bytes = receipt_bytes + self.receipt_signature = receipt_signature + self.ursula_id = ursula_id # TODO: We may still need a more elegant system for ID'ing Ursula. See #136. + + def __repr__(self): + return "WorkOrder (pfrags: {}) {} for {}".format([binascii.hexlify(bytes(p))[:6] for p in self.pfrags], + binascii.hexlify(self.receipt_bytes)[:6], + binascii.hexlify(self.ursula_id)[:6]) + + def __eq__(self, other): + return (self.receipt_bytes, self.receipt_signature) == (other.receipt_bytes, other.receipt_signature) + + @classmethod + def constructed_by_bob(cls, kfrag_hrac, pfrags, ursula_dht_key, bob): + receipt_bytes = b"wo:" + ursula_dht_key # TODO: represent the pfrags as bytes and hash them as part of the receipt, ie + keccak_digest(b"".join(pfrags)) - See #137 + receipt_signature = bob.seal(receipt_bytes) + return cls(bob, kfrag_hrac, pfrags, receipt_bytes, receipt_signature, ursula_dht_key) + + @classmethod + def from_rest_payload(cls, kfrag_hrac, rest_payload): + payload_splitter = BytestringSplitter(Signature, PublicKey) + signature, bob_pubkey_sig, (receipt_bytes, packed_pfrags) = payload_splitter(rest_payload, + msgpack_remainder=True) + pfrags = [PFrag(p) for p in msgpack.loads(packed_pfrags)] + verified = signature.verify(receipt_bytes, bob_pubkey_sig) + if not verified: + raise ValueError("This doesn't appear to be from Bob.") + bob = Bob.from_pubkey_sig_bytes(bob_pubkey_sig) + return cls(bob, kfrag_hrac, pfrags, receipt_bytes, signature) + + def payload(self): + pfrags_as_bytes = [bytes(p) for p in self.pfrags] + packed_receipt_and_pfrags = msgpack.dumps((self.receipt_bytes, msgpack.dumps(pfrags_as_bytes))) + return bytes(self.receipt_signature) + self.bob.seal + packed_receipt_and_pfrags diff --git a/tests/crypto/test_api.py b/tests/crypto/test_api.py index 5b1c7fedb..bed20bb0c 100644 --- a/tests/crypto/test_api.py +++ b/tests/crypto/test_api.py @@ -5,6 +5,7 @@ import sha3 from nacl.utils import EncryptedMessage from nkms.crypto import api +from nkms.crypto.fragments import PFrag from nkms.keystore.keypairs import PublicKey from npre import elliptic_curve as ec from npre import umbral @@ -297,10 +298,9 @@ class TestCrypto(unittest.TestCase): self.assertEqual(list, type(frags)) self.assertEqual(4, len(frags)) - self.assertEqual(tuple, type(enc_eph_data)) - self.assertEqual(2, len(enc_eph_data)) - self.assertEqual(umbral.EncryptedKey, type(enc_eph_data[0])) - self.assertEqual(EncryptedMessage, type(enc_eph_data[1])) + self.assertEqual(PFrag._EXPECTED_LENGTH, len(enc_eph_data)) + self.assertEqual(umbral.EncryptedKey, type(enc_eph_data.deserialized()[0])) + self.assertEqual(EncryptedMessage, type(enc_eph_data.deserialized()[1])) def test_ecies_combine(self): eph_priv = self.pre.gen_priv() @@ -320,7 +320,6 @@ class TestCrypto(unittest.TestCase): shares = [api.ecies_reencrypt(rk_frag, enc_key) for rk_frag in rk_selected] self.assertEqual(list, type(shares)) self.assertEqual(6, len(shares)) - [self.assertEqual(umbral.EncryptedKey, type(share)) for share in shares] e_b = api.ecies_combine(shares) self.assertEqual(umbral.EncryptedKey, type(e_b)) @@ -345,6 +344,6 @@ class TestCrypto(unittest.TestCase): self.assertEqual(umbral.RekeyFrag, type(rk_eb)) self.assertEqual(ec.ec_element, type(rk_eb.key)) - reenc_key = api.ecies_reencrypt(rk_eb, enc_key) - dec_key = api.ecies_decapsulate(self.privkey_b, reenc_key) + cfrag = api.ecies_reencrypt(rk_eb, enc_key) + dec_key = api.ecies_decapsulate(self.privkey_b, cfrag.encrypted_key) self.assertEqual(plain_key, dec_key) diff --git a/tests/crypto/test_bytestring_types.py b/tests/crypto/test_bytestring_types.py index 45452e1fd..d7d783cbd 100644 --- a/tests/crypto/test_bytestring_types.py +++ b/tests/crypto/test_bytestring_types.py @@ -1,6 +1,8 @@ import pytest +from nkms.crypto import api from nkms.crypto.api import secure_random +from nkms.crypto.fragments import KFrag from nkms.crypto.signature import Signature from nkms.crypto.utils import BytestringSplitter @@ -22,10 +24,22 @@ def test_split_signature_from_arbitrary_bytes(): some_bytes = secure_random(how_many_bytes) splitter = BytestringSplitter(Signature, (bytes, how_many_bytes)) - rebuilt_signature, rebuilt_bytes = splitter(signature + some_bytes) +def test_split_kfrag_from_arbitrary_bytes(): + rand_id = b'\x00' + api.secure_random(32) + rand_key = b'\x00' + api.secure_random(32) + kfrag = KFrag(rand_id + rand_key) + + how_many_bytes = 10 + some_bytes = secure_random(how_many_bytes) + + splitter = BytestringSplitter(KFrag, (bytes, how_many_bytes)) + rebuilt_kfrag, rebuilt_bytes = splitter(kfrag + some_bytes) + assert kfrag == rebuilt_kfrag + + def test_trying_to_extract_too_many_bytes_raises_typeerror(): how_many_bytes = 10 too_many_bytes = 11 diff --git a/tests/fixtures.py b/tests/fixtures.py index 7215ecdcf..5cec892b0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -36,7 +36,7 @@ def enacted_policy_group(alices_policy_group, ursulas): networky_stuff = MockNetworkyStuff(ursulas) alices_policy_group.find_n_ursulas(networky_stuff, offer) - alices_policy_group.enact_policies(networky_stuff) + alices_policy_group.enact_policies(networky_stuff) # REST call happens here. return alices_policy_group diff --git a/tests/keystore/test_keystore.py b/tests/keystore/test_keystore.py index 652d799df..0952ab5a8 100644 --- a/tests/keystore/test_keystore.py +++ b/tests/keystore/test_keystore.py @@ -1,6 +1,8 @@ import unittest import sha3 from sqlalchemy import create_engine + +from nkms.crypto.fragments import KFrag from nkms.keystore.db import Base from nkms.keystore import keystore, keypairs from npre.umbral import RekeyFrag @@ -72,19 +74,24 @@ class TestKeyStore(unittest.TestCase): key = self.ks.get_key(fingerprint_priv) def test_keyfrag_sqlite(self): + kfrag_component_length = 32 rand_sig = API.secure_random(65) - rand_id = b'\x00' + API.secure_random(32) - rand_key = b'\x00' + API.secure_random(32) + rand_id = b'\x00' + API.secure_random(kfrag_component_length) + rand_key = b'\x00' + API.secure_random(kfrag_component_length) rand_hrac = API.secure_random(32) - kfrag = RekeyFrag.from_bytes(rand_id+rand_key) + kfrag = KFrag(rand_id+rand_key) self.ks.add_kfrag(rand_hrac, kfrag, sig=rand_sig) # Check that kfrag was added - kfrag, signature = self.ks.get_kfrag(rand_hrac, get_sig=True) + kfrag_from_datastore, signature = self.ks.get_kfrag(rand_hrac, get_sig=True) self.assertEqual(rand_sig, signature) - self.assertEqual(kfrag.id, rand_id) - self.assertEqual(kfrag.key, rand_key) + + # De/serialization happens here, by dint of the slicing interface, which casts the kfrag to bytes. + # The +1 is to account for the metabyte. + self.assertEqual(kfrag_from_datastore[:kfrag_component_length + 1], rand_id) + self.assertEqual(kfrag_from_datastore[kfrag_component_length + 1:], rand_key) + self.assertEqual(kfrag_from_datastore, kfrag) # Check that kfrag gets deleted self.ks.del_kfrag(rand_hrac) diff --git a/tests/network/test_network_actors.py b/tests/network/test_network_actors.py index 68306f38a..a7f7a166f 100644 --- a/tests/network/test_network_actors.py +++ b/tests/network/test_network_actors.py @@ -107,16 +107,6 @@ def test_alice_creates_policy_group_with_correct_hrac(alices_policy_group): bytes(alice.seal) + bytes(bob.seal) + alice.__resource_id) -def test_alice_enacts_policies_in_policy_group_via_rest(enacted_policy_group): - """ - Now that Alice has made a PolicyGroup, she can enact its policies, using Ursula's Public Key to encrypt each offer - and transmitting them via REST. - """ - ursula = enacted_policy_group.policies[0].ursula - kfrag_that_was_set = ursula.keystore.get_kfrag(enacted_policy_group.hrac()) - assert bool(kfrag_that_was_set) # TODO: This can be a more poignant assertion. - - def test_alice_sets_treasure_map_on_network(enacted_policy_group, ursulas): """ Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and sends it to Ursula via the DHT. diff --git a/tests/network/test_network_upgrade.py b/tests/network/test_network_upgrade.py index e470412ed..49792ee5f 100644 --- a/tests/network/test_network_upgrade.py +++ b/tests/network/test_network_upgrade.py @@ -1,12 +1,21 @@ -from tests.utilities import EVENT_LOOP +from nkms.crypto import api +from tests.utilities import EVENT_LOOP, MockNetworkyStuff -def test_bob_can_follow_treasure_map(enacted_policy_group, ursulas): +def test_alice_enacts_policies_in_policy_group_via_rest(enacted_policy_group): + """ + Now that Alice has made a PolicyGroup, she can enact its policies, using Ursula's Public Key to encrypt each offer + and transmitting them via REST. + """ + ursula = enacted_policy_group.policies[0].ursula + kfrag_that_was_set = ursula.keystore.get_kfrag(enacted_policy_group.hrac()) + assert bool(kfrag_that_was_set) # TODO: This can be a more poignant assertion. + + +def test_bob_can_follow_treasure_map(enacted_policy_group, ursulas, alice, bob): """ Upon receiving a TreasureMap, Bob populates his list of Ursulas with the correct number. """ - alice = enacted_policy_group.alice - bob = enacted_policy_group.bob assert len(bob._ursulas) == 0 setter, encrypted_treasure_map, packed_encrypted_treasure_map, signature_for_bob, signature_for_ursula = alice.publish_treasure_map( @@ -15,3 +24,41 @@ def test_bob_can_follow_treasure_map(enacted_policy_group, ursulas): bob.follow_treasure_map(enacted_policy_group.treasure_map) assert len(bob._ursulas) == len(ursulas) + + +def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_policy_group, alice, bob, ursulas): + """ + Now that Bob has his list of Ursulas, he can issue a WorkOrder to one. Upon receiving the WorkOrder, Ursula + saves it and responds by re-encrypting and giving Bob a cFrag. + + This is a multipart test; it shows proper relations between the Characters Ursula and Bob and also proper + interchange between a KFrag, PFrag, and CFrag object in the context of REST-driven proxy re-encryption. + """ + + # We pick up our story with Bob already having followed the treasure map above, ie: + assert len(bob._ursulas) == len(ursulas) + + the_pfrag = enacted_policy_group.pfrag + + # We'll test against just a single Ursula - here, we made a WorkOrder for just one. + work_orders = bob.generate_work_orders(enacted_policy_group, the_pfrag, num_ursulas=1) + assert len(work_orders) == 1 + + networky_stuff = MockNetworkyStuff(ursulas) + + ursula_dht_key, work_order = list(work_orders.items())[0] + cfrags = bob.get_reencrypted_c_frag(networky_stuff, work_order) + + the_cfrag = cfrags[0] # We only gave one pFrag, so we only got one cFrag. + + # Wow, Bob has his cFrag! Let's make sure everything went properly. First, we'll show that it is in fact + # the correct cFrag (ie, that Ursula performed reencryption properly). + ursula = networky_stuff.get_ursula_by_id(work_order.ursula_id) + the_kfrag = ursula.keystore.get_kfrag(work_order.kfrag_hrac) + the_correct_cfrag = api.ecies_reencrypt(the_kfrag, the_pfrag.encrypted_key) + assert the_cfrag == the_correct_cfrag # It's the correct cfrag! + + # Now we'll show that Ursula saved the correct WorkOrder. + work_orders_from_bob = ursula.work_orders(bob=bob) + assert len(work_orders_from_bob) == 1 + assert work_orders_from_bob[0] == work_order diff --git a/tests/utilities.py b/tests/utilities.py index a177230ed..a591f7284 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -1,15 +1,16 @@ import asyncio -from apistar.test import TestClient +import pytest from sqlalchemy.engine import create_engine -from nkms.characters import Ursula, Alice, Bob +from apistar.test import TestClient +from nkms.characters import Ursula +from nkms.crypto.fragments import CFrag +from nkms.crypto.utils import RepeatingBytestringSplitter from nkms.keystore import keystore from nkms.keystore.db import Base from nkms.network.node import NetworkyStuff - - NUMBER_OF_URSULAS_IN_NETWORK = 6 EVENT_LOOP = asyncio.get_event_loop() @@ -52,6 +53,7 @@ class MockPolicyOfferResponse(object): class MockNetworkyStuff(NetworkyStuff): def __init__(self, ursulas): + self._ursulas = {u.interface_dht_key(): u for u in ursulas} self.ursulas = iter(ursulas) def go_live_with_policy(self, ursula, policy_offer): @@ -70,3 +72,20 @@ class MockNetworkyStuff(NetworkyStuff): mock_client = TestClient(ursula.rest_app) response = mock_client.post('http://localhost/kFrag/{}'.format(hrac.hex()), payload) return True, ursula.interface_dht_key() + + def get_ursula_by_id(self, ursula_id): + print(self._ursulas) + try: + ursula = self._ursulas[ursula_id] + except KeyError: + pytest.fail("No Ursula with ID {}".format(ursula_id)) + return ursula + + def reencrypt(self, work_order): + print(work_order) + ursula = self.get_ursula_by_id(work_order.ursula_id) + mock_client = TestClient(ursula.rest_app) + payload = work_order.payload() + response = mock_client.post('http://localhost/kFrag/{}/reencrypt'.format(work_order.kfrag_hrac.hex()), payload) + cfrags = RepeatingBytestringSplitter(CFrag)(response.content) + return cfrags