diff --git a/relay/application.py b/relay/application.py index 26d8d4a..b339672 100644 --- a/relay/application.py +++ b/relay/application.py @@ -35,6 +35,7 @@ class Application(web.Application): self['database'].load() self['client'] = HttpClient( + database = self.database, limit = self.config.push_limit, timeout = self.config.timeout, cache_size = self.config.json_cache @@ -155,6 +156,7 @@ class PushWorker(threading.Thread): def run(self): self.client = HttpClient( + database = self.app.database, limit = self.app.config.push_limit, timeout = self.app.config.timeout, cache_size = self.app.config.json_cache diff --git a/relay/database.py b/relay/database.py index 82adce4..d9cbe07 100644 --- a/relay/database.py +++ b/relay/database.py @@ -1,9 +1,9 @@ +import aputils import asyncio import json import logging import traceback -from Crypto.PublicKey import RSA from urllib.parse import urlparse @@ -17,22 +17,7 @@ class RelayDatabase(dict): }) self.config = config - self.PRIVKEY = None - - - @property - def PUBKEY(self): - return self.PRIVKEY.publickey() - - - @property - def pubkey(self): - return self.PUBKEY.exportKey('PEM').decode('utf-8') - - - @property - def privkey(self): - return self['private-key'] + self.signer = None @property @@ -45,11 +30,6 @@ class RelayDatabase(dict): return tuple(data['inbox'] for data in self['relay-list'].values()) - def generate_key(self): - self.PRIVKEY = RSA.generate(4096) - self['private-key'] = self.PRIVKEY.exportKey('PEM').decode('utf-8') - - def load(self): new_db = True @@ -94,12 +74,13 @@ class RelayDatabase(dict): if self.config.db.stat().st_size > 0: raise e from None - if not self.privkey: + if not self['private-key']: logging.info("No actor keys present, generating 4096-bit RSA keypair.") - self.generate_key() + self.signer = aputils.Signer.new(self.config.keyid, 4096) + self['private-key'] = self.signer.export() else: - self.PRIVKEY = RSA.importKey(self.privkey) + self.signer = aputils.Signer(self['private-key'], self.config.keyid) self.save() return not new_db diff --git a/relay/http_client.py b/relay/http_client.py index d664a88..f4337b3 100644 --- a/relay/http_client.py +++ b/relay/http_client.py @@ -1,3 +1,4 @@ +import aputils import logging import traceback @@ -12,9 +13,7 @@ from . import __version__ from .misc import ( MIMETYPES, DotDict, - Message, - create_signature_header, - generate_body_digest + Message ) @@ -30,7 +29,8 @@ class Cache(LRUCache): class HttpClient: - def __init__(self, limit=100, timeout=10, cache_size=1024): + def __init__(self, database, limit=100, timeout=10, cache_size=1024): + self.database = database self.cache = Cache(cache_size) self.cfg = {'limit': limit, 'timeout': timeout} self._conn = None @@ -47,29 +47,6 @@ class HttpClient: return self.cfg['timeout'] - def sign_headers(self, method, url, message=None): - parsed = urlparse(url) - headers = { - '(request-target)': f'{method.lower()} {parsed.path}', - 'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'), - 'Host': parsed.netloc - } - - if message: - data = message.to_json() - headers.update({ - 'Digest': f'SHA-256={generate_body_digest(data)}', - 'Content-Length': str(len(data.encode('utf-8'))) - }) - - headers['Signature'] = create_signature_header(headers) - - del headers['(request-target)'] - del headers['Host'] - - return headers - - async def open(self): if self._session: return @@ -110,7 +87,7 @@ class HttpClient: headers = {} if sign_headers: - headers.update(self.sign_headers('GET', url)) + headers.update(self.database.signer.sign_headers('GET', url)) try: logging.verbose(f'Fetching resource: {url}') @@ -162,8 +139,19 @@ class HttpClient: async def post(self, url, message): await self.open() + instance = self.database.get_inbox(url) + + ## Akkoma (and probably pleroma) doesn't support hs2019, so use the old algorithm + if instance.get('software') in {'akkoma', 'pleroma'}: + algorithm = aputils.Algorithm.RSASHA256 + + else: + algorithm = aputils.Algorithm.HS2019 + headers = {'Content-Type': 'application/activity+json'} - headers.update(self.sign_headers('POST', url, message)) + headers.update(self.database.signer.sign_headers('POST', url, message, algorithm=algorithm)) + + print(headers) try: logging.verbose(f'Sending "{message.type}" to {url}') @@ -185,7 +173,7 @@ class HttpClient: ## Additional methods ## - async def fetch_nodeinfo(domain): + async def fetch_nodeinfo(self, domain): nodeinfo_url = None wk_nodeinfo = await self.get(f'https://{domain}/.well-known/nodeinfo', loads=WKNodeinfo) diff --git a/relay/misc.py b/relay/misc.py index 628800d..c243a7d 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -1,3 +1,4 @@ +import aputils import asyncio import base64 import json @@ -6,9 +7,6 @@ import socket import traceback import uuid -from Crypto.Hash import SHA, SHA256, SHA512 -from Crypto.PublicKey import RSA -from Crypto.Signature import PKCS1_v1_5 from aiohttp.hdrs import METH_ALL as METHODS from aiohttp.web import Response as AiohttpResponse, View as AiohttpView from datetime import datetime @@ -21,12 +19,6 @@ from .http_debug import http_debug app = None -HASHES = { - 'sha1': SHA, - 'sha256': SHA256, - 'sha512': SHA512 -} - MIMETYPES = { 'activity': 'application/activity+json', 'html': 'text/html', @@ -92,67 +84,12 @@ def check_open_port(host, port): return False -def create_signature_header(headers): - headers = {k.lower(): v for k, v in headers.items()} - used_headers = headers.keys() - sigstring = build_signing_string(headers, used_headers) - - sig = { - 'keyId': app.config.keyid, - 'algorithm': 'rsa-sha256', - 'headers': ' '.join(used_headers), - 'signature': sign_signing_string(sigstring, app.database.PRIVKEY) - } - - chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()] - return ','.join(chunks) - - def distill_inboxes(actor, object_id): for inbox in app.database.inboxes: if inbox != actor.shared_inbox and urlparse(inbox).hostname != urlparse(object_id).hostname: yield inbox -def generate_body_digest(body): - h = SHA256.new(body.encode('utf-8')) - bodyhash = base64.b64encode(h.digest()).decode('utf-8') - - return bodyhash - - -def sign_signing_string(sigstring, key): - pkcs = PKCS1_v1_5.new(key) - h = SHA256.new() - h.update(sigstring.encode('ascii')) - sigdata = pkcs.sign(h) - - return base64.b64encode(sigdata).decode('utf-8') - - -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]) - - sigstring = build_signing_string(headers, signature['headers']) - logging.debug(f'sigstring: {sigstring}') - - sign_alg, _, hash_alg = signature['algorithm'].partition('-') - logging.debug(f'sign alg: {sign_alg}, hash alg: {hash_alg}') - - sigdata = base64.b64decode(signature['signature']) - - pkcs = PKCS1_v1_5.new(actor.PUBKEY) - h = HASHES[hash_alg].new() - h.update(sigstring.encode('ascii')) - result = pkcs.verify(h, sigdata) - - http_request['validated'] = result - - logging.debug(f'validates? {result}') - return result - - class DotDict(dict): def __init__(self, _data, **kwargs): dict.__init__(self) @@ -321,16 +258,6 @@ class Message(DotDict): # actor properties - @property - def PUBKEY(self): - return RSA.import_key(self.pubkey) - - - @property - def pubkey(self): - return self.publicKey.publicKeyPem - - @property def shared_inbox(self): return self.get('endpoints', {}).get('sharedInbox', self.inbox) @@ -353,6 +280,11 @@ class Message(DotDict): return self.object + @property + def signer(self): + return aputils.Signer.new_from_actor(self) + + class Nodeinfo(DotDict): @property def swname(self): diff --git a/relay/views.py b/relay/views.py index 76cafec..6b6b43f 100644 --- a/relay/views.py +++ b/relay/views.py @@ -1,3 +1,4 @@ +import aputils import asyncio import logging import subprocess @@ -66,7 +67,7 @@ a:hover {{ color: #8AF; }} async def actor(request): data = Message.new_actor( host = request.config.host, - pubkey = request.database.pubkey + pubkey = request.database.signer.pubkey ) return Response.new(data, ctype='activity') @@ -127,9 +128,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(request.actor, request.signature, request)): + try: + await request.actor.signer.validate_aiohttp_request(request) + + except aputils.SignatureValidationError as e: logging.verbose(f'signature validation failed for: {actor.id}') - return Response.new_error(401, 'signature check failed', 'json') + logging.debug(str(e)) + return Response.new_error(401, str(e), 'json') ## reject if activity type isn't 'Follow' and the actor isn't following if request.message.type != 'Follow' and not database.get_inbox(request.actor.domain): diff --git a/setup.cfg b/setup.cfg index 2345151..cc99d3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = click >= 8.1.2 pycryptodome >= 3.14.1 PyYAML >= 5.0.0 + aputils @ https://git.barkshark.xyz/barkshark/aputils/archive/0.1.1.tar.gz python_requires = >=3.6 [options.extras_require]