This closes the in §15 — the Android PQ ratchet had never been run against
another engine.
The tripwire was wired to nothing
This file-s header said the day Kotlin gained ML-KEM was the day its v1
assertion would fail and get updated. That day passed and the assertion did not
move, because it could not:
constn=awaitnegotiate(parsed,1);ok("JS reads the Kotlin bundle and negotiates v1",n.version===1&&...);
The harness capped negotiation at 1, so the bundle could have claimed anything
and negotiate() would still return v1. It had been proving v1 and nothing else.
What actually blocked v3
The driver published proto: 3 — and the web engine still read it as v1, correctly. bundleFromWire discards the whole v2 block unless pqspk, pqspk_sigand caps_sig are present, and negotiate() re-verifies both signatures. The driver
had been signing the bare PQ key with no domain string and sending no capability
signature at all.
Fixed to spec: FlamenetE2E_PQSPK_v2 ‖ pub, and FlamenetE2E_Caps_v2 ‖ uint8(3).
The assertion that matters
Thirteen new checks drive a v3 session both ways. The load-bearing one is the
web engine opening Kotlin-s reply: to send it, Kotlin had to decapsulate with
its prekey seed, adopt the web engine-s pqdh, and encapsulate a fresh key —
and doing those three in the wrong order (§13.2) derives a different root key with no error anywhere. Sixteen Kotlin unit tests cannot catch that, because
both sides of a self-test make the same mistake together.
Also pinned: a fresh ML-KEM key per step, not a reused one — reuse still
decrypts and silently gives up the healing §13.1 exists for.
v1 coverage kept
Every earlier section now asks the driver for a v1 bundle explicitly. Those paths
are not legacy trivia — the released fleet speaks them — and they would have
stopped being covered the moment the driver learned v3.
56 passed, 0 failed (was 43)
What it found
paulhitt/flamenet-messenger-android!4 — the Android engine was sending v3 ratchet
steps to v2 peers, which would have broken every conversation with the released
0.6 build. §15 should not go to until that merges.
This closes the in §15 — the Android PQ ratchet had never been run against
another engine.
### The tripwire was wired to nothing
This file-s header said the day Kotlin gained ML-KEM was the day its v1
assertion would fail and get updated. That day passed and the assertion did not
move, because it could not:
```js
const n = await negotiate(parsed, 1);
ok("JS reads the Kotlin bundle and negotiates v1", n.version === 1 && ...);
```
The harness capped negotiation at 1, so the bundle could have claimed anything
and `negotiate()` would still return v1. It had been proving v1 and nothing else.
### What actually blocked v3
The driver published `proto: 3` — and the web engine still read it as v1, correctly.
`bundleFromWire` discards the whole v2 block unless `pqspk`, `pqspk_sig` **and**
`caps_sig` are present, and `negotiate()` re-verifies both signatures. The driver
had been signing the bare PQ key with no domain string and sending no capability
signature at all.
Fixed to spec: `FlamenetE2E_PQSPK_v2 ‖ pub`, and `FlamenetE2E_Caps_v2 ‖ uint8(3)`.
### The assertion that matters
Thirteen new checks drive a v3 session both ways. The load-bearing one is **the
web engine opening Kotlin-s reply**: to send it, Kotlin had to decapsulate with
its prekey seed, adopt the web engine-s `pqdh`, and encapsulate a fresh key —
and doing those three in the wrong order (§13.2) derives a different root key
**with no error anywhere**. Sixteen Kotlin unit tests cannot catch that, because
both sides of a self-test make the same mistake together.
Also pinned: a fresh ML-KEM key per step, not a reused one — reuse still
decrypts and silently gives up the healing §13.1 exists for.
### v1 coverage kept
Every earlier section now asks the driver for a v1 bundle explicitly. Those paths
are not legacy trivia — the released fleet speaks them — and they would have
stopped being covered the moment the driver learned v3.
```
56 passed, 0 failed (was 43)
```
### What it found
paulhitt/flamenet-messenger-android!4 — the Android engine was sending v3 ratchet
steps to v2 peers, which would have broken every conversation with the released
0.6 build. §15 should not go to until that merges.
paulhitt
(Migrated from gitlab.com)
merged commit 2adadddea1 into main2026-08-31 17:10:05 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
This closes the in §15 — the Android PQ ratchet had never been run against
another engine.
The tripwire was wired to nothing
This file-s header said the day Kotlin gained ML-KEM was the day its v1
assertion would fail and get updated. That day passed and the assertion did not
move, because it could not:
The harness capped negotiation at 1, so the bundle could have claimed anything
and
negotiate()would still return v1. It had been proving v1 and nothing else.What actually blocked v3
The driver published
proto: 3— and the web engine still read it as v1, correctly.bundleFromWirediscards the whole v2 block unlesspqspk,pqspk_sigandcaps_sigare present, andnegotiate()re-verifies both signatures. The driverhad been signing the bare PQ key with no domain string and sending no capability
signature at all.
Fixed to spec:
FlamenetE2E_PQSPK_v2 ‖ pub, andFlamenetE2E_Caps_v2 ‖ uint8(3).The assertion that matters
Thirteen new checks drive a v3 session both ways. The load-bearing one is the
web engine opening Kotlin-s reply: to send it, Kotlin had to decapsulate with
its prekey seed, adopt the web engine-s
pqdh, and encapsulate a fresh key —and doing those three in the wrong order (§13.2) derives a different root key
with no error anywhere. Sixteen Kotlin unit tests cannot catch that, because
both sides of a self-test make the same mistake together.
Also pinned: a fresh ML-KEM key per step, not a reused one — reuse still
decrypts and silently gives up the healing §13.1 exists for.
v1 coverage kept
Every earlier section now asks the driver for a v1 bundle explicitly. Those paths
are not legacy trivia — the released fleet speaks them — and they would have
stopped being covered the moment the driver learned v3.
What it found
paulhitt/flamenet-messenger-android!4 — the Android engine was sending v3 ratchet
steps to v2 peers, which would have broken every conversation with the released
0.6 build. §15 should not go to until that merges.
mentioned in commit
2adadddea1changed the description
mentioned in merge request flamenet-messenger-android!4