Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #12417 -- Added signing functionality, including signing cookie…

…s. Many thanks to Simon, Stephan, Paul and everyone else involved.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16253 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit f60d42846365b2bf2f1c9bc7a3007c303122a20b 1 parent 1579330
Jannis Leidel authored May 21, 2011
6  django/conf/global_settings.py
@@ -476,6 +476,12 @@
476 476
 # The number of days a password reset link is valid for
477 477
 PASSWORD_RESET_TIMEOUT_DAYS = 3
478 478
 
  479
+###########
  480
+# SIGNING #
  481
+###########
  482
+
  483
+SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
  484
+
479 485
 ########
480 486
 # CSRF #
481 487
 ########
178  django/core/signing.py
... ...
@@ -0,0 +1,178 @@
  1
+"""
  2
+Functions for creating and restoring url-safe signed JSON objects.
  3
+
  4
+The format used looks like this:
  5
+
  6
+>>> signed.dumps("hello")
  7
+'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
  8
+
  9
+There are two components here, separatad by a '.'. The first component is a
  10
+URLsafe base64 encoded JSON of the object passed to dumps(). The second
  11
+component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
  12
+
  13
+signed.loads(s) checks the signature and returns the deserialised object.
  14
+If the signature fails, a BadSignature exception is raised.
  15
+
  16
+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
  17
+u'hello'
  18
+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
  19
+...
  20
+BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
  21
+
  22
+You can optionally compress the JSON prior to base64 encoding it to save
  23
+space, using the compress=True argument. This checks if compression actually
  24
+helps 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
+
  29
+The fact that the string is compressed is signalled by the prefixed '.' at the
  30
+start of the base64 JSON.
  31
+
  32
+There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
  33
+These functions make use of all of them.
  34
+"""
  35
+import base64
  36
+import time
  37
+import zlib
  38
+
  39
+from django.conf import settings
  40
+from django.core.exceptions import ImproperlyConfigured
  41
+from django.utils import baseconv, simplejson
  42
+from django.utils.crypto import constant_time_compare, salted_hmac
  43
+from django.utils.encoding import force_unicode, smart_str
  44
+from django.utils.importlib import import_module
  45
+
  46
+
  47
+class BadSignature(Exception):
  48
+    """
  49
+    Signature does not match
  50
+    """
  51
+    pass
  52
+
  53
+
  54
+class SignatureExpired(BadSignature):
  55
+    """
  56
+    Signature timestamp is older than required max_age
  57
+    """
  58
+    pass
  59
+
  60
+
  61
+def b64_encode(s):
  62
+    return base64.urlsafe_b64encode(s).strip('=')
  63
+
  64
+
  65
+def b64_decode(s):
  66
+    pad = '=' * (-len(s) % 4)
  67
+    return base64.urlsafe_b64decode(s + pad)
  68
+
  69
+
  70
+def base64_hmac(salt, value, key):
  71
+    return b64_encode(salted_hmac(salt, value, key).digest())
  72
+
  73
+
  74
+def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
  75
+    modpath = settings.SIGNING_BACKEND
  76
+    module, attr = modpath.rsplit('.', 1)
  77
+    try:
  78
+        mod = import_module(module)
  79
+    except ImportError, e:
  80
+        raise ImproperlyConfigured(
  81
+            'Error importing cookie signer %s: "%s"' % (modpath, e))
  82
+    try:
  83
+        Signer = getattr(mod, attr)
  84
+    except AttributeError, e:
  85
+        raise ImproperlyConfigured(
  86
+            'Error importing cookie signer %s: "%s"' % (modpath, e))
  87
+    return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
  88
+
  89
+
  90
+def dumps(obj, key=None, salt='django.core.signing', compress=False):
  91
+    """
  92
+    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
  93
+    None, settings.SECRET_KEY is used instead.
  94
+
  95
+    If compress is True (not the default) checks if compressing using zlib can
  96
+    save some space. Prepends a '.' to signify compression. This is included
  97
+    in the signature, to protect against zip bombs.
  98
+
  99
+    salt can be used to further salt the hash, in case you're worried
  100
+    that the NSA might try to brute-force your SHA-1 protected secret.
  101
+    """
  102
+    json = simplejson.dumps(obj, separators=(',', ':'))
  103
+
  104
+    # Flag for if it's been compressed or not
  105
+    is_compressed = False
  106
+
  107
+    if compress:
  108
+        # Avoid zlib dependency unless compress is being used
  109
+        compressed = zlib.compress(json)
  110
+        if len(compressed) < (len(json) - 1):
  111
+            json = compressed
  112
+            is_compressed = True
  113
+    base64d = b64_encode(json)
  114
+    if is_compressed:
  115
+        base64d = '.' + base64d
  116
+    return TimestampSigner(key, salt=salt).sign(base64d)
  117
+
  118
+
  119
+def loads(s, key=None, salt='django.core.signing', max_age=None):
  120
+    """
  121
+    Reverse of dumps(), raises BadSignature if signature fails
  122
+    """
  123
+    base64d = smart_str(
  124
+        TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
  125
+    decompress = False
  126
+    if base64d[0] == '.':
  127
+        # It's compressed; uncompress it first
  128
+        base64d = base64d[1:]
  129
+        decompress = True
  130
+    json = b64_decode(base64d)
  131
+    if decompress:
  132
+        json = zlib.decompress(json)
  133
+    return simplejson.loads(json)
  134
+
  135
+
  136
+class Signer(object):
  137
+    def __init__(self, key=None, sep=':', salt=None):
  138
+        self.sep = sep
  139
+        self.key = key or settings.SECRET_KEY
  140
+        self.salt = salt or ('%s.%s' %
  141
+            (self.__class__.__module__, self.__class__.__name__))
  142
+
  143
+    def signature(self, value):
  144
+        return base64_hmac(self.salt + 'signer', value, self.key)
  145
+
  146
+    def sign(self, value):
  147
+        value = smart_str(value)
  148
+        return '%s%s%s' % (value, self.sep, self.signature(value))
  149
+
  150
+    def unsign(self, signed_value):
  151
+        signed_value = smart_str(signed_value)
  152
+        if not self.sep in signed_value:
  153
+            raise BadSignature('No "%s" found in value' % self.sep)
  154
+        value, sig = signed_value.rsplit(self.sep, 1)
  155
+        if constant_time_compare(sig, self.signature(value)):
  156
+            return force_unicode(value)
  157
+        raise BadSignature('Signature "%s" does not match' % sig)
  158
+
  159
+
  160
+class TimestampSigner(Signer):
  161
+    def timestamp(self):
  162
+        return baseconv.base62.encode(int(time.time()))
  163
+
  164
+    def sign(self, value):
  165
+        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
  166
+        return '%s%s%s' % (value, self.sep, self.signature(value))
  167
+
  168
+    def unsign(self, value, max_age=None):
  169
+        result =  super(TimestampSigner, self).unsign(value)
  170
+        value, timestamp = result.rsplit(self.sep, 1)
  171
+        timestamp = baseconv.base62.decode(timestamp)
  172
+        if max_age is not None:
  173
+            # Check timestamp is not older than max_age
  174
+            age = time.time() - timestamp
  175
+            if age > max_age:
  176
+                raise SignatureExpired(
  177
+                    'Signature age %s > %s seconds' % (age, max_age))
  178
+        return value
30  django/http/__init__.py
@@ -122,6 +122,7 @@ def __init__(self, *args, **kwargs):
122 122
 from django.utils.http import cookie_date
123 123
 from django.http.multipartparser import MultiPartParser
124 124
 from django.conf import settings
  125
+from django.core import signing
125 126
 from django.core.files import uploadhandler
126 127
 from utils import *
127 128
 
@@ -132,6 +133,8 @@ def __init__(self, *args, **kwargs):
132 133
 class Http404(Exception):
133 134
     pass
134 135
 
  136
+RAISE_ERROR = object()
  137
+
135 138
 class HttpRequest(object):
136 139
     """A basic HTTP request."""
137 140
 
@@ -170,6 +173,29 @@ def get_full_path(self):
170 173
         # Rather than crash if this doesn't happen, we encode defensively.
171 174
         return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
172 175
 
  176
+    def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None):
  177
+        """
  178
+        Attempts to return a signed cookie. If the signature fails or the
  179
+        cookie has expired, raises an exception... unless you provide the
  180
+        default argument in which case that value will be returned instead.
  181
+        """
  182
+        try:
  183
+            cookie_value = self.COOKIES[key].encode('utf-8')
  184
+        except KeyError:
  185
+            if default is not RAISE_ERROR:
  186
