+1 for signed cookies. Your API looks reasonable and I'd agree that set_cookie(..., signed=True) fits better with the rest of the API as well. What about some sanity checking to make sure that, if SECRET_KEY is used, it is, at the very least, a non-empty string?
On Thu, Sep 24, 2009 at 1:18 PM, Simon Willison <[email protected]>wrote: > > As mentioned in the thread about cookie-based notifications, at the > DjangoCon Sprints I raised the subject of adding signing (and signed > cookies) to Django core. > > I've found myself using signing more and more over time, and I think > it's a concept which is common enough to deserve inclusion in Django - > if anything, its use should be actively encouraged by the framework. > > It's also something that's hard to do correctly. At the sprints Armin > pointed out that I should be using hmac, not straight sha1, for > generating signatures (something Django itself gets wrong in the few > places that implement signing already). Having a cryptographer- > approved implementation will save a lot of people from making the same > mistakes. > > Signed cookies > ============== > > On top of signing (which I imagine would live in django.utils) I'd > like to add a signed cookie implementation. Signed cookies are useful > for all sorts of things - most importantly, they can be used in place > of sessions in many places, which improves performance (and overall > scalability) by removing the need to access a persistent session > backend on every hit. Set the user's username in a signed cookie and > you can display "Logged in as X" messages on every page without any > persistence layer calls at all. > > I think signed cookies should either be a separate API from > response.set_cookie or should be provided as an additional argument to > that method. I'm not a fan of signing using middleware (as seen in > http://code.google.com/p/django-signedcookies/ ) since that approach > signs everything - some cookies, such as those used by Google > Analytics, need to remain unsigned. > > So the API could either be: > > response.set_signed_cookie(key, value) > > Or... > > response.set_cookie(key, value, signed=True) > > (I prefer the latter option) > > Proposed signing implementation > =============================== > > I'd be happy to donate my signing code from django-openid to the > cause, which was written to be usable entirely separately from the > rest of the django-openid codebase: > > http://github.com/simonw/django-openid/blob/master/django_openid/signed.py > > http://github.com/simonw/django-openid/blob/master/django_openid/tests/signing_tests.py > > This offers two APIs: sign/unsign and dumps/loads. sign and unsign > generate and append signatures to bytestrings and confirm that they > have not been tampered with. dumps and loads can be used to create > signed pickles of arbitrary Python objects. > > Here's what the API would look like with this library: > > >>> from django.utils import signed > >>> signed.sign('hello') > 'hello.9asVJn9dfv6qLJ_BYObzF7mmH8c' > > The signature is a URL-safe base64 encoded digest of the hmac/sha1. I > used base64 rather than .hexdigest() for space reasons - base64 > digests are 27 characters, hexadecimal digests are 40. When you're > including signatures in cookies and URLs (especially account recovery > URLs sent out in plain text, 80 character wide e-mails) every byte > counts. > > >>> signed.unsign('hello.9asVJn9dfv6qLJ_BYObzF7mmH8c') > 'hello' > >>> signed.unsign('hello.badsignature') > Traceback (most recent call last): > ... > BadSignature: Signature failed: badsignature > > BadSignature is a subclass of ValueError, meaning lazy developers > (like myself) can do the following rather than importing the exception > itself: > > try: > value = signed.unsign(signed_value) > except ValueError: > return tamper_error_view(request) > > >>> signed.dumps({"a": "foo"}) > 'KGRwMApTJ2EnCnAxClMnZm9vJwpwMgpzLg.mYepoYkzWwXRmsCTVJm3Mb0HHz4' > >>> signed.loads(_) > {'a': 'foo'} > > Again, the pickle is URL-safe base64 encoded to take up less valuable > cookie space and generally make it easier to pass around on the Web. A > nice thing about URL-safe base64 is that it uses 64 out of the 65 URL- > safe characters (by URL-safe I mean characters that are left unchanged > by Python's urllib.urlencode function) - the remaining character is > the period, which I use to separate the pickle from the signature. > > signed.dumps takes a couple of extra optional arguments. The first is > compress=True (default is False) which zlib compresses the pickle if > doing so will save any space: > > >>> import this # to get an object worth compressing > ... > >>> len(signed.dumps(this.s)) > 1207 > >>> len(signed.dumps(this.s, compress=True)) > 637 > > By default, all signatures use Django's SECRET_KEY. If you want to > sign with a different key, you can pass it as an argument to the > various functions: > > >>> signed.sign('hello', key='sekrit') > 'hello.o6MKehoOfZ2b2FU84wzibW6IWxI' > >>> signed.unsign(_, key='sekrit') > 'hello' > > The dumps and loads methods also take a key argument, as well as an > additional optional extra_key argument for if you want to generate > different signatures for different parts of your application (useful > for the extra paranoid): > > >>> signed.dumps('hello', extra_key='ultra') > 'UydoZWxsbycKcDAKLg.1XYDpILo5xqSwImfa3WuJJT4RPo' > >>> signed.loads(_, extra_key='ultra') > 'hello' > > We'd want to get a proper cryptographer to give this the once-over > before adding it to core, but I'm generally happy with the API. It > could be argued that it's over kill and just sticking signed.sign and > signed.unsign in would be enough, but I'm pretty keen on the > convenience of dumps and loads. > > Thinking about it further, an additional API that just gives you the > signature without including the original value would mean it could be > used for hashing passwords as well. > > Potential uses > ============== > > Lots of stuff: > > - Signed cookies (obviously) > - Generating CSRF tokens > - Secure /logout/ and /change-language/ links > - Securing /login/?next=/some/path/ > - Securing hidden fields in form wizards > - Recover-your-account links in e-mails > > We already use signing in a few places in Django core (mainly sessions > and form wizards), currently using md5 without hmac - sha1/hmac would > be an instant improvement. > > SECRET_KEY considerations > ========================= > > One thing that worries me slightly about increasing the amount of > signing going on in Django is that it elevates the importance of the > SECRET_KEY. I'm currently ignorant of best practices regarding > protecting this kind of shared secret, but the steps we take (***ing > it out from the debug pages and otherwise ignoring it) could almost > certainly be improved. > > One thing that's particularly interesting to me is what happens when > you change your secret. If you're changing your secret because it's > leaked then obviously you want stuff signed with the old secret to > become invalid immediately, but I can imagine some users wanting to > rotate their secret keys on a continual basis for added security > against brute force attacks. > > If you're rotating your secret, invalidating all of your users signed > cookies etc is a bit of an annoyance. It might be worth supporting two > secrets - the current SECRET_KEY and an optional OLD_SECRET_KEY - with > unsigning operations falling back on the old key if the current key > fails. This would allow users to deploy a new secret while keeping the > old one valid for a week or so, upgrading any tokens that use the old > key in the process. This suggestion is inspired by Amazon's recent > announcement of a similar feature for handling web service access > credentials: > > http://aws.typepad.com/aws/2009/09/aws-access-credential-rotation.html > > This is probably all too much complication, but it's something that's > been nagging at me since I started increasing my dependence on the > SECRET_KEY setting. > > So... what do people think? Is this a feature suitable for Django > (obviously I think so)? Is this as simple as getting a cryptographer's > input and dropping signed.py in to django.utils or are there other > design factors we should consider? > > Cheers, > > Simon > > > --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Django developers" group. To post to this group, send email to [email protected] To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/django-developers?hl=en -~----------~----~----~----~------~----~------~--~---
