diff --git a/spec/01-wire-format.md b/spec/01-wire-format.md index 43ce9da0..e2e3d8a3 100644 --- a/spec/01-wire-format.md +++ b/spec/01-wire-format.md @@ -79,13 +79,15 @@ When `hasSignature` is set, a 64-byte Ed25519 signature follows the payload sect ## 5. Signing -The signature, when present, is computed over the packet's encoded bytes with two substitutions: the `signature` section itself is omitted, and `ttl` is fixed to `0` regardless of the packet's actual TTL. `ttl` is excluded because a relay decrementing it in place would otherwise invalidate every signed packet it forwards. `isRSR` is likewise excluded, being set after the packet is signed. +The signature, when present, is computed over the packet's encoded bytes with three substitutions: the `signature` section itself is omitted, `ttl` is fixed to `0` regardless of the packet's actual TTL, and the frame is always padded per [Padding](#6-padding) — even for a message type that §6 transmits unpadded. `ttl` is excluded because a relay decrementing it in place would otherwise invalidate every signed packet it forwards. `isRSR` is likewise excluded, being set after the packet is signed. -A verifier MUST reconstruct the same fixed-TTL, signature-omitted, RSR-omitted frame before checking a signature against it. +A verifier MUST reconstruct the same fixed-TTL, signature-omitted, RSR-omitted, **padded** frame before checking a signature against it, regardless of whether the packet as received on the wire carried padding. ## 6. Padding -Only `noiseHandshake` and `noiseEncrypted` packets are padded; every other message type is encoded at its natural length. Padding is applied to the full encoded frame (header through payload, before the signature section) and is PKCS#7-style: the pad length is appended as that many bytes, each byte equal to the pad length itself. +This section defines the padding algorithm and states which message types carry padding **on the wire**. The signing transcript is a separate case: it is always padded by this same algorithm regardless of message type (see [Signing](#5-signing)), because the signature is computed before the type-dependent choice of whether to pad the transmitted frame is applied. + +Only `noiseHandshake` and `noiseEncrypted` packets are padded on the wire; every other message type is transmitted at its natural length. Padding is applied to the full encoded frame (header through payload, before the signature section) and is PKCS#7-style: the pad length is appended as that many bytes, each byte equal to the pad length itself. Padding targets the smallest of the block sizes `256`, `512`, `1024`, `2048` bytes that the frame (plus a 16-byte allowance for encryption overhead) fits into. Because the pad length must fit in a single byte, a frame that would need more than 255 bytes of padding to reach its target block is emitted **unpadded** instead of padded to a smaller-than-optimal bucket. A decoder MUST attempt to decode a frame as unpadded first, and only on failure retry after stripping trailing PKCS#7 padding. diff --git a/spec/02-ble-transport.md b/spec/02-ble-transport.md index 067428e4..cb2662bf 100644 --- a/spec/02-ble-transport.md +++ b/spec/02-ble-transport.md @@ -42,7 +42,9 @@ A packet whose encoded size exceeds the link MTU MUST be split into `fragment` ( ### 3.2 Fragment Cap and Lifetime -A sender MUST NOT split a packet into more than 256 fragments; a receiver MAY reject a fragment stream whose `total` exceeds this cap. This ceiling exists because it is a cross-platform contract, not an arbitrary internal limit — implementations that accept larger fragment counts still MUST NOT rely on peers doing the same. +A receiver MUST reject a fragment stream whose `total` exceeds 10,000; this is the general reassembly ceiling and applies regardless of the fragmented packet's type. + +A stricter cap applies to one case: a directed `fileTransfer (0x22)` packet (the legacy migration fallback for private media, used when the recipient has not advertised the `privateMedia` capability — see the Payloads chapter's [§5](04-payloads.md#5-peer-state-and-capabilities)) MUST NOT be split into more than 256 fragments, and a receiver MAY reject such a stream if `total` exceeds that lower cap. This exists as a cross-platform contract with clients whose reassembler enforces a 256-fragment ceiling on that path specifically; implementations that accept larger fragment counts on it still MUST NOT rely on peers doing the same. Public files and capability-gated encrypted private media (carried as `noiseEncrypted` fragments to a peer advertising `privateMedia`) are bound only by the general 10,000-fragment ceiling above. A receiving node reassembles fragments for a stream by collecting them keyed by `(sender, fragmentID)` until the number received equals `total`, then concatenates them in `index` order to recover the original packet's encoded bytes, which are then decoded per the Wire Format chapter using `originalType`. A stream with no new fragment arriving for 30 seconds is considered stalled; a node SHOULD request the missing fragments (see the Store and Forward chapter's sync mechanism) rather than discarding the stream outright. diff --git a/spec/03-noise.md b/spec/03-noise.md index 953de85b..0f80b2f0 100644 --- a/spec/03-noise.md +++ b/spec/03-noise.md @@ -117,6 +117,15 @@ A sender who instead holds one of the recipient's published one-time `prekey`s s A prekey MUST NOT be reused across more than one seal; once consumed, it MUST be discarded from future `prekeyBundle`s ([§5](#5-prekey-bundles)). +### 4.4 Domain Separation (Prologue) + +Both seal types in this section use Noise's `prologue` mechanism — arbitrary bytes mixed into the handshake hash via `MixHash` before the first handshake message, per the Noise specification — to keep an `X`-pattern transcript from ever being confused with the `XX` handshake ([§2](#2-live-sessions-the-xx-pattern), which uses the default empty prologue) or with the other seal type. Because the prologue is mixed into the handshake hash, it participates in every subsequent key derivation; sealing and opening a given envelope MUST use bit-identical prologue bytes or the handshake fails to authenticate. + +| Seal type | Prologue bytes | +|---|---| +| Courier envelope ([§4.2](#42-courier-envelopes), sealed to the recipient's static key) | ASCII `bitchat-courier-v1` (18 bytes) | +| Prekey envelope ([§4.3](#43-prekey-envelopes), sealed to a one-time prekey) | ASCII `bitchat-prekey-v1` (17 bytes) followed by the 4-byte big-endian `prekeyID` ([§5.2.1](#521-prekey-entry)) of the prekey being sealed against | + ## 5. Prekey Bundles A `prekey bundle` is how a device publishes a batch of one-time prekeys for other peers to seal [§4.3](#43-prekey-envelopes) envelopes against. @@ -132,7 +141,7 @@ A prekey bundle is carried as the payload of a `prekeyBundle (0x24)` packet (see | 0x01 | `noiseStaticPublicKey` | 32 | The issuing peer's long-term Curve25519 static public key, included so a recipient can validate the bundle's signature ([§5.3](#53-signature)) without a separate lookup. | | 0x02 | `prekeys` | variable | Repeated fixed-size entries (see [§5.2.1](#521-prekey-entry)). A sender MUST NOT include more than 8 entries in a single bundle. | | 0x03 | `generatedAt` | 8 | Milliseconds since the Unix epoch at which the bundle was generated. | -| 0x04 | `signature` | 64 | Ed25519 signature over the preceding fields, using the issuing peer's `signing key`. | +| 0x04 | `signature` | 64 | Ed25519 signature over the canonical transcript defined in [§5.3](#53-signature), using the issuing peer's `signing key`. | #### 5.2.1 Prekey Entry @@ -145,4 +154,15 @@ Each entry in the `prekeys` field is 36 bytes, with no further framing between c ### 5.3 Signature -A recipient MUST verify the `signature` field against the issuing peer's known `signing key` before trusting any prekey the bundle carries, and MUST discard the bundle if verification fails. +The `signature` field is computed over a canonical byte transcript distinct from the bundle's TLV encoding — not the raw concatenation of the preceding fields' TLV bytes. An encoder or verifier MUST construct this transcript as follows, in order: + +| Bytes | Contents | +|---|---| +| 1 | Length of the domain string below, as an unsigned 8-bit integer: `24`. | +| 24 | ASCII domain string `bitchat-prekey-bundle-v1`. | +| 32 | `noiseStaticPublicKey` ([§5.2](#52-fields)), raw. | +| 1 | Number of `prekeys` entries, as an unsigned 8-bit integer. | +| 36 × count | Each `prekeys` entry ([§5.2.1](#521-prekey-entry)) in order: 4-byte big-endian `prekeyID` followed by the 32-byte raw `publicKey` — the same layout as the wire encoding, not separately TLV-framed. | +| 8 | `generatedAt` ([§5.2](#52-fields)), big-endian. | + +A recipient MUST verify the `signature` field against this transcript, using the issuing peer's known `signing key`, before trusting any prekey the bundle carries, and MUST discard the bundle if verification fails. diff --git a/spec/04-payloads.md b/spec/04-payloads.md index 9844986f..603b5834 100644 --- a/spec/04-payloads.md +++ b/spec/04-payloads.md @@ -105,9 +105,11 @@ A decoder MUST reject a payload whose `kind` is absent or unrecognized, or that ### 6.1 Signing -A **post**'s `signature` covers, concatenated in this order: the ASCII context string `bitchat-board-v1`; `postID`; `geohash` and `content` each preceded by their own 2-byte big-endian length; `authorSigningKey`; `authorNickname` preceded by its 2-byte big-endian length; `createdAt`; `expiresAt`; and `flags`. +Both kinds' `signature` open with their ASCII context string preceded by its own 1-byte length (unlike the length-prefixed fields later in the transcript, this length is a single byte, not 2-byte big-endian) — `0x10` (16) + `bitchat-board-v1` for a post, `0x14` (20) + `bitchat-board-del-v1` for a tombstone. -A **tombstone**'s `signature` covers: the ASCII context string `bitchat-board-del-v1`, `postID`, and `deletedAt`. Only the original post's `authorSigningKey` can produce a valid tombstone for it. +A **post**'s `signature` covers, concatenated in this order: the length-prefixed context string `bitchat-board-v1`; `postID`; `geohash` and `content` each preceded by their own 2-byte big-endian length; `authorSigningKey`; `authorNickname` preceded by its 2-byte big-endian length; `createdAt`; `expiresAt`; and `flags`. + +A **tombstone**'s `signature` covers: the length-prefixed context string `bitchat-board-del-v1`, `postID`, and `deletedAt`. Only the original post's `authorSigningKey` can produce a valid tombstone for it. ## 7. Private Groups diff --git a/spec/07-conformance.md b/spec/07-conformance.md index 32c2f41b..ac88d714 100644 --- a/spec/07-conformance.md +++ b/spec/07-conformance.md @@ -16,8 +16,8 @@ Chapter 6's items are split by the REQUIRED/capability-gated boundary that chapt - [ ] A source route is a 1-byte hop count `N` followed by `N` 8-byte peer IDs; `N` MUST NOT exceed 255, and route bytes are excluded from `payloadLength` ([§4.2](01-wire-format.md#42-source-route)). - [ ] When `isCompressed` is set, the payload begins with a 2-byte (v1) or 4-byte (v2) big-endian original-size preamble, itself counted in `payloadLength` ([§4.3](01-wire-format.md#43-payload-and-compression)). - [ ] A signature, when `hasSignature` is set, is a 64-byte Ed25519 signature immediately following the payload ([§4.4](01-wire-format.md#44-signature)). -- [ ] A signature is computed over the packet with its `signature` section omitted, `ttl` fixed to `0`, and `isRSR` excluded; a verifier MUST reconstruct this exact frame ([§5](01-wire-format.md#5-signing)). -- [ ] Only `noiseHandshake`/`noiseEncrypted` packets are padded, using PKCS#7-style padding over header-through-payload targeting the smallest of the `256/512/1024/2048`-byte buckets ([§6](01-wire-format.md#6-padding)). +- [ ] A signature is computed over the packet with its `signature` section omitted, `ttl` fixed to `0`, `isRSR` excluded, and the frame always padded per §6 regardless of the message type's wire padding rule; a verifier MUST reconstruct this exact frame ([§5](01-wire-format.md#5-signing)). +- [ ] Only `noiseHandshake`/`noiseEncrypted` packets are padded **on the wire**; every other type is transmitted at its natural length. This does not apply to the signing transcript, which is always padded (see the item above) ([§6](01-wire-format.md#6-padding)). - [ ] A frame needing more than 255 bytes of padding to reach its target bucket MUST be emitted unpadded instead ([§6](01-wire-format.md#6-padding)). - [ ] A decoder MUST attempt unpadded decode first and retry with PKCS#7 stripping only on failure ([§6](01-wire-format.md#6-padding)). - [ ] The full `type` byte table MUST be supported for dispatch, and a decoder MUST skip (not reject the enclosing packet for) an unrecognized `type` ([§7](01-wire-format.md#7-message-types)). @@ -28,7 +28,7 @@ Chapter 6's items are split by the REQUIRED/capability-gated boundary that chapt - [ ] The service UUID, characteristic UUID, and characteristic properties (notify, write, write-without-response, read) match the defined values, with a single characteristic carrying traffic in both directions ([§1](02-ble-transport.md#1-gatt-service-and-characteristic)). - [ ] A packet exceeding the link MTU MUST be split into `fragment (0x20)` packets ([§3](02-ble-transport.md#3-fragmentation)). - [ ] The fragment header layout — `fragmentID`(8B), `index`(2B BE), `total`(2B BE), `originalType`(1B), then `fragmentData` — is a 13-byte fixed prefix ([§3.1](02-ble-transport.md#31-fragment-header)). -- [ ] A sender MUST NOT split a packet into more than 256 fragments; a receiver MAY reject a stream whose `total` exceeds this ([§3.2](02-ble-transport.md#32-fragment-cap-and-lifetime)). +- [ ] A receiver MUST reject a fragment stream whose `total` exceeds 10,000, regardless of type; a directed `fileTransfer (0x22)` packet (the legacy Android-compatible private-media fallback) is further capped at 256 fragments, MAY be rejected above that by a receiver, and MUST NOT be split above it by a sender ([§3.2](02-ble-transport.md#32-fragment-cap-and-lifetime)). - [ ] Reassembly is keyed by `(sender, fragmentID)` and concatenates fragments in `index` order ([§3.2](02-ble-transport.md#32-fragment-cap-and-lifetime)). - [ ] A v2 source-routed packet's fragments MAY carry the same route, with per-fragment chunk size MAY shrinking, floored at 64 bytes ([§3.3](02-ble-transport.md#33-route-aware-fragmentation-v2)). - [ ] An advertisement (and scan response) MUST carry only the service UUID — MUST NOT carry local name, TX power, peer ID, or any other peer-identifying bytes ([§4.1](02-ble-transport.md#41-advertisement-contents)). @@ -49,7 +49,9 @@ Chapter 6's items are split by the REQUIRED/capability-gated boundary that chapt - [ ] The `X` pattern's single message is `-> e, es, s, ss`, with `e` cleartext (32B) and `es,s,ss` ciphertext+tag (48B) ([§4.1](03-noise.md#41-handshake-message)). - [ ] A courier envelope seal uses the recipient's long-term static key, with no forward secrecy ([§4.2](03-noise.md#42-courier-envelopes)). - [ ] A prekey MUST NOT be reused across more than one seal, and MUST be discarded from future bundles once consumed ([§4.3](03-noise.md#43-prekey-envelopes)). +- [ ] The `X` pattern's prologue is non-empty and depends on the seal: ASCII `bitchat-courier-v1` for a courier envelope, ASCII `bitchat-prekey-v1` plus the 4-byte big-endian `prekeyID` for a prekey envelope; sealing and opening a given envelope MUST use bit-identical prologue bytes ([§4.4](03-noise.md#44-domain-separation-prologue)). - [ ] The `prekeyBundle (0x24)` packet uses TLV-16 framing, with fields `noiseStaticPublicKey`(32B), `prekeys`(repeated 36B entries, MUST NOT exceed 8), `generatedAt`(8B), `signature`(64B Ed25519) ([§5.1](03-noise.md#51-wire-packet), [§5.2](03-noise.md#52-fields)). +- [ ] A prekey bundle's `signature` covers the canonical transcript — 1-byte length + 24-byte domain string `bitchat-prekey-bundle-v1`, `noiseStaticPublicKey`, a 1-byte prekey count, each raw `(prekeyID, publicKey)` entry, then `generatedAt` — not the bundle's TLV encoding ([§5.3](03-noise.md#53-signature)). - [ ] A recipient MUST verify a prekey bundle's `signature` against the issuer's known signing key before trusting any prekey, and MUST discard the bundle on verification failure ([§5.3](03-noise.md#53-signature)). ## 4. Payloads @@ -61,7 +63,7 @@ Chapter 6's items are split by the REQUIRED/capability-gated boundary that chapt - [ ] `PeerCapabilities` is little-endian and minimal-byte-length (trailing zero bytes stripped); a decoder keeps only the low 64 bits so unknown high bits round-trip ([§5.1](04-payloads.md#51-peercapabilities-bitfield)). - [ ] The full `PeerCapabilities` bit table MUST be supported for dispatch, bit 10 (`nonDestructiveNoiseReplacement`) MUST never be advertised by a conforming encoder, and bits 11-63 MUST be `0` on encode while a decoder MUST preserve (not reject on) an unrecognized set bit ([§5.1](04-payloads.md#51-peercapabilities-bitfield)). - [ ] `boardPost (0x23)` is TLV-16; a decoder MUST reject an absent/unrecognized `kind` or a payload missing a field its `kind` requires, and `expiresAt` MUST NOT exceed `createdAt` + 7 days ([§6](04-payloads.md#6-board-posts)). -- [ ] Board post signatures use context strings `bitchat-board-v1` (post) / `bitchat-board-del-v1` (tombstone) over the defined field concatenation ([§6.1](04-payloads.md#61-signing)). +- [ ] Board post signatures open with a 1-byte length + context string (`bitchat-board-v1` post / `bitchat-board-del-v1` tombstone), not the bare context string, followed by the defined field concatenation ([§6.1](04-payloads.md#61-signing)). - [ ] `groupInvite (0x06)`/`groupKeyUpdate (0x07)` share the `GroupStatePayload` TLV-16 shape; a receiver MUST require the delivering session peer be the group's creator ([§7](04-payloads.md#7-private-groups)). - [ ] A private group MUST NOT exceed 16 members; a decoder MUST reject more than 16 roster entries or a `creatorFingerprint` absent from the roster ([§7.2](04-payloads.md#72-roster-encoding)). - [ ] Group state signatures use context `bitchat-group-v1`, verified against the roster member matching `creatorFingerprint`; a receiver MUST reject an absent or invalid signature ([§7.1](04-payloads.md#71-signing)).