Files
Paul HittandClaude Opus 5 b11a41f09b An inclusion proof that reconstructs nothing is refused
Six refusals were disabled one at a time. Five died: the three §20
identity checks, downgrade, signature and substitution, and both key-log
fork checks. This one survived all 719 engine and 473 client tests.

`verifyInclusion` ends with `rootFromInclusion(...) ?: return false`.
That line is what turns "this proof reconstructs nothing" into a
refusal, and flipping it to `?: return true` broke nothing.

It hid because the suite does assert the null cases, on
`rootFromInclusion` directly:

    assertNull(KeyLog.rootFromInclusion(leaf, size, size, proof))
    assertNull(KeyLog.rootFromInclusion(leaf, -1, size, proof))

The primitive was tested and the wrapper everything calls was not. With
that line wrong, a proof whose shape is wrong, a bad index, a truncated
path, a spare node, would have verified, which is the §19.2 refusal
itself.

The new test drives `verifyInclusion` with all four shapes and asserts
the published proof still verifies first, so the refusals cannot be
satisfied by a verifier that always says no.

`scripts/mutate.py` ships the harness, as in the engine and the relay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:20:14 -04:00

87 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Disable an Android refusal and see whether the engine and client suites notice.
Same question as the engine and the relay: not whether the app refuses, but
whether anything would notice if it stopped.
python3 scripts/mutate.py [repo-root] [name ...]
"""
import os
import subprocess
import sys
import pathlib
ROOT = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
MUTATIONS = [
("pq-downgrade", "engine/src/main/kotlin/io/flamenet/e2e/PQIdentity.kt",
" if (pinned != null) throw PQDowngradeException(bundle.deviceId)",
" if (false) throw PQDowngradeException(bundle.deviceId)",
"a device that published a §20 identity and now publishes none"),
("pq-signature", "engine/src/main/kotlin/io/flamenet/e2e/PQIdentity.kt",
" if (!PQSign.verify(ikPQ, bundle.signedPreKey, sig)) {\n throw PQVerificationException(bundle.deviceId)\n }",
" if (false) {\n throw PQVerificationException(bundle.deviceId)\n }",
"a §20 prekey signature that does not verify"),
("pq-substitution", "engine/src/main/kotlin/io/flamenet/e2e/PQIdentity.kt",
" if (pinned != null && pinned != seen) throw PQIdentityChangedException(bundle.deviceId)",
" if (false) throw PQIdentityChangedException(bundle.deviceId)",
"a peer whose pinned §20 identity was replaced"),
("log-shrank", "client/src/main/kotlin/io/flamenet/client/Trust.kt",
" if (pin.size > head.size) {\n throw KeyLogForkException(\"head went backwards, ${pin.size} -> ${head.size}\")\n }",
" if (false) {\n throw KeyLogForkException(\"head went backwards, ${pin.size} -> ${head.size}\")\n }",
"a transparency log that shrank"),
("log-fork", "client/src/main/kotlin/io/flamenet/client/Trust.kt",
" if (pin.root != head.root) {\n throw KeyLogForkException(\"two different roots at size ${head.size}\")\n }",
" if (false) {\n throw KeyLogForkException(\"two different roots at size ${head.size}\")\n }",
"two different roots at the same log size"),
("inclusion", "engine/src/main/kotlin/io/flamenet/e2e/KeyLog.kt",
" val got = rootFromInclusion(leaf, index, size, proof) ?: return false",
" val got = rootFromInclusion(leaf, index, size, proof) ?: return true",
"an inclusion proof that does not reconstruct a root"),
]
def run_tests():
env = dict(os.environ, JAVA_HOME="/opt/homebrew/opt/openjdk@21")
r = subprocess.run(["./gradlew", ":engine:test", ":client:test", "-q"],
cwd=ROOT, capture_output=True, text=True, timeout=1800, env=env)
return r.returncode == 0
def main():
wanted = set(sys.argv[2:])
survived, killed = [], []
for name, rel, old, new, disables in MUTATIONS:
if wanted and name not in wanted:
continue
path = ROOT / rel
original = path.read_text()
if original.count(old) != 1:
print(f" SKIP {name}: anchor matched {original.count(old)} times", flush=True)
continue
path.write_text(original.replace(old, new))
try:
ok = run_tests()
finally:
path.write_text(original)
if ok:
survived.append((name, disables))
print(f" SURVIVED {name}: nothing failed with {disables} accepted", flush=True)
else:
killed.append(name)
print(f" killed {name}", flush=True)
print(f"\nkilled {len(killed)}, survived {len(survived)}")
for name, disables in survived:
print(f" GAP: no test refuses {disables}")
return 1 if survived else 0
if __name__ == "__main__":
sys.exit(main())