#1805

Verify signed packets over the received payload bytes; add byte-exact wire-format vectors shared with Android

Openvincenzopalazzowants to merge intofrom

Summary

Two related changes to the binary protocol, each in its own commit:

  1. Verify and relay signed packets over the received payload bytes (BitFoundation). BinaryProtocol.decode inflates a compressed payload and drops the wire bytes, so the relay re-encode and toBinaryDataForSigning both run CompressionUtil over the decoded payload again. DEFLATE output is encoder-specific (Apple's Compression, java.util.zip on Android and Rust miniz all emit different streams for the same input), and a sender may also have chosen not to compress a payload we would compress. The rebuilt preimage then differs from what the originator signed, and every signed packet with a compressible payload (≥ 100 bytes, < 90 % distinct) from another encoder fails verification here although its signature is valid over the bytes that were sent. bitchat-android fixed its side in permissionlesstech/bitchat-android#863; this is the same change for the Swift codec: BitchatPacket.wirePayload (received bytes + compressed flag + SHA-256 of the payload they decode to), filled by decode, reused by encode while it still matches the payload, carried into the signing preimage. A locally built packet has nil and encodes exactly as before; a payload changed after decode is compressed afresh. wirePayload is excluded from Codable. No public signature changes.

    Retaining the wire bytes has a cost the stores must see: a valid DEFLATE stream can be far larger than the payload it inflates to (stored blocks; many empty blocks before a one-byte final block), and GossipSyncManager.PacketStore, its archive restore and BLEDirectedRelaySpool charged payload.count against their byte budgets. They now charge BitchatPacket.retainedPayloadBytes = max(payload.count, wirePayload?.bytes.count ?? 0), which equals payload.count for every frame a reference encoder emits (compressed bytes are always smaller than the payload) and the stream size for a crafted one. Tests: retainedBytesCountTheWireStreamWhenItIsLarger (BitFoundation), enqueueChargesRetainedWireBytesNotJustThePayload (spool), messageStoreChargesRetainedWireBytes (gossip).

  2. Byte-exact wire-format vectors shared with bitchat-android. bitchat-ios.json holds 181 cases produced by this codebase's own encoders at 30c58eee (1.8.0), plus two replay tests that assert BinaryProtocol, MessagePadding, CompressionUtil, PeerID, PeerCapabilities, AnnouncementPacket, AuthenticatedPeerStatePacket, PrivateMessagePacket, NoisePayload, BitchatFilePacket and BLEOutboundFragmentPlanner still emit and accept exactly these bytes. The same file is replayed by the Android client (permissionlesstech/bitchat-android#994) and by a third-party Rust implementation, so the three stay interoperable by construction: a change to the wire format now fails a unit test on every platform and has to ship with regenerated vectors instead of being discovered in the field. The vectors also pin receive-side behaviour that has no other test today (trailing bytes accepted after a frame, HAS_ROUTE ignored on v1, PrivateMessagePacket rejecting unknown TLVs, BitchatFilePacket rejecting its own encoding of empty content, non-minimal capability encodings rejected by AuthenticatedPeerStatePacket).

  3. A real bitchat-android frame pins the fix (BinaryProtocolWirePayloadTests.stockAndroidFrameKeepsTheAndroidSigningPreimage). The payload "A" followed by 33 x "B", repeated to 300 bytes, compresses differently under java.util.zip.Deflater (zlib level 6, Android) and Apple's COMPRESSION_ZLIB (level 5). The test decodes the signed v1 frame bitchat-android actually emits for it (case 10_v1_300_a_b33_sig22 in #994's vector file) and asserts that the signing preimage is Android's bytes. The same assertion fails on main @ 30c58eee: that is the stock-iOS verification failure against stock Android, reproduced with a native Android frame instead of a hand-built stored block. The vector file gains the matching iOS-side cases compress_a_b33_300 and v1_a_b33_300_signed.

Companion PR: permissionlesstech/bitchat-android#994 replays the Android codec's own vectors in the same file format.

How the vectors work

  • One JSON file, two copies (localPackages/BitFoundation/Tests/BitFoundationTests/ and bitchatTests/Protocols/), because the app-target types are not visible from the package. Each test resolves the file via SONAR_VECTOR_FILE or next to its own source (#filePath) and skips rather than fails when neither exists.
  • Byte fields are lowercase hex; a run of one byte repeated 1024+ times is written repeat:xx*N (e.g. repeat:00*65535). null means "encoder returned nil".
  • compress cases are compared byte-for-byte against libcompression's raw DEFLATE and additionally assert the inflate round-trip (the real interop contract). If a future libcompression changes its output, the test comment says where to relax to the round-trip and regenerate.
  • everyKindInTheFileIsReplayedSomewhere fails if the file grows a kind neither test knows about. Each test prints one REPLAY kind=… cases=… line.

Verification (clean checkout of main @ 30c58eee, Xcode 27 / Swift 6.4)

StepResult
swift test in localPackages/BitFoundation207 tests in 16 suites passed (188 before; +8 BinaryProtocolWirePayloadTests, +11 BitchatVectorReplayTests, 101 cases)
xcodebuild test -scheme "bitchat (iOS)" -only-testing:bitchatTests_iOS/BitchatAppVectorReplayTests (iPhone 17 Pro simulator)7 tests, 80 cases, 0 failures
stockAndroidFrameKeepsTheAndroidSigningPreimage on unpatched main @ 30c58eeefails: the re-compressed preimage is not the one Android signed
swiftlint lint --strict (ghcr.io/realm/swiftlint:0.65.0, the CI image) over the changed files0 violations (SwiftLint 0.65.1 from Homebrew; the pinned 0.65.0 container image would not pull here)
Mutation checkfive expected values corrupted in a copy of the JSON produced five failures

The replay passes with and without commit 1, as expected: the fix only changes how decoded packets are re-encoded, and no vector encodes a decoded packet.

Notes for reviewers

  • WirePayload.payloadSHA256 is what ties the stored bytes to the payload; BinaryProtocol.encode falls back to fresh compression the moment the payload no longer matches, so the var payload mutation paths in the app keep working.
  • wirePayload is deliberately outside Equatable/Codable: two packets with the same fields are the same packet whether or not one remembers its wire bytes.
  • The vectors are generated, not hand-written: a small Swift Testing dump is installed into a checkout of this repository at the pinned revision and run with swift test; the fixture's note field names the script and the generator's location. The generator can move in-tree too if you prefer a self-contained regeneration path.

🤖 Generated with Claude Code

Activity

7
chatgpt-codex-connector[bot]6d ago

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code Review✅ Completed 2026-10-26T11:55:09.863538Z004b978New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

chatgpt-codex-connector[bot]reviewed6d ago
chatgpt-codex-connector[bot]6d ago

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e59747cbda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

vincenzopalazzoreviewed5d ago
chatgpt-codex-connector[bot]reviewed4d ago
chatgpt-codex-connector[bot]4d ago

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 004b9787c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".