def __init__(self, alice, label, expiration: maya.MayaDT, bob=None, kfrags=(UNKNOWN_KFRAG,), public_key=None, m: int = None, alice_signature=NOT_SIGNED) -> None: """ :param kfrags: A list of KFrags to distribute per this Policy. :param label: The identity of the resource to which Bob is granted access. """ from nucypher.policy.collections import TreasureMap # TODO: Circular Import self.alice = alice # type: Alice self.label = label # type: bytes self.bob = bob # type: Bob self.kfrags = kfrags # type: List[KFrag] self.public_key = public_key self.treasure_map = TreasureMap(m=m) self.expiration = expiration self._accepted_arrangements = set() # type: Set[Arrangement] self._rejected_arrangements = set() # type: Set[Arrangement] self._spare_candidates = set() # type: Set[Ursula] self._enacted_arrangements = OrderedDict() self._published_arrangements = OrderedDict() self.alice_signature = alice_signature # TODO: This is unused / To Be Implemented?
def test_alice_web_character_control_grant(alice_web_controller_test_client, grant_control_request): method_name, params = grant_control_request endpoint = f'/{method_name}' response = alice_web_controller_test_client.put(endpoint, data=json.dumps(params)) assert response.status_code == 200 response_data = json.loads(response.data) assert 'treasure_map' in response_data['result'] assert 'policy_encrypting_key' in response_data['result'] 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 # Send bad data to assert error returns response = alice_web_controller_test_client.put(endpoint, data=json.dumps( {'bad': 'input'})) assert response.status_code == 400 bad_params = params.copy() # Malform the request del (bad_params['bob_encrypting_key']) response = alice_web_controller_test_client.put( endpoint, data=json.dumps(bad_params)) assert response.status_code == 400
def receive_treasure_map(treasure_map_id): from nucypher.policy.collections import TreasureMap try: treasure_map = TreasureMap.from_bytes( bytes_representation=request.data, verify=True) except TreasureMap.InvalidSignature: do_store = False else: # TODO: If we include the policy ID in this check, does that prevent map spam? 1736 do_store = treasure_map.public_id() == treasure_map_id if do_store: log.info("{} storing TreasureMap {}".format( this_node, treasure_map_id)) # TODO 341 - what if we already have this TreasureMap? treasure_map_index = bytes.fromhex(treasure_map_id) this_node.treasure_maps[treasure_map_index] = treasure_map return Response(bytes(treasure_map), status=202) else: # TODO: Make this a proper 500 or whatever. #341 log.info( "Bad TreasureMap ID; not storing {}".format(treasure_map_id)) assert False
def test_alice_sets_treasure_map(enacted_federated_policy): """ Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO """ enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) treasure_map_id = enacted_federated_policy.treasure_map.public_id() found = 0 for node in enacted_federated_policy.bob.matching_nodes_among(enacted_federated_policy.alice.known_nodes): with node.datastore.describe(DatastoreTreasureMap, treasure_map_id) as treasure_map_on_node: assert FederatedTreasureMap.from_bytes(treasure_map_on_node.treasure_map) == enacted_federated_policy.treasure_map found += 1 assert found
def test_bob_rpc_character_control_retrieve_with_tmap( enacted_blockchain_policy, blockchain_bob, blockchain_alice, bob_rpc_controller, retrieve_control_request): tmap_64 = b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode() method_name, params = retrieve_control_request params['treasure_map'] = tmap_64 request_data = {'method': method_name, 'params': params} response = bob_rpc_controller.send(request_data) assert response.data['result']['cleartexts'][ 0] == 'Welcome to flippering number 1.' # Make a wrong (empty) treasure map wrong_tmap = TreasureMap(m=0) wrong_tmap.prepare_for_publication( blockchain_bob.public_keys(DecryptingPower), blockchain_bob.public_keys(SigningPower), blockchain_alice.stamp, b'Wrong!') tmap_64 = b64encode(bytes(wrong_tmap)).decode() request_data['params']['treasure_map'] = tmap_64 with pytest.raises(TreasureMap.IsDisorienting): bob_rpc_controller.send(request_data)
def test_alice_sets_treasure_map(federated_alice, federated_bob, enacted_federated_policy): """ Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO """ treasure_map_id = enacted_federated_policy.treasure_map.public_id() found = 0 for node in federated_bob.matching_nodes_among( federated_alice.known_nodes): with node.datastore.describe(DatastoreTreasureMap, treasure_map_id) as treasure_map_on_node: assert FederatedTreasureMap.from_bytes( treasure_map_on_node.treasure_map ) == enacted_federated_policy.treasure_map found += 1 assert found
def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alice, federated_bob, federated_ursulas, enacted_federated_policy): """ The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it. """ treasure_map_id = enacted_federated_policy.treasure_map.public_id() an_ursula = federated_bob.matching_nodes_among(federated_ursulas)[0] with an_ursula.datastore.describe(DatastoreTreasureMap, treasure_map_id) as treasure_map_record: treasure_map_on_network = FederatedTreasureMap.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 map_id_by_bob = federated_bob.construct_map_id(federated_alice.stamp, enacted_federated_policy.label) assert map_id_by_bob == treasure_map_on_network.public_id()
def test_treasure_map_serialization(enacted_federated_policy, 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( enacted_federated_policy.alice) deserialized_map.orient(compass) assert deserialized_map.m == treasure_map.m assert deserialized_map.destinations == treasure_map.destinations
class Policy(ABC): """ An edict by Alice, arranged with n Ursulas, to perform re-encryption for a specific Bob for a specific path. Once Alice is ready to enact a Policy, she generates KFrags, which become part of the Policy. Each Ursula is offered a Arrangement (see above) for a given Policy by Alice. Once Alice has secured agreement with n Ursulas to enact a Policy, she sends each a KFrag, and generates a TreasureMap for the Policy, recording which Ursulas got a KFrag. """ POLICY_ID_LENGTH = 16 _arrangement_class = NotImplemented log = Logger("Policy") class Rejected(RuntimeError): """Too many Ursulas rejected""" def __init__(self, alice, label, expiration: maya.MayaDT, bob=None, kfrags=(UNKNOWN_KFRAG,), public_key=None, m: int = None, alice_signature=NOT_SIGNED) -> None: """ :param kfrags: A list of KFrags to distribute per this Policy. :param label: The identity of the resource to which Bob is granted access. """ from nucypher.policy.collections import TreasureMap # TODO: Circular Import self.alice = alice # type: Alice self.label = label # type: bytes self.bob = bob # type: Bob self.kfrags = kfrags # type: List[KFrag] self.public_key = public_key self.treasure_map = TreasureMap(m=m) self.expiration = expiration self._accepted_arrangements = set() # type: Set[Arrangement] self._rejected_arrangements = set() # type: Set[Arrangement] self._spare_candidates = set() # type: Set[Ursula] self._enacted_arrangements = OrderedDict() self._published_arrangements = OrderedDict() self.alice_signature = alice_signature # TODO: This is unused / To Be Implemented? class MoreKFragsThanArrangements(TypeError): """ Raised when a Policy has been used to generate Arrangements with Ursulas insufficient number such that we don't have enough KFrags to give to each Ursula. """ @property def n(self) -> int: return len(self.kfrags) @property def id(self) -> bytes: return construct_policy_id(self.label, bytes(self.bob.stamp)) def __repr__(self): return f"{self.__class__.__name__}:{self.id.hex()[:6]}" @property def accepted_ursulas(self) -> Set[Ursula]: return {arrangement.ursula for arrangement in self._accepted_arrangements} def hrac(self) -> bytes: """ # TODO: #180 - This function is hanging on for dear life. After 180 is closed, it can be completely deprecated. The "hashed resource authentication 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. """ return keccak_digest(bytes(self.alice.stamp) + bytes(self.bob.stamp) + self.label) def publish_treasure_map(self, network_middleware: RestMiddleware) -> dict: self.treasure_map.prepare_for_publication(self.bob.public_keys(DecryptingPower), self.bob.public_keys(SigningPower), self.alice.stamp, self.label) if not self.alice.known_nodes: # TODO: Optionally, block. raise RuntimeError("Alice hasn't learned of any nodes. Thus, she can't push the TreasureMap.") responses = dict() self.log.debug(f"Pushing {self.treasure_map} to all known nodes from {self.alice}") for node in self.alice.known_nodes: # TODO: # 342 - It's way overkill to push this to every node we know about. Come up with a system. try: treasure_map_id = self.treasure_map.public_id() # TODO: Certificate filepath needs to be looked up and passed here response = network_middleware.put_treasure_map_on_node(node=node, map_id=treasure_map_id, map_payload=bytes(self.treasure_map)) except NodeSeemsToBeDown: # TODO: Introduce good failure mode here if too few nodes receive the map. self.log.debug(f"Failed pushing {self.treasure_map} to unresponsive {node}") continue if response.status_code == 202: # TODO: #341 - Handle response wherein node already had a copy of this TreasureMap. responses[node] = response self.log.debug(f"{self.treasure_map} successfully pushed to {node}") else: # TODO: Do something useful here. message = f"Failed pushing {self.treasure_map} to {node}, with status {response.status_code}" self.log.debug(message) raise RuntimeError(message) return responses def credential(self, with_treasure_map=True): """ Creates a PolicyCredential for portable access to the policy via Alice or Bob. By default, it will include the treasure_map for the policy unless `with_treasure_map` is False. """ from nucypher.policy.collections import PolicyCredential treasure_map = self.treasure_map if not with_treasure_map: treasure_map = None return PolicyCredential(self.alice.stamp, self.label, self.expiration, self.public_key, treasure_map) def __assign_kfrags(self) -> Generator[Arrangement, None, None]: if len(self._accepted_arrangements) < self.n: raise self.MoreKFragsThanArrangements("Not enough candidate arrangements. " "Call make_arrangements to make more.") for kfrag in self.kfrags: for arrangement in self._accepted_arrangements: if not arrangement in self._enacted_arrangements.values(): arrangement.kfrag = kfrag self._enacted_arrangements[kfrag] = arrangement yield arrangement break # This KFrag is now assigned; break the inner loop and go back to assign other kfrags. else: # We didn't assign that KFrag. Trouble. # This is ideally an impossible situation, because we don't typically # enter this method unless we've already had n or more Arrangements accepted. raise self.MoreKFragsThanArrangements("Not enough accepted arrangements to assign all KFrags.") return def enact(self, network_middleware, publish=True) -> dict: """ Assign kfrags to ursulas_on_network, and distribute them via REST, populating enacted_arrangements """ for arrangement in self.__assign_kfrags(): arrangement_message_kit = arrangement.encrypt_payload_for_ursula() try: response = network_middleware.enact_policy(arrangement.ursula, arrangement.id, arrangement_message_kit.to_bytes()) except network_middleware.UnexpectedResponse as e: arrangement.status = e.status else: arrangement.status = response.status_code # Assuming response is what we hope for. self.treasure_map.add_arrangement(arrangement) else: # OK, let's check: if two or more Ursulas claimed we didn't pay, # we need to re-evaulate our situation here. arrangement_statuses = [a.status for a in self._accepted_arrangements] number_of_claims_of_freeloading = sum(status==402 for status in arrangement_statuses) if number_of_claims_of_freeloading > 2: raise self.alice.NotEnoughNodes # TODO: Clean this up and enable re-tries. self.treasure_map.check_for_sufficient_destinations() # TODO: Leave a note to try any failures later. pass # ...After *all* the arrangements are enacted # Create Alice's revocation kit self.revocation_kit = RevocationKit(self, self.alice.stamp) self.alice.add_active_policy(self) if publish is True: return self.publish_treasure_map(network_middleware=network_middleware) def consider_arrangement(self, network_middleware, ursula, arrangement) -> bool: negotiation_response = network_middleware.consider_arrangement(arrangement=arrangement) # TODO: check out the response: need to assess the result and see if we're actually good to go. arrangement_is_accepted = negotiation_response.status_code == 200 bucket = self._accepted_arrangements if arrangement_is_accepted else self._rejected_arrangements bucket.add(arrangement) return arrangement_is_accepted def make_arrangements(self, network_middleware: RestMiddleware, handpicked_ursulas: Optional[Set[Ursula]] = None, *args, **kwargs, ) -> None: sampled_ursulas = self.sample(handpicked_ursulas=handpicked_ursulas) if len(sampled_ursulas) < self.n: raise self.MoreKFragsThanArrangements( "To make a Policy in federated mode, you need to designate *all* ' \ the Ursulas you need (in this case, {}); there's no other way to ' \ know which nodes to use. Either pass them here or when you make ' \ the Policy.".format(self.n)) # TODO: One of these layers needs to add concurrency. self._consider_arrangements(network_middleware=network_middleware, candidate_ursulas=sampled_ursulas, *args, **kwargs) if len(self._accepted_arrangements) < self.n: raise self.Rejected(f'Selected Ursulas rejected too many arrangements ' f'- only {len(self._accepted_arrangements)} of {self.n} accepted.') @abstractmethod def make_arrangement(self, ursula: Ursula, *args, **kwargs): raise NotImplementedError @abstractmethod def sample_essential(self, quantity: int, handpicked_ursulas: Set[Ursula]) -> Set[Ursula]: raise NotImplementedError def sample(self, handpicked_ursulas: Optional[Set[Ursula]] = None) -> Set[Ursula]: selected_ursulas = set(handpicked_ursulas) if handpicked_ursulas else set() # Calculate the target sample quantity target_sample_quantity = self.n - len(selected_ursulas) if target_sample_quantity > 0: sampled_ursulas = self.sample_essential(quantity=target_sample_quantity, handpicked_ursulas=selected_ursulas) selected_ursulas.update(sampled_ursulas) return selected_ursulas def _consider_arrangements(self, network_middleware: RestMiddleware, candidate_ursulas: Set[Ursula], consider_everyone: bool = False, *args, **kwargs) -> None: for index, selected_ursula in enumerate(candidate_ursulas): arrangement = self.make_arrangement(ursula=selected_ursula, *args, **kwargs) try: is_accepted = self.consider_arrangement(ursula=selected_ursula, arrangement=arrangement, network_middleware=network_middleware) except NodeSeemsToBeDown as e: # TODO: #355 Also catch InvalidNode here? # This arrangement won't be added to the accepted bucket. # If too many nodes are down, it will fail in make_arrangements. # Also TODO: Prolly log this or something at this stage. continue else: # Bucket the arrangements if is_accepted: self.log.debug(f"Arrangement accepted by {selected_ursula}") self._accepted_arrangements.add(arrangement) accepted = len(self._accepted_arrangements) if accepted == self.n and not consider_everyone: try: spares = set(list(candidate_ursulas)[index+1::]) self._spare_candidates.update(spares) except IndexError: self._spare_candidates = set() break else: self.log.debug(f"Arrangement failed with {selected_ursula}") self._rejected_arrangements.add(arrangement)