Esempio n. 1
0
def test_add_appointment_multiple_times_different_users(
        internal_api,
        client,
        appointment,
        block_processor,
        n=MULTIPLE_APPOINTMENTS):
    # If the same appointment comes from different users, all are kept
    # Create user keys and appointment signatures
    user_keys = [generate_keypair() for _ in range(n)]
    signatures = [
        Cryptographer.sign(appointment.serialize(), key[0])
        for key in user_keys
    ]
    tmp_user_ids = [Cryptographer.get_compressed_pk(pk) for _, pk in user_keys]

    # Add one slot per public key
    for pair in user_keys:
        user_id = Cryptographer.get_compressed_pk(pair[1])
        internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo(
            available_slots=1,
            subscription_expiry=block_processor.get_block_count() + 1)

    # Send the appointments
    for compressed_pk, signature in zip(tmp_user_ids, signatures):
        r = add_appointment(client, {
            "appointment": appointment.to_dict(),
            "signature": signature
        }, compressed_pk)
        assert r.status_code == HTTP_OK
        assert r.json.get("available_slots") == 0
        assert r.json.get("start_block") == block_processor.get_block_count()

    # Check that all the appointments have been added and that there are no duplicates
    assert len(set(
        internal_api.watcher.locator_uuid_map[appointment.locator])) == n
Esempio n. 2
0
def test_add_appointment_in_cache_invalid_transaction(
        watcher, generate_dummy_appointment):
    # Generate an appointment that cannot be decrypted and add the dispute txid to the cache
    user_sk, user_pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(
        available_slots=1,
        subscription_expiry=watcher.block_processor.get_block_count() + 1)

    appointment, dispute_tx = generate_dummy_appointment()
    appointment.encrypted_blob = appointment.encrypted_blob[::-1]
    dispute_txid = watcher.block_processor.decode_raw_transaction(
        dispute_tx).get("txid")
    watcher.locator_cache.cache[appointment.locator] = dispute_txid

    # Try to add the appointment
    user_signature = Cryptographer.sign(appointment.serialize(), user_sk)
    response = watcher.add_appointment(appointment, user_signature)
    appointment_receipt = receipts.create_appointment_receipt(
        user_signature, response.get("start_block"))

    # The appointment is accepted but dropped (same as an invalid appointment that gets triggered)
    assert (response and response.get("locator") == appointment.locator
            and Cryptographer.get_compressed_pk(watcher.signing_key.public_key)
            == Cryptographer.get_compressed_pk(
                Cryptographer.recover_pk(appointment_receipt,
                                         response.get("signature"))))

    assert not watcher.locator_uuid_map.get(appointment.locator)
    assert appointment.locator not in [
        tracker.get("locator")
        for tracker in watcher.responder.trackers.values()
    ]
def test_add_too_many_appointments(watcher):
    # Simulate the user is registered
    user_sk, user_pk = generate_keypair()
    available_slots = 100
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=available_slots, subscription_expiry=10)

    # Appointments on top of the limit should be rejected
    watcher.appointments = dict()

    for i in range(MAX_APPOINTMENTS):
        appointment, dispute_tx = generate_dummy_appointment()
        appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk)

        response = watcher.add_appointment(appointment, appointment_signature)
        assert response.get("locator") == appointment.locator
        assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(appointment.serialize(), response.get("signature"))
        )
        assert response.get("available_slots") == available_slots - (i + 1)

    with pytest.raises(AppointmentLimitReached):
        appointment, dispute_tx = generate_dummy_appointment()
        appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk)
        watcher.add_appointment(appointment, appointment_signature)
Esempio n. 4
0
def test_add_too_many_appointments(watcher, generate_dummy_appointment,
                                   monkeypatch):
    # Adding appointment beyond the user limit should fail

    # Mock the user being registered
    expiry = 100
    user_info = UserInfo(MAX_APPOINTMENTS, expiry)
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: user_id)
    monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired",
                        lambda x: (False, expiry))
    monkeypatch.setattr(watcher.gatekeeper, "get_user_info",
                        lambda x: user_info)

    for i in range(user_info.available_slots):
        appointment = generate_dummy_appointment()
        response = watcher.add_appointment(appointment,
                                           appointment.user_signature)
        appointment_receipt = receipts.create_appointment_receipt(
            appointment.user_signature, response.get("start_block"))

        assert response.get("locator") == appointment.locator
        assert Cryptographer.get_compressed_pk(
            watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
                Cryptographer.recover_pk(appointment_receipt,
                                         response.get("signature")))

    with pytest.raises(AppointmentLimitReached):
        appointment = generate_dummy_appointment()
        appointment_signature = Cryptographer.sign(appointment.serialize(),
                                                   user_sk)
        watcher.add_appointment(appointment, appointment_signature)
def test_add_appointment_in_cache(watcher):
    # Generate an appointment and add the dispute txid to the cache
    user_sk, user_pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=10)

    appointment, dispute_tx = generate_dummy_appointment()
    dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid")
    watcher.locator_cache.cache[appointment.locator] = dispute_txid

    # Try to add the appointment
    response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))

    # The appointment is accepted but it's not in the Watcher
    assert (
        response
        and response.get("locator") == appointment.locator
        and Cryptographer.get_compressed_pk(watcher.signing_key.public_key)
        == Cryptographer.get_compressed_pk(Cryptographer.recover_pk(appointment.serialize(), response.get("signature")))
    )
    assert not watcher.locator_uuid_map.get(appointment.locator)

    # It went to the Responder straightaway
    assert appointment.locator in [tracker.get("locator") for tracker in watcher.responder.trackers.values()]

    # Trying to send it again should fail since it is already in the Responder
    with pytest.raises(AppointmentAlreadyTriggered):
        watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))
Esempio n. 6
0
def test_add_appointment(watcher, generate_dummy_appointment):
    # Simulate the user is registered
    user_sk, user_pk = generate_keypair()
    available_slots = 100
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(
        available_slots=available_slots,
        subscription_expiry=watcher.block_processor.get_block_count() + 1)

    appointment, dispute_tx = generate_dummy_appointment()
    appointment_signature = Cryptographer.sign(appointment.serialize(),
                                               user_sk)

    response = watcher.add_appointment(appointment, appointment_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment_signature, response.get("start_block")),
                response.get("signature"),
            ))
    assert response.get("available_slots") == available_slots - 1

    # Check that we can also add an already added appointment (same locator)
    response = watcher.add_appointment(appointment, appointment_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment_signature, response.get("start_block")),
                response.get("signature"),
            ))
    # The slot count should not have been reduced and only one copy is kept.
    assert response.get("available_slots") == available_slots - 1
    assert len(watcher.locator_uuid_map[appointment.locator]) == 1

    # If two appointments with the same locator come from different users, they are kept.
    another_user_sk, another_user_pk = generate_keypair()
    another_user_id = Cryptographer.get_compressed_pk(another_user_pk)
    watcher.gatekeeper.registered_users[another_user_id] = UserInfo(
        available_slots=available_slots,
        subscription_expiry=watcher.block_processor.get_block_count() + 1)

    appointment_signature = Cryptographer.sign(appointment.serialize(),
                                               another_user_sk)
    response = watcher.add_appointment(appointment, appointment_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment_signature, response.get("start_block")),
                response.get("signature"),
            ))
    assert response.get("available_slots") == available_slots - 1
    assert len(watcher.locator_uuid_map[appointment.locator]) == 2
Esempio n. 7
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_add_appointment_trigger_on_cache_cannot_decrypt(bitcoin_cli):
    commitment_tx, penalty_tx = create_txs(bitcoin_cli)

    # Let's send the commitment to the network and mine a block
    broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, bitcoin_cli.getnewaddress())
    sleep(1)

    # The appointment data is built using a random 32-byte value.
    appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx)

    # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually.
    appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid"))
    appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32))
    appointment = Appointment.from_dict(appointment_data)

    signature = Cryptographer.sign(appointment.serialize(), user_sk)
    data = {"appointment": appointment.to_dict(), "signature": signature}

    # Send appointment to the server.
    response = teos_cli.post_request(data, teos_add_appointment_endpoint)
    response_json = teos_cli.process_post_response(response)

    # Check that the server has accepted the appointment
    signature = response_json.get("signature")
    rpk = Cryptographer.recover_pk(appointment.serialize(), signature)
    assert teos_id == Cryptographer.get_compressed_pk(rpk)
    assert response_json.get("locator") == appointment.locator

    # The appointment should should have been inmediately dropped
    with pytest.raises(TowerResponseError):
        get_appointment_info(appointment_data["locator"])
