A v2 peer was being sent a post-quantum ratchet step, and v3 was never advertised #31

Merged
paulhitt merged 2 commits from 2-v3-cross-engine into main 2026-08-31 17:01:59 +00:00
2 Commits
Author SHA1 Message Date
Paul Hitt eaf860e794 Advertise v3, now that there is evidence for the claim
PROTOCOL_VERSION read 2 with a note saying 3 would be wrong until ML-KEM was in
every ratchet step, "a separate piece of work". That work is done and proven
against another engine, so the number moves in the same change as the evidence
rather than ahead of it.

Until now the ratchet was inert in the field: iOS advertises 3 and gates on the
peer doing the same, so an iOS-to-Android session negotiated v2 and no message
ever carried a ratchet step, whatever either engine implemented.

🔴 A version is a claim other devices act on. Peers pin the highest proven
version and the pin is monotonic (§13.4), so a build that advertises 3 and
cannot honour it does not degrade — it strands every contact that believed it.

Three tests moved with it, and one is new. `a v2 registration publishes the
whole block` asserted 2 and now asserts the constant. `a capability signature
does not transfer between versions` edited a bundle to `proto = 3` to prove the
signature would not follow — a hardcoded neighbour that stopped testing anything
the moment the engine published 3, so it uses `PROTOCOL_VERSION + 1`. The v2
gating test needed a peer that has genuinely proven 2, which this engine no
longer produces: `copy(proto = 2)` is not enough, because the capability
signature commits to the version, so `publishedV2` re-signs for it.

The new one checks the published bundle claims 3 AND proves it. Bumping the
constant without the signature following would produce a bundle every peer
silently reads as v1, with nothing reporting an error anywhere.

Also: `the sealed blob reveals nothing` was the same coin flip as its iOS twin —
sealing ids 41 and 42 and searching the BASE64 for "41" and "42", about a 14%
chance of a meaningless failure. It fired during this run. The iOS one was fixed
this morning and this was missed because only one engine was looked at.

606 tests, 0 failures. Cross-engine: 56 passed, 0 failed.
2026-08-31 13:00:22 -04:00
Paul HittandClaude Opus 4.8 1e96af0122 A v2 peer was being sent a post-quantum ratchet step
Proving the v3 ratchet across engines turned up a bug in shipping it, and this
one would have broken every existing Android conversation.

`E2ESession.initiate` turned the v3 ratchet on whenever the peer offered a PQ
prekey. Every v2 device offers one — offering a prekey is a claim to §12, not to
§13. So a build with this change would attach `pqdh`/`pqct` to messages for
0.6, the released build, whose ratchet has no post-quantum anything.

That fails twice over, which is why it would have looked like a network
problem. Their root step derives without `ss` and under the `_v1` info string
while ours used `_v3`, so the keys diverge; and their canonical header omits the
two fields, so the AAD does not match either. The first message never opens and
never would.

Gated on a PROVEN claim now, the way the iOS engine already does it:
`chosen != null && bundle.proto >= 3`. `bundle.proto` on its own is the relay's
word for it, but `chosen` is non-null only when the capability signature over
that same proto verified (§12.7), so the pair is a claim the peer actually made.

The test asserts on the absence of a header field, which is the only way to
catch this — every message still decrypts in a self-test, because both sides of
one engine agree with each other about the mistake.

The interop driver now publishes a real v3 bundle: `pqspk` under
`FlamenetE2E_PQSPK_v2`, and `caps_sig` over `FlamenetE2E_Caps_v2 ‖ uint8(3)`.
Both were needed before the web engine would negotiate anything but v1 — which
is why the harness had been quietly proving v1 and nothing else. It can still
publish a v1 bundle on request, because the fleet still holds devices that speak
it and that coverage must not disappear the day v3 lands.

605 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 12:55:44 -04:00