Projet

Général

Profil

0001-signature-do-not-require-nonce-if-not-verified-41245.patch

Thomas Noël, 02 avril 2020 14:12

Télécharger (7,29 ko)

Voir les différences:

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      | 62 ++++++++++++++++++++++++++++++++++++
 2 files changed, 82 insertions(+), 18 deletions(-)
 create mode 100644 tests/test_signature.py
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 '&timestamp=' 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 '&timestamp=%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
-