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:
parent
90d407b0c0
commit
9f6da963a4
|
@ -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
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue