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
This commit is contained in:
Sergi Delgado Segura
2020-03-25 17:13:35 +01:00
parent 519caec29a
commit 83b3913cb5
6 changed files with 133 additions and 74 deletions

View File

@@ -6,3 +6,6 @@ LOCATOR_LEN_BYTES = LOCATOR_LEN_HEX // 2
HTTP_OK = 200 HTTP_OK = 200
HTTP_BAD_REQUEST = 400 HTTP_BAD_REQUEST = 400
HTTP_SERVICE_UNAVAILABLE = 503 HTTP_SERVICE_UNAVAILABLE = 503
# Temporary constants, may be changed
ENCRYPTED_BLOB_MAX_SIZE_HEX = 2 * 2048

View File

@@ -315,3 +315,22 @@ class Cryptographer:
""" """
return pk.point() == rpk.point() 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")

View File

@@ -1,5 +1,6 @@
import os import os
import logging import logging
from math import ceil
from flask import Flask, request, abort, jsonify from flask import Flask, request, abort, jsonify
import teos.errors as errors import teos.errors as errors
@@ -7,7 +8,13 @@ from teos import HOST, PORT, LOG_PREFIX
from common.logger import Logger from common.logger import Logger
from common.appointment import Appointment 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 # ToDo: #5-add-async-to-api
@@ -34,6 +41,7 @@ def get_remote_addr():
class API: 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. 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.watcher = watcher
self.gatekeeper = gatekeeper self.gatekeeper = gatekeeper
# TODO: UNITTEST # TODO: UNITTEST, DOCS
def register(self): def register(self):
remote_addr = get_remote_addr() remote_addr = get_remote_addr()
@@ -86,6 +94,7 @@ class API:
return jsonify(response), rcode return jsonify(response), rcode
# FIXME: UNITTEST
def add_appointment(self): def add_appointment(self):
""" """
Main endpoint of the Watchtower. Main endpoint of the Watchtower.
@@ -108,20 +117,41 @@ class API:
if request.is_json: if request.is_json:
# Check content type once if properly defined # Check content type once if properly defined
request_data = request.get_json() 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: if rcode:
appointment_added, signature = self.watcher.add_appointment(appointment) rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Error {}: {}".format(rcode, message)
return jsonify({"error": error}), rcode
if appointment_added: else:
rcode = HTTP_OK user_pk = message
response = {"locator": appointment.locator, "signature": signature}
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: else:
rcode = HTTP_SERVICE_UNAVAILABLE rcode = errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS
error = "appointment rejected" error = "invalid signature or the user does not have enough slots available"
response = {"error": error} response = {"error": error}
elif type(appointment) == tuple: elif type(appointment) == tuple:

View File

@@ -6,7 +6,7 @@ APPOINTMENT_WRONG_FIELD_FORMAT = -4
APPOINTMENT_FIELD_TOO_SMALL = -5 APPOINTMENT_FIELD_TOO_SMALL = -5
APPOINTMENT_FIELD_TOO_BIG = -6 APPOINTMENT_FIELD_TOO_BIG = -6
APPOINTMENT_WRONG_FIELD = -7 APPOINTMENT_WRONG_FIELD = -7
APPOINTMENT_INVALID_SIGNATURE = -8 APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS = -8
# Registration errors [-65, -128] # Registration errors [-65, -128]
REGISTRATION_MISSING_FIELD = -65 REGISTRATION_MISSING_FIELD = -65

View File

@@ -1,8 +1,13 @@
import re 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: class Gatekeeper:
def __init__(self): def __init__(self):
self.registered_users = {} self.registered_users = {}
@@ -30,3 +35,63 @@ class Gatekeeper:
self.registered_users[user_pk] += SUBSCRIPTION_SLOTS self.registered_users[user_pk] += SUBSCRIPTION_SLOTS
return self.registered_users[user_pk] 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

View File

@@ -1,9 +1,7 @@
import re import re
from binascii import unhexlify
import common.cryptographer import common.cryptographer
from common.constants import LOCATOR_LEN_HEX from common.constants import LOCATOR_LEN_HEX
from common.cryptographer import Cryptographer, PublicKey
from teos import errors, LOG_PREFIX from teos import errors, LOG_PREFIX
from common.logger import Logger 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 BLOCKS_IN_A_MONTH = 4320 # 4320 = roughly a month in blocks
ENCRYPTED_BLOB_MAX_SIZE_HEX = 2 * 2048
class Inspector: class Inspector:
@@ -36,14 +33,13 @@ class Inspector:
self.block_processor = block_processor self.block_processor = block_processor
self.min_to_self_delay = min_to_self_delay 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. Inspects whether the data provided by the user is correct.
Args: Args:
appointment_data (:obj:`dict`): a dictionary containing the appointment data. 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: Returns:
:obj:`Appointment <teos.appointment.Appointment>` or :obj:`tuple`: An appointment initialized with the :obj:`Appointment <teos.appointment.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")) rcode, message = self.check_to_self_delay(appointment_data.get("to_self_delay"))
if rcode == 0: if rcode == 0:
rcode, message = self.check_blob(appointment_data.get("encrypted_blob")) 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: if rcode == 0:
r = Appointment.from_dict(appointment_data) r = Appointment.from_dict(appointment_data)
@@ -330,10 +324,6 @@ class Inspector:
rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE
message = "wrong encrypted_blob data type ({})".format(t) 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: elif re.search(r"^[0-9A-Fa-f]+$", encrypted_blob) is None:
rcode = errors.APPOINTMENT_WRONG_FIELD_FORMAT rcode = errors.APPOINTMENT_WRONG_FIELD_FORMAT
message = "wrong encrypted_blob format ({})".format(encrypted_blob) message = "wrong encrypted_blob format ({})".format(encrypted_blob)
@@ -342,51 +332,3 @@ class Inspector:
logger.error(message) logger.error(message)
return rcode, 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