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