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_BAD_REQUEST = 400
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()
# 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 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,22 +117,43 @@ 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 rcode:
rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Error {}: {}".format(rcode, message)
return jsonify({"error": error}), rcode
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 = 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:
rcode = HTTP_BAD_REQUEST
error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1])

View File

@@ -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

View File

@@ -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

View File

@@ -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 <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"))
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