def test_get_tower_info_empty(clear_state, internal_api, stub):
    response = stub.get_tower_info(Empty())
    assert isinstance(response, GetTowerInfoResponse)
    assert response.tower_id == Cryptographer.get_compressed_pk(teos_pk)
    assert response.n_registered_users == 0
    assert response.n_watcher_appointments == 0
    assert response.n_responder_trackers == 0
Esempio n. 10
0
def load_keys(user_sk_path):
    """
    Loads all the user private key and id.

    Args:
        user_sk_path (:obj:`str`): path to the user's private key file.

    Returns:
        :obj:`tuple`: A tuple containing a :obj:`PrivateKey` and a :obj:`str` representing the user sk and user id
        (compressed pk) respectively.

    Raises:
        :obj:`InvalidKey`: if any of the keys is invalid or cannot be loaded.
    """

    if not user_sk_path:
        raise InvalidKey(
            "Client's private key file not found. Please check your settings")

    try:
        user_sk_der = Cryptographer.load_key_file(user_sk_path)
        user_sk = Cryptographer.load_private_key_der(user_sk_der)

    except (InvalidParameter, InvalidKey):
        raise InvalidKey("Client private key is invalid or cannot be parsed")

    try:
        user_id = Cryptographer.get_compressed_pk(user_sk.public_key)

    except (InvalidParameter, InvalidKey):
        raise InvalidKey("Client public key cannot be loaded")

    return user_sk, user_id
Esempio n. 11
0
def generate_keys(data_dir):
    """
    Generates a key pair for the client.

    Args:
        data_dir (:obj:`str`): path to data directory where the keys will be stored.

    Returns:
        :obj:`tuple`: a tuple containing a ``PrivateKey`` and a ``str`` representing the client sk and compressed pk
        respectively.

    Raises:
        :obj:`FileExistsError`: if the key pair already exists in the given directory.
    """

    # Create the output folder it it does not exist (and all the parents if they don't either)
    Path(data_dir).mkdir(parents=True, exist_ok=True)
    sk_file_name = os.path.join(data_dir, "sk.der")

    if os.path.exists(sk_file_name):
        raise FileExistsError("The client key pair already exists")

    sk = PrivateKey()
    pk = sk.public_key
    save_key(sk, sk_file_name)

    return sk, Cryptographer.get_compressed_pk(pk)
Esempio n. 12
0
def test_register(api, client, monkeypatch):
    # Tests registering a user within the tower

    # Monkeypatch the response from the InternalAPI so the user is accepted
    slots = config.get("SUBSCRIPTION_SLOTS")
    expiry = config.get("SUBSCRIPTION_DURATION")
    receipt = receipts.create_registration_receipt(user_id, slots, expiry)
    signature = Cryptographer.sign(receipt, teos_sk)
    response = RegisterResponse(
        user_id=user_id,
        available_slots=slots,
        subscription_expiry=expiry,
        subscription_signature=signature,
    )
    monkeypatch.setattr(api.stub, "register", lambda x: response)

    #  Send the register request
    data = {"public_key": user_id}
    r = client.post(register_endpoint, json=data)

    # Check the reply
    assert r.status_code == HTTP_OK
    assert r.json.get("public_key") == user_id
    assert r.json.get("available_slots") == config.get("SUBSCRIPTION_SLOTS")
    assert r.json.get("subscription_expiry") == config.get(
        "SUBSCRIPTION_DURATION")
    rpk = Cryptographer.recover_pk(receipt,
                                   r.json.get("subscription_signature"))
    assert Cryptographer.get_compressed_pk(rpk) == teos_id
