def set_up_appointments(db_manager, total_appointments): appointments = dict() locator_uuid_map = dict() for i in range(total_appointments): uuid = uuid4().hex locator = get_random_value_hex(LOCATOR_LEN_BYTES) appointment = Appointment(locator, None, None, None, None) appointments[uuid] = {"locator": appointment.locator} locator_uuid_map[locator] = [uuid] db_manager.store_watcher_appointment(uuid, appointment.to_json()) db_manager.create_append_locator_map(locator, uuid) # Each locator can have more than one uuid assigned to it. if i % 2: uuid = uuid4().hex appointments[uuid] = {"locator": appointment.locator} locator_uuid_map[locator].append(uuid) db_manager.store_watcher_appointment(uuid, appointment.to_json()) db_manager.create_append_locator_map(locator, uuid) return appointments, locator_uuid_map
def test_to_dict(appointment_data): appointment = Appointment(appointment_data["locator"], appointment_data["encrypted_blob"], appointment_data["to_self_delay"]) dict_appointment = appointment.to_dict() assert (appointment_data["locator"] == dict_appointment["locator"] and appointment_data["to_self_delay"] == dict_appointment["to_self_delay"] and appointment_data["encrypted_blob"] == dict_appointment["encrypted_blob"])
def test_from_dict(appointment_data): # The appointment should be build if we don't miss any field appointment = Appointment.from_dict(appointment_data) assert isinstance(appointment, Appointment) # Otherwise it should fail for key in appointment_data.keys(): prev_val = appointment_data[key] appointment_data[key] = None with pytest.raises(ValueError, match="Wrong appointment data"): Appointment.from_dict(appointment_data) appointment_data[key] = prev_val
def test_add_appointment_with_invalid_signature(): # Simulate a request to add_appointment for dummy_appointment, but sign with a different key, # make sure that the right endpoint is requested, but the return value is False appointment = teos_client.create_appointment(dummy_appointment_data) user_signature = Cryptographer.sign(appointment.serialize(), dummy_user_sk) appointment_receipt = receipts.create_appointment_receipt( user_signature, CURRENT_HEIGHT) # Sign with a bad key response = { "locator": dummy_appointment.locator, "signature": Cryptographer.sign(appointment_receipt, another_sk), "available_slots": 100, "start_block": CURRENT_HEIGHT, "subscription_expiry": CURRENT_HEIGHT + 4320, } responses.add(responses.POST, add_appointment_endpoint, json=response, status=200) with pytest.raises(TowerResponseError): teos_client.add_appointment( Appointment.from_dict(dummy_appointment_data), dummy_user_sk, dummy_teos_id, teos_url) # should have performed exactly 1 network request assert len(responses.calls) == 1
def test_get_appointment_watcher(api, client, generate_dummy_appointment, monkeypatch): # Mock the appointment in the Watcher appointment = generate_dummy_appointment() app_data = AppointmentData(appointment=AppointmentProto( locator=appointment.locator, encrypted_blob=appointment.encrypted_blob, to_self_delay=appointment.to_self_delay, )) status = AppointmentStatus.BEING_WATCHED monkeypatch.setattr( api.stub, "get_appointment", lambda x: GetAppointmentResponse(appointment_data=app_data, status=status)) # Request it message = "get appointment {}".format(appointment.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": appointment.locator, "signature": signature} r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check that requested appointment data matches the mocked one # Cast the extended appointment (used by the tower) to a regular appointment (used by the user) local_appointment = Appointment.from_dict(appointment.to_dict()) assert r.json.get("status") == AppointmentStatus.BEING_WATCHED assert r.json.get("appointment") == local_appointment.to_dict()
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_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 from_dict(cls, appointment_data): """ Builds an appointment from a dictionary. This method is useful to load data from a database. Args: appointment_data (:obj:`dict`): a dictionary containing the following keys: ``{locator, to_self_delay, encrypted_blob, user_id}`` Returns: :obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>`: An appointment initialized using the provided data. Raises: ValueError: If one of the mandatory keys is missing in ``appointment_data``. """ appointment = Appointment.from_dict(appointment_data) user_id = appointment_data.get("user_id") user_signature = appointment_data.get("user_signature") start_block = appointment_data.get("start_block") if any(v is None for v in [user_id, user_signature, start_block]): raise ValueError("Wrong appointment data, some fields are missing") return cls( appointment.locator, appointment.encrypted_blob, appointment.to_self_delay, user_id, user_signature, start_block, )
def add_appointment(self, request, context): """Processes the request to add an appointment from a user.""" try: appointment = Appointment(request.appointment.locator, request.appointment.encrypted_blob, request.appointment.to_self_delay) return AddAppointmentResponse( **self.watcher.add_appointment(appointment, request.signature)) except (AuthenticationFailure, NotEnoughSlots): msg = "Invalid signature or user does not have enough slots available" status_code = grpc.StatusCode.UNAUTHENTICATED except AppointmentLimitReached: msg = "Appointment limit reached" status_code = grpc.StatusCode.RESOURCE_EXHAUSTED except SubscriptionExpired as e: msg = str(e) status_code = grpc.StatusCode.UNAUTHENTICATED except AppointmentAlreadyTriggered: msg = "The provided appointment has already been triggered" status_code = grpc.StatusCode.ALREADY_EXISTS except ConnectionRefusedError: msg = "Service unavailable" status_code = grpc.StatusCode.UNAVAILABLE context.set_details(msg) context.set_code(status_code) return AddAppointmentResponse()
def from_dict(cls, appointment_data): """ Builds an appointment from a dictionary. This method is useful to load data from a database. Args: appointment_data (:obj:`dict`): a dictionary containing the following keys: ``{locator, to_self_delay, encrypted_blob, user_id}`` Returns: :obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>`: An appointment initialized using the provided data. Raises: ValueError: If one of the mandatory keys is missing in ``appointment_data``. """ appointment = Appointment.from_dict(appointment_data) user_id = appointment_data.get("user_id") if not user_id: raise ValueError("Wrong appointment data, user_id is missing") else: appointment = cls(appointment.locator, appointment.to_self_delay, appointment.encrypted_blob, user_id) return appointment
def inspect(self, appointment_data): """ Inspects whether the data provided by the user is correct. Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. Returns: :obj:`Appointment <common.appointment.Appointment>`: An appointment initialized with the provided data. Raises: :obj:`InspectionFailed`: if any of the fields is wrong. """ if appointment_data is None: raise InspectionFailed(errors.APPOINTMENT_EMPTY_FIELD, "empty appointment received") elif not isinstance(appointment_data, dict): raise InspectionFailed(errors.APPOINTMENT_WRONG_FIELD, "wrong appointment format") self.check_locator(appointment_data.get("locator")) self.check_to_self_delay(appointment_data.get("to_self_delay")) self.check_blob(appointment_data.get("encrypted_blob")) return Appointment( appointment_data.get("locator"), appointment_data.get("encrypted_blob"), appointment_data.get("to_self_delay"), )
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_delete_outdated_users(gatekeeper): # This tests the deletion of users whose subscription has outdated (subscription expires now) # Create some users with associated data and add them to the gatekeeper users = {} current_height = gatekeeper.block_processor.get_block_count() for _ in range(10): appointments = { get_random_value_hex(32): Appointment(get_random_value_hex(32), None, None) } user_id = get_random_value_hex(16) user_info = UserInfo(available_slots=100, subscription_expiry=current_height, appointments=appointments) users[user_id] = user_info gatekeeper.registered_users[user_id] = user_info # Get a list of the users that should be deleted at this block height (must match the newly generated ones) users_to_be_deleted = gatekeeper.get_outdated_user_ids( current_height + gatekeeper.expiry_delta) assert users_to_be_deleted == list(users.keys()) # Delete the users Cleaner.delete_outdated_users(users_to_be_deleted, gatekeeper.registered_users, gatekeeper.user_db) # Check that the users are not in the gatekeeper anymore for user_id in users_to_be_deleted: assert user_id not in gatekeeper.registered_users assert not gatekeeper.user_db.load_user(user_id)
def test_get_appointment_in_watcher(internal_api, client, appointment, monkeypatch): # Mock the appointment in the Watcher uuid = hash_160("{}{}".format(appointment.locator, user_id)) extended_appointment_summary = { "locator": appointment.locator, "user_id": user_id } internal_api.watcher.appointments[uuid] = extended_appointment_summary internal_api.watcher.db_manager.store_watcher_appointment( uuid, appointment.to_dict()) # mock the gatekeeper (user won't be registered if the previous tests weren't ran) monkeypatch.setattr(internal_api.watcher.gatekeeper, "authenticate_user", mock_authenticate_user) # Next we can request it message = "get appointment {}".format(appointment.locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": appointment.locator, "signature": signature} r = client.post(get_appointment_endpoint, json=data) assert r.status_code == HTTP_OK # Check that the appointment is on the Watcher assert r.json.get("status") == AppointmentStatus.BEING_WATCHED # Cast the extended appointment (used by the tower) to a regular appointment (used by the user) appointment = Appointment.from_dict(appointment.to_dict()) # Check the the sent appointment matches the received one assert r.json.get("locator") == appointment.locator assert appointment.to_dict() == r.json.get("appointment")
def test_add_appointment(): # Simulate a request to add_appointment for dummy_appointment, make sure that the right endpoint is requested # and the return value is True appointment = teos_client.create_appointment(dummy_appointment_data) user_signature = Cryptographer.sign(appointment.serialize(), dummy_user_sk) appointment_receipt = receipts.create_appointment_receipt( user_signature, CURRENT_HEIGHT) response = { "locator": dummy_appointment.locator, "signature": Cryptographer.sign(appointment_receipt, dummy_teos_sk), "available_slots": 100, "start_block": CURRENT_HEIGHT, "subscription_expiry": CURRENT_HEIGHT + 4320, } responses.add(responses.POST, add_appointment_endpoint, json=response, status=200) result = teos_client.add_appointment( Appointment.from_dict(dummy_appointment_data), dummy_user_sk, dummy_teos_id, teos_url) assert len(responses.calls) == 1 assert responses.calls[0].request.url == add_appointment_endpoint assert result
def test_inspect(run_bitcoind): # At this point every single check function has been already tested, let's test inspect with an invalid and a valid # appointments. client_sk, client_pk = generate_keypair() client_pk_hex = client_pk.format().hex() # Valid appointment locator = get_random_value_hex(LOCATOR_LEN_BYTES) start_time = block_processor.get_block_count() + 5 end_time = start_time + 20 to_self_delay = MIN_TO_SELF_DELAY encrypted_blob = get_random_value_hex(64) appointment_data = { "locator": locator, "start_time": start_time, "end_time": end_time, "to_self_delay": to_self_delay, "encrypted_blob": encrypted_blob, } signature = Cryptographer.sign( Appointment.from_dict(appointment_data).serialize(), client_sk) appointment = inspector.inspect(appointment_data, signature, client_pk_hex) assert (type(appointment) == Appointment and appointment.locator == locator and appointment.start_time == start_time and appointment.end_time == end_time and appointment.to_self_delay == to_self_delay and appointment.encrypted_blob.data == encrypted_blob)
def create_appointment(appointment_data): """ Creates an appointment object from an appointment data dictionary provided by the user. Performs all the required sanity checks on the input data: - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. Returns: :obj:`common.appointment.Appointment`: An appointment built from the appointment data provided by the user. """ tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if not tx_id: raise InvalidParameter("Missing tx_id, locator cannot be computed") elif not is_256b_hex_str(tx_id): raise InvalidParameter("Wrong tx_id, locator cannot be computed") elif not tx: raise InvalidParameter("The tx field is missing in the provided data") elif not isinstance(tx, str): raise InvalidParameter("The provided tx field is not a string") appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id) return Appointment.from_dict(appointment_data)
def set_up_trackers(db_manager, total_trackers): trackers = dict() tx_tracker_map = dict() for i in range(total_trackers): uuid = uuid4().hex # We use the same txid for penalty and dispute here, it shouldn't matter penalty_txid = get_random_value_hex(32) dispute_txid = get_random_value_hex(32) locator = dispute_txid[:LOCATOR_LEN_HEX] # Appointment data appointment = Appointment(locator, None, None) # Store the data in the database and create a flag db_manager.store_watcher_appointment(uuid, appointment.to_dict()) db_manager.create_triggered_appointment_flag(uuid) # Assign both penalty_txid and dispute_txid the same id (it shouldn't matter) tracker = TransactionTracker(locator, dispute_txid, penalty_txid, None, None) trackers[uuid] = { "locator": tracker.locator, "penalty_txid": tracker.penalty_txid } tx_tracker_map[penalty_txid] = [uuid] db_manager.store_responder_tracker(uuid, tracker.to_dict()) # Each penalty_txid can have more than one uuid assigned to it. if i % 2: uuid = uuid4().hex trackers[uuid] = { "locator": tracker.locator, "penalty_txid": tracker.penalty_txid } tx_tracker_map[penalty_txid].append(uuid) db_manager.store_responder_tracker(uuid, tracker.to_dict()) # Add them to the Watcher's db too db_manager.store_watcher_appointment(uuid, appointment.to_dict()) db_manager.create_triggered_appointment_flag(uuid) return trackers, tx_tracker_map
def test_from_dict(appointment_data): # The appointment should be build if we don't miss any field appointment = Appointment.from_dict(appointment_data) assert isinstance(appointment, Appointment) # Otherwise it should fail for key in appointment_data.keys(): prev_val = appointment_data[key] appointment_data[key] = None try: Appointment.from_dict(appointment_data) assert False except ValueError: appointment_data[key] = prev_val assert True
def test_to_json(appointment_data): appointment = Appointment( appointment_data["locator"], appointment_data["start_time"], appointment_data["end_time"], appointment_data["to_self_delay"], appointment_data["encrypted_blob"], ) dict_appointment = json.loads(appointment.to_json()) assert (appointment_data["locator"] == dict_appointment["locator"] and appointment_data["start_time"] == dict_appointment["start_time"] and appointment_data["end_time"] == dict_appointment["end_time"] and appointment_data["to_self_delay"] == dict_appointment["to_self_delay"] and EncryptedBlob(appointment_data["encrypted_blob"]) == EncryptedBlob(dict_appointment["encrypted_blob"]))
def filter_valid_breaches(self, breaches): """ Filters what of the found breaches contain valid transaction data. The :obj:`Watcher` cannot if a given :obj:`EncryptedBlob <common.encrypted_blob.EncryptedBlob>` contains a valid transaction until a breach if seen. Blobs that contain arbitrary data are dropped and not sent to the :obj:`Responder <teos.responder.Responder>`. Args: breaches (:obj:`dict`): a dictionary containing channel breaches (``locator:txid``). Returns: :obj:`dict`: A dictionary containing all the breaches flagged either as valid or invalid. The structure is as follows: ``{locator, dispute_txid, penalty_txid, penalty_rawtx, valid_breach}`` """ valid_breaches = {} invalid_breaches = [] # A cache of the already decrypted blobs so replicate decryption can be avoided decrypted_blobs = {} for locator, dispute_txid in breaches.items(): for uuid in self.locator_uuid_map[locator]: appointment = Appointment.from_dict(self.db_manager.load_watcher_appointment(uuid)) if appointment.encrypted_blob.data in decrypted_blobs: penalty_tx, penalty_rawtx = decrypted_blobs[appointment.encrypted_blob.data] else: try: penalty_rawtx = Cryptographer.decrypt(appointment.encrypted_blob, dispute_txid) except ValueError: penalty_rawtx = None penalty_tx = self.block_processor.decode_raw_transaction(penalty_rawtx) decrypted_blobs[appointment.encrypted_blob.data] = (penalty_tx, penalty_rawtx) if penalty_tx is not None: valid_breaches[uuid] = { "locator": locator, "dispute_txid": dispute_txid, "penalty_txid": penalty_tx.get("txid"), "penalty_rawtx": penalty_rawtx, } logger.info( "Breach found for locator", locator=locator, uuid=uuid, penalty_txid=penalty_tx.get("txid") ) else: invalid_breaches.append(uuid) return valid_breaches, invalid_breaches
def test_init_appointment(appointment_data): # The appointment has no checks whatsoever, since the inspector is the one taking care or that, and the only one # creating appointments. appointment = Appointment(appointment_data["locator"], appointment_data["encrypted_blob"], appointment_data["to_self_delay"]) assert (appointment_data["locator"] == appointment.locator and appointment_data["to_self_delay"] == appointment.to_self_delay and appointment_data["encrypted_blob"] == appointment.encrypted_blob)
def inspect(self, appointment_data, signature, public_key): """ Inspects whether the data provided by the user is correct. Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. signature (:obj:`str`): the appointment signature provided by the user (hex encoded). public_key (:obj:`str`): the user's public key (hex encoded). Returns: :obj:`Appointment <teos.appointment.Appointment>` or :obj:`tuple`: An appointment initialized with the provided data if it is correct. Returns a tuple ``(return code, message)`` describing the error otherwise. Errors are defined in :mod:`Errors <teos.errors>`. """ block_height = self.block_processor.get_block_count() if block_height is not None: rcode, message = self.check_locator( appointment_data.get("locator")) if rcode == 0: rcode, message = self.check_start_time( appointment_data.get("start_time"), block_height) if rcode == 0: rcode, message = self.check_end_time( appointment_data.get("end_time"), appointment_data.get("start_time"), block_height) if rcode == 0: rcode, message = self.check_to_self_delay( appointment_data.get("to_self_delay")) if rcode == 0: rcode, message = self.check_blob( appointment_data.get("encrypted_blob")) if rcode == 0: rcode, message = self.check_appointment_signature( appointment_data, signature, public_key) if rcode == 0: r = Appointment.from_dict(appointment_data) else: r = (rcode, message) else: # In case of an unknown exception, assign a special rcode and reason. r = (errors.UNKNOWN_JSON_RPC_EXCEPTION, "Unexpected error occurred") return r
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 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 test_serialize(appointment_data): # From the tower end, appointments are only created if they pass the inspector tests, so not covering weird formats. # Serialize may fail if, from the user end, the user tries to do it with an weird appointment. Not critical. appointment = Appointment.from_dict(appointment_data) serialized_appointment = appointment.serialize() # Size must be 16 + len(encrypted_blob) + 4 assert len(serialized_appointment) >= 20 assert isinstance(serialized_appointment, bytes) locator = serialized_appointment[:16] encrypted_blob = serialized_appointment[16:-4] to_self_delay = serialized_appointment[-4:] assert locator.hex() == appointment.locator assert encrypted_blob.hex() == appointment.encrypted_blob assert int.from_bytes(to_self_delay, "big") == appointment.to_self_delay
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_serialize(appointment_data): # From the tower end, appointments are only created if they pass the inspector tests, so not covering weird formats. # Serialize may fail if, from the user end, the user tries to do it with an weird appointment. Not critical. appointment = Appointment.from_dict(appointment_data) serialized_appointment = appointment.serialize() # Size must be 16 + 4 + 4 + 4 + len(encrypted_blob) assert len(serialized_appointment) >= 28 assert isinstance(serialized_appointment, bytes) locator = serialized_appointment[:16] to_self_delay = serialized_appointment[16:20] encrypted_blob = serialized_appointment[20:] assert binascii.hexlify(locator).decode() == appointment.locator assert struct.unpack(">I", to_self_delay)[0] == appointment.to_self_delay assert binascii.hexlify( encrypted_blob).decode() == appointment.encrypted_blob
def test_init_appointment(appointment_data): # The appointment has no checks whatsoever, since the inspector is the one taking care or that, and the only one # creating appointments. # DISCUSS: whether this makes sense by design or checks should be ported from the inspector to the appointment # 35-appointment-checks appointment = Appointment( appointment_data["locator"], appointment_data["start_time"], appointment_data["end_time"], appointment_data["to_self_delay"], appointment_data["encrypted_blob"], ) assert (appointment_data["locator"] == appointment.locator and appointment_data["start_time"] == appointment.start_time and appointment_data["end_time"] == appointment.end_time and appointment_data["to_self_delay"] == appointment.to_self_delay and EncryptedBlob(appointment_data["encrypted_blob"]) == appointment.encrypted_blob)
def generate_dummy_appointment_data(real_height=True, start_time_offset=5, end_time_offset=30): if real_height: current_height = bitcoin_cli(bitcoind_connect_params).getblockcount() else: current_height = 10 dispute_tx = create_dummy_transaction() dispute_txid = dispute_tx.tx_id.hex() penalty_tx = create_dummy_transaction(dispute_txid) dummy_appointment_data = { "tx": penalty_tx.hex(), "tx_id": dispute_txid, "start_time": current_height + start_time_offset, "end_time": current_height + end_time_offset, "to_self_delay": 20, } # dummy keys for this test client_sk, client_pk = generate_keypair() client_pk_hex = client_pk.format().hex() locator = compute_locator(dispute_txid) blob = Blob(dummy_appointment_data.get("tx")) encrypted_blob = Cryptographer.encrypt(blob, dummy_appointment_data.get("tx_id")) appointment_data = { "locator": locator, "start_time": dummy_appointment_data.get("start_time"), "end_time": dummy_appointment_data.get("end_time"), "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, } signature = Cryptographer.sign(Appointment.from_dict(appointment_data).serialize(), client_sk) data = {"appointment": appointment_data, "signature": signature, "public_key": client_pk_hex} return data, dispute_tx.hex()