Merge branch 'follows' into 'master'
[relay.git/.git] / relay / http_signatures.py
index 6ccdd65..1093f5e 100644 (file)
@@ -7,6 +7,9 @@ from Crypto.PublicKey import RSA
 from Crypto.Hash import SHA, SHA256, SHA512
 from Crypto.Signature import PKCS1_v1_5
 
+from cachetools import LFUCache
+from async_lru import alru_cache
+
 from .remote_actor import fetch_actor
 
 
@@ -35,6 +38,34 @@ 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 generate_body_digest(body):
+    bodyhash = SIGSTRING_CACHE.get(body)
+
+    if not bodyhash:
+        h = SHA256.new(body.encode('utf-8'))
+        bodyhash = base64.b64encode(h.digest()).decode('utf-8')
+        SIGSTRING_CACHE[body] = bodyhash
+
+    return bodyhash
+
+
 def sign_headers(headers, key, key_id):
     headers = {x.lower(): y for x, y in headers.items()}
     used_headers = headers.keys()
@@ -44,19 +75,13 @@ def sign_headers(headers, key, key_id):
         'headers': ' '.join(used_headers)
     }
     sigstring = build_signing_string(headers, used_headers)
-
-    pkcs = PKCS1_v1_5.new(key)
-    h = SHA256.new()
-    h.update(sigstring.encode('ascii'))
-    sigdata = pkcs.sign(h)
-
-    sigdata = base64.b64encode(sigdata)
-    sig['signature'] = sigdata.decode('ascii')
+    sig['signature'] = sign_signing_string(sigstring, key)
 
     chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()]
     return ','.join(chunks)
 
 
+@alru_cache(maxsize=16384)
 async def fetch_actor_key(actor):
     actor_data = await fetch_actor(actor)
 
@@ -74,6 +99,9 @@ async def fetch_actor_key(actor):
 
 async def validate(actor, request):
     pubkey = await fetch_actor_key(actor)
+    if not pubkey:
+        return False
+
     logging.debug('actor key: %r', pubkey)
 
     headers = request.headers.copy()
@@ -105,13 +133,14 @@ async def http_signatures_middleware(app, handler):
     async def http_signatures_handler(request):
         request['validated'] = False
 
-        if 'signature' in request.headers:
+        if 'signature' in request.headers and request.method == 'POST':
             data = await request.json()
             if 'actor' not in data:
                 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, no actor in message')
 
             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')
 
             return (await handler(request))