+                return default
  187
+            else:
  188
+                raise
  189
+        try:
  190
+            value = signing.get_cookie_signer(salt=key + salt).unsign(
  191
+                cookie_value, max_age=max_age)
  192
+        except signing.BadSignature:
  193
+            if default is not RAISE_ERROR:
  194
+                return default
  195
+            else:
  196
+                raise
  197
+        return value
  198
+
173 199
     def build_absolute_uri(self, location=None):
174 200
         """
175 201
         Builds an absolute URI from the location and the variables available in
@@ -584,6 +610,10 @@ def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
584 610
         if httponly:
585 611
             self.cookies[key]['httponly'] = True
586 612
 
  613
+    def set_signed_cookie(self, key, value, salt='', **kwargs):
  614
+        value = signing.get_cookie_signer(salt=key + salt).sign(value)
  615
+        return self.set_cookie(key, value, **kwargs)
  616
+
587 617
     def delete_cookie(self, key, path='/', domain=None):
588 618
         self.set_cookie(key, max_age=0, path=path, domain=domain,
589 619
                         expires='Thu, 01-Jan-1970 00:00:00 GMT')
99  django/utils/baseconv.py
... ...
@@ -0,0 +1,99 @@
  1
+# Copyright (c) 2010 Taurinus Collective. All rights reserved.
  2
+# Copyright (c) 2009 Simon Willison. All rights reserved.
  3
+# Copyright (c) 2002 Drew Perttula. All rights reserved.
  4
+#
  5
+# License:
  6
+#   Python Software Foundation License version 2
  7
+#
  8
+# See the file "LICENSE" for terms & conditions for usage, and a DISCLAIMER OF
  9
+# ALL WARRANTIES.
  10
+#
  11
+# This Baseconv distribution contains no GNU General Public Licensed (GPLed)
  12
+# code so it may be used in proprietary projects just like prior ``baseconv``
  13
+# distributions.
  14
+#
  15
+# All trademarks referenced herein are property of their respective holders.
  16
+#
  17
+
  18
+"""
  19
+Convert numbers from base 10 integers to base X strings and back again.
  20
+
  21
+Sample usage::
  22
+
  23
+  >>> base20 = BaseConverter('0123456789abcdefghij')
  24
+  >>> base20.encode(1234)
  25
+  '31e'
  26
+  >>> base20.decode('31e')
  27
+  1234
  28
+  >>> base20.encode(-1234)
  29
+  '-31e'
  30
+  >>> base20.decode('-31e')
  31
+  -1234
  32
+  >>> base11 = BaseConverter('0123456789-', sign='$')
  33
+  >>> base11.encode('$1234')
  34
+  '$-22'
  35
+  >>> base11.decode('$-22')
  36
+  '$1234'
  37
+
  38
