add aputils module for hs2019 support
This commit is contained in:
parent
130111c847
commit
5d01211a34
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue