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 sign_headers(headers, key, key_id):
59 headers = {x.lower(): y for x, y in headers.items()}
60 used_headers = headers.keys()
63 'algorithm': 'rsa-sha256',
64 'headers': ' '.join(used_headers)
66 sigstring = build_signing_string(headers, used_headers)
67 sig['signature'] = sign_signing_string(sigstring, key)
69 chunks = ['{}="{}"'.format(k, v) for k, v in sig.items()]
70 return ','.join(chunks)
73 @alru_cache(maxsize=16384)
74 async def fetch_actor_key(actor):
75 actor_data = await fetch_actor(actor)
80 if 'publicKey' not in actor_data:
83 if 'publicKeyPem' not in actor_data['publicKey']:
86 return RSA.importKey(actor_data['publicKey']['publicKeyPem'])
89 async def validate(actor, request):
90 pubkey = await fetch_actor_key(actor)
94 logging.debug('actor key: %r', pubkey)
96 headers = request.headers.copy()
97 headers['(request-target)'] = ' '.join([request.method.lower(), request.path])
99 sig = split_signature(headers['signature'])
100 logging.debug('sigdata: %r', sig)
102 sigstring = build_signing_string(headers, sig['headers'])
103 logging.debug('sigstring: %r', sigstring)
105 sign_alg, _, hash_alg = sig['algorithm'].partition('-')
106 logging.debug('sign alg: %r, hash alg: %r', sign_alg, hash_alg)
108 sigdata = base64.b64decode(sig['signature'])
110 pkcs = PKCS1_v1_5.new(pubkey)
111 h = HASHES[hash_alg].new()
112 h.update(sigstring.encode('ascii'))
113 result = pkcs.verify(h, sigdata)
115 request['validated'] = result
117 logging.debug('validates? %r', result)
121 async def http_signatures_middleware(app, handler):
122 async def http_signatures_handler(request):
123 request['validated'] = False
125 if 'signature' in request.headers:
126 data = await request.json()
127 if 'actor' not in data:
128 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, no actor in message')
130 actor = data["actor"]
131 if not (await validate(actor, request)):
132 logging.info('Signature validation failed for: %r', actor)
133 raise aiohttp.web.HTTPUnauthorized(body='signature check failed, signature did not match key')
135 return (await handler(request))
137 return (await handler(request))
139 return http_signatures_handler