Account Options

  1. Sign in
The old Google Groups will be going away soon.
Switch to the new Google Groups.
Google Groups Home
« Groups Home
Message from discussion Adding signing (and signed cookies) to Django core
The group you are posting to is a Usenet group. Messages posted to this group will make your email address visible to anyone on the Internet.
Your reply message has not been sent.
Your post was successful
 
From:
To:
Cc:
Followup To:
Add Cc | Add Followup-to | Edit Subject
Subject:
Validation:
For verification purposes please type the characters you see in the picture below or the numbers you hear by clicking the accessibility icon. Listen and type the numbers you hear
 
Simon Willison  
View profile  
 More options Sep 24 2009, 1:18 pm
From: Simon Willison <si...@simonwillison.net>
Date: Thu, 24 Sep 2009 10:18:56 -0700 (PDT)
Local: Thurs, Sep 24 2009 1:18 pm
Subject: Adding signing (and signed cookies) to Django core
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/sign...
http://github.com/simonw/django-openid/blob/master/django_openid/test...

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 must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.