# bijoe - BI dashboard # Copyright (C) 2015 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import base64 import datetime import hashlib import hmac import logging import random import urllib from django.utils import six from django.utils.encoding import force_bytes, 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 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() timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ') if nonce is None: nonce = hex(random.getrandbits(128))[2:] new_query = query if new_query: new_query += '&' new_query += urlencode((('algo', algo), ('timestamp', timestamp), ('nonce', nonce))) signature = base64.b64encode(sign_string(new_query, key, algo=algo)) new_query += '&signature=' + quote(signature) return new_query def sign_string(s, key, algo='sha256', timedelta=30): digestmod = getattr(hashlib, algo) 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) def check_query(query, key, known_nonce=None, timedelta=30): res, error = check_query2(query, key, known_nonce=known_nonce, timedelta=timedelta) if not res: key_hash = 'md5:%s' % hashlib.md5(force_bytes(key)).hexdigest()[:6] logging.getLogger(__name__).warning( 'could not check signature of query %r with key %s: %s', query, key_hash, error ) return res def check_query2(query, key, known_nonce, timedelta): parsed = urlparse.parse_qs(query) try: signature = parsed['signature'][0] algo = parsed['algo'][0] timestamp = parsed['timestamp'][0] nonce = parsed['nonce'][0] except KeyError as e: return False, 'missing required field %r' % e.args[0] try: signature = base64.b64decode(parsed['signature'][0]) except Exception as e: return False, 'could not decode base64 signature (%s)' % e if algo not in hashlib.algorithms_guaranteed: return False, 'hash algorithm %s is not supported' % algo try: timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ') except Exception as e: return False, 'could not parse the timestamp %s' % timestamp unsigned_query = query.split('&signature=')[0] if known_nonce is not None and known_nonce(nonce): return False, 'used nonce' if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta): return False, 'timestamp is older than %d seconds' % timedelta if not check_string(unsigned_query, signature, key, algo=algo): return False, 'signature does not match' return True, None def check_string(s, signature, key, algo='sha256'): # constant time compare signature2 = sign_string(s, key, algo=algo) if len(signature2) != len(signature): return False res = 0 for a, b in zip(signature, signature2): res |= a ^ b return res == 0