Skip to content

Commit

Permalink
Add http signatures signer for request
Browse files Browse the repository at this point in the history
  • Loading branch information
yabirgb committed Feb 6, 2019
1 parent 1f26996 commit eaa127f
Showing 1 changed file with 78 additions and 111 deletions.
189 changes: 78 additions & 111 deletions src/activityPub/data_signature.py
@@ -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

0 comments on commit eaa127f

Please sign in to comment.