Files
flamenet-messenger-android/client
Paul HittandClaude Opus 5 26a14fb55a engine: Android verifies and pins a peer's section 20 identity
The receive half. PeerBundle now carries ik_pq and spk_pq_sig, PQIdentity
decides what to do with them, and Conversations enforces it where a session is
established.

The three refusals match the web engine and iOS: PQDowngradeException when a
device that was seen with an identity appears without one, PQVerificationException
when the signature does not verify, and PQIdentityChangedException when the
identity is not the pinned one.

The pin is the SHA-256 of ik_pq, not a boolean, because section 20.5 is
explicit about why: a relay that substituted its own ML-DSA key would satisfy
a boolean pin and the substitution would go unnoticed. Same fingerprint the
web engine stores, so it means the same thing on both.

Absent to present is allowed and present to absent is refused. That asymmetry
is section 20.2 and 20.5 together: a device that has never published an
identity is classical and may start later, and one that has may never stop.

Enforced on the new-session path only, because section 20.5 says the pin is
consulted when a session is established, which is the only moment a downgrade
can be attempted; an existing session never refetches a bundle. A bundle that
never opens a session therefore cannot move a pin.

PQIdentity.check is a pure function of the pin and the bundle, so every case
is testable without a vault or a relay. Thirteen cases, most of them a relay
misbehaving: stripping, substituting, a bent signature, an identity with no
signature, and a signature over a different device's prekey.

Falsified: disabling the two refusals turns the suite red on exactly the two
tests that cover them. Engine 707 green, client 469 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 14:26:12 -04:00
..