From 9f6da963a4aa424af9c8becf6b9b4a6ddd594fc9 Mon Sep 17 00:00:00 2001 From: Thomas NOEL Date: Thu, 2 Apr 2020 10:44:00 +0200 Subject: [PATCH] signature: do not require nonce if not verified (#41245) + some fixes, sync with http://git.entrouvert.org/hobo.git/tree/hobo/signature.py --- passerelle/base/signature.py | 38 ++++++++++---------- tests/test_signature.py | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 tests/test_signature.py diff --git a/passerelle/base/signature.py b/passerelle/base/signature.py index 8d249fcd..d879623e 100644 --- a/passerelle/base/signature.py +++ b/passerelle/base/signature.py @@ -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 diff --git a/tests/test_signature.py b/tests/test_signature.py new file mode 100644 index 00000000..319d41f1 --- /dev/null +++ b/tests/test_signature.py @@ -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 '×tamp=' 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 '×tamp=%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)