use signature keyid instead of object actor to fetch actor

This commit is contained in:
Izalia Mae 2022-11-17 16:30:56 -05:00
parent d2b243d88a
commit d08bd6625a
2 changed files with 43 additions and 43 deletions

View file

@ -106,20 +106,6 @@ def sign_signing_string(sigstring, key):
return base64.b64encode(sigdata).decode('utf-8') 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): async def fetch_actor_key(actor):
actor_data = await request(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() traceback.print_exc()
async def validate_signature(actor, http_request): async def validate_signature(actor, signature, http_request):
pubkey = await fetch_actor_key(actor)
if not pubkey:
return False
logging.debug(f'actor key: {pubkey}')
headers = {key.lower(): value for key, value in http_request.headers.items()} headers = {key.lower(): value for key, value in http_request.headers.items()}
headers['(request-target)'] = ' '.join([http_request.method.lower(), http_request.path]) headers['(request-target)'] = ' '.join([http_request.method.lower(), http_request.path])
sig = split_signature(headers['signature']) sigstring = build_signing_string(headers, signature['headers'])
logging.debug(f'sigdata: {sig}')
sigstring = build_signing_string(headers, sig['headers'])
logging.debug(f'sigstring: {sigstring}') 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}') 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 = HASHES[hash_alg].new()
h.update(sigstring.encode('ascii')) h.update(sigstring.encode('ascii'))
result = pkcs.verify(h, sigdata) result = pkcs.verify(h, sigdata)
@ -333,6 +309,22 @@ class DotDict(dict):
raise JSONDecodeError('Invalid body', data, 1) 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): def to_json(self, indent=None):
return json.dumps(self, indent=indent) return json.dumps(self, indent=indent)
@ -435,6 +427,11 @@ class Message(DotDict):
# actor properties # actor properties
@property
def PUBKEY(self):
return RSA.import_key(self.pubkey)
@property @property
def pubkey(self): def pubkey(self):
return self.publicKey.publicKeyPem return self.publicKey.publicKeyPem

View file

@ -6,7 +6,7 @@ from pathlib import Path
from . import __version__, misc from . import __version__, misc
from .http_debug import STATS from .http_debug import STATS
from .misc import Message, Response, WKNodeinfo from .misc import DotDict, Message, Response, WKNodeinfo
from .processors import run_processor from .processors import run_processor
@ -78,11 +78,14 @@ async def inbox(request):
database = request.app.database database = request.app.database
## reject if missing signature header ## 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') logging.verbose('Actor missing signature header')
raise HTTPUnauthorized(body='missing signature') raise HTTPUnauthorized(body='missing signature')
## read message and get actor id and domain ## read message
try: try:
data = await request.json(loads=Message.new_from_json) data = await request.json(loads=Message.new_from_json)
@ -96,22 +99,22 @@ async def inbox(request):
logging.verbose('Failed to parse inbox message') logging.verbose('Failed to parse inbox message')
return Response.new_error(400, 'failed to parse message', 'json') return Response.new_error(400, 'failed to parse message', 'json')
software = await misc.fetch_nodeinfo(data.domain) actor = await misc.request(signature.keyid)
actor = await misc.request(data.actorid) software = await misc.fetch_nodeinfo(actor.domain)
## reject if actor is empty ## reject if actor is empty
if not actor: 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') return Response.new_error(400, 'failed to fetch actor', 'json')
## reject if the actor isn't whitelisted while the whiltelist is enabled ## reject if the actor isn't whitelisted while the whiltelist is enabled
elif config.whitelist_enabled and not config.is_whitelisted(data.domain): elif config.whitelist_enabled and not config.is_whitelisted(actor.domain):
logging.verbose(f'Rejected actor for not being in the whitelist: {data.actorid}') logging.verbose(f'Rejected actor for not being in the whitelist: {actor.id}')
return Response.new_error(403, 'access denied', 'json') return Response.new_error(403, 'access denied', 'json')
## reject if actor is banned ## reject if actor is banned
if request.app['config'].is_banned(data.domain): if request.app['config'].is_banned(actor.domain):
logging.verbose(f'Ignored request from banned actor: {data.actorid}') logging.verbose(f'Ignored request from banned actor: {actor.id}')
return Response.new_error(403, 'access denied', 'json') return Response.new_error(403, 'access denied', 'json')
## reject if software used by actor is banned ## reject if software used by actor is banned
@ -120,13 +123,13 @@ async def inbox(request):
return Response.new_error(403, 'access denied', 'json') return Response.new_error(403, 'access denied', 'json')
## reject if the signature is invalid ## reject if the signature is invalid
if not (await misc.validate_signature(data.actorid, request)): if not (await misc.validate_signature(actor, signature, request)):
logging.verbose(f'signature validation failed for: {data.actorid}') logging.verbose(f'signature validation failed for: {actor.id}')
return Response.new_error(401, 'signature check failed', 'json') return Response.new_error(401, 'signature check failed', 'json')
## reject if activity type isn't 'Follow' and the actor isn't following ## reject if activity type isn't 'Follow' and the actor isn't following
if data['type'] != 'Follow' and not database.get_inbox(data.domain): if data['type'] != 'Follow' and not database.get_inbox(actor.domain):
logging.verbose(f'Rejected actor for trying to post while not following: {data.actorid}') logging.verbose(f'Rejected actor for trying to post while not following: {actor.id}')
return Response.new_error(401, 'access denied', 'json') return Response.new_error(401, 'access denied', 'json')
logging.debug(f">> payload {data}") logging.debug(f">> payload {data}")