def test_bob_rpc_character_control_retrieve_with_tmap( enacted_blockchain_policy, blockchain_bob, blockchain_alice, bob_rpc_controller, retrieve_control_request): # So that this test can run even independently. if not blockchain_bob.done_seeding: blockchain_bob.learn_from_teacher_node() 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 = SignedTreasureMap(m=0) wrong_tmap.prepare_for_publication( blockchain_bob.public_keys(DecryptingPower), blockchain_bob.public_keys(SigningPower), blockchain_alice.stamp, b'Wrong!') 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. tmap_bytes = bytes(wrong_tmap) tmap_64 = b64encode(tmap_bytes).decode() request_data['params']['treasure_map'] = tmap_64 with pytest.raises(SignedTreasureMap.IsDisorienting): bob_rpc_controller.send(request_data)
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 = SignedTreasureMap.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 use_ursula_as_an_involuntary_and_unbeknownst_cdn( self, policy, sucker_ursula): """ Ursula is a sucker. After I distract her, by paying for one Policy, maybe she'll store my copy of the Nicholas Cage remake of The Wicker Man (I have neither the respect nor the inclination to trick her into storing the original 1973 version, which after all is a very decent film). I'll make this work by fudging the HRAC a bit to create a new map ID which still appears to be connected to the Policy for which I paid. """ # Here's the proper map associated with the policy for which I paid. 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)) # 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( policy.bob.public_keys(DecryptingPower), plaintext=not_the_bees, signer=self.stamp, ) ############################################################################################# # Now I'll mess with the hrac just a bit. I can't touch the last 16 bytes, because these # # are checked against the blockchain. But the first half is up for grabs. # bad_hrac = the_map._hrac[:15] + int(i).to_bytes(length=1, byteorder="big") # # Note: if the hrac is reduced in length to 16 bytes, I'll be unable to perform this attack.# ############################################################################################# # Also note that we only touch the last byte to demonstrate that this attack isn't possible # 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() # 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) # Sucker. self.network_middleware.put_treasure_map_on_node( sucker_ursula, map_payload=bytes(like_a_map_but_awful))
def test_alice_sets_treasure_map_decentralized(enacted_blockchain_policy): """ Same as test_alice_sets_treasure_map except with a blockchain policy. """ enacted_blockchain_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) treasure_map_hrac = enacted_blockchain_policy.treasure_map._hrac[:16].hex() found = 0 for node in enacted_blockchain_policy.bob.matching_nodes_among(enacted_blockchain_policy.alice.known_nodes): with node.datastore.describe(DatastoreTreasureMap, treasure_map_hrac) as treasure_map_on_node: assert DecentralizedTreasureMap.from_bytes(treasure_map_on_node.treasure_map) == enacted_blockchain_policy.treasure_map found += 1 assert found
def receive_treasure_map(treasure_map_id): # TODO: Any of the codepaths that trigger 4xx Responses here are also SuspiciousActivity. if not this_node.federated_only: from nucypher.policy.collections import SignedTreasureMap as _MapClass else: from nucypher.policy.collections import TreasureMap as _MapClass try: treasure_map = _MapClass.from_bytes( bytes_representation=request.data, verify=True) except _MapClass.InvalidSignature: log.info("Bad TreasureMap HRAC Signature; not storing {}".format( treasure_map_id)) return Response("This TreasureMap's HRAC is not properly signed.", status=401) treasure_map_index = bytes.fromhex(treasure_map_id) # First let's see if we already have this map. try: previously_saved_map = this_node.treasure_maps[treasure_map_index] except KeyError: pass # We don't have the map. We'll validate and perhaps save it. else: if previously_saved_map == treasure_map: return Response("Already have this map.", status=303) # Otherwise, if it's a different map with the same ID, we move on to validation. if treasure_map.public_id() == treasure_map_id: do_store = True else: return Response("Can't save a TreasureMap with this ID from you.", status=409) if do_store and not this_node.federated_only: alice_checksum_address = this_node.policy_agent.contract.functions.getPolicyOwner( treasure_map._hrac[:16]).call() do_store = treasure_map.verify_blockchain_signature( checksum_address=alice_checksum_address) if do_store: log.info("{} storing TreasureMap {}".format( this_node, treasure_map_id)) this_node.treasure_maps[treasure_map_index] = treasure_map return Response(bytes(treasure_map), status=202) else: log.info( "Bad TreasureMap ID; not storing {}".format(treasure_map_id)) return Response("This TreasureMap doesn't match a paid Policy.", status=402)
def from_json(cls, data: str): """Deserializes the PolicyCredential from JSON.""" cred_json = json.loads(data) alice_verifying_key = UmbralPublicKey.from_bytes( cred_json['alice_verifying_key'], decoder=bytes.fromhex) label = bytes.fromhex(cred_json['label']) expiration = maya.MayaDT.from_iso8601(cred_json['expiration']) policy_pubkey = UmbralPublicKey.from_bytes(cred_json['policy_pubkey'], decoder=bytes.fromhex) treasure_map = None if 'treasure_map' in cred_json: # TODO: Support unsigned treasuremaps? treasure_map = SignedTreasureMap.from_bytes( bytes.fromhex(cred_json['treasure_map'])) return cls(alice_verifying_key, label, expiration, policy_pubkey, treasure_map)
def test_alice_sets_treasure_map_decentralized(enacted_blockchain_policy, blockchain_alice, blockchain_bob): """ Same as test_alice_sets_treasure_map except with a blockchain policy. """ treasure_map_hrac = enacted_blockchain_policy.treasure_map._hrac[:16].hex() 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 DecentralizedTreasureMap.from_bytes( treasure_map_on_node.treasure_map ) == enacted_blockchain_policy.treasure_map found += 1 assert found
def receive_treasure_map(): """ Okay, so we've received a TreasureMap to store. We begin verifying the treasure map by first validating the request and the received treasure map itself. We set the datastore identifier as the HRAC iff the node is running as a decentralized node. Otherwise, we use the map_id in federated mode. """ if not this_node.federated_only: from nucypher.policy.collections import SignedTreasureMap as _MapClass else: from nucypher.policy.collections import TreasureMap as _MapClass # Step 1: First, we verify the signature of the received treasure map. # This step also deserializes the treasure map iff 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()}" ) return Response("This TreasureMap's HRAC is not properly signed.", status=401) # Additionally, we determine the map identifier from the type of node. # If the node is federated, we also set the expiration for a week. if not this_node.federated_only: map_identifier = received_treasure_map._hrac.hex() else: map_identifier = received_treasure_map.public_id() expiration_date = MayaDT.from_datetime(datetime.utcnow() + timedelta(days=7)) # Step 2: Check if we already have the treasure map. try: with datastore.describe(TreasureMap, map_identifier) as stored_treasure_map: if _MapClass.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! pass # Step 3: If the node is decentralized, we check that the received # 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_data, alice_checksum_address = this_node.policy_agent.fetch_policy( received_treasure_map._hrac, with_owner=True) # If the Policy doesn't exist, the policy_data is all zeros. if not policy_data[5]: log.info( f"TreasureMap is for non-existent Policy; not storing {map_identifier}" ) return Response( "The Policy for this TreasureMap doesn't exist.", status=409) # Check that this treasure map is from Alice per the Policy. if not received_treasure_map.verify_blockchain_signature( checksum_address=alice_checksum_address): log.info(f"Bad TreasureMap ID; not storing {map_identifier}") return Response( "This TreasureMap doesn't match a paid Policy.", status=402) # Check that this treasure map is valid for the Policy datetime and that it's not disabled. if policy_data[0] or datetime.utcnow( ) >= datetime.utcfromtimestamp(policy_data[5]): log.info( f"Received TreasureMap for an expired/disabled policy; not storing {map_identifier}" ) return Response( "This TreasureMap is for an expired/disabled policy.", status=402) expiration_date = MayaDT.from_datetime( datetime.utcfromtimestamp(policy_data[5])) # Step 4: Finally, we store our treasure map under its identifier! log.info(f"{this_node} storing TreasureMap {map_identifier}") with datastore.describe(TreasureMap, map_identifier, writeable=True) as new_treasure_map: new_treasure_map.treasure_map = bytes(received_treasure_map) new_treasure_map.expiration = expiration_date return Response("Treasure map stored!", status=201)