Fixed #12417 -- Added signing functionality, including signing cookies. 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
This commit is contained in:
parent
15793309e1
commit
f60d428463
|
@ -476,6 +476,12 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
|
||||||
# The number of days a password reset link is valid for
|
# The number of days a password reset link is valid for
|
||||||
PASSWORD_RESET_TIMEOUT_DAYS = 3
|
PASSWORD_RESET_TIMEOUT_DAYS = 3
|
||||||
|
|
||||||
|
###########
|
||||||
|
# SIGNING #
|
||||||
|
###########
|
||||||
|
|
||||||
|
SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
|
||||||
|
|
||||||
########
|
########
|
||||||
# CSRF #
|
# CSRF #
|
||||||
########
|
########
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.utils import baseconv, simplejson
|
||||||
|
from django.utils.crypto import constant_time_compare, salted_hmac
|
||||||
|
from django.utils.encoding import force_unicode, smart_str
|
||||||
|
from django.utils.importlib import import_module
|
||||||
|
|
||||||
|
|
||||||
|
class BadSignature(Exception):
|
||||||
|
"""
|
||||||
|
Signature does not match
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureExpired(BadSignature):
|
||||||
|
"""
|
||||||
|
Signature timestamp is older than required max_age
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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(salt, value, key):
|
||||||
|
return b64_encode(salted_hmac(salt, value, key).digest())
|
||||||
|
|
||||||
|
|
||||||
|
def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
|
||||||
|
modpath = settings.SIGNING_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('django.http.cookies' + settings.SECRET_KEY, salt=salt)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps(obj, key=None, salt='django.core.signing', compress=False):
|
||||||
|
"""
|
||||||
|
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=(',', ':'))
|
||||||
|
|
||||||
|
# Flag for if it's been compressed or not
|
||||||
|
is_compressed = False
|
||||||
|
|
||||||
|
if compress:
|
||||||
|
# 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, salt=salt).sign(base64d)
|
||||||
|
|
||||||
|
|
||||||
|
def loads(s, key=None, salt='django.core.signing', max_age=None):
|
||||||
|
"""
|
||||||
|
Reverse of dumps(), raises BadSignature if signature fails
|
||||||
|
"""
|
||||||
|
base64d = smart_str(
|
||||||
|
TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
|
||||||
|
decompress = False
|
||||||
|
if base64d[0] == '.':
|
||||||
|
# It's compressed; uncompress it first
|
||||||
|
base64d = base64d[1:]
|
||||||
|
decompress = True
|
||||||
|
json = b64_decode(base64d)
|
||||||
|
if decompress:
|
||||||
|
json = zlib.decompress(json)
|
||||||
|
return simplejson.loads(json)
|
||||||
|
|
||||||
|
|
||||||
|
class Signer(object):
|
||||||
|
def __init__(self, key=None, sep=':', salt=None):
|
||||||
|
self.sep = sep
|
||||||
|
self.key = key or settings.SECRET_KEY
|
||||||
|
self.salt = salt or ('%s.%s' %
|
||||||
|
(self.__class__.__module__, self.__class__.__name__))
|
||||||
|
|
||||||
|
def signature(self, value):
|
||||||
|
return base64_hmac(self.salt + 'signer', value, self.key)
|
||||||
|
|
||||||
|
def sign(self, value):
|
||||||
|
value = smart_str(value)
|
||||||
|
return '%s%s%s' % (value, self.sep, self.signature(value))
|
||||||
|
|
||||||
|
def unsign(self, signed_value):
|
||||||
|
signed_value = smart_str(signed_value)
|
||||||
|
if not self.sep in signed_value:
|
||||||
|
raise BadSignature('No "%s" found in value' % self.sep)
|
||||||
|
value, sig = signed_value.rsplit(self.sep, 1)
|
||||||
|
if constant_time_compare(sig, self.signature(value)):
|
||||||
|
return force_unicode(value)
|
||||||
|
raise BadSignature('Signature "%s" does not match' % sig)
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampSigner(Signer):
|
||||||
|
def timestamp(self):
|
||||||
|
return baseconv.base62.encode(int(time.time()))
|
||||||
|
|
||||||
|
def sign(self, value):
|
||||||
|
value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
|
||||||
|
return '%s%s%s' % (value, self.sep, self.signature(value))
|
||||||
|
|
||||||
|
def unsign(self, value, max_age=None):
|
||||||
|
result = super(TimestampSigner, self).unsign(value)
|
||||||
|
value, timestamp = result.rsplit(self.sep, 1)
|
||||||
|
timestamp = baseconv.base62.decode(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
|
|
@ -122,6 +122,7 @@ from django.utils.encoding import smart_str, iri_to_uri, force_unicode
|
||||||
from django.utils.http import cookie_date
|
from django.utils.http import cookie_date
|
||||||
from django.http.multipartparser import MultiPartParser
|
from django.http.multipartparser import MultiPartParser
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core import signing
|
||||||
from django.core.files import uploadhandler
|
from django.core.files import uploadhandler
|
||||||
from utils import *
|
from utils import *
|
||||||
|
|
||||||
|
@ -132,6 +133,8 @@ absolute_http_url_re = re.compile(r"^https?://", re.I)
|
||||||
class Http404(Exception):
|
class Http404(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
RAISE_ERROR = object()
|
||||||
|
|
||||||
class HttpRequest(object):
|
class HttpRequest(object):
|
||||||
"""A basic HTTP request."""
|
"""A basic HTTP request."""
|
||||||
|
|
||||||
|
@ -170,6 +173,29 @@ class HttpRequest(object):
|
||||||
# Rather than crash if this doesn't happen, we encode defensively.
|
# Rather than crash if this doesn't happen, we encode defensively.
|
||||||
return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
|
return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
|
||||||
|
|
||||||
|
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 = signing.get_cookie_signer(salt=key + salt).unsign(
|
||||||
|
cookie_value, max_age=max_age)
|
||||||
|
except signing.BadSignature:
|
||||||
|
if default is not RAISE_ERROR:
|
||||||
|
return default
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
return value
|
||||||
|
|
||||||
def build_absolute_uri(self, location=None):
|
def build_absolute_uri(self, location=None):
|
||||||
"""
|
"""
|
||||||
Builds an absolute URI from the location and the variables available in
|
Builds an absolute URI from the location and the variables available in
|
||||||
|
@ -584,6 +610,10 @@ class HttpResponse(object):
|
||||||
if httponly:
|
if httponly:
|
||||||
self.cookies[key]['httponly'] = True
|
self.cookies[key]['httponly'] = True
|
||||||
|
|
||||||
|
def set_signed_cookie(self, key, value, salt='', **kwargs):
|
||||||
|
value = signing.get_cookie_signer(salt=key + salt).sign(value)
|
||||||
|
return self.set_cookie(key, value, **kwargs)
|
||||||
|
|
||||||
def delete_cookie(self, key, path='/', domain=None):
|
def delete_cookie(self, key, path='/', domain=None):
|
||||||
self.set_cookie(key, max_age=0, path=path, domain=domain,
|
self.set_cookie(key, max_age=0, path=path, domain=domain,
|
||||||
expires='Thu, 01-Jan-1970 00:00:00 GMT')
|
expires='Thu, 01-Jan-1970 00:00:00 GMT')
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Copyright (c) 2010 Taurinus Collective. All rights reserved.
|
||||||
|
# Copyright (c) 2009 Simon Willison. All rights reserved.
|
||||||
|
# Copyright (c) 2002 Drew Perttula. All rights reserved.
|
||||||
|
#
|
||||||
|
# License:
|
||||||
|
# Python Software Foundation License version 2
|
||||||
|
#
|
||||||
|
# See the file "LICENSE" for terms & conditions for usage, and a DISCLAIMER OF
|
||||||
|
# ALL WARRANTIES.
|
||||||
|
#
|
||||||
|
# This Baseconv distribution contains no GNU General Public Licensed (GPLed)
|
||||||
|
# code so it may be used in proprietary projects just like prior ``baseconv``
|
||||||
|
# distributions.
|
||||||
|
#
|
||||||
|
# All trademarks referenced herein are property of their respective holders.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert numbers from base 10 integers to base X strings and back again.
|
||||||
|
|
||||||
|
Sample usage::
|
||||||
|
|
||||||
|
>>> base20 = BaseConverter('0123456789abcdefghij')
|
||||||
|
>>> base20.encode(1234)
|
||||||
|
'31e'
|
||||||
|
>>> base20.decode('31e')
|
||||||
|
1234
|
||||||
|
>>> base20.encode(-1234)
|
||||||
|
'-31e'
|
||||||
|
>>> base20.decode('-31e')
|
||||||
|
-1234
|
||||||
|
>>> base11 = BaseConverter('0123456789-', sign='$')
|
||||||
|
>>> base11.encode('$1234')
|
||||||
|
'$-22'
|
||||||
|
>>> base11.decode('$-22')
|
||||||
|
'$1234'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
BASE2_ALPHABET = '01'
|
||||||
|
BASE16_ALPHABET = '0123456789ABCDEF'
|
||||||
|
BASE56_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz'
|
||||||
|
BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||||
|
BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||||
|
BASE64_ALPHABET = BASE62_ALPHABET + '-_'
|
||||||
|
|
||||||
|
class BaseConverter(object):
|
||||||
|
decimal_digits = '0123456789'
|
||||||
|
|
||||||
|
def __init__(self, digits, sign='-'):
|
||||||
|
self.sign = sign
|
||||||
|
self.digits = digits
|
||||||
|
if sign in self.digits:
|
||||||
|
raise ValueError('Sign character found in converter base digits.')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<BaseConverter: base%s (%s)>" % (len(self.digits), self.digits)
|
||||||
|
|
||||||
|
def encode(self, i):
|
||||||
|
neg, value = self.convert(i, self.decimal_digits, self.digits, '-')
|
||||||
|
if neg:
|
||||||
|
return self.sign + value
|
||||||
|
return value
|
||||||
|
|
||||||
|
def decode(self, s):
|
||||||
|
neg, value = self.convert(s, self.digits, self.decimal_digits, self.sign)
|
||||||
|
if neg:
|
||||||
|
value = '-' + value
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
def convert(self, number, from_digits, to_digits, sign):
|
||||||
|
if str(number)[0] == sign:
|
||||||
|
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(from_digits) + from_digits.index(digit)
|
||||||
|
|
||||||
|
# create the result in base 'len(to_digits)'
|
||||||
|
if x == 0:
|
||||||
|
res = to_digits[0]
|
||||||
|
else:
|
||||||
|
res = ''
|
||||||
|
while x > 0:
|
||||||
|
digit = x % len(to_digits)
|
||||||
|
res = to_digits[digit] + res
|
||||||
|
x = int(x / len(to_digits))
|
||||||
|
return neg, res
|
||||||
|
|
||||||
|
base2 = BaseConverter(BASE2_ALPHABET)
|
||||||
|
base16 = BaseConverter(BASE16_ALPHABET)
|
||||||
|
base36 = BaseConverter(BASE36_ALPHABET)
|
||||||
|
base56 = BaseConverter(BASE56_ALPHABET)
|
||||||
|
base62 = BaseConverter(BASE62_ALPHABET)
|
||||||
|
base64 = BaseConverter(BASE64_ALPHABET, sign='$')
|
|
@ -171,6 +171,7 @@ Other batteries included
|
||||||
* :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
|
* :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
|
||||||
* :doc:`Content types <ref/contrib/contenttypes>`
|
* :doc:`Content types <ref/contrib/contenttypes>`
|
||||||
* :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
|
* :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
|
||||||
|
* :doc:`Cryptographic signing <topics/signing>`
|
||||||
* :doc:`Databrowse <ref/contrib/databrowse>`
|
* :doc:`Databrowse <ref/contrib/databrowse>`
|
||||||
* :doc:`E-mail (sending) <topics/email>`
|
* :doc:`E-mail (sending) <topics/email>`
|
||||||
* :doc:`Flatpages <ref/contrib/flatpages>`
|
* :doc:`Flatpages <ref/contrib/flatpages>`
|
||||||
|
|
|
@ -240,6 +240,43 @@ Methods
|
||||||
|
|
||||||
Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
|
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.4
|
||||||
|
|
||||||
|
Returns a cookie value for a signed cookie, or raises a
|
||||||
|
:class:`~django.core.signing.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 not 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', 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', False, max_age=60)
|
||||||
|
False
|
||||||
|
|
||||||
|
See :doc:`cryptographic signing </topics/signing>` for more information.
|
||||||
|
|
||||||
.. method:: HttpRequest.is_secure()
|
.. method:: HttpRequest.is_secure()
|
||||||
|
|
||||||
Returns ``True`` if the request is secure; that is, if it was made with
|
Returns ``True`` if the request is secure; that is, if it was made with
|
||||||
|
@ -618,6 +655,17 @@ Methods
|
||||||
.. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
|
.. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
|
||||||
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
|
.. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
|
||||||
|
|
||||||
|
.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
Like :meth:`~HttpResponse.set_cookie()`, but
|
||||||
|
:doc:`cryptographic signing </topics/signing>` the cookie before setting
|
||||||
|
it. Use in conjunction with :meth:`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
|
||||||
|
:meth:`HttpRequest.get_signed_cookie` call.
|
||||||
|
|
||||||
.. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
|
.. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
|
||||||
|
|
||||||
Deletes the cookie with the given key. Fails silently if the key doesn't
|
Deletes the cookie with the given key. Fails silently if the key doesn't
|
||||||
|
|
|
@ -1647,6 +1647,19 @@ See :tfilter:`allowed date format strings <date>`.
|
||||||
|
|
||||||
See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
|
See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
|
||||||
|
|
||||||
|
.. setting:: SIGNING_BACKEND
|
||||||
|
|
||||||
|
SIGNING_BACKEND
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
Default: 'django.core.signing.TimestampSigner'
|
||||||
|
|
||||||
|
The backend used for signing cookies and other data.
|
||||||
|
|
||||||
|
See also the :doc:`/topics/signing` documentation.
|
||||||
|
|
||||||
.. setting:: SITE_ID
|
.. setting:: SITE_ID
|
||||||
|
|
||||||
SITE_ID
|
SITE_ID
|
||||||
|
|
|
@ -46,6 +46,15 @@ not custom filters. This has been rectified with a simple API previously
|
||||||
known as "FilterSpec" which was used internally. For more details, see the
|
known as "FilterSpec" which was used internally. For more details, see the
|
||||||
documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
|
documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
|
||||||
|
|
||||||
|
Tools for cryptographic signing
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Django 1.4 adds 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.
|
||||||
|
|
||||||
|
See :doc:`cryptographic signing </topics/signing>` docs for more information.
|
||||||
|
|
||||||
``reverse_lazy``
|
``reverse_lazy``
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ Introductions to all the key parts of Django you'll need to know:
|
||||||
auth
|
auth
|
||||||
cache
|
cache
|
||||||
conditional-view-processing
|
conditional-view-processing
|
||||||
|
signing
|
||||||
email
|
email
|
||||||
i18n/index
|
i18n/index
|
||||||
logging
|
logging
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
=====================
|
||||||
|
Cryptographic signing
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. module:: django.core.signing
|
||||||
|
:synopsis: Django's signing framework.
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
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 :djadmin:`startproject`, the
|
||||||
|
``settings.py`` file it generates automatically gets a random
|
||||||
|
:setting:`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
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. class:: Signer
|
||||||
|
|
||||||
|
Django's signing methods live in the ``django.core.signing`` module.
|
||||||
|
To sign a value, first instantiate a ``Signer`` instance::
|
||||||
|
|
||||||
|
>>> from django.core.signing 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
|
||||||
|
``django.core.signing.BadSigature`` exception will be raised::
|
||||||
|
|
||||||
|
>>> value += 'm'
|
||||||
|
>>> try:
|
||||||
|
... original = signer.unsign(value)
|
||||||
|
... except signing.BadSignature:
|
||||||
|
... print "Tampering detected!"
|
||||||
|
|
||||||
|
By default, the ``Signer`` class uses the :setting:`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 ``Signer``
|
||||||
|
class to further strengthen your :setting:`SECRET_KEY` against brute force
|
||||||
|
attacks. Using a salt will cause a new key to be derived from both the salt
|
||||||
|
and your :setting:`SECRET_KEY`::
|
||||||
|
|
||||||
|
>>> signer = Signer()
|
||||||
|
>>> signer.sign('My string')
|
||||||
|
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
|
||||||
|
>>> signer = Signer(salt='extra')
|
||||||
|
>>> signer.sign('My string')
|
||||||
|
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
|
||||||
|
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
|
||||||
|
u'My string'
|
||||||
|
|
||||||
|
Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
|
||||||
|
secret.
|
||||||
|
|
||||||
|
Verifying timestamped values
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. class:: TimestampSigner
|
||||||
|
|
||||||
|
``TimestampSigner`` is a subclass of :class:`~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.core.signing 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
|
||||||
|
signing module's dumps and loads functions. These imitate Python's pickle
|
||||||
|
module, but uses JSON serialization under the hood. JSON ensures that even
|
||||||
|
if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
|
||||||
|
execute arbitrary commands by exploiting the pickle format.::
|
||||||
|
|
||||||
|
>>> from django.core import signing
|
||||||
|
>>> value = signing.dumps({"foo": "bar"})
|
||||||
|
>>> value
|
||||||
|
'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
|
||||||
|
>>> signing.loads(value)
|
||||||
|
{'foo': 'bar'}
|
|
@ -0,0 +1 @@
|
||||||
|
# models.py file for tests to run.
|
|
@ -0,0 +1,61 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.core import signing
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
class SignedCookieTest(TestCase):
|
||||||
|
|
||||||
|
def test_can_set_and_read_signed_cookies(self):
|
||||||
|
response = HttpResponse()
|
||||||
|
response.set_signed_cookie('c', 'hello')
|
||||||
|
self.assertIn('c', response.cookies)
|
||||||
|
self.assertTrue(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(signing.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(signing.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):
|
||||||
|
value = u'hello'
|
||||||
|
_time = time.time
|
||||||
|
time.time = lambda: 123456789
|
||||||
|
try:
|
||||||
|
response = HttpResponse()
|
||||||
|
response.set_signed_cookie('c', value)
|
||||||
|
request = HttpRequest()
|
||||||
|
request.COOKIES['c'] = response.cookies['c'].value
|
||||||
|
self.assertEqual(request.get_signed_cookie('c'), value)
|
||||||
|
|
||||||
|
time.time = lambda: 123456800
|
||||||
|
self.assertEqual(request.get_signed_cookie('c', max_age=12), value)
|
||||||
|
self.assertEqual(request.get_signed_cookie('c', max_age=11), value)
|
||||||
|
self.assertRaises(signing.SignatureExpired,
|
||||||
|
request.get_signed_cookie, 'c', max_age = 10)
|
||||||
|
finally:
|
||||||
|
time.time = _time
|
|
@ -0,0 +1 @@
|
||||||
|
# models.py file for tests to run.
|
|
@ -0,0 +1,116 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.core import signing
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils.encoding import force_unicode
|
||||||
|
|
||||||
|
class TestSigner(TestCase):
|
||||||
|
|
||||||
|
def test_signature(self):
|
||||||
|
"signature() method should generate a signature"
|
||||||
|
signer = signing.Signer('predictable-secret')
|
||||||
|
signer2 = signing.Signer('predictable-secret2')
|
||||||
|
for s in (
|
||||||
|
'hello',
|
||||||
|
'3098247:529:087:',
|
||||||
|
u'\u2019'.encode('utf8'),
|
||||||
|
):
|
||||||
|
self.assertEqual(
|
||||||
|
signer.signature(s),
|
||||||
|
signing.base64_hmac(signer.salt + 'signer', s,
|
||||||
|
'predictable-secret')
|
||||||
|
)
|
||||||
|
self.assertNotEqual(signer.signature(s), signer2.signature(s))
|
||||||
|
|
||||||
|
def test_signature_with_salt(self):
|
||||||
|
"signature(value, salt=...) should work"
|
||||||
|
signer = signing.Signer('predictable-secret', salt='extra-salt')
|
||||||
|
self.assertEqual(
|
||||||
|
signer.signature('hello'),
|
||||||
|
signing.base64_hmac('extra-salt' + 'signer',
|
||||||
|
'hello', 'predictable-secret'))
|
||||||
|
self.assertNotEqual(
|
||||||
|
signing.Signer('predictable-secret', salt='one').signature('hello'),
|
||||||
|
signing.Signer('predictable-secret', salt='two').signature('hello'))
|
||||||
|
|
||||||
|
def test_sign_unsign(self):
|
||||||
|
"sign/unsign should be reversible"
|
||||||
|
signer = signing.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.assertNotEqual(
|
||||||
|
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 = signing.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(
|
||||||
|
signing.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.assertNotEqual(o, signing.dumps(o))
|
||||||
|
self.assertEqual(o, signing.loads(signing.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 = signing.dumps(value)
|
||||||
|
self.assertEqual(value, signing.loads(encoded))
|
||||||
|
for transform in transforms:
|
||||||
|
self.assertRaises(
|
||||||
|
signing.BadSignature, signing.loads, transform(encoded))
|
||||||
|
|
||||||
|
class TestTimestampSigner(TestCase):
|
||||||
|
|
||||||
|
def test_timestamp_signer(self):
|
||||||
|
value = u'hello'
|
||||||
|
_time = time.time
|
||||||
|
time.time = lambda: 123456789
|
||||||
|
try:
|
||||||
|
signer = signing.TimestampSigner('predictable-key')
|
||||||
|
ts = signer.sign(value)
|
||||||
|
self.assertNotEqual(ts,
|
||||||
|
signing.Signer('predictable-key').sign(value))
|
||||||
|
|
||||||
|
self.assertEqual(signer.unsign(ts), value)
|
||||||
|
time.time = lambda: 123456800
|
||||||
|
self.assertEqual(signer.unsign(ts, max_age=12), value)
|
||||||
|
self.assertEqual(signer.unsign(ts, max_age=11), value)
|
||||||
|
self.assertRaises(
|
||||||
|
signing.SignatureExpired, signer.unsign, ts, max_age=10)
|
||||||
|
finally:
|
||||||
|
time.time = _time
|
|
@ -0,0 +1,41 @@
|
||||||
|
from unittest import TestCase
|
||||||
|
from django.utils.baseconv import base2, base16, base36, base56, base62, base64, BaseConverter
|
||||||
|
|
||||||
|
class TestBaseConv(TestCase):
|
||||||
|
|
||||||
|
def test_baseconv(self):
|
||||||
|
nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
|
||||||
|
for converter in [base2, base16, base36, base56, base62, base64]:
|
||||||
|
for i in nums:
|
||||||
|
self.assertEqual(i, converter.decode(converter.encode(i)))
|
||||||
|
|
||||||
|
def test_base11(self):
|
||||||
|
base11 = BaseConverter('0123456789-', sign='$')
|
||||||
|
self.assertEqual(base11.encode(1234), '-22')
|
||||||
|
self.assertEqual(base11.decode('-22'), 1234)
|
||||||
|
self.assertEqual(base11.encode(-1234), '$-22')
|
||||||
|
self.assertEqual(base11.decode('$-22'), -1234)
|
||||||
|
|
||||||
|
def test_base20(self):
|
||||||
|
base20 = BaseConverter('0123456789abcdefghij')
|
||||||
|
self.assertEqual(base20.encode(1234), '31e')
|
||||||
|
self.assertEqual(base20.decode('31e'), 1234)
|
||||||
|
self.assertEqual(base20.encode(-1234), '-31e')
|
||||||
|
self.assertEqual(base20.decode('-31e'), -1234)
|
||||||
|
|
||||||
|
def test_base64(self):
|
||||||
|
self.assertEqual(base64.encode(1234), 'JI')
|
||||||
|
self.assertEqual(base64.decode('JI'), 1234)
|
||||||
|
self.assertEqual(base64.encode(-1234), '$JI')
|
||||||
|
self.assertEqual(base64.decode('$JI'), -1234)
|
||||||
|
|
||||||
|
def test_base7(self):
|
||||||
|
base7 = BaseConverter('cjdhel3', sign='g')
|
||||||
|
self.assertEqual(base7.encode(1234), 'hejd')
|
||||||
|
self.assertEqual(base7.decode('hejd'), 1234)
|
||||||
|
self.assertEqual(base7.encode(-1234), 'ghejd')
|
||||||
|
self.assertEqual(base7.decode('ghejd'), -1234)
|
||||||
|
|
||||||
|
def test_exception(self):
|
||||||
|
self.assertRaises(ValueError, BaseConverter, 'abc', sign='a')
|
||||||
|
self.assertTrue(isinstance(BaseConverter('abc', sign='d'), BaseConverter))
|
|
@ -17,3 +17,4 @@ from timesince import *
|
||||||
from datastructures import *
|
from datastructures import *
|
||||||
from tzinfo import *
|
from tzinfo import *
|
||||||
from datetime_safe import *
|
from datetime_safe import *
|
||||||
|
from baseconv import *
|
||||||
|
|
Loading…
Reference in New Issue