0%

Hashes, MACs, and Random Values · practice

Sign a Message with a Key

Suppose a server wants to hand a client a short message, such as “book 7 belongs to ana”, and recognize it when it comes back. The simplest idea is to send a digest along as a tag, and to check on return that the digest still fits the message. Here is that design, and a forger:

import hashlib

message = b"book 7 belongs to ana"
tag = hashlib.sha256(message).hexdigest()

forged = b"book 7 belongs to ben"
forged_tag = hashlib.sha256(forged).hexdigest()

# The server's check: digest the message that arrived,
# and compare it with the tag that arrived.
print(hashlib.sha256(forged).hexdigest() == forged_tag)

It prints True. The forger changed the message, computed the digest of the new one, and the server’s check passes. Nothing in the tag depended on anything the forger lacks. That is the weak design, and this lesson replaces it.

A key

The missing ingredient is a secret that the maker of the tag and its checker both hold, and that nobody else has: a key.

You have made a secret like this before. In with Python a came from secrets.token_urlsafe(16): 128 random bits from the , because the random can be predicted. A key is the same idea with two differences. It is bytes, not text, and this course uses 32 of them:

key = secrets.token_bytes(32)

A key is never text a person chose. A word or a sentence has far less randomness than its length suggests, and an attacker can try likely phrases. Thirty-two random bytes are 256 random bits, as many as SHA-256 puts out, and there is no shortcut to guessing them. signing.py holds the number 32 in a constant, KEY_BYTES.

A key is also the one thing in this module that must stay with the server. The message is not secret, and neither is the tag the server sends along with it. In this chapter a key lives only as long as one run of a program. Chapter 5 deals with where a real service keeps one.

hmac

The standard library’s hmac module computes a . HMAC is a published construction that mixes a key into a hash function. With SHA-256 it is called HMAC-SHA-256:

tag = hmac.new(key, message, hashlib.sha256).hexdigest()

Three things to know about this call:

  • The key and the message are both bytes. A str raises .

  • The third names the hash function, and it is required.

  • hexdigest() returns the tag as text: 64 hexadecimal characters, whatever the length of the message. SHA-256 gives 32 bytes, and hex writes each byte as two characters.

With the same key, hmac.new(key, b"", hashlib.sha256).hexdigest() and hmac.new(key, b"a" * 300, hashlib.sha256).hexdigest(): how long are the two tags?

Verify by recomputing

A tag is not decoded. There is nothing inside it to read. The checker takes the message it received and its own copy of the key, computes the tag again, and compares the result with the tag it received. If the message changed, the tag changed, or the key was a different one, the two do not match.

So verify(key, message, tag) is short: sign the message again, and compare. It returns True or False and nothing else. A tag that is too short, empty, or made with another key is not an error. It is a wrong tag, and the answer is False.

In this lesson, compare with ==. The next lesson looks hard at that one line.

Do not build your own

hashlib.sha256(key + message) looks like the same idea: put the key in front and hash. It is not the same. That construction has a known weakness with SHA-256: someone who sees one valid tag can compute a valid tag for a longer message that starts with the original one, without knowing the key. HMAC was designed so that this does not work. Use hmac.

Your task

Open signing.py. Its three are stubs, and main() is ready to use them. Write:

  1. new_key(), which returns secrets.token_bytes(KEY_BYTES).

  2. sign(key, message), which returns the HMAC-SHA-256 tag as 64 hex characters.

  3. verify(key, message, tag), which returns True when tag is the tag of message under key, and False otherwise.

Reach the standard library through its modules, as the imports at the top of the file do: secrets.token_bytes(...), hmac.new(...).

Then run the file:

python signing.py

It prints key: 32 bytes, a tag that is different on every run because the key is new, unchanged message: True, and changed message: False.

Now let the course helper play the forger:

python pl_sec.py forged-message

The run uses no server and no : it calls your three functions directly. It prints a message with its tag, and then hands five forgeries to your verify():

--- five forgeries, each handed to your verify() ---
the message changed, the old tag kept                        -> refused
the message changed, the tag is SHA-256 of the new message   -> refused
the right tag cut to its first 8 characters                  -> refused
an empty tag                                                 -> refused
a tag made with another key                                  -> refused
--- how verify() compared ---
hmac.compare_digest was not called: verify() compares some other way
--- result ---
5 of 5 forgeries refused

The second forgery is the one that fooled the digest design at the top of this lesson. The line under how verify() compared is expected today. It is about the == in your verify, and it changes in Lesson 4.

A server signs messages with a key and checks them with verify. Who can make a tag that verifies?

Then press Check my work. Your module now refuses every forgery the helper tried. The next lesson asks what an attacker could learn from the way verify says no.

Task

In signing.py, write three .

  • new_key() returns secrets.token_bytes(KEY_BYTES).

  • sign(key, message) returns hmac.new(key, message, hashlib.sha256).hexdigest(). key and message are bytes, and the result is 64 hex characters of text.

  • verify(key, message, tag) computes the tag of message under key again and returns True when it equals tag and False otherwise. Compare with == for now.

Reach the standard library through its , as the imports at the top of the file do.

Run python signing.py and python pl_sec.py forged-message in the .

Then press Check my work. It calls new_key() with a stand-in in place of secrets.token_bytes that records the size you ask for, so it can tell that the key is the result of one call of secrets.token_bytes(32), and it checks that two real keys differ. It compares sign() with for keys and messages you have not seen, among them an empty message and one with letters outside ASCII. It calls verify() with the right key, message, and tag, and with a changed message, a changed tag, another key, a shortened tag, an empty tag, a lengthened tag, and the plain digest: the first must give exactly True and the others exactly False. How verify() compares is not checked in this lesson. Nothing from your terminal is graded.