6 from Crypto.PublicKey import RSA
7 from Crypto.Hash import SHA, SHA256, SHA512
8 from Crypto.Signature import PKCS1_v1_5
10 from cachetools import LFUCache
11 from async_lru import alru_cache
13 from .remote_actor import fetch_actor
23 def split_signature(sig):
24 default = {"headers": "date"}
26 sig = sig.strip().split(',')
29 k, _, v = chunk.partition('=')
33 default['headers'] = default['headers'].split()
37 def build_signing_string(headers, used_headers):
38 return '\n'.join(map(lambda x: ': '.join([x.lower(), headers[x]]), used_headers))
41 SIGSTRING_CACHE = LFUCache(1024)
43 def sign_signing_string(sigstring, key):
44 if sigstring in SIGSTRING_CACHE:
45 return SIGSTRING_CACHE[sigstring]
47 pkcs = PKCS1_v1_5.new(key)
49 h.update(sigstring.encode('ascii'))
50 sigdata = pkcs.sign(h)
52 sigdata = base64.b64encode(sigdata)
53 SIGSTRING_CACHE[sigstring] = sigdata.decode('ascii')
55 return SIGSTRING_CACHE[sigstring]
58 def generate_body_digest(body):
59 bodyhash = SIGSTRING_CACHE.get(body)
62 h = SHA256.new(body.encode('utf-8'))
63 bodyhash = base64.b64encode(h.digest()).decode('utf-8')
64 SIGSTRING_CACHE[body] = bodyhash
69 def sign_headers(headers, key, key_id):
70 headers = {x.lower(): y for x, y in headers.items()}
71 used_headers = headers.keys()
74 'algorithm': 'rsa-sha256',
75 'headers': ' '.join(used_headers)
77 sigstring = build_signing_string(headers, used_headers)
78 sig['signature'] = sign_signing_string(sigstring, key)
80 chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()]
81 return ','.join(chunks)
84 @alru_cache(maxsize=16384)
85 async def fetch_actor_key(actor):
86 actor_data = await fetch_actor(actor)
91 if 'publicKey' not in actor_data:
94 if 'publicKeyPem' not in actor_data['publicKey']:
97 return RSA.importKey(actor_data['publicKey']['publicKeyPem'])
100 async def validate(actor, request):
101 pubkey = await fetch_actor_key(actor)
105 logging.debug('actor key: %r', pubkey)
107 headers = request.headers.copy()
108 headers['(request-target)'] = ' '.join([request.method.lower(), request.path])
110 sig = split_signature(headers['signature'])
111 logging.debug('sigdata: %r', sig)
113 sigstring = build_signing_string(headers, sig['headers'])
114 logging.debug('sigstring: %r', sigstring)
116 sign_alg, _, hash_alg = sig['algorithm'].partition('-')
117 logging.debug('sign alg: %r, hash alg: %r', sign_alg, hash_alg)
119 sigdata = base64.b64decode(sig['signature'])
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)
126 request['validated'] = result
128 logging.debug('validates? %r', result)
132 async def http_signatures_middleware(app, handler):
133 async def http_signatures_handler(request):
134 request['validated'] = False
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')
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')
146 return (await handler(request))
148 return (await handler(request))
150 return http_signatures_handler