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:
Jannis Leidel 2011-05-21 14:41:14 +00:00
parent 15793309e1
commit f60d428463
18 changed files with 741 additions and 0 deletions

View File

@ -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 #
######## ########

178
django/core/signing.py Normal file
View File

@ -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

View File

@ -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')

99
django/utils/baseconv.py Normal file
View File

@ -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='$')

View File

@ -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>`

View File

@ -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

View File

@ -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

View File

@ -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``
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~

View File

@ -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

135
docs/topics/signing.txt Normal file
View File

@ -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'}

View File

@ -0,0 +1 @@
# models.py file for tests to run.

View File

@ -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

View File

@ -0,0 +1 @@
# models.py file for tests to run.

View File

@ -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

View File

@ -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))

View File

@ -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 *