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