http signatures: hold actor keys in an LRU cache
[relay.git/.git] / relay / http_signatures.py
1 import aiohttp
2 import aiohttp.web
3 import base64
4 import logging
5
6 from Crypto.PublicKey import RSA
7 from Crypto.Hash import SHA, SHA256, SHA512
8 from Crypto.Signature import PKCS1_v1_5
9
10 from async_lru import alru_cache
11
12 from .remote_actor import fetch_actor
13
14
15 HASHES = {
16     'sha1': SHA,
17     'sha256': SHA256,
18     'sha512': SHA512
19 }
20
21
22 def split_signature(sig):
23     default = {"headers": "date"}
24
25     sig = sig.strip().split(',')
26
27     for chunk in sig:
28         k, _, v = chunk.partition('=')
29         v = v.strip('\"')
30         default[k] = v
31
32     default['headers'] = default['headers'].split()
33     return default
34
35
36 def build_signing_string(headers, used_headers):
37     return '\n'.join(map(lambda x: ': '.join([x.lower(), headers[x]]), used_headers))
38
39
40 def sign_headers(headers, key, key_id):
41     headers = {x.lower(): y for x, y in headers.items()}
42     used_headers = headers.keys()
43     sig = {
44         'keyId': key_id,
45         'algorithm': 'rsa-sha256',
46         'headers': ' '.join(used_headers)
47     }
48     sigstring = build_signing_string(headers, used_headers)
49
50     pkcs = PKCS1_v1_5.new(key)
51     h = SHA256.new()
52     h.update(sigstring.encode('ascii'))
53     sigdata = pkcs.sign(h)
54
55     sigdata = base64.b64encode(sigdata)
56     sig['signature'] = sigdata.decode('ascii')
57
58     chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()]
59     return ','.join(chunks)
60
61
62 @alru_cache(maxsize=16384)
63 async def fetch_actor_key(actor):
64     actor_data = await fetch_actor(actor)
65
66     if not actor_data:
67         return None
68
69     if 'publicKey' not in actor_data:
70         return None
71
72     if 'publicKeyPem' not in actor_data['publicKey']:
73         return None
74
75     return RSA.importKey(actor_data['publicKey']['publicKeyPem'])
76
77
78 async def validate(actor, request):
79     pubkey = await fetch_actor_key(actor)
80     if not pubkey:
81         return False
82
83     logging.debug('actor key: %r', pubkey)
84
85     headers = request.headers.copy()
86     headers['(request-target)'] = ' '.join([request.method.lower(), request.path])
87
88     sig = split_signature(headers['signature'])
89     logging.debug('sigdata: %r', sig)
90
91     sigstring = build_signing_string(headers, sig['headers'])
92     logging.debug('sigstring: %r', sigstring)
93
94     sign_alg, _, hash_alg = sig['algorithm'].partition('-')
95     logging.debug('sign alg: %r, hash alg: %r', sign_alg, hash_alg)
96
97     sigdata = base64.b64decode(sig['signature'])
98
99     pkcs = PKCS1_v1_5.new(pubkey)
100     h = HASHES[hash_alg].new()
101     h.update(sigstring.encode('ascii'))
102     result = pkcs.verify(h, sigdata)
103
104     request['validated'] = result
105
106     logging.debug('validates? %r', result)
107     return result
108
109
110 async def http_signatures_middleware(app, handler):
111     async def http_signatures_handler(request):
112         request['validated'] = False
113
114         if 'signature' in request.headers:
115             data = await request.json()
116             if 'actor' not in data:
117                 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, no actor in message')
118
119             actor = data["actor"]
120             if not (await validate(actor, request)):
121                 logging.info('Signature validation failed for: %r', actor)
122                 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, signature did not match key')
123
124             return (await handler(request))
125
126         return (await handler(request))
127
128     return http_signatures_handler