def test_add_appointment(watcher, generate_dummy_appointment, monkeypatch):
    # A registered user with no subscription issues should be able to add an appointment
    appointment = generate_dummy_appointment()

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

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

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

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

    appointment_signature = Cryptographer.sign(appointment.serialize(),
                                               another_user_sk)
    response = watcher.add_appointment(appointment, appointment_signature)
    assert response.get("locator") == appointment.locator
    assert Cryptographer.get_compressed_pk(
        watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(
                receipts.create_appointment_receipt(
                    appointment_signature, response.get("start_block")),
                response.get("signature"),
            ))
    assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def test_recover_pk_invalid_sigrec():
    message = "Hey, it's me".encode("utf-8")
    signature = "ddbfb019e4d56155b4175066c2b615ab765d317ae7996d188b4a5fae4cc394adf98fef46034d0553149392219ca6d37dca9abdfa6366a8e54b28f19d3e5efa8a14b556205dc7f33a"

    # The given signature, when zbase32 decoded, has a fist byte with value lower than 31.
    # The first byte of the signature should be 31 + SigRec, so this should fail
    with pytest.raises(SignatureError, match="Wrong SigRec"):
        Cryptographer.recover_pk(message, signature)
Beispiel #3
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
Beispiel #4
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()
    ]
Beispiel #5
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_too_many_appointments(watcher, generate_dummy_appointment,
                                   monkeypatch):
    # Adding appointment beyond the user limit should fail

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

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

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

    with pytest.raises(AppointmentLimitReached):
        appointment = generate_dummy_appointment()
        appointment_signature = Cryptographer.sign(appointment.serialize(),
                                                   user_sk)
        watcher.add_appointment(appointment, appointment_signature)
def test_appointment_wrong_decryption_key(bitcoin_cli):
    # This tests an appointment encrypted with a key that has not been derived from the same source as the locator.
    # Therefore the tower won't be able to decrypt the blob once the appointment is triggered.
    commitment_tx, penalty_tx = create_txs(bitcoin_cli)

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

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

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

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

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

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

    # The appointment should have been removed since the decryption failed.
    with pytest.raises(TowerResponseError):
        get_appointment_info(appointment.locator)
def test_add_appointment_in_cache(watcher):
    # Generate an appointment and add the dispute txid to the cache
    user_sk, user_pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=10)

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

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

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

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

    # Trying to send it again should fail since it is already in the Responder
    with pytest.raises(AppointmentAlreadyTriggered):
        watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))
Beispiel #9
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.")
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)
Beispiel #11
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_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"])
Beispiel #13
0
def test_verify_pk():
    sk, _ = generate_keypair()
    message = b"Test message"

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

    assert Cryptographer.verify_rpk(sk.public_key, rpk)
def test_recover_pk():
    sk, _ = generate_keypair()
    message = b"Test message"

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

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

    rpk = Cryptographer.recover_pk(message, zsig)

    assert org_pk == Cryptographer.get_compressed_pk(rpk)
Beispiel #16
0
def test_add_appointment(watcher):
    # We should be able to add appointments up to the limit
    for _ in range(10):
        appointment, dispute_tx = generate_dummy_appointment(
            start_time_offset=START_TIME_OFFSET,
            end_time_offset=END_TIME_OFFSET)
        added_appointment, sig = watcher.add_appointment(appointment)

        assert added_appointment is True
        assert Cryptographer.verify_rpk(
            watcher.signing_key.public_key,
            Cryptographer.recover_pk(appointment.serialize(), sig))

        # Check that we can also add an already added appointment (same locator)
        added_appointment, sig = watcher.add_appointment(appointment)

        assert added_appointment is True
        assert Cryptographer.verify_rpk(
            watcher.signing_key.public_key,
            Cryptographer.recover_pk(appointment.serialize(), sig))
def test_sign_ground_truth():
    # Generate a signature that has been verified by c-lightning.
    raw_sk = "24e9a981580d27d9277071a8381542e89a7c124868c4e862a13595dc75c6922f"
    sk = PrivateKey.from_hex(raw_sk)

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

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

    assert c_lightning_rpk == Cryptographer.get_compressed_pk(rpk)
