fix(test): AEAD tamper oracle must fail closed (audit F1) - #78
Conversation
tools/test_tls_record.py 4b asserted the carry flag only when the register dict contained a key named "P". VICE's binary monitor names the 6502 status register "FL", so that branch was dead on every VICE run and 4b always fell through to the fallback oracle, which asserted merely that the computed tag differed from the record's tag. Tags always differ on tampered input, so 4b passed whether or not aead_decrypt rejected the record: with the `bne @auth_fail` tag-rejection branch removed from src/crypto/aead.s the suite still reported 17/17. Fix: - carry_from_regs() looks the status register up under FL/P/FLAGS/SR, so the primary (and only sound) oracle actually fires. - The fallback pass path is gone. Not being able to read the carry means the tamper rejection could not be evaluated, which is a FAIL, not a pass. - The tag comparison survives only as diagnostic text printed on a C=0 failure -- it distinguishes "detectable but not rejected" from "tag never recomputed". - Launch VICE through _vice_helpers.default_vice_config() like every other in-tree VICE test, instead of a bare ViceConfig. A/B (VICE, ip65 default profile, --seed 20260813): pristine 17/17 pass, exit 0, 4b "PASS: decrypt returned C=1 (tag mismatch)" mutant (bne @auth_fail -> nop nop, verified in the linked PRG as the only two changed bytes, D0 31 -> EA EA at $67A9 = aead_decrypt+9) 16/17, 4b FAIL, exit 1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 13, 2026
Independently reproduced by the supervising session — not accepted on report alone. Verified in a clean copy of the repo at Pristine sources + fixed test — MUTANT ( Mutation confirmed live in the built binary before trusting the run — The root cause identified here is worth emphasising beyond "the fallback was weak": the primary assertion was gated on Confirms audit finding F1. |
F1 — the AEAD tamper test could not fail
Defect.
tools/test_tls_record.pytest 4b feedstls_record_decryptatampered record and is supposed to assert C=1 (rejected); it read the carry
only
if regs and "P" in regs, but VICE's binary monitor names the 6502 statusregister
FL(harnessbackends/vice_binary.py:1325:regs.get("FL", regs.get("FLAGS", regs.get("SR", 0)))), so that branch was deadon every VICE run and 4b always fell through to a fallback that asserted only
that the computed tag differed from the record's tag — true of any tampered
input whether or not
aead_decryptrejected it.Fix.
carry_from_regs()looks the status register up underFL/P/FLAGS/SRso the real oracle fires, and the fallback pass path is gone: not being able to
read the carry means the rejection could not be evaluated, which is a FAIL. The
tag comparison survives only as diagnostic text on a C=0 failure. Also switched
the VICE launch from a bare
ViceConfigto_vice_helpers.default_vice_config(), matching every other in-tree VICE test(same file; no other file touched).
Mutant effectiveness — proven in the built binary, not assumed
Mutation:
src/crypto/aead.s, thebne @auth_failafterjsr aead_verify_tag, replaced withnop/nop. Byte-diff of the linked PRGagainst the pristine one:
Exactly two bytes,
D0 31→EA EA(octal 320/61 → 352). PRG offset 24491(1-based) maps to
$0801 + 24489 - 2 = $67A9, andbuild/labels.txthasal C:67A0 .aead_decrypt— i.e.aead_decrypt+9, the branch itself. Mutationconfirmed present and in the right place. The mutation lived in the working tree
only and was reverted before the commit; the branch contains just the test fix.
Acceptance A/B (VICE, ip65 default profile,
--seed 20260813)PRISTINE ⇒ 17/17, exit 0
(
EXIT=0)MUTANT ⇒ 4b FAILS, exit 1
(
EXIT=1)The mutant's diagnostic line is the defect stated out loud: the tags did
differ, which is exactly the condition the old fallback scored as a PASS.
Blast radius
confirms
tls_record_decryptreally does return C=1 on tag mismatch — thepreviously-unexercised assertion is correct.
default_vice_config()adds-reu -reusize 512to this suite's VICE launch;17/17 with it, so no behavioural change beyond matching house style.
not populated and
ip65-build/ip65-c64.binis gitignored, so a first run needsgit submodule update --init --recursivethenmake ip65-libs && make ip65-blob(the blob produced is byte-identical to the main clone's).