mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		| @@ -476,6 +476,12 @@ LOGIN_REDIRECT_URL = '/accounts/profile/' | ||||
| # The number of days a password reset link is valid for | ||||
| PASSWORD_RESET_TIMEOUT_DAYS = 3 | ||||
|  | ||||
| ########### | ||||
| # SIGNING # | ||||
| ########### | ||||
|  | ||||
| SIGNING_BACKEND = 'django.core.signing.TimestampSigner' | ||||
|  | ||||
| ######## | ||||
| # CSRF # | ||||
| ######## | ||||
|   | ||||
							
								
								
									
										178
									
								
								django/core/signing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								django/core/signing.py
									
									
									
									
									
										Normal 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 | ||||
| @@ -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.http.multipartparser import MultiPartParser | ||||
| from django.conf import settings | ||||
| from django.core import signing | ||||
| from django.core.files import uploadhandler | ||||
| from utils import * | ||||
|  | ||||
| @@ -132,6 +133,8 @@ absolute_http_url_re = re.compile(r"^https?://", re.I) | ||||
| class Http404(Exception): | ||||
|     pass | ||||
|  | ||||
| RAISE_ERROR = object() | ||||
|  | ||||
| class HttpRequest(object): | ||||
|     """A basic HTTP request.""" | ||||
|  | ||||
| @@ -170,6 +173,29 @@ class HttpRequest(object): | ||||
|         # 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 '') | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
|         Builds an absolute URI from the location and the variables available in | ||||
| @@ -584,6 +610,10 @@ class HttpResponse(object): | ||||
|         if httponly: | ||||
|             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): | ||||
|         self.set_cookie(key, max_age=0, path=path, domain=domain, | ||||
|                         expires='Thu, 01-Jan-1970 00:00:00 GMT') | ||||
|   | ||||
							
								
								
									
										99
									
								
								django/utils/baseconv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								django/utils/baseconv.py
									
									
									
									
									
										Normal 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='$') | ||||
| @@ -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:`Content types <ref/contrib/contenttypes>` | ||||
|     * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>` | ||||
|     * :doc:`Cryptographic signing <topics/signing>` | ||||
|     * :doc:`Databrowse <ref/contrib/databrowse>` | ||||
|     * :doc:`E-mail (sending) <topics/email>` | ||||
|     * :doc:`Flatpages <ref/contrib/flatpages>` | ||||
|   | ||||
| @@ -240,6 +240,43 @@ Methods | ||||
|  | ||||
|    Example: ``"http://example.com/music/bands/the_beatles/?print=true"`` | ||||
|  | ||||
| .. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None) | ||||
|  | ||||
|    .. versionadded:: 1.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() | ||||
|  | ||||
|    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 | ||||
|     .. _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) | ||||
|  | ||||
|     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``. | ||||
|  | ||||
| .. 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 | ||||
|  | ||||
| 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 | ||||
| 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`` | ||||
| ~~~~~~~~~~~~~~~~ | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,7 @@ Introductions to all the key parts of Django you'll need to know: | ||||
|    auth | ||||
|    cache | ||||
|    conditional-view-processing | ||||
|    signing | ||||
|    email | ||||
|    i18n/index | ||||
|    logging | ||||
|   | ||||
							
								
								
									
										135
									
								
								docs/topics/signing.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								docs/topics/signing.txt
									
									
									
									
									
										Normal 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'} | ||||
							
								
								
									
										1
									
								
								tests/regressiontests/signed_cookies_tests/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/regressiontests/signed_cookies_tests/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| # models.py file for tests to run. | ||||
							
								
								
									
										61
									
								
								tests/regressiontests/signed_cookies_tests/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								tests/regressiontests/signed_cookies_tests/tests.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										0
									
								
								tests/regressiontests/signing/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/regressiontests/signing/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										1
									
								
								tests/regressiontests/signing/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/regressiontests/signing/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| # models.py file for tests to run. | ||||
							
								
								
									
										116
									
								
								tests/regressiontests/signing/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								tests/regressiontests/signing/tests.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										41
									
								
								tests/regressiontests/utils/baseconv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tests/regressiontests/utils/baseconv.py
									
									
									
									
									
										Normal 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)) | ||||
| @@ -17,3 +17,4 @@ from timesince import * | ||||
| from datastructures import * | ||||
| from tzinfo import * | ||||
| from datetime_safe import * | ||||
| from baseconv import * | ||||
|   | ||||
		Reference in New Issue
	
	Block a user