Moves validation code out of misc
This commit is contained in:
parent
fda095c195
commit
852e22bc62
2 changed files with 94 additions and 168 deletions
|
@ -1,183 +1,16 @@
|
||||||
import binascii
|
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from json import JSONDecodeError
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import aiohttp
|
from server.external import request_lbrynet
|
||||||
import ecdsa
|
|
||||||
from aiohttp import ClientConnectorError
|
|
||||||
from cryptography.exceptions import InvalidSignature
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
from cryptography.hazmat.primitives import hashes
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
|
||||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
ID_LIST = {'claim_id', 'parent_id', 'comment_id', 'channel_id'}
|
ID_LIST = {'claim_id', 'parent_id', 'comment_id', 'channel_id'}
|
||||||
|
|
||||||
ERRORS = {
|
|
||||||
'INVALID_PARAMS': {'code': -32602, 'message': 'Invalid Method Parameter(s).'},
|
|
||||||
'INTERNAL': {'code': -32603, 'message': 'Internal Server Error. Please notify a LBRY Administrator.'},
|
|
||||||
'METHOD_NOT_FOUND': {'code': -32601, 'message': 'The method does not exist / is not available.'},
|
|
||||||
'INVALID_REQUEST': {'code': -32600, 'message': 'The JSON sent is not a valid Request object.'},
|
|
||||||
'PARSE_ERROR': {
|
|
||||||
'code': -32700,
|
|
||||||
'message': 'Invalid JSON was received by the server.\n'
|
|
||||||
'An error occurred on the server while parsing the JSON text.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def make_error(error, exc=None) -> dict:
|
|
||||||
body = ERRORS[error] if error in ERRORS else ERRORS['INTERNAL']
|
|
||||||
try:
|
|
||||||
if exc:
|
|
||||||
exc_name = type(exc).__name__
|
|
||||||
body.update({exc_name: str(exc)})
|
|
||||||
|
|
||||||
finally:
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
async def report_error(app, exc, msg=''):
|
|
||||||
try:
|
|
||||||
if 'slack_webhook' in app['config']:
|
|
||||||
if msg:
|
|
||||||
msg = f'"{msg}"'
|
|
||||||
body = {
|
|
||||||
"text": f"Got `{type(exc).__name__}`: ```\n{str(exc)}```\n{msg}"
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession() as sesh:
|
|
||||||
async with sesh.post(app['config']['slack_webhook'], json=body) as resp:
|
|
||||||
await resp.wait_for_close()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.critical('Error while logging to slack webhook')
|
|
||||||
|
|
||||||
|
|
||||||
async def send_notifications(app, action: str, comments: List[dict]):
|
|
||||||
events = create_notification_batch(action, comments)
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
for event in events:
|
|
||||||
event.update(auth_token=app['config']['notifications']['auth_token'])
|
|
||||||
try:
|
|
||||||
async with session.get(app['config']['notifications']['url'], params=event) as resp:
|
|
||||||
logger.debug(f'Completed Notification: {await resp.text()}, HTTP Status: {resp.status}')
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f'Error requesting internal API, Status {resp.status}: {resp.text()}, '
|
|
||||||
f'comment_id: {event["comment_id"]}')
|
|
||||||
|
|
||||||
|
|
||||||
async def send_notification(app, action: str, comment: dict):
|
|
||||||
await send_notifications(app, action, [comment])
|
|
||||||
|
|
||||||
|
|
||||||
def create_notification_batch(action: str, comments: List[dict]) -> List[dict]:
|
|
||||||
action_type = action[0].capitalize() # to turn Create -> C, edit -> U, delete -> D
|
|
||||||
events = []
|
|
||||||
for comment in comments:
|
|
||||||
event = {
|
|
||||||
'action_type': action_type,
|
|
||||||
'comment_id': comment['comment_id'],
|
|
||||||
'claim_id': comment['claim_id']
|
|
||||||
}
|
|
||||||
if comment.get('channel_id'):
|
|
||||||
event['channel_id'] = comment['channel_id']
|
|
||||||
events.append(event)
|
|
||||||
return events
|
|
||||||
|
|
||||||
|
|
||||||
async def request_lbrynet(app, method, **params):
|
|
||||||
body = {'method': method, 'params': {**params}}
|
|
||||||
try:
|
|
||||||
async with aiohttp.request('POST', app['config']['lbrynet'], json=body) as req:
|
|
||||||
try:
|
|
||||||
resp = await req.json()
|
|
||||||
except JSONDecodeError as jde:
|
|
||||||
logger.exception(jde.msg)
|
|
||||||
raise Exception('JSON Decode Error In lbrynet request')
|
|
||||||
finally:
|
|
||||||
if 'result' in resp:
|
|
||||||
return resp['result']
|
|
||||||
raise ValueError('LBRYNET Request Error', {'error': resp['error']})
|
|
||||||
except (ConnectionRefusedError, ClientConnectorError):
|
|
||||||
logger.critical("Connection to the LBRYnet daemon failed, make sure it's running.")
|
|
||||||
raise Exception("Server cannot verify delete signature")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_claim_from_id(app, claim_id, **kwargs):
|
async def get_claim_from_id(app, claim_id, **kwargs):
|
||||||
return (await request_lbrynet(app, 'claim_search', claim_id=claim_id, **kwargs))['items'][0]
|
return (await request_lbrynet(app, 'claim_search', claim_id=claim_id, **kwargs))['items'][0]
|
||||||
|
|
||||||
|
|
||||||
def get_encoded_signature(signature):
|
|
||||||
signature = signature.encode() if type(signature) is str else signature
|
|
||||||
r = int(signature[:int(len(signature) / 2)], 16)
|
|
||||||
s = int(signature[int(len(signature) / 2):], 16)
|
|
||||||
return ecdsa.util.sigencode_der(r, s, len(signature) * 4)
|
|
||||||
|
|
||||||
|
|
||||||
def channel_matches_pattern_or_error(channel_id, channel_name):
|
|
||||||
assert channel_id and channel_name
|
|
||||||
assert re.fullmatch(
|
|
||||||
'^@(?:(?![\x00-\x08\x0b\x0c\x0e-\x1f\x23-\x26'
|
|
||||||
'\x2f\x3a\x3d\x3f-\x40\uFFFE-\U0000FFFF]).){1,255}$',
|
|
||||||
channel_name
|
|
||||||
)
|
|
||||||
assert re.fullmatch('([a-f0-9]|[A-F0-9]){40}', channel_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def is_signature_valid(encoded_signature, signature_digest, public_key_bytes):
|
|
||||||
try:
|
|
||||||
public_key = load_der_public_key(public_key_bytes, default_backend())
|
|
||||||
public_key.verify(encoded_signature, signature_digest, ec.ECDSA(Prehashed(hashes.SHA256())))
|
|
||||||
return True
|
|
||||||
except (ValueError, InvalidSignature):
|
|
||||||
logger.exception('Signature validation failed')
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_base_comment(comment, claim_id, parent_id=None, **kwargs):
|
|
||||||
try:
|
|
||||||
assert 0 < len(comment) <= 2000
|
|
||||||
assert (parent_id is None) or (0 < len(parent_id) <= 2000)
|
|
||||||
assert re.fullmatch('[a-z0-9]{40}', claim_id)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_credential_input(channel_id=None, channel_name=None, signature=None, signing_ts=None, **kwargs):
|
|
||||||
if channel_name or channel_name or signature or signing_ts:
|
|
||||||
try:
|
|
||||||
assert channel_matches_pattern_or_error(channel_id, channel_name)
|
|
||||||
if signature or signing_ts:
|
|
||||||
assert len(signature) == 128
|
|
||||||
assert signing_ts.isalnum()
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def validate_signature_from_claim(claim, signature, signing_ts, data: str):
|
|
||||||
try:
|
|
||||||
if claim:
|
|
||||||
public_key = claim['value']['public_key']
|
|
||||||
claim_hash = binascii.unhexlify(claim['claim_id'].encode())[::-1]
|
|
||||||
injest = b''.join((signing_ts.encode(), claim_hash, data.encode()))
|
|
||||||
return is_signature_valid(
|
|
||||||
encoded_signature=get_encoded_signature(signature),
|
|
||||||
signature_digest=hashlib.sha256(injest).digest(),
|
|
||||||
public_key_bytes=binascii.unhexlify(public_key.encode())
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def clean_input_params(kwargs: dict):
|
def clean_input_params(kwargs: dict):
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
if type(v) is str and k is not 'comment':
|
if type(v) is str and k is not 'comment':
|
||||||
|
|
93
src/server/validation.py
Normal file
93
src/server/validation.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import logging
|
||||||
|
import binascii
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
import ecdsa
|
||||||
|
import typing
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_channel(channel_id: str, channel_name: str) -> bool:
|
||||||
|
return channel_id and claim_id_is_valid(channel_id) and \
|
||||||
|
channel_name and channel_name_is_valid(channel_name)
|
||||||
|
|
||||||
|
|
||||||
|
def is_signature_valid(encoded_signature, signature_digest, public_key_bytes) -> bool:
|
||||||
|
try:
|
||||||
|
public_key = load_der_public_key(public_key_bytes, default_backend())
|
||||||
|
public_key.verify(encoded_signature, signature_digest, ec.ECDSA(Prehashed(hashes.SHA256())))
|
||||||
|
return True
|
||||||
|
except (ValueError, InvalidSignature):
|
||||||
|
logger.exception('Signature validation failed')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def channel_name_is_valid(channel_name: str) -> bool:
|
||||||
|
return re.fullmatch(
|
||||||
|
'@(?:(?![\x00-\x08\x0b\x0c\x0e-\x1f\x23-\x26'
|
||||||
|
'\x2f\x3a\x3d\x3f-\x40\uFFFE-\U0000FFFF]).){1,255}',
|
||||||
|
channel_name
|
||||||
|
) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def body_is_valid(comment: str) -> bool:
|
||||||
|
return 0 < len(comment) <= 2000
|
||||||
|
|
||||||
|
|
||||||
|
def comment_id_is_valid(comment_id: str) -> bool:
|
||||||
|
return re.fullmatch('([a-z0-9]{64}|[A-Z0-9]{64})', comment_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def claim_id_is_valid(claim_id: str) -> bool:
|
||||||
|
return re.fullmatch('([a-z0-9]{40}|[A-Z0-9]{40})', claim_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_base_comment(comment: str, claim_id: str, parent_id: str = None, **kwargs) -> bool:
|
||||||
|
return comment is not None and body_is_valid(comment) and \
|
||||||
|
claim_id is not None and claim_id_is_valid(claim_id) and \
|
||||||
|
(parent_id is None or comment_id_is_valid(parent_id))
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_credential_input(channel_id: str = None, channel_name: str = None,
|
||||||
|
signature: str = None, signing_ts: str = None, **kwargs) -> bool:
|
||||||
|
if channel_id or channel_name or signature or signing_ts:
|
||||||
|
try:
|
||||||
|
assert channel_id and channel_name and signature and signing_ts
|
||||||
|
assert is_valid_channel(channel_id, channel_name)
|
||||||
|
assert len(signature) == 128
|
||||||
|
assert signing_ts.isalnum()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_signature_from_claim(claim: dict, signature: typing.Union[str, bytes],
|
||||||
|
signing_ts: str, data: str) -> bool:
|
||||||
|
try:
|
||||||
|
if claim:
|
||||||
|
public_key = claim['value']['public_key']
|
||||||
|
claim_hash = binascii.unhexlify(claim['claim_id'].encode())[::-1]
|
||||||
|
injest = b''.join((signing_ts.encode(), claim_hash, data.encode()))
|
||||||
|
return is_signature_valid(
|
||||||
|
encoded_signature=get_encoded_signature(signature),
|
||||||
|
signature_digest=hashlib.sha256(injest).digest(),
|
||||||
|
public_key_bytes=binascii.unhexlify(public_key.encode())
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_encoded_signature(signature: typing.Union[str, bytes]) -> bytes:
|
||||||
|
signature = signature.encode() if type(signature) is str else signature
|
||||||
|
r = int(signature[:int(len(signature) / 2)], 16)
|
||||||
|
s = int(signature[int(len(signature) / 2):], 16)
|
||||||
|
return ecdsa.util.sigencode_der(r, s, len(signature) * 4)
|
Loading…
Reference in a new issue