Example #1
0
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
Example #4
0
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
Example #5
0
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)
Example #8
0
    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,
        )
Example #9
0
    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()
Example #10
0
    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
Example #11
0
    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"),
        )
Example #12
0
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
Example #13
0
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)
Example #14
0
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")
Example #15
0
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
Example #16
0
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)
Example #17
0
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)
Example #18
0
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
Example #19
0
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
Example #20
0
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"]))
Example #21
0
    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)
Example #23
0
    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
Example #24
0
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()
    ]
Example #25
0
    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
Example #27
0
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"
Example #28
0
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
Example #29
0
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)
Example #30
0
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()