signature: import signature module from passerelle (fixes #8892)

Also add tests and use SystemRandom instead of the twister PRNG.
This commit is contained in:
Benjamin Dauvergne 2015-11-06 10:08:47 +01:00
parent cc4d77ab12
commit 00df1122a4
2 changed files with 97 additions and 4 deletions

View File

@ -1,23 +1,26 @@
import datetime
import base64
import hmac
import hashlib
import datetime
import urllib
import urllib2
import urlparse
import random
import urlparse
'''Simple signature scheme for query strings'''
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
parsed = urlparse.urlparse(url)
new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
if timestamp is None:
timestamp = datetime.datetime.utcnow()
timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
if nonce is None:
nonce = hex(random.getrandbits(128))[2:-1]
nonce = hex(random.SystemRandom().getrandbits(128))[2:-1]
new_query = query
if new_query:
new_query += '&'
@ -29,9 +32,41 @@ def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
new_query += '&signature=' + urllib.quote(signature)
return new_query
def sign_string(s, key, algo='sha256', timedelta=30):
digestmod = getattr(hashlib, algo)
if isinstance(key, unicode):
key = key.encode('utf-8')
hash = hmac.HMAC(key, digestmod=digestmod, msg=s)
return hash.digest()
def check_url(url, key, known_nonce=None, timedelta=30):
parsed = urlparse.urlparse(url, 'https')
return check_query(parsed.query, key)
def check_query(query, key, known_nonce=None, timedelta=30):
parsed = urlparse.parse_qs(query)
signature = base64.b64decode(parsed['signature'][0])
algo = parsed['algo'][0]
timestamp = parsed['timestamp'][0]
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
nonce = parsed['nonce']
unsigned_query = query.split('&signature=')[0]
if known_nonce is not None and known_nonce(nonce):
return False
if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta):
return False
return check_string(unsigned_query, signature, key, algo=algo)
def check_string(s, signature, key, algo='sha256'):
# constant time compare
signature2 = sign_string(s, key, algo=algo)
if len(signature2) != len(signature):
return False
res = 0
for a, b in zip(signature, signature2):
res |= ord(a) ^ ord(b)
return res == 0

58
tests/test_signature.py Normal file
View File

@ -0,0 +1,58 @@
import datetime
import urllib
from hobo import signature
def test_signature():
KEY = 'xyz'
STRING = 'aaa'
URL = 'http://example.net/api/?coucou=zob'
QUERY = 'coucou=zob'
OTHER_KEY = 'abc'
# Passing test
assert signature.check_string(STRING, signature.sign_string(STRING, KEY), KEY)
assert signature.check_query(signature.sign_query(QUERY, KEY), KEY)
assert signature.check_url(signature.sign_url(URL, KEY), KEY)
# Not passing tests
assert not signature.check_string(STRING, signature.sign_string(STRING, KEY), OTHER_KEY)
assert not signature.check_query(signature.sign_query(QUERY, KEY), OTHER_KEY)
assert not signature.check_url(signature.sign_url(URL, KEY), OTHER_KEY)
# Test URL is preserved
assert URL in signature.sign_url(URL, KEY)
assert QUERY in signature.sign_query(QUERY, KEY)
# Test signed URL expected parameters
assert '&algo=sha256&' in signature.sign_url(URL, KEY)
assert '&timestamp=' in signature.sign_url(URL, KEY)
assert '&nonce=' in signature.sign_url(URL, KEY)
# Test unicode key conversion to UTF-8
assert signature.check_url(signature.sign_url(URL, u'\xe9\xe9'), '\xc3\xa9\xc3\xa9')
assert signature.check_url(signature.sign_url(URL, '\xc3\xa9\xc3\xa9'), u'\xe9\xe9')
# Test timedelta parameter
now = datetime.datetime.utcnow()
assert '&timestamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in \
signature.sign_url(URL, KEY, timestamp=now)
# Test nonce parameter
assert '&nonce=uuu&' in signature.sign_url(URL, KEY, nonce='uuu')
# Test known_nonce
assert signature.check_url(signature.sign_url(URL, KEY), KEY,
known_nonce=lambda nonce: nonce == 'xxx')
assert signature.check_url(signature.sign_url(URL, KEY, nonce='xxx'), KEY,
known_nonce=lambda nonce: nonce == 'xxx')
# Test timedelta
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=29))
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=30))
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=2))
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY, timedelta=2)