Ticket #12417: ticket12417-v2.diff
File ticket12417-v2.diff, 28.0 KB (added by , 15 years ago) |
---|
-
django/conf/global_settings.py
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 828aef5..87a721b 100644
a b LOGIN_REDIRECT_URL = '/accounts/profile/' 383 383 # The number of days a password reset link is valid for 384 384 PASSWORD_RESET_TIMEOUT_DAYS = 3 385 385 386 ################## 387 # SIGNED COOKIES # 388 ################## 389 390 COOKIE_SIGNER_BACKEND = 'django.utils.signed.TimestampSigner' 391 386 392 ######## 387 393 # CSRF # 388 394 ######## -
django/http/__init__.py
diff --git a/django/http/__init__.py b/django/http/__init__.py index 7b0c469..d81a75e 100644
a b from django.http.multipartparser import MultiPartParser 16 16 from django.conf import settings 17 17 from django.core.files import uploadhandler 18 18 from utils import * 19 from django.utils import signed 19 20 20 21 RESERVED_CHARS="!*'();:@&=+$,/?%#[]" 21 22 … … absolute_http_url_re = re.compile(r"^https?://", re.I) 24 25 class Http404(Exception): 25 26 pass 26 27 28 RAISE_ERROR = object() 29 27 30 class HttpRequest(object): 28 31 """A basic HTTP request.""" 29 32 … … class HttpRequest(object): 60 63 def get_full_path(self): 61 64 return '' 62 65 66 def get_signed_cookie(self, key, default=RAISE_ERROR, salt = '', 67 max_age=None): 68 """ 69 Attempts to return a signed cookie. If the signature fails or the 70 cookie has expired, raises an exception... unless you provide the 71 default argument in which case that value will be returned instead. 72 """ 73 try: 74 cookie_value = self.COOKIES[key].encode('utf-8') 75 except KeyError: 76 if default is not RAISE_ERROR: 77 return default 78 else: 79 raise 80 try: 81 value = signed.get_cookie_signer().unsign( 82 cookie_value, salt = key + salt, max_age = max_age 83 ) 84 except signed.BadSignature: 85 if default is not RAISE_ERROR: 86 return default 87 else: 88 raise 89 return value 90 63 91 def build_absolute_uri(self, location=None): 64 92 """ 65 93 Builds an absolute URI from the location and the variables available in … … class HttpResponse(object): 357 385 if secure: 358 386 self.cookies[key]['secure'] = True 359 387 388 def set_signed_cookie(self, key, value, salt = '', **kwargs): 389 value = signed.get_cookie_signer().sign(value, salt=key + salt) 390 return self.set_cookie(key, value, **kwargs) 391 360 392 def delete_cookie(self, key, path='/', domain=None): 361 393 self.set_cookie(key, max_age=0, path=path, domain=domain, 362 394 expires='Thu, 01-Jan-1970 00:00:00 GMT') -
new file django/utils/baseconv.py
diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py new file mode 100644 index 0000000..f1d5aa0
- + 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 class BaseConverter(object): 14 decimal_digits = "0123456789" 15 16 def __init__(self, digits): 17 self.digits = digits 18 19 def from_int(self, i): 20 return self.convert(i, self.decimal_digits, self.digits) 21 22 def to_int(self, s): 23 return int(self.convert(s, self.digits, self.decimal_digits)) 24 25 def convert(number, fromdigits, todigits): 26 # Based on http://code.activestate.com/recipes/111286/ 27 if str(number)[0] == '-': 28 number = str(number)[1:] 29 neg = 1 30 else: 31 neg = 0 32 33 # make an integer out of the number 34 x = 0 35 for digit in str(number): 36 x = x * len(fromdigits) + fromdigits.index(digit) 37 38 # create the result in base 'len(todigits)' 39 if x == 0: 40 res = todigits[0] 41 else: 42 res = "" 43 while x > 0: 44 digit = x % len(todigits) 45 res = todigits[digit] + res 46 x = int(x / len(todigits)) 47 if neg: 48 res = '-' + res 49 return res 50 convert = staticmethod(convert) 51 52 base2 = BaseConverter('01') 53 base16 = BaseConverter('0123456789ABCDEF') 54 base36 = BaseConverter('0123456789abcdefghijklmnopqrstuvwxyz') 55 base62 = BaseConverter( 56 '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 57 ) -
new file django/utils/signed.py
diff --git a/django/utils/signed.py b/django/utils/signed.py new file mode 100644 index 0000000..c7770bc
- + 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 36 from django.conf import settings 37 from django.utils.hashcompat import sha_constructor 38 from django.utils import baseconv, simplejson 39 from django.utils.encoding import force_unicode, smart_str 40 from django.utils.importlib import import_module 41 import hmac, base64, time 42 43 def dumps(obj, key = None, compress = False, salt = ''): 44 """ 45 Returns URL-safe, sha1 signed base64 compressed JSON string. If key is 46 None, settings.SECRET_KEY is used instead. 47 48 If compress is True (not the default) checks if compressing using zlib can 49 save some space. Prepends a '.' to signify compression. This is included 50 in the signature, to protect against zip bombs. 51 52 salt can be used to further salt the hash, in case you're worried 53 that the NSA might try to brute-force your SHA-1 protected secret. 54 """ 55 json = simplejson.dumps(obj, separators=(',', ':')) 56 is_compressed = False # Flag for if it's been compressed or not 57 if compress: 58 import zlib # Avoid zlib dependency unless compress is being used 59 compressed = zlib.compress(json) 60 if len(compressed) < (len(json) - 1): 61 json = compressed 62 is_compressed = True 63 base64d = b64_encode(json).strip('=') 64 if is_compressed: 65 base64d = '.' + base64d 66 return TimestampSigner(key).sign(base64d, salt=salt) 67 68 def loads(s, key = None, salt = '', max_age=None): 69 "Reverse of dumps(), raises BadSignature if signature fails" 70 try: 71 base64d = smart_str(TimestampSigner(key).unsign( 72 s, salt=salt, max_age=max_age 73 )) 74 except BadSignature: 75 raise 76 decompress = False 77 if base64d[0] == '.': 78 # It's compressed; uncompress it first 79 base64d = base64d[1:] 80 decompress = True 81 json = b64_decode(base64d) 82 if decompress: 83 import zlib 84 jsond = zlib.decompress(json) 85 return simplejson.loads(json) 86 87 def b64_encode(s): 88 return base64.urlsafe_b64encode(s).strip('=') 89 90 def b64_decode(s): 91 return base64.urlsafe_b64decode(s + '=' * (len(s) % 4)) 92 93 def base64_hmac(value, key): 94 return b64_encode( 95 (hmac.new(key, value, sha_constructor).digest()) 96 ) 97 98 class BadSignature(Exception): 99 "Signature does not match" 100 pass 101 102 class SignatureExpired(BadSignature): 103 "Signature timestamp is older than required max_age" 104 pass 105 106 def get_cookie_signer(): 107 modpath = settings.COOKIE_SIGNER_BACKEND 108 module, attr = modpath.rsplit('.', 1) 109 try: 110 mod = import_module(module) 111 except ImportError, e: 112 raise ImproperlyConfigured( 113 'Error importing cookie signer %s: "%s"' % (modpath, e) 114 ) 115 try: 116 Signer = getattr(mod, attr) 117 except AttributeError, e: 118 raise ImproperlyConfigured( 119 'Error importing cookie signer %s: "%s"' % (modpath, e) 120 ) 121 return Signer(settings.SECRET_KEY) 122 123 class Signer(object): 124 def __init__(self, key=None): 125 self.key = key or settings.SECRET_KEY 126 127 def signature(self, value, salt=''): 128 # Derive a new key from the SECRET_KEY, using the optional salt 129 key = sha_constructor('signer' + self.key + salt).hexdigest() 130 return base64_hmac(value, key) 131 132 def sign(self, value, salt='', sep=':'): 133 value = smart_str(value) 134 return '%s%s%s' % ( 135 value, sep, self.signature(value, salt=salt) 136 ) 137 138 def unsign(self, signed_value, salt='', sep=':'): 139 signed_value = smart_str(signed_value) 140 if not sep in signed_value: 141 raise BadSignature, "No '%s' found in value" % sep 142 value, sig = signed_value.rsplit(sep, 1) 143 expected = self.signature(value, salt=salt) 144 if sig != expected: 145 # Important: do NOT include the expected sig in the exception 146 # message, since it might leak up to an attacker! Can we enforce 147 # this in the Django debug templates? 148 raise BadSignature, 'Signature "%s" does not match' % sig 149 else: 150 return force_unicode(value) 151 152 class TimestampSigner(Signer): 153 def timestamp(self): 154 return baseconv.base62.from_int(int(time.time())) 155 156 def sign(self, value, salt='', sep=':'): 157 value = smart_str('%s%s%s' % (value, sep, self.timestamp())) 158 return '%s%s%s' % ( 159 value, sep, self.signature(value, salt=salt) 160 ) 161 162 def unsign(self, value, salt='', sep=':', max_age=None): 163 value, timestamp = super(TimestampSigner, self).unsign( 164 value, salt=salt, sep=sep 165 ).rsplit(sep, 1) 166 timestamp = baseconv.base62.to_int(timestamp) 167 if max_age is not None: 168 # Check timestamp is not older than max_age 169 age = time.time() - timestamp 170 if age > max_age: 171 raise SignatureExpired, 'Signature age %s > %s seconds' % ( 172 age, max_age 173 ) 174 return value -
docs/index.txt
diff --git a/docs/index.txt b/docs/index.txt index cec1d76..41ec97e 100644
a b Other batteries included 163 163 * :ref:`Comments <ref-contrib-comments-index>` | :ref:`Moderation <ref-contrib-comments-moderation>` | :ref:`Custom comments <ref-contrib-comments-custom>` 164 164 * :ref:`Content types <ref-contrib-contenttypes>` 165 165 * :ref:`Cross Site Request Forgery protection <ref-contrib-csrf>` 166 * :ref:`Cryptographic signing <topics-cryptographic-signing>` 166 167 * :ref:`Databrowse <ref-contrib-databrowse>` 167 168 * :ref:`E-mail (sending) <topics-email>` 168 169 * :ref:`Flatpages <ref-contrib-flatpages>` -
docs/ref/request-response.txt
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 77d991b..28b4eaa 100644
a b Methods 221 221 222 222 Example: ``"http://example.com/music/bands/the_beatles/?print=true"`` 223 223 224 .. method:: HttpRequest.get_signed_cookie(key, default=raise_error, salt = '', max_age=None) 225 226 .. versionadded:: 1.2 227 228 Returns a cookie value for a signed cookie, or raises a 229 ``utils.BadSignature`` exception if the signature is no longer valid. If 230 you provide the ``default`` argument the exception will be suppressed and 231 that default value will be returned instead. 232 233 The optional salt argument can be used to provide extra protection against 234 brute force attacks on your secret key. If supplied, the max_age argument 235 will be checked against the signed timestamp attached to the cookie value 236 to ensure the cookie is no older than max_age seconds. 237 238 For example:: 239 240 >>> request.get_signed_cookie('name') 241 'Tony' 242 >>> request.get_signed_cookie('name', salt='name-salt') 243 'Tony' # assuming cookie was set using the same salt 244 >>> request.get_signed_cookie('non-existing-cookie') 245 ... 246 KeyError: 'non-existing-cookie' 247 >>> request.get_signed_cookie('non-existing-cookie', default=False) 248 False 249 >>> request.get_signed_cookie('cookie-that-was-tampered-with') 250 ... 251 BadSignature: ... 252 >>> request.get_signed_cookie('name', max_age=60) 253 ... 254 SignatureExpired: Signature age 1677.3839159 > 60 seconds 255 >>> request.get_signed_cookie('name', max_age=60, default=False) 256 False 257 258 See :ref:`cryptographic signing <topics-signing>` for more information. 259 224 260 .. method:: HttpRequest.is_secure() 225 261 226 262 Returns ``True`` if the request is secure; that is, if it was made with … … Methods 522 558 523 559 .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel 524 560 561 .. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None) 562 563 Like ``set_cookie()``, but :ref:`cryptographically signs <topics-signing>` 564 the cookie before setting it. Use in conjunction with 565 ``HttpRequest.get_signed_cookie``. You can use the optional ``salt`` 566 argument for added key strength, but you will need to remember to pass it 567 to the corresponding ``get_signed_cookie`` call. 568 525 569 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None) 526 570 527 571 Deletes the cookie with the given key. Fails silently if the key doesn't -
docs/topics/index.txt
diff --git a/docs/topics/index.txt b/docs/topics/index.txt index 7fa283a..b80c101 100644
a b Introductions to all the key parts of Django you'll need to know: 20 20 auth 21 21 cache 22 22 conditional-view-processing 23 signing 23 24 email 24 25 i18n 25 26 pagination -
new file docs/topics/signing.txt
diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt new file mode 100644 index 0000000..68d4a30
- + 1 .. _topics-signing: 2 3 ===================== 4 Cryptographic signing 5 ===================== 6 7 .. versionadded:: 1.2 8 9 The golden rule of web application security is to never trust data from 10 untrusted sources. Sometimes it can be useful to pass data through an 11 untrusted medium. Cryptographically signed values can be passed through an 12 untrusted channel safe in the knowledge that any tampering will be detected. 13 14 Django provides both a low-level API for signing values and a high-level API 15 for setting and reading signed cookies, one of the most common uses of 16 signing in web applications. 17 18 You may also find signing useful for the following: 19 20 * Generating "recover my account" URLs for sending to users who have 21 lost their password. 22 23 * Ensuring data stored in hidden form fields has not been tampered with. 24 25 * Generating one-time secret URLs for allowing temporary access to a 26 protected resource, for example a downloadable file that a user has 27 paid for. 28 29 Protecting the SECRET_KEY 30 ========================= 31 32 When you create a new Django project using ``django-admin.py startproject``, 33 the ``settings.py`` file it generates automatically gets a random 34 ``SECRET_KEY`` value. This value is the key to securing signed data - it is 35 vital you keep this secure, or attackers could use it to generate their own 36 signed values. 37 38 Using the low-level API 39 ======================= 40 41 Django's signing methods live in the ``django.utils.signed module``. To 42 sign a value, first instantiate a ``Signer`` instance:: 43 44 >>> from django.utils.signed import Signer 45 >>> signer = Signer() 46 >>> value = signer.sign('My string') 47 >>> value 48 'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w' 49 50 The signature is appended to the end of the string, following the colon. You 51 can retrieve the original value using the ``unsign`` method:: 52 53 >>> original = signer.unsign(value) 54 >>> original 55 u'My string' 56 57 If the signature or value have been altered in any way, a 58 ``signed.BadSigature`` exception will be raised:: 59 60 >>> value += 'm' 61 >>> try: 62 ... original = signer.unsign(value) 63 ... except signed.BadSignature: 64 ... print "Tampering detected!" 65 66 By default, the ``Signer`` class uses your project's ``SECRET_KEY`` setting to 67 generate signatures. You can use a different secret by passing it to the 68 ``Signer`` constructor:: 69 70 >>> signer = Signer('my-other-secret') 71 >>> value = signer.sign('My string') 72 >>> value 73 'My string:EkfQJafvGyiofrdGnuthdxImIJw' 74 75 Using the salt argument 76 ----------------------- 77 78 If you do not wish to use the same key for every signing operation in your 79 application, you can use the optional ``salt`` argument to the ``sign`` and 80 ``unsign`` methods to further strengthen your ``SECRET_KEY`` against brute 81 force attacks. Using a salt will cause a new key to be derived from both the 82 salt and your ``SECRET_KEY``:: 83 84 >>> signer = Signer() 85 >>> signer.sign('My string') 86 'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w' 87 >>> signer.sign('My string', salt='extra') 88 'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw' 89 >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw', salt='extra') 90 u'My string' 91 92 Unlike your ``SECRET_KEY``, your salt argument does not need to stay secret. 93 94 Verifying timestamped values 95 ---------------------------- 96 97 ``TimestampSigner`` is a subclass of ``Signer`` that appends a signed 98 timestamp to the value. This allows you to confirm that a signed value was 99 created within a specified period of time:: 100 101 >>> from django.utils.signed import TimestampSigner 102 >>> signer = TimestampSigner() 103 >>> value = signer.sign('hello') 104 >>> value 105 'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c' 106 >>> signer.unsign(value) 107 u'hello' 108 >>> signer.unsign(value, max_age=10) 109 ... 110 SignatureExpired: Signature age 15.5289158821 > 10 seconds 111 >>> signer.unsign(value, max_age=20) 112 u'hello' 113 114 Protecting complex data structures 115 ---------------------------------- 116 117 If you wish to protect a list, tuple or dictionary you can do so using the 118 signed module's dumps and loads functions. These imitate Python's pickle 119 module, but use JSON serialization under the hood. JSON ensures that even if 120 your ``SECRET_KEY`` is stolen an attacker will not be able to execute 121 arbitrary commands by exploiting the pickle format.:: 122 123 >>> from django.utils import signed 124 >>> value = signed.dumps({"foo": "bar"}) 125 >>> value 126 'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI' 127 >>> signed.loads(value) 128 {'foo': 'bar'} -
new file tests/regressiontests/signed_cookie_tests/models.py
diff --git a/tests/regressiontests/signed_cookie_tests/__init__.py b/tests/regressiontests/signed_cookie_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/regressiontests/signed_cookie_tests/models.py b/tests/regressiontests/signed_cookie_tests/models.py new file mode 100644 index 0000000..71abcc5
- + 1 # models.py file for tests to run. -
new file tests/regressiontests/signed_cookie_tests/tests.py
diff --git a/tests/regressiontests/signed_cookie_tests/tests.py b/tests/regressiontests/signed_cookie_tests/tests.py new file mode 100644 index 0000000..cccc222
- + 1 from django.test import TestCase 2 from django.http import HttpRequest, HttpResponse 3 from django.utils import signed 4 import time 5 6 class SignedCookieTest(TestCase): 7 8 def test_can_set_and_read_signed_cookies(self): 9 response = HttpResponse() 10 response.set_signed_cookie('c', 'hello') 11 self.assert_('c' in response.cookies) 12 self.assert_(response.cookies['c'].value.startswith('hello:')) 13 request = HttpRequest() 14 request.COOKIES['c'] = response.cookies['c'].value 15 value = request.get_signed_cookie('c') 16 self.assertEqual(value, u'hello') 17 18 def test_can_use_salt(self): 19 response = HttpResponse() 20 response.set_signed_cookie('a', 'hello', salt='one') 21 request = HttpRequest() 22 request.COOKIES['a'] = response.cookies['a'].value 23 value = request.get_signed_cookie('a', salt='one') 24 self.assertEqual(value, u'hello') 25 self.assertRaises(signed.BadSignature, 26 request.get_signed_cookie, 'a', salt='two' 27 ) 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(signed.BadSignature, 35 request.get_signed_cookie, 'c' 36 ) 37 38 def test_default_argument_supresses_exceptions(self): 39 response = HttpResponse() 40 response.set_signed_cookie('c', 'hello') 41 request = HttpRequest() 42 request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$' 43 self.assertEqual(request.get_signed_cookie('c', default=None), None) 44 45 def test_max_age_argument(self): 46 old_time = time.time 47 time.time = lambda: 123456789 48 v = u'hello' 49 try: 50 response = HttpResponse() 51 response.set_signed_cookie('c', v) 52 request = HttpRequest() 53 request.COOKIES['c'] = response.cookies['c'].value 54 55 self.assertEqual(request.get_signed_cookie('c'), v) 56 57 time.time = lambda: 123456800 58 59 self.assertEqual(request.get_signed_cookie('c', max_age=12), v) 60 self.assertEqual(request.get_signed_cookie('c', max_age=11), v) 61 self.assertRaises( 62 signed.SignatureExpired, request.get_signed_cookie, 'c', 63 max_age = 10 64 ) 65 finally: 66 time.time = old_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..d149937
- + 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 -
new file tests/regressiontests/utils/signed.py
diff --git a/tests/regressiontests/utils/signed.py b/tests/regressiontests/utils/signed.py new file mode 100644 index 0000000..3720d2b
- + 1 from unittest import TestCase 2 from django.utils import signed 3 from django.utils.hashcompat import sha_constructor 4 from django.utils.encoding import force_unicode 5 import time 6 7 class TestSigner(TestCase): 8 9 def test_signature(self): 10 "signature() method should generate a signature" 11 signer = signed.Signer('predictable-secret') 12 signer2 = signed.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 signed.base64_hmac(s, sha_constructor( 21 'signer' + 'predictable-secret' 22 ).hexdigest()) 23 ) 24 self.assertNotEqual(signer.signature(s), signer2.signature(s)) 25 26 def test_signature_with_salt(self): 27 "signature(value, salt=...) should work" 28 signer = signed.Signer('predictable-secret') 29 self.assertEqual( 30 signer.signature('hello', salt='extra-salt'), 31 signed.base64_hmac('hello', sha_constructor( 32 'signer' + 'predictable-secret' + 'extra-salt' 33 ).hexdigest()) 34 ) 35 self.assertNotEqual( 36 signer.signature('hello', salt='one'), 37 signer.signature('hello', salt='two'), 38 ) 39 40 def test_sign_unsign(self): 41 "sign/unsign should be reversible" 42 signer = signed.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.assert_( 52 force_unicode(example) != force_unicode(signer.sign(example)) 53 ) 54 self.assertEqual(example, signer.unsign(signer.sign(example))) 55 56 def unsign_detects_tampering(self): 57 "unsign should raise an exception if the value has been tampered with" 58 signer = signed.Signer('predictable-secret') 59 value = 'Another string' 60 signed_value = signer.sign(value) 61 transforms = ( 62 lambda s: s.upper(), 63 lambda s: s + 'a', 64 lambda s: 'a' + s[1:], 65 lambda s: s.replace(':', ''), 66 ) 67 self.assertEqual(value, signer.unsign(signed_value)) 68 for transform in transforms: 69 self.assertRaises( 70 signed.BadSignature, signer.unsign, transform(signed_value) 71 ) 72 73 def test_dumps_loads(self): 74 "dumps and loads be reversible for any JSON serializable object" 75 objects = ( 76 ['a', 'list'], 77 'a string', 78 u'a unicode string \u2019', 79 {'a': 'dictionary'}, 80 ) 81 for o in objects: 82 self.assert_(o != signed.dumps(o)) 83 self.assertEqual(o, signed.loads(signed.dumps(o))) 84 85 def test_decode_detects_tampering(self): 86 "loads should raise exception for tampered objects" 87 transforms = ( 88 lambda s: s.upper(), 89 lambda s: s + 'a', 90 lambda s: 'a' + s[1:], 91 lambda s: s.replace(':', ''), 92 ) 93 value = {'foo': 'bar', 'baz': 1} 94 encoded = signed.dumps(value) 95 self.assertEqual(value, signed.loads(encoded)) 96 for transform in transforms: 97 self.assertRaises( 98 signed.BadSignature, signed.loads, transform(encoded) 99 ) 100 101 class TestTimestampSigner(TestCase): 102 103 def test_timestamp_signer(self): 104 old_time = time.time 105 time.time = lambda: 123456789 106 try: 107 signer = signed.TimestampSigner('predictable-key') 108 v = u'hello' 109 ts = signer.sign(v) 110 self.assertNotEqual(ts,signed.Signer('predictable-key').sign(v)) 111 112 self.assertEqual(signer.unsign(ts), v) 113 114 time.time = lambda: 123456800 115 116 self.assertEqual(signer.unsign(ts, max_age=12), v) 117 self.assertEqual(signer.unsign(ts, max_age=11), v) 118 self.assertRaises( 119 signed.SignatureExpired, signer.unsign, ts, max_age=10 120 ) 121 finally: 122 time.time = old_time -
tests/regressiontests/utils/tests.py
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 6258b81..6c6dee0 100644
a b import timesince 11 11 import datastructures 12 12 import dateformat 13 13 import itercompat 14 from baseconv import TestBaseConv 15 from signed import TestSigner, TestTimestampSigner 14 16 from decorators import DecoratorFromMiddlewareTests 15 17 16 18 # We need this because "datastructures" uses sorted() and the tests are run in