add http signature functions

This commit is contained in:
Izalia Mae 2020-03-05 15:02:25 -05:00
parent af0f7bc5fe
commit 95d5da969a
3 changed files with 140 additions and 0 deletions

132
IzzyLib/httpSignatures.py Normal file
View file

@ -0,0 +1,132 @@
'''
Functions for working with HTTP signatures
Note: I edited this while tired, so this may be broken
'''
from base64 import b64decode, b64encode
from urllib.parse import urlparse
from datetime import datetime
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA, SHA256, SHA512
from Crypto.Signature import PKCS1_v1_5
from .misc import formatUTC
from . import logging
CACHE = TTLCache(ttl='1h')
HASHES = {
'sha1': SHA,
'sha256': SHA256,
'sha512': SHA512
}
def sign_headers(headers, target, privkey, key_id):
'''sign headers and add the signature to them'''
key = RSA.importKey(privkey)
headers = {k.lower(): v for k, v in headers.items()}
headers['date'] = formatUTC(None)
headers['(request-target)'] = target
used_headers = headers.keys()
sigstring = build_sigstring(headers, used_headers, target=target)
signed_sigstring = sign_sigstring(sigstring, key)
sig = {
'keyId': key_id,
'algorithm': 'rsa-sha256',
'headers': ' '.join(used_headers),
'signature': signed_sigstring
}
sig_header = ['{}="{}"'.format(k, v) for k, v in sig.items()]
headers['signature'] = ','.join(sig_header)
del headers['(request-target)']
return headers
def validate(headers, pubkey):
'''validate a signature header from an incoming request'''
sig = parse_sig(headers.get('signature'))
if not sig:
raise MissingData('Missing signature')
pkcs = PKCS1_v1_5.new(RSA.importKey(pubkey))
sigstring = build_sigstring(request, sig['headers'])
logging.debug(f'Signing string: {sigstring}')
signalg, hashalg = sig['algorithm'].split('-')
sigdata = b64decode(sig['signature'])
h = HASHES[hashalg].new()
h.update(sigstring.encode('ascii'))
result = pkcs.verify(h, sigdata)
logging.debug(f'Sig verification result: {result}')
if not result:
raise error.ValidationFailed('Failed signature validation')
else:
return True
def parse_sig(sig):
if not sig:
raise error.MissingData('Missing signature header')
parts = {'headers': 'date'}
for part in sig.strip().split(','):
k, v = part.replace('"', '').split('=', maxsplit=1)
if k == 'headers':
parts['headers'] = v.split()
else:
parts[k] = v
return parts
def build_sigstring(headers, used_headers):
string = ''
for header in used_headers:
string += f'{header.lower()}: {headers[header]}'
if header != list(used_headers)[-1]:
string += '\n'
return string
def sign_sigstring(sigstring, key, hashalg='SHA256'):
cached_data = cache.sig.fetch(sigstring)
if cached_data:
logging.info('Returning cache sigstring')
return cached_data
pkcs = PKCS1_v1_5.new(key)
h = HASHES[hashalg.lower()].new()
h.update(sigstring.encode('ascii'))
sigdata = b64encode(pkcs.sign(h)).decode('ascii')
cache.sig.store(sigstring, sigdata)
return sigdata
class error:
class MissingData(Exception):
'''raise when data is expected but nothing is returned'''
class ValidationFailed(Exception):
'''raise when signature validation fails'''

View file

@ -4,6 +4,8 @@ Miscellaneous functions
import random, string
from datetime import datetime
from . import logging
@ -37,6 +39,11 @@ def randomgen(chars=20):
return ''.join(random.choices(string.ascii_letters + string.digits, k=chars))
def formatUTC(timestamp=None):
date = datetime.fromtimestamp(timestamp) if timestamp else datetime.utcnow()
return date.strftime('%a, %d %b %Y %H:%M:%S GMT')
def merp():
logging.setLevel('MERP')
logging.merp('heck')

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
pycryptodome>=3.9.4