From 83b3913cb5ee59cc52b287b3462bcd23c2d08c33 Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Wed, 25 Mar 2020 17:13:35 +0100 Subject: [PATCH] Adds basic authentication logic. + Users need to be registered in order to send appointments (free registration for now) + The tower gives them a number of appointments to work with + Non-registered users and users with no enough appoitnemnts slots return the same error (to avoid proving) - Authentication does not cover get_* requests yet - No tests - No docs --- common/constants.py | 3 ++ common/cryptographer.py | 19 ++++++++++++ teos/api.py | 52 ++++++++++++++++++++++++------- teos/errors.py | 2 +- teos/gatekeeper.py | 69 +++++++++++++++++++++++++++++++++++++++-- teos/inspector.py | 62 ++---------------------------------- 6 files changed, 133 insertions(+), 74 deletions(-) diff --git a/common/constants.py b/common/constants.py index b577044..bf119aa 100644 --- a/common/constants.py +++ b/common/constants.py @@ -6,3 +6,6 @@ LOCATOR_LEN_BYTES = LOCATOR_LEN_HEX // 2 HTTP_OK = 200 HTTP_BAD_REQUEST = 400 HTTP_SERVICE_UNAVAILABLE = 503 + +# Temporary constants, may be changed +ENCRYPTED_BLOB_MAX_SIZE_HEX = 2 * 2048 diff --git a/common/cryptographer.py b/common/cryptographer.py index 3e65099..ecd47a8 100644 --- a/common/cryptographer.py +++ b/common/cryptographer.py @@ -315,3 +315,22 @@ class Cryptographer: """ return pk.point() == rpk.point() + + # TODO: UNITTEST + @staticmethod + def get_compressed_pk(pk): + """ + Computes a compressed, hex encoded, public key given a ``PublicKey`` object. + + Args: + pk(:obj:`PublicKey`): a given public key. + + Returns: + :obj:`str`: A compressed, hex encoded, public key (33-byte long) + """ + + if not isinstance(pk, PublicKey): + logger.error("The received data is not a PublicKey object") + return None + + return hexlify(pk.format(compressed=True)).decode("utf-8") diff --git a/teos/api.py b/teos/api.py index 8abf790..79a5317 100644 --- a/teos/api.py +++ b/teos/api.py @@ -1,5 +1,6 @@ import os import logging +from math import ceil from flask import Flask, request, abort, jsonify import teos.errors as errors @@ -7,7 +8,13 @@ from teos import HOST, PORT, LOG_PREFIX from common.logger import Logger from common.appointment import Appointment -from common.constants import HTTP_OK, HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, LOCATOR_LEN_HEX +from common.constants import ( + HTTP_OK, + HTTP_BAD_REQUEST, + HTTP_SERVICE_UNAVAILABLE, + LOCATOR_LEN_HEX, + ENCRYPTED_BLOB_MAX_SIZE_HEX, +) # ToDo: #5-add-async-to-api @@ -34,6 +41,7 @@ def get_remote_addr(): class API: + # FIXME: DOCS """ The :class:`API` is in charge of the interface between the user and the tower. It handles and server user requests. @@ -49,7 +57,7 @@ class API: self.watcher = watcher self.gatekeeper = gatekeeper - # TODO: UNITTEST + # TODO: UNITTEST, DOCS def register(self): remote_addr = get_remote_addr() @@ -86,6 +94,7 @@ class API: return jsonify(response), rcode + # FIXME: UNITTEST def add_appointment(self): """ Main endpoint of the Watchtower. @@ -108,20 +117,41 @@ class API: if request.is_json: # Check content type once if properly defined request_data = request.get_json() - appointment = self.inspector.inspect( - request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key") + + rcode, message = self.gatekeeper.identify_user( + request_data.get("appointment"), request_data.get("signature") ) - if type(appointment) == Appointment: - appointment_added, signature = self.watcher.add_appointment(appointment) + if rcode: + rcode = HTTP_BAD_REQUEST + error = "appointment rejected. Error {}: {}".format(rcode, message) + return jsonify({"error": error}), rcode - if appointment_added: - rcode = HTTP_OK - response = {"locator": appointment.locator, "signature": signature} + else: + user_pk = message + + appointment = self.inspector.inspect(request_data.get("appointment")) + + if type(appointment) == Appointment: + # An appointment will fill 1 slot per ENCRYPTED_BLOB_MAX_SIZE_HEX block. + required_slots = ceil(len(appointment.encrypted_blob.data) / ENCRYPTED_BLOB_MAX_SIZE_HEX) + + if self.gatekeeper.get_slots(user_pk) >= required_slots: + appointment_added, signature = self.watcher.add_appointment(appointment) + + if appointment_added: + rcode = HTTP_OK + response = {"locator": appointment.locator, "signature": signature} + self.gatekeeper.fill_subscription_slots(user_pk, required_slots) + + else: + rcode = HTTP_SERVICE_UNAVAILABLE + error = "appointment rejected" + response = {"error": error} else: - rcode = HTTP_SERVICE_UNAVAILABLE - error = "appointment rejected" + rcode = errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS + error = "invalid signature or the user does not have enough slots available" response = {"error": error} elif type(appointment) == tuple: diff --git a/teos/errors.py b/teos/errors.py index c4b5b25..fb7ef1d 100644 --- a/teos/errors.py +++ b/teos/errors.py @@ -6,7 +6,7 @@ APPOINTMENT_WRONG_FIELD_FORMAT = -4 APPOINTMENT_FIELD_TOO_SMALL = -5 APPOINTMENT_FIELD_TOO_BIG = -6 APPOINTMENT_WRONG_FIELD = -7 -APPOINTMENT_INVALID_SIGNATURE = -8 +APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS = -8 # Registration errors [-65, -128] REGISTRATION_MISSING_FIELD = -65 diff --git a/teos/gatekeeper.py b/teos/gatekeeper.py index 75d71f7..79eb34c 100644 --- a/teos/gatekeeper.py +++ b/teos/gatekeeper.py @@ -1,8 +1,13 @@ import re -SUBSCRIPTION_SLOTS = 100 +import teos.errors as errors -# TODO: UNITTEST +from common.appointment import Appointment +from common.cryptographer import Cryptographer + +SUBSCRIPTION_SLOTS = 1 + +# TODO: UNITTEST, DOCS class Gatekeeper: def __init__(self): self.registered_users = {} @@ -30,3 +35,63 @@ class Gatekeeper: self.registered_users[user_pk] += SUBSCRIPTION_SLOTS return self.registered_users[user_pk] + + def fill_subscription_slots(self, user_pk, n): + slots = self.registered_users.get(user_pk) + + # FIXME: This looks pretty dangerous. I'm guessing race conditions can happen here. + if slots == n: + self.registered_users.pop(user_pk) + else: + self.registered_users[user_pk] -= n + + def identify_user(self, appointment_data, signature): + """ + Checks if the provided user signature is comes from a registered user with available appointment slots. + + Args: + appointment_data (:obj:`dict`): the appointment that was signed by the user. + signature (:obj:`str`): the user's signature (hex encoded). + + Returns: + :obj:`tuple`: A tuple (return code, message) as follows: + + - ``(0, None)`` if the user can be identified (recovered pk belongs to a registered user) and the user has + available slots. + - ``!= (0, None)`` otherwise. + + The possible return errors are: ``APPOINTMENT_EMPTY_FIELD`` and ``APPOINTMENT_INVALID_SIGNATURE``. + """ + + if signature is None: + rcode = errors.APPOINTMENT_EMPTY_FIELD + message = "empty signature received" + + else: + appointment = Appointment.from_dict(appointment_data) + rpk = Cryptographer.recover_pk(appointment.serialize(), signature) + compressed_user_pk = Cryptographer.get_compressed_pk(rpk) + + if compressed_user_pk and compressed_user_pk in self.registered_users: + rcode = 0 + message = compressed_user_pk + + else: + rcode = errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS + message = "invalid signature or the user does not have enough slots available" + + return rcode, message + + def get_slots(self, user_pk): + """ + Returns the number os available slots for a given user. + + Args: + user_pk(:mod:`str`): the public key that identifies the user (33-bytes hex str) + + Returns: + :obj:`int`: the number of available slots. + + """ + slots = self.registered_users.get(user_pk) + return slots if slots is not None else 0 diff --git a/teos/inspector.py b/teos/inspector.py index 60a83fc..d00cc08 100644 --- a/teos/inspector.py +++ b/teos/inspector.py @@ -1,9 +1,7 @@ import re -from binascii import unhexlify import common.cryptographer from common.constants import LOCATOR_LEN_HEX -from common.cryptographer import Cryptographer, PublicKey from teos import errors, LOG_PREFIX from common.logger import Logger @@ -19,7 +17,6 @@ common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_ BLOCKS_IN_A_MONTH = 4320 # 4320 = roughly a month in blocks -ENCRYPTED_BLOB_MAX_SIZE_HEX = 2 * 2048 class Inspector: @@ -36,14 +33,13 @@ class Inspector: self.block_processor = block_processor self.min_to_self_delay = min_to_self_delay - def inspect(self, appointment_data, signature, public_key): + def inspect(self, appointment_data): """ Inspects whether the data provided by the user is correct. Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. - 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 ` or :obj:`tuple`: An appointment initialized with the @@ -72,8 +68,6 @@ class Inspector: 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) @@ -330,10 +324,6 @@ class Inspector: rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE message = "wrong encrypted_blob data type ({})".format(t) - elif len(encrypted_blob) > ENCRYPTED_BLOB_MAX_SIZE_HEX: - rcode = errors.APPOINTMENT_FIELD_TOO_BIG - message = "encrypted_blob has to be 2Kib at most (current {})".format(len(encrypted_blob) // 2) - elif re.search(r"^[0-9A-Fa-f]+$", encrypted_blob) is None: rcode = errors.APPOINTMENT_WRONG_FIELD_FORMAT message = "wrong encrypted_blob format ({})".format(encrypted_blob) @@ -342,51 +332,3 @@ class Inspector: logger.error(message) return rcode, message - - @staticmethod - # Verifies that the appointment signature is a valid signature with public key - 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