Beispiel #18
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
Beispiel #19
0
def test_register(watcher, monkeypatch):
    # Register requests should work as long as the provided user_id is valid and bitcoind is reachable

    # Mock the interaction with the Gatekeeper
    slots = 100
    expiry = 200
    receipt = bytes.fromhex(get_random_value_hex(70))
    monkeypatch.setattr(watcher.gatekeeper, "add_update_user", lambda x:
                        (slots, expiry, receipt))

    # Request a registration and check the response
    available_slots, subscription_expiry, signature = watcher.register(user_id)
    assert available_slots == slots
    assert subscription_expiry == expiry
    assert Cryptographer.recover_pk(
        receipt, signature) == watcher.signing_key.public_key
Beispiel #20
0
def register(user_id, teos_id, teos_url):
    """
    Registers the user to the tower.

    Args:
        user_id (:obj:`str`): a 33-byte hex-encoded compressed public key representing the user.
        teos_id (:obj:`str`): the tower's compressed public key.
        teos_url (:obj:`str`): the teos base url.

    Returns:
        :obj:`tuple`: A tuple containing the available slots count and the subscription expiry.

    Raises:
        :obj:`InvalidParameter`: if `user_id` is invalid.
        :obj:`ConnectionError`: if the client cannot connect to the tower.
        :obj:`TowerResponseError`: if the tower responded with an error, or the
            response was invalid.
    """

    if not is_compressed_pk(user_id):
        raise InvalidParameter("The cli public key is not valid")

    # Send request to the server.
    register_endpoint = "{}/register".format(teos_url)
    data = {"public_key": user_id}

    logger.info("Registering in the Eye of Satoshi")
    response = process_post_response(post_request(data, register_endpoint))

    available_slots = response.get("available_slots")
    subscription_expiry = response.get("subscription_expiry")
    tower_signature = response.get("subscription_signature")

    # Check that the server signed the response as it should.
    if not tower_signature:
        raise TowerResponseError(
            "The response does not contain the signature of the subscription")

    # Check that the signature is correct.
    subscription_receipt = receipts.create_registration_receipt(
        user_id, available_slots, subscription_expiry)
    rpk = Cryptographer.recover_pk(subscription_receipt, tower_signature)
    if teos_id != Cryptographer.get_compressed_pk(rpk):
        raise TowerResponseError(
            "The returned appointment's signature is invalid")

    return available_slots, subscription_expiry
Beispiel #21
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()
    ]
Beispiel #22
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))
Beispiel #23
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
Beispiel #24
0
    def get_appointment(self):
        locator = request.get_json().get("locator")
        message = f"get appointment {locator}"
        user_id = Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(message.encode(),
                                     request.get_json().get("signature")))

        if (user_id in self.users and "appointments" in self.users[user_id]
                and locator in self.users[user_id]["appointments"]):
            rcode = constants.HTTP_OK
            response = self.users[user_id]["appointments"][locator]
            response["status"] = "being_watched"

        else:
            rcode = constants.HTTP_NOT_FOUND
            response = {"locator": locator, "status": "not_found"}

        return jsonify(response), rcode
Beispiel #25
0
def test_register(internal_api, client):
    # Tests registering a user within the tower
    current_height = internal_api.watcher.block_processor.get_block_count()
    data = {"public_key": user_id}
    r = client.post(register_endpoint, json=data)
    assert r.status_code == HTTP_OK
    assert r.json.get("public_key") == user_id
    assert r.json.get("available_slots") == config.get("SUBSCRIPTION_SLOTS")
    assert r.json.get("subscription_expiry"
                      ) == current_height + config.get("SUBSCRIPTION_DURATION")

    slots = r.json.get("available_slots")
    expiry = r.json.get("subscription_expiry")
    subscription_receipt = receipts.create_registration_receipt(
        user_id, slots, expiry)
    rpk = Cryptographer.recover_pk(subscription_receipt,
                                   r.json.get("subscription_signature"))
    assert Cryptographer.get_compressed_pk(rpk) == teos_id
