Files
python-teos/teos/inspector.py
Sergi Delgado Segura dd53ad68fb Fixes bug when dealing with empty JSON requests or empty appointment field
When posting a request via requests.post the json field was dumped to json, but it shouldn't have been since requests deals with this internally. That meant that the requests made by the code didn't match proper JSON.
In line with this, the API was only parsing this type POST requests correctly, making add_appointment to fail if a proper formatted JSON was passed.

On top of that, empty appointments were not checked in the Inspector before trying to get data from them, making it crash if a JSON was posted to add_appointment not containing the `appointment` field. Unit tests for this should be added.
2020-03-24 20:17:03 +01:00

393 lines
14 KiB
Python

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
from common.appointment import Appointment
logger = Logger(actor="Inspector", log_name_prefix=LOG_PREFIX)
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
# FIXME: The inspector logs the wrong messages sent form the users. A possible attack surface would be to send a really
# long field that, even if not accepted by TEOS, would be stored in the logs. This is a possible DoS surface
# since teos would store any kind of message (no matter the length). Solution: truncate the length of the fields
# stored + blacklist if multiple wrong requests are received.
BLOCKS_IN_A_MONTH = 4320 # 4320 = roughly a month in blocks
ENCRYPTED_BLOB_MAX_SIZE_HEX = 2 * 2048
class Inspector:
"""
The :class:`Inspector` class is in charge of verifying that the appointment data provided by the user is correct.
Args:
block_processor (:obj:`BlockProcessor <teos.block_processor.BlockProcessor>`): a ``BlockProcessor`` instance.
min_to_self_delay (:obj:`int`): the minimum to_self_delay accepted in appointments.
"""
def __init__(self, block_processor, min_to_self_delay):
self.block_processor = block_processor
self.min_to_self_delay = min_to_self_delay
def inspect(self, appointment_data, signature, public_key):
"""
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
provided data if it is correct.
Returns a tuple ``(return code, message)`` describing the error otherwise.
Errors are defined in :mod:`Errors <teos.errors>`.
"""
if appointment_data is None:
return errors.APPOINTMENT_EMPTY_FIELD, "empty appointment received"
block_height = self.block_processor.get_block_count()
if block_height is not None:
rcode, message = self.check_locator(appointment_data.get("locator"))
if rcode == 0:
rcode, message = self.check_start_time(appointment_data.get("start_time"), block_height)
if rcode == 0:
rcode, message = self.check_end_time(
appointment_data.get("end_time"), appointment_data.get("start_time"), block_height
)
if rcode == 0:
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)
else:
r = (rcode, message)
else:
# In case of an unknown exception, assign a special rcode and reason.
r = (errors.UNKNOWN_JSON_RPC_EXCEPTION, "Unexpected error occurred")
return r
@staticmethod
def check_locator(locator):
"""
Checks if the provided ``locator`` is correct.
Locators must be 16-byte hex encoded strings.
Args:
locator (:obj:`str`): the locator to be checked.
Returns:
:obj:`tuple`: A tuple (return code, message) as follows:
- ``(0, None)`` if the ``locator`` is correct.
- ``!= (0, None)`` otherwise.
The possible return errors are: ``APPOINTMENT_EMPTY_FIELD``, ``APPOINTMENT_WRONG_FIELD_TYPE``,
``APPOINTMENT_WRONG_FIELD_SIZE``, and ``APPOINTMENT_WRONG_FIELD_FORMAT``.
"""
message = None
rcode = 0
if locator is None:
rcode = errors.APPOINTMENT_EMPTY_FIELD
message = "empty locator received"
elif type(locator) != str:
rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE
message = "wrong locator data type ({})".format(type(locator))
elif len(locator) != LOCATOR_LEN_HEX:
rcode = errors.APPOINTMENT_WRONG_FIELD_SIZE
message = "wrong locator size ({})".format(len(locator))
# TODO: #12-check-txid-regexp
elif re.search(r"^[0-9A-Fa-f]+$", locator) is None:
rcode = errors.APPOINTMENT_WRONG_FIELD_FORMAT
message = "wrong locator format ({})".format(locator)
if message is not None:
logger.error(message)
return rcode, message
@staticmethod
def check_start_time(start_time, block_height):
"""
Checks if the provided ``start_time`` is correct.
Start times must be ahead the current best chain tip.
Args:
start_time (:obj:`int`): the block height at which the tower is requested to start watching for breaches.
block_height (:obj:`int`): the chain height.
Returns:
:obj:`tuple`: A tuple (return code, message) as follows:
- ``(0, None)`` if the ``start_time`` is correct.
- ``!= (0, None)`` otherwise.
The possible return errors are: ``APPOINTMENT_EMPTY_FIELD``, ``APPOINTMENT_WRONG_FIELD_TYPE``, and
``APPOINTMENT_FIELD_TOO_SMALL``.
"""
message = None
rcode = 0
# TODO: What's too close to the current height is not properly defined. Right now any appointment that is in the
# future will be accepted (even if it's only one block away).
t = type(start_time)
if start_time is None:
rcode = errors.APPOINTMENT_EMPTY_FIELD
message = "empty start_time received"
elif t != int:
rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE
message = "wrong start_time data type ({})".format(t)
elif start_time <= block_height:
rcode = errors.APPOINTMENT_FIELD_TOO_SMALL
if start_time < block_height:
message = "start_time is in the past"
else:
message = (
"start_time is too close to current height. "
"Accepted times are: [current_height+1, current_height+6]"
)
elif start_time > block_height + 6:
rcode = errors.APPOINTMENT_FIELD_TOO_BIG
message = "start_time is too far in the future. Accepted start times are up to 6 blocks in the future"
if message is not None:
logger.error(message)
return rcode, message
@staticmethod
def check_end_time(end_time, start_time, block_height):
"""
Checks if the provided ``end_time`` is correct.
End times must be ahead both the ``start_time`` and the current best chain tip.
Args:
end_time (:obj:`int`): the block height at which the tower is requested to stop watching for breaches.
start_time (:obj:`int`): the block height at which the tower is requested to start watching for breaches.
block_height (:obj:`int`): the chain height.
Returns:
:obj:`tuple`: A tuple (return code, message) as follows:
- ``(0, None)`` if the ``end_time`` is correct.
- ``!= (0, None)`` otherwise.
The possible return errors are: ``APPOINTMENT_EMPTY_FIELD``, ``APPOINTMENT_WRONG_FIELD_TYPE``, and
``APPOINTMENT_FIELD_TOO_SMALL``.
"""
message = None
rcode = 0
# TODO: What's too close to the current height is not properly defined. Right now any appointment that ends in
# the future will be accepted (even if it's only one block away).
t = type(end_time)
if end_time is None:
rcode = errors.APPOINTMENT_EMPTY_FIELD
message = "empty end_time received"
elif t != int:
rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE
message = "wrong end_time data type ({})".format(t)
elif end_time > block_height + BLOCKS_IN_A_MONTH: # 4320 = roughly a month in blocks
rcode = errors.APPOINTMENT_FIELD_TOO_BIG
message = "end_time should be within the next month (<= current_height + 4320)"
elif start_time >= end_time:
rcode = errors.APPOINTMENT_FIELD_TOO_SMALL
if start_time > end_time:
message = "end_time is smaller than start_time"
else:
message = "end_time is equal to start_time"
elif block_height >= end_time:
rcode = errors.APPOINTMENT_FIELD_TOO_SMALL
if block_height > end_time:
message = "end_time is in the past"
else:
message = "end_time is too close to current height"
if message is not None:
logger.error(message)
return rcode, message
def check_to_self_delay(self, to_self_delay):
"""
Checks if the provided ``to_self_delay`` is correct.
To self delays must be greater or equal to ``MIN_TO_SELF_DELAY``.
Args:
to_self_delay (:obj:`int`): The ``to_self_delay`` encoded in the ``csv`` of the ``htlc`` that this
appointment is covering.
Returns:
:obj:`tuple`: A tuple (return code, message) as follows:
- ``(0, None)`` if the ``to_self_delay`` is correct.
- ``!= (0, None)`` otherwise.
The possible return errors are: ``APPOINTMENT_EMPTY_FIELD``, ``APPOINTMENT_WRONG_FIELD_TYPE``, and
``APPOINTMENT_FIELD_TOO_SMALL``.
"""
message = None
rcode = 0
t = type(to_self_delay)
if to_self_delay is None:
rcode = errors.APPOINTMENT_EMPTY_FIELD
message = "empty to_self_delay received"
elif t != int:
rcode = errors.APPOINTMENT_WRONG_FIELD_TYPE
message = "wrong to_self_delay data type ({})".format(t)
elif to_self_delay > pow(2, 32):
rcode = errors.APPOINTMENT_FIELD_TOO_BIG
message = "to_self_delay must fit the transaction nLockTime field ({} > {})".format(
to_self_delay, pow(2, 32)
)
elif to_self_delay < self.min_to_self_delay:
rcode = errors.APPOINTMENT_FIELD_TOO_SMALL
message = "to_self_delay too small. The to_self_delay should be at least {} (current: {})".format(
self.min_to_self_delay, to_self_delay
)
if message is not None:
logger.error(message)
return rcode, message
# ToDo: #6-define-checks-encrypted-blob
@staticmethod
def check_blob(encrypted_blob):
"""
Checks if the provided ``encrypted_blob`` may be correct.
Args:
encrypted_blob (:obj:`str`): the encrypted blob to be checked (hex encoded).
Returns:
:obj:`tuple`: A tuple (return code, message) as follows:
- ``(0, None)`` if the ``encrypted_blob`` 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
t = type(encrypted_blob)
if encrypted_blob is None:
rcode = errors.APPOINTMENT_EMPTY_FIELD
message = "empty encrypted_blob received"
elif t != str:
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)
if message is not None:
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