add aputils module for hs2019 support

This commit is contained in:
Izalia Mae 2022-11-26 22:16:14 -05:00
parent 130111c847
commit 5d01211a34
6 changed files with 41 additions and 132 deletions

View file

@ -35,6 +35,7 @@ class Application(web.Application):
self['database'].load() self['database'].load()
self['client'] = HttpClient( self['client'] = HttpClient(
database = self.database,
limit = self.config.push_limit, limit = self.config.push_limit,
timeout = self.config.timeout, timeout = self.config.timeout,
cache_size = self.config.json_cache cache_size = self.config.json_cache
@ -155,6 +156,7 @@ class PushWorker(threading.Thread):
def run(self): def run(self):
self.client = HttpClient( self.client = HttpClient(
database = self.app.database,
limit = self.app.config.push_limit, limit = self.app.config.push_limit,
timeout = self.app.config.timeout, timeout = self.app.config.timeout,
cache_size = self.app.config.json_cache cache_size = self.app.config.json_cache

View file

@ -1,9 +1,9 @@
import aputils
import asyncio import asyncio
import json import json
import logging import logging
import traceback import traceback
from Crypto.PublicKey import RSA
from urllib.parse import urlparse from urllib.parse import urlparse
@ -17,22 +17,7 @@ class RelayDatabase(dict):
}) })
self.config = config self.config = config
self.PRIVKEY = None self.signer = 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']
@property @property
@ -45,11 +30,6 @@ class RelayDatabase(dict):
return tuple(data['inbox'] for data in self['relay-list'].values()) 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): def load(self):
new_db = True new_db = True
@ -94,12 +74,13 @@ class RelayDatabase(dict):
if self.config.db.stat().st_size > 0: if self.config.db.stat().st_size > 0:
raise e from None raise e from None
if not self.privkey: if not self['private-key']:
logging.info("No actor keys present, generating 4096-bit RSA keypair.") 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: else:
self.PRIVKEY = RSA.importKey(self.privkey) self.signer = aputils.Signer(self['private-key'], self.config.keyid)
self.save() self.save()
return not new_db return not new_db

View file

@ -1,3 +1,4 @@
import aputils
import logging import logging
import traceback import traceback
@ -12,9 +13,7 @@ from . import __version__
from .misc import ( from .misc import (
MIMETYPES, MIMETYPES,
DotDict, DotDict,
Message, Message
create_signature_header,
generate_body_digest
) )
@ -30,7 +29,8 @@ class Cache(LRUCache):
class HttpClient: 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.cache = Cache(cache_size)
self.cfg = {'limit': limit, 'timeout': timeout} self.cfg = {'limit': limit, 'timeout': timeout}
self._conn = None self._conn = None
@ -47,29 +47,6 @@ class HttpClient:
return self.cfg['timeout'] 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): async def open(self):
if self._session: if self._session:
return return
@ -110,7 +87,7 @@ class HttpClient:
headers = {} headers = {}
if sign_headers: if sign_headers:
headers.update(self.sign_headers('GET', url)) headers.update(self.database.signer.sign_headers('GET', url))
try: try:
logging.verbose(f'Fetching resource: {url}') logging.verbose(f'Fetching resource: {url}')
@ -162,8 +139,19 @@ class HttpClient:
async def post(self, url, message): async def post(self, url, message):
await self.open() 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 = {'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: try:
logging.verbose(f'Sending "{message.type}" to {url}') logging.verbose(f'Sending "{message.type}" to {url}')
@ -185,7 +173,7 @@ class HttpClient:
## Additional methods ## ## Additional methods ##
async def fetch_nodeinfo(domain): async def fetch_nodeinfo(self, domain):
nodeinfo_url = None nodeinfo_url = None
wk_nodeinfo = await self.get(f'https://{domain}/.well-known/nodeinfo', loads=WKNodeinfo) wk_nodeinfo = await self.get(f'https://{domain}/.well-known/nodeinfo', loads=WKNodeinfo)

View file

@ -1,3 +1,4 @@
import aputils
import asyncio import asyncio
import base64 import base64
import json import json
@ -6,9 +7,6 @@ import socket
import traceback import traceback
import uuid 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.hdrs import METH_ALL as METHODS
from aiohttp.web import Response as AiohttpResponse, View as AiohttpView from aiohttp.web import Response as AiohttpResponse, View as AiohttpView
from datetime import datetime from datetime import datetime
@ -21,12 +19,6 @@ from .http_debug import http_debug
app = None app = None
HASHES = {
'sha1': SHA,
'sha256': SHA256,
'sha512': SHA512
}
MIMETYPES = { MIMETYPES = {
'activity': 'application/activity+json', 'activity': 'application/activity+json',
'html': 'text/html', 'html': 'text/html',
@ -92,67 +84,12 @@ def check_open_port(host, port):
return False 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): def distill_inboxes(actor, object_id):
for inbox in app.database.inboxes: for inbox in app.database.inboxes:
if inbox != actor.shared_inbox and urlparse(inbox).hostname != urlparse(object_id).hostname: if inbox != actor.shared_inbox and urlparse(inbox).hostname != urlparse(object_id).hostname:
yield inbox 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): class DotDict(dict):
def __init__(self, _data, **kwargs): def __init__(self, _data, **kwargs):
dict.__init__(self) dict.__init__(self)
@ -321,16 +258,6 @@ class Message(DotDict):
# actor properties # actor properties
@property
def PUBKEY(self):
return RSA.import_key(self.pubkey)
@property
def pubkey(self):
return self.publicKey.publicKeyPem
@property @property
def shared_inbox(self): def shared_inbox(self):
return self.get('endpoints', {}).get('sharedInbox', self.inbox) return self.get('endpoints', {}).get('sharedInbox', self.inbox)
@ -353,6 +280,11 @@ class Message(DotDict):
return self.object return self.object
@property
def signer(self):
return aputils.Signer.new_from_actor(self)
class Nodeinfo(DotDict): class Nodeinfo(DotDict):
@property @property
def swname(self): def swname(self):

View file

@ -1,3 +1,4 @@
import aputils
import asyncio import asyncio
import logging import logging
import subprocess import subprocess
@ -66,7 +67,7 @@ a:hover {{ color: #8AF; }}
async def actor(request): async def actor(request):
data = Message.new_actor( data = Message.new_actor(
host = request.config.host, host = request.config.host,
pubkey = request.database.pubkey pubkey = request.database.signer.pubkey
) )
return Response.new(data, ctype='activity') return Response.new(data, ctype='activity')
@ -127,9 +128,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(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}') 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 ## 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): if request.message.type != 'Follow' and not database.get_inbox(request.actor.domain):

View file

@ -28,6 +28,7 @@ install_requires =
click >= 8.1.2 click >= 8.1.2
pycryptodome >= 3.14.1 pycryptodome >= 3.14.1
PyYAML >= 5.0.0 PyYAML >= 5.0.0
aputils @ https://git.barkshark.xyz/barkshark/aputils/archive/0.1.1.tar.gz
python_requires = >=3.6 python_requires = >=3.6
[options.extras_require] [options.extras_require]