From 54b78b9ca756f78c3b252e0705d8b6470d23e84d Mon Sep 17 00:00:00 2001 From: Amirhossein Rezaei Date: Mon, 27 Jul 2026 16:19:54 +0330 Subject: [PATCH] docs(spec): address review notes on signing, fragments, and TLVs Clarify that signature preimages use padded BinaryProtocol.encode; dispatch reassembled fragments by decoded type; and narrow unknown-TLV skip to the families that actually tolerate it. Bump spec to 1.0.1. --- spec/01-wire-format.md | 39 ++++++++++++++++++++++++++++++--------- spec/02-ble-transport.md | 14 ++++++++++++-- spec/03-noise.md | 2 +- spec/04-payloads.md | 25 +++++++++++++++++++++++-- spec/05-nostr-bridge.md | 2 +- spec/README.md | 9 +++++---- spec/VERSION | 2 +- spec/conformance.md | 7 +++++-- 8 files changed, 78 insertions(+), 22 deletions(-) diff --git a/spec/01-wire-format.md b/spec/01-wire-format.md index 88e811ad..a277601a 100644 --- a/spec/01-wire-format.md +++ b/spec/01-wire-format.md @@ -1,6 +1,6 @@ # 01 — Wire Format -**Spec:** 1.0.0 +**Spec:** 1.0.1 **Canonical source:** `localPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift` All multi-byte integers on the mesh wire are **network byte order (big-endian)** @@ -127,13 +127,20 @@ Decompression: `MessagePadding` buckets: **256, 512, 1024, 2048**. - Pad bytes are all equal to the pad length (1…255). -- If more than 255 pad bytes would be required to reach the next bucket, the - frame is left **unpadded**. +- Bucket selection (`optimalBlockSize`): choose the smallest bucket such that + `encodedSize + 16 ≤ bucket` (the `+16` accounts for AEAD tag headroom used by + the helper even when the frame is not a Noise ciphertext). If no bucket fits, + or more than 255 pad bytes would be required, the frame is left **unpadded**. - Decode tries the buffer as-is, then strips padding and retries. -**Only** `noiseHandshake` and `noiseEncrypted` frames are padded on the BLE -outbound path. All other types travel at natural length — payload length is -observable for those types. +Two different call sites use this helper — do not conflate them: + +| Path | Padding? | +|------|----------| +| **BLE outbound encode** (`padsBLEFrame`) | **Only** `noiseHandshake` / `noiseEncrypted`. All other types travel at natural length on the air (payload length observable). | +| **Packet signature preimage** (`toBinaryDataForSigning` → `BinaryProtocol.encode` default `padding: true`) | Padding **MAY** be present for *any* signed type (including announces). Verifiers **MUST** use the same padded canonical bytes. | + +See §9 for the signature preimage rules. --- @@ -187,12 +194,24 @@ packet signatures; they are distinct from the Noise static key. ## 9. Packet signatures - Algorithm: **Ed25519** (`Curve25519.Signing`), 64-byte signature. -- Canonical bytes: encode the packet with `signature = nil`, `ttl = 0`, - `isRSR = false` (TTL and RSR are mutable in flight and excluded). +- Canonical preimage (`BitchatPacket.toBinaryDataForSigning()`): + 1. Copy the packet with `signature = nil`, `ttl = 0`, `isRSR = false` + (TTL and RSR are mutable in flight and excluded from the signed bytes). + 2. Encode with `BinaryProtocol.encode(..., padding: true)` — the **default**. + Apply §6 PKCS#7-style padding / bucket selection to that encoding. + 3. Sign or verify those exact bytes. An unpadded encode of the same fields + **will not** verify against reference-client signatures whenever padding + was applied (common for compact announces that land in the 256-byte bucket). - Relays **MUST** decrement TTL without recomputing the signature. - Announces, leaves, public file transfers, and other authenticated public types set `hasSignature` when the reference clients emit them. +BLE air frames for non-Noise types are often sent **without** this padding +(§6 table). That is independent of the signature preimage: the signature +covers the padded canonical encoding, then the on-air frame for that type may +omit pad bytes. Verifiers rebuild the preimage themselves; they do not require +the received ATT blob to still carry the pad. + Optional helper `bitchat-announce-v1` binding bytes exist in the Noise service for nickname/key binding tests; live mesh announces sign the **full packet** canonical form above, verified against the Ed25519 key carried in the announce @@ -214,7 +233,9 @@ TLV payload. ## 11. Implementer checklist - [ ] Round-trip v1 packet with no recipient, no signature, empty payload. -- [ ] Round-trip v1 with recipient + signature. +- [ ] Round-trip v1 with recipient + signature using **padded** canonical bytes. +- [ ] Confirm an unpadded announce preimage fails verification against a + reference signature when padding would have applied. - [ ] Round-trip v2 with route hops; confirm route bytes are outside `payloadLength`. - [ ] Compress a low-entropy >100 B payload; confirm preamble + flag. - [ ] Reject version `0x00` / `0x03`. diff --git a/spec/02-ble-transport.md b/spec/02-ble-transport.md index 96c0014a..2061deef 100644 --- a/spec/02-ble-transport.md +++ b/spec/02-ble-transport.md @@ -1,6 +1,6 @@ # 02 — BLE Transport -**Spec:** 1.0.0 +**Spec:** 1.0.1 **Canonical source:** `bitchat/Services/BLE/BLEService.swift`, `BLEOutboundFragmentPlanner.swift`, `BLEFragmentAssemblyBuffer.swift`, `TransportConfig.swift` @@ -149,7 +149,16 @@ Validation: - Oversize fragments **MUST NOT** destroy an assembly they did not create On completion, concatenate chunks in index order and run `BinaryProtocol.decode` -on the result; then dispatch as `originalType`. +on the result. Dispatch using the **decoded inner packet's `type` field** +(`originalPacket.type`), then re-enter the normal receive path with that +packet (reference: `BLEFragmentHandler` sets `ttl = 0` and reinjects). + +The fragment-header `originalType` byte is **not authenticated** and **MUST +NOT** be used as the sole dispatch key. Implementations **SHOULD** require +`originalType == decoded.type` and drop the assembly on mismatch; at minimum +they **MUST** ignore the header value for parser/policy selection and follow +the decoded type so a spoofed header cannot steer valid inner bytes into the +wrong handler. --- @@ -191,4 +200,5 @@ interoperability timing): - [ ] Reassemble ATT notifications into BinaryProtocol frames before parsing. - [ ] Fragment at ≤469 B chunks with the 13-byte fragment header. - [ ] Cap assemblies (128 / 30 s) and reject conflicting fragment metadata. +- [ ] After reassembly, dispatch by decoded packet type (not header `originalType`). - [ ] Originate mesh packets with TTL 7 unless a documented exception applies. diff --git a/spec/03-noise.md b/spec/03-noise.md index d9fa5a68..fd335e46 100644 --- a/spec/03-noise.md +++ b/spec/03-noise.md @@ -1,6 +1,6 @@ # 03 — Noise Handshake and Encrypted Transport -**Spec:** 1.0.0 +**Spec:** 1.0.1 **Canonical source:** `bitchat/Noise/NoiseProtocol.swift`, `NoiseSession.swift`, `NoiseEncryptionService.swift`, `bitchat/Protocols/BitchatProtocol.swift` (`NoisePayloadType`) diff --git a/spec/04-payloads.md b/spec/04-payloads.md index 122a9612..bc8d961b 100644 --- a/spec/04-payloads.md +++ b/spec/04-payloads.md @@ -1,6 +1,6 @@ # 04 — Payload Layouts -**Spec:** 1.0.0 +**Spec:** 1.0.1 TLV conventions differ by packet family — do not mix length widths. @@ -10,7 +10,23 @@ TLV conventions differ by packet family — do not mix length widths. | Courier envelope / prekey bundle | `uint16` BE | | File transfer content TLV | `uint32` BE (canonical); other file TLVs `uint16` BE | -Unknown TLV types **MUST** be skipped (forward compatibility). +### Unknown-TLV policy (do not over-promise) + +Forward-compatible **skip** of unknown TLV types applies only where the +reference decoder actually skips: + +| Family | Unknown TLV behaviour | +|--------|------------------------| +| Announce | **Skip** and continue | +| Authenticated peer state | **Skip** and continue | +| Courier envelope | **Skip** and continue | +| Prekey bundle | **Skip** and continue | +| File transfer | **Skip** and continue | +| Private message (`PrivateMessagePacket`) | **Reject** entire payload (`nil`) | + +Adding a new private-message TLV under a SemVer **minor** bump would break +current reference DM decoders. Treat private-message TLV extensions as a +**MAJOR** wire change (or change the decoder first), not as a silent skip. --- @@ -197,6 +213,10 @@ TLV `[type:u8][len:u8][value]`: Prefixed by `NoisePayloadType.privateMessage` (`0x01`) when inside Noise. +Unlike announce/courier TLVs, any unknown type byte causes +`PrivateMessagePacket.decode` to return `nil` immediately (no skip). Both +`messageID` and `content` are required. + --- ## 10. Noise inner: authenticated peer state @@ -235,3 +255,4 @@ the encode/decode in: - [ ] File content TLV uses 4-byte length; tolerate legacy widths on decode. - [ ] Prekey bundle signature verifies over domain-prefixed signable bytes. - [ ] Private DM content is Noise-typed, not a public `0x02` packet. +- [ ] Confirm unknown private-message TLVs reject; unknown announce TLVs skip. diff --git a/spec/05-nostr-bridge.md b/spec/05-nostr-bridge.md index 4a856e12..56534da2 100644 --- a/spec/05-nostr-bridge.md +++ b/spec/05-nostr-bridge.md @@ -1,6 +1,6 @@ # 05 — Nostr Bridge -**Spec:** 1.0.0 +**Spec:** 1.0.1 **Canonical prose:** `WHITEPAPER.md` §5.3, §6.4; `README.md`; `docs/GeohashPresenceSpec.md` diff --git a/spec/README.md b/spec/README.md index 28b30ae6..82c779d0 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,6 +1,6 @@ # BitChat Protocol Specification -**Spec version:** [`1.0.0`](VERSION) +**Spec version:** [`1.0.1`](VERSION) **Status:** Draft extracted from the reference implementation **Canonical codec:** `localPackages/BitFoundation` **Architecture overview:** [`WHITEPAPER.md`](../WHITEPAPER.md) @@ -39,9 +39,10 @@ wire types) as authoritative until this document is amended. Open a PR against - **PATCH** — clarifications, errata, conformance notes with no wire change. - Spec version is **independent** of App Store / Android release numbers. -Unknown TLV types and unknown high capability bits **MUST** be skipped so -older clients can carry newer packets opaquely where the outer type is already -understood. +Unknown TLV types and unknown high capability bits are handled +per-family: most public TLV decoders skip unknowns so older clients can carry +newer packets opaquely, but some inner payloads (notably private-message TLVs) +reject unknowns — see [`04-payloads.md`](04-payloads.md). ## Suggested reading order for implementers diff --git a/spec/VERSION b/spec/VERSION index 3eefcb9d..7dea76ed 100644 --- a/spec/VERSION +++ b/spec/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 diff --git a/spec/conformance.md b/spec/conformance.md index 01374e4f..378a7c1e 100644 --- a/spec/conformance.md +++ b/spec/conformance.md @@ -1,6 +1,6 @@ # Conformance -**Spec:** 1.0.0 +**Spec:** 1.0.1 This file is a living checklist. Golden hex vectors for every mesh type are a planned follow-up (see issue @@ -40,7 +40,8 @@ just test - [ ] Encode/decode v1 and v2 packets with correct endianness. - [ ] Honor optional recipient, signature, compression, route flags. -- [ ] Exclude TTL/`isRSR` from signature canonicalization. +- [ ] Exclude TTL/`isRSR` from signature canonicalization; include PKCS#7 + padding from `encode(padding: true)` in the preimage. - [ ] Derive 8-byte peer IDs as `SHA256(noiseStatic)[0..<8]`. ### BLE @@ -48,6 +49,7 @@ just test - [ ] Use release GATT service UUID `…4B5C` and characteristic `…4C5D`. - [ ] Advertise service UUID only (no local name). - [ ] Fragment with 13-byte header; reassemble with first-wins metadata. +- [ ] Dispatch reassembled packets by decoded type, not header `originalType`. - [ ] Default origination TTL = 7. ### Noise @@ -64,6 +66,7 @@ just test - [ ] Capability bitfield little-endian. - [ ] File TLV content length 4-byte BE; private files via Noise `0x20`. - [ ] Courier recipient tag HMAC construction. +- [ ] Unknown-TLV skip only where listed; private-message unknown tags reject. ### Nostr