From d08bd6625aac260d3eb5111a0fc809cf4847303f Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Thu, 17 Nov 2022 16:30:56 -0500 Subject: [PATCH] use signature keyid instead of object actor to fetch actor --- relay/misc.py | 55 ++++++++++++++++++++++++-------------------------- relay/views.py | 31 +++++++++++++++------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/relay/misc.py b/relay/misc.py index 626faf8..e5f362e 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -106,20 +106,6 @@ def sign_signing_string(sigstring, key): return base64.b64encode(sigdata).decode('utf-8') -def split_signature(sig): - default = {"headers": "date"} - - sig = sig.strip().split(',') - - for chunk in sig: - k, _, v = chunk.partition('=') - v = v.strip('\"') - default[k] = v - - default['headers'] = default['headers'].split() - return default - - async def fetch_actor_key(actor): actor_data = await request(actor) @@ -250,29 +236,19 @@ async def request(uri, data=None, force=False, sign_headers=True, activity=True) traceback.print_exc() -async def validate_signature(actor, http_request): - pubkey = await fetch_actor_key(actor) - - if not pubkey: - return False - - logging.debug(f'actor key: {pubkey}') - +async def validate_signature(actor, signature, http_request): headers = {key.lower(): value for key, value in http_request.headers.items()} headers['(request-target)'] = ' '.join([http_request.method.lower(), http_request.path]) - sig = split_signature(headers['signature']) - logging.debug(f'sigdata: {sig}') - - sigstring = build_signing_string(headers, sig['headers']) + sigstring = build_signing_string(headers, signature['headers']) logging.debug(f'sigstring: {sigstring}') - sign_alg, _, hash_alg = sig['algorithm'].partition('-') + sign_alg, _, hash_alg = signature['algorithm'].partition('-') logging.debug(f'sign alg: {sign_alg}, hash alg: {hash_alg}') - sigdata = base64.b64decode(sig['signature']) + sigdata = base64.b64decode(signature['signature']) - pkcs = PKCS1_v1_5.new(pubkey) + pkcs = PKCS1_v1_5.new(actor.PUBKEY) h = HASHES[hash_alg].new() h.update(sigstring.encode('ascii')) result = pkcs.verify(h, sigdata) @@ -333,6 +309,22 @@ class DotDict(dict): raise JSONDecodeError('Invalid body', data, 1) + @classmethod + def new_from_signature(cls, sig): + data = cls({}) + + for chunk in sig.strip().split(','): + key, value = chunk.split('=', 1) + value = value.strip('\"') + + if key == 'headers': + value = value.split() + + data[key.lower()] = value + + return data + + def to_json(self, indent=None): return json.dumps(self, indent=indent) @@ -435,6 +427,11 @@ class Message(DotDict): # actor properties + @property + def PUBKEY(self): + return RSA.import_key(self.pubkey) + + @property def pubkey(self): return self.publicKey.publicKeyPem diff --git a/relay/views.py b/relay/views.py index c069c68..5e7b303 100644 --- a/relay/views.py +++ b/relay/views.py @@ -6,7 +6,7 @@ from pathlib import Path from . import __version__, misc from .http_debug import STATS -from .misc import Message, Response, WKNodeinfo +from .misc import DotDict, Message, Response, WKNodeinfo from .processors import run_processor @@ -78,11 +78,14 @@ async def inbox(request): database = request.app.database ## reject if missing signature header - if 'signature' not in request.headers: + try: + signature = DotDict.new_from_signature(request.headers['signature']) + + except KeyError: logging.verbose('Actor missing signature header') raise HTTPUnauthorized(body='missing signature') - ## read message and get actor id and domain + ## read message try: data = await request.json(loads=Message.new_from_json) @@ -96,22 +99,22 @@ async def inbox(request): logging.verbose('Failed to parse inbox message') return Response.new_error(400, 'failed to parse message', 'json') - software = await misc.fetch_nodeinfo(data.domain) - actor = await misc.request(data.actorid) + actor = await misc.request(signature.keyid) + software = await misc.fetch_nodeinfo(actor.domain) ## reject if actor is empty if not actor: - logging.verbose(f'Failed to fetch actor: {data.actorid}') + logging.verbose(f'Failed to fetch actor: {actor.id}') return Response.new_error(400, 'failed to fetch actor', 'json') ## reject if the actor isn't whitelisted while the whiltelist is enabled - elif config.whitelist_enabled and not config.is_whitelisted(data.domain): - logging.verbose(f'Rejected actor for not being in the whitelist: {data.actorid}') + elif config.whitelist_enabled and not config.is_whitelisted(actor.domain): + logging.verbose(f'Rejected actor for not being in the whitelist: {actor.id}') return Response.new_error(403, 'access denied', 'json') ## reject if actor is banned - if request.app['config'].is_banned(data.domain): - logging.verbose(f'Ignored request from banned actor: {data.actorid}') + if request.app['config'].is_banned(actor.domain): + logging.verbose(f'Ignored request from banned actor: {actor.id}') return Response.new_error(403, 'access denied', 'json') ## reject if software used by actor is banned @@ -120,13 +123,13 @@ async def inbox(request): return Response.new_error(403, 'access denied', 'json') ## reject if the signature is invalid - if not (await misc.validate_signature(data.actorid, request)): - logging.verbose(f'signature validation failed for: {data.actorid}') + if not (await misc.validate_signature(actor, signature, request)): + logging.verbose(f'signature validation failed for: {actor.id}') return Response.new_error(401, 'signature check failed', 'json') ## reject if activity type isn't 'Follow' and the actor isn't following - if data['type'] != 'Follow' and not database.get_inbox(data.domain): - logging.verbose(f'Rejected actor for trying to post while not following: {data.actorid}') + if data['type'] != 'Follow' and not database.get_inbox(actor.domain): + logging.verbose(f'Rejected actor for trying to post while not following: {actor.id}') return Response.new_error(401, 'access denied', 'json') logging.debug(f">> payload {data}")