Skip to content

Instantly share code, notes, and snippets.

@matthewdowney
Created November 9, 2017 13:07
Show Gist options
  • Save matthewdowney/c27cad6c9690cb2801f69a72478be3b0 to your computer and use it in GitHub Desktop.
Save matthewdowney/c27cad6c9690cb2801f69a72478be3b0 to your computer and use it in GitHub Desktop.
Python HMAC
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