Ticket #12417: ticket12417.diff

File ticket12417.diff, 28.1 KB (added by simon, 15 years ago)
  • 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..351d1a0 100644
    a b except ImportError:  
    1111    from cgi import parse_qsl
    1212
    1313from django.utils.datastructures import MultiValueDict, ImmutableList
     14from django.utils.importlib import import_module
    1415from django.utils.encoding import smart_str, iri_to_uri, force_unicode
    1516from django.http.multipartparser import MultiPartParser
    1617from django.conf import settings
    1718from django.core.files import uploadhandler
    1819from utils import *
     20from django.utils import signed
    1921
    2022RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
    2123
    absolute_http_url_re = re.compile(r"^https?://", re.I)  
    2426class Http404(Exception):
    2527    pass
    2628
     29def get_cookie_signer():
     30    modpath = settings.COOKIE_SIGNER_BACKEND
     31    module, attr = modpath.rsplit('.', 1)
     32    try:
     33        mod = import_module(module)
     34    except ImportError, e:
     35        raise ImproperlyConfigured(
     36            'Error importing cookie signer %s: "%s"' % (modpath, e)
     37        )
     38    try:
     39        Signer = getattr(mod, attr)
     40    except AttributeError, e:
     41        raise ImproperlyConfigured(
     42            'Error importing cookie signer %s: "%s"' % (modpath, e)
     43        )
     44    return Signer(settings.SECRET_KEY)
     45
     46raise_error = object()
     47
    2748class HttpRequest(object):
    2849    """A basic HTTP request."""
    2950
    class HttpRequest(object):  
    6081    def get_full_path(self):
    6182        return ''
    6283
     84    def get_signed_cookie(self, key, default=raise_error, salt = '',
     85            max_age=None):
     86        """
     87        Attempts to return a signed cookie. If the signature fails or the
     88        cookie has expired, raises an exception... unless you provide the
     89        default argument in which case that value will be returned instead.
     90        """
     91        try:
     92            cookie_value = self.COOKIES[key].encode('utf-8')
     93        except KeyError:
     94            if default is not raise_error:
     95                return default
     96            else:
     97                raise
     98        try:
     99            value = get_cookie_signer().unsign(
     100                cookie_value, salt = salt, max_age = max_age
     101            )
     102        except signed.BadSignature:
     103            if default is not raise_error:
     104                return default
     105            else:
     106                raise
     107        return value
     108
    63109    def build_absolute_uri(self, location=None):
    64110        """
    65111        Builds an absolute URI from the location and the variables available in
    class HttpResponse(object):  
    357403        if secure:
    358404            self.cookies[key]['secure'] = True
    359405
     406    def set_signed_cookie(self, key, value, salt = '', **kwargs):
     407        value = get_cookie_signer().sign(value, salt=salt)
     408        return self.set_cookie(key, value, **kwargs)
     409
    360410    def delete_cookie(self, key, path='/', domain=None):
    361411        self.set_cookie(key, max_age=0, path=path, domain=domain,
    362412                        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..b31c0df
    - +  
     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
     40import hmac, base64, time
     41
     42def dumps(obj, key = None, compress = False, salt = ''):
     43    """
     44    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
     45    None, settings.SECRET_KEY is used instead.
     46   
     47    If compress is True (not the default) checks if compressing using zlib can
     48    save some space. Prepends a '.' to signify compression. This is included
     49    in the signature, to protect against zip bombs.
     50   
     51    salt can be used to further salt the hash, in case you're worried
     52    that the NSA might try to brute-force your SHA-1 protected secret.
     53    """
     54    json = simplejson.dumps(obj, separators=(',', ':'))
     55    is_compressed = False # Flag for if it's been compressed or not
     56    if compress:
     57        import zlib # Avoid zlib dependency unless compress is being used
     58        compressed = zlib.compress(json)
     59        if len(compressed) < (len(json) - 1):
     60            json = compressed
     61            is_compressed = True
     62    base64d = b64_encode(json).strip('=')
     63    if is_compressed:
     64        base64d = '.' + base64d
     65    return TimestampSigner(key).sign(base64d, salt=salt)
     66
     67def loads(s, key = None, salt = '', max_age=None):
     68    "Reverse of dumps(), raises BadSignature if signature fails"
     69    try:
     70        base64d = smart_str(TimestampSigner(key).unsign(
     71            s, salt=salt, max_age=max_age
     72        ))
     73    except BadSignature:
     74        raise
     75    decompress = False
     76    if base64d[0] == '.':
     77        # It's compressed; uncompress it first
     78        base64d = base64d[1:]
     79        decompress = True
     80    json = b64_decode(base64d)
     81    if decompress:
     82        import zlib
     83        jsond = zlib.decompress(json)
     84    return simplejson.loads(json)
     85
     86def b64_encode(s):
     87    return base64.urlsafe_b64encode(s).strip('=')
     88
     89def b64_decode(s):
     90    return base64.urlsafe_b64decode(s + '=' * (len(s) % 4))
     91
     92def base64_hmac(value, key):
     93    return b64_encode(
     94        (hmac.new(key, value, sha_constructor).digest())
     95    )
     96
     97class BadSignature(Exception):
     98    "Signature does not match"
     99    pass
     100
     101class SignatureExpired(BadSignature):
     102    "Signature timestamp is older than required max_age"
     103    pass
     104
     105class Signer(object):
     106    def __init__(self, key=None):
     107        self.key = key or settings.SECRET_KEY
     108
     109    def signature(self, value, salt=''):
     110        if salt:
     111            # sha1 the appended salt, to avoid hash extension attacks
     112            key = sha_constructor(self.key + salt).hexdigest()
     113        else:
     114            key = self.key
     115        return base64_hmac(value, key)
     116
     117    def sign(self, value, salt='', sep=':'):
     118        value = smart_str(value)
     119        return '%s%s%s' % (
     120            value, sep, self.signature(value, salt=salt)
     121        )
     122
     123    def unsign(self, signed_value, salt='', sep=':'):
     124        signed_value = smart_str(signed_value)
     125        if not sep in signed_value:
     126            raise BadSignature, "No '%s' found in value" % sep
     127        value, sig = signed_value.rsplit(sep, 1)
     128        expected = self.signature(value, salt=salt)
     129        if sig != expected:
     130            # Important: do NOT include the expected sig in the exception
     131            # message, since it might leak up to an attacker! Can we enforce
     132            # this in the Django debug templates?
     133            raise BadSignature, 'Signature "%s" does not match' % sig
     134        else:
     135            return force_unicode(value)
     136
     137class TimestampSigner(Signer):
     138    def timestamp(self):
     139        return baseconv.base62.from_int(int(time.time()))
     140
     141    def sign(self, value, salt='', sep=':'):
     142        value = smart_str('%s%s%s' % (value, sep, self.timestamp()))
     143        return '%s%s%s' % (
     144            value, sep, self.signature(value, salt=salt)
     145        )
     146
     147    def unsign(self, value, salt='', sep=':', max_age=None):
     148        value, timestamp = super(TimestampSigner, self).unsign(
     149            value, salt=salt, sep=sep
     150        ).rsplit(sep, 1)
     151        timestamp = baseconv.base62.to_int(timestamp)
     152        if max_age is not None:
     153            # Check timestamp is not older than max_age
     154            age = time.time() - timestamp
     155            if age > max_age:
     156                raise SignatureExpired, 'Signature age %s > %s seconds' % (
     157                    age, max_age
     158                )
     159        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..9bf539e
    - +  
     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, 'predictable-secret')
     21            )
     22            self.assertNotEqual(signer.signature(s), signer2.signature(s))
     23
     24    def test_signature_with_salt(self):
     25        "signature(value, salt=...) should work"
     26        signer = signed.Signer('predictable-secret')
     27        self.assertEqual(
     28            signer.signature('hello', salt='extra-salt'),
     29            signed.base64_hmac('hello', sha_constructor(
     30                'predictable-secret' + 'extra-salt'
     31            ).hexdigest())
     32        )
     33        self.assertNotEqual(
     34            signer.signature('hello', salt='one'),
     35            signer.signature('hello', salt='two'),
     36        )
     37   
     38    def test_sign_unsign(self):
     39        "sign/unsign should be reversible"
     40        signer = signed.Signer('predictable-secret')
     41        examples = (
     42            'q;wjmbk;wkmb',
     43            '3098247529087',
     44            '3098247:529:087:',
     45            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
     46            u'\u2019',
     47        )
     48        for example in examples:
     49            self.assert_(
     50                force_unicode(example) != force_unicode(signer.sign(example))
     51            )
     52            self.assertEqual(example, signer.unsign(signer.sign(example)))
     53
     54    def unsign_detects_tampering(self):
     55        "unsign should raise an exception if the value has been tampered with"
     56        signer = signed.Signer('predictable-secret')
     57        value = 'Another string'
     58        signed_value = signer.sign(value)
     59        transforms = (
     60            lambda s: s.upper(),
     61            lambda s: s + 'a',
     62            lambda s: 'a' + s[1:],
     63            lambda s: s.replace(':', ''),
     64        )
     65        self.assertEqual(value, signer.unsign(signed_value))
     66        for transform in transforms:
     67            self.assertRaises(
     68                signed.BadSignature, signer.unsign, transform(signed_value)
     69            )
     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.assert_(o != signed.dumps(o))
     81            self.assertEqual(o, signed.loads(signed.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 = {'foo': 'bar', 'baz': 1}
     92        encoded = signed.dumps(value)
     93        self.assertEqual(value, signed.loads(encoded))
     94        for transform in transforms:
     95            self.assertRaises(
     96                signed.BadSignature, signed.loads, transform(encoded)
     97            )
     98
     99class TestTimestampSigner(TestCase):
     100
     101    def test_timestamp_signer(self):
     102        old_time = time.time
     103        time.time = lambda: 123456789
     104        try:
     105            signer = signed.TimestampSigner('predictable-key')
     106            v = u'hello'
     107            ts = signer.sign(v)
     108            self.assertNotEqual(ts,signed.Signer('predictable-key').sign(v))
     109
     110            self.assertEqual(signer.unsign(ts), v)
     111
     112            time.time = lambda: 123456800
     113
     114            self.assertEqual(signer.unsign(ts, max_age=12), v)
     115            self.assertEqual(signer.unsign(ts, max_age=11), v)
     116            self.assertRaises(
     117                signed.SignatureExpired, signer.unsign, ts, max_age=10
     118            )
     119        finally:
     120            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