* 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.

12

13

Potential uses for signing:

14

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

21

22

== Signed cookie API ==

23

24

This is under heavy discussion on the mailing list. Current proposals for setting a signed cookie:

25

26

{{{

27

response.set_cookie(key, value, signed=True)

28

}}}

29

Or

30

{{{

31

response.set_signed_cookie(key, value)

32

}}}

33

34

(Blanket signing everything is probably not an option as some cookies, such as those used by Google Analytics, need to remain unsigned)

35

36

Reading 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.

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.

64

65

{{{

66

>>> signed.unsign('hello.9asVJn9dfv6qLJ_BYObzF7mmH8c')

67

'hello'

68

>>> signed.unsign('hello.badsignature')

69

Traceback (most recent call last):

70

...

71

BadSignature: Signature failed: badsignature

72

}}}

73

74

!BadSignature would probably be a subclass of django.core.exceptions.!SuspiciousOperation

75

76

{{{

77

>>> signed.dumps({"a": "foo"})

78

'KGRwMApTJ2EnCnAxClMnZm9vJwpwMgpzLg.mYepoYkzWwXRmsCTVJm3Mb0HHz4'

79

>>> signed.loads(_)

80

{'a': 'foo'}

81

}}}

82

83

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.

84

85

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:

86

87

{{{

88

>>> import this # to get an object worth compressing

89

...

90

>>> len(signed.dumps(this.s))

91

1207

92

>>> len(signed.dumps(this.s, compress=True))

93

637

94

}}}

95

96

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:

97

98

{{{

99

>>> signed.sign('hello', key='sekrit')

100

'hello.o6MKehoOfZ2b2FU84wzibW6IWxI'

101

>>> signed.unsign(_, key='sekrit')

102

'hello'

103

}}}

104

105

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 within your application (useful for the extra paranoid):

106

107

{{{

108

>>> signed.dumps('hello', key='sekrit', extra_key='ultra')

109

'UydoZWxsbycKcDAKLg.1XYDpILo5xqSwImfa3WuJJT4RPo'

110

>>> signed.loads(_, key='sekrit', extra_key='ultra')

111

'hello'

112

}}}

113

114

=== Potential additions to the above API ===

115

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.

118

119

== SECRET_KEY considerations ==

120

121

Simon says:

122

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.

124

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.

126

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: