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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0034a57389
commit
1e96af0122
@@ -203,6 +203,34 @@ class HybridSessionTest {
|
||||
assertEquals("spk", moved.provenPostQuantumPreKey()?.kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 **A v2 peer must not be sent a v3 ratchet step.**
|
||||
*
|
||||
* The released fleet advertises `proto: 2`: it does post-quantum key
|
||||
* AGREEMENT and has no post-quantum ratchet. Attaching `pqdh`/`pqct` to a
|
||||
* message for one of those devices breaks the conversation twice over —
|
||||
* their root step derives without `ss` and under the `_v1` info string
|
||||
* while ours used `_v3`, and their canonical header omits the two fields
|
||||
* entirely so the AAD does not match either. The first message never opens
|
||||
* and never will.
|
||||
*
|
||||
* ⚠️ Offering a PQ prekey is NOT the same claim as implementing §13. Every
|
||||
* v2 device offers one. The version has to be what gates this, which is
|
||||
* what §13.4 says and what the iOS engine already does.
|
||||
*/
|
||||
@Test
|
||||
fun `a peer that only claims v2 is not sent a post-quantum ratchet step`() {
|
||||
val alice = DeviceIdentity.create()
|
||||
val bob = DeviceIdentity.create()
|
||||
// A real v2 bundle: PQ prekeys published, capability signed for 2.
|
||||
val session = E2ESession.initiate(alice, published(bob))
|
||||
val envelope = session.encrypt("hello")
|
||||
assertNull(envelope.header.pqdh,
|
||||
"a v2 peer was sent a v3 ratchet step it cannot read")
|
||||
assertNull(envelope.header.pqct,
|
||||
"a v2 peer was sent a PQ ciphertext it cannot decapsulate")
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user