Ticket #12417: ticket12417-v4.diff

File ticket12417-v4.diff, 29.3 KB (added by Jannis Leidel, 14 years ago)

Updated ticket with changes discussed in mailing list and ticket comments

  • 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..70fcc44
    - +  
     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 hmac
     36import base64
     37import time
     38
     39from django.conf import settings
     40from django.utils.hashcompat import sha_constructor
     41from django.utils import baseconv, simplejson
     42from django.utils.crypto import constant_time_compare
     43from django.utils.encoding import force_unicode, smart_str
     44from django.utils.importlib import import_module
     45
     46class BadSignature(Exception):
     47    """
     48    Signature does not match
     49    """
     50    pass
     51
     52
     53class SignatureExpired(BadSignature):
     54    """
     55    Signature timestamp is older than required max_age
     56    """
     57    pass
     58
     59
     60def b64_encode(s):
     61    return base64.urlsafe_b64encode(s).strip('=')
     62
     63
     64def b64_decode(s):
     65    pad = '=' * (-len(s) % 4)
     66    return base64.urlsafe_b64decode(s + pad)
     67
     68
     69def base64_hmac(value, key):
     70    return b64_encode((hmac.new(key, value, sha_constructor).digest()))
     71
     72
     73def get_cookie_signer():
     74    modpath = settings.SIGNING_BACKEND
     75    module, attr = modpath.rsplit('.', 1)
     76    try:
     77        mod = import_module(module)
     78    except ImportError, e:
     79        raise ImproperlyConfigured(
     80            'Error importing cookie signer %s: "%s"' % (modpath, e))
     81    try:
     82        Signer = getattr(mod, attr)
     83    except AttributeError, e:
     84        raise ImproperlyConfigured(
     85            'Error importing cookie signer %s: "%s"' % (modpath, e))
     86    return Signer('django.http.cookies' + settings.SECRET_KEY)
     87
     88
     89def dumps(obj, key=None, salt='', compress=False):
     90    """
     91    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
     92    None, settings.SECRET_KEY is used instead.
     93
     94    If compress is True (not the default) checks if compressing using zlib can
     95    save some space. Prepends a '.' to signify compression. This is included
     96    in the signature, to protect against zip bombs.
     97
     98    salt can be used to further salt the hash, in case you're worried
     99    that the NSA might try to brute-force your SHA-1 protected secret.
     100    """
     101    json = simplejson.dumps(obj, separators=(',', ':'))
     102
     103    # Flag for if it's been compressed or not
     104    is_compressed = False
     105
     106    if compress:
     107        # Avoid zlib dependency unless compress is being used
     108        import zlib
     109        compressed = zlib.compress(json)
     110        if len(compressed) < (len(json) - 1):
     111            json = compressed
     112            is_compressed = True
     113    base64d = b64_encode(json)
     114    if is_compressed:
     115        base64d = '.' + base64d
     116    return TimestampSigner(key).sign(base64d, salt=salt)
     117
     118
     119def loads(s, key=None, salt='', max_age=None):
     120    """
     121    Reverse of dumps(), raises BadSignature if signature fails
     122    """
     123    base64d = smart_str(
     124        TimestampSigner(key).unsign(s, salt=salt, max_age=max_age))
     125    decompress = False
     126    if base64d[0] == '.':
     127        # It's compressed; uncompress it first
     128        base64d = base64d[1:]
     129        decompress = True
     130    json = b64_decode(base64d)
     131    if decompress:
     132        import zlib
     133        jsond = zlib.decompress(json)
     134    return simplejson.loads(json)
     135
     136
     137class Signer(object):
     138    def __init__(self, key=None, sep=':'):
     139        self.sep = sep
     140        self.key = key or settings.SECRET_KEY
     141
     142    def signature(self, value, salt=''):
     143        # Derive a new key from the SECRET_KEY, using the optional salt
     144        key = sha_constructor(salt + 'signer' + self.key).hexdigest()
     145        return base64_hmac(value, key)
     146
     147    def sign(self, value, salt=''):
     148        value = smart_str(value)
     149        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
     150
     151    def unsign(self, signed_value, salt=''):
     152        signed_value = smart_str(signed_value)
     153        if not self.sep in signed_value:
     154            raise BadSignature('No "%s" found in value' % self.sep)
     155        value, sig = signed_value.rsplit(self.sep, 1)
     156        expected = self.signature(value, salt=salt)
     157        if constant_time_compare(sig, expected):
     158            return force_unicode(value)
     159        # Important: do NOT include the expected sig in the exception
     160        # message, since it might leak up to an attacker!
     161        # TODO: Can we enforce this in the Django debug templates?
     162        raise BadSignature('Signature "%s" does not match' % sig)
     163
     164
     165class TimestampSigner(Signer):
     166    def timestamp(self):
     167        return baseconv.base62.from_int(int(time.time()))
     168
     169    def sign(self, value, salt=''):
     170        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
     171        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
     172
     173    def unsign(self, value, salt='', max_age=None):
     174        value, timestamp = super(TimestampSigner, self).unsign(
     175            value, salt=salt).rsplit(self.sep, 1)
     176        timestamp = baseconv.base62.to_int(timestamp)
     177        if max_age is not None:
     178            # Check timestamp is not older than max_age
     179            age = time.time() - timestamp
     180            if age > max_age:
     181                raise SignatureExpired(
     182                    'Signature age %s > %s seconds' % (age, max_age))
     183        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..0c28f53
    - +  
     1import time
     2
     3from django.core import signing
     4from django.test import TestCase
     5from django.utils.encoding import force_unicode
     6from django.utils.hashcompat import sha_constructor
     7
     8class TestSigner(TestCase):
     9
     10    def test_signature(self):
     11        "signature() method should generate a signature"
     12        signer = signing.Signer('predictable-secret')
     13        signer2 = signing.Signer('predictable-secret2')
     14        for s in (
     15            'hello',
     16            '3098247:529:087:',
     17            u'\u2019'.encode('utf8'),
     18        ):
     19            self.assertEqual(
     20                signer.signature(s),
     21                signing.base64_hmac(s, sha_constructor(
     22                    'signer' + 'predictable-secret'
     23                ).hexdigest())
     24            )
     25            self.assertNotEqual(signer.signature(s), signer2.signature(s))
     26
     27    def test_signature_with_salt(self):
     28        "signature(value, salt=...) should work"
     29        signer = signing.Signer('predictable-secret')
     30        self.assertEqual(
     31            signer.signature('hello', salt='extra-salt'),
     32            signing.base64_hmac('hello', sha_constructor(
     33                'extra-salt' + 'signer' + 'predictable-secret'
     34            ).hexdigest())
     35        )
     36        self.assertNotEqual(
     37            signer.signature('hello', salt='one'),
     38            signer.signature('hello', salt='two'))
     39
     40    def test_sign_unsign(self):
     41        "sign/unsign should be reversible"
     42        signer = signing.Signer('predictable-secret')
     43        examples = (
     44            'q;wjmbk;wkmb',
     45            '3098247529087',
     46            '3098247:529:087:',
     47            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
     48            u'\u2019',
     49        )
     50        for example in examples:
     51            self.assertNotEqual(
     52                force_unicode(example), force_unicode(signer.sign(example)))
     53            self.assertEqual(example, signer.unsign(signer.sign(example)))
     54
     55    def unsign_detects_tampering(self):
     56        "unsign should raise an exception if the value has been tampered with"
     57        signer = signing.Signer('predictable-secret')
     58        value = 'Another string'
     59        signed_value = signer.sign(value)
     60        transforms = (
     61            lambda s: s.upper(),
     62            lambda s: s + 'a',
     63            lambda s: 'a' + s[1:],
     64            lambda s: s.replace(':', ''),
     65        )
     66        self.assertEqual(value, signer.unsign(signed_value))
     67        for transform in transforms:
     68            self.assertRaises(
     69                signing.BadSignature, signer.unsign, transform(signed_value))
     70
     71    def test_dumps_loads(self):
     72        "dumps and loads be reversible for any JSON serializable object"
     73        objects = (
     74            ['a', 'list'],
     75            'a string',
     76            u'a unicode string \u2019',
     77            {'a': 'dictionary'},
     78        )
     79        for o in objects:
     80            self.assertNotEqual(o, signing.dumps(o))
     81            self.assertEqual(o, signing.loads(signing.dumps(o)))
     82
     83    def test_decode_detects_tampering(self):
     84        "loads should raise exception for tampered objects"
     85        transforms = (
     86            lambda s: s.upper(),
     87            lambda s: s + 'a',
     88            lambda s: 'a' + s[1:],
     89            lambda s: s.replace(':', ''),
     90        )
     91        value = {
     92            'foo': 'bar',
     93            'baz': 1,
     94        }
     95        encoded = signing.dumps(value)
     96        self.assertEqual(value, signing.loads(encoded))
     97        for transform in transforms:
     98            self.assertRaises(
     99                signing.BadSignature, signing.loads, transform(encoded))
     100
     101class TestTimestampSigner(TestCase):
     102
     103    def test_timestamp_signer(self):
     104        value = u'hello'
     105        _time = time.time
     106        time.time = lambda: 123456789
     107        try:
     108            signer = signing.TimestampSigner('predictable-key')
     109            ts = signer.sign(value)
     110            self.assertNotEqual(ts,
     111                signing.Signer('predictable-key').sign(value))
     112
     113            self.assertEqual(signer.unsign(ts), value)
     114            time.time = lambda: 123456800
     115            self.assertEqual(signer.unsign(ts, max_age=12), value)
     116            self.assertEqual(signer.unsign(ts, max_age=11), value)
     117            self.assertRaises(
     118                signing.SignatureExpired, signer.unsign, ts, max_age=10)
     119        finally:
     120            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