+"""
  39
+
  40
+BASE2_ALPHABET = '01'
  41
+BASE16_ALPHABET = '0123456789ABCDEF'
  42
+BASE56_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz'
  43
+BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
  44
+BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
  45
+BASE64_ALPHABET = BASE62_ALPHABET + '-_'
  46
+
  47
+class BaseConverter(object):
  48
+    decimal_digits = '0123456789'
  49
+
  50
+    def __init__(self, digits, sign='-'):
  51
+        self.sign = sign
  52
+        self.digits = digits
  53
+        if sign in self.digits:
  54
+            raise ValueError('Sign character found in converter base digits.')
  55
+
  56
+    def __repr__(self):
  57
+        return "<BaseConverter: base%s (%s)>" % (len(self.digits), self.digits)
  58
+
  59
+    def encode(self, i):
  60
+        neg, value = self.convert(i, self.decimal_digits, self.digits, '-')
  61
+        if neg:
  62
+            return self.sign + value
  63
+        return value
  64
+
  65
+    def decode(self, s):
  66
+        neg, value = self.convert(s, self.digits, self.decimal_digits, self.sign)
  67
+        if neg:
  68
+            value = '-' + value
  69
+        return int(value)
  70
+
  71
+    def convert(self, number, from_digits, to_digits, sign):
  72
+        if str(number)[0] == sign:
  73
+            number = str(number)[1:]
  74
+            neg = 1
  75
+        else:
  76
+            neg = 0
  77
+
  78
+        # make an integer out of the number
  79
+        x = 0
  80
+        for digit in str(number):
  81
+            x = x * len(from_digits) + from_digits.index(digit)
  82
+
  83
+        # create the result in base 'len(to_digits)'
  84
+        if x == 0:
  85
+            res = to_digits[0]
  86
+        else:
  87
+            res = ''
  88
+            while x > 0:
  89
+                digit = x % len(to_digits)
  90
+                res = to_digits[digit] + res
  91
+                x = int(x / len(to_digits))
  92
+        return neg, res
  93
+
  94
+base2 = BaseConverter(BASE2_ALPHABET)
  95
+base16 = BaseConverter(BASE16_ALPHABET)
  96
+base36 = BaseConverter(BASE36_ALPHABET)
  97
+base56 = BaseConverter(BASE56_ALPHABET)
  98
+base62 = BaseConverter(BASE62_ALPHABET)
  99
+base64 = BaseConverter(BASE64_ALPHABET, sign='$')
1  docs/index.txt
@@ -171,6 +171,7 @@ Other batteries included
171 171
     * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
172 172
     * :doc:`Content types <ref/contrib/contenttypes>`
173 173
     * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
  174
+    * :doc:`Cryptographic signing <topics/signing>`
174 175
     * :doc:`Databrowse <ref/contrib/databrowse>`
175 176
     * :doc:`E-mail (sending) <topics/email>`
176 177
     * :doc:`Flatpages <ref/contrib/flatpages>`
48  docs/ref/request-response.txt
@@ -240,6 +240,43 @@ Methods
240 240
 
241 241
    Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
242 242
 
  243
+.. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
  244
+
  245
+   .. versionadded:: 1.4
  246
+
  247
+   Returns a cookie value for a signed cookie, or raises a
  248
+   :class:`~django.core.signing.BadSignature` exception if the signature is
  249
+   no longer valid. If you provide the ``default`` argument the exception
  250
+   will be suppressed and that default value will be returned instead.
  251
+
  252
+   The optional ``salt`` argument can be used to provide extra protection
  253
+   against brute force attacks on your secret key. If supplied, the
  254
+   ``max_age`` argument will be checked against the signed timestamp
  255
+   attached to the cookie value to ensure the cookie is not older than
  256
+   ``max_age`` seconds.
  257
+
  258
+   For example::
  259
+
  260
+          >>> request.get_signed_cookie('name')
  261
+          'Tony'
  262
+          >>> request.get_signed_cookie('name', salt='name-salt')
  263
+          'Tony' # assuming cookie was set using the same salt
  264
+          >>> request.get_signed_cookie('non-existing-cookie')
  265
+          ...
  266
+          KeyError: 'non-existing-cookie'
  267
+          >>> request.get_signed_cookie('non-existing-cookie', False)
  268
+          False
  269
+          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
  270
+          ...
  271
+          BadSignature: ...
  272
+          >>> request.get_signed_cookie('name', max_age=60)
  273
+          ...
  274
+          SignatureExpired: Signature age 1677.3839159 > 60 seconds
  275
+          >>> request.get_signed_cookie('name', False, max_age=60)
  276
+          False
  277
+
  278
+   See :doc:`cryptographic signing </topics/signing>` for more information.
  279
+
243 280
 .. method:: HttpRequest.is_secure()
244 281
 
245 282
    Returns ``True`` if the request is secure; that is, if it was made with
@@ -618,6 +655,17 @@ Methods
618 655
     .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
619 656
     .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
620 657
 
  658
+.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
  659
+
  660
+    .. versionadded:: 1.4
  661
+
  662
+    Like :meth:`~HttpResponse.set_cookie()`, but
  663
+    :doc:`cryptographic signing </topics/signing>` the cookie before setting
  664
+    it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`.
  665
+    You can use the optional ``salt`` argument for added key strength, but
  666
+    you will need to remember to pass it to the corresponding
  667
+    :meth:`HttpRequest.get_signed_cookie` call.
  668
+
621 669
 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
622 670
 
623 671
     Deletes the cookie with the given key. Fails silently if the key doesn't
13  docs/ref/settings.txt
@@ -1647,6 +1647,19 @@ See :tfilter:`allowed date format strings <date>`.
1647 1647
 
1648 1648
 See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
1649 1649
 
  1650
+.. setting:: SIGNING_BACKEND
  1651
+
  1652
+SIGNING_BACKEND
  1653
+---------------
  1654
+
  1655
+.. versionadded:: 1.4
  1656
+
  1657
+Default: 'django.core.signing.TimestampSigner'
  1658
+
  1659
+The backend used for signing cookies and other data.
  1660
+
  1661
+See also the :doc:`/topics/signing` documentation.
  1662
+
1650 1663
 .. setting:: SITE_ID
1651 1664
 
1652 1665
 SITE_ID
9  docs/releases/1.4.txt
@@ -46,6 +46,15 @@ not custom filters. This has been rectified with a simple API previously
46 46
 known as "FilterSpec" which was used internally. For more details, see the
47 47
 documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
48 48
 
  49
+Tools for cryptographic signing
  50
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  51
+
  52
+Django 1.4 adds both a low-level API for signing values and a high-level API
  53
+for setting and reading signed cookies, one of the most common uses of
  54
+signing in Web applications.
  55
+
  56
+See :doc:`cryptographic signing </topics/signing>` docs for more information.
  57
+
49 58
 ``reverse_lazy``
50 59
 ~~~~~~~~~~~~~~~~
51 60
 
1  docs/topics/index.txt
@@ -18,6 +18,7 @@ Introductions to all the key parts of Django you'll need to know:
18 18
    auth
19 19
    cache
20 20
    conditional-view-processing
  21
+   signing
21 22
    email
22 23
    i18n/index
23 24
    logging
135  docs/topics/signing.txt
... ...
@@ -0,0 +1,135 @@
  1
+=====================
  2
+Cryptographic signing
  3
+=====================
  4
+
  5
+.. module:: django.core.signing
  6
+   :synopsis: Django's signing framework.
  7
+
  8
+.. versionadded:: 1.4
  9
+
  10
+The golden rule of Web application security is to never trust data from
  11
+untrusted sources. Sometimes it can be useful to pass data through an
  12
+untrusted medium. Cryptographically signed values can be passed through an
  13
+untrusted channel safe in the knowledge that any tampering will be detected.
  14
+
  15
+Django provides both a low-level API for signing values and a high-level API
  16
+for setting and reading signed cookies, one of the most common uses of
  17
+signing in Web applications.
  18
+
  19
+You may also find signing useful for the following:
  20
+
  21
+    * Generating "recover my account" URLs for sending to users who have
  22
+      lost their password.
  23
+
  24
+    * Ensuring data stored in hidden form fields has not been tampered with.
  25
+
  26
+    * Generating one-time secret URLs for allowing temporary access to a
  27
+      protected resource, for example a downloadable file that a user has
  28
+      paid for.
  29
+
  30
+Protecting the SECRET_KEY
  31
+=========================
  32
+
  33
+When you create a new Django project using :djadmin:`startproject`, the
  34
+``settings.py`` file it generates automatically gets a random
  35
+:setting:`SECRET_KEY` value. This value is the key to securing signed
  36
+data -- it is vital you keep this secure, or attackers could use it to
  37
+generate their own signed values.
  38
+
  39
+Using the low-level API
  40
+=======================
  41
+
  42
+.. class:: Signer
  43
+
  44
+Django's signing methods live in the ``django.core.signing`` module.
  45
+To sign a value, first instantiate a ``Signer`` instance::
  46
+
  47
+    >>> from django.core.signing import Signer
  48
+    >>> signer = Signer()
  49
+    >>> value = signer.sign('My string')
  50
+    >>> value
  51
+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
  52
+
  53
+The signature is appended to the end of the string, following the colon.
  54
+You can retrieve the original value using the ``unsign`` method::
  55
+
  56
+    >>> original = signer.unsign(value)
  57
+    >>> original
  58
+    u'My string'
  59
+
  60
+If the signature or value have been altered in any way, a
  61
+``django.core.signing.BadSigature`` exception will be raised::
  62
+
  63
+    >>> value += 'm'
  64
+    >>> try:
  65
+    ...    original = signer.unsign(value)
  66
+    ... except signing.BadSignature:
  67
+    ...    print "Tampering detected!"
  68
+
  69
+By default, the ``Signer`` class uses the :setting:`SECRET_KEY` setting to
  70
+generate signatures. You can use a different secret by passing it to the
  71
+``Signer`` constructor::
  72
+
  73
+    >>> signer = Signer('my-other-secret')
  74
+    >>> value = signer.sign('My string')
  75
+    >>> value
  76
+    'My string:EkfQJafvGyiofrdGnuthdxImIJw'
  77
+
  78
+Using the salt argument
  79
+-----------------------
  80
+
  81
+If you do not wish to use the same key for every signing operation in your
  82
+application, you can use the optional ``salt`` argument to the ``Signer``
  83
+class to further strengthen your :setting:`SECRET_KEY` against brute force
  84
+attacks. Using a salt will cause a new key to be derived from both the salt
  85
+and your :setting:`SECRET_KEY`::
  86
+
  87
+    >>> signer = Signer()
  88
+    >>> signer.sign('My string')
  89
+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
  90
+    >>> signer = Signer(salt='extra')
  91
+    >>> signer.sign('My string')
  92
+    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
  93
+    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
  94
+    u'My string'
  95
+
  96
+Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
  97
+secret.
  98
+
  99
+Verifying timestamped values
  100
+----------------------------
  101
+
  102
+.. class:: TimestampSigner
  103
+
  104
+``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed
  105
+timestamp to the value. This allows you to confirm that a signed value was
  106
+created within a specified period of time::
  107
+
  108
+    >>> from django.core.signing import TimestampSigner
  109
+    >>> signer = TimestampSigner()
  110
+    >>> value = signer.sign('hello')
  111
+    >>> value
  112
+    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
  113
+    >>> signer.unsign(value)
  114
+    u'hello'
  115
+    >>> signer.unsign(value, max_age=10)
  116
+    ...
  117
+    SignatureExpired: Signature age 15.5289158821 > 10 seconds
  118
+    >>> signer.unsign(value, max_age=20)
  119
+    u'hello'
  120
+
  121
+Protecting complex data structures
  122
+----------------------------------
  123
+
  124
+If you wish to protect a list, tuple or dictionary you can do so using the
  125
+signing module's dumps and loads functions. These imitate Python's pickle
  126
+module, but uses JSON serialization under the hood. JSON ensures that even
  127
+if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
  128
+execute arbitrary commands by exploiting the pickle format.::
  129
+
  130
+    >>> from django.core import signing
  131
+    >>> value = signing.dumps({"foo": "bar"})
  132
+    >>> value
  133
+    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
  134
+    >>> signing.loads(value)
  135
+    {'foo': 'bar'}
0  tests/regressiontests/signed_cookies_tests/__init__.py
No changes.
1  tests/regressiontests/signed_cookies_tests/models.py
... ...
@@ -0,0 +1 @@
  1
+# models.py file for tests to run.
61  tests/regressiontests/signed_cookies_tests/tests.py
... ...
@@ -0,0 +1,61 @@
  1
+import time
  2
+
  3
+from django.core import signing
  4
+from django.http import HttpRequest, HttpResponse
  5
+from django.test import TestCase
  6
+
  7
+class SignedCookieTest(TestCase):
  8
+
  9
+    def test_can_set_and_read_signed_cookies(self):
  10
+        response = HttpResponse()
  11
+        response.set_signed_cookie('c', 'hello')
  12
+        self.assertIn('c', response.cookies)
  13
+        self.assertTrue(response.cookies['c'].value.startswith('hello:'))
  14
+        request = HttpRequest()
  15
+        request.COOKIES['c'] = response.cookies['c'].value
  16
+        value = request.get_signed_cookie('c')
  17
+        self.assertEqual(value, u'hello')
  18
+
  19
+    def test_can_use_salt(self):
  20
+        response = HttpResponse()
  21
+        response.set_signed_cookie('a', 'hello', salt='one')
  22
+        request = HttpRequest()
  23
+        request.COOKIES['a'] = response.cookies['a'].value
  24
+        value = request.get_signed_cookie('a', salt='one')
  25
+        self.assertEqual(value, u'hello')
  26
+        self.assertRaises(signing.BadSignature,
  27
+            request.get_signed_cookie, 'a', salt='two')
  28
+
  29
+    def test_detects_tampering(self):
  30
+        response = HttpResponse()
  31
+        response.set_signed_cookie('c', 'hello')
  32
+        request = HttpRequest()
  33
+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
  34
+        self.assertRaises(signing.BadSignature,
  35
+            request.get_signed_cookie, 'c')
  36
+
  37
+    def test_default_argument_supresses_exceptions(self):
  38
+        response = HttpResponse()
  39
+        response.set_signed_cookie('c', 'hello')
  40
+        request = HttpRequest()
  41
+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
  42
+        self.assertEqual(request.get_signed_cookie('c', default=None), None)
  43
+
  44
+    def test_max_age_argument(self):
  45
+        value = u'hello'
  46
+        _time = time.time
  47
+        time.time = lambda: 123456789
  48
+        try:
  49
+            response = HttpResponse()
  50
+            response.set_signed_cookie('c', value)
  51
+            request = HttpRequest()
  52
+            request.COOKIES['c'] = response.cookies['c'].value
  53
+            self.assertEqual(request.get_signed_cookie('c'), value)
  54
+
  55
+            time.time = lambda: 123456800
  56
+            self.assertEqual(request.get_signed_cookie('c', max_age=12), value)
  57
+            self.assertEqual(request.get_signed_cookie('c', max_age=11), value)
  58
+            self.assertRaises(signing.SignatureExpired,
  59
+                request.get_signed_cookie, 'c', max_age = 10)
  60
+        finally:
  61
+            time.time = _time
0  tests/regressiontests/signing/__init__.py
No changes.
1  tests/regressiontests/signing/models.py
... ...
@@ -0,0 +1 @@
  1
+# models.py file for tests to run.
116  tests/regressiontests/signing/tests.py
... ...
@@ -0,0 +1,116 @@
  1
+import time
  2
+
  3
+from django.core import signing
  4
+from django.test import TestCase
  5
+from django.utils.encoding import force_unicode
  6
+
  7
+class TestSigner(TestCase):
  8
+
  9
+    def test_signature(self):
  10
+        "signature() method should generate a signature"
  11
+        signer = signing.Signer('predictable-secret')
  12
+        signer2 = signing.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
+                signing.base64_hmac(signer.salt + 'signer', s,
  21
+                    'predictable-secret')
  22
+            )
  23
+            self.assertNotEqual(signer.signature(s), signer2.signature(s))
  24
+
  25
+    def test_signature_with_salt(self):
  26
+        "signature(value, salt=...) should work"
  27
+        signer = signing.Signer('predictable-secret', salt='extra-salt')
  28
+        self.assertEqual(
  29
+            signer.signature('hello'),
  30
+                signing.base64_hmac('extra-salt' + 'signer',
  31
+                'hello', 'predictable-secret'))
  32
+        self.assertNotEqual(
  33
+            signing.Signer('predictable-secret', salt='one').signature('hello'),
  34
+            signing.Signer('predictable-secret', salt='two').signature('hello'))
  35
+
  36
+    def test_sign_unsign(self):
  37
+        "sign/unsign should be reversible"
  38
+        signer = signing.Signer('predictable-secret')
  39
+        examples = (
  40
+            'q;wjmbk;wkmb',
  41
+            '3098247529087',
  42
+            '3098247:529:087:',
  43
+            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
  44
+            u'\u2019',
  45
+        )
  46
+        for example in examples:
  47
+            self.assertNotEqual(
  48
+                force_unicode(example), force_unicode(signer.sign(example)))
  49
+            self.assertEqual(example, signer.unsign(signer.sign(example)))
  50
+
  51
+    def unsign_detects_tampering(self):
  52
+        "unsign should raise an exception if the value has been tampered with"
  53
+        signer = signing.Signer('predictable-secret')
  54
+        value = 'Another string'
  55
+        signed_value = signer.sign(value)
  56
+        transforms = (
  57
+            lambda s: s.upper(),
  58
+            lambda s: s + 'a',
  59
+            lambda s: 'a' + s[1:],
  60
+            lambda s: s.replace(':', ''),
  61
+        )
  62
+        self.assertEqual(value, signer.unsign(signed_value))
  63
+        for transform in transforms:
  64
+            self.assertRaises(
  65
+                signing.BadSignature, signer.unsign, transform(signed_value))
  66
+
  67
+    def test_dumps_loads(self):
  68
+        "dumps and loads be reversible for any JSON serializable object"
  69
+        objects = (
  70
+            ['a', 'list'],
  71
+            'a string',
  72
+            u'a unicode string \u2019',
  73
+            {'a': 'dictionary'},
  74
+        )
  75
+        for o in objects:
  76
+            self.assertNotEqual(o, signing.dumps(o))
  77
+            self.assertEqual(o, signing.loads(signing.dumps(o)))
  78
+
  79
+    def test_decode_detects_tampering(self):
  80
+        "loads should raise exception for tampered objects"
  81
+        transforms = (
  82
+            lambda s: s.upper(),
  83
+            lambda s: s + 'a',
  84
+            lambda s: 'a' + s[1:],
  85
+            lambda s: s.replace(':', ''),
  86
+        )
  87
+        value = {
  88
+            'foo': 'bar',
  89
+            'baz': 1,
  90
+        }
  91
+        encoded = signing.dumps(value)
  92
+        self.assertEqual(value, signing.loads(encoded))
  93
+        for transform in transforms:
  94
+            self.assertRaises(
  95
+                signing.BadSignature, signing.loads, transform(encoded))
  96
+
  97
+class TestTimestampSigner(TestCase):
  98
+
  99
+    def test_timestamp_signer(self):
  100
+        value = u'hello'
  101
+        _time = time.time
  102
+        time.time = lambda: 123456789
  103
+        try:
  104
+            signer = signing.TimestampSigner('predictable-key')
  105
+            ts = signer.sign(value)
  106
+            self.assertNotEqual(ts,
  107
+                signing.Signer('predictable-key').sign(value))
  108
+
  109
+            self.assertEqual(signer.unsign(ts), value)
  110
+            time.time = lambda: 123456800
  111
+            self.assertEqual(signer.unsign(ts, max_age=12), value)
  112
+            self.assertEqual(signer.unsign(ts, max_age=11), value)
  113
+            self.assertRaises(
  114
+                signing.SignatureExpired, signer.unsign, ts, max_age=10)
  115
+        finally:
  116
+            time.time = _time
41  tests/regressiontests/utils/baseconv.py
... ...
@@ -0,0 +1,41 @@
  1
+from unittest import TestCase
  2
+from django.utils.baseconv import base2, base16, base36, base56, base62, base64, BaseConverter
  3
+
  4
+class TestBaseConv(TestCase):
  5
+
  6
+    def test_baseconv(self):
  7
+        nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
  8
+        for converter in [base2, base16, base36, base56, base62, base64]:
  9
+            for i in nums:
  10
+                self.assertEqual(i, converter.decode(converter.encode(i)))
  11
+
  12
+    def test_base11(self):
  13
+        base11 = BaseConverter('0123456789-', sign='$')
  14
+        self.assertEqual(base11.encode(1234), '-22')
  15
+        self.assertEqual(base11.decode('-22'), 1234)
  16
+        self.assertEqual(base11.encode(-1234), '$-22')
  17
+        self.assertEqual(base11.decode('$-22'), -1234)
  18
+
  19
+    def test_base20(self):
  20
+        base20 = BaseConverter('0123456789abcdefghij')
  21
+        self.assertEqual(base20.encode(1234), '31e')
  22
+        self.assertEqual(base20.decode('31e'), 1234)
  23
+        self.assertEqual(base20.encode(-1234), '-31e')
  24
+        self.assertEqual(base20.decode('-31e'), -1234)
  25
+
  26
+    def test_base64(self):
  27
+        self.assertEqual(base64.encode(1234), 'JI')
  28
+        self.assertEqual(base64.decode('JI'), 1234)
  29
+        self.assertEqual(base64.encode(-1234), '$JI')
  30
+        self.assertEqual(base64.decode('$JI'), -1234)
  31
+
  32
+    def test_base7(self):
  33
+        base7 = BaseConverter('cjdhel3', sign='g')
  34
+        self.assertEqual(base7.encode(1234), 'hejd')
  35
+        self.assertEqual(base7.decode('hejd'), 1234)
  36
+        self.assertEqual(base7.encode(-1234), 'ghejd')
  37
+        self.assertEqual(base7.decode('ghejd'), -1234)
  38
+
  39
+    def test_exception(self):
  40
+        self.assertRaises(ValueError, BaseConverter, 'abc', sign='a')
  41
+        self.assertTrue(isinstance(BaseConverter('abc', sign='d'), BaseConverter))
1  tests/regressiontests/utils/tests.py
@@ -17,3 +17,4 @@
17 17
 from datastructures import *
18 18
 from tzinfo import *
19 19
 from datetime_safe import *
  20
+from baseconv import *

0 notes on commit f60d428

Please sign in to comment.
Something went wrong with that request. Please try again.