Appendix D: Solutions for Chapter 14
This page collects solutions and editorial notes for the exercises in Chapter 14: One-time signatures from hash functions. Compute and derivation exercises have worked solutions; open-ended exercises have an editorial note describing what a strong answer addresses.
The fuller versions of these routines are in the lamport_merkle package. Its directory in the companion repository is solutions/ch14-lamport, and pytest tests/ch14 runs its suite from a clone. Appendix C has the setup.
Exercise 1
Section titled “Exercise 1”The Lamport scheme signs the SHA-256 digest of the message, not the raw bytes: hashlib.sha256(b"\x5C").digest() begins with the byte 0xa9, so the bit string is 10101001 (MSB to LSB at positions 0-7). The signer reveals secret at every position where is that digest bit. The verifier hashes each revealed secret and compares to the corresponding public-key half . The structural answer: exactly 8 secrets are revealed (one per bit) and exactly 8 hash comparisons verify the signature. The exact secret bytes depend on how the seed is expanded; the verification logic is independent of that expansion.
Exercise 2
Section titled “Exercise 2”For two distinct messages with digests , the adversary observes secret for each bit of and for each bit of . At every position where the adversary holds both halves of the secret pair. The expected Hamming distance between two random 256-bit digests is 128. Here b"alpha" and b"beta" give 133, so 133 positions leak both halves and the remaining 123 leak one. The five lowest such positions are .
For target with digest , position is forgeable iff has been observed, which happens iff or . At the 133 both-halves positions that is certain. At the other 123 it holds with probability , so the conditional expectation is . For the target b"gamma" the measured count is 189 of 256, leaving 67 positions whose secret the adversary cannot supply.
Two framings of “how unlikely is a full forgery” appear across this chapter, and they answer different questions. Conditioned on an observed Hamming distance , a random third digest is forgeable at every position with probability , which is the the chapter quotes at . Averaged over all three digests instead, each position is forgeable with probability , giving . Both say the same thing: two signatures expose structure without yielding a complete signature on a random target.
import hashlib
def bit(digest, i): return (digest[i // 8] >> (7 - (i % 8))) & 1
h1 = hashlib.sha256(b"alpha").digest()h2 = hashlib.sha256(b"beta").digest()h3 = hashlib.sha256(b"gamma").digest()
hamming = sum(bin(a ^ b).count("1") for a, b in zip(h1, h2))print(hamming)# ==> 133
# A position leaks both halves when the two signed digests disagree there.known = [{bit(h1, i), bit(h2, i)} for i in range(256)]print([i for i in range(256) if len(known[i]) == 2][:5])# ==> [1, 2, 3, 4, 6]
print(sum(1 for i in range(256) if bit(h3, i) in known[i]))# ==> 189Exercise 3
Section titled “Exercise 3”The tree at depth 3 has 8 leaves, 4 nodes at level 1, 2 at level 2, 1 root. The authentication path for leaf 5 (binary 101) is the sibling at each level: leaf 4, node-pair-(6,7), and node-pair-(0..3). Verification: hash leaf 5, combine with the level-0 sibling, then iterate up.
import hashlib
def H(*args): h = hashlib.sha256() for a in args: h.update(a) return h.digest()
leaves = [H(f"ex3-leaf-{i}".encode()) for i in range(8)]level1 = [H(leaves[2 * i], leaves[2 * i + 1]) for i in range(4)]level2 = [H(level1[2 * i], level1[2 * i + 1]) for i in range(2)]root = H(level2[0], level2[1])
# Authentication path for leaf 5 (right child in pair (4, 5))auth = [leaves[4], level1[3], level2[0]]node = H(auth[0], leaves[5]) # combine sibling left || self rightnode = H(node, auth[1]) # pair (4, 5) is left at level 2; sibling is rightnode = H(auth[2], node) # this subtree was right at top; sibling is leftprint(root.hex()[:8])print(node == root)# ==> de559961# ==> TrueExercise 4
Section titled “Exercise 4”NIST defines categories 1, 3, and 5 by the computational resources of key search on AES-128, AES-192, and AES-256, and prices those references as gate counts under a depth limit: , , and quantum gates (National Institute of Standards and Technology, 2016). In the idealized serial-query model, Grover’s key search on a -bit key costs queries and a preimage search on an -bit hash costs , so a hash pairs with the AES key of the same length by query count alone. That pairing is what the last column records. It is not a category claim, because the table costs no circuits. The chapter’s table under “Grover’s impact on hash preimage security” draws the same line.
| classical | quantum | AES analogue (query model) | |
|---|---|---|---|
| 128 | equal to AES-128 () | ||
| 160 | between AES-128 () and AES-192 () | ||
| 192 | equal to AES-192 () | ||
| 224 | between AES-192 () and AES-256 () | ||
| 256 | equal to AES-256 () | ||
| 384 | above AES-256 | ||
| 512 | above AES-256 |
In query counts, , , and equal AES-128, AES-192, and AES-256 in turn. The outputs and sit strictly between two adjacent key lengths, and exceeds AES-256. Reading a row as “meets category 1” needs two things the table does not supply: the gate count of the hash oracle inside the Grover circuit, and the depth limit it runs under. That is how NIST prices the AES references, and it is what Jaques et al. cost for AES itself (Jaques et al., 2020). SHA-256’s quantum preimage query count equals AES-256’s key-search query count. Whether a SHA-256 preimage circuit costs more or fewer than gates is a circuit question this table does not answer.