BLE announce trust verified the packet signature against the Ed25519
signing key carried inside the same announce, and the trust policy only
rejected on noise-key mismatch. Since peerIDs derive from the broadcast
(public) noise key, an on-mesh attacker could replay a victim's
peerID+noiseKey with their own signing key, nickname, and a valid
self-signature; the registry/persisted identity was then overwritten
unconditionally, enabling mesh nickname spoofing and forged attribution
of signed public/broadcast messages.
Fix: TOFU-pin the Ed25519 signing key per peer (noise-key-derived
peerID) on the announce path.
- BLEAnnounceTrustPolicy rejects announces whose signing key differs
from the one already recorded for the peer (.signingKeyMismatch).
- BLEPeerRegistry.upsertVerifiedAnnounce refuses to replace a pinned
signing key (returns nil) and never drops a pinned key.
- BLEAnnounceHandler falls back to the persisted cryptographic identity
(persistedSigningPublicKey) when the registry has no signing key, so
the pin survives registry eviction and app restart.
- SecureIdentityStateManager.upsertCryptographicIdentity refuses to
replace a persisted signing key with a different one, and the
cryptographic identities (incl. the pin) now live in the encrypted,
persisted IdentityCache with synchronous, teardown-safe saves.
Rebased onto main and integrated with #1432 (Noise session identity
binding + signed leaves): both are complementary. #1432's signed-leave
verification reads the same registry/persisted signing key that this
change protects from announce-path poisoning. main's evolution is kept:
BLEPeerRegistry.upsertVerifiedAnnounce still takes capabilities/
bridgeGeohash (nil return propagates as a refusal), the handler's
linkBoundToOtherPeer env is preserved, CryptographicIdentity keeps
main's field set, and EphemeralIdentity uses main's initializer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>