Hashes, MACs, and Random Values
Why == Is the Wrong Comparison
Your verify answers True or False, and an attacker who sends a wrong tag learns only that it was wrong. Now suppose they learned one thing more: how hard the comparison had to work before it said False. This lesson shows what that extra piece is worth. It is the reason behind finding F2, which you wrote down in Chapter 1 without one.
A counting model
Here is a comparison written out by hand. It walks through two
Every guess but the last is wrong, and the answer is False each time. The count is not the same each time. 00000000 is wrong at its first character, so one character was looked at. 7f3a0000 starts with four right characters, so the count is 5. The better the guess, the higher the count.
This is a model. Python’s own == on two strings first checks that they have the same length and then compares their memory. It keeps no count, and nothing on this site is timed. What the model shares with the real thing is the shape: the work can depend on where the first difference is.
With the secret 7f3a91c2, what does examined("7f300000", secret) return?
What the count is worth
An attacker who can see the count does not have to guess the whole secret at once. They can find it one character at a time: try each of the 16 hex digits in the first position, and keep the one for which the count goes up. Then do the same for the second position. recover does that, using nothing but what check reports:
It prints ('7f3a91c2', 67): the whole secret, after 67 guesses. A wrong letter at position p stops the count at p + 1, so a higher count means a right letter. Without the count, an attacker has to try whole values, and there are 16 ** 8 of them, which is 4,294,967,296. The count turned one hard problem into eight easy ones. For a real tag of 64 characters the same
Which secret is at risk in verify? Lesson 2 said a tag is not secret. For a message the server signed, that is true: the client has both. The right tag for a message the attacker wrote is different. Only the key holder can compute it, and verify computes it on every call. If the comparison shows how much of a guess is right, the attacker can work that tag out one character at a time and hold a forged message that verifies, without the key.
From a count to a clock
A real attacker has no count. What they may have is the time a reply takes, because more work takes longer. That signal is tiny and buried in noise, so reading it takes a great many requests and careful statistics.
You do not have to judge how practical the attack is against your service, because the defense costs one line. That is a pattern worth keeping: when closing a weakness is cheap, close it instead of arguing that nobody would use it.
hmac.compare_digest
The standard library has a comparison made for secrets:
It prints True, False, False, as == would. The difference is in how it works: hmac.compare_digest looks at every character whatever the guess is, so its work says nothing about where two values differ. A comparison with that property is called a constant-time comparison.
Two details matter when you use it:
It can still reveal that the two values have different lengths, and its documentation says so. For a tag that is harmless, because everyone knows a tag has 64 characters.
It accepts two
strvalues only when both are plain ASCII, and raisesotherwise. A tag arrives from a client, and a client can send anything. So compare bytes: call .encode("utf-8")on both sides first.
What can hmac.compare_digest still give away?
Where the rule applies
The rule is: when a secret is compared with one expected hmac.compare_digest. In this course that is a tag, a password hash in Chapter 3, and one more token in Chapter 8.
It is not every lookup. state["sessions"].get(session_id) finds a != on a password and not about the session store.
Which line needs hmac.compare_digest?
Why keys come from secrets
One more loose end from Lesson 2: why secrets.token_bytes, and not the random
The two lines are the same 32 characters, and the last line prints True. random is built to be repeatable: from the same starting point, the seed, it always produces the same values. That is a feature for a simulation or a game. For a key it is fatal, because anyone who learns the seed, or sees enough of the output, can compute what comes next. secrets asks the
A program calls random.seed(2026) and then random.randbytes(16).hex(). It is run again tomorrow. What does the second run print?
You now have the reason for F2 and the tool that fixes it. The project in the next lesson puts hmac.compare_digest into verify and proves it with tests.