0001-signature-do-not-require-nonce-if-not-verified-41245.patch
passerelle/base/signature.py | ||
---|---|---|
6 | 6 | |
7 | 7 |
from django.utils import six |
8 | 8 |
from django.utils.encoding import smart_bytes |
9 |
from django.utils.http import quote, urlencode |
|
9 | 10 |
from django.utils.six.moves.urllib import parse as urlparse |
10 | 11 | |
12 | ||
11 | 13 |
'''Simple signature scheme for query strings''' |
12 |
# from http://repos.entrouvert.org/portail-citoyen.git/tree/portail_citoyen/apps/data_source_plugin/signature.py |
|
14 |
# from http://git.entrouvert.org/hobo.git/tree/hobo/signature.py |
|
15 | ||
13 | 16 | |
14 | 17 |
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None): |
15 | 18 |
parsed = urlparse.urlparse(url) |
16 | 19 |
new_query = sign_query(parsed.query, key, algo, timestamp, nonce) |
17 | 20 |
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:]) |
18 | 21 | |
22 | ||
19 | 23 |
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None): |
20 | 24 |
if timestamp is None: |
21 | 25 |
timestamp = datetime.datetime.utcnow() |
... | ... | |
25 | 29 |
new_query = query |
26 | 30 |
if new_query: |
27 | 31 |
new_query += '&' |
28 |
new_query += urlparse.urlencode((
|
|
32 |
new_query += urlencode(( |
|
29 | 33 |
('algo', algo), |
30 |
('timestamp', timestamp), |
|
31 |
('nonce', nonce))) |
|
34 |
('timestamp', timestamp))) |
|
35 |
if nonce: # we don't add nonce if it's an empty string |
|
36 |
new_query += '&nonce=' + quote(nonce) |
|
32 | 37 |
signature = base64.b64encode(sign_string(new_query, key, algo=algo)) |
33 |
new_query += '&signature=' + urlparse.quote(signature)
|
|
38 |
new_query += '&signature=' + quote(signature) |
|
34 | 39 |
return new_query |
35 | 40 | |
36 |
def sign_string(s, key, algo='sha256', timedelta=30): |
|
41 | ||
42 |
def sign_string(s, key, algo='sha256'): |
|
37 | 43 |
digestmod = getattr(hashlib, algo) |
38 | 44 |
if isinstance(key, six.text_type): |
39 | 45 |
key = key.encode('utf-8') |
40 | 46 |
hash = hmac.HMAC(smart_bytes(key), digestmod=digestmod, msg=smart_bytes(s)) |
41 | 47 |
return hash.digest() |
42 | 48 | |
49 | ||
43 | 50 |
def check_url(url, key, known_nonce=None, timedelta=30): |
44 | 51 |
parsed = urlparse.urlparse(url, 'https') |
45 |
return check_query(parsed.query, key) |
|
52 |
return check_query(parsed.query, key, known_nonce=known_nonce, timedelta=timedelta) |
|
53 | ||
46 | 54 | |
47 | 55 |
def check_query(query, key, known_nonce=None, timedelta=30): |
48 | 56 |
parsed = urlparse.parse_qs(query) |
49 | 57 |
if not ('signature' in parsed and 'algo' in parsed and |
50 |
'timestamp' in parsed and 'nonce' in parsed):
|
|
58 |
'timestamp' in parsed): |
|
51 | 59 |
return False |
60 |
if known_nonce is not None: |
|
61 |
if ('nonce' not in parsed) or known_nonce(parsed['nonce'][0]): |
|
62 |
return False |
|
52 | 63 |
unsigned_query, signature_content = query.split('&signature=', 1) |
53 | 64 |
if '&' in signature_content: |
54 | 65 |
return False # signature must be the last parameter |
... | ... | |
56 | 67 |
algo = parsed['algo'][0] |
57 | 68 |
timestamp = parsed['timestamp'][0] |
58 | 69 |
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ') |
59 |
nonce = parsed['nonce'] |
|
60 |
if known_nonce is not None and known_nonce(nonce): |
|
61 |
return False |
|
62 | 70 |
if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta): |
63 | 71 |
return False |
64 | 72 |
return check_string(unsigned_query, signature, key, algo=algo) |
65 | 73 | |
74 | ||
66 | 75 |
def check_string(s, signature, key, algo='sha256'): |
67 | 76 |
# constant time compare |
68 | 77 |
signature2 = sign_string(s, key, algo=algo) |
... | ... | |
76 | 85 |
for a, b in zip(signature, signature2): |
77 | 86 |
res |= ord(a) ^ ord(b) |
78 | 87 |
return res == 0 |
79 | ||
80 | ||
81 |
if __name__ == '__main__': |
|
82 |
key = '12345' |
|
83 |
signed_query = sign_query('NameId=_12345&orig=montpellier', key) |
|
84 |
assert check_query(signed_query, key, timedelta=0) is False |
|
85 |
assert check_query(signed_query, key) is True |
tests/test_signature.py | ||
---|---|---|
1 |
import datetime |
|
2 | ||
3 |
from django.utils.six.moves.urllib import parse as urllib |
|
4 | ||
5 |
from passerelle.base import signature |
|
6 | ||
7 | ||
8 |
def test_signature(): |
|
9 |
KEY = 'xyz' |
|
10 |
STRING = 'aaa' |
|
11 |
URL = 'http://example.net/api/?coucou=zob' |
|
12 |
QUERY = 'coucou=zob' |
|
13 |
OTHER_KEY = 'abc' |
|
14 | ||
15 |
# Passing test |
|
16 |
assert signature.check_string(STRING, signature.sign_string(STRING, KEY), KEY) |
|
17 |
assert signature.check_query(signature.sign_query(QUERY, KEY), KEY) |
|
18 |
assert signature.check_url(signature.sign_url(URL, KEY), KEY) |
|
19 | ||
20 |
# Not passing tests |
|
21 |
assert not signature.check_string(STRING, signature.sign_string(STRING, KEY), OTHER_KEY) |
|
22 |
assert not signature.check_query(signature.sign_query(QUERY, KEY), OTHER_KEY) |
|
23 |
assert not signature.check_url(signature.sign_url(URL, KEY), OTHER_KEY) |
|
24 |
assert not signature.check_url('%s&foo=bar' % signature.sign_url(URL, KEY), KEY) |
|
25 | ||
26 |
# Test URL is preserved |
|
27 |
assert URL in signature.sign_url(URL, KEY) |
|
28 |
assert QUERY in signature.sign_query(QUERY, KEY) |
|
29 | ||
30 |
# Test signed URL expected parameters |
|
31 |
assert '&algo=sha256&' in signature.sign_url(URL, KEY) |
|
32 |
assert '×tamp=' in signature.sign_url(URL, KEY) |
|
33 |
assert '&nonce=' in signature.sign_url(URL, KEY) |
|
34 | ||
35 |
# Test unicode key conversion to UTF-8 |
|
36 |
assert signature.check_url(signature.sign_url(URL, u'\xe9\xe9'), b'\xc3\xa9\xc3\xa9') |
|
37 |
assert signature.check_url(signature.sign_url(URL, b'\xc3\xa9\xc3\xa9'), u'\xe9\xe9') |
|
38 | ||
39 |
# Test timedelta parameter |
|
40 |
now = datetime.datetime.utcnow() |
|
41 |
assert '×tamp=%s' % urllib.quote(now.strftime('%Y-%m-%dT%H:%M:%SZ')) in \ |
|
42 |
signature.sign_url(URL, KEY, timestamp=now) |
|
43 | ||
44 |
# Test nonce parameter |
|
45 |
assert '&nonce=uuu&' in signature.sign_url(URL, KEY, nonce='uuu') |
|
46 |
assert '&nonce=' in signature.sign_url(URL, KEY) |
|
47 |
assert '&nonce=' not in signature.sign_url(URL, KEY, nonce='') |
|
48 | ||
49 |
# Test known_nonce |
|
50 |
def known_nonce(nonce): |
|
51 |
return nonce == 'xxx' |
|
52 |
assert signature.check_url(signature.sign_url(URL, KEY), KEY, known_nonce=known_nonce) |
|
53 |
assert signature.check_url(signature.sign_url(URL, KEY, nonce='zzz'), KEY, known_nonce=known_nonce) |
|
54 |
assert not signature.check_url(signature.sign_url(URL, KEY, nonce='xxx'), KEY, known_nonce=known_nonce) |
|
55 |
assert not signature.check_url(signature.sign_url(URL, KEY, nonce=''), KEY, known_nonce=known_nonce) |
|
56 | ||
57 |
# Test timedelta |
|
58 |
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=20)) |
|
59 |
assert signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) |
|
60 |
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY, timedelta=10) |
|
61 |
now = (datetime.datetime.utcnow() - datetime.timedelta(seconds=40)) |
|
62 |
assert not signature.check_url(signature.sign_url(URL, KEY, timestamp=now), KEY) |
|
0 |
- |