Files
Paul HittandClaude Opus 5 518b1a864e Revocation is tested off the inbox, where it was not
Five refusals in the auth path were disabled one at a time to see which
the suite noticed. Four died. This one survived all 736 tests: the check
in `authenticate` that refuses a revoked device still holding a live
token, the one whose comment cites audit M2.

Two tests do cover revocation and one says it is exercising the
retirement path. Both call `poll`, and `poll` carries its own
`device_revoked` guard on the device row, so both pass whether or not
`authenticate` refuses anything. The `authenticate` check is what every
other authenticated route relies on, and nothing touched it.

With it gone a revoked phone keeps fetching bundles, sending, replenishing
prekeys and mutating groups until its token expires. Only its inbox
closes.

The test goes through `bundles`, which has no revocation check of its
own, and asserts the target's one-time prekey is still in the pool
afterwards, so the refusal is shown to happen before the route does any
work.

`Scripts/mutate.py` ships the harness. The four that died are real
coverage: proof signature, clock skew, replay admission and device key
mismatch.

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

84 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Disable a relay refusal and see whether its own test target notices.
The relay is the component the threat model assumes is hostile, so the
question is not whether it refuses but whether anything would notice if it
stopped. Each mutation below removes one refusal.
A mutation that SURVIVES, meaning `swift test` still passes with the check
gone, is a gap.
python3 Scripts/mutate.py [repo-root] [name ...]
"""
import subprocess
import sys
import pathlib
ROOT = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
MUTATIONS = [
("proof-signature", "Sources/FlamenetRelay/Token.swift",
" guard deviceKey.isValidSignature(sigBytes, for: canonical) else {\n throw AuthError.badProof\n }",
" if false {\n throw AuthError.badProof\n }",
"a per-request proof whose signature does not verify"),
("proof-skew", "Sources/FlamenetRelay/Token.swift",
" guard let ts = Int(timestamp), abs(now - ts) <= skew else { throw AuthError.badProof }",
" guard let ts = Int(timestamp), true || abs(now - ts) <= skew else { throw AuthError.badProof }",
"a proof whose timestamp is arbitrarily old"),
("proof-replay", "Sources/FlamenetRelay/Routes.swift",
" guard let proofSig, await replay.admit(proofSig, now: now) else {\n throw AuthError.replayed\n }",
" guard let proofSig else {\n throw AuthError.replayed\n }\n _ = await replay.admit(proofSig, now: now)",
"a proof signature replayed a second time"),
("device-mismatch", "Sources/FlamenetRelay/Routes.swift",
" guard known.ikSig == caller.identityKey.base64EncodedString() else {\n throw AuthError.deviceMismatch\n }",
" if false {\n throw AuthError.deviceMismatch\n }",
"a token naming a device whose registered key is a different one"),
("device-retired", "Sources/FlamenetRelay/Routes.swift",
" } else if try await KeyLog.isRetired(deviceID: caller.deviceID, on: request.db) {",
" } else if false, try await KeyLog.isRetired(deviceID: caller.deviceID, on: request.db) {",
"a revoked device finishing its token's lifetime"),
]
def run_tests():
r = subprocess.run(["swift", "test"], cwd=ROOT, capture_output=True,
text=True, timeout=3600)
return r.returncode == 0, (r.stdout + r.stderr)[-400:]
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, tail = 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())