Changes between Version 1 and Version 2 of Signing

09/25/2009 03:09:39 AM (6 years ago)

Notes from our initial discussions


  • Signing

    v1 v2  
    1 == Signing and Signed Cookies ==
     1= Signing and Signed Cookies =
     3Proposal for 1.2: Django core should include low-level code for signing and verifying signatures on arbitrary bytestrings. This should be used to support a new higher level signed cookie feature.
    35Under discussion here
     7== Justification ==
     9 * Signing is a widely used web application security technique. Django uses it in a few places already (the form wizard and sessions contrib apps), and it is useful any time an application might want to pass data through an untrusted channel and ensure it hasn't been tampered on the other side. The Web is generally an untrusted channel.
     10 * Signed cookies can replace sessions in many use cases, with the significant bonus that unlike sessions they do not require a round-trip to a persistent store.
     11 * Signing is hard to do right, and most hand-rolled implementations make similar mistakes. Signing is best handled with hmac and sha1, but Django implementations currently tend to use the weaker MD5 without hmac. A signing implementation in Django core could be audited by expert cryptographers, providing a secure base on which other Django applications could build.
     13Potential uses for signing:
     15 * Signed cookies
     16 * Generating CSRF tokens
     17 * Secure /logout/ and /change-language/ links
     18 * Securing /login/?next=/some/path/
     19 * Securing hidden fields in form wizards
     20 * Recover-your-account links in e-mails
     22== Signed cookie API ==
     24This is under heavy discussion on the mailing list. Current proposals for setting a signed cookie:
     27response.set_cookie(key, value, signed=True)
     31response.set_signed_cookie(key, value)
     34(Blanket signing everything is probably not an option as some cookies, such as those used by Google Analytics, need to remain unsigned)
     36Reading a signed cookie is harder. Since cookies may be both signed and unsigned, it's not going to be possible to transparently verify signed cookies and return them using the same API as unsigned cookies. Benjamin Slavin pointed out that an attacker could then set bad data in an unsigned cookie and the auto-unsigning code would assume it was never signed in the first place. This means we need an explicit API for reading cookies that should have been signed.
     38Current suggestions include:
     41request.unsign_cookie(key) # reflects the lower-level API, but unintuitive
     42request.get_signed_cookie(key) # quite verbose
     43request.COOKIES.get_signed(key) # similar to request.POST.get_list
     47== Proposed signing API ==
     49Simon Willison has offered to donate the signing code from his django-openid library for the low level signing API:
     55The API would look like this:
     58>>> from django.utils import signed
     59>>> signed.sign('hello')
     63The 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.
     66>>> signed.unsign('hello.9asVJn9dfv6qLJ_BYObzF7mmH8c')
     68>>> signed.unsign('hello.badsignature')
     69Traceback (most recent call last):
     71BadSignature: Signature failed: badsignature
     74!BadSignature would probably be a subclass of django.core.exceptions.!SuspiciousOperation
     77>>> signed.dumps({"a": "foo"})
     79>>> signed.loads(_)
     80{'a': 'foo'}
     83Again, 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.
     85signed.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:
     88>>> import this # to get an object worth compressing
     90>>> len(signed.dumps(this.s))
     92>>> len(signed.dumps(this.s, compress=True))
     96By 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:
     99>>> signed.sign('hello', key='sekrit')
     101>>> signed.unsign(_, key='sekrit')
     105The 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 within your application (useful for the extra paranoid):
     108>>> signed.dumps('hello', key='sekrit', extra_key='ultra')
     110>>> signed.loads(_, key='sekrit', extra_key='ultra')
     114=== Potential additions to the above API ===
     116 * Low level access to just the signature generation routine itself (at the moment sign() and unsign() append the signature to the string themselves)
     117 * Maybe functions or parameters for creating timestamped signatures and failing the signature if it is older than a certain expiry timeout.
     119== SECRET_KEY considerations ==
     121Simon says:
     123> 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.
     125> 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.
     127> 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. Amazon recently announced a similar feature for handling web service credentials, which inspired this suggestion:
     131> 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.
Back to Top