Everyone Can Learn Program
中文 Start learning
Specialty · SecurityLesson 7 of 9Free to read

Tokens and HMAC · Cyber Security Basics

How does a server know a cookie wasn't changed by the user?

Course contents

Why 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(), not random, 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 yourself

The 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")))
Step by step
  1. L2Only the server knows this key. Real keys are long and random.
  2. L5The tag depends on both the key and every character of the data.
  3. L9rsplit("|", 1) splits at the last bar only.
  4. L15Untouched token: the tags match.True
  5. L16Edited token: the tag no longer matches the data.False

Why == can leak

Try it yourself

This 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))
Step by step
  1. L3zip pairs the characters of both strings.
  2. L6Stops at the first difference.
  3. L11Wrong from the start: 1 step.000000 (False, 1)
  4. 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"))
  1. A
    3 4
  2. B
    4 4
  3. C
    2 4
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?

  1. AThe server accepts it: the tag is still there
  2. BThe server rejects it: the tag doesn't match the new data
  3. CThe user can compute a new tag from the old one
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.

  1. PredictWhich tokens pass?

    Type exactly what this program prints.

  2. ModifyA sturdier verify

    Improve verify(token):

    • compare with hmac.compare_digest instead of ==
    • return False (instead of crashing) when the token has no "|" in it
  3. WriteTokens that expire

    Password-reset links should stop working after a while. Write two functions:

    • make_token(user, expires) returns user + "|" + str(expires) + "|" + tag, where tag is the HMAC (as in sign) of the text user + "|" + str(expires).
    • check_token(token, now) returns the user if the tag is valid and now is less than or equal to expires; otherwise None.

    expires and now are whole numbers (seconds). Use hmac.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

TermChineseDefinition
HMACHMAC 消息认证码A keyed hash that proves data was produced by someone holding the key and wasn't changed.
Session id会话 IDA random value that identifies a logged-in user between requests.
Timing attack时序攻击Learning a secret from how long an operation takes.
secrets modulesecrets 模块Python's module for generating unpredictable tokens.