Tokens and HMAC · Cyber Security Basics
How does a server know a cookie wasn't changed by the user?
Course contentsWhy you need it
After you log in, the server gives your browser a token, usually in a cookie, and trusts it on every request. But the browser belongs to the user, who can edit anything stored there. Signed tokens, unpredictable session ids and careful comparison are what stop someone from turning role=user into role=admin.
The intuition
Think of a wax seal on a letter. Anyone can read the letter, but only the sender owns the seal, and a broken or fake seal is obvious.
An HMAC is that seal for data: hmac.new(SECRET, data, sha256) mixes a secret key only the server knows into a hash of the data. The server sends data|tag. When the token comes back, it computes the tag again; if the user changed even one character, the tags won't match, and without the key they can't make a new valid tag.
Two more details:
- Compare tags with
hmac.compare_digest, which takes the same time whether the first or the last character differs. A normal==stops at the first difference, and that tiny timing difference can leak the tag bit by bit. - Session ids must be unpredictable: use
secrets.token_hex(), notrandom, which is designed to be fast and repeatable, not secret.
Definition
HMAC
A hash of the data mixed with a secret key. Anyone can check it only if they know the key, so it proves the data came from the key owner and wasn't changed.
Timing attack
Learning a secret by measuring how long a program takes, for example a comparison that returns sooner when the first characters are wrong.
Examples
Sign and verify a token
Try it yourselfThe user can read the token, but changing it breaks the signature.
import hmac, hashlib
SECRET = b"server-only-key"
def sign(data):
tag = hmac.new(SECRET, data.encode(), hashlib.sha256).hexdigest()
return data + "|" + tag
def verify(token):
data, tag = token.rsplit("|", 1)
good = hmac.new(SECRET, data.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(tag, good)
t = sign("user=amy;role=user")
print(t[:40] + "...")
print(verify(t))
print(verify(t.replace("role=user", "role=admin")))- L2Only the server knows this key. Real keys are long and random.
- L5The tag depends on both the key and every character of the data.
- L9
rsplit("|", 1)splits at the last bar only. - L15Untouched token: the tags match.
True - L16Edited token: the tag no longer matches the data.
False
Why == can leak
Try it yourselfThis comparison counts how many characters it checks before it stops. More correct characters mean more steps, which means more time.
def naive_equal(a, b):
steps = 0
for x, y in zip(a, b):
steps += 1
if x != y:
return False, steps
return len(a) == len(b), steps
secret = "7f3a9c"
for guess in ["000000", "7f0000", "7f3a00"]:
print(guess, naive_equal(secret, guess))- L3
zippairs the characters of both strings. - L6Stops at the first difference.
- L11Wrong from the start: 1 step.
000000 (False, 1) - L11Two correct characters: 3 steps. An attacker measuring time learns the prefix is right.
7f0000 (False, 3)
Common mistakes
random for secrets
random produces numbers that can be predicted from earlier output. Use the secrets module for tokens, reset codes and session ids.
Comparing secrets with ==
== on strings stops at the first difference. For tags, tokens and password hashes use hmac.compare_digest.
Quick quiz
1. What does this print?
def naive_equal(a, b):
steps = 0
for x, y in zip(a, b):
steps += 1
if x != y:
return steps
return steps
print(naive_equal("abcd", "abzz"), naive_equal("abcd", "abcd"))Show the answer
Answer: A
a and b match, c and z differ at the 3rd step. Identical strings go through all 4 characters.
2. A token is `user=amy|<tag>` signed with HMAC. The user changes it to `user=bob|<tag>`. What happens?
Show the answer
Answer: B
The tag was computed from user=amy and the secret key. The server recomputes it for user=bob, gets a different value and rejects the token. Without the key, the user can't produce the right tag.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictWhich tokens pass?
Type exactly what this program prints.
- ModifyA sturdier verify
Improve
verify(token):- compare with
hmac.compare_digestinstead of== - return
False(instead of crashing) when the token has no"|"in it
- compare with
- WriteTokens that expire
Password-reset links should stop working after a while. Write two functions:
make_token(user, expires)returnsuser + "|" + str(expires) + "|" + tag, wheretagis the HMAC (as insign) of the textuser + "|" + str(expires).check_token(token, now)returns the user if the tag is valid andnowis less than or equal toexpires; otherwiseNone.
expiresandnoware whole numbers (seconds). Usehmac.compare_digest.
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| HMAC | HMAC 消息认证码 | A keyed hash that proves data was produced by someone holding the key and wasn't changed. |
| Session id | 会话 ID | A random value that identifies a logged-in user between requests. |
| Timing attack | 时序攻击 | Learning a secret from how long an operation takes. |
| secrets module | secrets 模块 | Python's module for generating unpredictable tokens. |