General Concepts¶
Serializer vs Signer¶
ItsDangerous provides two levels of data handling. The Signing Interface is
the basic system that signs a given bytes
value based on the given
signing parameters. The Serialization Interface wraps a signer to enable
serializing and signing other data besides bytes
.
Typically, you’ll want to use a serializer, not a signer. You can configure the signing parameters through the serializer, and even provide fallback signers to upgrade old tokens to new parameters.
The Secret Key¶
Signatures are secured by the secret_key
. Typically one secret key
is used with all signers, and the salt is used to distinguish different
contexts. Changing the secret key will invalidate existing tokens.
It should be a long random string of bytes. This value must be kept secret and should not be saved in source code or committed to version control. If an attacker learns the secret key, they can change and resign data to look valid. If you suspect this happened, change the secret key to invalidate existing tokens.
One way to keep the secret key separate is to read it from an environment variable. When deploying for the first time, generate a key and set the environment variable when running the application. All process managers (like systemd) and hosting services have a way to specify environment variables.
import os
from itsdangerous.serializer import Serializer
SECRET_KEY = os.environ.get("SECRET_KEY")
s = Serializer(SECRET_KEY)
$ export SECRET_KEY="base64 encoded random bytes"
$ python application.py
One way to generate a key is to use os.urandom()
.
$ python3 -c 'import os; print(os.urandom(16).hex())'
The Salt¶
The salt is combined with the secret key to derive a unique key for distinguishing different contexts. Unlike the secret key, the salt doesn’t have to be random, and can be saved in code. It only has to be unique between contexts, not private.
For example, you want to email activation links to activate user accounts, and upgrade links to upgrade users to a paid accounts. If all you sign is the user id, and you don’t use different salts, a user could reuse the token from the activation link to upgrade the account. If you use different salts, the signatures will be different and will not be valid in the other context.
from itsdangerous.url_safe import URLSafeSerializer
s1 = URLSafeSerializer("secret-key", salt="activate")
s1.dumps(42)
'NDI.MHQqszw6Wc81wOBQszCrEE_RlzY'
s2 = URLSafeSerializer("secret-key", salt="upgrade")
s2.dumps(42)
'NDI.c0MpsD6gzpilOAeUPra3NShPXsE'
The second serializer can’t load data dumped with the first because the salts differ.
s2.loads(s1.dumps(42))
Traceback (most recent call last):
...
BadSignature: Signature does not match
Only the serializer with the same salt can load the data.
s2.loads(s2.dumps(42))
42
Key Rotation¶
Key rotation can provide an extra layer of mitigation against an attacker discovering a secret key. A rotation system will keep a list of valid keys, generating a new key and removing the oldest key periodically. If it takes four weeks for an attacker to crack a key, but the key is rotated out after three weeks, they will not be able to use any keys they crack. However, if a user doesn’t refresh their token within three weeks it will be invalid too.
The system that generates and maintains this list is outside the scope of ItsDangerous, but ItsDangerous does support validating against a list of keys.
Instead of passing a single key, you can pass a list of keys, oldest to newest. When signing the last (newest) key will be used, and when validating each key will be tried from newest to oldest before raising a validation error.
SECRET_KEYS = ["2b9cd98e", "169d7886", "b6af09f5"]
# sign some data with the latest key
s = Serializer(SECRET_KEYS)
t = s.dumps({"id": 42})
# rotate a new key in and the oldest key out
SECRET_KEYS.append("cf9b3588")
del SECRET_KEYS[0]
s = Serializer(SECRET_KEYS)
s.loads(t) # valid even though it was signed with a previous key
Digest Method Security¶
A signer is configured with a digest_method
, a hash function that
is used as an intermediate step when generating the HMAC signature. The
default method is hashlib.sha1()
. Occasionally, users are
concerned about this default because they have heard about hash
collisions with SHA-1.
When used as the intermediate, iterated step in HMAC, SHA-1 is not insecure. In fact, even MD5 is still secure in HMAC. The security of the hash alone doesn’t apply when used in HMAC.
If a project considers SHA-1 a risk anyway, they can configure the
signer with a different digest method such as hashlib.sha512()
.
A fallback signer for SHA-1 can be configured so that old tokens will be
upgraded. SHA-512 produces a longer hash, so tokens will take up more
space, which is relevant in cookies and URLs.