Repository navigation
hmac_drbg: own the DRBG key; HKDF/Finished no longer leak K - #279
Merged
Merged
Conversation
hmac_key is the key input of hmac_sha256. HKDF (extract and expand) and
the Finished MAC write it, and it was also the HMAC_DRBG's K. Every
handshake therefore left K holding a key-schedule value the peer knows
(after tls_derive_traffic_keys: the server application traffic secret),
and the next tls_connect in the same boot drew client_random (sent in
clear) and then the ECDHE private key from that K:
K' = HMAC(Kx, R || 00), V' = HMAC(K', R), privkey = HMAC(K', V')
so whoever knew the previous session's key schedule could compute the
next session's ECDHE key and decrypt it passively. That includes a MITM
whose handshake the client rejected at the certificate: the handshake
keys are derived before verification.
K is now drbg_k, 32 B of BSS defined in src/crypto/hmac_drbg.s and not
exported, so no other module can name it without failing the link.
drbg_hmac (11 B, falls into hmac_sha256) loads it into hmac_key before
each of the DRBG's five HMACs; the three K stores go to drbg_k.
Tests:
tools/test_drbg_isolation.py (VICE, run_all_tests "drbg_isolation"):
every key-schedule HMAC user leaves K and V byte-identical; a host
HMAC_DRBG from the image's own K,V reproduces tls_connect's two draws
(model check); the attacker's prediction from the hmac_key residue
plus the wire client_random does not give the ECDHE key, after a full
key schedule and after handshake keys only.
tools/test_drbg_state_owner.py (pytest, no build): drbg_k defined only
in hmac_drbg.s and not exported; no DRBG routine calls hmac_sha256 or
stores hmac_key directly; only hmac_drbg.s stores hmac_val.
Red/green (VICE, -reu -reusize 512):
master 249f65d, ip65 default drbg_isolation 2/7: all three isolation
cases and both prediction cases FAIL
(predicted privkey == actual in both
scenarios); only the two model checks
pass. state_owner 2/4.
master, uci-onchip (45bea88a) same 5 FAIL (reviewer's PoC also
PREDICTED: True on this image)
this commit, ip65 default drbg_isolation 7/7, state_owner 4/4
(PRG e903c753...)
Mutations (each on this commit):
drbg_k = hmac_key (re-alias) drbg_isolation 2/7 (the same 5 FAIL),
state_owner FAIL "no longer defines"
generate's HMAC via hmac_sha256 drbg_isolation 5/7 (both model checks
FAIL), state_owner FAIL "calls
hmac_sha256 directly"
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Defence in depth behind the previous commit. The DRBG was seeded once at boot and then only stepped, so every connection's client_random and ECDHE key were a deterministic function of the boot seed and of every draw since. drbg_reseed collects 32 fresh SID osc3 ^ CIA1 timer A bytes and runs update(seed) on the RUNNING state (SP 800-90A reseed: K and V are kept, so entropy already in the state is not discarded), then forces a fresh generate. tls_connect calls it before its two draws. drbg_init_entropy is split into drbg_collect_seed + instantiate, sharing the tail with drbg_reseed: +15 B in CRYPTO_AUX_CODE2, +3 B in tls13.s CODE, no BSS. Cost at run time is four HMACs per connection. The separation, not this, is the fix: SID/CIA entropy quality is not measured, and test_drbg_isolation.py's prediction cases deliberately drive the draws WITHOUT tls_connect, so they keep testing the separation alone. Test: test_drbg_isolation.py gains "tls_connect reseeds before drawing" -- the real tls_connect (callees after the draws stubbed) must not produce generate(K, V) of the state it was entered with. master 249f65d FAIL (client_random == generate(K, V)) this commit 8/8 (default ip65 build, PRG a361dbb6...) this commit, reseed call deleted from tls13.s 7/8, exactly this case FAIL Branch head, all profiles (measure_margins --build; uci-m3 links no DRBG and is byte-identical to master, b124ea9f...): ip65 a361dbb6 ip65-onchip 275bd5cc uci 214ce9c5 uci-onchip 2e24da0f uci-comb 30f34930 LOADER -3 B, NET_CODE -24 B on every profile; drbg_k takes 32 B of the ip65 COLD_SHADOW alignment hole (103 -> 71). Reviewer's PoC on uci-onchip 2e24da0f: PREDICTED: False. Hardware, U64E fw 3.15 3a1ff9ff / FPGA 126 / core 1.50, 48 MHz, onchip: rig_https_local (HTTPS_SNI=www.foo.invalid, PRG 09c31fd1...): PASS rig_https_banner (en.wikipedia.org /wiki/Commodore_64, PRG 0dbfd115...): PASS, 751,196 B == Content-Length two 'G' in one boot, same PRG (scratch driver over the banner rig's helpers): en.wikipedia.org /robots.txt HTTP 200, 28,275 B == Content-Length; then lwn.net / HTTP 200, terminal chunk after 26,119 B. Both closed cleanly to the menu. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adversarial-review follow-ups on f0c878f. No code bytes change: every profile's PRG hash is identical to f0c878f (ip65 a361dbb6, ip65-onchip 275bd5cc, uci 214ce9c5, uci-onchip 2e24da0f, uci-comb 30f34930, ip65-wiki 2b458d3b, ip65-onchip-wiki 2800f819, uci-comb-demo-store fcf93260) and measure_margins --build is byte-identical apart from its temp paths. test_drbg_isolation.py, "tls_connect reseeds before drawing" (patch from the review): reads drbg_seed back and requires client_random == generate(update(K, V, seed)) -- which is also the first behavioural model check of update-with-seed -- then re-enters from the same K, V and requires a different seed and a different client_random. The old check (client_random != generate(K, V)) passed three mutants: old now noentropy 8/8 7/8 (== generate(update) FAIL, seed FAIL) constseed 8/8 7/8 (different-seed FAIL) step5raw 8/8 7/8 (== generate(update) FAIL; also state_owner FAIL) test_drbg_state_owner.py: drbg_hmac must be exactly the drbg_k -> hmac_key copy falling into hmac_sha256. Mutant revcopy (copy the other way) passed the static guard before; now state_owner FAIL (test_drbg_hmac_is_exactly_the_k_load), and VICE 5/8 (both model checks and the reseed case FAIL). hmac_drbg.s: the drbg_reseed comment claimed "32 fresh SID+CIA bytes". CIA1 timer A decrements at a fixed rate between reads and osc3 moves slowly, so a sample carries roughly the entropy of its start instant; the comment now says so and that it is unmeasured. Not done: zeroing hmac_key/hmac_opad_block after the DRBG's HMACs. It is hygiene only (the residue is reachable by nothing, and 'Q''s scrub covers it) and costs more than 6 B, since drbg_hmac falls into hmac_sha256 and a post-call wipe needs a wrapper. Correction to f0c878f's message: "+15 B in CRYPTO_AUX_CODE2" is wrong. Measured: drbg_hmac +11 B (1c51d39), the reseed split +13 B (f0c878f), NET_CODE -24 B overall, as its own margin line says. Red/green (VICE, -reu -reusize 512, ip65 default): master 249f65d (PRG 5f261d3c) drbg_isolation 2/8 (only the two model checks pass), state_owner 3 FAIL of 5 this commit (PRG a361dbb6) drbg_isolation 8/8, state_owner 5/5 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defect
hmac_keyis the key input ofhmac_sha256. HKDF and the Finished MAC write it, and it was also the HMAC_DRBG's K. Every handshake therefore left K holding a key-schedule value the peer knows; aftertls_derive_traffic_keysthat is the server application traffic secret.A second 'G' in the same boot then drew
client_random, which is sent in clear, and the ECDHE private key from that K:So anyone who knew the previous session's key schedule could compute the next session's ECDHE key and decrypt that session passively. This includes a MITM whose handshake the client rejected at the certificate, because the handshake keys are derived before verification. A retry after a rejected MITM is exactly that second 'G'.
Fix
drbg_k, 32 B of BSS insrc/crypto/hmac_drbg.s, and it is not exported. Any other module that names it fails the link.drbg_hmac(11 B) loads it intohmac_keybefore each DRBG HMAC.tls_connectcallsdrbg_reseed, an SP 800-90A update with 32 fresh SID/CIA bytes on the running state, before its draws. That entropy is unmeasured; the separation in 1c51d39 is the fix.drbg_hmac) and an honest reseed comment. Every PRG is byte-identical to f0c878f.Evidence
Tests:
tools/test_drbg_isolation.py(VICE): key-schedule HMACs leave K and V untouched; a host model of the DRBG reproducestls_connect's draws; the attacker's prediction does not give the ECDHE key.tools/test_drbg_state_owner.py(pytest, no build): onlyhmac_drbg.snamesdrbg_kor stores V.5f261d3c…)a361dbb6…)Mutants, each red:
drbg_ktohmac_keygeneratekeyed viahmac_sha256Hardware: U64E, fw 3.15 3a1ff9ff, FPGA 126, core 1.50, 48 MHz, uci-onchip build.
rig_https_banneragainst en.wikipedia.org/wiki/Commodore_64 (PRG0dbfd115…): PASS, 751,196 B equals Content-Length.Size
LOADER is down 3 B and NET_CODE down 24 B on every profile.
drbg_ktakes 32 B of the ip65 COLD_SHADOW alignment hole. uci-m3 is unchanged (b124ea9f…).PRG sha256 at f49d8ab:
a361dbb6275bd5cc214ce9c52e24da0f30f34930Note: f0c878f's message says "+15 B in CRYPTO_AUX_CODE2". That is wrong; f49d8ab corrects it:
drbg_hmac+11 B, the reseed split +13 B, NET_CODE -24 B overall.🤖 Generated with Claude Code