Ticket #12417: ticket12417-v4.diff
File ticket12417-v4.diff, 29.3 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..70fcc44
- + 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 hmac 36 import base64 37 import time 38 39 from django.conf import settings 40 from django.utils.hashcompat import sha_constructor 41 from django.utils import baseconv, simplejson 42 from django.utils.crypto import constant_time_compare 43 from django.utils.encoding import force_unicode, smart_str 44 from django.utils.importlib import import_module 45 46 class BadSignature(Exception): 47 """ 48 Signature does not match 49 """ 50 pass 51 52 53 class SignatureExpired(BadSignature): 54 """ 55 Signature timestamp is older than required max_age 56 """ 57 pass 58 59 60 def b64_encode(s): 61 return base64.urlsafe_b64encode(s).strip('=') 62 63 64 def b64_decode(s): 65 pad = '=' * (-len(s) % 4) 66 return base64.urlsafe_b64decode(s + pad) 67 68 69 def base64_hmac(value, key): 70 return b64_encode((hmac.new(key, value, sha_constructor).digest())) 71 72 73 def get_cookie_signer(): 74 modpath = settings.SIGNING_BACKEND 75 module, attr = modpath.rsplit('.', 1) 76 try: 77 mod = import_module(module) 78 except ImportError, e: 79 raise ImproperlyConfigured( 80 'Error importing cookie signer %s: "%s"' % (modpath, e)) 81 try: 82 Signer = getattr(mod, attr) 83 except AttributeError, e: 84 raise ImproperlyConfigured( 85 'Error importing cookie signer %s: "%s"' % (modpath, e)) 86 return Signer('django.http.cookies' + settings.SECRET_KEY) 87 88 89 def dumps(obj, key=None, salt='', compress=False): 90 """ 91 Returns URL-safe, sha1 signed base64 compressed JSON string. If key is 92 None, settings.SECRET_KEY is used instead. 93 94 If compress is True (not the default) checks if compressing using zlib can 95 save some space. Prepends a '.' to signify compression. This is included 96 in the signature, to protect against zip bombs. 97 98 salt can be used to further salt the hash, in case you're worried 99 that the NSA might try to brute-force your SHA-1 protected secret. 100 """ 101 json = simplejson.dumps(obj, separators=(',', ':')) 102 103 # Flag for if it's been compressed or not 104 is_compressed = False 105 106 if compress: 107 # Avoid zlib dependency unless compress is being used 108 import zlib 109 compressed = zlib.compress(json) 110 if len(compressed) < (len(json) - 1): 111 json = compressed 112 is_compressed = True 113 base64d = b64_encode(json) 114 if is_compressed: 115 base64d = '.' + base64d 116 return TimestampSigner(key).sign(base64d, salt=salt) 117 118 119 def loads(s, key=None, salt='', max_age=None): 120 """ 121 Reverse of dumps(), raises BadSignature if signature fails 122 """ 123 base64d = smart_str( 124 TimestampSigner(key).unsign(s, salt=salt, max_age=max_age)) 125 decompress = False 126 if base64d[0] == '.': 127 # It's compressed; uncompress it first 128 base64d = base64d[1:] 129 decompress = True 130 json = b64_decode(base64d) 131 if decompress: 132 import zlib 133 jsond = zlib.decompress(json) 134 return simplejson.loads(json) 135 136 137 class Signer(object): 138 def __init__(self, key=None, sep=':'): 139 self.sep = sep 140 self.key = key or settings.SECRET_KEY 141 142 def signature(self, value, salt=''): 143 # Derive a new key from the SECRET_KEY, using the optional salt 144 key = sha_constructor(salt + 'signer' + self.key).hexdigest() 145 return base64_hmac(value, key) 146 147 def sign(self, value, salt=''): 148 value = smart_str(value) 149 return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt)) 150 151 def unsign(self, signed_value, salt=''): 152 signed_value = smart_str(signed_value) 153 if not self.sep in signed_value: 154 raise BadSignature('No "%s" found in value' % self.sep) 155 value, sig = signed_value.rsplit(self.sep, 1) 156 expected = self.signature(value, salt=salt) 157 if constant_time_compare(sig, expected): 158 return force_unicode(value) 159 # Important: do NOT include the expected sig in the exception 160 # message, since it might leak up to an attacker! 161 # TODO: Can we enforce this in the Django debug templates? 162 raise BadSignature('Signature "%s" does not match' % sig) 163 164 165 class TimestampSigner(Signer): 166 def timestamp(self): 167 return baseconv.base62.from_int(int(time.time())) 168 169 def sign(self, value, salt=''): 170 value = smart_str('%s%s%s' % (value, self.sep, self.timestamp())) 171 return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt)) 172 173 def unsign(self, value, salt='', max_age=None): 174 value, timestamp = super(TimestampSigner, self).unsign( 175 value, salt=salt).rsplit(self.sep, 1) 176 timestamp = baseconv.base62.to_int(timestamp) 177 if max_age is not None: 178 # Check timestamp is not older than max_age 179 age = time.time() - timestamp 180 if age > max_age: 181 raise SignatureExpired( 182 'Signature age %s > %s seconds' % (age, max_age)) 183 return value -
django/http/__init__.py
diff --git a/django/http/__init__.py b/django/http/__init__.py index 0d28ec0..0a0d665 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='', 177 max_age=None): 178 """ 179 Attempts to return a signed cookie. If the signature fails or the 180 cookie has expired, raises an exception... unless you provide the 181 default argument in which case that value will be returned instead. 182 """ 183 try: 184 cookie_value = self.COOKIES[key].encode('utf-8') 185 except KeyError: 186 if default is not RAISE_ERROR: 187 return default 188 else: 189 raise 190 try: 191 value = signing.get_cookie_signer().unsign( 192 cookie_value, salt=key + salt, max_age=max_age) 193 except signing.BadSignature: 194 if default is not RAISE_ERROR: 195 return default 196 else: 197 raise 198 return value 199 173 200 def build_absolute_uri(self, location=None): 174 201 """ 175 202 Builds an absolute URI from the location and the variables available in … … class HttpResponse(object): 584 611 if httponly: 585 612 self.cookies[key]['httponly'] = True 586 613 614 def set_signed_cookie(self, key, value, salt='', **kwargs): 615 value = signing.get_cookie_signer().sign(value, salt=key + salt) 616 return self.set_cookie(key, value, **kwargs) 617 587 618 def delete_cookie(self, key, path='/', domain=None): 588 619 self.set_cookie(key, max_age=0, path=path, domain=domain, 589 620 expires='Thu, 01-Jan-1970 00:00:00 GMT') … … def str_to_unicode(s, encoding): 686 717 return unicode(s, encoding, 'replace') 687 718 else: 688 719 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..db152f7
- + 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.from_int(1234) 8 '31e' 9 >>> base20.to_int('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 from_int(self, i): 21 return self.convert(i, self.decimal_digits, self.digits) 22 23 def to_int(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..c94462c
- + 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 ``sign`` and 85 ``unsign`` methods to further strengthen your :setting:`SECRET_KEY` against 86 brute force attacks. Using a salt will cause a new key to be derived from 87 both the salt and your :setting:`SECRET_KEY`:: 88 89 >>> signer = Signer() 90 >>> signer.sign('My string') 91 'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w' 92 >>> signer.sign('My string', salt='extra') 93 'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw' 94 >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw', salt='extra') 95 u'My string' 96 97 Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay 98 secret. 99 100 Verifying timestamped values 101 ---------------------------- 102 103 .. class:: TimestampSigner 104 105 ``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed 106 timestamp to the value. This allows you to confirm that a signed value was 107 created within a specified period of time:: 108 109 >>> from django.core.signing import TimestampSigner 110 >>> signer = TimestampSigner() 111 >>> value = signer.sign('hello') 112 >>> value 113 'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c' 114 >>> signer.unsign(value) 115 u'hello' 116 >>> signer.unsign(value, max_age=10) 117 ... 118 SignatureExpired: Signature age 15.5289158821 > 10 seconds 119 >>> signer.unsign(value, max_age=20) 120 u'hello' 121 122 Protecting complex data structures 123 ---------------------------------- 124 125 If you wish to protect a list, tuple or dictionary you can do so using the 126 signing module's dumps and loads functions. These imitate Python's pickle 127 module, but uses JSON serialization under the hood. JSON ensures that even 128 if your :setting:`SECRET_KEY` is stolen an attacker will not be able to 129 execute arbitrary commands by exploiting the pickle format.:: 130 131 >>> from django.core import signing 132 >>> value = signing.dumps({"foo": "bar"}) 133 >>> value 134 'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI' 135 >>> signing.loads(value) 136 {'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..0c28f53
- + 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 from django.utils.hashcompat import sha_constructor 7 8 class TestSigner(TestCase): 9 10 def test_signature(self): 11 "signature() method should generate a signature" 12 signer = signing.Signer('predictable-secret') 13 signer2 = signing.Signer('predictable-secret2') 14 for s in ( 15 'hello', 16 '3098247:529:087:', 17 u'\u2019'.encode('utf8'), 18 ): 19 self.assertEqual( 20 signer.signature(s), 21 signing.base64_hmac(s, sha_constructor( 22 'signer' + 'predictable-secret' 23 ).hexdigest()) 24 ) 25 self.assertNotEqual(signer.signature(s), signer2.signature(s)) 26 27 def test_signature_with_salt(self): 28 "signature(value, salt=...) should work" 29 signer = signing.Signer('predictable-secret') 30 self.assertEqual( 31 signer.signature('hello', salt='extra-salt'), 32 signing.base64_hmac('hello', sha_constructor( 33 'extra-salt' + 'signer' + 'predictable-secret' 34 ).hexdigest()) 35 ) 36 self.assertNotEqual( 37 signer.signature('hello', salt='one'), 38 signer.signature('hello', salt='two')) 39 40 def test_sign_unsign(self): 41 "sign/unsign should be reversible" 42 signer = signing.Signer('predictable-secret') 43 examples = ( 44 'q;wjmbk;wkmb', 45 '3098247529087', 46 '3098247:529:087:', 47 'jkw osanteuh ,rcuh nthu aou oauh ,ud du', 48 u'\u2019', 49 ) 50 for example in examples: 51 self.assertNotEqual( 52 force_unicode(example), force_unicode(signer.sign(example))) 53 self.assertEqual(example, signer.unsign(signer.sign(example))) 54 55 def unsign_detects_tampering(self): 56 "unsign should raise an exception if the value has been tampered with" 57 signer = signing.Signer('predictable-secret') 58 value = 'Another string' 59 signed_value = signer.sign(value) 60 transforms = ( 61 lambda s: s.upper(), 62 lambda s: s + 'a', 63 lambda s: 'a' + s[1:], 64 lambda s: s.replace(':', ''), 65 ) 66 self.assertEqual(value, signer.unsign(signed_value)) 67 for transform in transforms: 68 self.assertRaises( 69 signing.BadSignature, signer.unsign, transform(signed_value)) 70 71 def test_dumps_loads(self): 72 "dumps and loads be reversible for any JSON serializable object" 73 objects = ( 74 ['a', 'list'], 75 'a string', 76 u'a unicode string \u2019', 77 {'a': 'dictionary'}, 78 ) 79 for o in objects: 80 self.assertNotEqual(o, signing.dumps(o)) 81 self.assertEqual(o, signing.loads(signing.dumps(o))) 82 83 def test_decode_detects_tampering(self): 84 "loads should raise exception for tampered objects" 85 transforms = ( 86 lambda s: s.upper(), 87 lambda s: s + 'a', 88 lambda s: 'a' + s[1:], 89 lambda s: s.replace(':', ''), 90 ) 91 value = { 92 'foo': 'bar', 93 'baz': 1, 94 } 95 encoded = signing.dumps(value) 96 self.assertEqual(value, signing.loads(encoded)) 97 for transform in transforms: 98 self.assertRaises( 99 signing.BadSignature, signing.loads, transform(encoded)) 100 101 class TestTimestampSigner(TestCase): 102 103 def test_timestamp_signer(self): 104 value = u'hello' 105 _time = time.time 106 time.time = lambda: 123456789 107 try: 108 signer = signing.TimestampSigner('predictable-key') 109 ts = signer.sign(value) 110 self.assertNotEqual(ts, 111 signing.Signer('predictable-key').sign(value)) 112 113 self.assertEqual(signer.unsign(ts), value) 114 time.time = lambda: 123456800 115 self.assertEqual(signer.unsign(ts, max_age=12), value) 116 self.assertEqual(signer.unsign(ts, max_age=11), value) 117 self.assertRaises( 118 signing.SignatureExpired, signer.unsign, ts, max_age=10) 119 finally: 120 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..90fe77f
- + 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( 11 i, convertor.to_int(convertor.from_int(i)) 12 ) 13 -
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 *