def test_appointment_wrong_key(bitcoin_cli, create_txs):
    # This tests an appointment encrypted with a key that has not been derived from the same source as the locator.
    # Therefore the tower won't be able to decrypt the blob once the appointment is triggered.
    commitment_tx, penalty_tx = create_txs

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

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

    teos_pk, cli_sk, cli_pk_der = teos_cli.load_keys(
        cli_config.get("TEOS_PUBLIC_KEY"), cli_config.get("CLI_PRIVATE_KEY"), cli_config.get("CLI_PUBLIC_KEY")
    )
    hex_pk_der = binascii.hexlify(cli_pk_der)

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

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

    # Check that the server has accepted the appointment
    signature = response_json.get("signature")
    assert signature is not None
    rpk = Cryptographer.recover_pk(appointment.serialize(), signature)
    assert Cryptographer.verify_rpk(teos_pk, rpk) is True
    assert response_json.get("locator") == appointment.locator

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

    # The appointment should have been removed since the decryption failed.
    sleep(1)
    appointment_info = get_appointment_info(appointment.locator)

    assert appointment_info is not None
    assert len(appointment_info) == 1
    assert appointment_info[0].get("status") == "not_found"
Beispiel #27
0
def test_recover_pk_wrong_inputs():
    str_message = "Test message"
    message = bytes(20)
    str_sig = "aaaaaaaa"
    sig = bytes(20)

    # Wrong input type
    assert Cryptographer.recover_pk(message, str_sig) is None
    assert Cryptographer.recover_pk(str_message, str_sig) is None
    assert Cryptographer.recover_pk(str_message, sig) is None
    assert Cryptographer.recover_pk(message, str_sig) is None

    # Wrong input size or format
    assert Cryptographer.recover_pk(message, sig) is None
    assert Cryptographer.recover_pk(message, bytes(104)) is None
Beispiel #28
0
def test_add_too_many_appointments(watcher):
    # Any appointment on top of those should fail
    watcher.appointments = dict()

    for _ in range(config.get("MAX_APPOINTMENTS")):
        appointment, dispute_tx = generate_dummy_appointment(
            start_time_offset=START_TIME_OFFSET,
            end_time_offset=END_TIME_OFFSET)
        added_appointment, sig = watcher.add_appointment(appointment)

        assert added_appointment is True
        assert Cryptographer.verify_rpk(
            watcher.signing_key.public_key,
            Cryptographer.recover_pk(appointment.serialize(), sig))

    appointment, dispute_tx = generate_dummy_appointment(
        start_time_offset=START_TIME_OFFSET, end_time_offset=END_TIME_OFFSET)
    added_appointment, sig = watcher.add_appointment(appointment)

    assert added_appointment is False
    assert sig is None
Beispiel #29
0
    def get_appointment(self):
        locator = request.get_json().get("locator")
        message = f"get appointment {locator}"
        user_id = Cryptographer.get_compressed_pk(
            Cryptographer.recover_pk(message.encode("utf-8"),
                                     request.get_json().get("signature")))

        if (user_id in self.users and "appointments" in self.users[user_id]
                and locator in self.users[user_id]["appointments"]):
            rcode = constants.HTTP_OK
            response = self.users[user_id]["appointments"][locator]
            response["status"] = AppointmentStatus.BEING_WATCHED

        else:
            rcode = constants.HTTP_NOT_FOUND
            response = {
                "locator": locator,
                "status": AppointmentStatus.NOT_FOUND
            }

        return jsonify(response), rcode
def test_recover_pk_wrong_inputs():
    str_message = "Test message"
    message = bytes(20)
    str_sig = "aaaaaaaa"
    sig = bytes(20)

    # Wrong input type
    with pytest.raises(InvalidParameter,
                       match="Wrong value passed as zbase32_sig"):
        Cryptographer.recover_pk(message, sig)

    with pytest.raises(InvalidParameter,
                       match="Wrong value passed as message"):
        Cryptographer.recover_pk(str_message, str_sig)

    with pytest.raises(InvalidParameter,
                       match="Wrong value passed as message"):
        Cryptographer.recover_pk(str_message, sig)

    # Wrong input size
    with pytest.raises(SignatureError,
                       match="Serialized signature must be 65 bytes long"):
        Cryptographer.recover_pk(message, str_sig)