def test_appointment_wrong_decryption_key(bitcoin_cli):
    # This tests an appointment encrypted with a key that has not been derived from the same source as the locator.
    # Therefore the tower won't be able to decrypt the blob once the appointment is triggered.
    commitment_tx, penalty_tx = create_txs(bitcoin_cli)

    # The appointment data is built using a random 32-byte value.
    appointment_data = build_appointment_data(get_random_value_hex(32), penalty_tx)

    # We cannot use teos_cli.add_appointment here since it computes the locator internally, so let's do it manually.
    # We will encrypt the blob using the random value and derive the locator from the commitment tx.
    appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid"))
    appointment_data["encrypted_blob"] = Cryptographer.encrypt(penalty_tx, get_random_value_hex(32))
    appointment = Appointment.from_dict(appointment_data)

    signature = Cryptographer.sign(appointment.serialize(), user_sk)
    data = {"appointment": appointment.to_dict(), "signature": signature}

    # Send appointment to the server.
    response = teos_cli.post_request(data, teos_add_appointment_endpoint)
    response_json = teos_cli.process_post_response(response)

    # Check that the server has accepted the appointment
    signature = response_json.get("signature")
    rpk = Cryptographer.recover_pk(appointment.serialize(), signature)
    assert teos_id == Cryptographer.get_compressed_pk(rpk)
    assert response_json.get("locator") == appointment.locator

    # Trigger the appointment
    new_addr = bitcoin_cli.getnewaddress()
    broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr)

    # The appointment should have been removed since the decryption failed.
    with pytest.raises(TowerResponseError):
        get_appointment_info(appointment.locator)
Esempio n. 14
0
def load_teos_id(teos_pk_path):
    """
    Loads the tower id from disk.

    Args:
        teos_pk_path (:obj:`str`): path to the tower's public key file.

    Returns:
        :obj:`str`: The tower id.

    Raises:
        :obj:`InvalidKey`: if the public key is invalid or cannot be loaded.
    """

    if not teos_pk_path:
        raise InvalidKey(
            "TEOS's public key file not found. Have you registered with the tower?"
        )

    try:
        teos_id = Cryptographer.get_compressed_pk(
            PublicKey(Cryptographer.load_key_file(teos_pk_path)))

    except (InvalidParameter, InvalidKey, ValueError):
        raise InvalidKey(
            "TEOS public key cannot be loaded. Try registering again")

    return teos_id
Esempio n. 15
0
    def authenticate_user(self, message, signature):
        """
        Checks if a request comes from a registered user by ec-recovering their public key from a signed message.

        Args:
            message (:obj:`bytes`): byte representation of the original message from where the signature was generated.
            signature (:obj:`str`): the user's signature (hex-encoded).

        Returns:
            :obj:`str`: a compressed key recovered from the signature and matching a registered user.

        Raises:
            :obj:`AuthenticationFailure`: if the user cannot be authenticated.
        """

        try:
            rpk = Cryptographer.recover_pk(message, signature)
            user_id = Cryptographer.get_compressed_pk(rpk)

            if user_id in self.registered_users:
                return user_id
            else:
                raise AuthenticationFailure("User not found.")

        except (InvalidParameter, InvalidKey, SignatureError):
            raise AuthenticationFailure("Wrong message or signature.")
Esempio n. 16
0
def load_keys(data_dir):
    """
    Loads a the client key pair.

    Args:
        data_dir (:obj:`str`): path to data directory where the keys are stored.

    Returns:
        :obj:`tuple`: a tuple containing a ``PrivateKey`` and a ``str`` representing the client sk and compressed pk
        respectively.

    Raises:
        :obj:`InvalidKey <cli.exceptions.InvalidKey>`: if any of the keys is invalid or cannot be loaded.
    """

    if not isinstance(data_dir, str):
        raise ValueError("Invalid data_dir. Please check your settings")

    sk_file_path = os.path.join(data_dir, "sk.der")

    cli_sk_der = Cryptographer.load_key_file(sk_file_path)
    cli_sk = Cryptographer.load_private_key_der(cli_sk_der)

    if cli_sk is None:
        raise InvalidKey("Client private key is invalid or cannot be parsed")

    compressed_cli_pk = Cryptographer.get_compressed_pk(cli_sk.public_key)

    if compressed_cli_pk is None:
        raise InvalidKey("Client public key cannot be loaded")

    return cli_sk, compressed_cli_pk
