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
Exemple #2
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,
        )
Exemple #3
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()
Exemple #4
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
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 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)
Exemple #7
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
Exemple #8
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
Exemple #9
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
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)
Exemple #11
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")
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"])
Exemple #13
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
Exemple #14
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
Exemple #15
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
Exemple #16
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()
    ]
Exemple #17
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
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"
Exemple #20
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
Exemple #21
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()
def test_check_appointment_signature():
    # The inspector receives the public key as hex
    client_sk, client_pk = generate_keypair()
    client_pk_hex = client_pk.format().hex()

    dummy_appointment_data, _ = generate_dummy_appointment_data(
        real_height=False)
    assert Inspector.check_appointment_signature(
        dummy_appointment_data["appointment"],
        dummy_appointment_data["signature"],
        dummy_appointment_data["public_key"])

    fake_sk, _ = generate_keypair()

    # Create a bad signature to make sure inspector rejects it
    bad_signature = Cryptographer.sign(
        Appointment.from_dict(
            dummy_appointment_data["appointment"]).serialize(), fake_sk)
    assert (Inspector.check_appointment_signature(
        dummy_appointment_data["appointment"], bad_signature,
        client_pk_hex)[0] == APPOINTMENT_INVALID_SIGNATURE)
Exemple #23
0
def test_add_appointment_in_cache_invalid_transaction(internal_api, client,
                                                      block_processor):
    internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo(
        available_slots=1,
        subscription_expiry=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,
    }

    appointment = Appointment.from_dict(appointment_data)
    internal_api.watcher.locator_cache.cache[
        appointment.locator] = commitment_txid
    appointment_signature = Cryptographer.sign(appointment.serialize(),
                                               user_sk)

    # Add the data to the cache
    internal_api.watcher.locator_cache.cache[
        commitment_txid] = appointment.locator

    # The appointment should be accepted
    r = add_appointment(client, {
        "appointment": appointment.to_dict(),
        "signature": appointment_signature
    }, user_id)
    assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0
            and r.json.get("start_block") == block_processor.get_block_count())
Exemple #24
0
    def add_appointment(self):
        appointment = Appointment.from_dict(
            request.get_json().get("appointment"))
        signature = request.get_json().get("signature")
        user_id = Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(appointment.serialize(), signature))

        if mocked_return == "success":
            response, rtype = add_appointment_success(appointment, signature,
                                                      self.users[user_id],
                                                      self.sk)
        elif mocked_return == "reject_no_slots":
            response, rtype = add_appointment_reject_no_slots()
        elif mocked_return == "reject_invalid":
            response, rtype = add_appointment_reject_invalid()
        elif mocked_return == "misbehaving_tower":
            response, rtype = add_appointment_misbehaving_tower(
                appointment, signature, self.users[user_id], self.sk)
        else:
            response, rtype = add_appointment_service_unavailable()

        return jsonify(response), rtype
Exemple #25
0
def add_appointment(appointment_data, user_sk, teos_id, teos_url):
    """
    Manages the add_appointment command.

    The life cycle of the function is as follows:
        - Check that the given commitment_txid is correct (proper format and not missing)
        - Check that the transaction is correct (not missing)
        - Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx
        - Sign the appointment
        - Send the appointment to the tower
        - Wait for the response
        - Check the tower's response and signature

    Args:
        appointment_data (:obj:`dict`): a dictionary containing the appointment data.
        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 (`:obj:Appointment <common.appointment.Appointment>`, :obj:`str`) containing the
        appointment and the tower's signature.

    Raises:
        :obj:`InvalidParameter <cli.exceptions.InvalidParameter>`: if `appointment_data` or any of its fields is
        invalid.
        :obj:`ValueError`: if the appointment cannot be signed.
        :obj:`ConnectionError`: if the client cannot connect to the tower.
        :obj:`TowerResponseError <cli.exceptions.TowerResponseError>`: if the tower responded with an error, or the
        response was invalid.
    """

    if not appointment_data:
        raise InvalidParameter("The provided appointment JSON is empty")

    tx_id = appointment_data.get("tx_id")
    tx = appointment_data.get("tx")

    if not is_256b_hex_str(tx_id):
        raise InvalidParameter("The provided locator is wrong or missing")

    if not tx:
        raise InvalidParameter("The provided data is missing the transaction")

    appointment_data["locator"] = compute_locator(tx_id)
    appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id)
    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.
    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")
    # 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.serialize(), 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(response.get("start_block")))

    return appointment, tower_signature
