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>
84 lines
3.5 KiB
Python
Executable File
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())
|