Esempio n. 17
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()
    ]
Esempio n. 18
0
def test_add_appointment_in_cache(watcher,
                                  generate_dummy_appointment_w_trigger,
                                  monkeypatch):
    # Adding an appointment which trigger is in the cache should be accepted
    appointment, commitment_txid = generate_dummy_appointment_w_trigger()
    # We need the blob and signature to be valid
    appointment.user_signature = Cryptographer.sign(
        appointment.encrypted_blob.encode(), user_sk)

    # Mock the transaction being in the cache and all the way until sending it to the Responder
    expiry = 100
    user_info = UserInfo(MAX_APPOINTMENTS, expiry)
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: user_id)
    monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired",
                        lambda x: (False, expiry))
    monkeypatch.setattr(watcher.gatekeeper, "get_user_info",
                        lambda x: user_info)
    monkeypatch.setattr(watcher.locator_cache, "get_txid",
                        lambda x: commitment_txid)
    monkeypatch.setattr(watcher.responder, "handle_breach", mock_receipt_true)

    # Try to add the appointment
    # user_signature = Cryptographer.sign(appointment.serialize(), user_sk)
    response = watcher.add_appointment(appointment, appointment.user_signature)
    appointment_receipt = receipts.create_appointment_receipt(
        appointment.user_signature, response.get("start_block"))

    # The appointment is accepted but it's not in the Watcher
    assert (response and response.get("locator") == appointment.locator
            and Cryptographer.get_compressed_pk(watcher.signing_key.public_key)
            == Cryptographer.get_compressed_pk(
                Cryptographer.recover_pk(appointment_receipt,
                                         response.get("signature"))))
    assert not watcher.locator_uuid_map.get(appointment.locator)

    # It went to the Responder straightaway, we can check this by querying the database
    for uuid, db_appointment in watcher.db_manager.load_watcher_appointments(
            include_triggered=True).items():
        if db_appointment.get("locator") == appointment.locator:
            assert uuid in watcher.db_manager.load_all_triggered_flags()

    # Trying to send it again should fail since it is already in the Responder
    monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: True)
    with pytest.raises(AppointmentAlreadyTriggered):
        watcher.add_appointment(
            appointment, Cryptographer.sign(appointment.serialize(), user_sk))
Esempio n. 19
0
def load_keys(teos_pk_path, user_sk_path):
    """
    Loads all the keys required so sign, send, and verify the appointment.

    Args:
        teos_pk_path (:obj:`str`): path to the tower's public key file.
        user_sk_path (:obj:`str`): path to the user's private key file.

    Returns:
        :obj:`tuple`: a three-item tuple containing a ``str``, a ``PrivateKey`` and a ``str``
        representing the tower id (compressed pk), user sk and user id (compressed pk) respectively.

    Raises:
        :obj:`InvalidKey <cli.exceptions.InvalidKey>`: if any of the keys is invalid or cannot be loaded.
    """

    if not teos_pk_path:
        raise InvalidKey(
            "TEOS's public key file not found. Please check your settings")

    if not user_sk_path:
        raise InvalidKey(
            "Client's private key file not found. Please check your settings")

    try:
        teos_pk_der = Cryptographer.load_key_file(teos_pk_path)
        teos_id = Cryptographer.get_compressed_pk(PublicKey(teos_pk_der))

    except (InvalidParameter, InvalidKey, ValueError):
        raise InvalidKey("TEOS public key cannot be loaded")

    try:
        user_sk_der = Cryptographer.load_key_file(user_sk_path)
        user_sk = Cryptographer.load_private_key_der(user_sk_der)

    except (InvalidParameter, InvalidKey):
        raise InvalidKey("Client private key is invalid or cannot be parsed")

    try:
        user_id = Cryptographer.get_compressed_pk(user_sk.public_key)

    except (InvalidParameter, InvalidKey):
        raise InvalidKey("Client public key cannot be loaded")

    return teos_id, user_sk, user_id
def test_recover_pk_ground_truth():
    # Use a message a signature generated by c-lightning and see if we recover the proper key
    message = b"Test message"
    org_pk = "02b821c749295d5c24f6166ae77d8353eaa36fc4e47326670c6d2522cbd344bab9"
    zsig = "rbwewwyr4zem3w5t39fd1xyeamfzbmfgztwm4b613ybjtmoeod5kazaxqo3akn3ae75bqi3aqeds8cs6n43w4p58ft34itjnnb61bp54"

    rpk = Cryptographer.recover_pk(message, zsig)

    assert org_pk == Cryptographer.get_compressed_pk(rpk)
Esempio n. 21
0
def test_add_appointment(watcher, generate_dummy_appointment, monkeypatch):
    # A registered user with no subscription issues should be able to add an appointment
    appointment = generate_dummy_appointment()

    # Mock a registered user
    expiry = 100
    user_info = UserInfo(MAX_APPOINTMENTS, expiry)
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: user_id)
    monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired",
                        lambda x: (False, expiry))
    monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: False)
    monkeypatch.setattr(watcher.gatekeeper, "add_update_appointment",
                        lambda x, y, z: MAX_APPOINTMENTS - 1)
    monkeypatch.setattr(watcher.gatekeeper, "get_user_info",
                        lambda x: user_info)

    response = watcher.add_appointment(appointment, appointment.user_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment.user_signature, response.get("start_block")),
                response.get("signature"),
            ))

    # Check that we can also add an already added appointment (same locator)
    response = watcher.add_appointment(appointment, appointment.user_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment.user_signature, response.get("start_block")),
                response.get("signature"),
            ))
    # One one copy is kept since the appointments were the same
    # (the slot count should have not been reduced, but that's something to be tested in the Gatekeeper)
    assert len(watcher.locator_uuid_map[appointment.locator]) == 1

    # If two appointments with the same locator come from different users, they are kept.
    another_user_sk, another_user_pk = generate_keypair()
    another_user_id = Cryptographer.get_compressed_pk(another_user_pk)
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: another_user_id)

    appointment_signature = Cryptographer.sign(appointment.serialize(),
                                               another_user_sk)
    response = watcher.add_appointment(appointment, appointment_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment_signature, response.get("start_block")),
                response.get("signature"),
            ))
    assert len(watcher.locator_uuid_map[appointment.locator]) == 2
Esempio n. 22
0
def test_add_appointment_no_slots(watcher):
    # Appointments from register users with no available slots should aso fail
    user_sk, user_pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=0, subscription_expiry=10)

    appointment, dispute_tx = generate_dummy_appointment()
    appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk)

    with pytest.raises(NotEnoughSlots):
        watcher.add_appointment(appointment, appointment_signature)
def test_sign_ground_truth():
    # Generate a signature that has been verified by c-lightning.
    raw_sk = "24e9a981580d27d9277071a8381542e89a7c124868c4e862a13595dc75c6922f"
    sk = PrivateKey.from_hex(raw_sk)

    c_lightning_rpk = "0235293db86c6aaa74aff69ebacad8471d5242901ea9f6a0341a8dca331875e62c"
    message = b"Test message"

    sig = Cryptographer.sign(message, sk)
    rpk = Cryptographer.recover_pk(message, sig)

    assert c_lightning_rpk == Cryptographer.get_compressed_pk(rpk)
Esempio n. 24
0
def test_identify_user(gatekeeper):
    # Identify user should return a user_pk for registered users. It raises
    # IdentificationFailure for invalid parameters or non-registered users.

    # Let's first register a user
    sk, pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(pk)
    gatekeeper.add_update_user(user_id)

    message = "Hey, it's me"
    signature = Cryptographer.sign(message.encode(), sk)

    assert gatekeeper.authenticate_user(message.encode(), signature) == user_id
Esempio n. 25
0
def test_add_appointment_not_registered(internal_api, client, appointment):
    # Properly formatted appointment, user is not registered
    tmp_sk, tmp_pk = generate_keypair()
    tmp_user_id = Cryptographer.get_compressed_pk(tmp_pk)

    appointment_signature = Cryptographer.sign(appointment.serialize(), tmp_sk)
    r = add_appointment(client, {
        "appointment": appointment.to_dict(),
        "signature": appointment_signature
    }, tmp_user_id)
    assert r.status_code == HTTP_BAD_REQUEST
    assert errors.APPOINTMENT_INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR == r.json.get(
        "error_code")
