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_recover_pk_invalid_sigrec(): message = "Hey, it's me".encode("utf-8") signature = "ddbfb019e4d56155b4175066c2b615ab765d317ae7996d188b4a5fae4cc394adf98fef46034d0553149392219ca6d37dca9abdfa6366a8e54b28f19d3e5efa8a14b556205dc7f33a" # The given signature, when zbase32 decoded, has a fist byte with value lower than 31. # The first byte of the signature should be 31 + SigRec, so this should fail with pytest.raises(SignatureError, match="Wrong SigRec"): Cryptographer.recover_pk(message, signature)
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 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 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_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_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 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 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 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_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_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_verify_pk(): sk, _ = generate_keypair() message = b"Test message" zbase32_sig = Cryptographer.sign(message, sk) rpk = Cryptographer.recover_pk(message, zbase32_sig) assert Cryptographer.verify_rpk(sk.public_key, rpk)
def test_recover_pk(): sk, _ = generate_keypair() message = b"Test message" zbase32_sig = Cryptographer.sign(message, sk) rpk = Cryptographer.recover_pk(message, zbase32_sig) assert isinstance(rpk, PublicKey)
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): # We should be able to add appointments up to the limit for _ in range(10): appointment, dispute_tx = generate_dummy_appointment( start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET) added_appointment, sig = watcher.add_appointment(appointment) assert added_appointment is True assert Cryptographer.verify_rpk( watcher.signing_key.public_key, Cryptographer.recover_pk(appointment.serialize(), sig)) # Check that we can also add an already added appointment (same locator) added_appointment, sig = watcher.add_appointment(appointment) assert added_appointment is True assert Cryptographer.verify_rpk( watcher.signing_key.public_key, Cryptographer.recover_pk(appointment.serialize(), sig))
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 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_register(watcher, monkeypatch): # Register requests should work as long as the provided user_id is valid and bitcoind is reachable # Mock the interaction with the Gatekeeper slots = 100 expiry = 200 receipt = bytes.fromhex(get_random_value_hex(70)) monkeypatch.setattr(watcher.gatekeeper, "add_update_user", lambda x: (slots, expiry, receipt)) # Request a registration and check the response available_slots, subscription_expiry, signature = watcher.register(user_id) assert available_slots == slots assert subscription_expiry == expiry assert Cryptographer.recover_pk( receipt, signature) == watcher.signing_key.public_key
def register(user_id, teos_id, teos_url): """ Registers the user to the tower. Args: user_id (:obj:`str`): a 33-byte hex-encoded compressed public key representing the user. 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 available slots count and the subscription expiry. Raises: :obj:`InvalidParameter`: if `user_id` is invalid. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError`: if the tower responded with an error, or the response was invalid. """ if not is_compressed_pk(user_id): raise InvalidParameter("The cli public key is not valid") # Send request to the server. register_endpoint = "{}/register".format(teos_url) data = {"public_key": user_id} logger.info("Registering in the Eye of Satoshi") response = process_post_response(post_request(data, register_endpoint)) available_slots = response.get("available_slots") subscription_expiry = response.get("subscription_expiry") tower_signature = response.get("subscription_signature") # Check that the server signed the response as it should. if not tower_signature: raise TowerResponseError( "The response does not contain the signature of the subscription") # Check that the signature is correct. subscription_receipt = receipts.create_registration_receipt( user_id, available_slots, subscription_expiry) rpk = Cryptographer.recover_pk(subscription_receipt, tower_signature) if teos_id != Cryptographer.get_compressed_pk(rpk): raise TowerResponseError( "The returned appointment's signature is invalid") return available_slots, subscription_expiry
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 check_appointment_signature(appointment_data, signature, pk): """ Checks if the provided user signature is correct. Args: appointment_data (:obj:`dict`): the appointment that was signed by the user. signature (:obj:`str`): the user's signature (hex encoded). pk (:obj:`str`): the user's public key (hex encoded). Returns: :obj:`tuple`: A tuple (return code, message) as follows: - ``(0, None)`` if the ``signature`` is correct. - ``!= (0, None)`` otherwise. The possible return errors are: ``APPOINTMENT_EMPTY_FIELD``, ``APPOINTMENT_WRONG_FIELD_TYPE``, and ``APPOINTMENT_WRONG_FIELD_FORMAT``. """ message = None rcode = 0 if signature is None: rcode = errors.APPOINTMENT_EMPTY_FIELD message = "empty signature received" elif pk is None: rcode = errors.APPOINTMENT_EMPTY_FIELD message = "empty public key received" elif re.match(r"^[0-9A-Fa-f]{66}$", pk) is None: rcode = errors.APPOINTMENT_WRONG_FIELD message = "public key must be a hex encoded 33-byte long value" else: appointment = Appointment.from_dict(appointment_data) rpk = Cryptographer.recover_pk(appointment.serialize(), signature) pk = PublicKey(unhexlify(pk)) valid_sig = Cryptographer.verify_rpk(pk, rpk) if not valid_sig: rcode = errors.APPOINTMENT_INVALID_SIGNATURE message = "invalid signature" return rcode, message
def get_appointment(self): locator = request.get_json().get("locator") message = f"get appointment {locator}" user_id = Cryptographer.get_compressed_pk( Cryptographer.recover_pk(message.encode(), request.get_json().get("signature"))) if (user_id in self.users and "appointments" in self.users[user_id] and locator in self.users[user_id]["appointments"]): rcode = constants.HTTP_OK response = self.users[user_id]["appointments"][locator] response["status"] = "being_watched" else: rcode = constants.HTTP_NOT_FOUND response = {"locator": locator, "status": "not_found"} return jsonify(response), rcode
def test_register(internal_api, client): # Tests registering a user within the tower current_height = internal_api.watcher.block_processor.get_block_count() data = {"public_key": user_id} r = client.post(register_endpoint, json=data) 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" ) == current_height + config.get("SUBSCRIPTION_DURATION") slots = r.json.get("available_slots") expiry = r.json.get("subscription_expiry") subscription_receipt = receipts.create_registration_receipt( user_id, slots, expiry) rpk = Cryptographer.recover_pk(subscription_receipt, r.json.get("subscription_signature")) assert Cryptographer.get_compressed_pk(rpk) == teos_id
def test_appointment_wrong_key(bitcoin_cli, create_txs): # 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 # The appointment data is built using a random 32-byte value. appointment_data = build_appointment_data(bitcoin_cli, get_random_value_hex(32), penalty_tx) # We can't 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(Blob(penalty_tx), get_random_value_hex(32)) appointment = Appointment.from_dict(appointment_data) teos_pk, cli_sk, cli_pk_der = teos_cli.load_keys( cli_config.get("TEOS_PUBLIC_KEY"), cli_config.get("CLI_PRIVATE_KEY"), cli_config.get("CLI_PUBLIC_KEY") ) hex_pk_der = binascii.hexlify(cli_pk_der) signature = Cryptographer.sign(appointment.serialize(), cli_sk) data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")} # Send appointment to the server. response = teos_cli.post_appointment(data, teos_add_appointment_endpoint) response_json = teos_cli.process_post_appointment_response(response) # Check that the server has accepted the appointment signature = response_json.get("signature") assert signature is not None rpk = Cryptographer.recover_pk(appointment.serialize(), signature) assert Cryptographer.verify_rpk(teos_pk, rpk) is True 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. sleep(1) appointment_info = get_appointment_info(appointment.locator) assert appointment_info is not None assert len(appointment_info) == 1 assert appointment_info[0].get("status") == "not_found"
def test_recover_pk_wrong_inputs(): str_message = "Test message" message = bytes(20) str_sig = "aaaaaaaa" sig = bytes(20) # Wrong input type assert Cryptographer.recover_pk(message, str_sig) is None assert Cryptographer.recover_pk(str_message, str_sig) is None assert Cryptographer.recover_pk(str_message, sig) is None assert Cryptographer.recover_pk(message, str_sig) is None # Wrong input size or format assert Cryptographer.recover_pk(message, sig) is None assert Cryptographer.recover_pk(message, bytes(104)) is None
def test_add_too_many_appointments(watcher): # Any appointment on top of those should fail watcher.appointments = dict() for _ in range(config.get("MAX_APPOINTMENTS")): appointment, dispute_tx = generate_dummy_appointment( start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET) added_appointment, sig = watcher.add_appointment(appointment) assert added_appointment is True assert Cryptographer.verify_rpk( watcher.signing_key.public_key, Cryptographer.recover_pk(appointment.serialize(), sig)) appointment, dispute_tx = generate_dummy_appointment( start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET) added_appointment, sig = watcher.add_appointment(appointment) assert added_appointment is False assert sig is None
def get_appointment(self): locator = request.get_json().get("locator") message = f"get appointment {locator}" user_id = Cryptographer.get_compressed_pk( Cryptographer.recover_pk(message.encode("utf-8"), request.get_json().get("signature"))) if (user_id in self.users and "appointments" in self.users[user_id] and locator in self.users[user_id]["appointments"]): rcode = constants.HTTP_OK response = self.users[user_id]["appointments"][locator] response["status"] = AppointmentStatus.BEING_WATCHED else: rcode = constants.HTTP_NOT_FOUND response = { "locator": locator, "status": AppointmentStatus.NOT_FOUND } return jsonify(response), rcode
def test_recover_pk_wrong_inputs(): str_message = "Test message" message = bytes(20) str_sig = "aaaaaaaa" sig = bytes(20) # Wrong input type with pytest.raises(InvalidParameter, match="Wrong value passed as zbase32_sig"): Cryptographer.recover_pk(message, sig) with pytest.raises(InvalidParameter, match="Wrong value passed as message"): Cryptographer.recover_pk(str_message, str_sig) with pytest.raises(InvalidParameter, match="Wrong value passed as message"): Cryptographer.recover_pk(str_message, sig) # Wrong input size with pytest.raises(SignatureError, match="Serialized signature must be 65 bytes long"): Cryptographer.recover_pk(message, str_sig)