Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add http signatures signer for request
- Loading branch information
Showing
1 changed file
with
78 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,139 +1,106 @@ | ||
import aiohttp | ||
import aiohttp.web | ||
import base64 | ||
import logging | ||
import hashlib | ||
import base64 | ||
from typing import (Any, Dict, Optional, List) | ||
from datetime import datetime | ||
from urllib.parse import urlparse | ||
|
||
from Crypto.PublicKey import RSA | ||
from Crypto.Hash import SHA, SHA256, SHA512 | ||
from Crypto.Hash import SHA256 | ||
from Crypto.Signature import PKCS1_v1_5 | ||
from requests.auth import AuthBase | ||
|
||
from cachetools import LFUCache | ||
from async_lru import alru_cache | ||
|
||
from activityPub.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.lower(), headers[x]]), used_headers)) | ||
|
||
|
||
SIGSTRING_CACHE = LFUCache(1024) | ||
|
||
def sign_signing_string(sigstring, key): | ||
if sigstring in SIGSTRING_CACHE: | ||
return SIGSTRING_CACHE[sigstring] | ||
|
||
pkcs = PKCS1_v1_5.new(key) | ||
h = SHA256.new() | ||
h.update(sigstring.encode('ascii')) | ||
sigdata = pkcs.sign(h) | ||
|
||
sigdata = base64.b64encode(sigdata) | ||
SIGSTRING_CACHE[sigstring] = sigdata.decode('ascii') | ||
|
||
return SIGSTRING_CACHE[sigstring] | ||
|
||
|
||
def sign_headers(headers, key, key_id): | ||
headers = {x.lower(): y for x, y in headers.items()} | ||
used_headers = headers.keys() | ||
sig = { | ||
'keyId': key_id, | ||
'algorithm': 'rsa-sha256', | ||
'headers': ' '.join(used_headers) | ||
} | ||
sigstring = build_signing_string(headers, used_headers) | ||
sig['signature'] = sign_signing_string(sigstring, key) | ||
|
||
chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()] | ||
return ','.join(chunks) | ||
from src.activityPub.key import CryptoKey | ||
|
||
|
||
@alru_cache(maxsize=16384) | ||
async def fetch_actor_key(actor): | ||
actor_data = await fetch_actor(actor) | ||
log = logging.getLogger(__name__) | ||
|
||
if not actor_data: | ||
return None | ||
|
||
if 'publicKey' not in actor_data: | ||
return None | ||
class HTTPSignaturesAuth(AuthBase): | ||
|
||
if 'publicKeyPem' not in actor_data['publicKey']: | ||
return None | ||
""" | ||
Plugin for request to sign petitions | ||
http://docs.python-requests.org/en/master/user/authentication/#new-forms-of-authentication | ||
""" | ||
|
||
return RSA.importKey(actor_data['publicKey']['publicKeyPem']) | ||
def __init__(self, key: CryptoKey) -> None: | ||
self.key = key | ||
|
||
self.headers = ["(request-target)", "user-agent", "host", "date", "digest", "content-type"] | ||
|
||
async def validate(actor, request): | ||
pubkey = await fetch_actor_key(actor) | ||
if not pubkey: | ||
return False | ||
def _build_signed_string(self, | ||
signed_headers: List, | ||
method: str, | ||
path: str, | ||
headers: Any, | ||
body_digest: str | ||
) -> str: | ||
|
||
logging.debug('actor key: %r', pubkey) | ||
production = [] | ||
|
||
headers = request.headers.copy() | ||
headers['(request-target)'] = ' '.join([request.method.lower(), request.path]) | ||
for header in signed_headers: | ||
|
||
sig = split_signature(headers['signature']) | ||
logging.debug('sigdata: %r', sig) | ||
if header == '(request-target)': | ||
production.append('(request-target): ' + method.lower() + ' ' + path) | ||
elif header == 'digest' | ||
production.append('digest: ' + body_digest) | ||
else: | ||
production.append(header + ': ' + headers[header]) | ||
|
||
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) | ||
return '\n'.join(production) | ||
|
||
sigdata = base64.b64decode(sig['signature']) | ||
def __call__(self, r): | ||
|
||
# Get the domain target of the request | ||
host = urlparse(r.url).netloc | ||
|
||
pkcs = PKCS1_v1_5.new(pubkey) | ||
h = HASHES[hash_alg].new() | ||
h.update(sigstring.encode('ascii')) | ||
result = pkcs.verify(h, sigdata) | ||
# Create hasher using sha256 | ||
hasher = hashlib.new('sha256') | ||
|
||
request['validated'] = result | ||
body = r.body | ||
try: | ||
body = r.body.encode('utf-8') | ||
except: | ||
pass | ||
|
||
logging.debug('validates? %r', result) | ||
return result | ||
hasher.update(body) | ||
digest = "SHA-256=" + base64.b64encode(hasher.digest()).decode("utf-8") | ||
|
||
date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") | ||
|
||
new_headers = { | ||
"Digest": digest, | ||
"Date": date, | ||
"Host": host | ||
} | ||
|
||
r.headers.update(new_headers) | ||
|
||
async def http_signatures_middleware(app, handler): | ||
async def http_signatures_handler(request): | ||
request['validated'] = False | ||
hstring = " ".join(self.headers) | ||
|
||
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') | ||
to_sign = self._build_signed_string( | ||
self.headers, | ||
r.method, | ||
r.path_url, | ||
r.headers, | ||
digest | ||
) | ||
|
||
actor = data["actor"] | ||
if not (await validate(actor, request)): | ||
logging.info('Signature validation failed for: %r', actor) | ||
raise aiohttp.web.HTTPUnauthorized(body='signature check failed, signature did not match key') | ||
signer = PKCS1_v1_5.new(self.key.privkey) | ||
|
||
# Digest of the headers | ||
hdigest = SHA256.new() | ||
hdigest.update(to_sign.encode('utf-8')) | ||
sig = base64.b64encode(signer.sign(hdigest)) | ||
sig = sig.decode('utf-8') | ||
|
||
return (await handler(request)) | ||
key_id = self.key.key_id() | ||
|
||
headers = { | ||
"Signature": f'keyId="{key_id}",algorithm="rsa-sha256",headers="{hstring}",signature="{sig}"' | ||
} | ||
|
||
return (await handler(request)) | ||
log.debug(f'Signed request headers {headers}') | ||
|
||
return http_signatures_handler | ||
r.headers.update(headers) | ||
return r |