Esempio n. 26
0
def test_add_appointment_in_cache_invalid_blob_or_tx(
        watcher, generate_dummy_appointment_w_trigger, monkeypatch):
    # Trying to add an appointment with invalid data (blob does not decrypt to a tx or the tx in not invalid) with a
    # trigger in the cache will be accepted, but the data will de dropped.

    appointment, commitment_txid = generate_dummy_appointment_w_trigger()

    # Mock the trigger being in the cache
    expiry = 100
    user_info = UserInfo(MAX_APPOINTMENTS, expiry)
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: user_id)
    monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired",
                        lambda x: (False, expiry))
    monkeypatch.setattr(watcher.gatekeeper, "get_user_info",
                        lambda x: user_info)
    monkeypatch.setattr(watcher.locator_cache, "get_txid",
                        lambda x: commitment_txid)

    # Check for both the blob being invalid, and the transaction being invalid
    for mocked_return in [raise_encryption_error, raise_invalid_tx_format]:
        # Mock the data check (invalid blob)
        monkeypatch.setattr(watcher.responder, "handle_breach", mocked_return)

        # Try to add the appointment
        response = watcher.add_appointment(appointment,
                                           appointment.user_signature)
        appointment_receipt = receipts.create_appointment_receipt(
            appointment.user_signature, response.get("start_block"))

        # The appointment is accepted but dropped
        assert (response and response.get("locator") == appointment.locator and
                Cryptographer.get_compressed_pk(watcher.signing_key.public_key)
                == Cryptographer.get_compressed_pk(
                    Cryptographer.recover_pk(appointment_receipt,
                                             response.get("signature"))))

        # Check the appointment didn't go to the Responder (by checking there are no triggered flags)
        assert watcher.db_manager.load_all_triggered_flags() == []
Esempio n. 27
0
def add_appointment(appointment, user_sk, teos_id, teos_url):
    """
    Manages the add_appointment command. The life cycle of the function is as follows:
        - Sign the appointment
        - Send the appointment to the tower
        - Wait for the response
        - Check the tower's response and signature

    Args:
        appointment (:obj:`Appointment <common.appointment.Appointment>`): an appointment object.
        user_sk (:obj:`PrivateKey`): the user's private key.
        teos_id (:obj:`str`): the tower's compressed public key.
        teos_url (:obj:`str`): the teos base url.

    Returns:
        :obj:`tuple`: A tuple containing the start block and the tower's signature of the appointment.

    Raises:
        :obj:`ValueError`: if the appointment cannot be signed.
        :obj:`ConnectionError`: if the client cannot connect to the tower.
        :obj:`TowerResponseError`: if the tower responded with an error, or the response was invalid.
    """

    signature = Cryptographer.sign(appointment.serialize(), user_sk)
    data = {"appointment": appointment.to_dict(), "signature": signature}

    # Send appointment to the server.
    logger.info("Sending appointment to the Eye of Satoshi")
    add_appointment_endpoint = "{}/add_appointment".format(teos_url)
    response = process_post_response(
        post_request(data, add_appointment_endpoint))

    tower_signature = response.get("signature")
    start_block = response.get("start_block")
    appointment_receipt = receipts.create_appointment_receipt(
        signature, start_block)
    # Check that the server signed the appointment as it should.
    if not tower_signature:
        raise TowerResponseError(
            "The response does not contain the signature of the appointment")

    rpk = Cryptographer.recover_pk(appointment_receipt, tower_signature)
    if teos_id != Cryptographer.get_compressed_pk(rpk):
        raise TowerResponseError(
            "The returned appointment's signature is invalid")

    logger.info("Appointment accepted and signed by the Eye of Satoshi")
    logger.info("Remaining slots: {}".format(response.get("available_slots")))
    logger.info("Start block: {}".format(start_block))

    return start_block, tower_signature
