diff --git a/viera/__init__.py b/viera/__init__.py index 71b7c2a..99bd3a3 100644 --- a/viera/__init__.py +++ b/viera/__init__.py @@ -15,7 +15,12 @@ def load_config(): CONFIG = load_config() -app = aiohttp.web.Application() +from .http_signatures import http_signatures_middleware + + +app = aiohttp.web.Application(middlewares=[ + http_signatures_middleware +]) from . import database diff --git a/viera/actor.py b/viera/actor.py index 95e194c..1ba32e8 100644 --- a/viera/actor.py +++ b/viera/actor.py @@ -1,3 +1,4 @@ +import aiohttp import aiohttp.web import logging from Crypto.PublicKey import RSA diff --git a/viera/http_signatures.py b/viera/http_signatures.py new file mode 100644 index 0000000..44d4a27 --- /dev/null +++ b/viera/http_signatures.py @@ -0,0 +1,92 @@ +import aiohttp +import aiohttp.web +import base64 +import logging + +from Crypto.PublicKey import RSA +from Crypto.Hash import SHA, SHA256, SHA512 +from Crypto.Signature import PKCS1_v1_5 + +from .remote_actor import fetch_actor + + +HASHES = { + 'sha1': SHA, + 'sha256': SHA256, + 'sha512': SHA512 +} + + +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 + + +def build_signing_string(headers, used_headers): + return '\n'.join(map(lambda x: ': '.join([x, headers[x]]), used_headers)) + + +async def fetch_actor_key(actor): + actor_data = await fetch_actor(actor) + + if 'publicKey' not in actor_data: + return None + + if 'publicKeyPem' not in actor_data['publicKey']: + return None + + return RSA.importKey(actor_data['publicKey']['publicKeyPem']) + + +async def validate(actor, request): + pubkey = await fetch_actor_key(actor) + logging.debug('actor key: %r', pubkey) + + headers = request.headers.copy() + headers['(request-target)'] = ' '.join([request.method.lower(), request.path]) + + sig = split_signature(headers['signature']) + logging.debug('sigdata: %r', sig) + + sigstring = build_signing_string(headers, sig['headers']) + logging.debug('sigstring: %r', sigstring) + + sign_alg, _, hash_alg = sig['algorithm'].partition('-') + logging.debug('sign alg: %r, hash alg: %r', sign_alg, hash_alg) + + sigdata = base64.b64decode(sig['signature']) + + pkcs = PKCS1_v1_5.new(pubkey) + h = HASHES[hash_alg].new() + h.update(sigstring.encode('ascii')) + result = pkcs.verify(h, sigdata) + + logging.debug('validates? %r', result) + return result + + +async def http_signatures_middleware(app, handler): + async def http_signatures_handler(request): + if 'signature' in request.headers: + data = await request.json() + if 'actor' not in data: + raise aiohttp.web.HTTPUnauthorized(body='signature check failed, no actor in message') + + actor = data["actor"] + if not (await validate(actor, request)): + raise aiohttp.web.HTTPUnauthorized(body='signature check failed, signature did not match key') + + return (await handler(request)) + + return (await handler(request)) + + return http_signatures_handler diff --git a/viera/remote_actor.py b/viera/remote_actor.py new file mode 100644 index 0000000..d1862f9 --- /dev/null +++ b/viera/remote_actor.py @@ -0,0 +1,14 @@ +import aiohttp +from .database import DATABASE + + +ACTORS = DATABASE.get("actors", {}) +async def fetch_actor(uri, force=False): + if uri in ACTORS and not force: + return ACTORS[uri] + + async with aiohttp.ClientSession() as session: + async with session.get(uri, headers={'Accept': 'application/activity+json'}) as resp: + ACTORS[uri] = (await resp.json()) + DATABASE["actors"] = ACTORS + return ACTORS[uri]