def test_add_appointment_multiple_times_different_users( internal_api, client, appointment, block_processor, n=MULTIPLE_APPOINTMENTS): # If the same appointment comes from different users, all are kept # Create user keys and appointment signatures user_keys = [generate_keypair() for _ in range(n)] signatures = [ Cryptographer.sign(appointment.serialize(), key[0]) for key in user_keys ] tmp_user_ids = [Cryptographer.get_compressed_pk(pk) for _, pk in user_keys] # Add one slot per public key for pair in user_keys: user_id = Cryptographer.get_compressed_pk(pair[1]) internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=block_processor.get_block_count() + 1) # Send the appointments for compressed_pk, signature in zip(tmp_user_ids, signatures): r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": signature }, compressed_pk) assert r.status_code == HTTP_OK assert r.json.get("available_slots") == 0 assert r.json.get("start_block") == block_processor.get_block_count() # Check that all the appointments have been added and that there are no duplicates assert len(set( internal_api.watcher.locator_uuid_map[appointment.locator])) == n
def test_add_appointment_in_cache_invalid_transaction( watcher, generate_dummy_appointment): # Generate an appointment that cannot be decrypted and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment, dispute_tx = generate_dummy_appointment() appointment.encrypted_blob = appointment.encrypted_blob[::-1] dispute_txid = watcher.block_processor.decode_raw_transaction( dispute_tx).get("txid") watcher.locator_cache.cache[appointment.locator] = dispute_txid # Try to add the appointment user_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, user_signature) appointment_receipt = receipts.create_appointment_receipt( user_signature, response.get("start_block")) # The appointment is accepted but dropped (same as an invalid appointment that gets triggered) assert (response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature")))) assert not watcher.locator_uuid_map.get(appointment.locator) assert appointment.locator not in [ tracker.get("locator") for tracker in watcher.responder.trackers.values() ]
def test_add_too_many_appointments(watcher): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=available_slots, subscription_expiry=10) # Appointments on top of the limit should be rejected watcher.appointments = dict() for i in range(MAX_APPOINTMENTS): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) assert response.get("available_slots") == available_slots - (i + 1) with pytest.raises(AppointmentLimitReached): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
def test_add_too_many_appointments(watcher, generate_dummy_appointment, monkeypatch): # Adding appointment beyond the user limit should fail # Mock the user being registered expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) for i in range(user_info.available_slots): appointment = generate_dummy_appointment() response = watcher.add_appointment(appointment, appointment.user_signature) appointment_receipt = receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature"))) with pytest.raises(AppointmentLimitReached): appointment = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
def test_add_appointment_in_cache(watcher): # Generate an appointment and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=10) appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") watcher.locator_cache.cache[appointment.locator] = dispute_txid # Try to add the appointment response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk)) # The appointment is accepted but it's not in the Watcher assert ( response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(Cryptographer.recover_pk(appointment.serialize(), response.get("signature"))) ) assert not watcher.locator_uuid_map.get(appointment.locator) # It went to the Responder straightaway assert appointment.locator in [tracker.get("locator") for tracker in watcher.responder.trackers.values()] # Trying to send it again should fail since it is already in the Responder with pytest.raises(AppointmentAlreadyTriggered): watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))
def test_add_appointment(watcher, generate_dummy_appointment): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=available_slots, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert response.get("available_slots") == available_slots - 1 # Check that we can also add an already added appointment (same locator) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) # The slot count should not have been reduced and only one copy is kept. assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 1 # If two appointments with the same locator come from different users, they are kept. another_user_sk, another_user_pk = generate_keypair() another_user_id = Cryptographer.get_compressed_pk(another_user_pk) watcher.gatekeeper.registered_users[another_user_id] = UserInfo( available_slots=available_slots, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment_signature = Cryptographer.sign(appointment.serialize(), another_user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def send_appointment(tower_id, tower, appointment_dict, signature): data = {"appointment": appointment_dict, "signature": signature} add_appointment_endpoint = f"{tower.netaddr}/add_appointment" response = process_post_response( post_request(data, add_appointment_endpoint, tower_id)) tower_signature = response.get("signature") # Check that the server signed the appointment as it should. if not tower_signature: raise SignatureError( "The response does not contain the signature of the appointment", signature=None) rpk = Cryptographer.recover_pk( Appointment.from_dict(appointment_dict).serialize(), tower_signature) recovered_id = Cryptographer.get_compressed_pk(rpk) if tower_id != recovered_id: raise SignatureError( "The returned appointment's signature is invalid", tower_id=tower_id, recovered_id=recovered_id, signature=tower_signature, ) return response
def test_add_appointment_trigger_on_cache_cannot_decrypt(bitcoin_cli): commitment_tx, penalty_tx = create_txs(bitcoin_cli) # Let's send the commitment to the network and mine a block broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, bitcoin_cli.getnewaddress()) sleep(1) # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx) # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. response = teos_cli.post_request(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert teos_id == Cryptographer.get_compressed_pk(rpk) assert response_json.get("locator") == appointment.locator # The appointment should should have been inmediately dropped with pytest.raises(TowerResponseError): get_appointment_info(appointment_data["locator"])
def test_get_tower_info_empty(clear_state, internal_api, stub): response = stub.get_tower_info(Empty()) assert isinstance(response, GetTowerInfoResponse) assert response.tower_id == Cryptographer.get_compressed_pk(teos_pk) assert response.n_registered_users == 0 assert response.n_watcher_appointments == 0 assert response.n_responder_trackers == 0
def load_keys(user_sk_path): """ Loads all the user private key and id. Args: user_sk_path (:obj:`str`): path to the user's private key file. Returns: :obj:`tuple`: A tuple containing a :obj:`PrivateKey` and a :obj:`str` representing the user sk and user id (compressed pk) respectively. Raises: :obj:`InvalidKey`: if any of the keys is invalid or cannot be loaded. """ if not user_sk_path: raise InvalidKey( "Client's private key file not found. Please check your settings") try: user_sk_der = Cryptographer.load_key_file(user_sk_path) user_sk = Cryptographer.load_private_key_der(user_sk_der) except (InvalidParameter, InvalidKey): raise InvalidKey("Client private key is invalid or cannot be parsed") try: user_id = Cryptographer.get_compressed_pk(user_sk.public_key) except (InvalidParameter, InvalidKey): raise InvalidKey("Client public key cannot be loaded") return user_sk, user_id
def generate_keys(data_dir): """ Generates a key pair for the client. Args: data_dir (:obj:`str`): path to data directory where the keys will be stored. Returns: :obj:`tuple`: a tuple containing a ``PrivateKey`` and a ``str`` representing the client sk and compressed pk respectively. Raises: :obj:`FileExistsError`: if the key pair already exists in the given directory. """ # Create the output folder it it does not exist (and all the parents if they don't either) Path(data_dir).mkdir(parents=True, exist_ok=True) sk_file_name = os.path.join(data_dir, "sk.der") if os.path.exists(sk_file_name): raise FileExistsError("The client key pair already exists") sk = PrivateKey() pk = sk.public_key save_key(sk, sk_file_name) return sk, Cryptographer.get_compressed_pk(pk)
def test_register(api, client, monkeypatch): # Tests registering a user within the tower # Monkeypatch the response from the InternalAPI so the user is accepted slots = config.get("SUBSCRIPTION_SLOTS") expiry = config.get("SUBSCRIPTION_DURATION") receipt = receipts.create_registration_receipt(user_id, slots, expiry) signature = Cryptographer.sign(receipt, teos_sk) response = RegisterResponse( user_id=user_id, available_slots=slots, subscription_expiry=expiry, subscription_signature=signature, ) monkeypatch.setattr(api.stub, "register", lambda x: response) # Send the register request data = {"public_key": user_id} r = client.post(register_endpoint, json=data) # Check the reply assert r.status_code == HTTP_OK assert r.json.get("public_key") == user_id assert r.json.get("available_slots") == config.get("SUBSCRIPTION_SLOTS") assert r.json.get("subscription_expiry") == config.get( "SUBSCRIPTION_DURATION") rpk = Cryptographer.recover_pk(receipt, r.json.get("subscription_signature")) assert Cryptographer.get_compressed_pk(rpk) == teos_id
def test_appointment_wrong_decryption_key(bitcoin_cli): # This tests an appointment encrypted with a key that has not been derived from the same source as the locator. # Therefore the tower won't be able to decrypt the blob once the appointment is triggered. commitment_tx, penalty_tx = create_txs(bitcoin_cli) # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx) # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually. # We will encrypt the blob using the random value and derive the locator from the commitment tx. appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")) appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. response = teos_cli.post_request(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert teos_id == Cryptographer.get_compressed_pk(rpk) assert response_json.get("locator") == appointment.locator # Trigger the appointment new_addr = bitcoin_cli.getnewaddress() broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr) # The appointment should have been removed since the decryption failed. with pytest.raises(TowerResponseError): get_appointment_info(appointment.locator)
def load_teos_id(teos_pk_path): """ Loads the tower id from disk. Args: teos_pk_path (:obj:`str`): path to the tower's public key file. Returns: :obj:`str`: The tower id. Raises: :obj:`InvalidKey`: if the public key is invalid or cannot be loaded. """ if not teos_pk_path: raise InvalidKey( "TEOS's public key file not found. Have you registered with the tower?" ) try: teos_id = Cryptographer.get_compressed_pk( PublicKey(Cryptographer.load_key_file(teos_pk_path))) except (InvalidParameter, InvalidKey, ValueError): raise InvalidKey( "TEOS public key cannot be loaded. Try registering again") return teos_id
def authenticate_user(self, message, signature): """ Checks if a request comes from a registered user by ec-recovering their public key from a signed message. Args: message (:obj:`bytes`): byte representation of the original message from where the signature was generated. signature (:obj:`str`): the user's signature (hex-encoded). Returns: :obj:`str`: a compressed key recovered from the signature and matching a registered user. Raises: :obj:`AuthenticationFailure`: if the user cannot be authenticated. """ try: rpk = Cryptographer.recover_pk(message, signature) user_id = Cryptographer.get_compressed_pk(rpk) if user_id in self.registered_users: return user_id else: raise AuthenticationFailure("User not found.") except (InvalidParameter, InvalidKey, SignatureError): raise AuthenticationFailure("Wrong message or signature.")
def load_keys(data_dir): """ Loads a the client key pair. Args: data_dir (:obj:`str`): path to data directory where the keys are stored. Returns: :obj:`tuple`: a tuple containing a ``PrivateKey`` and a ``str`` representing the client sk and compressed pk respectively. Raises: :obj:`InvalidKey <cli.exceptions.InvalidKey>`: if any of the keys is invalid or cannot be loaded. """ if not isinstance(data_dir, str): raise ValueError("Invalid data_dir. Please check your settings") sk_file_path = os.path.join(data_dir, "sk.der") cli_sk_der = Cryptographer.load_key_file(sk_file_path) cli_sk = Cryptographer.load_private_key_der(cli_sk_der) if cli_sk is None: raise InvalidKey("Client private key is invalid or cannot be parsed") compressed_cli_pk = Cryptographer.get_compressed_pk(cli_sk.public_key) if compressed_cli_pk is None: raise InvalidKey("Client public key cannot be loaded") return cli_sk, compressed_cli_pk
def test_add_appointment_in_cache_invalid_blob(watcher): # Generate an appointment with an invalid transaction and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=watcher.block_processor.get_block_count() + 1) # We need to create the appointment manually commitment_tx, commitment_txid, penalty_tx = create_txs() locator = compute_locator(commitment_tx) dummy_appointment_data = { "tx": penalty_tx, "tx_id": commitment_txid, "to_self_delay": 20 } encrypted_blob = Cryptographer.encrypt(penalty_tx[::-1], commitment_txid) appointment_data = { "locator": locator, "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, "user_id": get_random_value_hex(16), } appointment = Appointment.from_dict(appointment_data) watcher.locator_cache.cache[appointment.locator] = commitment_txid # Try to add the appointment user_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, user_signature) appointment_receipt = receipts.create_appointment_receipt( user_signature, response.get("start_block")) # The appointment is accepted but dropped (same as an invalid appointment that gets triggered) assert (response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature")))) assert not watcher.locator_uuid_map.get(appointment.locator) assert appointment.locator not in [ tracker.get("locator") for tracker in watcher.responder.trackers.values() ]
def test_add_appointment_in_cache(watcher, generate_dummy_appointment_w_trigger, monkeypatch): # Adding an appointment which trigger is in the cache should be accepted appointment, commitment_txid = generate_dummy_appointment_w_trigger() # We need the blob and signature to be valid appointment.user_signature = Cryptographer.sign( appointment.encrypted_blob.encode(), user_sk) # Mock the transaction being in the cache and all the way until sending it to the Responder expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) monkeypatch.setattr(watcher.locator_cache, "get_txid", lambda x: commitment_txid) monkeypatch.setattr(watcher.responder, "handle_breach", mock_receipt_true) # Try to add the appointment # user_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment.user_signature) appointment_receipt = receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")) # The appointment is accepted but it's not in the Watcher assert (response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature")))) assert not watcher.locator_uuid_map.get(appointment.locator) # It went to the Responder straightaway, we can check this by querying the database for uuid, db_appointment in watcher.db_manager.load_watcher_appointments( include_triggered=True).items(): if db_appointment.get("locator") == appointment.locator: assert uuid in watcher.db_manager.load_all_triggered_flags() # Trying to send it again should fail since it is already in the Responder monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: True) with pytest.raises(AppointmentAlreadyTriggered): watcher.add_appointment( appointment, Cryptographer.sign(appointment.serialize(), user_sk))
def load_keys(teos_pk_path, user_sk_path): """ Loads all the keys required so sign, send, and verify the appointment. Args: teos_pk_path (:obj:`str`): path to the tower's public key file. user_sk_path (:obj:`str`): path to the user's private key file. Returns: :obj:`tuple`: a three-item tuple containing a ``str``, a ``PrivateKey`` and a ``str`` representing the tower id (compressed pk), user sk and user id (compressed pk) respectively. Raises: :obj:`InvalidKey <cli.exceptions.InvalidKey>`: if any of the keys is invalid or cannot be loaded. """ if not teos_pk_path: raise InvalidKey( "TEOS's public key file not found. Please check your settings") if not user_sk_path: raise InvalidKey( "Client's private key file not found. Please check your settings") try: teos_pk_der = Cryptographer.load_key_file(teos_pk_path) teos_id = Cryptographer.get_compressed_pk(PublicKey(teos_pk_der)) except (InvalidParameter, InvalidKey, ValueError): raise InvalidKey("TEOS public key cannot be loaded") try: user_sk_der = Cryptographer.load_key_file(user_sk_path) user_sk = Cryptographer.load_private_key_der(user_sk_der) except (InvalidParameter, InvalidKey): raise InvalidKey("Client private key is invalid or cannot be parsed") try: user_id = Cryptographer.get_compressed_pk(user_sk.public_key) except (InvalidParameter, InvalidKey): raise InvalidKey("Client public key cannot be loaded") return teos_id, user_sk, user_id
def test_recover_pk_ground_truth(): # Use a message a signature generated by c-lightning and see if we recover the proper key message = b"Test message" org_pk = "02b821c749295d5c24f6166ae77d8353eaa36fc4e47326670c6d2522cbd344bab9" zsig = "rbwewwyr4zem3w5t39fd1xyeamfzbmfgztwm4b613ybjtmoeod5kazaxqo3akn3ae75bqi3aqeds8cs6n43w4p58ft34itjnnb61bp54" rpk = Cryptographer.recover_pk(message, zsig) assert org_pk == Cryptographer.get_compressed_pk(rpk)
def test_add_appointment(watcher, generate_dummy_appointment, monkeypatch): # A registered user with no subscription issues should be able to add an appointment appointment = generate_dummy_appointment() # Mock a registered user expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: False) monkeypatch.setattr(watcher.gatekeeper, "add_update_appointment", lambda x, y, z: MAX_APPOINTMENTS - 1) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) response = watcher.add_appointment(appointment, appointment.user_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")), response.get("signature"), )) # Check that we can also add an already added appointment (same locator) response = watcher.add_appointment(appointment, appointment.user_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")), response.get("signature"), )) # One one copy is kept since the appointments were the same # (the slot count should have not been reduced, but that's something to be tested in the Gatekeeper) assert len(watcher.locator_uuid_map[appointment.locator]) == 1 # If two appointments with the same locator come from different users, they are kept. another_user_sk, another_user_pk = generate_keypair() another_user_id = Cryptographer.get_compressed_pk(another_user_pk) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: another_user_id) appointment_signature = Cryptographer.sign(appointment.serialize(), another_user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def test_add_appointment_no_slots(watcher): # Appointments from register users with no available slots should aso fail user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=0, subscription_expiry=10) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) with pytest.raises(NotEnoughSlots): watcher.add_appointment(appointment, appointment_signature)
def test_sign_ground_truth(): # Generate a signature that has been verified by c-lightning. raw_sk = "24e9a981580d27d9277071a8381542e89a7c124868c4e862a13595dc75c6922f" sk = PrivateKey.from_hex(raw_sk) c_lightning_rpk = "0235293db86c6aaa74aff69ebacad8471d5242901ea9f6a0341a8dca331875e62c" message = b"Test message" sig = Cryptographer.sign(message, sk) rpk = Cryptographer.recover_pk(message, sig) assert c_lightning_rpk == Cryptographer.get_compressed_pk(rpk)
def test_identify_user(gatekeeper): # Identify user should return a user_pk for registered users. It raises # IdentificationFailure for invalid parameters or non-registered users. # Let's first register a user sk, pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(pk) gatekeeper.add_update_user(user_id) message = "Hey, it's me" signature = Cryptographer.sign(message.encode(), sk) assert gatekeeper.authenticate_user(message.encode(), signature) == user_id
def test_add_appointment_not_registered(internal_api, client, appointment): # Properly formatted appointment, user is not registered tmp_sk, tmp_pk = generate_keypair() tmp_user_id = Cryptographer.get_compressed_pk(tmp_pk) appointment_signature = Cryptographer.sign(appointment.serialize(), tmp_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, tmp_user_id) assert r.status_code == HTTP_BAD_REQUEST assert errors.APPOINTMENT_INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR == r.json.get( "error_code")
def test_add_appointment_in_cache_invalid_blob_or_tx( watcher, generate_dummy_appointment_w_trigger, monkeypatch): # Trying to add an appointment with invalid data (blob does not decrypt to a tx or the tx in not invalid) with a # trigger in the cache will be accepted, but the data will de dropped. appointment, commitment_txid = generate_dummy_appointment_w_trigger() # Mock the trigger being in the cache expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) monkeypatch.setattr(watcher.locator_cache, "get_txid", lambda x: commitment_txid) # Check for both the blob being invalid, and the transaction being invalid for mocked_return in [raise_encryption_error, raise_invalid_tx_format]: # Mock the data check (invalid blob) monkeypatch.setattr(watcher.responder, "handle_breach", mocked_return) # Try to add the appointment response = watcher.add_appointment(appointment, appointment.user_signature) appointment_receipt = receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")) # The appointment is accepted but dropped assert (response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature")))) # Check the appointment didn't go to the Responder (by checking there are no triggered flags) assert watcher.db_manager.load_all_triggered_flags() == []
def add_appointment(appointment, user_sk, teos_id, teos_url): """ Manages the add_appointment command. The life cycle of the function is as follows: - Sign the appointment - Send the appointment to the tower - Wait for the response - Check the tower's response and signature Args: appointment (:obj:`Appointment <common.appointment.Appointment>`): an appointment object. user_sk (:obj:`PrivateKey`): the user's private key. teos_id (:obj:`str`): the tower's compressed public key. teos_url (:obj:`str`): the teos base url. Returns: :obj:`tuple`: A tuple containing the start block and the tower's signature of the appointment. Raises: :obj:`ValueError`: if the appointment cannot be signed. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError`: if the tower responded with an error, or the response was invalid. """ signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. logger.info("Sending appointment to the Eye of Satoshi") add_appointment_endpoint = "{}/add_appointment".format(teos_url) response = process_post_response( post_request(data, add_appointment_endpoint)) tower_signature = response.get("signature") start_block = response.get("start_block") appointment_receipt = receipts.create_appointment_receipt( signature, start_block) # Check that the server signed the appointment as it should. if not tower_signature: raise TowerResponseError( "The response does not contain the signature of the appointment") rpk = Cryptographer.recover_pk(appointment_receipt, tower_signature) if teos_id != Cryptographer.get_compressed_pk(rpk): raise TowerResponseError( "The returned appointment's signature is invalid") logger.info("Appointment accepted and signed by the Eye of Satoshi") logger.info("Remaining slots: {}".format(response.get("available_slots"))) logger.info("Start block: {}".format(start_block)) return start_block, tower_signature
def test_watchtower_multiple_towers(node_factory): """ Test sending data to multiple towers at the same time""" global mocked_return # Create the new tower another_tower_netaddr = "localhost" another_tower_port = "5678" another_tower_sk = PrivateKey() another_tower_id = Cryptographer.get_compressed_pk( another_tower_sk.public_key) another_tower = TowerMock(another_tower_sk) Thread(target=another_tower.app.run, kwargs={ "host": another_tower_netaddr, "port": another_tower_port }, daemon=True).start() l1, l2 = node_factory.line_graph(2, opts=[{ "may_fail": True, "allow_broken_log": True }, { "plugin": plugin_path }]) # Register a new tower l2.rpc.registertower("{}@{}:{}".format(another_tower_id, another_tower_netaddr, another_tower_port)) # Make sure the tower in our list of towers tower_ids = [ tower.get("id") for tower in l2.rpc.listtowers().get("towers") ] assert another_tower_id in tower_ids # Force a new commitment mocked_return = "success" l1.rpc.pay(l2.rpc.invoice(25000000, "lbl6", "desc")["bolt11"]) # Check that both towers got it another_tower_appointments = l2.rpc.gettowerinfo(another_tower_id).get( "appointments") assert another_tower_appointments assert not l2.rpc.gettowerinfo(another_tower_id).get( "pending_appointments") assert set(another_tower_appointments).issubset( l2.rpc.gettowerinfo(tower_id).get("appointments"))
def test_add_update_appointment(gatekeeper, generate_dummy_appointment): # add_update_appointment should decrease the slot count if a new appointment is added # let's add a new user sk, pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(pk) gatekeeper.add_update_user(user_id) # And now update add a new appointment appointment, _ = generate_dummy_appointment() appointment_uuid = get_random_value_hex(16) remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment) # This is a standard size appointment, so it should have reduced the slots by one assert appointment_uuid in gatekeeper.registered_users[ user_id].appointments assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1 # Updates can leave the count as is, decrease it, or increase it, depending on the appointment size (modulo # ENCRYPTED_BLOB_MAX_SIZE_HEX) # Appointments of the same size leave it as is appointment_same_size, _ = generate_dummy_appointment() remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment) assert appointment_uuid in gatekeeper.registered_users[ user_id].appointments assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1 # Bigger appointments decrease it appointment_x2_size = appointment_same_size appointment_x2_size.encrypted_blob = "A" * (ENCRYPTED_BLOB_MAX_SIZE_HEX + 1) remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment_x2_size) assert appointment_uuid in gatekeeper.registered_users[ user_id].appointments assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 2 # Smaller appointments increase it remaining_slots = gatekeeper.add_update_appointment( user_id, appointment_uuid, appointment) assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1 # If the appointment needs more slots than there's free, it should fail gatekeeper.registered_users[user_id].available_slots = 1 appointment_uuid = get_random_value_hex(16) with pytest.raises(NotEnoughSlots): gatekeeper.add_update_appointment(user_id, appointment_uuid, appointment_x2_size)
def test_get_users(teosd, rpc_client): _, teos_id = teosd # Create a fresh user tmp_user_id = Cryptographer.get_compressed_pk(Cryptographer.generate_key().public_key) users = json.loads(rpc_client.get_users()) assert tmp_user_id not in users # Register the fresh user teos_client.register(tmp_user_id, teos_id, teos_base_endpoint) users = json.loads(rpc_client.get_users()) assert tmp_user_id in users