mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 06:04:21 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
52
teos/api.py
52
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user