Created
November 9, 2017 13:07
-
-
Save matthewdowney/c27cad6c9690cb2801f69a72478be3b0 to your computer and use it in GitHub Desktop.
Python HMAC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import hmac | |
from functools import wraps | |
def safe_equal(a, b): | |
""" | |
Perform an equality check between strings that guards against a timing attack by checking every index in the each | |
string. Note that length is compared first, so the length may be leaked through a timing attack, which does not | |
compromise the security of an HMAC. | |
See: https://codahale.com/a-lesson-in-timing-attacks/ | |
""" | |
if len(a) != len(b): | |
return False | |
result = 0 | |
for x, y in zip(map(ord, a), map(ord, b)): | |
result |= x ^ y | |
return result == 0 | |
def hmac_decorator(options): | |
""" | |
Build a decorator that enforces an HMAC requirement. A function such decorated will execute normally if the HMAC is | |
valid, otherwise the provided failure function will be executed instead. | |
An example for a django view that returns just good/bad signature. Assumes that key & sig are passed in as GET | |
params and that there's a model called APIKey with fields key and secret. Note the encoding used for each value. | |
``` | |
require_hmac = hmac_decorator({ | |
'digest_mod': hashlib.sha256, | |
'on_fail': lambda req: HttpResponseBadRequest("Bad signature."), | |
'sig_extractor': lambda req: bytes.fromhex(req.GET['signature']), | |
'key_extractor': lambda req: APIKey.objects.get(key=req.GET['key']).secret.encode('utf-8'), | |
'message_extractor': lambda req: (req.method + req.path + req.body).encode('utf-8') | |
}) | |
@require_hmac | |
def my_view(request): | |
return HttpResponse("Good signature.") | |
``` | |
:param options: Requires a dictionary containing: | |
digest_mod The type of signature digest to be used, e.g. hashlib.sha256. | |
on_fail A function that accepts the decorated function's parameters and performs some | |
alternative logic only run on failure. This could raise an exception, return an | |
HttpBadRequest, etc. | |
sig_extractor A function that accepts the decorated function's parameters and returns the signature | |
attached. E.g. if the signature is a header param, in Django this would be | |
`lambda request: request.META['HTTP_SIGNATURE']`. Expects a byte value. | |
key_extractor A function that accepts the decorated function's parameters and returns the key used to | |
sign the message. The signing key should not be included in any requests. E.g. the key | |
extractor might be `lambda request: database.find_api_secret(request.api_key)`. Expects | |
a byte value. | |
message_extractor A function that accepts the decorated function's parameters and returns the message | |
that is HMAC'd. For a HTTP request the message might be verb + path + data, e.g. | |
"GET /api/v1/endpoint?q=something&nonce=1510231495 {'req_body': ''}". Expects a byte | |
value. | |
:return: A decorator to be applied to a protected function. | |
""" | |
fields = ('digest_mod', 'on_fail', 'sig_extractor', 'key_extractor', 'message_extractor') | |
digest, fail_f, sig_f, key_f, msg_f = (options[x] for x in fields) | |
def decorator(protected_f): | |
""" | |
:param protected_f: The function that should only be invoked if a valid HMAC is present. | |
""" | |
@wraps(protected_f) | |
def wrapper(*args, **kwargs): | |
""" | |
Extract the sig, key, and msg using the functions available as options in the outer scope, then check to see | |
if the HMAC matches. If so, invoke the protected function. Otherwise, invoke the on_fail function. | |
""" | |
# Get the given signature, then the actual key & message | |
sig, key, msg = (f(*args, **kwargs) for f in (sig_f, key_f, msg_f)) | |
print(sig, key, msg) | |
# Check that the signature for the key & message match | |
expected_sig = hmac.new(key, msg, digest).hexdigest() | |
handler = fail_f if not safe_equal(sig.hex(), expected_sig) else protected_f | |
return handler(*args, **kwargs) | |
return wrapper | |
return decorator |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment