diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 828aef5..87a721b 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -383,6 +383,12 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
 # The number of days a password reset link is valid for
 PASSWORD_RESET_TIMEOUT_DAYS = 3
 
+##################
+# SIGNED COOKIES #
+##################
+
+COOKIE_SIGNER_BACKEND = 'django.utils.signed.TimestampSigner'
+
 ########
 # CSRF #
 ########
diff --git a/django/http/__init__.py b/django/http/__init__.py
index 7b0c469..d81a75e 100644
--- a/django/http/__init__.py
+++ b/django/http/__init__.py
@@ -16,6 +16,7 @@ from django.http.multipartparser import MultiPartParser
 from django.conf import settings
 from django.core.files import uploadhandler
 from utils import *
+from django.utils import signed
 
 RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
 
@@ -24,6 +25,8 @@ absolute_http_url_re = re.compile(r"^https?://", re.I)
 class Http404(Exception):
     pass
 
+RAISE_ERROR = object()
+
 class HttpRequest(object):
     """A basic HTTP request."""
 
@@ -60,6 +63,31 @@ class HttpRequest(object):
     def get_full_path(self):
         return ''
 
+    def get_signed_cookie(self, key, default=RAISE_ERROR, salt = '',
+            max_age=None):
+        """
+        Attempts to return a signed cookie. If the signature fails or the 
+        cookie has expired, raises an exception... unless you provide the 
+        default argument in which case that value will be returned instead.
+        """
+        try:
+            cookie_value = self.COOKIES[key].encode('utf-8')
+        except KeyError:
+            if default is not RAISE_ERROR:
+                return default
+            else:
+                raise
+        try:
+            value = signed.get_cookie_signer().unsign(
+                cookie_value, salt = key + salt, max_age = max_age
+            )
+        except signed.BadSignature:
+            if default is not RAISE_ERROR:
+                return default
+            else:
+                raise
+        return value
+
     def build_absolute_uri(self, location=None):
         """
         Builds an absolute URI from the location and the variables available in
@@ -357,6 +385,10 @@ class HttpResponse(object):
         if secure:
             self.cookies[key]['secure'] = True
 
+    def set_signed_cookie(self, key, value, salt = '', **kwargs):
+        value = signed.get_cookie_signer().sign(value, salt=key + salt)
+        return self.set_cookie(key, value, **kwargs)
+
     def delete_cookie(self, key, path='/', domain=None):
         self.set_cookie(key, max_age=0, path=path, domain=domain,
                         expires='Thu, 01-Jan-1970 00:00:00 GMT')
diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py
new file mode 100644
index 0000000..f1d5aa0
--- /dev/null
+++ b/django/utils/baseconv.py
@@ -0,0 +1,57 @@
+"""
+Convert numbers from base 10 integers to base X strings and back again.
+
+Sample usage:
+
+>>> base20 = BaseConverter('0123456789abcdefghij')
+>>> base20.from_int(1234)
+'31e'
+>>> base20.to_int('31e')
+1234
+"""
+
+class BaseConverter(object):
+    decimal_digits = "0123456789"
+    
+    def __init__(self, digits):
+        self.digits = digits
+    
+    def from_int(self, i):
+        return self.convert(i, self.decimal_digits, self.digits)
+    
+    def to_int(self, s):
+        return int(self.convert(s, self.digits, self.decimal_digits))
+    
+    def convert(number, fromdigits, todigits):
+        # Based on http://code.activestate.com/recipes/111286/
+        if str(number)[0] == '-':
+            number = str(number)[1:]
+            neg = 1
+        else:
+            neg = 0
+
+        # make an integer out of the number
+        x = 0
+        for digit in str(number):
+           x = x * len(fromdigits) + fromdigits.index(digit)
+    
+        # create the result in base 'len(todigits)'
+        if x == 0:
+            res = todigits[0]
+        else:
+            res = ""
+            while x > 0:
+                digit = x % len(todigits)
+                res = todigits[digit] + res
+                x = int(x / len(todigits))
+            if neg:
+                res = '-' + res
+        return res
+    convert = staticmethod(convert)
+
+base2 = BaseConverter('01')
+base16 = BaseConverter('0123456789ABCDEF')
+base36 = BaseConverter('0123456789abcdefghijklmnopqrstuvwxyz')
+base62 = BaseConverter(
+    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+)
diff --git a/django/utils/signed.py b/django/utils/signed.py
new file mode 100644
index 0000000..df445ad
--- /dev/null
+++ b/django/utils/signed.py
@@ -0,0 +1,175 @@
+"""
+Functions for creating and restoring url-safe signed JSON objects.
+
+The format used looks like this:
+
+>>> signed.dumps("hello")
+'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
+
+There are two components here, separatad by a '.'. The first component is a 
+URLsafe base64 encoded JSON of the object passed to dumps(). The second 
+component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
+
+signed.loads(s) checks the signature and returns the deserialised object. 
+If the signature fails, a BadSignature exception is raised.
+
+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
+u'hello'
+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
+...
+BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
+
+You can optionally compress the JSON prior to base64 encoding it to save 
+space, using the compress=True argument. This checks if compression actually
+helps and only applies compression if the result is a shorter string:
+
+>>> signed.dumps(range(1, 20), compress=True)
+'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
+
+The fact that the string is compressed is signalled by the prefixed '.' at the
+start of the base64 JSON.
+
+There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'. 
+These functions make use of all of them.
+"""
+
+from django.conf import settings
+from django.utils.hashcompat import sha_constructor
+from django.utils import baseconv, simplejson
+from django.utils.encoding import force_unicode, smart_str
+from django.utils.importlib import import_module
+import hmac, base64, time
+
+def dumps(obj, key = None, compress = False, salt = ''):
+    """
+    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is 
+    None, settings.SECRET_KEY is used instead.
+    
+    If compress is True (not the default) checks if compressing using zlib can
+    save some space. Prepends a '.' to signify compression. This is included 
+    in the signature, to protect against zip bombs.
+    
+    salt can be used to further salt the hash, in case you're worried 
+    that the NSA might try to brute-force your SHA-1 protected secret.
+    """
+    json = simplejson.dumps(obj, separators=(',', ':'))
+    is_compressed = False # Flag for if it's been compressed or not
+    if compress:
+        import zlib # Avoid zlib dependency unless compress is being used
+        compressed = zlib.compress(json)
+        if len(compressed) < (len(json) - 1):
+            json = compressed
+            is_compressed = True
+    base64d = b64_encode(json)
+    if is_compressed:
+        base64d = '.' + base64d
+    return TimestampSigner(key).sign(base64d, salt=salt)
+
+def loads(s, key = None, salt = '', max_age=None):
+    "Reverse of dumps(), raises BadSignature if signature fails"
+    try:
+        base64d = smart_str(TimestampSigner(key).unsign(
+            s, salt=salt, max_age=max_age
+        ))
+    except BadSignature:
+        raise
+    decompress = False
+    if base64d[0] == '.':
+        # It's compressed; uncompress it first
+        base64d = base64d[1:]
+        decompress = True
+    json = b64_decode(base64d)
+    if decompress:
+        import zlib
+        jsond = zlib.decompress(json)
+    return simplejson.loads(json)
+
+def b64_encode(s):
+    return base64.urlsafe_b64encode(s).strip('=')
+
+def b64_decode(s):
+    pad = '=' * (-len(s) % 4)
+    return base64.urlsafe_b64decode(s + pad)
+
+def base64_hmac(value, key):
+    return b64_encode(
+        (hmac.new(key, value, sha_constructor).digest())
+    )
+
+class BadSignature(Exception):
+    "Signature does not match"
+    pass
+
+class SignatureExpired(BadSignature):
+    "Signature timestamp is older than required max_age"
+    pass
+
+def get_cookie_signer():
+    modpath = settings.COOKIE_SIGNER_BACKEND
+    module, attr = modpath.rsplit('.', 1)
+    try:
+        mod = import_module(module)
+    except ImportError, e:
+        raise ImproperlyConfigured(
+            'Error importing cookie signer %s: "%s"' % (modpath, e)
+        )
+    try:
+        Signer = getattr(mod, attr)
+    except AttributeError, e:
+        raise ImproperlyConfigured(
+            'Error importing cookie signer %s: "%s"' % (modpath, e)
+        )
+    return Signer(settings.SECRET_KEY)
+
+class Signer(object):
+    def __init__(self, key=None):
+        self.key = key or settings.SECRET_KEY
+
+    def signature(self, value, salt=''):
+        # Derive a new key from the SECRET_KEY, using the optional salt
+        key = sha_constructor('signer' + self.key + salt).hexdigest()
+        return base64_hmac(value, key)
+
+    def sign(self, value, salt='', sep=':'):
+        value = smart_str(value)
+        return '%s%s%s' % (
+            value, sep, self.signature(value, salt=salt)
+        )
+
+    def unsign(self, signed_value, salt='', sep=':'):
+        signed_value = smart_str(signed_value)
+        if not sep in signed_value:
+            raise BadSignature, "No '%s' found in value" % sep
+        value, sig = signed_value.rsplit(sep, 1)
+        expected = self.signature(value, salt=salt)
+        if sig != expected:
+            # Important: do NOT include the expected sig in the exception 
+            # message, since it might leak up to an attacker! Can we enforce 
+            # this in the Django debug templates?
+            raise BadSignature, 'Signature "%s" does not match' % sig
+        else:
+            return force_unicode(value)
+
+class TimestampSigner(Signer):
+    def timestamp(self):
+        return baseconv.base62.from_int(int(time.time()))
+
+    def sign(self, value, salt='', sep=':'):
+        value = smart_str('%s%s%s' % (value, sep, self.timestamp()))
+        return '%s%s%s' % (
+            value, sep, self.signature(value, salt=salt)
+        )
+
+    def unsign(self, value, salt='', sep=':', max_age=None):
+        value, timestamp = super(TimestampSigner, self).unsign(
+            value, salt=salt, sep=sep
+        ).rsplit(sep, 1)
+        timestamp = baseconv.base62.to_int(timestamp)
+        if max_age is not None:
+            # Check timestamp is not older than max_age
+            age = time.time() - timestamp
+            if age > max_age:
+                raise SignatureExpired, 'Signature age %s > %s seconds' % (
+                    age, max_age
+                )
+        return value
diff --git a/docs/index.txt b/docs/index.txt
index cec1d76..41ec97e 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -163,6 +163,7 @@ Other batteries included
     * :ref:`Comments <ref-contrib-comments-index>` | :ref:`Moderation <ref-contrib-comments-moderation>` | :ref:`Custom comments <ref-contrib-comments-custom>`
     * :ref:`Content types <ref-contrib-contenttypes>`
     * :ref:`Cross Site Request Forgery protection <ref-contrib-csrf>`
+    * :ref:`Cryptographic signing <topics-cryptographic-signing>`
     * :ref:`Databrowse <ref-contrib-databrowse>`
     * :ref:`E-mail (sending) <topics-email>`
     * :ref:`Flatpages <ref-contrib-flatpages>`
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index 77d991b..28b4eaa 100644
--- a/docs/ref/request-response.txt
+++ b/docs/ref/request-response.txt
@@ -221,6 +221,42 @@ Methods
 
    Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
 
+.. method:: HttpRequest.get_signed_cookie(key, default=raise_error, salt = '', max_age=None)
+
+   .. versionadded:: 1.2
+
+   Returns a cookie value for a signed cookie, or raises a 
+   ``utils.BadSignature`` exception if the signature is no longer valid. If 
+   you provide the ``default`` argument the exception will be suppressed and
+   that default value will be returned instead.
+   
+   The optional salt argument can be used to provide extra protection against 
+   brute force attacks on your secret key. If supplied, the max_age argument 
+   will be checked against the signed timestamp attached to the cookie value 
+   to ensure the cookie is no older than max_age seconds.
+   
+   For example::
+   
+          >>> request.get_signed_cookie('name')
+          'Tony'
+          >>> request.get_signed_cookie('name', salt='name-salt')
+          'Tony' # assuming cookie was set using the same salt
+          >>> request.get_signed_cookie('non-existing-cookie')
+          ...
+          KeyError: 'non-existing-cookie'
+          >>> request.get_signed_cookie('non-existing-cookie', default=False)
+          False
+          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
+          ...
+          BadSignature: ...
+          >>> request.get_signed_cookie('name', max_age=60)
+          ...
+          SignatureExpired: Signature age 1677.3839159 > 60 seconds
+          >>> request.get_signed_cookie('name', max_age=60, default=False)
+          False
+   
+   See :ref:`cryptographic signing <topics-signing>` for more information.
+
 .. method:: HttpRequest.is_secure()
 
    Returns ``True`` if the request is secure; that is, if it was made with
@@ -522,6 +558,14 @@ Methods
 
     .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
 
+.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None)
+
+    Like ``set_cookie()``, but :ref:`cryptographically signs <topics-signing>` 
+    the cookie before setting it. Use in conjunction with 
+    ``HttpRequest.get_signed_cookie``. You can use the optional ``salt`` 
+    argument for added key strength, but you will need to remember to pass it 
+    to the corresponding ``get_signed_cookie`` call.
+
 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
 
     Deletes the cookie with the given key. Fails silently if the key doesn't
diff --git a/docs/topics/index.txt b/docs/topics/index.txt
index 7fa283a..b80c101 100644
--- a/docs/topics/index.txt
+++ b/docs/topics/index.txt
@@ -20,6 +20,7 @@ Introductions to all the key parts of Django you'll need to know:
    auth
    cache
    conditional-view-processing
+   signing
    email
    i18n
    pagination
diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
new file mode 100644
index 0000000..68d4a30
--- /dev/null
+++ b/docs/topics/signing.txt
@@ -0,0 +1,128 @@
+.. _topics-signing:
+
+=====================
+Cryptographic signing
+=====================
+
+.. versionadded:: 1.2
+
+The golden rule of web application security is to never trust data from 
+untrusted sources. Sometimes it can be useful to pass data through an 
+untrusted medium. Cryptographically signed values can be passed through an 
+untrusted channel safe in the knowledge that any tampering will be detected.
+
+Django provides both a low-level API for signing values and a high-level API 
+for setting and reading signed cookies, one of the most common uses of 
+signing in web applications.
+
+You may also find signing useful for the following:
+
+    * Generating "recover my account" URLs for sending to users who have 
+      lost their password.
+
+    * Ensuring data stored in hidden form fields has not been tampered with.
+
+    * Generating one-time secret URLs for allowing temporary access to a 
+      protected resource, for example a downloadable file that a user has 
+      paid for.
+
+Protecting the SECRET_KEY
+=========================
+
+When you create a new Django project using ``django-admin.py startproject``,
+the ``settings.py`` file it generates automatically gets a random 
+``SECRET_KEY`` value. This value is the key to securing signed data - it is 
+vital you keep this secure, or attackers could use it to generate their own 
+signed values.
+
+Using the low-level API
+=======================
+
+Django's signing methods live in the ``django.utils.signed module``. To 
+sign a value, first instantiate a ``Signer`` instance::
+
+    >>> from django.utils.signed import Signer
+    >>> signer = Signer()
+    >>> value = signer.sign('My string')
+    >>> value
+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
+
+The signature is appended to the end of the string, following the colon. You 
+can retrieve the original value using the ``unsign`` method::
+
+    >>> original = signer.unsign(value)
+    >>> original
+    u'My string'
+
+If the signature or value have been altered in any way, a 
+``signed.BadSigature`` exception will be raised::
+
+    >>> value += 'm'
+    >>> try:
+    ...    original = signer.unsign(value)
+    ... except signed.BadSignature:
+    ...    print "Tampering detected!"
+
+By default, the ``Signer`` class uses your project's ``SECRET_KEY`` setting to 
+generate signatures. You can use a different secret by passing it to the 
+``Signer`` constructor::
+
+    >>> signer = Signer('my-other-secret')
+    >>> value = signer.sign('My string')
+    >>> value
+    'My string:EkfQJafvGyiofrdGnuthdxImIJw'
+
+Using the salt argument
+-----------------------
+
+If you do not wish to use the same key for every signing operation in your 
+application, you can use the optional ``salt`` argument to the ``sign`` and 
+``unsign`` methods to further strengthen your ``SECRET_KEY`` against brute 
+force attacks. Using a salt will cause a new key to be derived from both the 
+salt and your ``SECRET_KEY``::
+
+    >>> signer = Signer()
+    >>> signer.sign('My string')
+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
+    >>> signer.sign('My string', salt='extra')
+    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
+    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw', salt='extra')
+    u'My string'
+
+Unlike your ``SECRET_KEY``, your salt argument does not need to stay secret.
+
+Verifying timestamped values
+----------------------------
+
+``TimestampSigner`` is a subclass of ``Signer`` that appends a signed 
+timestamp to the value. This allows you to confirm that a signed value was 
+created within a specified period of time::
+
+    >>> from django.utils.signed import TimestampSigner
+    >>> signer = TimestampSigner()
+    >>> value = signer.sign('hello')
+    >>> value
+    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
+    >>> signer.unsign(value)
+    u'hello'
+    >>> signer.unsign(value, max_age=10)
+    ...
+    SignatureExpired: Signature age 15.5289158821 > 10 seconds
+    >>> signer.unsign(value, max_age=20)
+    u'hello'
+
+Protecting complex data structures
+----------------------------------
+
+If you wish to protect a list, tuple or dictionary you can do so using the 
+signed module's dumps and loads functions. These imitate Python's pickle 
+module, but use JSON serialization under the hood. JSON ensures that even if 
+your ``SECRET_KEY`` is stolen an attacker will not be able to execute 
+arbitrary commands by exploiting the pickle format.::
+
+    >>> from django.utils import signed
+    >>> value = signed.dumps({"foo": "bar"})
+    >>> value
+    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
+    >>> signed.loads(value)
+    {'foo': 'bar'}
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
--- /dev/null
+++ b/tests/regressiontests/signed_cookie_tests/models.py
@@ -0,0 +1 @@
+# models.py file for tests to run.
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
--- /dev/null
+++ b/tests/regressiontests/signed_cookie_tests/tests.py
@@ -0,0 +1,66 @@
+from django.test import TestCase
+from django.http import HttpRequest, HttpResponse
+from django.utils import signed
+import time
+
+class SignedCookieTest(TestCase):
+
+    def test_can_set_and_read_signed_cookies(self):
+        response = HttpResponse()
+        response.set_signed_cookie('c', 'hello')
+        self.assert_('c' in response.cookies)
+        self.assert_(response.cookies['c'].value.startswith('hello:'))
+        request = HttpRequest()
+        request.COOKIES['c'] = response.cookies['c'].value
+        value = request.get_signed_cookie('c')
+        self.assertEqual(value, u'hello')
+
+    def test_can_use_salt(self):
+        response = HttpResponse()
+        response.set_signed_cookie('a', 'hello', salt='one')
+        request = HttpRequest()
+        request.COOKIES['a'] = response.cookies['a'].value
+        value = request.get_signed_cookie('a', salt='one')
+        self.assertEqual(value, u'hello')
+        self.assertRaises(signed.BadSignature,
+            request.get_signed_cookie, 'a', salt='two'
+        )
+
+    def test_detects_tampering(self):
+        response = HttpResponse()
+        response.set_signed_cookie('c', 'hello')
+        request = HttpRequest()
+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
+        self.assertRaises(signed.BadSignature,
+            request.get_signed_cookie, 'c'
+        )
+
+    def test_default_argument_supresses_exceptions(self):
+        response = HttpResponse()
+        response.set_signed_cookie('c', 'hello')
+        request = HttpRequest()
+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
+        self.assertEqual(request.get_signed_cookie('c', default=None), None)
+
+    def test_max_age_argument(self):
+        old_time = time.time
+        time.time = lambda: 123456789
+        v = u'hello'
+        try:
+            response = HttpResponse()
+            response.set_signed_cookie('c', v)
+            request = HttpRequest()
+            request.COOKIES['c'] = response.cookies['c'].value
+
+            self.assertEqual(request.get_signed_cookie('c'), v)
+
+            time.time = lambda: 123456800
+
+            self.assertEqual(request.get_signed_cookie('c', max_age=12), v)
+            self.assertEqual(request.get_signed_cookie('c', max_age=11), v)
+            self.assertRaises(
+                signed.SignatureExpired, request.get_signed_cookie, 'c',
+                max_age = 10
+            )
+        finally:
+            time.time = old_time
diff --git a/tests/regressiontests/utils/baseconv.py b/tests/regressiontests/utils/baseconv.py
new file mode 100644
index 0000000..d149937
--- /dev/null
+++ b/tests/regressiontests/utils/baseconv.py
@@ -0,0 +1,13 @@
+from unittest import TestCase
+from django.utils.baseconv import base2, base16, base36, base62
+
+class TestBaseConv(TestCase):
+    
+    def test_baseconv(self):
+        nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
+        for convertor in [base2, base16, base36, base62]:
+            for i in nums:
+                self.assertEqual(
+                    i, convertor.to_int(convertor.from_int(i))
+                )
+
diff --git a/tests/regressiontests/utils/signed.py b/tests/regressiontests/utils/signed.py
new file mode 100644
index 0000000..3720d2b
--- /dev/null
+++ b/tests/regressiontests/utils/signed.py
@@ -0,0 +1,122 @@
+from unittest import TestCase
+from django.utils import signed
+from django.utils.hashcompat import sha_constructor
+from django.utils.encoding import force_unicode
+import time
+
+class TestSigner(TestCase):
+
+    def test_signature(self):
+        "signature() method should generate a signature"
+        signer = signed.Signer('predictable-secret')
+        signer2 = signed.Signer('predictable-secret2')
+        for s in (
+            'hello',
+            '3098247:529:087:',
+            u'\u2019'.encode('utf8'),
+        ):
+            self.assertEqual(
+                signer.signature(s),
+                signed.base64_hmac(s, sha_constructor(
+                    'signer' + 'predictable-secret'
+                ).hexdigest())
+            )
+            self.assertNotEqual(signer.signature(s), signer2.signature(s))
+
+    def test_signature_with_salt(self):
+        "signature(value, salt=...) should work"
+        signer = signed.Signer('predictable-secret')
+        self.assertEqual(
+            signer.signature('hello', salt='extra-salt'),
+            signed.base64_hmac('hello', sha_constructor(
+                'signer' + 'predictable-secret' + 'extra-salt'
+            ).hexdigest())
+        )
+        self.assertNotEqual(
+            signer.signature('hello', salt='one'),
+            signer.signature('hello', salt='two'),
+        )
+    
+    def test_sign_unsign(self):
+        "sign/unsign should be reversible"
+        signer = signed.Signer('predictable-secret')
+        examples = (
+            'q;wjmbk;wkmb',
+            '3098247529087',
+            '3098247:529:087:',
+            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
+            u'\u2019',
+        )
+        for example in examples:
+            self.assert_(
+                force_unicode(example) != force_unicode(signer.sign(example))
+            )
+            self.assertEqual(example, signer.unsign(signer.sign(example)))
+
+    def unsign_detects_tampering(self):
+        "unsign should raise an exception if the value has been tampered with"
+        signer = signed.Signer('predictable-secret')
+        value = 'Another string'
+        signed_value = signer.sign(value)
+        transforms = (
+            lambda s: s.upper(),
+            lambda s: s + 'a',
+            lambda s: 'a' + s[1:],
+            lambda s: s.replace(':', ''),
+        )
+        self.assertEqual(value, signer.unsign(signed_value))
+        for transform in transforms:
+            self.assertRaises(
+                signed.BadSignature, signer.unsign, transform(signed_value)
+            )
+
+    def test_dumps_loads(self):
+        "dumps and loads be reversible for any JSON serializable object"
+        objects = (
+            ['a', 'list'],
+            'a string',
+            u'a unicode string \u2019',
+            {'a': 'dictionary'},
+        )
+        for o in objects:
+            self.assert_(o != signed.dumps(o))
+            self.assertEqual(o, signed.loads(signed.dumps(o)))
+
+    def test_decode_detects_tampering(self):
+        "loads should raise exception for tampered objects"
+        transforms = (
+            lambda s: s.upper(),
+            lambda s: s + 'a',
+            lambda s: 'a' + s[1:],
+            lambda s: s.replace(':', ''),
+        )
+        value = {'foo': 'bar', 'baz': 1}
+        encoded = signed.dumps(value)
+        self.assertEqual(value, signed.loads(encoded))
+        for transform in transforms:
+            self.assertRaises(
+                signed.BadSignature, signed.loads, transform(encoded)
+            )
+
+class TestTimestampSigner(TestCase):
+
+    def test_timestamp_signer(self):
+        old_time = time.time
+        time.time = lambda: 123456789
+        try:
+            signer = signed.TimestampSigner('predictable-key')
+            v = u'hello'
+            ts = signer.sign(v)
+            self.assertNotEqual(ts,signed.Signer('predictable-key').sign(v))
+
+            self.assertEqual(signer.unsign(ts), v)
+
+            time.time = lambda: 123456800
+
+            self.assertEqual(signer.unsign(ts, max_age=12), v)
+            self.assertEqual(signer.unsign(ts, max_age=11), v)
+            self.assertRaises(
+                signed.SignatureExpired, signer.unsign, ts, max_age=10
+            )
+        finally:
+            time.time = old_time
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
index 6258b81..6c6dee0 100644
--- a/tests/regressiontests/utils/tests.py
+++ b/tests/regressiontests/utils/tests.py
@@ -11,6 +11,8 @@ import timesince
 import datastructures
 import dateformat
 import itercompat
+from baseconv import TestBaseConv
+from signed import TestSigner, TestTimestampSigner
 from decorators import DecoratorFromMiddlewareTests
 
 # We need this because "datastructures" uses sorted() and the tests are run in
