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>
87 lines
3.8 KiB
Python
Executable File
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())
|