diff --git a/nucypher/characters/control/interfaces.py b/nucypher/characters/control/interfaces.py index 6b47a2667..dd52211b5 100644 --- a/nucypher/characters/control/interfaces.py +++ b/nucypher/characters/control/interfaces.py @@ -116,7 +116,7 @@ class AliceInterface(CharacterPublicInterface): revocation, fail_reason = attempt if fail_reason == RestMiddleware.NotFound: del (failed_revocations[node_id]) - if len(failed_revocations) <= (policy.n - policy.treasure_map.m + 1): + if len(failed_revocations) <= (policy.n - policy.m + 1): del (self.implementer.active_policies[policy_id]) response_data = {'failed_revocations': len(failed_revocations)} @@ -192,18 +192,14 @@ class BobInterface(CharacterPublicInterface): self.implementer.join_policy(label=label, publisher_verifying_key=alice_verifying_key) - if self.implementer.federated_only: - from nucypher.policy.maps import TreasureMap as _MapClass - else: - from nucypher.policy.maps import SignedTreasureMap as _MapClass + from nucypher.policy.maps import EncryptedTreasureMap - # TODO: This LBYL is ugly and fraught with danger. NRN - #2751 if isinstance(treasure_map, bytes): - treasure_map = _MapClass.from_bytes(treasure_map) + treasure_map = EncryptedTreasureMap.from_bytes(treasure_map) if isinstance(treasure_map, str): tmap_bytes = treasure_map.encode() - treasure_map = _MapClass.from_bytes(b64decode(tmap_bytes)) + treasure_map = EncryptedTreasureMap.from_bytes(b64decode(tmap_bytes)) plaintexts = self.implementer.retrieve(message_kit, enrico=enrico, diff --git a/nucypher/characters/control/specifications/fields/treasuremap.py b/nucypher/characters/control/specifications/fields/treasuremap.py index eb902b2a5..be79fefe4 100644 --- a/nucypher/characters/control/specifications/fields/treasuremap.py +++ b/nucypher/characters/control/specifications/fields/treasuremap.py @@ -22,30 +22,13 @@ from marshmallow import fields from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes from nucypher.control.specifications.exceptions import InvalidInputData from nucypher.control.specifications.fields.base import BaseField +from nucypher.policy.maps import EncryptedTreasureMap as EncryptedTreasureMapClass class EncryptedTreasureMap(BaseField, fields.Field): """ JSON Parameter representation of TreasureMap. - - Requires that either federated or non-federated (blockchain) treasure maps are expected to function correcty. This - information is indicated either: - - At creation time of the field via constructor parameter 'federated_only' (takes precedence) - OR - - Via the parent Schema context it is running in. In this case, the parent Schema context dictionary should have a - key-value entry in it with the IS_FEDERATED_CONTEXT_KEY class constant as the key, and a value of True/False. - - If neither is provided, the TreasureMap is assumed to be a SignedTreasureMap. The federated/non-federated context - of the TreasureMap only applies to deserialization and validation since the received value is in base64 encoded - bytes. """ - IS_FEDERATED_CONTEXT_KEY = 'federated' - - def __init__(self, federated_only=None, *args, **kwargs): - self.federated_only = federated_only - BaseField.__init__(self, *args, **kwargs) - fields.Field.__init__(self, *args, **kwargs) - def _serialize(self, value, attr, obj, **kwargs): return b64encode(bytes(value)).decode() @@ -56,23 +39,7 @@ class EncryptedTreasureMap(BaseField, fields.Field): raise InvalidInputData(f"Could not parse {self.name}: {e}") def _validate(self, value): - from nucypher.policy.maps import SignedTreasureMap - from nucypher.policy.maps import TreasureMap as UnsignedTreasureMap - - # determine context: federated or non-federated defined by field or schema - is_federated_context = False # default to non-federated - if self.federated_only is not None: - # defined by field itself - is_federated_context = self.federated_only - else: - # defined by schema - if self.parent is not None and self.parent.context.get('federated') is not None: - is_federated_context = self.context.get('federated') - try: - splitter = SignedTreasureMap.get_splitter(value) if not is_federated_context else UnsignedTreasureMap.get_splitter(value) - _ = splitter(value) - return True - except InvalidNativeDataTypes as e: - # store exception - raise InvalidInputData(f"Could not parse {self.name} (federated={is_federated_context}): {e}") + EncryptedTreasureMapClass.from_bytes(value) + except Exception as e: + raise InvalidInputData(f"Could not convert input for {self.name} to an EncryptedTreasureMap: {e}") from e diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index db6e30dab..314039a08 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -77,7 +77,6 @@ from nucypher.config.constants import END_OF_POLICIES_PROBATIONARY_PERIOD from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage from nucypher.control.controllers import WebController from nucypher.control.emitters import StdoutEmitter -from nucypher.crypto.constants import HRAC_LENGTH from nucypher.crypto.keypairs import HostingKeypair from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.powers import ( @@ -108,7 +107,7 @@ from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher from nucypher.network.protocols import InterfaceInfo, parse_node_uri from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app from nucypher.network.trackers import AvailabilityTracker -from nucypher.policy.maps import TreasureMap, AuthorizedKeyFrag +from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap, AuthorizedKeyFrag from nucypher.policy.orders import WorkOrder from nucypher.policy.policies import Policy from nucypher.utilities.logging import Logger @@ -404,7 +403,7 @@ class Alice(Character, BlockchainPolicyAuthor): """ try: # Wait for a revocation threshold of nodes to be known ((n - m) + 1) - revocation_threshold = ((policy.n - policy.treasure_map.m) + 1) + revocation_threshold = ((policy.n - policy.m) + 1) self.block_until_specific_nodes_are_known( policy.revocation_kit.revokable_addresses, allow_missing=(policy.n - revocation_threshold)) @@ -624,13 +623,13 @@ class Bob(Character): return unknown_ursulas, known_ursulas, treasure_map.m - def _try_orient(self, treasure_map, publisher_verifying_key): + def _try_orient(self, enc_treasure_map, publisher_verifying_key): # TODO: should be rather a Publisher character alice = Alice.from_public_keys(verifying_key=publisher_verifying_key) compass = self.make_compass_for_alice(alice) try: - treasure_map.orient(compass) - except treasure_map.InvalidSignature: + return enc_treasure_map.orient(compass) + except EncryptedTreasureMap.InvalidSignature: raise # TODO: Maybe do something here? NRN def get_treasure_map(self, publisher_verifying_key: PublicKey, label: bytes): @@ -646,9 +645,9 @@ class Bob(Character): if not self.known_nodes: raise self.NotEnoughTeachers("Can't retrieve without knowing about any nodes at all. Pass a teacher or seed node.") - treasure_map = self.get_treasure_map_from_known_ursulas(hrac) + enc_treasure_map = self.get_treasure_map_from_known_ursulas(hrac) - self._try_orient(treasure_map, publisher_verifying_key) + treasure_map = self._try_orient(enc_treasure_map, publisher_verifying_key) self.treasure_maps[hrac] = treasure_map return treasure_map @@ -656,7 +655,9 @@ class Bob(Character): return partial(self.verify_from, alice, decrypt=True) def construct_policy_hrac(self, publisher_verifying_key: PublicKey, label: bytes) -> bytes: - _hrac = keccak_digest(bytes(publisher_verifying_key) + bytes(self.stamp) + label)[:HRAC_LENGTH] + _hrac = TreasureMap.derive_hrac(publisher_verifying_key=publisher_verifying_key, + bob_verifying_key=self.stamp.as_umbral_pubkey(), + label=label) return _hrac def get_treasure_map_from_known_ursulas(self, hrac: bytes, timeout=3): @@ -931,11 +932,11 @@ class Bob(Character): def _handle_treasure_map(self, publisher_verifying_key: PublicKey, label: bytes, - treasure_map: Optional['TreasureMap'] = None, + enc_treasure_map: Optional['EncryptedTreasureMap'] = None, ) -> 'TreasureMap': - if treasure_map is not None: - self._try_orient(treasure_map, publisher_verifying_key) - # self.treasure_maps[treasure_map.hrac] = treasure_map # TODO: Can we? + if enc_treasure_map is not None: + treasure_map = self._try_orient(enc_treasure_map, publisher_verifying_key) + self.treasure_maps[treasure_map.hrac] = treasure_map else: hrac = self.construct_policy_hrac(publisher_verifying_key, label) try: @@ -954,7 +955,7 @@ class Bob(Character): *message_kits: UmbralMessageKit, label: bytes, policy_encrypting_key: Optional[PublicKey] = None, - treasure_map: Optional['TreasureMap'] = None, + treasure_map: Optional['EncryptedTreasureMap'] = None, # Source Authentication enrico: Optional["Enrico"] = None, @@ -972,7 +973,7 @@ class Bob(Character): # If a policy publisher's verifying key is not passed, use Alice's by default. publisher_verifying_key = alice_verifying_key - treasure_map, m = self._handle_treasure_map(treasure_map=treasure_map, + treasure_map, m = self._handle_treasure_map(enc_treasure_map=treasure_map, publisher_verifying_key=publisher_verifying_key, label=label) diff --git a/nucypher/characters/unlawful.py b/nucypher/characters/unlawful.py index 87bf3f848..da797b1b4 100644 --- a/nucypher/characters/unlawful.py +++ b/nucypher/characters/unlawful.py @@ -28,7 +28,7 @@ from nucypher.config.constants import TEMPORARY_DOMAIN from nucypher.crypto.utils import encrypt_and_sign from nucypher.crypto.powers import CryptoPower, SigningPower, DecryptingPower, TransactingPower from nucypher.exceptions import DevelopmentInstallationRequired -from nucypher.policy.maps import SignedTreasureMap +from nucypher.policy.maps import EncryptedTreasureMap class Vladimir(Ursula): @@ -113,12 +113,11 @@ class Vladimir(Ursula): If I see a TreasureMap being published, I can substitute my own payload and hope that Ursula will store it for me for free. """ - old_message_kit = legit_treasure_map.message_kit + old_message_kit = legit_treasure_map._encrypted_tmap new_message_kit, _signature = self.encrypt_for(self, b"I want to store this message for free.") - legit_treasure_map.message_kit = new_message_kit + legit_treasure_map._encrypted_tmap = new_message_kit # I'll copy Alice's key so that Ursula thinks that the HRAC has been properly signed. - legit_treasure_map.message_kit.sender_verifying_key = old_message_kit.sender_verifying_key - legit_treasure_map._set_payload() + legit_treasure_map._encrypted_tmap.sender_verifying_key = old_message_kit.sender_verifying_key response = self.network_middleware.put_treasure_map_on_node(node=target_node, map_payload=bytes(legit_treasure_map)) @@ -210,13 +209,13 @@ class Amonia(Alice): the_map = policy.treasure_map # I'll make a copy of it to modify for use in this attack. - like_a_map_but_awful = SignedTreasureMap.from_bytes(bytes(the_map)) + like_a_map_but_awful = EncryptedTreasureMap.from_bytes(bytes(the_map)) # I'll split the film up into segments, because I know Ursula checks that the file size is under 50k. for i in range(50): # I'll include a small portion of this awful film in a new message kit. We don't care about the signature for bob. not_the_bees = b"Not the bees!" + int(i).to_bytes(length=4, byteorder="big") - like_a_map_but_awful.message_kit, _signature_for_bob_which_is_never_Used = encrypt_and_sign( + like_a_map_but_awful._encrypted_tmap, _signature_for_bob_which_is_never_Used = encrypt_and_sign( bob.public_keys(DecryptingPower), plaintext=not_the_bees, signer=self.stamp, @@ -228,15 +227,15 @@ class Amonia(Alice): # I know Ursula checks the public signature because she thinks I'm Alice. So I'll sign my bad hrac. like_a_map_but_awful._public_signature = self.stamp(bytes(self.stamp) + bad_hrac) - like_a_map_but_awful._hrac = bad_hrac - - # With the bad hrac and the segment of the film, I'm ready to make a phony payload and map ID. - like_a_map_but_awful._set_payload() - like_a_map_but_awful._set_id() + like_a_map_but_awful.hrac = bad_hrac # I'll sign it again, so that it appears to match the policy for which I already paid. transacting_power = self._crypto_power.power_ups(TransactingPower) - like_a_map_but_awful.include_blockchain_signature(blockchain_signer=transacting_power.sign_message) + like_a_map_but_awful._blockchain_signature = EncryptedTreasureMap._make_blockchain_signature( + blockchain_signer=transacting_power.sign_message, + public_signature=like_a_map_but_awful._public_signature, + hrac=like_a_map_but_awful.hrac, + encrypted_tmap=like_a_map_but_awful._encrypted_tmap) # Sucker. response = self.network_middleware.put_treasure_map_on_node(sucker_ursula, map_payload=bytes(like_a_map_but_awful)) diff --git a/nucypher/crypto/constants.py b/nucypher/crypto/constants.py index ff368c2a7..9eff147cf 100644 --- a/nucypher/crypto/constants.py +++ b/nucypher/crypto/constants.py @@ -19,7 +19,6 @@ along with nucypher. If not, see . from cryptography.hazmat.primitives import hashes # Policy component sizes -HRAC_LENGTH = 16 SIGNATURE_SIZE = 64 EIP712_MESSAGE_SIGNATURE_SIZE = 65 diff --git a/nucypher/crypto/splitters.py b/nucypher/crypto/splitters.py index 1667f711f..3828527a3 100644 --- a/nucypher/crypto/splitters.py +++ b/nucypher/crypto/splitters.py @@ -18,7 +18,6 @@ along with nucypher. If not, see . from bytestring_splitter import BytestringSplitter -from nucypher.crypto.constants import HRAC_LENGTH from nucypher.crypto.umbral_adapter import CapsuleFrag, PublicKey, Capsule, Signature, KeyFrag key_splitter = BytestringSplitter((PublicKey, PublicKey.serialized_size())) @@ -26,4 +25,3 @@ signature_splitter = BytestringSplitter((Signature, Signature.serialized_size()) capsule_splitter = BytestringSplitter((Capsule, Capsule.serialized_size())) cfrag_splitter = BytestringSplitter((CapsuleFrag, CapsuleFrag.serialized_size())) kfrag_splitter = BytestringSplitter((KeyFrag, KeyFrag.serialized_size())) -hrac_splitter = BytestringSplitter((bytes, HRAC_LENGTH)) diff --git a/nucypher/datastore/models.py b/nucypher/datastore/models.py index c001b0cd1..0c591dee0 100644 --- a/nucypher/datastore/models.py +++ b/nucypher/datastore/models.py @@ -51,7 +51,7 @@ class Workorder(DatastoreRecord): class EncryptedTreasureMap(DatastoreRecord): - # Ideally this is a `policy.collections.TreasureMap`, but it causes a huge + # Ideally this is a `policy.maps.EncryptedTreasureMap`, but it causes a huge # circular import due to `Bob` and `Character` in `policy.collections`. # TODO #2126 _treasure_map = RecordField(bytes) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 8eb06bb88..2dd237d1a 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -308,25 +308,22 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) -> We set the datastore identifier as the HRAC. """ - if not this_node.federated_only: - from nucypher.policy.maps import SignedTreasureMap as _MapClass - else: - from nucypher.policy.maps import TreasureMap as _MapClass + from nucypher.policy.maps import EncryptedTreasureMap # Step 1: First, we verify the signature of the received treasure map. # This step also deserializes the treasure map if it's signed correctly. try: - received_treasure_map = _MapClass.from_bytes(bytes_representation=request.data, verify=True) - except _MapClass.InvalidSignature: - log.info(f"Bad TreasureMap HRAC Signature; not storing for HRAC {received_treasure_map._hrac.hex()}") + received_treasure_map = EncryptedTreasureMap.from_bytes(request.data) + except EncryptedTreasureMap.InvalidSignature: + log.info(f"Bad TreasureMap HRAC Signature; not storing for HRAC {received_treasure_map.hrac.hex()}") return Response("This TreasureMap's HRAC is not properly signed.", status=401) - hrac = received_treasure_map._hrac + hrac = received_treasure_map.hrac # Step 2: Check if we already have the treasure map. try: with datastore.describe(TreasureMapModel, hrac) as stored_treasure_map: - if _MapClass.from_bytes(stored_treasure_map.treasure_map) == received_treasure_map: + if EncryptedTreasureMap.from_bytes(stored_treasure_map.treasure_map) == received_treasure_map: return Response("Already have this map.", status=303) except RecordNotFound: # This appears to be a new treasure map that we don't have! @@ -336,7 +333,7 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) -> # treasure map is valid pursuant to an active policy. # We also set the expiration from the data on the blockchain here. if not this_node.federated_only: - policy = this_node.policy_agent.fetch_policy(policy_id=received_treasure_map._hrac) + policy = this_node.policy_agent.fetch_policy(policy_id=received_treasure_map.hrac) # If the Policy doesn't exist, the policy_data is all zeros. if policy.sponsor is NULL_ADDRESS: log.info(f"TreasureMap is for non-existent Policy; not storing {hrac.hex()}") diff --git a/nucypher/network/treasuremap.py b/nucypher/network/treasuremap.py index e07ade596..da9e8e04f 100644 --- a/nucypher/network/treasuremap.py +++ b/nucypher/network/treasuremap.py @@ -23,6 +23,7 @@ from nucypher.acumen.perception import FleetSensor from nucypher.crypto.signing import InvalidSignature from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.nodes import Learner +from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap def get_treasure_map_from_known_ursulas(learner: Learner, @@ -33,11 +34,6 @@ def get_treasure_map_from_known_ursulas(learner: Learner, Iterate through the nodes we know, asking for the TreasureMap. Return the first one who has it. """ - if learner.federated_only: - from nucypher.policy.maps import TreasureMap as _MapClass - else: - from nucypher.policy.maps import SignedTreasureMap as _MapClass - start = maya.now() # Spend no more than half the timeout finding the nodes. 8 nodes is arbitrary. Come at me. @@ -63,7 +59,7 @@ def get_treasure_map_from_known_ursulas(learner: Learner, if response.status_code == 200 and response.content: try: - treasure_map = _MapClass.from_bytes(response.content) + treasure_map = EncryptedTreasureMap.from_bytes(response.content) return treasure_map except InvalidSignature: # TODO: What if a node gives a bunk TreasureMap? NRN @@ -74,8 +70,8 @@ def get_treasure_map_from_known_ursulas(learner: Learner, learner.learn_from_teacher_node() if (start - maya.now()).seconds > timeout: - raise _MapClass.NowhereToBeFound(f"Asked {len(learner.known_nodes)} nodes, " - f"but none had map {hrac.hex()}") + raise TreasureMap.NowhereToBeFound(f"Asked {len(learner.known_nodes)} nodes, " + f"but none had map {hrac.hex()}") def find_matching_nodes(known_nodes: FleetSensor, diff --git a/nucypher/policy/maps.py b/nucypher/policy/maps.py index 0a517afe2..220de41b2 100644 --- a/nucypher/policy/maps.py +++ b/nucypher/policy/maps.py @@ -16,31 +16,150 @@ along with nucypher. If not, see . """ -from typing import Optional, Callable, Union, Sequence +from typing import Optional, Callable, Union, Sequence, Dict from bytestring_splitter import ( BytestringSplitter, VariableLengthBytestring, - BytestringKwargifier, BytestringSplittingError, - VersioningMixin, - BrandingMixin ) from constant_sorrow.constants import NO_DECRYPTION_PERFORMED, NOT_SIGNED from eth_utils.address import to_checksum_address, to_canonical_address +from eth_typing.evm import ChecksumAddress from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH from nucypher.characters.base import Character -from nucypher.crypto.constants import HRAC_LENGTH, EIP712_MESSAGE_SIGNATURE_SIZE +from nucypher.crypto.constants import EIP712_MESSAGE_SIGNATURE_SIZE from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.powers import DecryptingPower, SigningPower from nucypher.crypto.signing import SignatureStamp -from nucypher.crypto.splitters import signature_splitter, hrac_splitter, kfrag_splitter +from nucypher.crypto.splitters import signature_splitter, kfrag_splitter from nucypher.crypto.umbral_adapter import KeyFrag, VerifiedKeyFrag, PublicKey, Signature from nucypher.crypto.utils import keccak_digest, encrypt_and_sign, verify_eip_191 from nucypher.network.middleware import RestMiddleware +class TreasureMap: + + HRAC_LENGTH = 16 + + class NowhereToBeFound(RestMiddleware.NotFound): + """ + Called when no known nodes have it. + """ + + class IsDisorienting(Exception): + """ + Called when an oriented TreasureMap lists fewer than m destinations, which + leaves Bob disoriented. + """ + + main_splitter = BytestringSplitter( + (int, 1, {'byteorder': 'big'}), + (bytes, HRAC_LENGTH), + ) + + ursula_and_kfrag_payload_splitter = BytestringSplitter( + (to_checksum_address, ETH_ADDRESS_BYTE_LENGTH), + (UmbralMessageKit, VariableLengthBytestring), + ) + + @staticmethod + def derive_hrac(publisher_verifying_key: PublicKey, bob_verifying_key: PublicKey, label: bytes) -> bytes: + """ + Here's our "hashed resource access code". + + A hash of: + * Alice's public key + * Bob's public key + * the label + + Alice and Bob have all the information they need to construct this. + Ursula does not, so we share it with her. + + This way, Bob can generate it and use it to find the TreasureMap. + """ + return keccak_digest(bytes(publisher_verifying_key) + bytes(bob_verifying_key) + label)[:TreasureMap.HRAC_LENGTH] + + @classmethod + def construct_by_publisher(cls, + publisher: 'Alice', + bob: 'Bob', + label: bytes, + ursulas: Sequence['Ursula'], + verified_kfrags: Sequence[VerifiedKeyFrag], + m: int, + ) -> 'TreasureMap': + """Create a new treasure map for a collection of ursulas and kfrags.""" + + if m < 1 or m > 255: + raise ValueError("The threshold must be between 1 and 255.") + + if len(ursulas) < m: + raise ValueError( + f"The number of destinations ({len(ursulas)}) " + f"must be equal or greater than the threshold ({m})") + + # The HRAC is needed to produce kfrag writs. + hrac = cls.derive_hrac(publisher_verifying_key=publisher.stamp.as_umbral_pubkey(), + bob_verifying_key=bob.public_keys(SigningPower), + label=label) + + # Encrypt each kfrag for an Ursula. + destinations = {} + for ursula, verified_kfrag in zip(ursulas, verified_kfrags): + kfrag_payload = bytes(AuthorizedKeyFrag.construct_by_publisher(hrac=hrac, + verified_kfrag=verified_kfrag, + publisher_stamp=publisher.stamp)) + encrypted_kfrag, _signature = encrypt_and_sign(recipient_pubkey_enc=ursula.public_keys(DecryptingPower), + plaintext=kfrag_payload, + signer=publisher.stamp) + + destinations[ursula.checksum_address] = encrypted_kfrag + + return cls(m=m, hrac=hrac, destinations=destinations) + + def __init__(self, + m: int, + hrac: bytes, + destinations: Dict[ChecksumAddress, bytes], + ): + self.m = m + self.destinations = destinations + self.hrac = hrac + + def prepare_for_publication(self, publisher, bob, blockchain_signer=None): + return EncryptedTreasureMap.construct_by_publisher( + self, publisher, bob, blockchain_signer=blockchain_signer) + + def _nodes_as_bytes(self): + nodes_as_bytes = b"" + for ursula_address, encrypted_kfrag in self.destinations.items(): + node_id = to_canonical_address(ursula_address) + kfrag = bytes(VariableLengthBytestring(encrypted_kfrag.to_bytes())) + nodes_as_bytes += (node_id + kfrag) + return nodes_as_bytes + + def __bytes__(self): + return self.m.to_bytes(1, "big") + self.hrac + self._nodes_as_bytes() + + @classmethod + def from_bytes(cls, data): + try: + m, hrac, remainder = cls.main_splitter(data, return_remainder=True) + ursula_and_kfrags = cls.ursula_and_kfrag_payload_splitter.repeat(remainder) + except BytestringSplittingError as e: + raise cls.IsDisorienting('Invalid treasure map contents.') from e + destinations = {u: k for u, k in ursula_and_kfrags} + return cls(m, hrac, destinations) + + def __iter__(self): + return iter(self.destinations.items()) + + def __len__(self): + return len(self.destinations) + + class AuthorizedKeyFrag: _WRIT_CHECKSUM_SIZE = 32 @@ -50,8 +169,8 @@ class AuthorizedKeyFrag: ENCRYPTED_SIZE = 619 _splitter = BytestringSplitter( - hrac_splitter, # HRAC - BytestringSplitter((bytes, _WRIT_CHECKSUM_SIZE)), # kfrag checksum + (bytes, TreasureMap.HRAC_LENGTH), # HRAC + (bytes, _WRIT_CHECKSUM_SIZE), # kfrag checksum signature_splitter, # Publisher's signature kfrag_splitter, ) @@ -103,215 +222,49 @@ class AuthorizedKeyFrag: return cls(hrac, kfrag_checksum, writ_signature, kfrag) -class TreasureMapSplitter(BrandingMixin, VersioningMixin, BytestringKwargifier): - pass +class EncryptedTreasureMap: - -class TreasureMap: - VERSION_NUMBER = 1 # Increment when serialization format changes. - - _BRAND = b'TM' - _VERSION = int(VERSION_NUMBER).to_bytes(VersioningMixin.HEADER_LENGTH, 'big') - - class NowhereToBeFound(RestMiddleware.NotFound): - """ - Called when no known nodes have it. - """ - - class IsDisorienting(Exception): - """ - Called when an oriented TreasureMap lists fewer than m destinations, which - leaves Bob disoriented. - """ - - class OldVersion(Exception): - """Raised when a treasure map's version is too old or contents are incompatible.""" - - ursula_and_kfrag_payload_splitter = BytestringSplitter( - (to_checksum_address, ETH_ADDRESS_BYTE_LENGTH), + _splitter = BytestringSplitter( + signature_splitter, + (bytes, TreasureMap.HRAC_LENGTH), (UmbralMessageKit, VariableLengthBytestring), - ) + (bytes, 1)) from nucypher.crypto.signing import \ InvalidSignature # Raised when the public signature (typically intended for Ursula) is not valid. - def __init__(self, - m: int = None, - destinations=None, - message_kit: UmbralMessageKit = None, - public_signature: Signature = None, - hrac: Optional[bytes] = None, - version: bytes = None - ) -> None: + @staticmethod + def _make_blockchain_signature(blockchain_signer, public_signature, hrac, encrypted_tmap): + # This method exists mainly to link this scheme to the corresponding test + payload = bytes(public_signature) + hrac + encrypted_tmap.to_bytes() + return blockchain_signer(payload) - if version is not None: - self.version = version + @classmethod + def construct_by_publisher(cls, treasure_map, publisher, bob, blockchain_signer=None): + # TODO: `publisher` here can be different from the one in TreasureMap, it seems. + # Do we ever cross-check them? Do we want to enforce them to be the same? - if m is not None: - if m > 255: - raise ValueError("Largest allowed value for m is 255.") - self._m = m + bob_encrypting_key = bob.public_keys(DecryptingPower) - self._destinations = destinations or {} + encrypted_tmap, _signature_for_bob = encrypt_and_sign(bob_encrypting_key, + plaintext=bytes(treasure_map), + signer=publisher.stamp) + public_signature = publisher.stamp(bytes(publisher.stamp) + treasure_map.hrac) + + if blockchain_signer is not None: + blockchain_signature = EncryptedTreasureMap._make_blockchain_signature( + blockchain_signer, public_signature, treasure_map.hrac, encrypted_tmap) else: - self._m = NO_DECRYPTION_PERFORMED - self._destinations = NO_DECRYPTION_PERFORMED + blockchain_signature = None - self._id = None + return cls(treasure_map.hrac, public_signature, encrypted_tmap, blockchain_signature=blockchain_signature) - self.message_kit = message_kit + def __init__(self, hrac, public_signature, encrypted_tmap, blockchain_signature=None): + self.hrac = hrac self._public_signature = public_signature - self._hrac = hrac - self._payload = None - - if message_kit is not None: - self.message_kit = message_kit - self._set_id() - else: - self.message_kit = None - - def __eq__(self, other): - try: - return self.public_id() == other.public_id() - except AttributeError: - raise TypeError( - f"Can't compare {type(other).__name__} to a TreasureMap (it needs to implement public_id() )") - - def __iter__(self): - return iter(self.destinations.items()) - - def __len__(self): - return len(self.destinations) - - def __bytes__(self): - if self._payload is None: - self._set_payload() - return self._BRAND + self._VERSION + self._payload - - @classmethod - def get_splitter(cls, bytes_representation: bytes) -> BytestringKwargifier: - """ - Takes a bytes representation of a treasure map and returns a splitter matching the apparent format. - In the event of a missing or malformed header, returns the splitter designed for unversioned maps. - """ - representation_metadata = TreasureMapSplitter.get_metadata(bytes_representation) - - # header = bytes_representation[:cls._HEADER_SIZE] - brand_matches = representation_metadata['brand'] == cls._BRAND - version = representation_metadata['version'] - - if version in cls._splitters and brand_matches: - # This representation is compatible with a known stencil. - splitter = cls._splitters[version] - else: - # It's possible that this is a preversioned representation. - splitter = cls._splitters['unversioned'] # TODO: In this case, it's still a map from a previous version - how will we handle sin KFrags? - return splitter - - @classmethod - def from_bytes(cls, bytes_representation: bytes, verify: bool = True) -> Union['TreasureMap', 'SignedTreasureMap']: - splitter = cls.get_splitter(bytes_representation) - treasure_map = splitter(bytes_representation) - if verify: - treasure_map.public_verify() - return treasure_map - - @property - def _verifying_key(self): - return self.message_kit.sender_verifying_key - - @property - def m(self) -> int: - if self._m == NO_DECRYPTION_PERFORMED: - raise TypeError("The TreasureMap is probably encrypted. You must decrypt it first.") - return self._m - - @property - def destinations(self): - if self._destinations == NO_DECRYPTION_PERFORMED: - raise TypeError("The TreasureMap is probably encrypted. You must decrypt it first.") - return self._destinations - - def _set_id(self) -> None: - self._id = keccak_digest(bytes(self._verifying_key) + bytes(self._hrac)).hex() - - def _set_payload(self) -> None: - self._payload = bytes(self._public_signature) \ - + self._hrac \ - + bytes(VariableLengthBytestring(self.message_kit.to_bytes())) - - def derive_hrac(self, publisher_stamp: SignatureStamp, bob_verifying_key: PublicKey, label: bytes) -> None: - """ - Here's our "hashed resource access code". - - A hash of: - * Alice's public key - * Bob's public key - * the label - - Alice and Bob have all the information they need to construct this. - Ursula does not, so we share it with her. - - This way, Bob can generate it and use it to find the TreasureMap. - """ - self._hrac = keccak_digest(bytes(publisher_stamp) + bytes(bob_verifying_key) + label)[:HRAC_LENGTH] - - def prepare_for_publication(self, bob_encrypting_key, publisher_stamp): - plaintext = self._m.to_bytes(1, "big") + self.nodes_as_bytes() - self.message_kit, _signature_for_bob = encrypt_and_sign(bob_encrypting_key, - plaintext=plaintext, - signer=publisher_stamp) - self._public_signature = publisher_stamp(bytes(publisher_stamp) + self._hrac) - self._set_payload() - self._set_id() - - def nodes_as_bytes(self): - if self.destinations == NO_DECRYPTION_PERFORMED: - return NO_DECRYPTION_PERFORMED - else: - nodes_as_bytes = b"" - for ursula_address, encrypted_kfrag in self.destinations.items(): - node_id = to_canonical_address(ursula_address) - kfrag = bytes(VariableLengthBytestring(encrypted_kfrag.to_bytes())) - nodes_as_bytes += (node_id + kfrag) - return nodes_as_bytes - - def add_kfrag(self, ursula, verified_kfrag: VerifiedKeyFrag, publisher_stamp: SignatureStamp) -> None: - if self.destinations == NO_DECRYPTION_PERFORMED: - # Unsure how this situation can arise, but let's raise an error just in case. - raise TypeError("This TreasureMap is encrypted. You can't add another node without decrypting it.") - - if not self._hrac: - # TODO: Use a better / different exception or encapsulate HRAC derivation with KFrag population. - raise RuntimeError( - 'Cannot add KFrag to TreasureMap without an HRAC set. Call "derive_hrac" and try again.') - - # Encrypt this kfrag payload for Ursula. - kfrag_payload = bytes(AuthorizedKeyFrag.construct_by_publisher(hrac=self._hrac, - verified_kfrag=verified_kfrag, - publisher_stamp=publisher_stamp)) - encrypted_kfrag, _signature = encrypt_and_sign(recipient_pubkey_enc=ursula.public_keys(DecryptingPower), - plaintext=kfrag_payload, - signer=publisher_stamp) - - # Set the encrypted kfrag payload into the map. - self.destinations[ursula.checksum_address] = encrypted_kfrag - - def public_id(self) -> str: - """ - We need an ID that Bob can glean from knowledge he already has *and* which Ursula can verify came from Alice. - Ursula will refuse to propagate this if it she can't prove the payload is signed by Alice's public key, - which is included in it, - """ - return self._id - - def public_verify(self) -> bool: - message = bytes(self._verifying_key) + self._hrac - verified = self._public_signature.verify(self._verifying_key, message=message) - if verified: - return True - else: - raise self.InvalidSignature("This TreasureMap is not properly publicly signed by Alice.") + self._verifying_key = encrypted_tmap.sender_verifying_key + self._encrypted_tmap = encrypted_tmap + self._blockchain_signature = blockchain_signature def orient(self, compass: Callable): """ @@ -319,113 +272,47 @@ class TreasureMap: payload message kit). """ try: - map_in_the_clear = compass(message_kit=self.message_kit) + map_in_the_clear = compass(self._encrypted_tmap) except Character.InvalidSignature: raise self.InvalidSignature("This TreasureMap does not contain the correct signature from Alice to Bob.") - self._m = map_in_the_clear[0] - try: - ursula_and_kfrags = self.ursula_and_kfrag_payload_splitter.repeat(map_in_the_clear[1:]) - except BytestringSplittingError: - raise self.IsDisorienting('Invalid treasure map contents.') - self._destinations = {u: k for u, k in ursula_and_kfrags} - self.check_for_sufficient_destinations() # TODO: Remove this check, how is this even possible? - def check_for_sufficient_destinations(self): - if len(self._destinations) < self._m or self._m == 0: - raise self.IsDisorienting( - f"TreasureMap lists only {len(self._destinations)} destination, " - f"but requires interaction with {self._m} nodes.") + return TreasureMap.from_bytes(map_in_the_clear) - @classmethod - def construct_by_publisher(cls, - publisher: 'Alice', - bob: 'Bob', - label: bytes, - ursulas: Sequence['Ursula'], - verified_kfrags: Sequence[VerifiedKeyFrag], - m: int - ) -> 'TreasureMap': - """Create a new treasure map for a collection of ursulas and kfrags.""" - - # The HRAC is needed to produce kfrag writs. - treasure_map = cls(m=m) - treasure_map.derive_hrac(publisher_stamp=publisher.stamp, - bob_verifying_key=bob.public_keys(SigningPower), - label=label) - - # Encrypt each kfrag for an Ursula. - for ursula, verified_kfrag in zip(ursulas, verified_kfrags): - treasure_map.add_kfrag(ursula=ursula, - verified_kfrag=verified_kfrag, - publisher_stamp=publisher.stamp) - - # Sign the map if needed before sending it out into the world. - treasure_map.prepare_for_publication(bob_encrypting_key=bob.public_keys(DecryptingPower), - publisher_stamp=publisher.stamp) - - return treasure_map - - -# FIXME: a dirty hack to make the tests pass. Fix it ASAP. -# The problem with __new__ is that it does not get called before the first object of the class -# is instantiated, so when we call `from_bytes()` for the first time, the `_splitters` dict -# needs to already be populated. -TreasureMap._splitters = { - 'unversioned': BytestringKwargifier(TreasureMap, - public_signature=signature_splitter, - hrac=(bytes, HRAC_LENGTH), - message_kit=(UmbralMessageKit, VariableLengthBytestring), - ), - 1: TreasureMapSplitter(TreasureMap, - public_signature=signature_splitter, - hrac=(bytes, HRAC_LENGTH), - message_kit=(UmbralMessageKit, VariableLengthBytestring), - ) - } - - -class SignedTreasureMap(TreasureMap): - - _BRAND = b'SM' - - def __init__(self, blockchain_signature=NOT_SIGNED, *args, **kwargs): - self._blockchain_signature = blockchain_signature - super().__init__(*args, **kwargs) - - def include_blockchain_signature(self, blockchain_signer): - if self._payload is None: - self._set_payload() - self._blockchain_signature = blockchain_signer(self._payload) + def __bytes__(self): + return (bytes(self._public_signature) + + self.hrac + + bytes(VariableLengthBytestring(self._encrypted_tmap.to_bytes())) + + # Other variants: + # - store all zeros if no blockchain signature + # - store the flag too in addition to all zeros, for added sanity check + # - use a faux blockchain signer in federated case + (b'\x00' if self._blockchain_signature is None else (b'\x01' + bytes(self._blockchain_signature))) + ) def verify_blockchain_signature(self, checksum_address): - self._set_payload() - return verify_eip_191(message=self._payload, + if self._blockchain_signature is None: + raise ValueError("This EncryptedTreasureMap is not blockchain-signed") + payload = bytes(self._public_signature) + self.hrac + self._encrypted_tmap.to_bytes() + return verify_eip_191(message=payload, signature=self._blockchain_signature, address=checksum_address) - def __bytes__(self): - if self._blockchain_signature is NOT_SIGNED: - raise self.InvalidSignature( - "Can't cast a SignedTreasureMap to bytes until it has a blockchain signature " - "(otherwise, is it really a 'SignedTreasureMap'?") - if self._payload is None: - self._set_payload() - return self._BRAND + self._VERSION + self._blockchain_signature + self._payload + def _public_verify(self) -> bool: + message = bytes(self._verifying_key) + self.hrac + if not self._public_signature.verify(self._verifying_key, message=message): + raise self.InvalidSignature("This TreasureMap is not properly publicly signed by Alice.") + @classmethod + def from_bytes(cls, data): + try: + public_signature, hrac, message_kit, bc_sig, remainder = cls._splitter(data, return_remainder=True) + if bc_sig == b'\x01': + blockchain_signature, = BytestringSplitter((bytes, EIP712_MESSAGE_SIGNATURE_SIZE))(remainder) + else: + blockchain_signature = None + except BytestringSplittingError as e: + raise TreasureMap.IsDisorienting('Invalid treasure map contents.') from e -# FIXME: a dirty hack to make the tests pass. Fix it ASAP. -# See the comment at `TreasureMap._splitters` above. -SignedTreasureMap._splitters = { - 'unversioned': BytestringKwargifier(SignedTreasureMap, - blockchain_signature=EIP712_MESSAGE_SIGNATURE_SIZE, - public_signature=signature_splitter, - hrac=(bytes, HRAC_LENGTH), - message_kit=(UmbralMessageKit, VariableLengthBytestring), - ), - 1: TreasureMapSplitter(SignedTreasureMap, - blockchain_signature=EIP712_MESSAGE_SIGNATURE_SIZE, - public_signature=signature_splitter, - hrac=(bytes, HRAC_LENGTH), - message_kit=(UmbralMessageKit, VariableLengthBytestring), - ) - } + result = cls(hrac, public_signature, message_kit, blockchain_signature=blockchain_signature) + result._public_verify() + return result diff --git a/nucypher/policy/orders.py b/nucypher/policy/orders.py index 36a46726a..ed92e22ed 100644 --- a/nucypher/policy/orders.py +++ b/nucypher/policy/orders.py @@ -32,7 +32,6 @@ from nucypher.crypto.splitters import ( capsule_splitter, cfrag_splitter, signature_splitter, - hrac_splitter, kfrag_splitter ) from nucypher.crypto.umbral_adapter import ( @@ -41,7 +40,7 @@ from nucypher.crypto.umbral_adapter import ( PublicKey, Signature ) -from nucypher.policy.maps import AuthorizedKeyFrag +from nucypher.policy.maps import AuthorizedKeyFrag, TreasureMap class WorkOrder: @@ -59,7 +58,7 @@ class WorkOrder: + key_splitter \ + key_splitter \ + key_splitter \ - + hrac_splitter \ + + BytestringSplitter((bytes, TreasureMap.HRAC_LENGTH)) \ + BytestringSplitter((bytes, VariableLengthBytestring)) class PRETask: diff --git a/nucypher/policy/policies.py b/nucypher/policy/policies.py index 559817ce0..eda2dbe02 100644 --- a/nucypher/policy/policies.py +++ b/nucypher/policy/policies.py @@ -26,14 +26,14 @@ from eth_typing.evm import ChecksumAddress from twisted.internet import reactor from nucypher.blockchain.eth.constants import POLICY_ID_LENGTH -from nucypher.crypto.constants import HRAC_LENGTH from nucypher.crypto.kits import RevocationKit -from nucypher.crypto.powers import TransactingPower +from nucypher.crypto.powers import TransactingPower, DecryptingPower from nucypher.crypto.splitters import key_splitter from nucypher.crypto.utils import keccak_digest from nucypher.crypto.umbral_adapter import PublicKey, VerifiedKeyFrag, Signature from nucypher.crypto.utils import construct_policy_id from nucypher.network.middleware import RestMiddleware +from nucypher.policy.maps import TreasureMap from nucypher.policy.reservoir import ( make_federated_staker_reservoir, MergedReservoir, @@ -231,7 +231,9 @@ class Policy(ABC): Alice and Bob have all the information they need to construct this. 'Ursula' does not, so we share it with her. """ - self.hrac = keccak_digest(bytes(self.publisher.stamp) + bytes(self.bob.stamp) + self.label)[:HRAC_LENGTH] + self.hrac = TreasureMap.derive_hrac(publisher_verifying_key=self.publisher.stamp.as_umbral_pubkey(), + bob_verifying_key=self.bob.stamp.as_umbral_pubkey(), + label=self.label) def __repr__(self): return f"{self.__class__.__name__}:{self._id.hex()[:6]}" @@ -342,21 +344,8 @@ class Policy(ABC): return accepted_arrangements - def _make_treasure_map(self, - network_middleware: RestMiddleware, - arrangements: Dict['Ursula', Arrangement], - ) -> 'TreasureMap': - """Author a new treasure map for this policy as Alice..""" - treasure_map = self._treasure_map_class.construct_by_publisher(publisher=self.publisher, - bob=self.bob, - label=self.label, - ursulas=list(arrangements), - verified_kfrags=self.kfrags, - m=self.m) - return treasure_map - def _make_publisher(self, - treasure_map: 'TreasureMap', + treasure_map: 'EncryptedTreasureMap', network_middleware: RestMiddleware, ) -> TreasureMapPublisher: @@ -369,6 +358,9 @@ class Policy(ABC): nodes=target_nodes, network_middleware=network_middleware) + def _encrypt_treasure_map(self, treasure_map): + return treasure_map.prepare_for_publication(self.publisher, self.bob) + def enact(self, network_middleware: RestMiddleware, handpicked_ursulas: Optional[Iterable['Ursula']] = None, @@ -388,18 +380,27 @@ class Policy(ABC): self._enact_arrangements(arrangements) - treasure_map = self._make_treasure_map(network_middleware=network_middleware, - arrangements=arrangements) - treasure_map_publisher = self._make_publisher(treasure_map=treasure_map, + treasure_map = TreasureMap.construct_by_publisher(publisher=self.publisher, + bob=self.bob, + label=self.label, + ursulas=list(arrangements), + verified_kfrags=self.kfrags, + m=self.m) + + enc_treasure_map = self._encrypt_treasure_map(treasure_map) + + treasure_map_publisher = self._make_publisher(treasure_map=enc_treasure_map, network_middleware=network_middleware) - revocation_kit = RevocationKit(treasure_map=treasure_map, signer=self.publisher.stamp) # TODO: Signal revocation without using encrypted kfrag + # TODO: Signal revocation without using encrypted kfrag + revocation_kit = RevocationKit(treasure_map=treasure_map, signer=self.publisher.stamp) enacted_policy = EnactedPolicy(self._id, self.hrac, self.label, self.public_key, - treasure_map, + treasure_map.m, + enc_treasure_map, treasure_map_publisher, revocation_kit, self.publisher.stamp.as_umbral_pubkey()) @@ -426,8 +427,6 @@ class Policy(ABC): class FederatedPolicy(Policy): - from nucypher.policy.maps import TreasureMap as __map_class - _treasure_map_class = __map_class def _not_enough_ursulas_exception(self): return Policy.NotEnoughUrsulas @@ -445,9 +444,6 @@ class BlockchainPolicy(Policy): A collection of n Arrangements representing a single Policy """ - from nucypher.policy.maps import SignedTreasureMap as __map_class - _treasure_map_class = __map_class - class InvalidPolicyValue(ValueError): pass @@ -542,15 +538,12 @@ class BlockchainPolicy(Policy): def _enact_arrangements(self, arrangements: Dict['Ursula', Arrangement]) -> None: self._publish_to_blockchain(ursulas=list(arrangements)) - def _make_treasure_map(self, - network_middleware: RestMiddleware, - arrangements: Dict['Ursula', Arrangement], - ) -> 'TreasureMap': - - treasure_map = super()._make_treasure_map(network_middleware, arrangements) + def _encrypt_treasure_map(self, treasure_map): transacting_power = self.publisher._crypto_power.power_ups(TransactingPower) - treasure_map.include_blockchain_signature(transacting_power.sign_message) - return treasure_map + return treasure_map.prepare_for_publication( + self.publisher, + self.bob, + blockchain_signer=transacting_power.sign_message) class EnactedPolicy: @@ -560,7 +553,8 @@ class EnactedPolicy: hrac: bytes, label: bytes, public_key: PublicKey, - treasure_map: 'TreasureMap', + m: int, + treasure_map: 'EncryptedTreasureMap', treasure_map_publisher: TreasureMapPublisher, revocation_kit: RevocationKit, publisher_verifying_key: PublicKey, @@ -573,7 +567,8 @@ class EnactedPolicy: self.treasure_map = treasure_map self.treasure_map_publisher = treasure_map_publisher self.revocation_kit = revocation_kit - self.n = len(self.treasure_map.destinations) + self.m = m + self.n = len(self.revocation_kit) self.publisher_verifying_key = publisher_verifying_key def publish_treasure_map(self): diff --git a/nucypher/utilities/porter/control/interfaces.py b/nucypher/utilities/porter/control/interfaces.py index e5aad1829..11b47ef2b 100644 --- a/nucypher/utilities/porter/control/interfaces.py +++ b/nucypher/utilities/porter/control/interfaces.py @@ -19,7 +19,6 @@ from typing import List, Optional from eth_typing import ChecksumAddress from nucypher.crypto.umbral_adapter import PublicKey -from nucypher.characters.control.specifications.fields import EncryptedTreasureMap from nucypher.control.interfaces import ControlInterface, attach_schema from nucypher.utilities.porter.control.specifications import porter_schema @@ -27,8 +26,6 @@ from nucypher.utilities.porter.control.specifications import porter_schema class PorterInterface(ControlInterface): def __init__(self, porter: 'Porter' = None, *args, **kwargs): super().__init__(implementer=porter, *args, **kwargs) - # set federated/non-federated context for publish treasure map schema - PorterInterface.publish_treasure_map._schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = porter.federated_only # # Alice Endpoints diff --git a/nucypher/utilities/porter/control/specifications/fields/hrac.py b/nucypher/utilities/porter/control/specifications/fields/hrac.py index 806f35698..dd31d8ebf 100644 --- a/nucypher/utilities/porter/control/specifications/fields/hrac.py +++ b/nucypher/utilities/porter/control/specifications/fields/hrac.py @@ -20,7 +20,7 @@ from marshmallow import fields from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes from nucypher.control.specifications.exceptions import InvalidInputData from nucypher.control.specifications.fields.base import BaseField -from nucypher.crypto.constants import HRAC_LENGTH +from nucypher.policy.maps import TreasureMap class HRAC(BaseField, fields.String): @@ -40,5 +40,5 @@ class HRAC(BaseField, fields.String): if not isinstance(value, bytes): raise InvalidInputData(f"Could not convert input for {self.name} to a valid TreasureMap HRAC: must be a bytestring") - if len(value) != HRAC_LENGTH: + if len(value) != TreasureMap.HRAC_LENGTH: raise InvalidInputData(f"Could not convert input for {self.name} to a valid TreasureMap HRAC: invalid length") diff --git a/tests/acceptance/characters/control/test_rpc_control_blockchain.py b/tests/acceptance/characters/control/test_rpc_control_blockchain.py index 94954c4b9..d3ef6c6ac 100644 --- a/tests/acceptance/characters/control/test_rpc_control_blockchain.py +++ b/tests/acceptance/characters/control/test_rpc_control_blockchain.py @@ -22,13 +22,18 @@ from nucypher.characters.control.interfaces import AliceInterface from nucypher.characters.control.interfaces import BobInterface, EnricoInterface from nucypher.crypto.constants import EIP712_MESSAGE_SIGNATURE_SIZE from nucypher.crypto.powers import DecryptingPower -from nucypher.policy.maps import SignedTreasureMap +from nucypher.policy.maps import TreasureMap from tests.utils.controllers import get_fields, validate_json_rpc_response_data -def test_bob_rpc_character_control_join_policy(bob_rpc_controller, join_control_request, enacted_blockchain_policy, blockchain_bob, blockchain_ursulas): +def test_bob_rpc_character_control_join_policy(bob_rpc_controller, + join_control_request, + blockchain_treasure_map, + enacted_blockchain_policy, + blockchain_bob, + blockchain_ursulas): for ursula in blockchain_ursulas: - if ursula.checksum_address in enacted_blockchain_policy.treasure_map.destinations: + if ursula.checksum_address in blockchain_treasure_map.destinations: # Simulate passing in a teacher-uri blockchain_bob.remember_node(ursula) break @@ -68,21 +73,13 @@ def test_bob_rpc_character_control_retrieve_with_tmap( response = bob_rpc_controller.send(request_data) assert response.data['result']['cleartexts'][0] == 'Welcome to flippering number 1.' - # Make a wrong (empty) treasure map + # Make a wrong treasure map + enc_wrong_tmap = bytes(enacted_blockchain_policy.treasure_map)[:-1] - wrong_tmap = SignedTreasureMap(m=0) - wrong_tmap.derive_hrac(publisher_stamp=blockchain_alice.stamp, bob_verifying_key=blockchain_bob.stamp, label=b"Wrong") - wrong_tmap.prepare_for_publication( - bob_encrypting_key=blockchain_bob.public_keys(DecryptingPower), - publisher_stamp=blockchain_alice.stamp) - wrong_tmap._blockchain_signature = b"this is not a signature, but we don't need one for this test....." # ...because it only matters when Ursula looks at it. (65 bytes) - - assert len(wrong_tmap._blockchain_signature) == EIP712_MESSAGE_SIGNATURE_SIZE - - tmap_bytes = bytes(wrong_tmap) + tmap_bytes = bytes(enc_wrong_tmap) tmap_64 = b64encode(tmap_bytes).decode() request_data['params']['treasure_map'] = tmap_64 - with pytest.raises(SignedTreasureMap.IsDisorienting): + with pytest.raises(TreasureMap.IsDisorienting): bob_rpc_controller.send(request_data) diff --git a/tests/acceptance/characters/control/test_treasure_map_field_non_federated.py b/tests/acceptance/characters/control/test_treasure_map_field_non_federated.py index b3b009c93..e2cfcfe36 100644 --- a/tests/acceptance/characters/control/test_treasure_map_field_non_federated.py +++ b/tests/acceptance/characters/control/test_treasure_map_field_non_federated.py @@ -25,7 +25,7 @@ from nucypher.control.specifications.exceptions import InvalidInputData def test_treasure_map(enacted_blockchain_policy): treasure_map = enacted_blockchain_policy.treasure_map - field = EncryptedTreasureMap(federated_only=False) + field = EncryptedTreasureMap() serialized = field._serialize(value=treasure_map, attr=None, obj=None) assert serialized == b64encode(bytes(treasure_map)).decode() diff --git a/tests/acceptance/characters/control/test_web_control_blockchain.py b/tests/acceptance/characters/control/test_web_control_blockchain.py index 324c80f96..2dd7343c5 100644 --- a/tests/acceptance/characters/control/test_web_control_blockchain.py +++ b/tests/acceptance/characters/control/test_web_control_blockchain.py @@ -26,7 +26,7 @@ from click.testing import CliRunner import nucypher from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.powers import DecryptingPower -from nucypher.policy.maps import TreasureMap, SignedTreasureMap +from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap click_runner = CliRunner() @@ -89,8 +89,8 @@ def test_alice_web_character_control_grant(alice_web_controller_test_client, gra assert 'alice_verifying_key' in response_data['result'] map_bytes = b64decode(response_data['result']['treasure_map']) - encrypted_map = SignedTreasureMap.from_bytes(map_bytes) - assert encrypted_map._hrac is not None + encrypted_map = EncryptedTreasureMap.from_bytes(map_bytes) + assert encrypted_map.hrac is not None # Send bad data to assert error returns response = alice_web_controller_test_client.put(endpoint, data=json.dumps({'bad': 'input'})) @@ -175,14 +175,19 @@ def test_alice_character_control_decrypt(alice_web_controller_test_client, assert response.status_code == 405 -def test_bob_character_control_join_policy(bob_web_controller_test_client, enacted_blockchain_policy, blockchain_alice, blockchain_bob, blockchain_ursulas): +def test_bob_character_control_join_policy(bob_web_controller_test_client, + blockchain_treasure_map, + enacted_blockchain_policy, + blockchain_alice, + blockchain_bob, + blockchain_ursulas): request_data = { 'label': enacted_blockchain_policy.label.decode(), 'publisher_verifying_key': bytes(enacted_blockchain_policy.publisher_verifying_key).hex(), } for ursula in blockchain_ursulas: - if ursula.checksum_address in enacted_blockchain_policy.treasure_map.destinations: + if ursula.checksum_address in blockchain_treasure_map.destinations: # Simulate passing in a teacher-uri blockchain_bob.remember_node(ursula) break diff --git a/tests/acceptance/characters/test_decentralized_grant.py b/tests/acceptance/characters/test_decentralized_grant.py index c9a40ac9b..8bb7a10ec 100644 --- a/tests/acceptance/characters/test_decentralized_grant.py +++ b/tests/acceptance/characters/test_decentralized_grant.py @@ -24,7 +24,7 @@ import pytest from nucypher.crypto.kits import PolicyMessageKit from nucypher.crypto.utils import keccak_digest from nucypher.datastore.models import EncryptedTreasureMap as DatastoreTreasureMap -from nucypher.policy.maps import SignedTreasureMap +from nucypher.policy.maps import EncryptedTreasureMap def test_decentralized_grant(blockchain_alice, blockchain_bob, blockchain_ursulas): @@ -45,13 +45,15 @@ def test_decentralized_grant(blockchain_alice, blockchain_bob, blockchain_ursula policy_id = keccak_digest(label + bytes(blockchain_bob.stamp)) assert policy_id == policy.id + treasure_map = blockchain_bob._try_orient(policy.treasure_map, policy.publisher_verifying_key) + # The number of actually enacted arrangements is exactly equal to n. - assert len(policy.treasure_map.destinations) == n + assert len(treasure_map.destinations) == n # Let's look at the enacted arrangements. for ursula in blockchain_ursulas: - if ursula.checksum_address in policy.treasure_map.destinations: - kfrag_kit = policy.treasure_map.destinations[ursula.checksum_address] + if ursula.checksum_address in treasure_map.destinations: + kfrag_kit = treasure_map.destinations[ursula.checksum_address] # TODO: try to decrypt? # TODO: Use a new type for EncryptedKFrags? @@ -62,11 +64,11 @@ def test_alice_sets_treasure_map_decentralized(enacted_blockchain_policy, blockc """ Same as test_alice_sets_treasure_map except with a blockchain policy. """ - treasure_map_hrac = enacted_blockchain_policy.treasure_map._hrac + treasure_map_hrac = enacted_blockchain_policy.treasure_map.hrac found = 0 for node in blockchain_bob.matching_nodes_among(blockchain_alice.known_nodes): with node.datastore.describe(DatastoreTreasureMap, treasure_map_hrac) as treasure_map_on_node: - assert SignedTreasureMap.from_bytes(treasure_map_on_node.treasure_map) == enacted_blockchain_policy.treasure_map + assert EncryptedTreasureMap.from_bytes(treasure_map_on_node.treasure_map).hrac == enacted_blockchain_policy.hrac found += 1 assert found @@ -94,4 +96,4 @@ def test_bob_retrieves_treasure_map_from_decentralized_node(enacted_blockchain_p # Now he'll have better success finding that map. treasure_map_from_wire = bob.get_treasure_map(blockchain_alice.stamp.as_umbral_pubkey(), enacted_blockchain_policy.label) - assert enacted_blockchain_policy.treasure_map == treasure_map_from_wire + assert enacted_blockchain_policy.treasure_map.hrac == treasure_map_from_wire.hrac diff --git a/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py index 173c26c6d..5255e5dd8 100644 --- a/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py +++ b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py @@ -619,7 +619,8 @@ def test_collect_rewards_integration(click_runner, handpicked_ursulas={ursula}) # Ensure that the handpicked Ursula was selected for the policy - assert ursula.checksum_address in blockchain_policy.treasure_map.destinations + treasure_map = blockchain_bob._try_orient(blockchain_policy.treasure_map, blockchain_policy.publisher_verifying_key) + assert ursula.checksum_address in treasure_map.destinations # Bob learns about the new staker and joins the policy blockchain_bob.start_learning_loop() diff --git a/tests/acceptance/porter/control/test_porter_rpc_control_blockchain.py b/tests/acceptance/porter/control/test_porter_rpc_control_blockchain.py index ec9daf65e..41067a4f1 100644 --- a/tests/acceptance/porter/control/test_porter_rpc_control_blockchain.py +++ b/tests/acceptance/porter/control/test_porter_rpc_control_blockchain.py @@ -20,7 +20,6 @@ from base64 import b64encode, b64decode import pytest from nucypher.crypto.umbral_adapter import PublicKey -from nucypher.crypto.constants import HRAC_LENGTH from nucypher.crypto.powers import DecryptingPower from nucypher.network.nodes import Learner from nucypher.policy.maps import TreasureMap @@ -90,7 +89,7 @@ def test_publish_and_get_treasure_map(blockchain_porter_rpc_controller, random_bob_encrypting_key = PublicKey.from_bytes( bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac")) random_hrac = "93a9482bdf3b4f2e9df906a35144ca84" - assert len(bytes.fromhex(random_hrac)) == HRAC_LENGTH + assert len(bytes.fromhex(random_hrac)) == TreasureMap.HRAC_LENGTH get_treasure_map_params = { 'hrac': random_hrac, 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() diff --git a/tests/acceptance/porter/control/test_porter_web_control_blockchain.py b/tests/acceptance/porter/control/test_porter_web_control_blockchain.py index c8f4c216d..d65652936 100644 --- a/tests/acceptance/porter/control/test_porter_web_control_blockchain.py +++ b/tests/acceptance/porter/control/test_porter_web_control_blockchain.py @@ -22,7 +22,6 @@ from base64 import b64encode import pytest from nucypher.crypto.umbral_adapter import PublicKey -from nucypher.crypto.constants import HRAC_LENGTH from nucypher.crypto.powers import DecryptingPower from nucypher.network.nodes import Learner from nucypher.policy.maps import TreasureMap @@ -106,7 +105,7 @@ def test_publish_and_get_treasure_map(blockchain_porter_web_controller, random_bob_encrypting_key = PublicKey.from_bytes( bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac")) random_hrac = "93a9482bdf3b4f2e9df906a35144ca84" - assert len(bytes.fromhex(random_hrac)) == HRAC_LENGTH + assert len(bytes.fromhex(random_hrac)) == TreasureMap.HRAC_LENGTH get_treasure_map_params = { 'hrac': random_hrac, 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() diff --git a/tests/acceptance/porter/test_decentralized_porter.py b/tests/acceptance/porter/test_decentralized_porter.py index 3bd55d0a8..9c286db5b 100644 --- a/tests/acceptance/porter/test_decentralized_porter.py +++ b/tests/acceptance/porter/test_decentralized_porter.py @@ -18,7 +18,6 @@ import pytest from nucypher.crypto.umbral_adapter import PublicKey -from nucypher.crypto.constants import HRAC_LENGTH from nucypher.crypto.powers import DecryptingPower from nucypher.policy.maps import TreasureMap from tests.utils.middleware import MockRestMiddleware @@ -82,7 +81,7 @@ def test_publish_and_get_treasure_map(blockchain_porter, random_bob_encrypting_key = PublicKey.from_bytes( bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac")) random_hrac = bytes.fromhex("93a9482bdf3b4f2e9df906a35144ca84") - assert len(random_hrac) == HRAC_LENGTH + assert len(random_hrac) == TreasureMap.HRAC_LENGTH blockchain_porter.get_treasure_map(hrac=random_hrac, bob_encrypting_key=random_bob_encrypting_key) @@ -100,7 +99,7 @@ def test_publish_and_get_treasure_map(blockchain_porter, enacted_policy.label) retrieved_treasure_map = blockchain_porter.get_treasure_map(hrac=hrac, bob_encrypting_key=blockchain_bob_encrypting_key) - assert retrieved_treasure_map == treasure_map + assert retrieved_treasure_map.hrac == treasure_map.hrac def test_exec_work_order(blockchain_porter, diff --git a/tests/acceptance/porter/test_publish_treasure_map_schema_non_federated_context.py b/tests/acceptance/porter/test_publish_treasure_map_schema_non_federated_context.py index 5b9a849eb..aa902d18a 100644 --- a/tests/acceptance/porter/test_publish_treasure_map_schema_non_federated_context.py +++ b/tests/acceptance/porter/test_publish_treasure_map_schema_non_federated_context.py @@ -19,29 +19,18 @@ from base64 import b64encode import pytest -from nucypher.characters.control.specifications.fields import EncryptedTreasureMap from nucypher.control.specifications.exceptions import InvalidInputData from nucypher.crypto.powers import DecryptingPower from nucypher.utilities.porter.control.specifications.porter_schema import AlicePublishTreasureMap -def test_alice_publish_treasure_map_schema_blockchain_context_default(enacted_blockchain_policy, blockchain_bob): +def test_alice_publish_treasure_map_schema_blockchain_context(enacted_blockchain_policy, blockchain_bob): alice_publish_treasure_map_schema = AlicePublishTreasureMap() # default is decentralized run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema=alice_publish_treasure_map_schema, enacted_blockchain_policy=enacted_blockchain_policy, blockchain_bob=blockchain_bob) -def test_alice_publish_treasure_map_schema_blockchain_context_set_false(enacted_blockchain_policy, blockchain_bob): - # since non-federated, schema's context doesn't have to be set, but set it anyway to ensure that setting to - # False still works as expected. - alice_publish_treasure_map_schema = AlicePublishTreasureMap() # default is decentralized - alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = False - run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema=alice_publish_treasure_map_schema, - enacted_blockchain_policy=enacted_blockchain_policy, - blockchain_bob=blockchain_bob) - - def run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema, enacted_blockchain_policy, blockchain_bob): # no args with pytest.raises(InvalidInputData): @@ -84,9 +73,3 @@ def run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema, enac response_data = {'published': True} output = alice_publish_treasure_map_schema.dump(obj=response_data) assert output == response_data - - # setting federated context to True - alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = True - with pytest.raises(InvalidInputData): - # failed because federated treasure map expected, but instead non-federated (blockchain) treasure map provided - alice_publish_treasure_map_schema.load(required_data) diff --git a/tests/fixtures.py b/tests/fixtures.py index 37f394de5..1137be17a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -244,6 +244,15 @@ def enacted_federated_policy(idle_federated_policy, federated_ursulas): return enacted_policy +@pytest.fixture(scope="module") +def federated_treasure_map(enacted_federated_policy, federated_bob): + """ + The unencrypted treasure map corresponding to the one in `enacted_federated_policy` + """ + yield federated_bob._try_orient(enacted_federated_policy.treasure_map, + enacted_federated_policy.publisher_verifying_key) + + @pytest.fixture(scope="module") def idle_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, token_economics): """ @@ -281,6 +290,15 @@ def enacted_blockchain_policy(idle_blockchain_policy, blockchain_ursulas): return enacted_policy +@pytest.fixture(scope="module") +def blockchain_treasure_map(enacted_blockchain_policy, blockchain_bob): + """ + The unencrypted treasure map corresponding to the one in `enacted_blockchain_policy` + """ + yield blockchain_bob._try_orient(enacted_blockchain_policy.treasure_map, + enacted_blockchain_policy.publisher_verifying_key) + + @pytest.fixture(scope="function") def random_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, token_economics): random_label = generate_random_label() diff --git a/tests/integration/characters/control/test_rpc_control_federated.py b/tests/integration/characters/control/test_rpc_control_federated.py index fae6f3e52..366d466fe 100644 --- a/tests/integration/characters/control/test_rpc_control_federated.py +++ b/tests/integration/characters/control/test_rpc_control_federated.py @@ -72,10 +72,15 @@ def test_alice_rpc_character_control_grant(alice_rpc_test_client, grant_control_ assert 'jsonrpc' in response.data -def test_bob_rpc_character_control_join_policy(bob_rpc_controller, join_control_request, enacted_federated_policy, federated_bob, federated_ursulas): +def test_bob_rpc_character_control_join_policy(bob_rpc_controller, + join_control_request, + federated_treasure_map, + enacted_federated_policy, + federated_bob, + federated_ursulas): for ursula in federated_ursulas: - if ursula.checksum_address in enacted_federated_policy.treasure_map.destinations: + if ursula.checksum_address in federated_treasure_map.destinations: # Simulate passing in a teacher-uri federated_bob.remember_node(ursula) break diff --git a/tests/integration/characters/control/test_web_control_federated.py b/tests/integration/characters/control/test_web_control_federated.py index 1f2b1f261..3022b4b85 100644 --- a/tests/integration/characters/control/test_web_control_federated.py +++ b/tests/integration/characters/control/test_web_control_federated.py @@ -27,7 +27,7 @@ from click.testing import CliRunner import nucypher from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.powers import DecryptingPower -from nucypher.policy.maps import TreasureMap +from nucypher.policy.maps import EncryptedTreasureMap click_runner = CliRunner() @@ -89,8 +89,8 @@ def test_alice_web_character_control_grant(alice_web_controller_test_client, gra assert 'alice_verifying_key' in response_data['result'] map_bytes = b64decode(response_data['result']['treasure_map']) - encrypted_map = TreasureMap.from_bytes(map_bytes) - assert encrypted_map._hrac is not None + encrypted_map = EncryptedTreasureMap.from_bytes(map_bytes) + assert encrypted_map.hrac is not None # Send bad data to assert error returns response = alice_web_controller_test_client.put(endpoint, data=json.dumps({'bad': 'input'})) @@ -169,14 +169,18 @@ def test_alice_character_control_decrypt(alice_web_controller_test_client, assert response.status_code == 405 -def test_bob_character_control_join_policy(bob_web_controller_test_client, federated_bob, federated_ursulas, enacted_federated_policy): +def test_bob_character_control_join_policy(bob_web_controller_test_client, + federated_bob, + federated_ursulas, + federated_treasure_map, + enacted_federated_policy): request_data = { 'label': enacted_federated_policy.label.decode(), 'publisher_verifying_key': bytes(enacted_federated_policy.publisher_verifying_key).hex(), } for ursula in federated_ursulas: - if ursula.checksum_address in enacted_federated_policy.treasure_map.destinations: + if ursula.checksum_address in federated_treasure_map.destinations: # Simulate passing in a teacher-uri federated_bob.remember_node(ursula) break diff --git a/tests/integration/characters/test_bob_handles_frags.py b/tests/integration/characters/test_bob_handles_frags.py index 28b2e8c9a..9664e3cc4 100644 --- a/tests/integration/characters/test_bob_handles_frags.py +++ b/tests/integration/characters/test_bob_handles_frags.py @@ -30,20 +30,19 @@ from nucypher.policy.maps import AuthorizedKeyFrag from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware -def test_bob_cannot_follow_the_treasure_map_in_isolation(enacted_federated_policy, federated_bob): +def test_bob_cannot_follow_the_treasure_map_in_isolation(federated_treasure_map, federated_bob): # Assume for the moment that Bob has already received a TreasureMap, perhaps via a side channel. - hrac, treasure_map = enacted_federated_policy.hrac, enacted_federated_policy.treasure_map # Bob knows of no Ursulas. assert len(federated_bob.known_nodes) == 0 # He can't successfully follow the TreasureMap until he learns of a node to ask. - unknown, known = federated_bob.peek_at_treasure_map(treasure_map=treasure_map) + unknown, known = federated_bob.peek_at_treasure_map(treasure_map=federated_treasure_map) assert len(known) == 0 # TODO: Show that even with learning loop going, nothing happens here. # Probably use Clock? - federated_bob.follow_treasure_map(treasure_map=treasure_map) + federated_bob.follow_treasure_map(treasure_map=federated_treasure_map) assert len(known) == 0 @@ -67,11 +66,11 @@ def test_bob_already_knows_all_nodes_in_treasure_map(enacted_federated_policy, assert len(unknown) == 0 # ...because he already knew of all the Ursulas on the map. - assert len(known) == len(enacted_federated_policy.treasure_map) + assert len(known) == enacted_federated_policy.n @pytest_twisted.inlineCallbacks -def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_federated_policy, +def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(federated_treasure_map, federated_ursulas, certificates_tempdir): """ @@ -90,7 +89,6 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f federated_only=True) # Again, let's assume that he received the TreasureMap via a side channel. - hrac, treasure_map = enacted_federated_policy.hrac, enacted_federated_policy.treasure_map # Now, let's create a scenario in which Bob knows of only one node. assert len(bob.known_nodes) == 0 @@ -99,13 +97,13 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f assert len(bob.known_nodes) == 1 # This time, when he follows the TreasureMap... - unknown_nodes, known_nodes = bob.peek_at_treasure_map(treasure_map=treasure_map) + unknown_nodes, known_nodes = bob.peek_at_treasure_map(treasure_map=federated_treasure_map) # Bob already knew about one node; the rest are unknown. - assert len(unknown_nodes) == len(treasure_map) - 1 + assert len(unknown_nodes) == len(federated_treasure_map) - 1 # He needs to actually follow the treasure map to get the rest. - bob.follow_treasure_map(treasure_map=treasure_map) + bob.follow_treasure_map(treasure_map=federated_treasure_map) # The nodes in the learning loop are now his top target, but he's not learning yet. assert not bob._learning_task.running @@ -121,11 +119,12 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f yield d # ...and he now has no more unknown_nodes. - assert len(bob.known_nodes) == len(treasure_map) + assert len(bob.known_nodes) == len(federated_treasure_map) bob.disenchant() def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_policy, + federated_treasure_map, federated_bob, federated_alice, federated_ursulas, @@ -139,10 +138,9 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic """ # We pick up our story with Bob already having followed the treasure map above, ie: - hrac, treasure_map = enacted_federated_policy.hrac, enacted_federated_policy.treasure_map federated_bob.start_learning_loop() - federated_bob.follow_treasure_map(treasure_map=treasure_map, block=True, timeout=1) + federated_bob.follow_treasure_map(treasure_map=federated_treasure_map, block=True, timeout=1) assert len(federated_bob.known_nodes) == len(federated_ursulas) @@ -158,7 +156,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic work_orders, _ = federated_bob.work_orders_for_capsules( message_kit.capsule, label=enacted_federated_policy.label, - treasure_map=treasure_map, + treasure_map=federated_treasure_map, alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(), num_ursulas=1) @@ -172,7 +170,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic retained_work_orders, _ = federated_bob.work_orders_for_capsules( message_kit.capsule, label=enacted_federated_policy.label, - treasure_map=treasure_map, + treasure_map=federated_treasure_map, alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(), num_ursulas=1) @@ -206,7 +204,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic raise RuntimeError("We've lost track of the Ursula that has the WorkOrder. Can't really proceed.") # Ursula decrypts the encrypted KFrag - encrypted_kfrag = enacted_federated_policy.treasure_map.destinations[ursula.checksum_address] + encrypted_kfrag = federated_treasure_map.destinations[ursula.checksum_address] alice = Alice.from_public_keys(verifying_key=federated_alice.stamp.as_umbral_pubkey()) plaintext_kfrag_payload = ursula.verify_from(stranger=alice, message_kit=encrypted_kfrag, @@ -227,6 +225,7 @@ def test_bob_can_issue_a_work_order_to_a_specific_ursula(enacted_federated_polic def test_bob_can_use_cfrag_attached_to_completed_workorder(enacted_federated_policy, + federated_treasure_map, federated_alice, federated_bob, federated_ursulas, @@ -243,7 +242,7 @@ def test_bob_can_use_cfrag_attached_to_completed_workorder(enacted_federated_pol incomplete_work_orders, complete_work_orders = federated_bob.work_orders_for_capsules( last_capsule_on_side_channel, label=enacted_federated_policy.label, - treasure_map=enacted_federated_policy.treasure_map, + treasure_map=federated_treasure_map, alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(), num_ursulas=1, ) @@ -262,9 +261,12 @@ def test_bob_can_use_cfrag_attached_to_completed_workorder(enacted_federated_pol federated_bob._reencrypt(new_work_order, message_kit_dict) -def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_federated_policy, federated_alice, +def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_federated_policy, + federated_treasure_map, + federated_alice, federated_bob, - federated_ursulas, capsule_side_channel): + federated_ursulas, + capsule_side_channel): # In our last episode, Bob made a single WorkOrder... work_orders = list(federated_bob._completed_work_orders.by_ursula.values()) assert len(work_orders) == 1 @@ -293,7 +295,7 @@ def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_feder incomplete_work_orders, complete_work_orders = federated_bob.work_orders_for_capsules( last_capsule_on_side_channel, label=enacted_federated_policy.label, - treasure_map=enacted_federated_policy.treasure_map, + treasure_map=federated_treasure_map, alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(), num_ursulas=1) id_of_this_new_ursula, new_work_order = list(incomplete_work_orders.items())[0] @@ -321,7 +323,7 @@ def test_bob_remembers_that_he_has_cfrags_for_a_particular_capsule(enacted_feder last_message_kit_on_side_channel.attach_cfrag(new_cfrag) -def test_bob_gathers_and_combines(enacted_federated_policy, federated_bob, federated_alice, capsule_side_channel): +def test_bob_gathers_and_combines(enacted_federated_policy, federated_treasure_map, federated_bob, federated_alice, capsule_side_channel): # The side channel delivers all that Bob needs at this point: # - A single MessageKit, containing a Capsule # - A representation of the data source @@ -331,13 +333,13 @@ def test_bob_gathers_and_combines(enacted_federated_policy, federated_bob, feder assert len(federated_bob._completed_work_orders) == 2 # ...but the policy requires us to collect more cfrags. - assert len(federated_bob._completed_work_orders) < enacted_federated_policy.treasure_map.m + assert len(federated_bob._completed_work_orders) < federated_treasure_map.m # Bob can't decrypt yet with just two CFrags. He needs to gather at least m. with pytest.raises(DecryptingKeypair.DecryptionFailed): federated_bob.decrypt(the_message_kit) - number_left_to_collect = enacted_federated_policy.treasure_map.m - len(federated_bob._completed_work_orders) + number_left_to_collect = federated_treasure_map.m - len(federated_bob._completed_work_orders) the_message_kit.set_correctness_keys( delegating=the_data_source.policy_pubkey, @@ -347,7 +349,7 @@ def test_bob_gathers_and_combines(enacted_federated_policy, federated_bob, feder new_incomplete_work_orders, _ = federated_bob.work_orders_for_capsules( the_message_kit.capsule, label=enacted_federated_policy.label, - treasure_map=enacted_federated_policy.treasure_map, + treasure_map=federated_treasure_map, alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(), num_ursulas=number_left_to_collect) _id_of_yet_another_ursula, new_work_order = list(new_incomplete_work_orders.items())[0] diff --git a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py index ceffab412..182c05172 100644 --- a/tests/integration/characters/test_bob_joins_policy_and_retrieves.py +++ b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py @@ -35,10 +35,10 @@ def test_federated_bob_full_retrieve_flow(federated_ursulas, federated_bob, federated_alice, capsule_side_channel, + federated_treasure_map, enacted_federated_policy): # Assume for the moment that Bob has already received a TreasureMap. - treasure_map = enacted_federated_policy.treasure_map - federated_bob.treasure_maps[treasure_map._hrac] = treasure_map + federated_bob.treasure_maps[federated_treasure_map.hrac] = federated_treasure_map for ursula in federated_ursulas: federated_bob.remember_node(ursula) @@ -98,12 +98,8 @@ def test_bob_joins_policy_and_retrieves(federated_alice, bob.join_policy(label=label, publisher_verifying_key=federated_alice.stamp.as_umbral_pubkey(), block=True) - except policy.treasure_map.NowhereToBeFound: - maps = [] - for ursula in federated_ursulas: - for map in ursula.treasure_maps.values(): - maps.append(map) - if policy.treasure_map in maps: + except TreasureMap.NowhereToBeFound: + if policy.treasure_map.hrac in ursula.treasure_maps: # This is a nice place to put a breakpoint to examine Bob's failure to join a policy. bob.join_policy(label=label, publisher_verifying_key=federated_alice.stamp.as_umbral_pubkey(), @@ -173,29 +169,6 @@ def test_bob_joins_policy_and_retrieves(federated_alice, bob.disenchant() -def test_treasure_map_serialization(enacted_federated_policy, federated_alice, federated_bob): - treasure_map = enacted_federated_policy.treasure_map - assert treasure_map.m is not None - assert treasure_map.m != NO_DECRYPTION_PERFORMED - assert treasure_map.m == MOCK_POLICY_DEFAULT_M, 'm value is not correct' - - serialized_map = bytes(treasure_map) - deserialized_map = TreasureMap.from_bytes(serialized_map) - assert deserialized_map._hrac == treasure_map._hrac - - # TreasureMap is currently encrypted - with pytest.raises(TypeError): - deserialized_map.m - - with pytest.raises(TypeError): - deserialized_map.destinations - - compass = federated_bob.make_compass_for_alice(federated_alice) - deserialized_map.orient(compass) - assert deserialized_map.m == treasure_map.m - assert deserialized_map.destinations == treasure_map.destinations - - def test_bob_retrieves_with_treasure_map( federated_bob, federated_ursulas, enacted_federated_policy, capsule_side_channel): diff --git a/tests/integration/characters/test_federated_grant_and_revoke.py b/tests/integration/characters/test_federated_grant_and_revoke.py index 2ef62fe39..fe5b0b8bd 100644 --- a/tests/integration/characters/test_federated_grant_and_revoke.py +++ b/tests/integration/characters/test_federated_grant_and_revoke.py @@ -21,6 +21,7 @@ import datetime import maya import pytest +from nucypher.crypto.kits import PolicyMessageKit from nucypher.characters.lawful import Enrico from nucypher.crypto.utils import keccak_digest from nucypher.policy.orders import Revocation @@ -43,13 +44,19 @@ def test_federated_grant(federated_alice, federated_bob, federated_ursulas): assert policy_id in federated_alice.active_policies assert federated_alice.active_policies[policy_id] == policy + treasure_map = federated_bob._try_orient(policy.treasure_map, policy.publisher_verifying_key) + # The number of actually enacted arrangements is exactly equal to n. - assert len(policy.treasure_map.destinations) == n + assert len(treasure_map.destinations) == n # Let's look at the enacted arrangements. for ursula in federated_ursulas: - if ursula.checksum_address in policy.treasure_map.destinations: - assert ursula.checksum_address in policy.treasure_map.destinations + if ursula.checksum_address in treasure_map.destinations: + kfrag_kit = treasure_map.destinations[ursula.checksum_address] + + # TODO: try to decrypt? + # TODO: Use a new type for EncryptedKFrags? + assert isinstance(kfrag_kit, PolicyMessageKit) def test_federated_alice_can_decrypt(federated_alice, federated_bob): diff --git a/tests/integration/characters/test_specifications.py b/tests/integration/characters/test_specifications.py index d322ce1e3..1d8b3cb0b 100644 --- a/tests/integration/characters/test_specifications.py +++ b/tests/integration/characters/test_specifications.py @@ -73,7 +73,7 @@ def test_treasuremap_validation(enacted_federated_policy): """Tell people exactly what's wrong with their treasuremaps""" class TreasureMapsOnly(BaseSchema): - tmap = EncryptedTreasureMap(federated_only=True) + tmap = EncryptedTreasureMap() # this will raise a base64 error with pytest.raises(SpecificationError) as e: @@ -87,8 +87,8 @@ def test_treasuremap_validation(enacted_federated_policy): with pytest.raises(InvalidInputData) as e: TreasureMapsOnly().load({'tmap': "VGhpcyBpcyB0b3RhbGx5IG5vdCBhIHRyZWFzdXJlbWFwLg=="}) - assert "Could not parse tmap" in str(e) - assert "Can't split a message with more bytes than the original splittable" in str(e) + assert "Could not convert input for tmap" in str(e) + assert "Invalid treasure map contents" in str(e) # a valid treasuremap for once... tmap_bytes = bytes(enacted_federated_policy.treasure_map) diff --git a/tests/integration/network/test_failure_modes.py b/tests/integration/network/test_failure_modes.py index b2bfccdce..960eed602 100644 --- a/tests/integration/network/test_failure_modes.py +++ b/tests/integration/network/test_failure_modes.py @@ -87,7 +87,7 @@ def test_alice_can_grant_even_when_the_first_nodes_she_tries_are_down(federated_ # policy = alice_grant_action() # The number of actually enacted arrangements is exactly equal to n. - assert len(policy.treasure_map.destinations) == n + assert policy.n == n def test_node_has_changed_cert(federated_alice, federated_ursulas): diff --git a/tests/integration/network/test_treasure_map_integration.py b/tests/integration/network/test_treasure_map_integration.py index 799f71ed1..d65131412 100644 --- a/tests/integration/network/test_treasure_map_integration.py +++ b/tests/integration/network/test_treasure_map_integration.py @@ -21,7 +21,7 @@ import pytest from nucypher.characters.lawful import Ursula from nucypher.crypto.utils import keccak_digest from nucypher.datastore.models import EncryptedTreasureMap as DatastoreTreasureMap -from nucypher.policy.maps import TreasureMap as FederatedTreasureMap +from nucypher.policy.maps import EncryptedTreasureMap def test_alice_creates_policy_with_correct_hrac(federated_alice, federated_bob, idle_federated_policy): @@ -37,11 +37,11 @@ def test_alice_sets_treasure_map(federated_alice, federated_bob, enacted_federat """ Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO """ - hrac = enacted_federated_policy.treasure_map._hrac + hrac = enacted_federated_policy.treasure_map.hrac found = 0 for node in federated_bob.matching_nodes_among(federated_alice.known_nodes): with node.datastore.describe(DatastoreTreasureMap, hrac) as treasure_map_on_node: - assert FederatedTreasureMap.from_bytes(treasure_map_on_node.treasure_map) == enacted_federated_policy.treasure_map + assert EncryptedTreasureMap.from_bytes(treasure_map_on_node.treasure_map).hrac == enacted_federated_policy.hrac found += 1 assert found @@ -52,10 +52,10 @@ def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alic The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it. """ - hrac = enacted_federated_policy.treasure_map._hrac + hrac = enacted_federated_policy.treasure_map.hrac an_ursula = federated_bob.matching_nodes_among(federated_ursulas)[0] with an_ursula.datastore.describe(DatastoreTreasureMap, hrac) as treasure_map_record: - treasure_map_on_network = FederatedTreasureMap.from_bytes(treasure_map_record.treasure_map) + treasure_map_on_network = EncryptedTreasureMap.from_bytes(treasure_map_record.treasure_map) hrac_by_bob = federated_bob.construct_policy_hrac(federated_alice.stamp, enacted_federated_policy.label) assert enacted_federated_policy.hrac == hrac_by_bob @@ -90,14 +90,14 @@ def test_bob_can_retrieve_the_treasure_map_and_decrypt_it(federated_alice, feder treasure_map_from_wire = bob.get_treasure_map(federated_alice.stamp.as_umbral_pubkey(), enacted_federated_policy.label) - assert enacted_federated_policy.treasure_map == treasure_map_from_wire + assert enacted_federated_policy.hrac == treasure_map_from_wire.hrac -def test_treasure_map_is_legit(federated_bob, enacted_federated_policy): +def test_treasure_map_is_legit(federated_bob, federated_treasure_map, enacted_federated_policy): """ Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network. """ - for ursula_address, _node_id in enacted_federated_policy.treasure_map: + for ursula_address, _node_id in federated_treasure_map: if ursula_address not in federated_bob.known_nodes.addresses(): pytest.fail(f"Bob didn't know about {ursula_address}") diff --git a/tests/integration/porter/conftest.py b/tests/integration/porter/conftest.py index 9e2cf2c96..816046ed9 100644 --- a/tests/integration/porter/conftest.py +++ b/tests/integration/porter/conftest.py @@ -36,4 +36,7 @@ def random_federated_treasure_map_data(federated_alice, federated_bob, federated verified_kfrags=kfrags, m=threshold) - yield federated_bob.public_keys(DecryptingPower), random_treasure_map + enc_treasure_map = random_treasure_map.prepare_for_publication(publisher=federated_alice, + bob=federated_bob) + + yield federated_bob.public_keys(DecryptingPower), enc_treasure_map diff --git a/tests/integration/porter/control/test_porter_rpc_control_federated.py b/tests/integration/porter/control/test_porter_rpc_control_federated.py index c51e69648..cbee787dc 100644 --- a/tests/integration/porter/control/test_porter_rpc_control_federated.py +++ b/tests/integration/porter/control/test_porter_rpc_control_federated.py @@ -89,7 +89,7 @@ def test_publish_and_get_treasure_map(federated_porter_rpc_controller, # ensure that random treasure map cannot be obtained since not available with pytest.raises(TreasureMap.NowhereToBeFound): get_treasure_map_params = { - 'hrac': random_treasure_map._hrac.hex(), + 'hrac': random_treasure_map.hrac.hex(), 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() } request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params} @@ -106,7 +106,7 @@ def test_publish_and_get_treasure_map(federated_porter_rpc_controller, # try getting the random treasure map now get_treasure_map_params = { - 'hrac': random_treasure_map._hrac.hex(), + 'hrac': random_treasure_map.hrac.hex(), 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() } request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params} diff --git a/tests/integration/porter/control/test_porter_web_control_federated.py b/tests/integration/porter/control/test_porter_web_control_federated.py index 21416f01e..b40d202a7 100644 --- a/tests/integration/porter/control/test_porter_web_control_federated.py +++ b/tests/integration/porter/control/test_porter_web_control_federated.py @@ -103,7 +103,7 @@ def test_publish_and_get_treasure_map(federated_porter_web_controller, # ensure that random treasure map cannot be obtained since not available with pytest.raises(TreasureMap.NowhereToBeFound): get_treasure_map_params = { - 'hrac': random_treasure_map._hrac.hex(), + 'hrac': random_treasure_map.hrac.hex(), 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() } federated_porter_web_controller.get('/get_treasure_map', data=json.dumps(get_treasure_map_params)) @@ -121,7 +121,7 @@ def test_publish_and_get_treasure_map(federated_porter_web_controller, # try getting the random treasure map now get_treasure_map_params = { - 'hrac': random_treasure_map._hrac.hex(), + 'hrac': random_treasure_map.hrac.hex(), 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() } response = federated_porter_web_controller.get('/get_treasure_map', @@ -206,7 +206,7 @@ def test_endpoints_basic_auth(federated_porter_basic_auth_web_controller, # /get_treasure_map get_treasure_map_params = { - 'hrac': random_treasure_map._hrac.hex(), + 'hrac': random_treasure_map.hrac.hex(), 'bob_encrypting_key': bytes(random_bob_encrypting_key).hex() } response = federated_porter_basic_auth_web_controller.get('/get_treasure_map', diff --git a/tests/integration/porter/test_federated_porter.py b/tests/integration/porter/test_federated_porter.py index 0859d3bf5..caf50b4da 100644 --- a/tests/integration/porter/test_federated_porter.py +++ b/tests/integration/porter/test_federated_porter.py @@ -77,7 +77,7 @@ def test_publish_and_get_treasure_map(federated_porter, # ensure that random treasure map cannot be obtained since not available with pytest.raises(TreasureMap.NowhereToBeFound): - federated_porter.get_treasure_map(hrac=random_treasure_map._hrac, + federated_porter.get_treasure_map(hrac=random_treasure_map.hrac, bob_encrypting_key=random_bob_encrypting_key) # publish the random treasure map @@ -85,16 +85,16 @@ def test_publish_and_get_treasure_map(federated_porter, bob_encrypting_key=random_bob_encrypting_key) # try getting the random treasure map now - treasure_map = federated_porter.get_treasure_map(hrac=random_treasure_map._hrac, + treasure_map = federated_porter.get_treasure_map(hrac=random_treasure_map.hrac, bob_encrypting_key=random_bob_encrypting_key) - assert treasure_map._hrac == random_treasure_map._hrac + assert treasure_map.hrac == random_treasure_map.hrac # try getting an already existing policy hrac = federated_bob.construct_policy_hrac(federated_alice.stamp.as_umbral_pubkey(), enacted_federated_policy.label) treasure_map = federated_porter.get_treasure_map(hrac=hrac, bob_encrypting_key=federated_bob.public_keys(DecryptingPower)) - assert treasure_map == enacted_federated_policy.treasure_map + assert treasure_map.hrac == enacted_federated_policy.hrac def test_exec_work_order(federated_porter, diff --git a/tests/integration/porter/test_porter_specifications.py b/tests/integration/porter/test_porter_specifications.py index 084b1218f..44614ff80 100644 --- a/tests/integration/porter/test_porter_specifications.py +++ b/tests/integration/porter/test_porter_specifications.py @@ -19,7 +19,6 @@ from base64 import b64encode import pytest -from nucypher.characters.control.specifications.fields import EncryptedTreasureMap from nucypher.control.specifications.exceptions import InvalidArgumentCombo, InvalidInputData from nucypher.crypto.powers import DecryptingPower from nucypher.crypto.umbral_adapter import SecretKey @@ -169,7 +168,6 @@ def test_alice_publish_treasure_map_schema_federated_context(enacted_federated_p # since federated, schema's context must be set - so create one schema # and reuse (it doesn't hold state other than the context) alice_publish_treasure_map_schema = AlicePublishTreasureMap() - alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = True # no args with pytest.raises(InvalidInputData): @@ -213,12 +211,6 @@ def test_alice_publish_treasure_map_schema_federated_context(enacted_federated_p output = alice_publish_treasure_map_schema.dump(obj=response_data) assert output == response_data - # setting federated context to False fails - alice_publish_treasure_map_schema.context[EncryptedTreasureMap.IS_FEDERATED_CONTEXT_KEY] = False - with pytest.raises(InvalidInputData): - # failed because non-federated (blockchain) treasure map expected, but instead federated treasure map provided - alice_publish_treasure_map_schema.load(required_data) - def test_alice_revoke(): pass # TODO diff --git a/tests/unit/characters/control/test_character_fields.py b/tests/unit/characters/control/test_character_fields.py index bc658b07b..17b4bf8fb 100644 --- a/tests/unit/characters/control/test_character_fields.py +++ b/tests/unit/characters/control/test_character_fields.py @@ -164,7 +164,7 @@ def test_umbral_signature(): def test_treasure_map(enacted_federated_policy): treasure_map = enacted_federated_policy.treasure_map - field = EncryptedTreasureMap(federated_only=True) + field = EncryptedTreasureMap() serialized = field._serialize(value=treasure_map, attr=None, obj=None) assert serialized == b64encode(bytes(treasure_map)).decode() diff --git a/tests/unit/test_porter.py b/tests/unit/test_porter.py index af0b6424f..138393adb 100644 --- a/tests/unit/test_porter.py +++ b/tests/unit/test_porter.py @@ -30,7 +30,7 @@ from tests.utils.policy import work_order_setup def test_hrac_field(enacted_federated_policy): - hrac = enacted_federated_policy.treasure_map._hrac + hrac = enacted_federated_policy.treasure_map.hrac field = HRAC() serialized = field._serialize(value=hrac, attr=None, obj=None) diff --git a/tests/unit/test_treasure_maps.py b/tests/unit/test_treasure_maps.py index 9f7d2e053..4e61dbce4 100644 --- a/tests/unit/test_treasure_maps.py +++ b/tests/unit/test_treasure_maps.py @@ -20,50 +20,55 @@ import os import pytest -from nucypher.crypto.powers import DecryptingPower, SigningPower from nucypher.crypto.umbral_adapter import KeyFrag -from nucypher.policy.maps import TreasureMap, AuthorizedKeyFrag +from nucypher.policy.maps import TreasureMap, EncryptedTreasureMap, AuthorizedKeyFrag def test_complete_treasure_map_journey(federated_alice, federated_bob, federated_ursulas, idle_federated_policy, mocker): - treasure_map = TreasureMap(m=1) + label = "chili con carne 🔥".encode('utf-8') + kfrags = idle_federated_policy.kfrags + ursulas = list(federated_ursulas)[:len(kfrags)] - bob_encrypting_key = federated_bob.public_keys(DecryptingPower) - bob_verifying_key = federated_bob.public_keys(SigningPower) + treasure_map = TreasureMap.construct_by_publisher(publisher=federated_alice, + bob=federated_bob, + label=label, + ursulas=ursulas, + verified_kfrags=kfrags, + m=1) - kfrag = idle_federated_policy.kfrags[0] - make_kfrag_payload_spy = mocker.spy(AuthorizedKeyFrag, '__bytes__') - - treasure_map.derive_hrac(publisher_stamp=federated_alice.stamp, - bob_verifying_key=bob_verifying_key, - label="chili con carne 🔥".encode('utf-8')) - - encrypted_kfrags = dict() - for ursula in federated_ursulas: - treasure_map.add_kfrag(ursula, kfrag, federated_alice.stamp) - encrypted_kfrags[ursula.checksum_address] = make_kfrag_payload_spy.spy_return - - treasure_map.prepare_for_publication(bob_encrypting_key=bob_encrypting_key, - publisher_stamp=federated_alice.stamp) - - ursula_rolodex = {u.checksum_address: u for u in federated_ursulas} + ursula_rolodex = {u.checksum_address: u for u in ursulas} for ursula_address, encrypted_kfrag in treasure_map.destinations.items(): assert ursula_address in ursula_rolodex ursula = ursula_rolodex[ursula_address] - kfrag_payload = encrypted_kfrags[ursula.checksum_address] - assert kfrag_payload == ursula.verify_from(federated_alice, encrypted_kfrag, decrypt=True) # FIXME: 2203 + auth_kfrag_bytes = ursula.verify_from(federated_alice, encrypted_kfrag, decrypt=True) # FIXME: 2203 + auth_kfrag = AuthorizedKeyFrag.from_bytes(auth_kfrag_bytes) + ursula.verify_kfrag_authorization(hrac=treasure_map.hrac, + author=federated_alice, + publisher=federated_alice, + authorized_kfrag=auth_kfrag) serialized_map = bytes(treasure_map) # ... deserialized_map = TreasureMap.from_bytes(serialized_map) - compass = federated_bob.make_compass_for_alice(federated_alice) - deserialized_map.orient(compass) + assert treasure_map.destinations == deserialized_map.destinations + assert treasure_map.hrac == deserialized_map.hrac - assert treasure_map.m == deserialized_map.m == 1 - assert set(treasure_map.destinations) == set(deserialized_map.destinations) - assert treasure_map == deserialized_map + + enc_treasure_map = treasure_map.prepare_for_publication(publisher=federated_alice, + bob=federated_bob) + + enc_serialized_map = bytes(enc_treasure_map) + # ... + enc_deserialized_map = EncryptedTreasureMap.from_bytes(enc_serialized_map) + + compass = federated_bob.make_compass_for_alice(federated_alice) + decrypted_map = enc_deserialized_map.orient(compass) + + assert treasure_map.m == decrypted_map.m == 1 + assert treasure_map.destinations == decrypted_map.destinations + assert treasure_map.hrac == decrypted_map.hrac @pytest.mark.skip(reason='Backwards-incompatible with umbral 0.2+') diff --git a/tests/utils/policy.py b/tests/utils/policy.py index a7eeb98e4..f7e6f9abd 100644 --- a/tests/utils/policy.py +++ b/tests/utils/policy.py @@ -42,8 +42,9 @@ def work_order_setup(enacted_policy, bob, alice): + treasure_map = bob._try_orient(enacted_policy.treasure_map, enacted_policy.publisher_verifying_key) + # We pick up our story with Bob already having followed the treasure map above, ie: - treasure_map = enacted_policy.treasure_map bob.start_learning_loop() bob.follow_treasure_map(treasure_map=treasure_map, block=True, timeout=1)