Exemple #26
0
def generate_dummy_appointment(real_height=True, start_time_offset=5, end_time_offset=30):
    appointment_data, dispute_tx = generate_dummy_appointment_data(
        real_height=real_height, start_time_offset=start_time_offset, end_time_offset=end_time_offset
    )

    return Appointment.from_dict(appointment_data["appointment"]), dispute_tx
Exemple #27
0
def add_appointment(args, teos_url, config):
    """
    Manages the add_appointment command, from argument parsing, trough sending the appointment to the tower, until
    saving the appointment receipt.

    The life cycle of the function is as follows:
        - Load the add_appointment arguments
        - Check that the given commitment_txid is correct (proper format and not missing)
        - Check that the transaction is correct (not missing)
        - Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx
        - Load the client private key and sign the appointment
        - Send the appointment to the tower
        - Wait for the response
        - Check the tower's response and signature
        - Store the receipt (appointment + signature) on disk

    If any of the above-mentioned steps fails, the method returns false, otherwise it returns true.

    Args:
        args (:obj:`list`): a list of arguments to pass to ``parse_add_appointment_args``. Must contain a json encoded
            appointment, or the file option and the path to a file containing a json encoded appointment.
        teos_url (:obj:`str`): the teos base url.
        config (:obj:`dict`): a config dictionary following the format of :func:`create_config_dict <common.config_loader.ConfigLoader.create_config_dict>`.

    Returns:
        :obj:`bool`: True if the appointment is accepted by the tower and the receipt is properly stored, false if any
        error occurs during the process.
    """

    # Currently the base_url is the same as the add_appointment_endpoint
    add_appointment_endpoint = teos_url

    teos_pk, cli_sk, cli_pk_der = load_keys(config.get("TEOS_PUBLIC_KEY"),
                                            config.get("CLI_PRIVATE_KEY"),
                                            config.get("CLI_PUBLIC_KEY"))

    try:
        hex_pk_der = binascii.hexlify(cli_pk_der)

    except binascii.Error as e:
        logger.error("Could not successfully encode public key as hex",
                     error=str(e))
        return False

    if teos_pk is None:
        return False

    # Get appointment data from user.
    appointment_data = parse_add_appointment_args(args)

    if appointment_data is None:
        logger.error("The provided appointment JSON is empty")
        return False

    valid_txid = check_sha256_hex_format(appointment_data.get("tx_id"))

    if not valid_txid:
        logger.error("The provided txid is not valid")
        return False

    tx_id = appointment_data.get("tx_id")
    tx = appointment_data.get("tx")

    if None not in [tx_id, tx]:
        appointment_data["locator"] = compute_locator(tx_id)
        appointment_data["encrypted_blob"] = Cryptographer.encrypt(
            Blob(tx), tx_id)

    else:
        logger.error("Appointment data is missing some fields")
        return False

    appointment = Appointment.from_dict(appointment_data)
    signature = Cryptographer.sign(appointment.serialize(), cli_sk)

    if not (appointment and signature):
        return False

    data = {
        "appointment": appointment.to_dict(),
        "signature": signature,
        "public_key": hex_pk_der.decode("utf-8")
    }

    # Send appointment to the server.
    server_response = post_appointment(data, add_appointment_endpoint)
    if server_response is None:
        return False

    response_json = process_post_appointment_response(server_response)

    if response_json is None:
        return False

    signature = response_json.get("signature")
    # Check that the server signed the appointment as it should.
    if signature is None:
        logger.error(
            "The response does not contain the signature of the appointment")
        return False

    rpk = Cryptographer.recover_pk(appointment.serialize(), signature)
    if not Cryptographer.verify_rpk(teos_pk, rpk):
        logger.error("The returned appointment's signature is invalid")
        return False

    logger.info("Appointment accepted and signed by the Eye of Satoshi")

    # All good, store appointment and signature
    return save_appointment_receipt(appointment.to_dict(), signature, config)
Exemple #28
0
    "tx": get_random_value_hex(192),
    "tx_id": get_random_value_hex(32),
    "to_self_delay": 200
}

# This is the format appointment turns into once it hits "add_appointment"
dummy_appointment_dict = {
    "locator":
    compute_locator(dummy_appointment_data.get("tx_id")),
    "to_self_delay":
    dummy_appointment_data.get("to_self_delay"),
    "encrypted_blob":
    Cryptographer.encrypt(dummy_appointment_data.get("tx"),
                          dummy_appointment_data.get("tx_id")),
}
dummy_appointment = Appointment.from_dict(dummy_appointment_dict)

dummy_user_data = {
    "appointments": [],
    "available_slots": 100,
    "subscription_expiry": 7000
}

# The height is never checked in the tests, so we can make it up
CURRENT_HEIGHT = 300


@pytest.fixture
def keyfiles():
    # generate a private/public key pair, and an empty file, and return their names