Merge branch 'follows' into 'master'
[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 cachetools import LFUCache
11 from async_lru import alru_cache
12
13 from .remote_actor import fetch_actor
14
15
16 HASHES = {
17     'sha1': SHA,
18     'sha256': SHA256,
19     'sha512': SHA512
20 }
21
22
23 def split_signature(sig):
24     default = {"headers": "date"}
25
26     sig = sig.strip().split(',')
27
28     for chunk in sig:
29         k, _, v = chunk.partition('=')
30         v = v.strip('\"')
31         default[k] = v
32
33     default['headers'] = default['headers'].split()
34     return default
35
36
37 def build_signing_string(headers, used_headers):
38     return '\n'.join(map(lambda x: ': '.join([x.lower(), headers[x]]), used_headers))
39
40
41 SIGSTRING_CACHE = LFUCache(1024)
42
43 def sign_signing_string(sigstring, key):
44     if sigstring in SIGSTRING_CACHE:
45         return SIGSTRING_CACHE[sigstring]
46
47     pkcs = PKCS1_v1_5.new(key)
48     h = SHA256.new()
49     h.update(sigstring.encode('ascii'))
50     sigdata = pkcs.sign(h)
51
52     sigdata = base64.b64encode(sigdata)
53     SIGSTRING_CACHE[sigstring] = sigdata.decode('ascii')
54
55     return SIGSTRING_CACHE[sigstring]
56
57
58 def generate_body_digest(body):
59     bodyhash = SIGSTRING_CACHE.get(body)
60
61     if not bodyhash:
62         h = SHA256.new(body.encode('utf-8'))
63         bodyhash = base64.b64encode(h.digest()).decode('utf-8')
64         SIGSTRING_CACHE[body] = bodyhash
65
66     return bodyhash
67
68
69 def sign_headers(headers, key, key_id):
70     headers = {x.lower(): y for x, y in headers.items()}
71     used_headers = headers.keys()
72     sig = {
73         'keyId': key_id,
74         'algorithm': 'rsa-sha256',
75         'headers': ' '.join(used_headers)
76     }
77     sigstring = build_signing_string(headers, used_headers)
78     sig['signature'] = sign_signing_string(sigstring, key)
79
80     chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()]
81     return ','.join(chunks)
82
83
84 @alru_cache(maxsize=16384)
85 async def fetch_actor_key(actor):
86     actor_data = await fetch_actor(actor)
87
88     if not actor_data:
89         return None
90
91     if 'publicKey' not in actor_data:
92         return None
93
94     if 'publicKeyPem' not in actor_data['publicKey']:
95         return None
96
97     return RSA.importKey(actor_data['publicKey']['publicKeyPem'])
98
99
100 async def validate(actor, request):
101     pubkey = await fetch_actor_key(actor)
102     if not pubkey:
103         return False
104
105     logging.debug('actor key: %r', pubkey)
106
107     headers = request.headers.copy()
108     headers['(request-target)'] = ' '.join([request.method.lower(), request.path])
109
110     sig = split_signature(headers['signature'])
111     logging.debug('sigdata: %r', sig)
112
113     sigstring = build_signing_string(headers, sig['headers'])
114     logging.debug('sigstring: %r', sigstring)
115
116     sign_alg, _, hash_alg = sig['algorithm'].partition('-')
117     logging.debug('sign alg: %r, hash alg: %r', sign_alg, hash_alg)
118
119     sigdata = base64.b64decode(sig['signature'])
120
121     pkcs = PKCS1_v1_5.new(pubkey)
122     h = HASHES[hash_alg].new()
123     h.update(sigstring.encode('ascii'))
124     result = pkcs.verify(h, sigdata)
125
126     request['validated'] = result
127
128     logging.debug('validates? %r', result)
129     return result
130
131
132 async def http_signatures_middleware(app, handler):
133     async def http_signatures_handler(request):
134         request['validated'] = False
135
136         if 'signature' in request.headers and request.method == 'POST':
137             data = await request.json()
138             if 'actor' not in data:
139                 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, no actor in message')
140
141             actor = data["actor"]
142             if not (await validate(actor, request)):
143                 logging.info('Signature validation failed for: %r', actor)
144                 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, signature did not match key')
145
146             return (await handler(request))
147
148         return (await handler(request))
149
150     return http_signatures_handler