Ticket #12417: ticket12417-v6.diff

File ticket12417-v6.diff, 28.9 KB (added by Jannis Leidel, 13 years ago)

Updated method argument signature as discussed on the mailing list

  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 88aa5a3..c98cab7 100644
    a b LOGIN_REDIRECT_URL = '/accounts/profile/'  
    476476# The number of days a password reset link is valid for
    477477PASSWORD_RESET_TIMEOUT_DAYS = 3
    478478
     479###########
     480# SIGNING #
     481###########
     482
     483SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
     484
    479485########
    480486# CSRF #
    481487########
  • new file django/core/signing.py

    diff --git a/django/core/signing.py b/django/core/signing.py
    new file mode 100644
    index 0000000..e0fff04
    - +  
     1"""
     2Functions for creating and restoring url-safe signed JSON objects.
     3
     4The format used looks like this:
     5
     6>>> signed.dumps("hello")
     7'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
     8
     9There are two components here, separatad by a '.'. The first component is a
     10URLsafe base64 encoded JSON of the object passed to dumps(). The second
     11component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
     12
     13signed.loads(s) checks the signature and returns the deserialised object.
     14If the signature fails, a BadSignature exception is raised.
     15
     16>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
     17u'hello'
     18>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
     19...
     20BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
     21
     22You can optionally compress the JSON prior to base64 encoding it to save
     23space, using the compress=True argument. This checks if compression actually
     24helps and only applies compression if the result is a shorter string:
     25
     26>>> signed.dumps(range(1, 20), compress=True)
     27'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
     28
     29The fact that the string is compressed is signalled by the prefixed '.' at the
     30start of the base64 JSON.
     31
     32There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
     33These functions make use of all of them.
     34"""
     35import base64
     36import hashlib
     37import random
     38import time
     39import zlib
     40
     41from django.conf import settings
     42from django.core.exceptions import ImproperlyConfigured
     43from django.utils import baseconv, simplejson
     44from django.utils.crypto import constant_time_compare, salted_hmac
     45from django.utils.encoding import force_unicode, smart_str
     46from django.utils.importlib import import_module
     47
     48
     49class BadSignature(Exception):
     50    """
     51    Signature does not match
     52    """
     53    pass
     54
     55
     56class SignatureExpired(BadSignature):
     57    """
     58    Signature timestamp is older than required max_age
     59    """
     60    pass
     61
     62
     63def b64_encode(s):
     64    return base64.urlsafe_b64encode(s).strip('=')
     65
     66
     67def b64_decode(s):
     68    pad = '=' * (-len(s) % 4)
     69    return base64.urlsafe_b64decode(s + pad)
     70
     71
     72def base64_hmac(salt, value, key):
     73    return b64_encode(salted_hmac(salt, value, key).digest())
     74
     75
     76def get_cookie_signer(salt=''):
     77    modpath = settings.SIGNING_BACKEND
     78    module, attr = modpath.rsplit('.', 1)
     79    try:
     80        mod = import_module(module)
     81    except ImportError, e:
     82        raise ImproperlyConfigured(
     83            'Error importing cookie signer %s: "%s"' % (modpath, e))
     84    try:
     85        Signer = getattr(mod, attr)
     86    except AttributeError, e:
     87        raise ImproperlyConfigured(
     88            'Error importing cookie signer %s: "%s"' % (modpath, e))
     89    return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
     90
     91
     92def dumps(obj, key=None, salt='', compress=False):
     93    """
     94    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
     95    None, settings.SECRET_KEY is used instead.
     96
     97    If compress is True (not the default) checks if compressing using zlib can
     98    save some space. Prepends a '.' to signify compression. This is included
     99    in the signature, to protect against zip bombs.
     100
     101    salt can be used to further salt the hash, in case you're worried
     102    that the NSA might try to brute-force your SHA-1 protected secret.
     103    """
     104    json = simplejson.dumps(obj, separators=(',', ':'))
     105
     106    # Flag for if it's been compressed or not
     107    is_compressed = False
     108
     109    if compress:
     110        # Avoid zlib dependency unless compress is being used
     111        compressed = zlib.compress(json)
     112        if len(compressed) < (len(json) - 1):
     113            json = compressed
     114            is_compressed = True
     115    base64d = b64_encode(json)
     116    if is_compressed:
     117        base64d = '.' + base64d
     118    return TimestampSigner(key, salt=salt).sign(base64d)
     119
     120
     121def loads(s, key=None, salt='', max_age=None):
     122    """
     123    Reverse of dumps(), raises BadSignature if signature fails
     124    """
     125    base64d = smart_str(
     126        TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
     127    decompress = False
     128    if base64d[0] == '.':
     129        # It's compressed; uncompress it first
     130        base64d = base64d[1:]
     131        decompress = True
     132    json = b64_decode(base64d)
     133    if decompress:
     134        json = zlib.decompress(json)
     135    return simplejson.loads(json)
     136
     137
     138class Signer(object):
     139    def __init__(self, key=None, sep=':', salt=None):
     140        self.sep = sep
     141        if salt is None:
     142            salt = hashlib.sha1(
     143                str(random.random()) + str(random.random())).hexdigest()[:5]
     144        self.salt = salt
     145        self.key = key or settings.SECRET_KEY
     146
     147    def signature(self, value):
     148        return base64_hmac(self.salt + 'signer', value, self.key)
     149
     150    def sign(self, value):
     151        value = smart_str(value)
     152        return '%s%s%s' % (value, self.sep, self.signature(value))
     153
     154    def unsign(self, signed_value):
     155        signed_value = smart_str(signed_value)
     156        if not self.sep in signed_value:
     157            raise BadSignature('No "%s" found in value' % self.sep)
     158        value, sig = signed_value.rsplit(self.sep, 1)
     159        if constant_time_compare(sig, self.signature(value)):
     160            return force_unicode(value)
     161        raise BadSignature('Signature "%s" does not match' % sig)
     162
     163
     164class TimestampSigner(Signer):
     165    def timestamp(self):
     166        return baseconv.base62.encode(int(time.time()))
     167
     168    def sign(self, value):
     169        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
     170        return '%s%s%s' % (value, self.sep, self.signature(value))
     171
     172    def unsign(self, value, max_age=None):
     173        result =  super(TimestampSigner, self).unsign(value)
     174        value, timestamp = result.rsplit(self.sep, 1)
     175        timestamp = baseconv.base62.decode(timestamp)
     176        if max_age is not None:
     177            # Check timestamp is not older than max_age
     178            age = time.time() - timestamp
     179            if age > max_age:
     180                raise SignatureExpired(
     181                    'Signature age %s > %s seconds' % (age, max_age))
     182        return value
  • django/http/__init__.py

    diff --git a/django/http/__init__.py b/django/http/__init__.py
    index 0d28ec0..a3fd7f5 100644
    a b from django.utils.encoding import smart_str, iri_to_uri, force_unicode  
    122122from django.utils.http import cookie_date
    123123from django.http.multipartparser import MultiPartParser
    124124from django.conf import settings
     125from django.core import signing
    125126from django.core.files import uploadhandler
    126127from utils import *
    127128
    absolute_http_url_re = re.compile(r"^https?://", re.I)  
    132133class Http404(Exception):
    133134    pass
    134135
     136RAISE_ERROR = object()
     137
    135138class HttpRequest(object):
    136139    """A basic HTTP request."""
    137140
    class HttpRequest(object):  
    170173        # Rather than crash if this doesn't happen, we encode defensively.
    171174        return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
    172175
     176    def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None):
     177        """
     178        Attempts to return a signed cookie. If the signature fails or the
     179        cookie has expired, raises an exception... unless you provide the
     180        default argument in which case that value will be returned instead.
     181        """
     182        try:
     183            cookie_value = self.COOKIES[key].encode('utf-8')
     184        except KeyError:
     185            if default is not RAISE_ERROR:
     186                return default
     187            else:
     188                raise
     189        try:
     190            value = signing.get_cookie_signer(salt=key + salt).unsign(
     191                cookie_value, max_age=max_age)
     192        except signing.BadSignature:
     193            if default is not RAISE_ERROR:
     194                return default
     195            else:
     196                raise
     197        return value
     198
    173199    def build_absolute_uri(self, location=None):
    174200        """
    175201        Builds an absolute URI from the location and the variables available in
    class HttpResponse(object):  
    584610        if httponly:
    585611            self.cookies[key]['httponly'] = True
    586612
     613    def set_signed_cookie(self, key, value, salt='', **kwargs):
     614        value = signing.get_cookie_signer(salt=key + salt).sign(value)
     615        return self.set_cookie(key, value, **kwargs)
     616
    587617    def delete_cookie(self, key, path='/', domain=None):
    588618        self.set_cookie(key, max_age=0, path=path, domain=domain,
    589619                        expires='Thu, 01-Jan-1970 00:00:00 GMT')
    def str_to_unicode(s, encoding):  
    686716        return unicode(s, encoding, 'replace')
    687717    else:
    688718        return s
    689 
  • new file django/utils/baseconv.py

    diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py
    new file mode 100644
    index 0000000..4647675
    - +  
     1"""
     2Convert numbers from base 10 integers to base X strings and back again.
     3
     4Sample usage:
     5
     6>>> base20 = BaseConverter('0123456789abcdefghij')
     7>>> base20.encode(1234)
     8'31e'
     9>>> base20.decode('31e')
     101234
     11"""
     12
     13
     14class BaseConverter(object):
     15    decimal_digits = "0123456789"
     16
     17    def __init__(self, digits):
     18        self.digits = digits
     19
     20    def encode(self, i):
     21        return self.convert(i, self.decimal_digits, self.digits)
     22
     23    def decode(self, s):
     24        return int(self.convert(s, self.digits, self.decimal_digits))
     25
     26    def convert(number, fromdigits, todigits):
     27        # Based on http://code.activestate.com/recipes/111286/
     28        if str(number)[0] == '-':
     29            number = str(number)[1:]
     30            neg = 1
     31        else:
     32            neg = 0
     33
     34        # make an integer out of the number
     35        x = 0
     36        for digit in str(number):
     37            x = x * len(fromdigits) + fromdigits.index(digit)
     38
     39        # create the result in base 'len(todigits)'
     40        if x == 0:
     41            res = todigits[0]
     42        else:
     43            res = ""
     44            while x > 0:
     45                digit = x % len(todigits)
     46                res = todigits[digit] + res
     47                x = int(x / len(todigits))
     48            if neg:
     49                res = '-' + res
     50        return res
     51    convert = staticmethod(convert)
     52
     53base2 = BaseConverter('01')
     54base16 = BaseConverter('0123456789ABCDEF')
     55base36 = BaseConverter('0123456789abcdefghijklmnopqrstuvwxyz')
     56base62 = BaseConverter(
     57    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
     58)
  • docs/index.txt

    diff --git a/docs/index.txt b/docs/index.txt
    index 9135d32..8b4ae53 100644
    a b Other batteries included  
    171171    * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
    172172    * :doc:`Content types <ref/contrib/contenttypes>`
    173173    * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
     174    * :doc:`Cryptographic signing <topics/signing>`
    174175    * :doc:`Databrowse <ref/contrib/databrowse>`
    175176    * :doc:`E-mail (sending) <topics/email>`
    176177    * :doc:`Flatpages <ref/contrib/flatpages>`
  • docs/ref/request-response.txt

    diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
    index 6281120..e17c0a7 100644
    a b Methods  
    240240
    241241   Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
    242242
     243.. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
     244
     245   .. versionadded:: 1.4
     246
     247   Returns a cookie value for a signed cookie, or raises a
     248   :class:`~django.core.signing.BadSignature` exception if the signature is
     249   no longer valid. If you provide the ``default`` argument the exception
     250   will be suppressed and that default value will be returned instead.
     251
     252   The optional ``salt`` argument can be used to provide extra protection
     253   against brute force attacks on your secret key. If supplied, the
     254   ``max_age`` argument will be checked against the signed timestamp
     255   attached to the cookie value to ensure the cookie is not older than
     256   ``max_age`` seconds.
     257
     258   For example::
     259
     260          >>> request.get_signed_cookie('name')
     261          'Tony'
     262          >>> request.get_signed_cookie('name', salt='name-salt')
     263          'Tony' # assuming cookie was set using the same salt
     264          >>> request.get_signed_cookie('non-existing-cookie')
     265          ...
     266          KeyError: 'non-existing-cookie'
     267          >>> request.get_signed_cookie('non-existing-cookie', False)
     268          False
     269          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
     270          ...
     271          BadSignature: ...
     272          >>> request.get_signed_cookie('name', max_age=60)
     273          ...
     274          SignatureExpired: Signature age 1677.3839159 > 60 seconds
     275          >>> request.get_signed_cookie('name', False, max_age=60)
     276          False
     277
     278   See :ref:`cryptographic signing <topics-signing>` for more information.
     279
    243280.. method:: HttpRequest.is_secure()
    244281
    245282   Returns ``True`` if the request is secure; that is, if it was made with
    Methods  
    618655    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
    619656    .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
    620657
     658.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
     659
     660    .. versionadded:: 1.4
     661
     662    Like :meth:`~HttpResponse.set_cookie()`, but
     663    :ref:`cryptographically signs <topics-signing>` the cookie before setting
     664    it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`.
     665    You can use the optional ``salt`` argument for added key strength, but
     666    you will need to remember to pass it to the corresponding
     667    :meth:`HttpRequest.get_signed_cookie` call.
     668
    621669.. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
    622670
    623671    Deletes the cookie with the given key. Fails silently if the key doesn't
  • docs/ref/settings.txt

    diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
    index f5f1226..38977e8 100644
    a b See :tfilter:`allowed date format strings <date>`.  
    16471647
    16481648See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
    16491649
     1650.. setting:: SIGNING_BACKEND
     1651
     1652SIGNING_BACKEND
     1653---------------
     1654
     1655.. versionadded:: 1.4
     1656
     1657Default: 'django.core.signing.TimestampSigner'
     1658
     1659The backend used for signing cookies and other data.
     1660
     1661See also the :ref:`topics-signing` documentation.
     1662
    16501663.. setting:: SITE_ID
    16511664
    16521665SITE_ID
  • docs/topics/index.txt

    diff --git a/docs/topics/index.txt b/docs/topics/index.txt
    index 49a03be..84f9e9f 100644
    a b Introductions to all the key parts of Django you'll need to know:  
    1818   auth
    1919   cache
    2020   conditional-view-processing
     21   signing
    2122   email
    2223   i18n/index
    2324   logging
  • new file docs/topics/signing.txt

    diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
    new file mode 100644
    index 0000000..8947212
    - +  
     1.. _topics-signing:
     2
     3=====================
     4Cryptographic signing
     5=====================
     6
     7.. module:: django.core.signing
     8   :synopsis: Django's signing framework.
     9
     10.. versionadded:: 1.4
     11
     12The golden rule of Web application security is to never trust data from
     13untrusted sources. Sometimes it can be useful to pass data through an
     14untrusted medium. Cryptographically signed values can be passed through an
     15untrusted channel safe in the knowledge that any tampering will be detected.
     16
     17Django provides both a low-level API for signing values and a high-level API
     18for setting and reading signed cookies, one of the most common uses of
     19signing in Web applications.
     20
     21You may also find signing useful for the following:
     22
     23    * Generating "recover my account" URLs for sending to users who have
     24      lost their password.
     25
     26    * Ensuring data stored in hidden form fields has not been tampered with.
     27
     28    * Generating one-time secret URLs for allowing temporary access to a
     29      protected resource, for example a downloadable file that a user has
     30      paid for.
     31
     32Protecting the SECRET_KEY
     33=========================
     34
     35When you create a new Django project using :djadmin:`startproject`, the
     36``settings.py`` file it generates automatically gets a random
     37:setting:`SECRET_KEY` value. This value is the key to securing signed
     38data -- it is vital you keep this secure, or attackers could use it to
     39generate their own signed values.
     40
     41Using the low-level API
     42=======================
     43
     44.. class:: Signer
     45
     46Django's signing methods live in the ``django.core.signing`` module.
     47To sign a value, first instantiate a ``Signer`` instance::
     48
     49    >>> from django.core.signing import Signer
     50    >>> signer = Signer()
     51    >>> value = signer.sign('My string')
     52    >>> value
     53    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
     54
     55The signature is appended to the end of the string, following the colon.
     56You can retrieve the original value using the ``unsign`` method::
     57
     58    >>> original = signer.unsign(value)
     59    >>> original
     60    u'My string'
     61
     62If the signature or value have been altered in any way, a
     63``django.core.signing.BadSigature`` exception will be raised::
     64
     65    >>> value += 'm'
     66    >>> try:
     67    ...    original = signer.unsign(value)
     68    ... except signing.BadSignature:
     69    ...    print "Tampering detected!"
     70
     71By default, the ``Signer`` class uses the :setting:`SECRET_KEY` setting to
     72generate signatures. You can use a different secret by passing it to the
     73``Signer`` constructor::
     74
     75    >>> signer = Signer('my-other-secret')
     76    >>> value = signer.sign('My string')
     77    >>> value
     78    'My string:EkfQJafvGyiofrdGnuthdxImIJw'
     79
     80Using the salt argument
     81-----------------------
     82
     83If you do not wish to use the same key for every signing operation in your
     84application, you can use the optional ``salt`` argument to the ``Signer``
     85class to further strengthen your :setting:`SECRET_KEY` against brute force
     86attacks. Using a salt will cause a new key to be derived from both the salt
     87and your :setting:`SECRET_KEY`::
     88
     89    >>> signer = Signer()
     90    >>> signer.sign('My string')
     91    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
     92    >>> signer = Signer(salt='extra')
     93    >>> signer.sign('My string')
     94    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
     95    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
     96    u'My string'
     97
     98Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
     99secret.
     100
     101Verifying timestamped values
     102----------------------------
     103
     104.. class:: TimestampSigner
     105
     106``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed
     107timestamp to the value. This allows you to confirm that a signed value was
     108created within a specified period of time::
     109
     110    >>> from django.core.signing import TimestampSigner
     111    >>> signer = TimestampSigner()
     112    >>> value = signer.sign('hello')
     113    >>> value
     114    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
     115    >>> signer.unsign(value)
     116    u'hello'
     117    >>> signer.unsign(value, max_age=10)
     118    ...
     119    SignatureExpired: Signature age 15.5289158821 > 10 seconds
     120    >>> signer.unsign(value, max_age=20)
     121    u'hello'
     122
     123Protecting complex data structures
     124----------------------------------
     125
     126If you wish to protect a list, tuple or dictionary you can do so using the
     127signing module's dumps and loads functions. These imitate Python's pickle
     128module, but uses JSON serialization under the hood. JSON ensures that even
     129if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
     130execute arbitrary commands by exploiting the pickle format.::
     131
     132    >>> from django.core import signing
     133    >>> value = signing.dumps({"foo": "bar"})
     134    >>> value
     135    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
     136    >>> signing.loads(value)
     137    {'foo': 'bar'}
  • new file tests/regressiontests/signed_cookies_tests/models.py

    diff --git a/tests/regressiontests/signed_cookies_tests/__init__.py b/tests/regressiontests/signed_cookies_tests/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/signed_cookies_tests/models.py b/tests/regressiontests/signed_cookies_tests/models.py
    new file mode 100644
    index 0000000..71abcc5
    - +  
     1# models.py file for tests to run.
  • new file tests/regressiontests/signed_cookies_tests/tests.py

    diff --git a/tests/regressiontests/signed_cookies_tests/tests.py b/tests/regressiontests/signed_cookies_tests/tests.py
    new file mode 100644
    index 0000000..c28892a
    - +  
     1import time
     2
     3from django.core import signing
     4from django.http import HttpRequest, HttpResponse
     5from django.test import TestCase
     6
     7class SignedCookieTest(TestCase):
     8
     9    def test_can_set_and_read_signed_cookies(self):
     10        response = HttpResponse()
     11        response.set_signed_cookie('c', 'hello')
     12        self.assertIn('c', response.cookies)
     13        self.assertTrue(response.cookies['c'].value.startswith('hello:'))
     14        request = HttpRequest()
     15        request.COOKIES['c'] = response.cookies['c'].value
     16        value = request.get_signed_cookie('c')
     17        self.assertEqual(value, u'hello')
     18
     19    def test_can_use_salt(self):
     20        response = HttpResponse()
     21        response.set_signed_cookie('a', 'hello', salt='one')
     22        request = HttpRequest()
     23        request.COOKIES['a'] = response.cookies['a'].value
     24        value = request.get_signed_cookie('a', salt='one')
     25        self.assertEqual(value, u'hello')
     26        self.assertRaises(signing.BadSignature,
     27            request.get_signed_cookie, 'a', salt='two')
     28
     29    def test_detects_tampering(self):
     30        response = HttpResponse()
     31        response.set_signed_cookie('c', 'hello')
     32        request = HttpRequest()
     33        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
     34        self.assertRaises(signing.BadSignature,
     35            request.get_signed_cookie, 'c')
     36
     37    def test_default_argument_supresses_exceptions(self):
     38        response = HttpResponse()
     39        response.set_signed_cookie('c', 'hello')
     40        request = HttpRequest()
     41        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
     42        self.assertEqual(request.get_signed_cookie('c', default=None), None)
     43
     44    def test_max_age_argument(self):
     45        value = u'hello'
     46        _time = time.time
     47        time.time = lambda: 123456789
     48        try:
     49            response = HttpResponse()
     50            response.set_signed_cookie('c', value)
     51            request = HttpRequest()
     52            request.COOKIES['c'] = response.cookies['c'].value
     53            self.assertEqual(request.get_signed_cookie('c'), value)
     54
     55            time.time = lambda: 123456800
     56            self.assertEqual(request.get_signed_cookie('c', max_age=12), value)
     57            self.assertEqual(request.get_signed_cookie('c', max_age=11), value)
     58            self.assertRaises(signing.SignatureExpired,
     59                request.get_signed_cookie, 'c', max_age = 10)
     60        finally:
     61            time.time = _time
  • new file tests/regressiontests/signing/models.py

    diff --git a/tests/regressiontests/signing/__init__.py b/tests/regressiontests/signing/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/regressiontests/signing/models.py b/tests/regressiontests/signing/models.py
    new file mode 100644
    index 0000000..71abcc5
    - +  
     1# models.py file for tests to run.
  • new file tests/regressiontests/signing/tests.py

    diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py
    new file mode 100644
    index 0000000..0b0cacf
    - +  
     1import time
     2
     3from django.core import signing
     4from django.test import TestCase
     5from django.utils.encoding import force_unicode
     6
     7class TestSigner(TestCase):
     8
     9    def test_signature(self):
     10        "signature() method should generate a signature"
     11        signer = signing.Signer('predictable-secret')
     12        signer2 = signing.Signer('predictable-secret2')
     13        for s in (
     14            'hello',
     15            '3098247:529:087:',
     16            u'\u2019'.encode('utf8'),
     17        ):
     18            self.assertEqual(
     19                signer.signature(s),
     20                signing.base64_hmac(signer.salt + 'signer', s,
     21                    'predictable-secret')
     22            )
     23            self.assertNotEqual(signer.signature(s), signer2.signature(s))
     24
     25    def test_signature_with_salt(self):
     26        "signature(value, salt=...) should work"
     27        signer = signing.Signer('predictable-secret', salt='extra-salt')
     28        self.assertEqual(
     29            signer.signature('hello'),
     30                signing.base64_hmac('extra-salt' + 'signer',
     31                'hello', 'predictable-secret'))
     32        self.assertNotEqual(
     33            signing.Signer('predictable-secret', salt='one').signature('hello'),
     34            signing.Signer('predictable-secret', salt='two').signature('hello'))
     35
     36    def test_sign_unsign(self):
     37        "sign/unsign should be reversible"
     38        signer = signing.Signer('predictable-secret')
     39        examples = (
     40            'q;wjmbk;wkmb',
     41            '3098247529087',
     42            '3098247:529:087:',
     43            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
     44            u'\u2019',
     45        )
     46        for example in examples:
     47            self.assertNotEqual(
     48                force_unicode(example), force_unicode(signer.sign(example)))
     49            self.assertEqual(example, signer.unsign(signer.sign(example)))
     50
     51    def unsign_detects_tampering(self):
     52        "unsign should raise an exception if the value has been tampered with"
     53        signer = signing.Signer('predictable-secret')
     54        value = 'Another string'
     55        signed_value = signer.sign(value)
     56        transforms = (
     57            lambda s: s.upper(),
     58            lambda s: s + 'a',
     59            lambda s: 'a' + s[1:],
     60            lambda s: s.replace(':', ''),
     61        )
     62        self.assertEqual(value, signer.unsign(signed_value))
     63        for transform in transforms:
     64            self.assertRaises(
     65                signing.BadSignature, signer.unsign, transform(signed_value))
     66
     67    def test_dumps_loads(self):
     68        "dumps and loads be reversible for any JSON serializable object"
     69        objects = (
     70            ['a', 'list'],
     71            'a string',
     72            u'a unicode string \u2019',
     73            {'a': 'dictionary'},
     74        )
     75        for o in objects:
     76            self.assertNotEqual(o, signing.dumps(o))
     77            self.assertEqual(o, signing.loads(signing.dumps(o)))
     78
     79    def test_decode_detects_tampering(self):
     80        "loads should raise exception for tampered objects"
     81        transforms = (
     82            lambda s: s.upper(),
     83            lambda s: s + 'a',
     84            lambda s: 'a' + s[1:],
     85            lambda s: s.replace(':', ''),
     86        )
     87        value = {
     88            'foo': 'bar',
     89            'baz': 1,
     90        }
     91        encoded = signing.dumps(value)
     92        self.assertEqual(value, signing.loads(encoded))
     93        for transform in transforms:
     94            self.assertRaises(
     95                signing.BadSignature, signing.loads, transform(encoded))
     96
     97class TestTimestampSigner(TestCase):
     98
     99    def test_timestamp_signer(self):
     100        value = u'hello'
     101        _time = time.time
     102        time.time = lambda: 123456789
     103        try:
     104            signer = signing.TimestampSigner('predictable-key')
     105            ts = signer.sign(value)
     106            self.assertNotEqual(ts,
     107                signing.Signer('predictable-key').sign(value))
     108
     109            self.assertEqual(signer.unsign(ts), value)
     110            time.time = lambda: 123456800
     111            self.assertEqual(signer.unsign(ts, max_age=12), value)
     112            self.assertEqual(signer.unsign(ts, max_age=11), value)
     113            self.assertRaises(
     114                signing.SignatureExpired, signer.unsign, ts, max_age=10)
     115        finally:
     116            time.time = _time
  • new file tests/regressiontests/utils/baseconv.py

    diff --git a/tests/regressiontests/utils/baseconv.py b/tests/regressiontests/utils/baseconv.py
    new file mode 100644
    index 0000000..c407aef
    - +  
     1from unittest import TestCase
     2from django.utils.baseconv import base2, base16, base36, base62
     3
     4class TestBaseConv(TestCase):
     5
     6    def test_baseconv(self):
     7        nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
     8        for convertor in [base2, base16, base36, base62]:
     9            for i in nums:
     10                self.assertEqual(i, convertor.decode(convertor.encode(i)))
     11
  • tests/regressiontests/utils/tests.py

    diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
    index 5c4c060..2b61627 100644
    a b from timesince import *  
    1717from datastructures import *
    1818from tzinfo import *
    1919from datetime_safe import *
     20from baseconv import *
Back to Top