On Fri, Sep 25, 2009 at 9:26 AM, Simon Willison <si...@simonwillison.net> wrote: > The API would look something like this:
>>>> s = signed.sign('value', timestamp=True) >>>> v = signed.unsign(s, expire_after=24 * 60 * 60)
> A SignatureExpired exception would be raised if the signature was > older than the expire_after argument (SignatureExpired would subclass > BadSignature)
Very true. I had considered baking the timestamp right into the value portion as well, but I was concerned about the extra space it would take. It looks like it would only be 8 characters max, if encoded as base64, which could be shortened to 6 if we strip out the == at the end. I think I remember reading somewhere that those can be reattached programmatically on the other end if necessary. All in all, the space usage isn't that bad, and since it would be an optional component anyway, it wouldn't add any overhead to the common case.
Would expire_after on the unsign just automatically imply timestamp=True? There's been a lot of concern raised about parity in the API, and it reads a little weird with the two different arguments. I'm not sure it's a problem, but it's just a little funny.
Even though Ben rightly pointed out that we can't autodetect whether a value is signed or not, I wonder if we could at least autodetect the presence of a timestamp within a signature, once we already know that the value is supposed to be signed. Essentially the unsigning code could look for two different types of signatures, one with a timestamp and one without. The timestamp would then be the actual expiration time, rather than the time it was signed, so the API can look like this instead (with a key added per prior discussion).
>>> s = signed.sign('key', 'value') >>> v = signed.unsign('key', s) >>> s = signed.sign('key', 'value', expire_after=24 * 60 * 60) >>> v = signed.unsign('key', s)
This does make some assumptions about the format of the signed value, but once we explicitly establish that the value is signed (by way of passing it into the unsigning code), it's safe to make certain assumptions about the format of the value. Or am I missing something obvious here?
In the cookie case, it might be appropriate to use the combination of an explicit expiration and signed=True to imply that an expiration timestamp should be added to the cookie as well. I think that would be the desired behavior, but I'm not quite sure.
Aside from some of the specifics of the cookie implementation, I think we're getting close to an API here. I just hope we can get some input from a cryptographer to make sure we get a solid implementation before we go too far with this.
On Sep 25, 3:10 pm, Marty Alchin <gulop...@gmail.com> wrote:
> The timestamp would then be the actual expiration
> time, rather than the time it was signed, so the API can look like
> this instead (with a key added per prior discussion).
> >>> s = signed.sign('key', 'value')
> >>> v = signed.unsign('key', s)
> >>> s = signed.sign('key', 'value', expire_after=24 * 60 * 60)
> >>> v = signed.unsign('key', s)
Baking an expiration time in to a signed value and checking for it
when we unsign is definitely possible, but I'm a bit torn between the
two options. I prefer setting the expiry time when I read the cookie
rather than when I set it, but I can't say exactly why. I think it's
based on bad experiences with memcached - I've frequently found a need
to set things in the cache for ever and decide whether or not to treat
them as stale on retrieval (for implementing things like serve-stale-
on-error) which has made my instinct be to bake the time-of-creation
in to the thing and leave the is-this-stale decision until as late as
possible.
While that makes sense for caching, I couldn't say if it makes sense
for signatures or not - when we sign something, will we always know
the point at which we want that signature to expire? I don't know.
On Sep 25, 3:39 pm, Simon Willison <si...@simonwillison.net> wrote:
> While that makes sense for caching, I couldn't say if it makes sense
> for signatures or not - when we sign something, will we always know
> the point at which we want that signature to expire? I don't know.
Here's a good argument for signing things with the creation-timestamp
rather than the expiration-timestamp - it leaves the door open for a
mechanism whereby historic SECRET_KEYs are stored. When we see a
signed string, we can use its timestamp to decide which of our
historic keys should be used to validate it.
BIt of an edge case (I can't say if we'd ever want to do this) but
it's an example of something that's not possible with expire-at
timestamps.
<si...@simonwillison.net> wrote: > On Sep 25, 3:39 pm, Simon Willison <si...@simonwillison.net> wrote: >> While that makes sense for caching, I couldn't say if it makes sense >> for signatures or not - when we sign something, will we always know >> the point at which we want that signature to expire? I don't know.
> Here's a good argument for signing things with the creation-timestamp > rather than the expiration-timestamp - it leaves the door open for a > mechanism whereby historic SECRET_KEYs are stored. When we see a > signed string, we can use its timestamp to decide which of our > historic keys should be used to validate it.
But that only works for signatures that do in fact use a timestamp. If the API makes timestamps optional, then there's still the question of what to do for signatures that aren't stamped anyawy. In those cases, I assumed the protocol would be to simply try to most recent SECRET_KEY and work backward if the signature failed. Once all current and deprecated keys have been tried (which should only be a total of two, I would think), it would raise BadSignature at that point.
If we already have that behavior for non-timestamped cookies (and correct me if I'm wrong on that point), I don't know that it's much of an argument in favor of which timestamp to use. As long as there's not a huge list of valid SECRET_KEYs to choose from, the overhead of trying them each individually should be negligible, so I'm not sure how much of an issue it would be.
I do agree though that you do have a point about there being possible reasons for expiring the key at a different point after the fact, but I'd argue that in those cases, you'd want to set an explicit expiration time. In your example, you provided expire_after=24 * 60 * 60, but that wouldn't let you expire a cookie because of an event that happened 2 hours ago. I would think you'd want to pass in a specific time that cookies should be considered expired instead.
Of course, that then goes back to stamping cookies with their creation time anyway, because otherwise you couldn't really do it right. If I say "expire as of time X" I want to expire all cookies issued prior to that point in time, while leaving any issued after that point intact. The only way to make that decision is to know the time it was issued, rather than when it was originally expected to expire.
I think we're getting a bit ahead of ourselves, though. There's nothing stopping an application timestamping its own signatures and validating them however they like, so we're really just discussing a reasonable default here. Heck, since the timestamp behavior is driven entirely within an already-signed value, we don't need to provide any behavior at all if it's not a common requirement. I think it's a good idea, but I'd like to hear from other people before I really stand behind including it.
> Would expire_after on the unsign just automatically imply > timestamp=True? There's been a lot of concern raised about parity in > the API, and it reads a little weird with the two different arguments. > I'm not sure it's a problem, but it's just a little funny.
Regarding parity, let me advertise a Signer object again:
# transparently sign cookies set via response.set_cookie() # and unsign cookies in request.COOKIES in place: @signer.sign_cookies def view0(request): ... @signer.sign_cookies('cookie', 'names') def view1(request): ...
This would make more options and customization feasible (by subclassing): - put signatures in separate cookies (or a single separate cookie) - automatically include request.user.id (to prevent users from sharing cookies) - swap out the hash, serialization, or compression algorithm without changing the token format - customize when and how expiration is determined
On Thu, Sep 24, 2009 at 5:33 PM, Chris Beaven <smileych...@gmail.com> wrote:
> Personally, I don't see much point in specifically reporting on > incorrectly signed cookies - imo they should just be treated as if > they never existed. If someone really cared, they can look in > request.COOKIES to see if the cookie was in there but not in > SIGNED_COOKIES.
I suppose. IMHO silent failures are usually a bad thing. I generally like to know if (a) there's an error on my site, or (b) someone is trying to do something nasty, even if all that means is:
try: ... except BadSignature: pass # or a log.debug(...)
> # transparently sign cookies set via response.set_cookie()
> # and unsign cookies in request.COOKIES in place:
> @signer.sign_cookies
> def view0(request):
> ...
> @signer.sign_cookies('cookie', 'names')
> def view1(request):
> ...
> This would make more options and customization feasible (by
> subclassing):
OK, you got me. You obviously know my weakness for customisation by
subclassing :) Having a Signer class is a smart idea, I'll kick it
around a bit and see how it looks.
On Sep 24, 10:18 am, Simon Willison <si...@simonwillison.net> wrote:
> 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.
Unpickling data sent by the client seems dangerous, since it can
execute arbitrary code on the server [1]. Obviously signing the data
goes a long way toward preventing such attacks, but I'm still not
comfortable with the idea that a leaked signing key could instantly be
escalated to arbitrary code execution. (For example, if the config
files are exposed through a misconfigured web server or accidentally
checked into public source control.) If you use JSON or some other
object serialization by default, then the damage from a leaked secret
key would be much more limited in most cases.
On Oct 5, 6:33 pm, Matt Brubeck <mbrub...@limpet.net> wrote:
> On Sep 24, 10:18 am, Simon Willison <si...@simonwillison.net> wrote:
> > 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.
> Unpickling data sent by the client seems dangerous, since it can
> execute arbitrary code on the server [1]. Obviously signing the data
> goes a long way toward preventing such attacks, but I'm still not
> comfortable with the idea that a leaked signing key could instantly be
> escalated to arbitrary code execution. (For example, if the config
> files are exposed through a misconfigured web server or accidentally
> checked into public source control.) If you use JSON or some other
> object serialization by default, then the damage from a leaked secret
> key would be much more limited in most cases.
You know what, I have to admit I hadn't really thought about JSON as
an alternative. Obviously I knew that unpickling was unsafe - that's
one of the reasons I'm so keen on signing - but it really doesn't make
sense to expose this kind of risk if it's not necessary. The pickling
trick is cute, but is the convenience of being able to pass any pickle-
able object around really worth the risk? I don't think it is - I
think I'll ditch the dumps/loads signed pickle methods in favour for
similar functionality that uses JSON instead. Other than dates being a
bit more annoying to pass around, I really don't think that telling
people they can only dumps/loads JSON-encodable data would be a huge
burden.
On Oct 5, 1:44 pm, Simon Willison <si...@simonwillison.net> wrote:
> Other than dates being a bit more annoying to pass around, I really
> don't think that telling people they can only dumps/loads JSON-
> encodable data would be a huge burden.
You could use YAML instead if you want date support... although JSON
does seem to have broader mind-share now.
> 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.
So, there seem to be several alternatives.
From the pastiche.org article's premise 2, it seems that a signed
cookie containing a user identifier
is sufficient for cookie-based login.
Very easy to implement with your signed cookie implementation.
On the other hand, cookie-based login can be achieved with regular
cookies and
some server state.
The later solution also allows identification of stolen cookies when
the real user logs in
and a mutating cookie which reduces a stolen cookie's lifetime.
But this requires implementing their specs and lots of testing.
>> # transparently sign cookies set via response.set_cookie() >> # and unsign cookies in request.COOKIES in place: >> @signer.sign_cookies >> def view0(request): >> ... >> @signer.sign_cookies('cookie', 'names') >> def view1(request): >> ...
>> This would make more options and customization feasible (by >> subclassing):
> OK, you got me. You obviously know my weakness for customisation by > subclassing :) Having a Signer class is a smart idea, I'll kick it > around a bit and see how it looks.