Code

Ticket #12417: ticket12417-v5.diff

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

Updated patch to use salted_hmac from django.utils.crypto

Line 
1diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
2index 88aa5a3..c98cab7 100644
3--- a/django/conf/global_settings.py
4+++ b/django/conf/global_settings.py
5@@ -476,6 +476,12 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
6 # The number of days a password reset link is valid for
7 PASSWORD_RESET_TIMEOUT_DAYS = 3
8 
9+###########
10+# SIGNING #
11+###########
12+
13+SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
14+
15 ########
16 # CSRF #
17 ########
18diff --git a/django/core/signing.py b/django/core/signing.py
19new file mode 100644
20index 0000000..c55983b
21--- /dev/null
22+++ b/django/core/signing.py
23@@ -0,0 +1,180 @@
24+"""
25+Functions for creating and restoring url-safe signed JSON objects.
26+
27+The format used looks like this:
28+
29+>>> signed.dumps("hello")
30+'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
31+
32+There are two components here, separatad by a '.'. The first component is a
33+URLsafe base64 encoded JSON of the object passed to dumps(). The second
34+component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
35+
36+signed.loads(s) checks the signature and returns the deserialised object.
37+If the signature fails, a BadSignature exception is raised.
38+
39+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
40+u'hello'
41+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
42+...
43+BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
44+
45+You can optionally compress the JSON prior to base64 encoding it to save
46+space, using the compress=True argument. This checks if compression actually
47+helps and only applies compression if the result is a shorter string:
48+
49+>>> signed.dumps(range(1, 20), compress=True)
50+'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
51+
52+The fact that the string is compressed is signalled by the prefixed '.' at the
53+start of the base64 JSON.
54+
55+There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
56+These functions make use of all of them.
57+"""
58+import base64
59+import time
60+
61+from django.conf import settings
62+from django.core.exceptions import ImproperlyConfigured
63+from django.utils import baseconv, simplejson
64+from django.utils.crypto import constant_time_compare, salted_hmac
65+from django.utils.encoding import force_unicode, smart_str
66+from django.utils.importlib import import_module
67+
68+class BadSignature(Exception):
69+    """
70+    Signature does not match
71+    """
72+    pass
73+
74+
75+class SignatureExpired(BadSignature):
76+    """
77+    Signature timestamp is older than required max_age
78+    """
79+    pass
80+
81+
82+def b64_encode(s):
83+    return base64.urlsafe_b64encode(s).strip('=')
84+
85+
86+def b64_decode(s):
87+    pad = '=' * (-len(s) % 4)
88+    return base64.urlsafe_b64decode(s + pad)
89+
90+
91+def base64_hmac(salt, value, key):
92+    return b64_encode(salted_hmac(salt, value, key).digest())
93+
94+
95+def get_cookie_signer():
96+    modpath = settings.SIGNING_BACKEND
97+    module, attr = modpath.rsplit('.', 1)
98+    try:
99+        mod = import_module(module)
100+    except ImportError, e:
101+        raise ImproperlyConfigured(
102+            'Error importing cookie signer %s: "%s"' % (modpath, e))
103+    try:
104+        Signer = getattr(mod, attr)
105+    except AttributeError, e:
106+        raise ImproperlyConfigured(
107+            'Error importing cookie signer %s: "%s"' % (modpath, e))
108+    return Signer('django.http.cookies' + settings.SECRET_KEY)
109+
110+
111+def dumps(obj, key=None, salt='', compress=False):
112+    """
113+    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
114+    None, settings.SECRET_KEY is used instead.
115+
116+    If compress is True (not the default) checks if compressing using zlib can
117+    save some space. Prepends a '.' to signify compression. This is included
118+    in the signature, to protect against zip bombs.
119+
120+    salt can be used to further salt the hash, in case you're worried
121+    that the NSA might try to brute-force your SHA-1 protected secret.
122+    """
123+    json = simplejson.dumps(obj, separators=(',', ':'))
124+
125+    # Flag for if it's been compressed or not
126+    is_compressed = False
127+
128+    if compress:
129+        # Avoid zlib dependency unless compress is being used
130+        import zlib
131+        compressed = zlib.compress(json)
132+        if len(compressed) < (len(json) - 1):
133+            json = compressed
134+            is_compressed = True
135+    base64d = b64_encode(json)
136+    if is_compressed:
137+        base64d = '.' + base64d
138+    return TimestampSigner(key).sign(base64d, salt=salt)
139+
140+
141+def loads(s, key=None, salt='', max_age=None):
142+    """
143+    Reverse of dumps(), raises BadSignature if signature fails
144+    """
145+    base64d = smart_str(
146+        TimestampSigner(key).unsign(s, salt=salt, max_age=max_age))
147+    decompress = False
148+    if base64d[0] == '.':
149+        # It's compressed; uncompress it first
150+        base64d = base64d[1:]
151+        decompress = True
152+    json = b64_decode(base64d)
153+    if decompress:
154+        import zlib
155+        jsond = zlib.decompress(json)
156+    return simplejson.loads(json)
157+
158+
159+class Signer(object):
160+    def __init__(self, key=None, sep=':'):
161+        self.sep = sep
162+        self.key = key
163+
164+    def signature(self, value, salt=''):
165+        return base64_hmac(salt + 'signer', value, self.key)
166+
167+    def sign(self, value, salt=''):
168+        value = smart_str(value)
169+        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
170+
171+    def unsign(self, signed_value, salt=''):
172+        signed_value = smart_str(signed_value)
173+        if not self.sep in signed_value:
174+            raise BadSignature('No "%s" found in value' % self.sep)
175+        value, sig = signed_value.rsplit(self.sep, 1)
176+        expected = self.signature(value, salt=salt)
177+        if constant_time_compare(sig, expected):
178+            return force_unicode(value)
179+        # Important: do NOT include the expected sig in the exception
180+        # message, since it might leak up to an attacker!
181+        # TODO: Can we enforce this in the Django debug templates?
182+        raise BadSignature('Signature "%s" does not match' % sig)
183+
184+
185+class TimestampSigner(Signer):
186+    def timestamp(self):
187+        return baseconv.base62.from_int(int(time.time()))
188+
189+    def sign(self, value, salt=''):
190+        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
191+        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
192+
193+    def unsign(self, value, salt='', max_age=None):
194+        value, timestamp = super(TimestampSigner, self).unsign(
195+            value, salt=salt).rsplit(self.sep, 1)
196+        timestamp = baseconv.base62.to_int(timestamp)
197+        if max_age is not None:
198+            # Check timestamp is not older than max_age
199+            age = time.time() - timestamp
200+            if age > max_age:
201+                raise SignatureExpired(
202+                    'Signature age %s > %s seconds' % (age, max_age))
203+        return value
204diff --git a/django/http/__init__.py b/django/http/__init__.py
205index 0d28ec0..0a0d665 100644
206--- a/django/http/__init__.py
207+++ b/django/http/__init__.py
208@@ -122,6 +122,7 @@ from django.utils.encoding import smart_str, iri_to_uri, force_unicode
209 from django.utils.http import cookie_date
210 from django.http.multipartparser import MultiPartParser
211 from django.conf import settings
212+from django.core import signing
213 from django.core.files import uploadhandler
214 from utils import *
215 
216@@ -132,6 +133,8 @@ absolute_http_url_re = re.compile(r"^https?://", re.I)
217 class Http404(Exception):
218     pass
219 
220+RAISE_ERROR = object()
221+
222 class HttpRequest(object):
223     """A basic HTTP request."""
224 
225@@ -170,6 +173,30 @@ class HttpRequest(object):
226         # Rather than crash if this doesn't happen, we encode defensively.
227         return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
228 
229+    def get_signed_cookie(self, key, default=RAISE_ERROR, salt='',
230+                          max_age=None):
231+        """
232+        Attempts to return a signed cookie. If the signature fails or the
233+        cookie has expired, raises an exception... unless you provide the
234+        default argument in which case that value will be returned instead.
235+        """
236+        try:
237+            cookie_value = self.COOKIES[key].encode('utf-8')
238+        except KeyError:
239+            if default is not RAISE_ERROR:
240+                return default
241+            else:
242+                raise
243+        try:
244+            value = signing.get_cookie_signer().unsign(
245+                cookie_value, salt=key + salt, max_age=max_age)
246+        except signing.BadSignature:
247+            if default is not RAISE_ERROR:
248+                return default
249+            else:
250+                raise
251+        return value
252+
253     def build_absolute_uri(self, location=None):
254         """
255         Builds an absolute URI from the location and the variables available in
256@@ -584,6 +611,10 @@ class HttpResponse(object):
257         if httponly:
258             self.cookies[key]['httponly'] = True
259 
260+    def set_signed_cookie(self, key, value, salt='', **kwargs):
261+        value = signing.get_cookie_signer().sign(value, salt=key + salt)
262+        return self.set_cookie(key, value, **kwargs)
263+
264     def delete_cookie(self, key, path='/', domain=None):
265         self.set_cookie(key, max_age=0, path=path, domain=domain,
266                         expires='Thu, 01-Jan-1970 00:00:00 GMT')
267@@ -686,4 +717,3 @@ def str_to_unicode(s, encoding):
268         return unicode(s, encoding, 'replace')
269     else:
270         return s
271-
272diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py
273new file mode 100644
274index 0000000..db152f7
275--- /dev/null
276+++ b/django/utils/baseconv.py
277@@ -0,0 +1,58 @@
278+"""
279+Convert numbers from base 10 integers to base X strings and back again.
280+
281+Sample usage:
282+
283+>>> base20 = BaseConverter('0123456789abcdefghij')
284+>>> base20.from_int(1234)
285+'31e'
286+>>> base20.to_int('31e')
287+1234
288+"""
289+
290+
291+class BaseConverter(object):
292+    decimal_digits = "0123456789"
293+
294+    def __init__(self, digits):
295+        self.digits = digits
296+
297+    def from_int(self, i):
298+        return self.convert(i, self.decimal_digits, self.digits)
299+
300+    def to_int(self, s):
301+        return int(self.convert(s, self.digits, self.decimal_digits))
302+
303+    def convert(number, fromdigits, todigits):
304+        # Based on http://code.activestate.com/recipes/111286/
305+        if str(number)[0] == '-':
306+            number = str(number)[1:]
307+            neg = 1
308+        else:
309+            neg = 0
310+
311+        # make an integer out of the number
312+        x = 0
313+        for digit in str(number):
314+            x = x * len(fromdigits) + fromdigits.index(digit)
315+
316+        # create the result in base 'len(todigits)'
317+        if x == 0:
318+            res = todigits[0]
319+        else:
320+            res = ""
321+            while x > 0:
322+                digit = x % len(todigits)
323+                res = todigits[digit] + res
324+                x = int(x / len(todigits))
325+            if neg:
326+                res = '-' + res
327+        return res
328+    convert = staticmethod(convert)
329+
330+base2 = BaseConverter('01')
331+base16 = BaseConverter('0123456789ABCDEF')
332+base36 = BaseConverter('0123456789abcdefghijklmnopqrstuvwxyz')
333+base62 = BaseConverter(
334+    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
335+)
336diff --git a/docs/index.txt b/docs/index.txt
337index 9135d32..8b4ae53 100644
338--- a/docs/index.txt
339+++ b/docs/index.txt
340@@ -171,6 +171,7 @@ Other batteries included
341     * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
342     * :doc:`Content types <ref/contrib/contenttypes>`
343     * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
344+    * :doc:`Cryptographic signing <topics/signing>`
345     * :doc:`Databrowse <ref/contrib/databrowse>`
346     * :doc:`E-mail (sending) <topics/email>`
347     * :doc:`Flatpages <ref/contrib/flatpages>`
348diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
349index 6281120..e17c0a7 100644
350--- a/docs/ref/request-response.txt
351+++ b/docs/ref/request-response.txt
352@@ -240,6 +240,43 @@ Methods
353 
354    Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
355 
356+.. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
357+
358+   .. versionadded:: 1.4
359+
360+   Returns a cookie value for a signed cookie, or raises a
361+   :class:`~django.core.signing.BadSignature` exception if the signature is
362+   no longer valid. If you provide the ``default`` argument the exception
363+   will be suppressed and that default value will be returned instead.
364+
365+   The optional ``salt`` argument can be used to provide extra protection
366+   against brute force attacks on your secret key. If supplied, the
367+   ``max_age`` argument will be checked against the signed timestamp
368+   attached to the cookie value to ensure the cookie is not older than
369+   ``max_age`` seconds.
370+
371+   For example::
372+
373+          >>> request.get_signed_cookie('name')
374+          'Tony'
375+          >>> request.get_signed_cookie('name', salt='name-salt')
376+          'Tony' # assuming cookie was set using the same salt
377+          >>> request.get_signed_cookie('non-existing-cookie')
378+          ...
379+          KeyError: 'non-existing-cookie'
380+          >>> request.get_signed_cookie('non-existing-cookie', False)
381+          False
382+          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
383+          ...
384+          BadSignature: ...
385+          >>> request.get_signed_cookie('name', max_age=60)
386+          ...
387+          SignatureExpired: Signature age 1677.3839159 > 60 seconds
388+          >>> request.get_signed_cookie('name', False, max_age=60)
389+          False
390+
391+   See :ref:`cryptographic signing <topics-signing>` for more information.
392+
393 .. method:: HttpRequest.is_secure()
394 
395    Returns ``True`` if the request is secure; that is, if it was made with
396@@ -618,6 +655,17 @@ Methods
397     .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
398     .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
399 
400+.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
401+
402+    .. versionadded:: 1.4
403+
404+    Like :meth:`~HttpResponse.set_cookie()`, but
405+    :ref:`cryptographically signs <topics-signing>` the cookie before setting
406+    it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`.
407+    You can use the optional ``salt`` argument for added key strength, but
408+    you will need to remember to pass it to the corresponding
409+    :meth:`HttpRequest.get_signed_cookie` call.
410+
411 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
412 
413     Deletes the cookie with the given key. Fails silently if the key doesn't
414diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
415index f5f1226..38977e8 100644
416--- a/docs/ref/settings.txt
417+++ b/docs/ref/settings.txt
418@@ -1647,6 +1647,19 @@ See :tfilter:`allowed date format strings <date>`.
419 
420 See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
421 
422+.. setting:: SIGNING_BACKEND
423+
424+SIGNING_BACKEND
425+---------------
426+
427+.. versionadded:: 1.4
428+
429+Default: 'django.core.signing.TimestampSigner'
430+
431+The backend used for signing cookies and other data.
432+
433+See also the :ref:`topics-signing` documentation.
434+
435 .. setting:: SITE_ID
436 
437 SITE_ID
438diff --git a/docs/topics/index.txt b/docs/topics/index.txt
439index 49a03be..84f9e9f 100644
440--- a/docs/topics/index.txt
441+++ b/docs/topics/index.txt
442@@ -18,6 +18,7 @@ Introductions to all the key parts of Django you'll need to know:
443    auth
444    cache
445    conditional-view-processing
446+   signing
447    email
448    i18n/index
449    logging
450diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
451new file mode 100644
452index 0000000..c94462c
453--- /dev/null
454+++ b/docs/topics/signing.txt
455@@ -0,0 +1,136 @@
456+.. _topics-signing:
457+
458+=====================
459+Cryptographic signing
460+=====================
461+
462+.. module:: django.core.signing
463+   :synopsis: Django's signing framework.
464+
465+.. versionadded:: 1.4
466+
467+The golden rule of Web application security is to never trust data from
468+untrusted sources. Sometimes it can be useful to pass data through an
469+untrusted medium. Cryptographically signed values can be passed through an
470+untrusted channel safe in the knowledge that any tampering will be detected.
471+
472+Django provides both a low-level API for signing values and a high-level API
473+for setting and reading signed cookies, one of the most common uses of
474+signing in Web applications.
475+
476+You may also find signing useful for the following:
477+
478+    * Generating "recover my account" URLs for sending to users who have
479+      lost their password.
480+
481+    * Ensuring data stored in hidden form fields has not been tampered with.
482+
483+    * Generating one-time secret URLs for allowing temporary access to a
484+      protected resource, for example a downloadable file that a user has
485+      paid for.
486+
487+Protecting the SECRET_KEY
488+=========================
489+
490+When you create a new Django project using :djadmin:`startproject`, the
491+``settings.py`` file it generates automatically gets a random
492+:setting:`SECRET_KEY` value. This value is the key to securing signed
493+data -- it is vital you keep this secure, or attackers could use it to
494+generate their own signed values.
495+
496+Using the low-level API
497+=======================
498+
499+.. class:: Signer
500+
501+Django's signing methods live in the ``django.core.signing`` module.
502+To sign a value, first instantiate a ``Signer`` instance::
503+
504+    >>> from django.core.signing import Signer
505+    >>> signer = Signer()
506+    >>> value = signer.sign('My string')
507+    >>> value
508+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
509+
510+The signature is appended to the end of the string, following the colon.
511+You can retrieve the original value using the ``unsign`` method::
512+
513+    >>> original = signer.unsign(value)
514+    >>> original
515+    u'My string'
516+
517+If the signature or value have been altered in any way, a
518+``django.core.signing.BadSigature`` exception will be raised::
519+
520+    >>> value += 'm'
521+    >>> try:
522+    ...    original = signer.unsign(value)
523+    ... except signing.BadSignature:
524+    ...    print "Tampering detected!"
525+
526+By default, the ``Signer`` class uses the :setting:`SECRET_KEY` setting to
527+generate signatures. You can use a different secret by passing it to the
528+``Signer`` constructor::
529+
530+    >>> signer = Signer('my-other-secret')
531+    >>> value = signer.sign('My string')
532+    >>> value
533+    'My string:EkfQJafvGyiofrdGnuthdxImIJw'
534+
535+Using the salt argument
536+-----------------------
537+
538+If you do not wish to use the same key for every signing operation in your
539+application, you can use the optional ``salt`` argument to the ``sign`` and
540+``unsign`` methods to further strengthen your :setting:`SECRET_KEY` against
541+brute force attacks. Using a salt will cause a new key to be derived from
542+both the salt and your :setting:`SECRET_KEY`::
543+
544+    >>> signer = Signer()
545+    >>> signer.sign('My string')
546+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
547+    >>> signer.sign('My string', salt='extra')
548+    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
549+    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw', salt='extra')
550+    u'My string'
551+
552+Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
553+secret.
554+
555+Verifying timestamped values
556+----------------------------
557+
558+.. class:: TimestampSigner
559+
560+``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed
561+timestamp to the value. This allows you to confirm that a signed value was
562+created within a specified period of time::
563+
564+    >>> from django.core.signing import TimestampSigner
565+    >>> signer = TimestampSigner()
566+    >>> value = signer.sign('hello')
567+    >>> value
568+    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
569+    >>> signer.unsign(value)
570+    u'hello'
571+    >>> signer.unsign(value, max_age=10)
572+    ...
573+    SignatureExpired: Signature age 15.5289158821 > 10 seconds
574+    >>> signer.unsign(value, max_age=20)
575+    u'hello'
576+
577+Protecting complex data structures
578+----------------------------------
579+
580+If you wish to protect a list, tuple or dictionary you can do so using the
581+signing module's dumps and loads functions. These imitate Python's pickle
582+module, but uses JSON serialization under the hood. JSON ensures that even
583+if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
584+execute arbitrary commands by exploiting the pickle format.::
585+
586+    >>> from django.core import signing
587+    >>> value = signing.dumps({"foo": "bar"})
588+    >>> value
589+    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
590+    >>> signing.loads(value)
591+    {'foo': 'bar'}
592diff --git a/tests/regressiontests/signed_cookies_tests/__init__.py b/tests/regressiontests/signed_cookies_tests/__init__.py
593new file mode 100644
594index 0000000..e69de29
595diff --git a/tests/regressiontests/signed_cookies_tests/models.py b/tests/regressiontests/signed_cookies_tests/models.py
596new file mode 100644
597index 0000000..71abcc5
598--- /dev/null
599+++ b/tests/regressiontests/signed_cookies_tests/models.py
600@@ -0,0 +1 @@
601+# models.py file for tests to run.
602diff --git a/tests/regressiontests/signed_cookies_tests/tests.py b/tests/regressiontests/signed_cookies_tests/tests.py
603new file mode 100644
604index 0000000..c28892a
605--- /dev/null
606+++ b/tests/regressiontests/signed_cookies_tests/tests.py
607@@ -0,0 +1,61 @@
608+import time
609+
610+from django.core import signing
611+from django.http import HttpRequest, HttpResponse
612+from django.test import TestCase
613+
614+class SignedCookieTest(TestCase):
615+
616+    def test_can_set_and_read_signed_cookies(self):
617+        response = HttpResponse()
618+        response.set_signed_cookie('c', 'hello')
619+        self.assertIn('c', response.cookies)
620+        self.assertTrue(response.cookies['c'].value.startswith('hello:'))
621+        request = HttpRequest()
622+        request.COOKIES['c'] = response.cookies['c'].value
623+        value = request.get_signed_cookie('c')
624+        self.assertEqual(value, u'hello')
625+
626+    def test_can_use_salt(self):
627+        response = HttpResponse()
628+        response.set_signed_cookie('a', 'hello', salt='one')
629+        request = HttpRequest()
630+        request.COOKIES['a'] = response.cookies['a'].value
631+        value = request.get_signed_cookie('a', salt='one')
632+        self.assertEqual(value, u'hello')
633+        self.assertRaises(signing.BadSignature,
634+            request.get_signed_cookie, 'a', salt='two')
635+
636+    def test_detects_tampering(self):
637+        response = HttpResponse()
638+        response.set_signed_cookie('c', 'hello')
639+        request = HttpRequest()
640+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
641+        self.assertRaises(signing.BadSignature,
642+            request.get_signed_cookie, 'c')
643+
644+    def test_default_argument_supresses_exceptions(self):
645+        response = HttpResponse()
646+        response.set_signed_cookie('c', 'hello')
647+        request = HttpRequest()
648+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
649+        self.assertEqual(request.get_signed_cookie('c', default=None), None)
650+
651+    def test_max_age_argument(self):
652+        value = u'hello'
653+        _time = time.time
654+        time.time = lambda: 123456789
655+        try:
656+            response = HttpResponse()
657+            response.set_signed_cookie('c', value)
658+            request = HttpRequest()
659+            request.COOKIES['c'] = response.cookies['c'].value
660+            self.assertEqual(request.get_signed_cookie('c'), value)
661+
662+            time.time = lambda: 123456800
663+            self.assertEqual(request.get_signed_cookie('c', max_age=12), value)
664+            self.assertEqual(request.get_signed_cookie('c', max_age=11), value)
665+            self.assertRaises(signing.SignatureExpired,
666+                request.get_signed_cookie, 'c', max_age = 10)
667+        finally:
668+            time.time = _time
669diff --git a/tests/regressiontests/signing/__init__.py b/tests/regressiontests/signing/__init__.py
670new file mode 100644
671index 0000000..e69de29
672diff --git a/tests/regressiontests/signing/models.py b/tests/regressiontests/signing/models.py
673new file mode 100644
674index 0000000..71abcc5
675--- /dev/null
676+++ b/tests/regressiontests/signing/models.py
677@@ -0,0 +1 @@
678+# models.py file for tests to run.
679diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py
680new file mode 100644
681index 0000000..31daf82
682--- /dev/null
683+++ b/tests/regressiontests/signing/tests.py
684@@ -0,0 +1,117 @@
685+import time
686+
687+from django.core import signing
688+from django.test import TestCase
689+from django.utils.encoding import force_unicode
690+
691+class TestSigner(TestCase):
692+
693+    def test_signature(self):
694+        "signature() method should generate a signature"
695+        signer = signing.Signer('predictable-secret')
696+        signer2 = signing.Signer('predictable-secret2')
697+        for s in (
698+            'hello',
699+            '3098247:529:087:',
700+            u'\u2019'.encode('utf8'),
701+        ):
702+            self.assertEqual(
703+                signer.signature(s),
704+                signing.base64_hmac('signer', s,
705+                    'predictable-secret')
706+            )
707+            self.assertNotEqual(signer.signature(s), signer2.signature(s))
708+
709+    def test_signature_with_salt(self):
710+        "signature(value, salt=...) should work"
711+        signer = signing.Signer('predictable-secret')
712+        self.assertEqual(
713+            signer.signature('hello', salt='extra-salt'),
714+            signing.base64_hmac('extra-salt' + 'signer', 'hello',
715+                'predictable-secret')
716+        )
717+        self.assertNotEqual(
718+            signer.signature('hello', salt='one'),
719+            signer.signature('hello', salt='two'))
720+
721+    def test_sign_unsign(self):
722+        "sign/unsign should be reversible"
723+        signer = signing.Signer('predictable-secret')
724+        examples = (
725+            'q;wjmbk;wkmb',
726+            '3098247529087',
727+            '3098247:529:087:',
728+            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
729+            u'\u2019',
730+        )
731+        for example in examples:
732+            self.assertNotEqual(
733+                force_unicode(example), force_unicode(signer.sign(example)))
734+            self.assertEqual(example, signer.unsign(signer.sign(example)))
735+
736+    def unsign_detects_tampering(self):
737+        "unsign should raise an exception if the value has been tampered with"
738+        signer = signing.Signer('predictable-secret')
739+        value = 'Another string'
740+        signed_value = signer.sign(value)
741+        transforms = (
742+            lambda s: s.upper(),
743+            lambda s: s + 'a',
744+            lambda s: 'a' + s[1:],
745+            lambda s: s.replace(':', ''),
746+        )
747+        self.assertEqual(value, signer.unsign(signed_value))
748+        for transform in transforms:
749+            self.assertRaises(
750+                signing.BadSignature, signer.unsign, transform(signed_value))
751+
752+    def test_dumps_loads(self):
753+        "dumps and loads be reversible for any JSON serializable object"
754+        objects = (
755+            ['a', 'list'],
756+            'a string',
757+            u'a unicode string \u2019',
758+            {'a': 'dictionary'},
759+        )
760+        for o in objects:
761+            self.assertNotEqual(o, signing.dumps(o))
762+            self.assertEqual(o, signing.loads(signing.dumps(o)))
763+
764+    def test_decode_detects_tampering(self):
765+        "loads should raise exception for tampered objects"
766+        transforms = (
767+            lambda s: s.upper(),
768+            lambda s: s + 'a',
769+            lambda s: 'a' + s[1:],
770+            lambda s: s.replace(':', ''),
771+        )
772+        value = {
773+            'foo': 'bar',
774+            'baz': 1,
775+        }
776+        encoded = signing.dumps(value)
777+        self.assertEqual(value, signing.loads(encoded))
778+        for transform in transforms:
779+            self.assertRaises(
780+                signing.BadSignature, signing.loads, transform(encoded))
781+
782+class TestTimestampSigner(TestCase):
783+
784+    def test_timestamp_signer(self):
785+        value = u'hello'
786+        _time = time.time
787+        time.time = lambda: 123456789
788+        try:
789+            signer = signing.TimestampSigner('predictable-key')
790+            ts = signer.sign(value)
791+            self.assertNotEqual(ts,
792+                signing.Signer('predictable-key').sign(value))
793+
794+            self.assertEqual(signer.unsign(ts), value)
795+            time.time = lambda: 123456800
796+            self.assertEqual(signer.unsign(ts, max_age=12), value)
797+            self.assertEqual(signer.unsign(ts, max_age=11), value)
798+            self.assertRaises(
799+                signing.SignatureExpired, signer.unsign, ts, max_age=10)
800+        finally:
801+            time.time = _time
802diff --git a/tests/regressiontests/utils/baseconv.py b/tests/regressiontests/utils/baseconv.py
803new file mode 100644
804index 0000000..90fe77f
805--- /dev/null
806+++ b/tests/regressiontests/utils/baseconv.py
807@@ -0,0 +1,13 @@
808+from unittest import TestCase
809+from django.utils.baseconv import base2, base16, base36, base62
810+
811+class TestBaseConv(TestCase):
812+
813+    def test_baseconv(self):
814+        nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
815+        for convertor in [base2, base16, base36, base62]:
816+            for i in nums:
817+                self.assertEqual(
818+                    i, convertor.to_int(convertor.from_int(i))
819+                )
820+
821diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
822index 5c4c060..2b61627 100644
823--- a/tests/regressiontests/utils/tests.py
824+++ b/tests/regressiontests/utils/tests.py
825@@ -17,3 +17,4 @@ from timesince import *
826 from datastructures import *
827 from tzinfo import *
828 from datetime_safe import *
829+from baseconv import *