Ticket #12417: ticket12417-v5.diff

File ticket12417-v5.diff, 29.0 KB (added by steph, 13 years ago)

Updated patch to use salted_hmac from django.utils.crypto

  • 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..c55983b
    - +  
     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 time
     37
     38from django.conf import settings
     39from django.core.exceptions import ImproperlyConfigured
     40from django.utils import baseconv, simplejson
     41from django.utils.crypto import constant_time_compare, salted_hmac
     42from django.utils.encoding import force_unicode, smart_str
     43from django.utils.importlib import import_module
     44
     45class BadSignature(Exception):
     46    """
     47    Signature does not match
     48    """
     49    pass
     50
     51
     52class SignatureExpired(BadSignature):
     53    """
     54    Signature timestamp is older than required max_age
     55    """
     56    pass
     57
     58
     59def b64_encode(s):
     60    return base64.urlsafe_b64encode(s).strip('=')
     61
     62
     63def b64_decode(s):
     64    pad = '=' * (-len(s) % 4)
     65    return base64.urlsafe_b64decode(s + pad)
     66
     67
     68def base64_hmac(salt, value, key):
     69    return b64_encode(salted_hmac(salt, value, key).digest())
     70
     71
     72def get_cookie_signer():
     73    modpath = settings.SIGNING_BACKEND
     74    module, attr = modpath.rsplit('.', 1)
     75    try:
     76        mod = import_module(module)
     77    except ImportError, e:
     78        raise ImproperlyConfigured(
     79            'Error importing cookie signer %s: "%s"' % (modpath, e))
     80    try:
     81        Signer = getattr(mod, attr)
     82    except AttributeError, e:
     83        raise ImproperlyConfigured(
     84            'Error importing cookie signer %s: "%s"' % (modpath, e))
     85    return Signer('django.http.cookies' + settings.SECRET_KEY)
     86
     87
     88def dumps(obj, key=None, salt='', compress=False):
     89    """
     90    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
     91    None, settings.SECRET_KEY is used instead.
     92
     93    If compress is True (not the default) checks if compressing using zlib can
     94    save some space. Prepends a '.' to signify compression. This is included
     95    in the signature, to protect against zip bombs.
     96
     97    salt can be used to further salt the hash, in case you're worried
     98    that the NSA might try to brute-force your SHA-1 protected secret.
     99    """
     100    json = simplejson.dumps(obj, separators=(',', ':'))
     101
     102    # Flag for if it's been compressed or not
     103    is_compressed = False
     104
     105    if compress:
     106        # Avoid zlib dependency unless compress is being used
     107        import zlib
     108        compressed = zlib.compress(json)
     109        if len(compressed) < (len(json) - 1):
     110            json = compressed
     111            is_compressed = True
     112    base64d = b64_encode(json)
     113    if is_compressed:
     114        base64d = '.' + base64d
     115    return TimestampSigner(key).sign(base64d, salt=salt)
     116
     117
     118def loads(s, key=None, salt='', max_age=None):
     119    """
     120    Reverse of dumps(), raises BadSignature if signature fails
     121    """
     122    base64d = smart_str(
     123        TimestampSigner(key).unsign(s, salt=salt, max_age=max_age))
     124    decompress = False
     125    if base64d[0] == '.':
     126        # It's compressed; uncompress it first
     127        base64d = base64d[1:]
     128        decompress = True
     129    json = b64_decode(base64d)
     130    if decompress:
     131        import zlib
     132        jsond = zlib.decompress(json)
     133    return simplejson.loads(json)
     134
     135
     136class Signer(object):
     137    def __init__(self, key=None, sep=':'):
     138        self.sep = sep
     139        self.key = key
     140
     141    def signature(self, value, salt=''):
     142        return base64_hmac(salt + 'signer', value, self.key)
     143
     144    def sign(self, value, salt=''):
     145        value = smart_str(value)
     146        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
     147
     148    def unsign(self, signed_value, salt=''):
     149        signed_value = smart_str(signed_value)
     150        if not self.sep in signed_value:
     151            raise BadSignature('No "%s" found in value' % self.sep)
     152        value, sig = signed_value.rsplit(self.sep, 1)
     153        expected = self.signature(value, salt=salt)
     154        if constant_time_compare(sig, expected):
     155            return force_unicode(value)
     156        # Important: do NOT include the expected sig in the exception
     157        # message, since it might leak up to an attacker!
     158        # TODO: Can we enforce this in the Django debug templates?
     159        raise BadSignature('Signature "%s" does not match' % sig)
     160
     161
     162class TimestampSigner(Signer):
     163    def timestamp(self):
     164        return baseconv.base62.from_int(int(time.time()))
     165
     166    def sign(self, value, salt=''):
     167        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
     168        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
     169
     170    def unsign(self, value, salt='', max_age=None):
     171        value, timestamp = super(TimestampSigner, self).unsign(
     172            value, salt=salt).rsplit(self.sep, 1)
     173        timestamp = baseconv.base62.to_int(timestamp)
     174        if max_age is not None:
     175            # Check timestamp is not older than max_age
     176            age = time.time() - timestamp
     177            if age > max_age:
     178                raise SignatureExpired(
     179                    'Signature age %s > %s seconds' % (age, max_age))
     180        return value
  • django/http/__init__.py

    diff --git a/django/http/__init__.py b/django/http/__init__.py
    index 0d28ec0..0a0d665 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='',
     177                          max_age=None):
     178        """
     179        Attempts to return a signed cookie. If the signature fails or the
     180        cookie has expired, raises an exception... unless you provide the
     181        default argument in which case that value will be returned instead.
     182        """
     183        try:
     184            cookie_value = self.COOKIES[key].encode('utf-8')
     185        except KeyError:
     186            if default is not RAISE_ERROR:
     187                return default
     188            else:
     189                raise
     190        try:
     191            value = signing.get_cookie_signer().unsign(
     192                cookie_value, salt=key + salt, max_age=max_age)
     193        except signing.BadSignature:
     194            if default is not RAISE_ERROR:
     195                return default
     196            else:
     197                raise
     198        return value
     199
    173200    def build_absolute_uri(self, location=None):
    174201        """
    175202        Builds an absolute URI from the location and the variables available in
    class HttpResponse(object):  
    584611        if httponly:
    585612            self.cookies[key]['httponly'] = True
    586613
     614    def set_signed_cookie(self, key, value, salt='', **kwargs):
     615        value = signing.get_cookie_signer().sign(value, salt=key + salt)
     616        return self.set_cookie(key, value, **kwargs)
     617
    587618    def delete_cookie(self, key, path='/', domain=None):
    588619        self.set_cookie(key, max_age=0, path=path, domain=domain,
    589620                        expires='Thu, 01-Jan-1970 00:00:00 GMT')
    def str_to_unicode(s, encoding):  
    686717        return unicode(s, encoding, 'replace')
    687718    else:
    688719        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..db152f7
    - +  
     1"""
     2Convert numbers from base 10 integers to base X strings and back again.
     3
     4Sample usage:
     5
     6>>> base20 = BaseConverter('0123456789abcdefghij')
     7>>> base20.from_int(1234)
     8'31e'
     9>>> base20.to_int('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 from_int(self, i):
     21        return self.convert(i, self.decimal_digits, self.digits)
     22
     23    def to_int(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..c94462c
    - +  
     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 ``sign`` and
     85``unsign`` methods to further strengthen your :setting:`SECRET_KEY` against
     86brute force attacks. Using a salt will cause a new key to be derived from
     87both the salt and your :setting:`SECRET_KEY`::
     88
     89    >>> signer = Signer()
     90    >>> signer.sign('My string')
     91    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
     92    >>> signer.sign('My string', salt='extra')
     93    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
     94    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw', salt='extra')
     95    u'My string'
     96
     97Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
     98secret.
     99
     100Verifying timestamped values
     101----------------------------
     102
     103.. class:: TimestampSigner
     104
     105``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed
     106timestamp to the value. This allows you to confirm that a signed value was
     107created within a specified period of time::
     108
     109    >>> from django.core.signing import TimestampSigner
     110    >>> signer = TimestampSigner()
     111    >>> value = signer.sign('hello')
     112    >>> value
     113    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
     114    >>> signer.unsign(value)
     115    u'hello'
     116    >>> signer.unsign(value, max_age=10)
     117    ...
     118    SignatureExpired: Signature age 15.5289158821 > 10 seconds
     119    >>> signer.unsign(value, max_age=20)
     120    u'hello'
     121
     122Protecting complex data structures
     123----------------------------------
     124
     125If you wish to protect a list, tuple or dictionary you can do so using the
     126signing module's dumps and loads functions. These imitate Python's pickle
     127module, but uses JSON serialization under the hood. JSON ensures that even
     128if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
     129execute arbitrary commands by exploiting the pickle format.::
     130
     131    >>> from django.core import signing
     132    >>> value = signing.dumps({"foo": "bar"})
     133    >>> value
     134    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
     135    >>> signing.loads(value)
     136    {'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..31daf82
    - +  
     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', 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')
     28        self.assertEqual(
     29            signer.signature('hello', salt='extra-salt'),
     30            signing.base64_hmac('extra-salt' + 'signer', 'hello',
     31                'predictable-secret')
     32        )
     33        self.assertNotEqual(
     34            signer.signature('hello', salt='one'),
     35            signer.signature('hello', salt='two'))
     36
     37    def test_sign_unsign(self):
     38        "sign/unsign should be reversible"
     39        signer = signing.Signer('predictable-secret')
     40        examples = (
     41            'q;wjmbk;wkmb',
     42            '3098247529087',
     43            '3098247:529:087:',
     44            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
     45            u'\u2019',
     46        )
     47        for example in examples:
     48            self.assertNotEqual(
     49                force_unicode(example), force_unicode(signer.sign(example)))
     50            self.assertEqual(example, signer.unsign(signer.sign(example)))
     51
     52    def unsign_detects_tampering(self):
     53        "unsign should raise an exception if the value has been tampered with"
     54        signer = signing.Signer('predictable-secret')
     55        value = 'Another string'
     56        signed_value = signer.sign(value)
     57        transforms = (
     58            lambda s: s.upper(),
     59            lambda s: s + 'a',
     60            lambda s: 'a' + s[1:],
     61            lambda s: s.replace(':', ''),
     62        )
     63        self.assertEqual(value, signer.unsign(signed_value))
     64        for transform in transforms:
     65            self.assertRaises(
     66                signing.BadSignature, signer.unsign, transform(signed_value))
     67
     68    def test_dumps_loads(self):
     69        "dumps and loads be reversible for any JSON serializable object"
     70        objects = (
     71            ['a', 'list'],
     72            'a string',
     73            u'a unicode string \u2019',
     74            {'a': 'dictionary'},
     75        )
     76        for o in objects:
     77            self.assertNotEqual(o, signing.dumps(o))
     78            self.assertEqual(o, signing.loads(signing.dumps(o)))
     79
     80    def test_decode_detects_tampering(self):
     81        "loads should raise exception for tampered objects"
     82        transforms = (
     83            lambda s: s.upper(),
     84            lambda s: s + 'a',
     85            lambda s: 'a' + s[1:],
     86            lambda s: s.replace(':', ''),
     87        )
     88        value = {
     89            'foo': 'bar',
     90            'baz': 1,
     91        }
     92        encoded = signing.dumps(value)
     93        self.assertEqual(value, signing.loads(encoded))
     94        for transform in transforms:
     95            self.assertRaises(
     96                signing.BadSignature, signing.loads, transform(encoded))
     97
     98class TestTimestampSigner(TestCase):
     99
     100    def test_timestamp_signer(self):
     101        value = u'hello'
     102        _time = time.time
     103        time.time = lambda: 123456789
     104        try:
     105            signer = signing.TimestampSigner('predictable-key')
     106            ts = signer.sign(value)
     107            self.assertNotEqual(ts,
     108                signing.Signer('predictable-key').sign(value))
     109
     110            self.assertEqual(signer.unsign(ts), value)
     111            time.time = lambda: 123456800
     112            self.assertEqual(signer.unsign(ts, max_age=12), value)
     113            self.assertEqual(signer.unsign(ts, max_age=11), value)
     114            self.assertRaises(
     115                signing.SignatureExpired, signer.unsign, ts, max_age=10)
     116        finally:
     117            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..90fe77f
    - +  
     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(
     11                    i, convertor.to_int(convertor.from_int(i))
     12                )
     13
  • 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