signature: do not require nonce if not verified (#41245)

+ some fixes, sync with http://git.entrouvert.org/hobo.git/tree/hobo/signature.py
This commit is contained in:
Thomas NOËL 2020-04-02 10:44:00 +02:00
parent 90d407b0c0
commit 9f6da963a4
2 changed files with 89 additions and 18 deletions

View File

@ -6,16 +6,20 @@ import random
from django.utils import six
from django.utils.encoding import smart_bytes
from django.utils.http import quote, urlencode
from django.utils.six.moves.urllib import parse as urlparse
'''Simple signature scheme for query strings'''
# from http://repos.entrouvert.org/portail-citoyen.git/tree/portail_citoyen/apps/data_source_plugin/signature.py
# from http://git.entrouvert.org/hobo.git/tree/hobo/signature.py
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()
@ -25,30 +29,37 @@ def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
new_query = query
if new_query:
new_query += '&'
new_query += urlparse.urlencode((
new_query += urlencode((
('algo', algo),
('timestamp', timestamp),
('nonce', nonce)))
('timestamp', timestamp)))
if nonce: # we don't add nonce if it's an empty string
new_query += '&nonce=' + quote(nonce)
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
new_query += '&signature=' + urlparse.quote(signature)
new_query += '&signature=' + quote(signature)
return new_query
def sign_string(s, key, algo='sha256', timedelta=30):
def sign_string(s, key, algo='sha256'):
digestmod = getattr(hashlib, algo)
if isinstance(key, six.text_type):
key = key.encode('utf-8')
hash = hmac.HMAC(smart_bytes(key), digestmod=digestmod, msg=smart_bytes(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)
return check_query(parsed.query, key, known_nonce=known_nonce, timedelta=timedelta)
def check_query(query, key, known_nonce=None, timedelta=30):
parsed = urlparse.parse_qs(query)
if not ('signature' in parsed and 'algo' in parsed and
'timestamp' in parsed and 'nonce' in parsed):
'timestamp' in parsed):
return False
if known_nonce is not None:
if ('nonce' not in parsed) or known_nonce(parsed['nonce'][0]):
return False
unsigned_query, signature_content = query.split('&signature=', 1)
if '&' in signature_content:
return False # signature must be the last parameter
@ -56,13 +67,11 @@ def check_query(query, key, known_nonce=None, timedelta=30):
algo = parsed['algo'][0]
timestamp = parsed['timestamp'][0]
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
nonce = parsed['nonce']
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)
@ -76,10 +85,3 @@ def check_string(s, signature, key, algo='sha256'):
for a, b in zip(signature, signature2):
res |= ord(a) ^ ord(b)
return res == 0
if __name__ == '__main__':
key = '12345'
signed_query = sign_query('NameId=_12345&orig=montpellier', key)
assert check_query(signed_query, key, timedelta=0) is False
assert check_query(signed_query, key) is True

69
tests/test_signature.py Normal file
View File

@ -0,0 +1,69 @@
import datetime
from django.utils.six.moves.urllib import parse as urllib
from passerelle.base 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)
assert not signature.check_url('%s&foo=bar' % signature.sign_url(URL, KEY), 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'), b'\xc3\xa9\xc3\xa9')
assert signature.check_url(signature.sign_url(URL, b'\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')
assert '&nonce=' in signature.sign_url(URL, KEY)
assert '&nonce=' not in signature.sign_url(URL, KEY, nonce='')
# Test known_nonce
def known_nonce(nonce):
return nonce == 'xxx'
assert signature.check_url(signature.sign_url(URL, KEY), KEY, known_nonce=known_nonce)
assert signature.check_url(signature.sign_url(URL, KEY, nonce='zzz'), KEY, known_nonce=known_nonce)
assert not signature.check_url(signature.sign_url(URL, KEY, nonce='xxx'), KEY, known_nonce=known_nonce)
assert not signature.check_url(signature.sign_url(URL, KEY, nonce=''), KEY, known_nonce=known_nonce)
# Test timedelta
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=20))
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)
now = (datetime.datetime.utcnow() + datetime.timedelta(seconds=20))
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)
# too late
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=40))
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=20))
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY, timedelta=10)
# too early
now = (datetime.datetime.utcnow() + datetime.timedelta(seconds=40))
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY)