Ticket #12417: ticket12417-v2.diff

File ticket12417-v2.diff, 28.0 KB (added by simon, 15 years ago)

Updated based on feedback on the mailing list

  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 828aef5..87a721b 100644
    a b LOGIN_REDIRECT_URL = '/accounts/profile/'  
    383383# The number of days a password reset link is valid for
    384384PASSWORD_RESET_TIMEOUT_DAYS = 3
    385385
     386##################
     387# SIGNED COOKIES #
     388##################
     389
     390COOKIE_SIGNER_BACKEND = 'django.utils.signed.TimestampSigner'
     391
    386392########
    387393# CSRF #
    388394########
  • django/http/__init__.py

    diff --git a/django/http/__init__.py b/django/http/__init__.py
    index 7b0c469..d81a75e 100644
    a b from django.http.multipartparser import MultiPartParser  
    1616from django.conf import settings
    1717from django.core.files import uploadhandler
    1818from utils import *
     19from django.utils import signed
    1920
    2021RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
    2122
    absolute_http_url_re = re.compile(r"^https?://", re.I)  
    2425class Http404(Exception):
    2526    pass
    2627
     28RAISE_ERROR = object()
     29
    2730class HttpRequest(object):
    2831    """A basic HTTP request."""
    2932
    class HttpRequest(object):  
    6063    def get_full_path(self):
    6164        return ''
    6265
     66    def get_signed_cookie(self, key, default=RAISE_ERROR, salt = '',
     67            max_age=None):
     68        """
     69        Attempts to return a signed cookie. If the signature fails or the
     70        cookie has expired, raises an exception... unless you provide the
     71        default argument in which case that value will be returned instead.
     72        """
     73        try:
     74            cookie_value = self.COOKIES[key].encode('utf-8')
     75        except KeyError:
     76            if default is not RAISE_ERROR:
     77                return default
     78            else:
     79                raise
     80        try:
     81            value = signed.get_cookie_signer().unsign(
     82                cookie_value, salt = key + salt, max_age = max_age
     83            )
     84        except signed.BadSignature:
     85            if default is not RAISE_ERROR:
     86                return default
     87            else:
     88                raise
     89        return value
     90
    6391    def build_absolute_uri(self, location=None):
    6492        """
    6593        Builds an absolute URI from the location and the variables available in
    class HttpResponse(object):  
    357385        if secure:
    358386            self.cookies[key]['secure'] = True
    359387
     388    def set_signed_cookie(self, key, value, salt = '', **kwargs):
     389        value = signed.get_cookie_signer().sign(value, salt=key + salt)
     390        return self.set_cookie(key, value, **kwargs)
     391
    360392    def delete_cookie(self, key, path='/', domain=None):
    361393        self.set_cookie(key, max_age=0, path=path, domain=domain,
    362394                        expires='Thu, 01-Jan-1970 00:00:00 GMT')
  • new file django/utils/baseconv.py

    diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py
    new file mode 100644
    index 0000000..f1d5aa0
    - +  
     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
     13class BaseConverter(object):
     14    decimal_digits = "0123456789"
     15   
     16    def __init__(self, digits):
     17        self.digits = digits
     18   
     19    def from_int(self, i):
     20        return self.convert(i, self.decimal_digits, self.digits)
     21   
     22    def to_int(self, s):
     23        return int(self.convert(s, self.digits, self.decimal_digits))
     24   
     25    def convert(number, fromdigits, todigits):
     26        # Based on http://code.activestate.com/recipes/111286/
     27        if str(number)[0] == '-':
     28            number = str(number)[1:]
     29            neg = 1
     30        else:
     31            neg = 0
     32
     33        # make an integer out of the number
     34        x = 0
     35        for digit in str(number):
     36           x = x * len(fromdigits) + fromdigits.index(digit)
     37   
     38        # create the result in base 'len(todigits)'
     39        if x == 0:
     40            res = todigits[0]
     41        else:
     42            res = ""
     43            while x > 0:
     44                digit = x % len(todigits)
     45                res = todigits[digit] + res
     46                x = int(x / len(todigits))
     47            if neg:
     48                res = '-' + res
     49        return res
     50    convert = staticmethod(convert)
     51
     52base2 = BaseConverter('01')
     53base16 = BaseConverter('0123456789ABCDEF')
     54base36 = BaseConverter('0123456789abcdefghijklmnopqrstuvwxyz')
     55base62 = BaseConverter(
     56    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
     57)
  • new file django/utils/signed.py

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

    diff --git a/docs/index.txt b/docs/index.txt
    index cec1d76..41ec97e 100644
    a b Other batteries included  
    163163    * :ref:`Comments <ref-contrib-comments-index>` | :ref:`Moderation <ref-contrib-comments-moderation>` | :ref:`Custom comments <ref-contrib-comments-custom>`
    164164    * :ref:`Content types <ref-contrib-contenttypes>`
    165165    * :ref:`Cross Site Request Forgery protection <ref-contrib-csrf>`
     166    * :ref:`Cryptographic signing <topics-cryptographic-signing>`
    166167    * :ref:`Databrowse <ref-contrib-databrowse>`
    167168    * :ref:`E-mail (sending) <topics-email>`
    168169    * :ref:`Flatpages <ref-contrib-flatpages>`
  • docs/ref/request-response.txt

    diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
    index 77d991b..28b4eaa 100644
    a b Methods  
    221221
    222222   Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
    223223
     224.. method:: HttpRequest.get_signed_cookie(key, default=raise_error, salt = '', max_age=None)
     225
     226   .. versionadded:: 1.2
     227
     228   Returns a cookie value for a signed cookie, or raises a
     229   ``utils.BadSignature`` exception if the signature is no longer valid. If
     230   you provide the ``default`` argument the exception will be suppressed and
     231   that default value will be returned instead.
     232   
     233   The optional salt argument can be used to provide extra protection against
     234   brute force attacks on your secret key. If supplied, the max_age argument
     235   will be checked against the signed timestamp attached to the cookie value
     236   to ensure the cookie is no older than max_age seconds.
     237   
     238   For example::
     239   
     240          >>> request.get_signed_cookie('name')
     241          'Tony'
     242          >>> request.get_signed_cookie('name', salt='name-salt')
     243          'Tony' # assuming cookie was set using the same salt
     244          >>> request.get_signed_cookie('non-existing-cookie')
     245          ...
     246          KeyError: 'non-existing-cookie'
     247          >>> request.get_signed_cookie('non-existing-cookie', default=False)
     248          False
     249          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
     250          ...
     251          BadSignature: ...
     252          >>> request.get_signed_cookie('name', max_age=60)
     253          ...
     254          SignatureExpired: Signature age 1677.3839159 > 60 seconds
     255          >>> request.get_signed_cookie('name', max_age=60, default=False)
     256          False
     257   
     258   See :ref:`cryptographic signing <topics-signing>` for more information.
     259
    224260.. method:: HttpRequest.is_secure()
    225261
    226262   Returns ``True`` if the request is secure; that is, if it was made with
    Methods  
    522558
    523559    .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
    524560
     561.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None)
     562
     563    Like ``set_cookie()``, but :ref:`cryptographically signs <topics-signing>`
     564    the cookie before setting it. Use in conjunction with
     565    ``HttpRequest.get_signed_cookie``. You can use the optional ``salt``
     566    argument for added key strength, but you will need to remember to pass it
     567    to the corresponding ``get_signed_cookie`` call.
     568
    525569.. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
    526570
    527571    Deletes the cookie with the given key. Fails silently if the key doesn't
  • docs/topics/index.txt

    diff --git a/docs/topics/index.txt b/docs/topics/index.txt
    index 7fa283a..b80c101 100644
    a b Introductions to all the key parts of Django you'll need to know:  
    2020   auth
    2121   cache
    2222   conditional-view-processing
     23   signing
    2324   email
    2425   i18n
    2526   pagination
  • new file docs/topics/signing.txt

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

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

    diff --git a/tests/regressiontests/signed_cookie_tests/tests.py b/tests/regressiontests/signed_cookie_tests/tests.py
    new file mode 100644
    index 0000000..cccc222
    - +  
     1from django.test import TestCase
     2from django.http import HttpRequest, HttpResponse
     3from django.utils import signed
     4import time
     5
     6class SignedCookieTest(TestCase):
     7
     8    def test_can_set_and_read_signed_cookies(self):
     9        response = HttpResponse()
     10        response.set_signed_cookie('c', 'hello')
     11        self.assert_('c' in response.cookies)
     12        self.assert_(response.cookies['c'].value.startswith('hello:'))
     13        request = HttpRequest()
     14        request.COOKIES['c'] = response.cookies['c'].value
     15        value = request.get_signed_cookie('c')
     16        self.assertEqual(value, u'hello')
     17
     18    def test_can_use_salt(self):
     19        response = HttpResponse()
     20        response.set_signed_cookie('a', 'hello', salt='one')
     21        request = HttpRequest()
     22        request.COOKIES['a'] = response.cookies['a'].value
     23        value = request.get_signed_cookie('a', salt='one')
     24        self.assertEqual(value, u'hello')
     25        self.assertRaises(signed.BadSignature,
     26            request.get_signed_cookie, 'a', salt='two'
     27        )
     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(signed.BadSignature,
     35            request.get_signed_cookie, 'c'
     36        )
     37
     38    def test_default_argument_supresses_exceptions(self):
     39        response = HttpResponse()
     40        response.set_signed_cookie('c', 'hello')
     41        request = HttpRequest()
     42        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
     43        self.assertEqual(request.get_signed_cookie('c', default=None), None)
     44
     45    def test_max_age_argument(self):
     46        old_time = time.time
     47        time.time = lambda: 123456789
     48        v = u'hello'
     49        try:
     50            response = HttpResponse()
     51            response.set_signed_cookie('c', v)
     52            request = HttpRequest()
     53            request.COOKIES['c'] = response.cookies['c'].value
     54
     55            self.assertEqual(request.get_signed_cookie('c'), v)
     56
     57            time.time = lambda: 123456800
     58
     59            self.assertEqual(request.get_signed_cookie('c', max_age=12), v)
     60            self.assertEqual(request.get_signed_cookie('c', max_age=11), v)
     61            self.assertRaises(
     62                signed.SignatureExpired, request.get_signed_cookie, 'c',
     63                max_age = 10
     64            )
     65        finally:
     66            time.time = old_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..d149937
    - +  
     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
  • new file tests/regressiontests/utils/signed.py

    diff --git a/tests/regressiontests/utils/signed.py b/tests/regressiontests/utils/signed.py
    new file mode 100644
    index 0000000..3720d2b
    - +  
     1from unittest import TestCase
     2from django.utils import signed
     3from django.utils.hashcompat import sha_constructor
     4from django.utils.encoding import force_unicode
     5import time
     6
     7class TestSigner(TestCase):
     8
     9    def test_signature(self):
     10        "signature() method should generate a signature"
     11        signer = signed.Signer('predictable-secret')
     12        signer2 = signed.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                signed.base64_hmac(s, sha_constructor(
     21                    'signer' + 'predictable-secret'
     22                ).hexdigest())
     23            )
     24            self.assertNotEqual(signer.signature(s), signer2.signature(s))
     25
     26    def test_signature_with_salt(self):
     27        "signature(value, salt=...) should work"
     28        signer = signed.Signer('predictable-secret')
     29        self.assertEqual(
     30            signer.signature('hello', salt='extra-salt'),
     31            signed.base64_hmac('hello', sha_constructor(
     32                'signer' + 'predictable-secret' + 'extra-salt'
     33            ).hexdigest())
     34        )
     35        self.assertNotEqual(
     36            signer.signature('hello', salt='one'),
     37            signer.signature('hello', salt='two'),
     38        )
     39   
     40    def test_sign_unsign(self):
     41        "sign/unsign should be reversible"
     42        signer = signed.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.assert_(
     52                force_unicode(example) != force_unicode(signer.sign(example))
     53            )
     54            self.assertEqual(example, signer.unsign(signer.sign(example)))
     55
     56    def unsign_detects_tampering(self):
     57        "unsign should raise an exception if the value has been tampered with"
     58        signer = signed.Signer('predictable-secret')
     59        value = 'Another string'
     60        signed_value = signer.sign(value)
     61        transforms = (
     62            lambda s: s.upper(),
     63            lambda s: s + 'a',
     64            lambda s: 'a' + s[1:],
     65            lambda s: s.replace(':', ''),
     66        )
     67        self.assertEqual(value, signer.unsign(signed_value))
     68        for transform in transforms:
     69            self.assertRaises(
     70                signed.BadSignature, signer.unsign, transform(signed_value)
     71            )
     72
     73    def test_dumps_loads(self):
     74        "dumps and loads be reversible for any JSON serializable object"
     75        objects = (
     76            ['a', 'list'],
     77            'a string',
     78            u'a unicode string \u2019',
     79            {'a': 'dictionary'},
     80        )
     81        for o in objects:
     82            self.assert_(o != signed.dumps(o))
     83            self.assertEqual(o, signed.loads(signed.dumps(o)))
     84
     85    def test_decode_detects_tampering(self):
     86        "loads should raise exception for tampered objects"
     87        transforms = (
     88            lambda s: s.upper(),
     89            lambda s: s + 'a',
     90            lambda s: 'a' + s[1:],
     91            lambda s: s.replace(':', ''),
     92        )
     93        value = {'foo': 'bar', 'baz': 1}
     94        encoded = signed.dumps(value)
     95        self.assertEqual(value, signed.loads(encoded))
     96        for transform in transforms:
     97            self.assertRaises(
     98                signed.BadSignature, signed.loads, transform(encoded)
     99            )
     100
     101class TestTimestampSigner(TestCase):
     102
     103    def test_timestamp_signer(self):
     104        old_time = time.time
     105        time.time = lambda: 123456789
     106        try:
     107            signer = signed.TimestampSigner('predictable-key')
     108            v = u'hello'
     109            ts = signer.sign(v)
     110            self.assertNotEqual(ts,signed.Signer('predictable-key').sign(v))
     111
     112            self.assertEqual(signer.unsign(ts), v)
     113
     114            time.time = lambda: 123456800
     115
     116            self.assertEqual(signer.unsign(ts, max_age=12), v)
     117            self.assertEqual(signer.unsign(ts, max_age=11), v)
     118            self.assertRaises(
     119                signed.SignatureExpired, signer.unsign, ts, max_age=10
     120            )
     121        finally:
     122            time.time = old_time
  • tests/regressiontests/utils/tests.py

    diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
    index 6258b81..6c6dee0 100644
    a b import timesince  
    1111import datastructures
    1212import dateformat
    1313import itercompat
     14from baseconv import TestBaseConv
     15from signed import TestSigner, TestTimestampSigner
    1416from decorators import DecoratorFromMiddlewareTests
    1517
    1618# We need this because "datastructures" uses sorted() and the tests are run in
Back to Top