Esempio n. 28
0
def test_watchtower_multiple_towers(node_factory):
    """ Test sending data to multiple towers at the same time"""
    global mocked_return

    # Create the new tower
    another_tower_netaddr = "localhost"
    another_tower_port = "5678"
    another_tower_sk = PrivateKey()
    another_tower_id = Cryptographer.get_compressed_pk(
        another_tower_sk.public_key)

    another_tower = TowerMock(another_tower_sk)
    Thread(target=another_tower.app.run,
           kwargs={
               "host": another_tower_netaddr,
               "port": another_tower_port
           },
           daemon=True).start()

    l1, l2 = node_factory.line_graph(2,
                                     opts=[{
                                         "may_fail": True,
                                         "allow_broken_log": True
                                     }, {
                                         "plugin": plugin_path
                                     }])

    # Register a new tower
    l2.rpc.registertower("{}@{}:{}".format(another_tower_id,
                                           another_tower_netaddr,
                                           another_tower_port))

    # Make sure the tower in our list of towers
    tower_ids = [
        tower.get("id") for tower in l2.rpc.listtowers().get("towers")
    ]
    assert another_tower_id in tower_ids

    # Force a new commitment
    mocked_return = "success"
    l1.rpc.pay(l2.rpc.invoice(25000000, "lbl6", "desc")["bolt11"])

    # Check that both towers got it
    another_tower_appointments = l2.rpc.gettowerinfo(another_tower_id).get(
        "appointments")
    assert another_tower_appointments
    assert not l2.rpc.gettowerinfo(another_tower_id).get(
        "pending_appointments")
    assert set(another_tower_appointments).issubset(
        l2.rpc.gettowerinfo(tower_id).get("appointments"))
Esempio n. 29
0
def test_add_update_appointment(gatekeeper, generate_dummy_appointment):
    # add_update_appointment should decrease the slot count if a new appointment is added
    # let's add a new user
    sk, pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(pk)
    gatekeeper.add_update_user(user_id)

    # And now update add a new appointment
    appointment, _ = generate_dummy_appointment()
    appointment_uuid = get_random_value_hex(16)
    remaining_slots = gatekeeper.add_update_appointment(
        user_id, appointment_uuid, appointment)

    # This is a standard size appointment, so it should have reduced the slots by one
    assert appointment_uuid in gatekeeper.registered_users[
        user_id].appointments
    assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1

    # Updates can leave the count as is, decrease it, or increase it, depending on the appointment size (modulo
    # ENCRYPTED_BLOB_MAX_SIZE_HEX)

    # Appointments of the same size leave it as is
    appointment_same_size, _ = generate_dummy_appointment()
    remaining_slots = gatekeeper.add_update_appointment(
        user_id, appointment_uuid, appointment)
    assert appointment_uuid in gatekeeper.registered_users[
        user_id].appointments
    assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1

    # Bigger appointments decrease it
    appointment_x2_size = appointment_same_size
    appointment_x2_size.encrypted_blob = "A" * (ENCRYPTED_BLOB_MAX_SIZE_HEX +
                                                1)
    remaining_slots = gatekeeper.add_update_appointment(
        user_id, appointment_uuid, appointment_x2_size)
    assert appointment_uuid in gatekeeper.registered_users[
        user_id].appointments
    assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 2

    # Smaller appointments increase it
    remaining_slots = gatekeeper.add_update_appointment(
        user_id, appointment_uuid, appointment)
    assert remaining_slots == config.get("SUBSCRIPTION_SLOTS") - 1

    # If the appointment needs more slots than there's free, it should fail
    gatekeeper.registered_users[user_id].available_slots = 1
    appointment_uuid = get_random_value_hex(16)
    with pytest.raises(NotEnoughSlots):
        gatekeeper.add_update_appointment(user_id, appointment_uuid,
                                          appointment_x2_size)
Esempio n. 30
0
def test_get_users(teosd, rpc_client):
    _, teos_id = teosd

    # Create a fresh user
    tmp_user_id = Cryptographer.get_compressed_pk(Cryptographer.generate_key().public_key)

    users = json.loads(rpc_client.get_users())
    assert tmp_user_id not in users

    # Register the fresh user
    teos_client.register(tmp_user_id, teos_id, teos_base_endpoint)

    users = json.loads(rpc_client.get_users())
    assert tmp_user_id in users