diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f936f..6579ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +This project follows a loose [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) +convention. Released versions live as frozen snapshots under `versions/vX.Y.Z/` +in the repo; the rolling tip of `main` is what `latest` deploys. + +## [Unreleased] + +_(nothing yet)_ + +## [v0.1.0] — 2026-05-12 + +First cut release of the spec. Snapshotted as `versions/v0.1.0/`, deployed +at . + +The dated entries below describe the substantive passes whose output is +captured in this release. Future releases will follow the standard +Added / Changed / Fixed / Removed grouping under their version heading. + +--- + ## 2026-05-12 — Publication-Readiness Pass **Upstream baseline:** Validated against diff --git a/README.md b/README.md index 4c19f2c..baaef35 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ A comprehensive specification and conformance test suite for the [MeshCore](https://github.com/meshcore-dev/MeshCore) mesh networking protocol. **📖 Read the spec online:** -(rolling `latest` build, plus a frozen snapshot per `vX.Y.Z` tag — pick one -from the version-picker in the top-right of any page) +(rolling `latest` from `main`, plus a frozen snapshot per release — +current: [v0.1.0](https://swaits.github.io/meshcore-spec/v0.1.0/). Use +the version-picker in the top-right of any page to switch.) MeshCore Spec aims to be the authoritative, machine-readable definition of the MeshCore wire format — much like [RubySpec](https://github.com/ruby/spec) became diff --git a/versions/v0.1.0/00-overview.md b/versions/v0.1.0/00-overview.md new file mode 100644 index 0000000..39294af --- /dev/null +++ b/versions/v0.1.0/00-overview.md @@ -0,0 +1,186 @@ +# MeshCore Protocol Specification + +## Section 0: Overview + +### Status and Provenance + +> **This is an unofficial, independent specification.** It is **not** part of +> the upstream [MeshCore project](https://github.com/meshcore-dev/MeshCore), +> not affiliated with or endorsed by its maintainers, and not authored by +> them. Read this section before relying on the spec for anything +> load-bearing. + +This document was primarily written by an AI (Claude, by Anthropic — mostly +the Opus 4.x family) under the direction of Stephen Waits, with human +proofreading and editing on top. Substantive edits — additions, audit passes, +errata fixes — are produced through model-driven analysis of the upstream +MeshCore C++ source and reviewed before commit. + +The **upstream MeshCore C++ firmware is the source of truth** for all +protocol behavior. This spec is downstream and derivative. Whenever the spec +and the upstream C++ disagree, **upstream wins** — and a fix to this spec is +filed against this repo, not against MeshCore. Each substantive change in +[CHANGELOG.md](https://github.com/swaits/meshcore-spec/blob/main/CHANGELOG.md) +records the upstream commit hash it was validated against. + +Refinements have also been driven in part by observations from building +DongLoRa firmware and apps against live MeshCore deployments. Where DongLoRa +behavior diverges from upstream, this spec follows upstream and treats the +DongLoRa side as a follow-up bug, not a spec change. + +Conformance against the [test corpus](https://github.com/swaits/meshcore-spec/tree/main/corpus) +is a useful signal but **not a substitute** for cross-checking against +upstream firmware. If you find a divergence from upstream, please +[file an issue](https://github.com/swaits/meshcore-spec/issues) — that is +exactly the kind of report this project exists to catch. + +### Spec Versions + +The published site preserves prior spec versions alongside the rolling +`latest` build. The URL layout is: + +- `https://swaits.github.io/meshcore-spec/latest/` — built from the tip of + `main`. May change between any two visits. +- `https://swaits.github.io/meshcore-spec/v0.1.0/` (and likewise for any + future `vX.Y.Z`) — a frozen content snapshot. Suitable for citing or for + certifying an implementation against. + +Frozen versions live as their own directory in the repo +(`versions/vX.Y.Z/`) — a literal copy of `src/` taken at release time. +This deliberately separates **content** (versioned, frozen) from **site +infrastructure** (theme, picker, build pipeline; lives at the repo root +and is unversioned). Every deploy rebuilds every published version using +today's infrastructure, so site improvements show up everywhere without +backporting. + +The version-picker in the top-right of every page lists every published +version; switching keeps you on the same chapter where it exists in the +target version. Spec versions use semver and are **independent** of the +upstream MeshCore firmware's own versioning — each release records the +upstream commit hash it was validated against in +[CHANGELOG.md](https://github.com/swaits/meshcore-spec/blob/main/CHANGELOG.md). + +### Scope + +This specification covers: + +- The packet wire format (framing, header, path, payload) +- All payload types and their binary encodings +- Cryptographic operations (encryption, signing, key exchange, hashing) +- The BLE/Serial companion protocol +- The KISS modem protocol +- The bridge protocol (RS232/ESP-NOW) + +It does NOT cover: + +- Application-level behavior (e.g., room server logic, repeater policies) +- Radio-layer parameters (LoRa spreading factor, bandwidth, frequency) +- Duty cycle management or transmission scheduling +- User interface or companion app behavior + +### Terminology + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be +interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +Additional terms used throughout: + +| Term | Definition | +|------|-----------| +| **Packet** | The fundamental transmission unit in MeshCore. | +| **Header** | The first byte of a packet, encoding route type, payload type, and protocol version. | +| **Path** | A sequence of node hashes representing the route a packet has taken or should follow. | +| **Hash** | A truncated prefix of a node's Ed25519 public key, used for routing. | +| **Payload** | The data portion of a packet, whose structure depends on the payload type. | +| **Transport Codes** | Optional 4-byte field present in transport-mode packets, used for regional scoping. | +| **MAC** | Message Authentication Code — a 2-byte truncated HMAC-SHA256 used for integrity verification. | +| **MTU** | Maximum Transmission Unit — 255 bytes for MeshCore. | +| **Flood Routing** | Routing mode where packets are broadcast and repeated by intermediate nodes, building up the path as they travel. | +| **Direct Routing** | Routing mode where the path is supplied by the sender and the packet follows a specific route. | + +### Byte Order + +All multi-byte integer fields in MeshCore are encoded in **little-endian** byte +order unless explicitly stated otherwise. The one exception is CayenneLPP sensor +data, which uses big-endian encoding. + +### Notation + +- Binary data is shown as hex bytes separated by spaces: `0D 00 EF BE AD DE` +- Bit fields are shown in binary with MSB first: `0bVVPPPPRR` +- Bit numbering is LSB = bit 0, MSB = bit 7 +- Field sizes are in bytes unless noted as bits +- `[field(N)]` denotes a field of N bytes +- `[field(N)?]` denotes an optional field of N bytes + +### Protocol Constants + +These constants define the fundamental limits of the protocol: + +| Constant | Value | Description | +|----------|-------|-------------| +| `MAX_TRANS_UNIT` | 255 | Maximum packet size on the wire (bytes) | +| `MAX_PACKET_PAYLOAD` | 184 | Maximum payload size (bytes) | +| `MAX_PATH_SIZE` | 64 | Maximum path size (bytes) | +| `PUB_KEY_SIZE` | 32 | Ed25519 public key size (bytes) | +| `PRV_KEY_SIZE` | 64 | Ed25519 private key size (bytes) | +| `SIGNATURE_SIZE` | 64 | Ed25519 signature size (bytes) | +| `CIPHER_KEY_SIZE` | 16 | AES-128 key size (bytes) | +| `CIPHER_BLOCK_SIZE` | 16 | AES-128 block size (bytes) | +| `CIPHER_MAC_SIZE` | 2 | Truncated HMAC-SHA256 MAC size (bytes) | +| `PATH_HASH_SIZE` | 1 | Default path hash size for v1 (bytes) | +| `MAX_HASH_SIZE` | 8 | Maximum hash size for deduplication (bytes) | +| `MAX_ADVERT_DATA_SIZE` | 32 | Maximum advertisement app data size (bytes) | + +### Protocol Versions + +The protocol version is encoded in bits 6-7 of the header byte: + +| Version | Value | Status | Description | +|---------|-------|--------|-------------| +| V1 | `0x00` | Active | 1-byte src/dest hashes, 2-byte MAC | +| V2 | `0x01` | Reserved | Future (e.g., 2-byte hashes, 4-byte MAC) | +| V3 | `0x02` | Reserved | Future | +| V4 | `0x03` | Reserved | Future | + +Currently only V1 is defined. Implementations MUST support V1. +Implementations SHOULD reject packets with unrecognized version values unless +they have explicit support for that version. + +### Document Organization + +| Section | Title | Description | +|---------|-------|-------------| +| 00 | Overview | This document | +| 01 | Wire Format | Packet framing and field layout | +| 02 | Header | Header byte encoding | +| 03 | Path | Path length encoding and path field | +| 04 | Payload: ACK | Acknowledgment payload | +| 05 | Payload: Advertisement | Node advertisement and app data | +| 06 | Payload: Encrypted | REQ/RESPONSE/TXT_MSG payloads | +| 07 | Payload: Anonymous Request | Anonymous request payload | +| 08 | Payload: Group | Group text and data payloads | +| 09 | Payload: Path Return | Encrypted path return payload | +| 10 | Payload: Trace | Path trace payload | +| 11 | Payload: Multipart | Multi-packet payload | +| 12 | Payload: Control | Control and discovery payloads | +| 13 | Payload: Raw Custom | Custom raw payload | +| 14 | Cryptography | AES-128, HMAC-SHA256, encrypt-then-MAC | +| 15 | Identity | Ed25519 keys, ECDH, key hashing | +| 16 | Packet Hash | SHA-256 deduplication hashing | +| 17 | Routing | Flood and direct routing behavior | +| 18 | Companion Protocol | BLE/Serial companion communication | +| 19 | KISS Protocol | KISS modem framing and extensions | +| 20 | Bridge Protocol | RS232/ESP-NOW bridge framing | + +### References + +- [MeshCore Firmware](https://github.com/meshcore-dev/MeshCore) — Reference C++ implementation +- [MeshCore Documentation](https://docs.meshcore.io) — Official protocol documentation +- [MeshCore.js](https://github.com/meshcore-dev/meshcore.js) — JavaScript implementation +- [meshcore-rs](https://github.com/Duncaen/meshcore-rs) — Rust implementation (community) +- [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) — Key words for requirement levels +- [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) — Ed25519 digital signatures +- [FIPS 197](https://csrc.nist.gov/publications/detail/fips/197/final) — AES specification +- [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) — SHA-256 specification diff --git a/versions/v0.1.0/01-wire-format.md b/versions/v0.1.0/01-wire-format.md new file mode 100644 index 0000000..f409c2b --- /dev/null +++ b/versions/v0.1.0/01-wire-format.md @@ -0,0 +1,140 @@ +# MeshCore Protocol Specification + +## Section 1: Wire Format + +### Overview + +A MeshCore packet is the fundamental transmission unit. It consists of a 1-byte +header, an optional 4-byte transport codes field, a 1-byte path length, a +variable-length path, and a variable-length payload. The maximum packet size on +the wire is 255 bytes (MAX_TRANS_UNIT). + +### Packet Layout + +``` + 0 1 2 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 ... ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Header | Transport Codes (opt, 4B) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Path Length | Path (variable) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Payload (variable) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +The fields appear in this order on the wire: + +``` +[header(1)][transport_codes(4)?][path_len(1)][path(0-64)][payload(1-184)] +``` + +### Field Summary + +| Field | Size | Required | Description | +|-------|------|----------|-------------| +| Header | 1 byte | Yes | Route type, payload type, protocol version (see [Section 2](02-header.md)) | +| Transport Codes | 4 bytes | Conditional | Two uint16_le values; present only when route type is `TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03) | +| Path Length | 1 byte | Yes | Encoded hash count and hash size (see [Section 3](03-path.md)) | +| Path | 0-64 bytes | Yes | Sequence of node hashes; length = hash_count × hash_size | +| Payload | 1-184 bytes | Yes | Packet data; structure depends on payload type | + +### Transport Codes + +Transport codes are present if and only if the route type (header bits 0-1) is +`TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03). + +When present, transport codes consist of two little-endian 16-bit unsigned +integers, for a total of 4 bytes: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Code 1 (uint16_le) | Code 2 (uint16_le) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +- **Code 1**: Calculated from regional scope parameters +- **Code 2**: Reserved (typically 0) + +When transport codes are not present (route types `FLOOD` and `DIRECT`), the +implementation MUST set both transport code values to zero internally. + +### Encoding Algorithm + +To encode a packet to wire format: + +1. Write the header byte. +2. If the route type is `TRANSPORT_FLOOD` or `TRANSPORT_DIRECT`: + a. Write transport_codes[0] as uint16_le (2 bytes). + b. Write transport_codes[1] as uint16_le (2 bytes). +3. Write the path_len byte. +4. Calculate path byte length as `hash_count × hash_size` (see [Section 3](03-path.md)). +5. Write `path_byte_length` bytes from the path array. +6. Write `payload_len` bytes from the payload array. + +The total wire length is: +``` +1 + (hasTransportCodes ? 4 : 0) + 1 + path_byte_length + payload_len +``` + +This MUST NOT exceed MAX_TRANS_UNIT (255) bytes. + +### Decoding Algorithm + +To decode a packet from wire format: + +1. Read 1 byte as the header. Extract route type from bits 0-1. +2. If route type is `TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03): + a. Read 2 bytes as transport_codes[0] (uint16_le). + b. Read 2 bytes as transport_codes[1] (uint16_le). + Otherwise, set both transport codes to zero. +3. Read 1 byte as path_len. +4. Validate path_len (see [Section 3](03-path.md)): + a. Extract hash_size = (path_len >> 6) + 1. If hash_size is 4, the packet + is INVALID. + b. Extract hash_count = path_len & 63. + c. If hash_count × hash_size > MAX_PATH_SIZE (64), the packet is INVALID. +5. Calculate path_byte_length = hash_count × hash_size. +6. Read path_byte_length bytes as the path. +7. Let `i` be the current read position. If `i >= total_length`, the packet is + INVALID (payload MUST contain at least 1 byte). +8. Calculate payload_len = total_length - i. +9. If payload_len > MAX_PACKET_PAYLOAD (184), the packet is INVALID. +10. Read payload_len bytes as the payload. + +### Size Constraints + +| Constraint | Value | Enforcement | +|-----------|-------|-------------| +| Maximum wire length | 255 bytes | Encoder MUST NOT produce packets exceeding this | +| Maximum payload | 184 bytes | Decoder MUST reject payloads exceeding this | +| Maximum path | 64 bytes | Decoder MUST reject paths exceeding this | +| Minimum payload | 1 byte | Decoder MUST reject packets with zero-length payload | +| Minimum packet | 3 bytes | Header (1) + path_len (1) + payload (1 minimum) | + +### Error Conditions + +A conforming decoder MUST reject packets with any of the following: + +1. Total wire length less than 3 bytes (no room for header + path_len + payload) +2. Total wire length less than minimum required for the route type: + - 3 bytes for FLOOD or DIRECT + - 7 bytes for TRANSPORT_FLOOD or TRANSPORT_DIRECT (adds 4 transport code bytes) +3. Invalid path_len encoding (see [Section 3](03-path.md)) +4. Path byte length exceeds remaining bytes +5. Zero-length payload after header, transport codes, path_len, and path +6. Payload length exceeding MAX_PACKET_PAYLOAD (184) + +### Cross-References + +- [Section 2: Header](02-header.md) — Header byte encoding details +- [Section 3: Path](03-path.md) — Path length encoding and path field +- Test vectors: `corpus/wire-format/framing/`, `corpus/wire-format/invalid/` + +### Reference Implementation + +- `Packet::writeTo()` in `src/Packet.cpp` — Encoding +- `Packet::readFrom()` in `src/Packet.cpp` — Decoding +- `Packet::getRawLength()` in `src/Packet.cpp` — Wire length calculation diff --git a/versions/v0.1.0/02-header.md b/versions/v0.1.0/02-header.md new file mode 100644 index 0000000..f031ff8 --- /dev/null +++ b/versions/v0.1.0/02-header.md @@ -0,0 +1,125 @@ +# MeshCore Protocol Specification + +## Section 2: Header + +### Overview + +The header is the first byte of every MeshCore packet. It encodes three fields +using bit packing: the route type (2 bits), the payload type (4 bits), and the +protocol version (2 bits). + +### Bit Layout + +``` + Bit: 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+ + | V1 V0| P3 P2 P1 P0| R1 R0| + +---+---+---+---+---+---+---+---+ + | | | + | | +-- Route Type (bits 0-1) + | +-------------- Payload Type (bits 2-5) + +------------------------ Protocol Version (bits 6-7) +``` + +The header byte value is computed as: + +``` +header = (version << 6) | (payload_type << 2) | route_type +``` + +And fields are extracted as: + +``` +route_type = header & 0x03 +payload_type = (header >> 2) & 0x0F +version = (header >> 6) & 0x03 +``` + +### Route Type (bits 0-1) + +The route type determines how the packet is routed through the mesh and whether +transport codes are present. + +| Value | Name | Transport Codes | Description | +|-------|------|----------------|-------------| +| 0x00 | TRANSPORT_FLOOD | Yes (4 bytes) | Flood routing with transport codes for regional scoping | +| 0x01 | FLOOD | No | Standard flood routing; path is built up by intermediate nodes | +| 0x02 | DIRECT | No | Direct routing; path is supplied by the sender | +| 0x03 | TRANSPORT_DIRECT | Yes (4 bytes) | Direct routing with transport codes | + +Transport codes MUST be present in the wire format when route type is 0x00 or +0x03, and MUST NOT be present when route type is 0x01 or 0x02. + +### Payload Type (bits 2-5) + +The payload type identifies the structure of the payload data. + +| Value | Name | Description | +|-------|------|-------------| +| 0x00 | REQUEST | Encrypted request (dest_hash + src_hash + MAC + ciphertext) | +| 0x01 | RESPONSE | Encrypted response to REQ or ANON_REQ | +| 0x02 | TXT_MSG | Encrypted text message | +| 0x03 | ACK | Simple acknowledgment (4-byte CRC) | +| 0x04 | ADVERT | Node identity advertisement | +| 0x05 | GRP_TXT | Encrypted group text message | +| 0x06 | GRP_DATA | Encrypted group datagram | +| 0x07 | ANON_REQ | Anonymous request (full public key instead of hash) | +| 0x08 | PATH | Encrypted returned path information | +| 0x09 | TRACE | Path trace with per-hop SNR collection | +| 0x0A | MULTIPART | One part of a multi-packet sequence | +| 0x0B | CONTROL | Control/discovery packet | +| 0x0C | (reserved) | Reserved for future use | +| 0x0D | (reserved) | Reserved for future use | +| 0x0E | (reserved) | Reserved for future use | +| 0x0F | RAW_CUSTOM | Custom raw bytes for application-defined payloads | + +Implementations SHOULD silently discard packets with reserved payload type values +(0x0C-0x0E) unless they have explicit support for extended types. + +### Protocol Version (bits 6-7) + +| Value | Name | Description | +|-------|------|-------------| +| 0x00 | V1 | Current version: 1-byte src/dest hashes, 2-byte MAC | +| 0x01 | V2 | Reserved for future use | +| 0x02 | V3 | Reserved for future use | +| 0x03 | V4 | Reserved for future use | + +Implementations MUST support version 0x00 (V1). Implementations SHOULD reject +packets with unrecognized version values. + +### Special Header Values + +The header value `0xFF` is used internally as a "do not retransmit" marker. This +is an in-memory sentinel only and MUST NOT appear on the wire. A header of +`0xFF` would decode as version=3, payload_type=0x0F (RAW_CUSTOM), route_type=3 +(TRANSPORT_DIRECT). + +### Encoding Examples + +| Header Byte | Binary | Version | Payload Type | Route Type | +|-------------|--------|---------|-------------|------------| +| `0x01` | `00 000000 01` | V1 (0) | REQUEST (0) | FLOOD (1) | +| `0x05` | `00 000001 01` | V1 (0) | RESPONSE (1) | FLOOD (1) | +| `0x09` | `00 000010 01` | V1 (0) | TXT_MSG (2) | FLOOD (1) | +| `0x0D` | `00 000011 01` | V1 (0) | ACK (3) | FLOOD (1) | +| `0x11` | `00 000100 01` | V1 (0) | ADVERT (4) | FLOOD (1) | +| `0x0C` | `00 000011 00` | V1 (0) | ACK (3) | TRANSPORT_FLOOD (0) | +| `0x0E` | `00 000011 10` | V1 (0) | ACK (3) | DIRECT (2) | +| `0x0F` | `00 000011 11` | V1 (0) | ACK (3) | TRANSPORT_DIRECT (3) | +| `0x4D` | `01 000011 01` | V2 (1, reserved) | ACK (3) | FLOOD (1) | + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Overall packet structure +- [Section 3: Path](03-path.md) — Path encoding +- Test vectors: `corpus/wire-format/header/` + +### Reference Implementation + +- `Packet::getRouteType()` in `src/Packet.h` — `header & 0x03` +- `Packet::getPayloadType()` in `src/Packet.h` — `(header >> 2) & 0x0F` +- `Packet::getPayloadVer()` in `src/Packet.h` — `(header >> 6) & 0x03` +- `Packet::hasTransportCodes()` in `src/Packet.h` — route type 0x00 or 0x03 +- Route type constants in `src/Packet.h` +- Payload type constants in `src/Packet.h` diff --git a/versions/v0.1.0/03-path.md b/versions/v0.1.0/03-path.md new file mode 100644 index 0000000..a8f7770 --- /dev/null +++ b/versions/v0.1.0/03-path.md @@ -0,0 +1,155 @@ +# MeshCore Protocol Specification + +## Section 3: Path + +### Overview + +The path field carries routing information as a sequence of node hashes. In +flood routing, intermediate nodes append their hash to the path as they forward +the packet. In direct routing, the sender supplies the full path. The path +length byte uses bit packing to encode both the number of hashes and the size of +each hash. + +### Path Length Byte + +The path length byte immediately follows the header (and transport codes, if +present). It encodes two values: + +``` + Bit: 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+ + | S1 S0| C5 C4 C3 C2 C1 C0| + +---+---+---+---+---+---+---+---+ + | | + | +-- Hash Count (bits 0-5): 0-63 + +----------------- Hash Size Code (bits 6-7): 0-2 (3 is reserved) +``` + +| Field | Bits | Extraction | Range | Description | +|-------|------|-----------|-------|-------------| +| Hash Count | 0-5 | `path_len & 0x3F` | 0-63 | Number of hashes in the path | +| Hash Size Code | 6-7 | `path_len >> 6` | 0-2 | Encoded hash size: actual size = code + 1 | + +The **actual hash size** in bytes is: + +``` +hash_size = (path_len >> 6) + 1 +``` + +| Hash Size Code | Actual Hash Size | Description | +|---------------|-----------------|-------------| +| 0 (`0b00`) | 1 byte | V1 default (PATH_HASH_SIZE) | +| 1 (`0b01`) | 2 bytes | Extended precision | +| 2 (`0b10`) | 3 bytes | Extended precision | +| 3 (`0b11`) | **RESERVED** | MUST be rejected as invalid | + +The path length byte is constructed as: + +``` +path_len = ((hash_size - 1) << 6) | (hash_count & 0x3F) +``` + +### Path Field + +The path field immediately follows the path length byte. Its size in bytes is: + +``` +path_byte_length = hash_count × hash_size +``` + +The path contains `hash_count` consecutive hashes, each `hash_size` bytes long. +Each hash is a prefix of a node's Ed25519 public key (see [Section 15](15-identity.md)). + +``` ++----------+----------+-----+----------+ +| Hash 0 | Hash 1 | ... | Hash N-1 | +| (H bytes)| (H bytes)| ... | (H bytes)| ++----------+----------+-----+----------+ + +where H = hash_size, N = hash_count +``` + +When hash_count is 0, the path field is empty (zero bytes on the wire). + +### Validation Rules + +A conforming implementation MUST reject a packet if any of the following are +true: + +1. **Reserved hash size**: Hash size code is 3 (bits 6-7 = `0b11`), meaning + hash_size would be 4. This value is reserved for future use. + +2. **Path overflow**: `hash_count × hash_size > MAX_PATH_SIZE` (64 bytes). + +The maximum number of hashes depends on hash size: + +| Hash Size | Max Hash Count | Max Path Bytes | +|-----------|---------------|----------------| +| 1 byte | 63 | 63 | +| 2 bytes | 32 | 64 | +| 3 bytes | 21 | 63 | + +Note: With 1-byte hashes the maximum hash count is 63 (not 64), because the +count field is only 6 bits wide. With 2-byte hashes, 32 × 2 = 64 bytes exactly +fills MAX_PATH_SIZE. With 3-byte hashes, 21 × 3 = 63 bytes is the maximum +before 22 × 3 = 66 would overflow. + +### Path Manipulation + +**Appending a hash (flood routing):** +When a node forwards a flood-routed packet, it appends its own hash to the path: + +1. Let `n` = current hash_count. +2. Verify `(n + 1) × hash_size <= MAX_PATH_SIZE`. If not, the node MUST NOT + forward the packet. +3. Copy the node's hash (first `hash_size` bytes of its public key) to + `path[n × hash_size]`. +4. Increment hash_count: `setPathHashCount(n + 1)`. + +**Setting hash size and count:** +``` +path_len = ((hash_size - 1) << 6) | (hash_count & 0x3F) +``` + +### Encoding Examples + +| path_len byte | Binary | Hash Size Code | Hash Count | Hash Size | Path Bytes | +|---------------|--------|---------------|------------|-----------|------------| +| `0x00` | `00 000000` | 0 | 0 | 1 | 0 | +| `0x01` | `00 000001` | 0 | 1 | 1 | 1 | +| `0x03` | `00 000011` | 0 | 3 | 1 | 3 | +| `0x3F` | `00 111111` | 0 | 63 | 1 | 63 | +| `0x41` | `01 000001` | 1 | 1 | 2 | 2 | +| `0x42` | `01 000010` | 1 | 2 | 2 | 4 | +| `0x60` | `01 100000` | 1 | 32 | 2 | 64 | +| `0x81` | `10 000001` | 2 | 1 | 3 | 3 | +| `0x95` | `10 010101` | 2 | 21 | 3 | 63 | +| `0xC0` | `11 000000` | 3 | 0 | **INVALID** | — | +| `0xFF` | `11 111111` | 3 | 63 | **INVALID** | — | + +### Known Implementation Discrepancy + +The Rust implementation [meshcore-rs](https://github.com/Duncaen/meshcore-rs) +treats the path_len byte as a raw byte count rather than decoding hash_count and +hash_size from the bitfields. For example, a path_len value of `0x42` (binary +`01 000010`) should decode as hash_size=2, hash_count=2, path_bytes=4. The +incorrect interpretation reads it as a raw count of 66 bytes. + +Test vectors in this corpus specifically exercise multi-byte hash sizes to catch +this class of bug. + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Overall packet structure +- [Section 2: Header](02-header.md) — Header encoding +- [Section 15: Identity](15-identity.md) — Hash derivation from public keys +- Test vectors: `corpus/wire-format/path/` + +### Reference Implementation + +- `Packet::getPathHashSize()` in `src/Packet.h` — `(path_len >> 6) + 1` +- `Packet::getPathHashCount()` in `src/Packet.h` — `path_len & 63` +- `Packet::getPathByteLen()` in `src/Packet.h` — `getPathHashCount() * getPathHashSize()` +- `Packet::setPathHashSizeAndCount()` in `src/Packet.h` — `((sz - 1) << 6) | (n & 63)` +- `Packet::isValidPathLen()` in `src/Packet.cpp` — Validation logic +- `Packet::writePath()` in `src/Packet.cpp` — Path serialization diff --git a/versions/v0.1.0/04-payload-ack.md b/versions/v0.1.0/04-payload-ack.md new file mode 100644 index 0000000..bf635a4 --- /dev/null +++ b/versions/v0.1.0/04-payload-ack.md @@ -0,0 +1,99 @@ +# MeshCore Protocol Specification + +## Section 4: Payload — ACK + +### Overview + +The ACK (acknowledgment) payload is the simplest payload type. It consists of a +single 4-byte CRC value used to confirm receipt of a previously sent message. +The CRC is a truncated SHA-256 over the original TXT_MSG plaintext prefix +concatenated with the sender's public key. + +### Payload Type + +Header payload type field: `0x03` (ACK) + +### Wire Format + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| ACK CRC (uint32_le) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| ack_crc | 0 | 4 bytes | uint32_le | CRC identifying the acknowledged message | + +### ACK CRC Computation + +The 4-byte ACK CRC is computed as the first 4 bytes of a SHA-256 digest over +the acknowledged TXT_MSG's plaintext prefix followed by the **sender's** +Ed25519 public key: + +``` +ack_crc = SHA-256( plaintext_prefix || sender_pub_key )[0..3] +``` + +Where `plaintext_prefix` is the TXT_MSG plaintext up to and including the +message text, **excluding any trailing NUL terminator**: + +``` +plaintext_prefix = timestamp_le(4) || txt_type_attempt(1) || text +``` + +- `timestamp_le` and `txt_type_attempt` are defined in + [Section 6](06-payload-encrypted.md#plaintext-format-txt_msg). +- `text` is the raw UTF-8 message bytes with no NUL terminator. +- `sender_pub_key` is the full 32-byte Ed25519 public key of the node that + originated the TXT_MSG (i.e., the node expecting this ACK back). + +For TXT_TYPE_SIGNED_PLAIN messages, the 4-byte signature prefix is included: + +``` +plaintext_prefix = timestamp_le(4) || txt_type_attempt(1) || signature(4) || text +ack_crc = SHA-256( plaintext_prefix || receiver_pub_key )[0..3] +``` + +Note the SIGNED_PLAIN variant hashes against the **receiver's** public key; +see `BaseChatMesh::onPeerDataRecv()`. + +Because `txt_type_attempt` includes the 2-bit `attempt` counter, each retry +with an incremented `attempt` produces a different ACK CRC. This lets the +sender attribute each received ACK to a specific transmission attempt. See +[Section 6: Reliable DM Delivery](06-payload-encrypted.md#reliable-dm-delivery). + +### Encoding + +1. Compute the 4-byte CRC value for the message being acknowledged (above). +2. Write the CRC as a little-endian 32-bit unsigned integer. +3. Set payload_len to 4. + +### Decoding + +1. Read 4 bytes from the payload as a little-endian uint32. +2. If fewer than 4 bytes are available, the packet is INVALID. + +### Constraints + +- The payload MUST be exactly 4 bytes. +- Implementations SHOULD log or discard ACK packets with payload_len < 4. + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Packet framing +- [Section 11: Multipart](11-payload-multipart.md) — Multipart ACK encoding +- Test vectors: `corpus/payloads/ack/` + +### Reference Implementation + +- `Mesh::createAck()` in `src/Mesh.cpp` — Packet construction +- `BaseChatMesh::composeMsgPacket()` in `src/helpers/BaseChatMesh.cpp` — + Sender-side expected-ACK computation (`expected_ack`) +- `BaseChatMesh::onPeerDataRecv()` in `src/helpers/BaseChatMesh.cpp` — + Receiver-side ACK CRC computation (`ack_hash`) +- ACK reception in `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ACK` diff --git a/versions/v0.1.0/05-payload-advert.md b/versions/v0.1.0/05-payload-advert.md new file mode 100644 index 0000000..21de754 --- /dev/null +++ b/versions/v0.1.0/05-payload-advert.md @@ -0,0 +1,123 @@ +# MeshCore Protocol Specification + +## Section 5: Payload — Advertisement + +### Overview + +The advertisement payload allows a node to announce its identity to the mesh. It +contains the node's Ed25519 public key, a timestamp, a signature proving +ownership of the key, and optional application data describing the node's type, +location, features, and name. + +### Payload Type + +Header payload type field: `0x04` (ADVERT) + +### Wire Format + +``` + 0 31 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | +| Ed25519 Public Key (32 bytes) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Timestamp (uint32_le) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | +| Ed25519 Signature (64 bytes) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| App Data (0-32 bytes) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| pub_key | 0 | 32 bytes | raw | Ed25519 public key of the advertising node | +| timestamp | 32 | 4 bytes | uint32_le | Unix epoch timestamp of advertisement creation | +| signature | 36 | 64 bytes | raw | Ed25519 signature (see Signature section below) | +| app_data | 100 | 0-32 bytes | structured | Optional application data (see App Data section below) | + +### Minimum and Maximum Sizes + +- Minimum payload: 100 bytes (pub_key + timestamp + signature, no app_data) +- Maximum payload: 132 bytes (100 + MAX_ADVERT_DATA_SIZE of 32) + +### Signature + +The signature is computed over the concatenation of: + +``` +message = pub_key(32) || timestamp_le(4) || app_data(0-32) +``` + +The node signs this message using its Ed25519 private key. Receivers MUST verify +the signature using the public key from the payload. Packets with invalid +signatures MUST be discarded. + +### App Data Format + +When present (payload_len > 100), app_data is a structured field beginning with +a flags byte: + +``` + Bit: 7 6 5 4 3 2 1 0 + +-------+-------+-------+-------+---+---+---+---+ + |has_ |has_ |has_ |has_ | Node Type | + |name |feat2 |feat1 |loc | (4 bits) | + +-------+-------+-------+-------+---+---+---+---+ +``` + +| Field | Offset | Size | Condition | Type | Description | +|-------|--------|------|-----------|------|-------------| +| flags | 0 | 1 byte | Always | uint8 | See bit layout above | +| latitude | 1 | 4 bytes | flags bit 4 set | int32_le | Latitude × 1,000,000 | +| longitude | 5 | 4 bytes | flags bit 4 set | int32_le | Longitude × 1,000,000 | +| feat1 | varies | 2 bytes | flags bit 5 set | uint16_le | Feature field 1 | +| feat2 | varies | 2 bytes | flags bit 6 set | uint16_le | Feature field 2 | +| name | varies | remaining | flags bit 7 set | UTF-8 | Node name (no null terminator) | + +Fields appear in the order listed. The offset of each field depends on which +preceding optional fields are present. + +### Node Types (flags bits 0-3) + +| Value | Name | Description | +|-------|------|-------------| +| 0 | None | Unspecified | +| 1 | Chat | Chat client node | +| 2 | Repeater | Mesh repeater node | +| 3 | Room | Room server node | +| 4 | Sensor | Sensor node | + +### Decoding Algorithm + +1. Read 32 bytes as pub_key. +2. Read 4 bytes as timestamp (uint32_le). +3. Read 64 bytes as signature. +4. If offset (100) > payload_len, the packet is INVALID (incomplete). +5. If offset (100) == payload_len, there is no app_data. Done. +6. Read remaining bytes as app_data (up to MAX_ADVERT_DATA_SIZE = 32 bytes). + If more than 32 bytes remain, implementations MUST truncate to 32. +7. Construct the signature verification message: pub_key || timestamp_le || app_data. +8. Verify the Ed25519 signature. If invalid, the receiver MUST discard the packet. +9. Parse app_data: + a. Read 1 byte as flags. + b. If flags bit 4 set: read 4 bytes latitude (int32_le), 4 bytes longitude (int32_le). + c. If flags bit 5 set: read 2 bytes feat1 (uint16_le). + d. If flags bit 6 set: read 2 bytes feat2 (uint16_le). + e. If flags bit 7 set: read remaining bytes as UTF-8 name. + +### Cross-References + +- [Section 15: Identity](15-identity.md) — Ed25519 key management +- [Section 14: Cryptography](14-crypto.md) — Signature operations +- Test vectors: `corpus/payloads/advert/` + +### Reference Implementation + +- `Mesh::createAdvert()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ADVERT` in `src/Mesh.cpp` — Decoding and verification diff --git a/versions/v0.1.0/06-payload-encrypted.md b/versions/v0.1.0/06-payload-encrypted.md new file mode 100644 index 0000000..a2d2b5f --- /dev/null +++ b/versions/v0.1.0/06-payload-encrypted.md @@ -0,0 +1,169 @@ +# MeshCore Protocol Specification + +## Section 6: Payload — Encrypted (REQ / RESPONSE / TXT_MSG) + +### Overview + +The REQUEST, RESPONSE, and TXT_MSG payload types share an identical wire format. +They consist of a destination hash, source hash, and an encrypt-then-MAC +ciphertext blob. The three types differ only in their semantic meaning at the +application layer. + +### Payload Types + +| Value | Name | Description | +|-------|------|-------------| +| 0x00 | REQUEST | Encrypted request (e.g., login, data query) | +| 0x01 | RESPONSE | Encrypted response to a REQUEST or ANON_REQ | +| 0x02 | TXT_MSG | Encrypted text message | + +### Wire Format + +``` + 0 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| dest_hash(1) | src_hash(1) | Cipher MAC (2 bytes) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | +| Ciphertext (N × 16 bytes, N ≥ 1) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| dest_hash | 0 | PATH_HASH_SIZE (1) | raw | First byte of recipient's Ed25519 public key | +| src_hash | 1 | PATH_HASH_SIZE (1) | raw | First byte of sender's Ed25519 public key | +| cipher_mac | 2 | CIPHER_MAC_SIZE (2) | raw | Truncated HMAC-SHA256 over ciphertext | +| ciphertext | 4 | variable (multiple of 16) | raw | AES-128-ECB encrypted data | + +### Minimum Size + +- Minimum payload: 4 + 16 = 20 bytes (dest_hash + src_hash + MAC + 1 AES block) + +### Encryption + +The ciphertext is produced using the encrypt-then-MAC scheme described in +[Section 14](14-crypto.md): + +1. Compute the ECDH shared secret between sender and recipient. +2. Zero-pad the plaintext on the right with `0x00` bytes until its length is a + multiple of 16 (AES block size). If the plaintext is already a multiple of + 16, no padding is added (the scheme is not unambiguous PKCS#7-style + padding — the receiver MUST disambiguate trailing zeros using the inner + plaintext format; see "Plaintext Format" below). +3. Encrypt the padded plaintext using AES-128-ECB with the first 16 bytes of + the shared secret as the key. +4. Compute HMAC-SHA256 over the ciphertext using the full 32-byte shared + secret as the HMAC key. Truncate to 2 bytes. +5. Prepend the 2-byte MAC to the ciphertext. + +### Decryption + +1. Extract dest_hash and src_hash. +2. If dest_hash matches this node, search for peers matching src_hash. +3. For each matching peer, compute or retrieve the shared secret. +4. Verify the MAC: compute HMAC-SHA256 over the ciphertext portion using the + full 32-byte shared secret. Compare the first 2 bytes with cipher_mac. +5. If the MAC is valid, decrypt the ciphertext using AES-128-ECB with the first + 16 bytes of the shared secret. The decrypted data may contain trailing zero + bytes from padding. +6. If no peer's MAC matches, the packet is not for this node (or the peer is + unknown). The packet MAY still be forwarded. + +### Plaintext Format (TXT_MSG) + +For TXT_MSG payloads, the decrypted plaintext has this structure: + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| timestamp | 0 | 4 bytes | uint32_le | Message timestamp | +| txt_type_attempt | 4 | 1 byte | uint8 | Bits 2-7: message type (see [Section 4](04-payload-ack.md)); bits 0-1: attempt counter (0-3). See "Attempt Counter Semantics" below. | +| text | 5 | remaining | UTF-8 | Message text | + +#### Attempt Counter Semantics + +The `attempt` sub-field (bits 0-1 of `txt_type_attempt`) carries the low two +bits of a transmission attempt counter used by the sender's retry logic. For +a given logical TXT_MSG: + +- The `timestamp` field MUST remain stable across all retries of the same + message. +- The `attempt` counter MUST be 0 on the first transmission and MUST be + incremented by 1 on each retry. +- Bits 0–1 of `txt_type_attempt` MUST always carry `attempt & 3`. These bits + are part of the ACK CRC input (see + [Section 4](04-payload-ack.md#ack-crc-computation)), so each retry of a + message produces a distinct expected ACK CRC. +- For attempts in `0..3`, no further encoding is needed; the plaintext ends + with the text. +- For attempts `> 3`, the sender MUST append a 2-byte tail + `[NUL_terminator(1)][attempt_full(1)]` to the plaintext after the text, + where `attempt_full` is the full 1-byte counter value (not just the low + 2 bits). The receiver locates the text by scanning for the NUL terminator + and recovers the full attempt value from the trailing byte. Because this + tail costs 2 bytes of plaintext capacity, the maximum text length is + reduced by 2 bytes when `attempt > 3`. +- The ACK CRC is computed over `timestamp(4) || txt_type_attempt(1) || text` + only. The `[NUL][attempt_full]` tail (when present) and the implicit C + string NUL terminator (when not) are excluded from the hash input. This + means a retry with attempt 4 and a retry with attempt 0 produce ACK CRCs + that match (since `4 & 3 == 0`); senders relying on attempts beyond 3 + SHOULD also verify the trailing attempt byte if they need to distinguish + rolled-over attempts. + +See `BaseChatMesh::composeMsgPacket()` in +`src/helpers/BaseChatMesh.cpp` for the reference encoding. + +### Reliable DM Delivery + +The following sender-side behavior is informational and documents how +interoperating implementations deliver TXT_MSG reliably. Exact timeouts, +backoff schedules, and queue depths are implementation-defined; see +[Section 17 — Sender Behavior](17-routing.md#sender-behavior-informational) +for routing-layer details. + +- **Expected-ACK tracking.** On transmit, the sender SHOULD compute the + expected `ack_crc` (see [Section 4](04-payload-ack.md#ack-crc-computation)) + and record it in a pending-ACK table keyed by CRC. The table SHOULD bound + entries by capacity and age out entries after the retry schedule has + completed (plus a margin for in-flight ACKs). +- **Retry.** If no matching ACK is received within an implementation-defined + timeout, the sender SHOULD retransmit with `attempt` incremented (per + "Attempt Counter Semantics" above) and re-register the new expected + `ack_crc`. Up to 4 attempts are representable in the 2-bit `attempt` + field. +- **ACK consumption.** On receipt of a PAYLOAD_TYPE_ACK or a MULTIPART-wrapped + ACK whose `ack_crc` matches a pending entry, the sender MUST treat the + message as delivered, remove the pending-ACK entry, and cancel any queued + retries for that CRC. +- **Duplicate DM handling at the receiver.** A receiver MAY cache its most + recent reply per peer keyed by the inbound DM's plaintext and, on a + duplicate inbound DM within a short window, re-emit the cached reply as a + loss-recovery optimization. This is not normative; the normative behavior + is "process once and rely on [Section 16](16-packet-hash.md) dedup." + Implementations that re-emit cached replies SHOULD suppress re-emission + while a retry for that reply is still pending, to avoid piling on. + +### Plaintext Format (REQUEST / RESPONSE) + +The plaintext format for REQUEST and RESPONSE is application-defined. Common +patterns include: + +- **REQUEST**: `[timestamp(4)][request_data...]` +- **RESPONSE**: `[timestamp(4)][response_data...]` + +### Cross-References + +- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC details +- [Section 15: Identity](15-identity.md) — ECDH shared secret computation +- Test vectors: `corpus/payloads/encrypted/` + +### Reference Implementation + +- `Mesh::createDatagram()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, cases `PAYLOAD_TYPE_REQ/RESPONSE/TXT_MSG` — Decoding +- `Utils::encryptThenMAC()` in `src/Utils.cpp` — Encryption +- `Utils::MACThenDecrypt()` in `src/Utils.cpp` — Decryption and verification diff --git a/versions/v0.1.0/07-payload-anon-req.md b/versions/v0.1.0/07-payload-anon-req.md new file mode 100644 index 0000000..adafc00 --- /dev/null +++ b/versions/v0.1.0/07-payload-anon-req.md @@ -0,0 +1,74 @@ +# MeshCore Protocol Specification + +## Section 7: Payload — Anonymous Request + +### Overview + +The anonymous request payload allows a node to send an encrypted message to a +recipient without the recipient needing to know the sender in advance. Instead of +a 1-byte source hash, the full 32-byte Ed25519 public key of the sender is +included, enabling the recipient to compute the shared secret for decryption. + +### Payload Type + +Header payload type field: `0x07` (ANON_REQ) + +### Wire Format + +``` + 0 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| dest_hash(1) | | ++-+-+-+-+-+-+-+-+ | +| Sender Public Key (32 bytes) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Cipher MAC (2 bytes) | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +| Ciphertext (N × 16 bytes, N ≥ 1) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| dest_hash | 0 | 1 byte | raw | First byte of recipient's Ed25519 public key | +| sender_pub_key | 1 | 32 bytes | raw | Full Ed25519 public key of sender | +| cipher_mac | 33 | 2 bytes | raw | Truncated HMAC-SHA256 over ciphertext | +| ciphertext | 35 | variable (multiple of 16) | raw | AES-128-ECB encrypted data | + +### Minimum Size + +- Minimum payload: 1 + 32 + 2 + 16 = 51 bytes + +### Encryption + +1. Sender computes ECDH shared secret using its private key and the recipient's + public key. +2. Encrypt plaintext using AES-128-ECB with first 16 bytes of shared secret. +3. Compute HMAC-SHA256 over ciphertext with full 32-byte shared secret. + Truncate to 2 bytes. +4. Assemble: dest_hash || sender_pub_key || MAC || ciphertext. + +### Decryption + +1. Read dest_hash. If it does not match this node, the packet is not for us + (but MAY be forwarded). +2. Read 32-byte sender_pub_key. Construct an Identity from it. +3. Compute ECDH shared secret between this node's private key and sender's + public key. +4. Verify the MAC and decrypt as in [Section 14](14-crypto.md). + +### Cross-References + +- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Standard encrypted format +- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC +- [Section 15: Identity](15-identity.md) — ECDH key exchange +- Test vectors: `corpus/payloads/anon-req/` + +### Reference Implementation + +- `Mesh::createAnonDatagram()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ANON_REQ` — Decoding diff --git a/versions/v0.1.0/08-payload-group.md b/versions/v0.1.0/08-payload-group.md new file mode 100644 index 0000000..184d911 --- /dev/null +++ b/versions/v0.1.0/08-payload-group.md @@ -0,0 +1,81 @@ +# MeshCore Protocol Specification + +## Section 8: Payload — Group (GRP_TXT / GRP_DATA) + +### Overview + +Group payloads enable encrypted communication within a channel. All members of a +channel share a symmetric secret. The payload begins with a 1-byte channel hash +(derived from the channel's shared key), followed by an encrypt-then-MAC blob. + +### Payload Types + +| Value | Name | Description | +|-------|------|-------------| +| 0x05 | GRP_TXT | Group text message | +| 0x06 | GRP_DATA | Group datagram (binary data) | + +### Wire Format + +``` + 0 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +|channel_hash(1)| Cipher MAC (2 bytes) | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +| Ciphertext (N × 16 bytes, N ≥ 1) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| channel_hash | 0 | 1 byte | raw | First byte of SHA-256(channel_shared_key) | +| cipher_mac | 1 | 2 bytes | raw | Truncated HMAC-SHA256 over ciphertext | +| ciphertext | 3 | variable (multiple of 16) | raw | AES-128-ECB encrypted data | + +### Minimum Size + +- Minimum payload: 1 + 2 + 16 = 19 bytes + +### Channel Hash Derivation + +The channel_hash is the first byte of the SHA-256 hash of the channel's 32-byte +shared key (PUB_KEY_SIZE bytes): + +``` +channel_hash = SHA256(channel_shared_key)[0] +``` + +This means hash collisions are expected (1 in 256 chance). When a receiver finds +a matching channel_hash, it attempts decryption with each matching channel's +secret. Only a successful MAC verification confirms the correct channel. + +### Encryption + +1. Encrypt plaintext using AES-128-ECB with first 16 bytes of the channel's + shared key (GroupChannel.secret). +2. Compute HMAC-SHA256 over ciphertext using all 32 bytes of the channel's + shared key. Truncate to 2 bytes. +3. Assemble: channel_hash || MAC || ciphertext. + +### Decryption + +1. Read channel_hash (1 byte). +2. Search local channel database for channels with matching hash. Multiple + channels may match (up to 4 in reference implementation). +3. For each matching channel, attempt MAC verification and decryption using the + channel's shared key. +4. The first channel that produces a valid MAC yields the correct decryption. + +### Cross-References + +- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC +- Test vectors: `corpus/payloads/group/` + +### Reference Implementation + +- `Mesh::createGroupDatagram()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, cases `PAYLOAD_TYPE_GRP_TXT/GRP_DATA` — Decoding +- `Mesh::searchChannelsByHash()` — Channel lookup diff --git a/versions/v0.1.0/09-payload-path.md b/versions/v0.1.0/09-payload-path.md new file mode 100644 index 0000000..f7cb20d --- /dev/null +++ b/versions/v0.1.0/09-payload-path.md @@ -0,0 +1,90 @@ +# MeshCore Protocol Specification + +## Section 9: Payload — Path Return + +### Overview + +The PATH payload carries an encrypted return path from one node to another. It +uses the same dest_hash + src_hash + encrypt-then-MAC envelope as REQ/RESPONSE/ +TXT_MSG, but the decrypted contents contain routing path data and optional extra +application data. + +### Payload Type + +Header payload type field: `0x08` (PATH) + +### Wire Format (Outer) + +``` + 0 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| dest_hash(1) | src_hash(1) | Cipher MAC (2 bytes) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | +| Ciphertext (N × 16 bytes, N ≥ 1) | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +The outer format is identical to [Section 6](06-payload-encrypted.md). + +### Decrypted Inner Format + +``` ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| path_len(1) | Path (hash_count × hash_size) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| extra_type(1) | Extra Data (variable) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +| Field | Size | Type | Description | +|-------|------|------|-------------| +| path_len | 1 byte | encoded | Same bitfield encoding as packet path_len (see [Section 3](03-path.md)) | +| path | hash_count × hash_size | raw | The return path to the sender | +| extra_type | 1 byte | uint8 | Type of extra data (lower 4 bits used; upper 4 reserved) | +| extra | remaining | raw | Extra data (may include trailing zero padding from AES) | + +### Extra Type Values + +| Value | Description | +|-------|-------------| +| 0x00 | No meaningful extra data (reciprocal path) | +| 0xFF | Dummy/padding (used when no real extra data; followed by 4 random bytes) | +| Other | Application-defined | + +When creating a path return with no extra data, the reference implementation +appends `0xFF` as extra_type followed by 4 random bytes to ensure the packet +hash is unique. + +### Encoding + +1. Build the inner plaintext: + a. Write path_len byte (encoded hash_size and hash_count). + b. Write path bytes (hash_count × hash_size). + c. If extra data is provided: write extra_type (lower 4 bits), write extra. + d. If no extra data: write `0xFF`, write 4 random bytes. +2. Encrypt the inner plaintext using encrypt-then-MAC with the shared secret. +3. Prepend dest_hash and src_hash. + +### Decoding + +1. Decode the outer envelope (dest_hash, src_hash, MAC, ciphertext) as in + [Section 6](06-payload-encrypted.md). +2. After decryption: + a. Read 1 byte as path_len. Decode hash_size and hash_count. + b. Read hash_count × hash_size bytes as the path. + c. Read 1 byte as extra_type (use lower 4 bits only). + d. Remaining bytes are extra data (may include AES zero-padding). + +### Cross-References + +- [Section 3: Path](03-path.md) — Path length encoding +- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Outer envelope format +- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC +- Test vectors: `corpus/payloads/path-return/` + +### Reference Implementation + +- `Mesh::createPathReturn()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, PATH case within REQ/RESPONSE/TXT_MSG handling — Decoding diff --git a/versions/v0.1.0/10-payload-trace.md b/versions/v0.1.0/10-payload-trace.md new file mode 100644 index 0000000..ef99943 --- /dev/null +++ b/versions/v0.1.0/10-payload-trace.md @@ -0,0 +1,106 @@ +# MeshCore Protocol Specification + +## Section 10: Payload — Trace + +### Overview + +The TRACE payload enables path discovery with per-hop signal quality measurement. +A trace packet travels along a specified direct path, and each intermediate node +appends its received SNR value to the packet's path field. When the trace reaches +its final hop, the accumulated SNR values and node hashes are delivered. + +### Payload Type + +Header payload type field: `0x09` (TRACE) + +### Wire Format + +The trace payload has a fixed 9-byte header followed by an optional path hashes +section (appended when sent via direct routing): + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Tag (uint32_le) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Auth Code (uint32_le) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Flags | Path Hashes (variable) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| tag | 0 | 4 bytes | uint32_le | Random tag set by the initiator for correlation | +| auth_code | 4 | 4 bytes | uint32_le | Authentication code | +| flags | 8 | 1 byte | uint8 | Flags (see below) | +| path_hashes | 9 | variable | raw | Node hashes for the path to trace (appended for direct send) | + +### Flags Field + +``` + Bit: 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+ + | Reserved | S1 S0| + +---+---+---+---+---+---+---+---+ +``` + +| Bits | Description | +|------|-------------| +| 0-1 | Path hash size for the trace path hashes (as a power of 2: 0=1 byte, 1=2 bytes, 2=4 bytes) | +| 2-7 | Reserved (must be zero) | + +Note: The flags field path hash size encoding (`1 << (flags & 0x03)`) differs +from the packet path_len encoding (`(code >> 6) + 1`). + +### Direct Sending + +When a TRACE packet is sent via direct routing, the path hashes are appended +directly to the payload (after the 9-byte header), not placed in the packet's +path field. The packet's path field is instead used to accumulate SNR values +from intermediate nodes. + +``` +On creation: payload = [tag(4)][auth_code(4)][flags(1)] (9 bytes) +On sendDirect: payload += path_hashes (9 + N bytes) + packet.path_len = 0 (path used for SNR) +``` + +### SNR Accumulation + +As each intermediate node forwards a TRACE packet, it appends its received SNR +value as a single signed byte to the packet's path field: + +``` +path[path_len++] = (int8_t)(SNR × 4) +``` + +The SNR value is stored as SNR × 4 for 0.25 dB precision, matching the format +used in RxMeta frames. + +### Trace Completion + +A TRACE is considered complete when the path_len counter (used as an offset into +the path hashes in the payload) reaches or exceeds the remaining path hashes. +At that point, `onTraceRecv` is called with: +- The accumulated SNR values (in packet.path) +- The path hashes (in packet.payload, after byte 9) +- The path length (number of hops traversed) + +### Minimum Size + +- Minimum payload: 9 bytes (tag + auth_code + flags, no path hashes) + +### Cross-References + +- [Section 16: Packet Hash](16-packet-hash.md) — TRACE packets include path_len in hash +- Test vectors: `corpus/payloads/trace/` + +### Reference Implementation + +- `Mesh::createTrace()` in `src/Mesh.cpp` — Creation (9-byte base) +- `Mesh::sendDirect()` — Path appended to payload for TRACE type +- `Mesh::onRecvPacket()`, TRACE handling — SNR accumulation and completion check diff --git a/versions/v0.1.0/11-payload-multipart.md b/versions/v0.1.0/11-payload-multipart.md new file mode 100644 index 0000000..967db5a --- /dev/null +++ b/versions/v0.1.0/11-payload-multipart.md @@ -0,0 +1,124 @@ +# MeshCore Protocol Specification + +## Section 11: Payload — Multipart + +### Overview + +The MULTIPART payload wraps another payload type to indicate it is part of a +multi-packet sequence. The first byte encodes the number of remaining packets in +the sequence and the sub-payload type. Currently, only multipart ACK is defined +in the reference implementation. + +### Payload Type + +Header payload type field: `0x0A` (MULTIPART) + +### Wire Format + +``` + 0 1 + 0 1 2 3 4 5 6 7 8 9 0 ... ++-+-+-+-+-+-+-+-+-+-+-+-+ +|Rem| SubType | Sub-payload | ++-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +First byte encoding: + +``` + Bit: 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+ + | Remaining (4) | Sub-Type (4) | + +---+---+---+---+---+---+---+---+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| remaining | 0 (bits 4-7) | 4 bits | uint | Number of remaining packets in sequence (0-15) | +| sub_type | 0 (bits 0-3) | 4 bits | uint | Payload type of the wrapped content | +| sub_payload | 1 | variable | raw | The wrapped payload data | + +``` +first_byte = (remaining << 4) | (sub_type & 0x0F) +``` + +### Multipart ACK + +The only currently defined multipart sub-type is ACK (sub_type = 0x03): + +``` ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| (rem<<4)|0x03| ACK CRC (uint32_le) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +Total payload: 5 bytes (1 byte header + 4 byte CRC). + +Multipart ACKs are used to send additional ACK retransmissions for reliability. +The `remaining` field indicates how many more ACK packets follow this one, +counting down: a chain of K extra copies is emitted as MULTIPART with +`remaining = K, K-1, …, 1`, followed by exactly one final plain +`PAYLOAD_TYPE_ACK` packet (NOT a MULTIPART with `remaining = 0`). All copies +in the chain carry the same `ack_crc`. Receivers MUST treat the first copy +received as the authoritative ACK and dedup subsequent copies by `ack_crc` +at the application layer. + +### Multipart ACK Usage Constraints + +- A sender MUST only emit MULTIPART-wrapped ACKs on direct-routed return + paths (i.e., when a direct path to the original sender is known). When the + return path is unknown, the sender MUST fall back to emitting a plain + PAYLOAD_TYPE_ACK via flood routing. This reflects the firmware behavior in + `Mesh::routeDirectRecvAcks()` (which emits MULTIPART copies only when a + direct path is available) and `BaseChatMesh::sendAckTo()` (which falls back + to `sendFloodScoped()` when `out_path_len == OUT_PATH_UNKNOWN`). +- All MULTIPART-ACK copies and the final plain ACK for the same acknowledged + message carry an identical 4-byte `ack_crc`. Receivers MUST treat them as + the same logical ACK: the first one received satisfies the acknowledgment, + and subsequent duplicates MUST be deduplicated per + [Section 16](16-packet-hash.md) (each copy has a distinct packet hash + because the wrapper byte differs, so ordinary dedup does not suppress them + on the wire — application-level dedup by `ack_crc` is required). +- The typical inter-copy delay in the reference implementation is + approximately 300 ms plus the direct retransmit delay + (`Mesh::routeDirectRecvAcks()`); exact timing is implementation-defined. +- The number of extra copies is configured via `getExtraAckTransmitCount()` + and is implementation-defined. The firmware default is 0 (plain ACK only). + +A receiver MUST NOT infer delivery reliability from the `remaining` counter +alone: the counter is a hint about how many additional copies the sender +intends to emit, not a guarantee that they will arrive. + +### Encoding (Multipart ACK) + +1. Set first byte to `(remaining << 4) | PAYLOAD_TYPE_ACK`. +2. Copy the 4-byte ACK CRC starting at offset 1. +3. Set payload_len to 5. + +### Decoding (Multipart ACK) + +1. Read first byte. Extract remaining = byte >> 4, sub_type = byte & 0x0F. +2. If sub_type == 0x03 (ACK) and payload_len >= 5: + a. Read 4 bytes at offset 1 as ACK CRC (uint32_le). +3. For other sub_types: reserved for future use. + +### Constraints + +- Payload MUST be at least 2 bytes (1 header + 1 sub-payload minimum). +- For multipart ACK, payload MUST be at least 5 bytes. + +### Cross-References + +- [Section 4: ACK](04-payload-ack.md) — ACK CRC format +- Test vectors: `corpus/payloads/multipart/` + +### Reference Implementation + +- `Mesh::createMultiAck()` in `src/Mesh.cpp` — Encoding +- `Mesh::routeDirectRecvAcks()` in `src/Mesh.cpp` — Direct-only gating and + inter-copy spacing +- `BaseChatMesh::sendAckTo()` in `src/helpers/BaseChatMesh.cpp` — Flood + fallback when no direct return path is known +- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_MULTIPART` — Decoding diff --git a/versions/v0.1.0/12-payload-control.md b/versions/v0.1.0/12-payload-control.md new file mode 100644 index 0000000..5d4f7f5 --- /dev/null +++ b/versions/v0.1.0/12-payload-control.md @@ -0,0 +1,68 @@ +# MeshCore Protocol Specification + +## Section 12: Payload — Control + +### Overview + +The CONTROL payload carries control and discovery data. Control packets are raw +byte payloads whose structure is determined by the first byte. A subset of +control packets (those with bit 7 of the first byte set) are restricted to +zero-hop delivery only. + +### Payload Type + +Header payload type field: `0x0B` (CONTROL) + +### Wire Format + +``` + 0 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Control Byte | Data... | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| control_byte | 0 | 1 byte | uint8 | Control type identifier | +| data | 1 | variable | raw | Control-specific data | + +### Zero-Hop Restriction + +When bit 7 (`0x80`) of the control byte is set, the packet is restricted to +zero-hop delivery only. The reference implementation enforces this: + +- If `(payload[0] & 0x80) != 0` AND the packet is direct-routed: + - It is processed only if `getPathHashCount() == 0`. + - It is NOT forwarded to other nodes. + +### Discovery Protocols + +Control packets are commonly used for node discovery. The specific sub-protocol +formats are application-defined, but typically include: + +- **Discovery Request**: Sent zero-hop to find nearby nodes +- **Discovery Response**: Contains node type, name, SNR, and other metadata + +### Encoding + +1. Construct the raw control data bytes. +2. If the control packet should be zero-hop only, set bit 7 of the first byte. +3. Copy data to payload, set payload_len. + +### Constraints + +- Payload MUST be at least 1 byte (the control byte). +- Maximum payload: 184 bytes (MAX_PACKET_PAYLOAD). + +### Cross-References + +- [Section 17: Routing](17-routing.md) — Zero-hop delivery +- Test vectors: `corpus/payloads/control/` + +### Reference Implementation + +- `Mesh::createControlData()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, CONTROL handling — Zero-hop check and delivery diff --git a/versions/v0.1.0/13-payload-raw-custom.md b/versions/v0.1.0/13-payload-raw-custom.md new file mode 100644 index 0000000..8b47f6e --- /dev/null +++ b/versions/v0.1.0/13-payload-raw-custom.md @@ -0,0 +1,49 @@ +# MeshCore Protocol Specification + +## Section 13: Payload — Raw Custom + +### Overview + +The RAW_CUSTOM payload carries application-defined raw bytes with no prescribed +structure. It is intended for applications that implement their own encryption, +framing, or payload formats on top of the MeshCore transport. + +### Payload Type + +Header payload type field: `0x0F` (RAW_CUSTOM) + +### Wire Format + +``` + 0 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Raw Data (1-184 bytes) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +### Fields + +| Field | Offset | Size | Type | Description | +|-------|--------|------|------|-------------| +| data | 0 | 1-184 bytes | raw | Application-defined payload | + +### Routing + +In the current reference implementation, RAW_CUSTOM packets are only processed +when received via direct routing. They are NOT flood-routed. + +### Constraints + +- Payload MUST be at least 1 byte. +- Maximum payload: 184 bytes (MAX_PACKET_PAYLOAD). +- Maximum total data: limited by `sizeof(Packet::payload)` which is + MAX_PACKET_PAYLOAD (184). + +### Cross-References + +- Test vectors: `corpus/payloads/raw-custom/` + +### Reference Implementation + +- `Mesh::createRawData()` in `src/Mesh.cpp` — Encoding +- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_RAW_CUSTOM` — Processing (direct only) diff --git a/versions/v0.1.0/14-crypto.md b/versions/v0.1.0/14-crypto.md new file mode 100644 index 0000000..09db99f --- /dev/null +++ b/versions/v0.1.0/14-crypto.md @@ -0,0 +1,120 @@ +# MeshCore Protocol Specification + +## Section 14: Cryptography + +### Overview + +MeshCore uses a combination of symmetric and asymmetric cryptography. Data +confidentiality is provided by AES-128 in ECB mode with zero padding. Data +integrity is provided by HMAC-SHA256 truncated to 2 bytes. These are combined +in an encrypt-then-MAC scheme. + +### Algorithms + +| Operation | Algorithm | Key Size | Output | +|-----------|-----------|----------|--------| +| Symmetric Encryption | AES-128-ECB | 16 bytes | Multiple of 16 bytes | +| Message Authentication | HMAC-SHA256 | 32 bytes | 2 bytes (truncated) | +| Digital Signature | Ed25519 | 32/64 bytes | 64 bytes | +| Key Exchange | X25519 (ECDH) | 32 bytes | 32 bytes | +| Hashing | SHA-256 | — | Up to 32 bytes | + +### AES-128-ECB Encryption + +MeshCore uses AES-128 in Electronic Codebook (ECB) mode with zero-byte padding. + +**Key**: The first 16 bytes (CIPHER_KEY_SIZE) of the shared secret. + +**Encryption**: + +1. Set the AES-128 key to `shared_secret[0..15]`. +2. Process the plaintext in 16-byte blocks: + a. For each complete 16-byte block, encrypt in place. + b. For the final partial block (< 16 bytes): + - Copy the remaining bytes to a 16-byte buffer. + - Fill remaining bytes with zeros. + - Encrypt the padded block. +3. If the plaintext is empty, encrypt a 16-byte all-zero block. +4. The output is always a multiple of 16 bytes. + +**Decryption**: + +1. Set the AES-128 key to `shared_secret[0..15]`. +2. Process the ciphertext in 16-byte blocks, decrypting each. +3. The output length equals the ciphertext length. +4. The final block MAY contain trailing zero bytes from padding. + The application must know the original plaintext length or use + a length-prefixed format to determine where meaningful data ends. + +### HMAC-SHA256 (Truncated to 2 Bytes) + +MeshCore computes HMAC-SHA256 over the ciphertext and truncates the result to +CIPHER_MAC_SIZE (2) bytes. + +**Key**: The full 32 bytes (PUB_KEY_SIZE) of the shared secret. + +Note: The HMAC key uses the full 32-byte shared secret, while the AES key uses +only the first 16 bytes. This is an important distinction. + +**Computation**: + +``` +mac = HMAC-SHA256(key=shared_secret[0..31], message=ciphertext)[0..1] +``` + +The first 2 bytes of the HMAC-SHA256 output are used as the MAC. + +### Encrypt-then-MAC + +The encrypt-then-MAC scheme combines AES-128-ECB encryption with HMAC-SHA256 +authentication: + +**Encoding** (`encryptThenMAC`): + +1. Encrypt the plaintext using AES-128-ECB (see above). + Store ciphertext at `dest[2..]` (offset by MAC size). +2. Compute HMAC-SHA256 over the ciphertext using the full 32-byte shared + secret. Truncate to 2 bytes. +3. Store the 2-byte MAC at `dest[0..1]`. +4. Return total length: 2 + ciphertext_length. + +Output format: +``` +[MAC (2 bytes)][Ciphertext (N × 16 bytes)] +``` + +**Decoding** (`MACThenDecrypt`): + +1. If input length ≤ 2 (CIPHER_MAC_SIZE), return failure (invalid). +2. Recompute HMAC-SHA256 over `src[2..]` (the ciphertext portion) using the + full 32-byte shared secret. Truncate to 2 bytes. +3. Compare the computed MAC with `src[0..1]`. +4. If MACs do not match, return failure (0 = invalid HMAC). +5. If MACs match, decrypt `src[2..]` using AES-128-ECB. +6. Return the decrypted plaintext length. + +### Security Notes + +- **ECB mode**: AES-ECB encrypts each 16-byte block independently. Identical + plaintext blocks produce identical ciphertext blocks. This is a known + weakness of ECB mode, but MeshCore's short payloads and mesh-network context + make this acceptable for its use case. +- **2-byte MAC**: The truncated HMAC provides only 16 bits of authentication. + This means there is approximately a 1-in-65536 chance of a forged MAC being + accepted. This trade-off prioritizes bandwidth over authentication strength. +- **Key derivation**: The shared secret is used directly as both AES key + (first 16 bytes) and HMAC key (full 32 bytes). No key derivation function + (KDF) is applied. + +### Cross-References + +- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Usage in packet payloads +- [Section 15: Identity](15-identity.md) — Shared secret computation via ECDH +- Test vectors: `corpus/crypto/` + +### Reference Implementation + +- `Utils::encrypt()` in `src/Utils.cpp` — AES-128-ECB encryption +- `Utils::decrypt()` in `src/Utils.cpp` — AES-128-ECB decryption +- `Utils::encryptThenMAC()` in `src/Utils.cpp` — Encrypt-then-MAC +- `Utils::MACThenDecrypt()` in `src/Utils.cpp` — MAC-then-Decrypt diff --git a/versions/v0.1.0/15-identity.md b/versions/v0.1.0/15-identity.md new file mode 100644 index 0000000..e62491d --- /dev/null +++ b/versions/v0.1.0/15-identity.md @@ -0,0 +1,92 @@ +# MeshCore Protocol Specification + +## Section 15: Identity + +### Overview + +Every MeshCore node has an Ed25519 key pair. The 32-byte public key serves as +the node's identity. Hashes used in routing are truncated prefixes of the public +key. Shared secrets for encryption are computed via ECDH key exchange using +X25519 (Ed25519 keys transposed to Curve25519). + +### Key Types + +| Type | Size | Description | +|------|------|-------------| +| Public Key | 32 bytes (PUB_KEY_SIZE) | Ed25519 public key | +| Private Key | 64 bytes (PRV_KEY_SIZE) | Ed25519 private key (seed + public) | +| Signature | 64 bytes (SIGNATURE_SIZE) | Ed25519 signature | +| Shared Secret | 32 bytes (PUB_KEY_SIZE) | ECDH shared secret | + +### Hash Derivation + +A node's routing hash is simply a prefix of its public key: + +``` +hash = pub_key[0..hash_size-1] +``` + +Where `hash_size` is 1, 2, or 3 bytes depending on the protocol version and +path encoding (see [Section 3](03-path.md)). + +For V1, `PATH_HASH_SIZE` is 1, meaning a node's hash is the first byte of its +public key. This gives only 256 unique hash values, so hash collisions are +common and expected. The protocol handles this by attempting decryption with all +peers matching a given hash. + +### Hash Matching + +To check if a hash matches a node's identity: + +``` +match = memcmp(hash, pub_key, hash_size) == 0 +``` + +### Ed25519 Signing + +Used for advertisement signatures (see [Section 5](05-payload-advert.md)): + +1. Construct the message to sign (e.g., pub_key || timestamp || app_data). +2. Sign using the Ed25519 private key. +3. The 64-byte signature is included in the advertisement payload. + +Verification: + +1. Extract the public key and signature from the payload. +2. Reconstruct the message. +3. Verify the signature using Ed25519 verification. +4. Receivers MUST discard the packet if verification fails. + +### ECDH Key Exchange (X25519) + +To compute a shared secret between two nodes: + +1. Convert the Ed25519 keys to X25519 (Curve25519) format. +2. Perform X25519 Diffie-Hellman: `shared_secret = X25519(my_private, their_public)`. +3. The resulting 32-byte shared secret is used for: + - AES-128 key: `shared_secret[0..15]` + - HMAC-SHA256 key: `shared_secret[0..31]` + +### Identity Serialization + +Identities can be serialized for storage: + +- **Public Identity**: 32 bytes (public key only) +- **Local Identity**: 64 bytes (private key) + 32 bytes (public key) = 96 bytes + +Hex representation uses uppercase characters with 2 hex digits per byte. + +### Cross-References + +- [Section 3: Path](03-path.md) — Hash size in path encoding +- [Section 5: Advertisement](05-payload-advert.md) — Signature usage +- [Section 14: Cryptography](14-crypto.md) — Shared secret usage +- Test vectors: `corpus/crypto/ed25519/`, `corpus/crypto/ecdh/` + +### Reference Implementation + +- `Identity` class in `src/Identity.h` — Public key and verification +- `LocalIdentity` class in `src/Identity.h` — Key pair, signing, ECDH +- `Identity::copyHashTo()` — Hash derivation (prefix copy) +- `Identity::isHashMatch()` — Hash comparison +- `LocalIdentity::calcSharedSecret()` — ECDH key exchange diff --git a/versions/v0.1.0/16-packet-hash.md b/versions/v0.1.0/16-packet-hash.md new file mode 100644 index 0000000..3660fe7 --- /dev/null +++ b/versions/v0.1.0/16-packet-hash.md @@ -0,0 +1,67 @@ +# MeshCore Protocol Specification + +## Section 16: Packet Hash + +### Overview + +MeshCore uses SHA-256 hashing for packet deduplication. Each node maintains a +table of recently seen packet hashes and discards packets it has already +processed. The hash is computed over the payload type and payload contents, with +a special case for TRACE packets. + +### Hash Computation + +The packet hash is computed as: + +``` +hash = SHA-256(payload_type_byte || payload_data)[0..MAX_HASH_SIZE-1] +``` + +Where: +- `payload_type_byte` is a single byte: `(header >> 2) & 0x0F` +- `payload_data` is the raw payload bytes (payload_len bytes) +- The SHA-256 output is truncated to MAX_HASH_SIZE (8) bytes + +### TRACE Packet Special Case + +For TRACE packets (payload_type = 0x09), the path_len byte is included in the +hash to distinguish trace packets that revisit the same node on their return +path: + +``` +hash = SHA-256(payload_type_byte || path_len_byte || payload_data)[0..MAX_HASH_SIZE-1] +``` + +### Algorithm + +``` +function calculatePacketHash(packet): + sha = SHA256_init() + type_byte = getPayloadType(packet.header) // single byte + SHA256_update(sha, type_byte) + if type_byte == 0x09: // TRACE + SHA256_update(sha, packet.path_len) // 1 byte (as uint8) + SHA256_update(sha, packet.payload, packet.payload_len) + hash = SHA256_finalize(sha) + return hash[0..7] // first MAX_HASH_SIZE bytes +``` + +### Properties + +- The hash does NOT include the header byte, path, or transport codes. This + means the same logical message received via different routes produces the + same hash. +- The hash does NOT include the route type or version. Only the payload type + and payload content determine the hash. +- For TRACE packets, including path_len ensures that the same trace payload + at different stages of traversal produces different hashes. + +### Cross-References + +- [Section 10: Trace](10-payload-trace.md) — TRACE packet special handling +- Test vectors: `corpus/crypto/sha256/packet-hash.json` + +### Reference Implementation + +- `Packet::calculatePacketHash()` in `src/Packet.cpp` +- `MeshTables::hasSeen()` — Deduplication table lookup diff --git a/versions/v0.1.0/17-routing.md b/versions/v0.1.0/17-routing.md new file mode 100644 index 0000000..8b25bfd --- /dev/null +++ b/versions/v0.1.0/17-routing.md @@ -0,0 +1,208 @@ +# MeshCore Protocol Specification + +## Section 17: Routing + +### Overview + +MeshCore supports two routing modes: flood routing and direct routing. Flood +routing broadcasts packets to all neighbors, building up a path as the packet +propagates. Direct routing sends packets along a pre-determined path. + +### Flood Routing + +In flood routing, the sender emits a packet with an empty path. Each +intermediate node that forwards the packet appends its hash to the path before +retransmitting. + +**Sending (flood)**: + +1. Set route type to FLOOD (0x01) or TRANSPORT_FLOOD (0x00). +2. Set path to empty: `setPathHashSizeAndCount(hash_size, 0)`. +3. Transmit the packet. + +**Forwarding (flood)**: + +1. Receive a flood-routed packet. +2. Check the deduplication table. If already seen, discard. +3. Process the packet (decrypt if addressed to this node, etc.). +4. If the packet should be forwarded (node is a repeater): + a. Verify `(hash_count + 1) × hash_size <= MAX_PATH_SIZE`. + b. Append this node's hash at `path[hash_count × hash_size]`. + c. Increment hash_count. + d. Retransmit with a random delay. + +**Path growth**: The path grows by one hash per hop. When the path reaches +MAX_PATH_SIZE (64 bytes), no more nodes can be appended and the packet stops +propagating. + +### Direct Routing + +In direct routing, the sender provides the complete path. Each intermediate +node checks whether it is the next hop, removes itself from the path, and +forwards to the next node. + +**Sending (direct)**: + +1. Set route type to DIRECT (0x02) or TRANSPORT_DIRECT (0x03). +2. Set the path to the destination's known route. +3. Transmit the packet. + +**Forwarding (direct)**: + +1. Receive a direct-routed packet. +2. Check if the first hash in the path matches this node. +3. If yes, and the node allows forwarding: + a. Remove the first hash from the path (shift remaining hashes left). + b. Decrement hash_count. + c. Retransmit. +4. If no, discard (this node is not the next hop). + +**Path removal** (`removeSelfFromPath`): + +1. Decrement hash_count. +2. Shift the path array: for each index k from 0 to (hash_count × hash_size), + copy `path[k + hash_size]` to `path[k]`. + +### Zero-Hop Delivery + +Zero-hop packets are direct-routed packets with hash_count = 0. They reach only +immediate neighbors (nodes within radio range). Used for: + +- Control packets with bit 7 set in the control byte +- Discovery protocols +- Local-only operations + +### Transport Codes + +Transport codes (present in TRANSPORT_FLOOD and TRANSPORT_DIRECT) enable +regional scoping. Nodes can filter packets based on transport codes, accepting +only packets from their region. + +### TRACE Routing + +TRACE packets are a special case. They are sent via direct routing, but instead +of the normal path field, the path hashes are appended to the payload. The +packet's path field is repurposed to accumulate SNR values from each hop +(see [Section 10](10-payload-trace.md)). + +### Retransmission Priority + +Packets are prioritized for transmission: + +| Priority | Packet Type | Description | +|----------|-------------|-------------| +| 0 (highest) | Direct routed | Routed traffic | +| 1 | Path return, standard | Most flood packets | +| 2 | Path return (flood) | Path packets | +| 3 | Advertisement | De-prioritized | +| 5 | Trace | Trace forwarding | +| N (hash_count) | Flood forwarded | Lower priority for more distant sources | + +### Sender Behavior (Informational) + +This section documents sender-side behavior that is not fully constrained by +the wire protocol but is common across interoperating implementations. The +reference firmware establishes the baseline; client implementations SHOULD +follow these patterns to interoperate cleanly. Specific numeric tuning +parameters (timeouts, queue sizes, retry counts) are implementation-defined. + +#### Path Learning + +- When a node receives a flood-routed packet from a peer, the accumulated + `path` field records the hops the packet traversed. The receiving node + SHOULD cache the **reversed** path keyed by the peer's source hash and + subsequently use that cached path to send direct-routed traffic back. +- When multiple paths are observed for the same peer, the **reference + implementation unconditionally replaces** the cached path with the most + recently observed one (`BaseChatMesh::onContactPathRecv()` in + `src/helpers/BaseChatMesh.cpp` — its source comment explicitly flags + hash-size or SNR-based selection as future work). Implementations MAY + layer a quality heuristic on top (e.g., prefer larger hash sizes or + better SNR margin) if they store and compare multiple candidate paths, + but this is not part of the reference behavior and the exact policy is + implementation-defined. +- Cached paths in the reference implementation do **not** expire on a + timer; they live as long as the contact entry, and are overwritten + whenever a fresher path is observed. Implementations MAY add a TTL or + age-based eviction policy as an extension. The reference implementation + does not persist paths across reboots. + +#### PATH_RETURN + +After receiving a flood-routed DM, a receiver MAY proactively emit a +PAYLOAD_TYPE_PATH packet ([Section 9](09-payload-path.md)) that embeds the +reversed path and optionally an ACK as extra data. This lets the original +sender learn a direct return path before its first reply, converting +subsequent traffic from flood to direct routing. See +`BaseChatMesh::onPeerDataRecv()` and `Mesh::createPathReturn()`. + +The reference implementation schedules the PATH_RETURN transmission with a +small delay (`TXT_ACK_DELAY`, default 200 ms) to avoid colliding with other +packets the receiver is about to emit. Exact timing is +implementation-defined; the goal is to space PATH_RETURN, ACK, and any +application reply across distinct on-air slots. + +#### Flood Fallback and Retries + +- A sender SHOULD track outstanding DMs by their expected `ack_crc` + (see [Section 4](04-payload-ack.md)) and retry transmissions that are not + acknowledged within a timeout. Timeout values and backoff schedules are + implementation-defined and typically depend on LoRa airtime parameters. +- On each retry, the sender MUST increment the `attempt` sub-field of + `txt_type_attempt` (see + [Section 6](06-payload-encrypted.md#plaintext-format-txt_msg)). The + `timestamp` field MUST be held constant across retries of the same logical + message. This changes the ACK CRC per attempt, letting the sender attribute + a returned ACK to a specific transmission. +- After N unsuccessful direct-routed retries, the sender SHOULD discard the + cached direct path and retry via flood routing, so that a stale cached path + does not indefinitely block delivery. The value of N is + implementation-defined (the reference is small — single-digit). +- On receipt of a matching ACK (plain or MULTIPART, per + [Section 11](11-payload-multipart.md#multipart-ack-usage-constraints)), + the sender MUST cancel any queued retries for that ACK CRC. + +#### Transmission Priority (sender-originated) + +Client implementations that maintain a TX queue SHOULD prioritize +sender-originated packets roughly as follows (highest first): + +| Priority | Packet Type | +|----------|-------------| +| 0 | ACK (plain or MULTIPART) | +| 1 | PATH / PATH_RETURN | +| 2 | Direct-routed reply (DM) | +| 3 | Flood-routed reply (DM) | +| 4 | Request/response (e.g., login, keep-alive) | +| 5 | Group text / group data | +| 6 | Advertisement | + +This ordering complements the forwarding-priority table above: ACKs and +PATH returns are time-sensitive and short; direct replies take precedence +over flood replies to reduce channel occupancy. Exact ordering and queue +depth are implementation-defined. + +### Packet Deduplication + +Before processing or forwarding any packet, nodes check if the packet has +already been seen using `hasSeen()` with the packet hash from +[Section 16](16-packet-hash.md). This prevents infinite loops in flood routing. + +Once a packet is marked as seen, it is NOT forwarded again even if received via +a different route. This is a "first packet wins" approach. + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Packet structure +- [Section 3: Path](03-path.md) — Path encoding +- [Section 10: Trace](10-payload-trace.md) — TRACE routing special case +- [Section 16: Packet Hash](16-packet-hash.md) — Deduplication + +### Reference Implementation + +- `Mesh::sendFlood()` in `src/Mesh.cpp` — Flood send +- `Mesh::sendDirect()` in `src/Mesh.cpp` — Direct send +- `Mesh::sendZeroHop()` in `src/Mesh.cpp` — Zero-hop send +- `Mesh::routeRecvPacket()` in `src/Mesh.cpp` — Flood forwarding +- `Mesh::removeSelfFromPath()` in `src/Mesh.cpp` — Path manipulation +- `Mesh::onRecvPacket()` in `src/Mesh.cpp` — Direct forwarding diff --git a/versions/v0.1.0/18-companion-protocol.md b/versions/v0.1.0/18-companion-protocol.md new file mode 100644 index 0000000..0e3612a --- /dev/null +++ b/versions/v0.1.0/18-companion-protocol.md @@ -0,0 +1,96 @@ +# MeshCore Protocol Specification + +## Section 18: Companion Protocol + +### Overview + +The companion protocol enables BLE (Bluetooth Low Energy) and serial +communication between a MeshCore radio and a companion app (phone, tablet, or +computer). It uses a binary framing format over the Nordic UART Service (NUS) +BLE profile. + +### BLE Service + +| UUID | Description | +|------|-------------| +| `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` | Nordic UART Service | +| `6E400002-...` | RX Characteristic (app writes to radio) | +| `6E400003-...` | TX Characteristic (radio notifies app) | + +### Frame Format + +Each companion protocol frame consists of a 1-byte type identifier followed by +variable-length data: + +``` +[type(1)][data(0-171)] +``` + +Maximum frame size is 172 bytes. + +### Message Types (Radio to App) + +| Type | Value | Description | +|------|-------|-------------| +| PACKET_CHANNEL_MSG_RECV | 0x01 | Channel message received | +| PACKET_CONTACT_MSG_RECV | 0x02 | Contact message received | +| PACKET_ADV_RECV | 0x03 | Advertisement received | +| PACKET_CONTACT_MSG_RECV_V3 | 0x04 | Contact message V3 (includes SNR) | +| PACKET_CHANNEL_MSG_RECV_V3 | 0x05 | Channel message V3 (includes SNR) | + +### Command Types (App to Radio) + +| Command | Value | Description | +|---------|-------|-------------| +| CMD_APP_START | 0x01 | Initialize connection | +| CMD_DEVICE_QUERY | 0x02 | Query device info | +| CMD_SET_CHANNEL | 0x03 | Set active channel | +| CMD_SEND_CHANNEL_MESSAGE | 0x04 | Send message to channel | +| CMD_SEND_CONTACT_MESSAGE | 0x05 | Send message to contact | +| CMD_GET_CONTACTS | 0x06 | Get contact list | +| CMD_GET_CHANNELS | 0x07 | Get channel list | +| CMD_SET_TIME | 0x08 | Set device time | +| CMD_ADD_CONTACT | 0x09 | Add a contact | +| CMD_ADD_CHANNEL | 0x0A | Add a channel | +| CMD_SEND_LOGIN | 0x0B | Login to room/repeater | +| CMD_GET_SETTINGS | 0x0C | Get device settings | +| CMD_SET_SETTINGS | 0x0D | Set device settings | +| CMD_REMOVE_CONTACT | 0x0E | Remove a contact | +| CMD_REMOVE_CHANNEL | 0x0F | Remove a channel | +| CMD_SHARE_CONTACT | 0x10 | Share contact info | +| CMD_SET_ADV_NAME | 0x11 | Set advertisement name | +| CMD_REBOOT | 0x12 | Reboot device | +| CMD_SEND_RAW | 0x13 | Send raw packet | + +### Connection Sequence + +1. Scan for BLE devices advertising the Nordic UART Service. +2. Connect and discover services. +3. Enable notifications on the TX characteristic. +4. Send `CMD_APP_START` to initialize. +5. Send `CMD_DEVICE_QUERY` to get device info. +6. Send `CMD_SET_TIME` to synchronize clock. +7. Send `CMD_GET_CONTACTS` and `CMD_GET_CHANNELS` to fetch state. + +### Byte Order + +All multi-byte integers in the companion protocol are **little-endian**, except +for CayenneLPP sensor data which uses **big-endian**. + +### Message Length Limit + +Text messages are limited to 133 characters. + +### MTU Considerations + +The default BLE MTU is 23 bytes (20 bytes of payload). For complex operations, +implementations SHOULD request a larger MTU (up to 512 bytes). + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Underlying packet format +- Test vectors: `corpus/companion/` + +### Reference Implementation + +- `docs/companion_protocol.md` in the MeshCore repository diff --git a/versions/v0.1.0/19-kiss-protocol.md b/versions/v0.1.0/19-kiss-protocol.md new file mode 100644 index 0000000..b3e54a6 --- /dev/null +++ b/versions/v0.1.0/19-kiss-protocol.md @@ -0,0 +1,180 @@ +# MeshCore Protocol Specification + +## Section 19: KISS Modem Protocol + +### Overview + +The KISS (Keep It Simple, Stupid) modem protocol provides a serial interface to +MeshCore LoRa radios. It follows the KA9Q/K3MC KISS TNC specification with +MeshCore-specific SetHardware extensions for cryptographic operations, radio +control, and telemetry. + +### Serial Configuration + +- Baud rate: 115200 +- Data bits: 8 +- Parity: None +- Stop bits: 1 +- Flow control: None + +### Frame Format + +``` +┌──────┬───────────┬──────────────┬──────┐ +│ FEND │ Type Byte │ Data (escaped)│ FEND │ +│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ +└──────┴───────────┴──────────────┴──────┘ +``` + +### Special Bytes + +| Byte | Name | Value | Description | +|------|------|-------|-------------| +| FEND | Frame End | 0xC0 | Frame delimiter | +| FESC | Frame Escape | 0xDB | Escape character | +| TFEND | Transposed FEND | 0xDC | FESC + TFEND represents 0xC0 in data | +| TFESC | Transposed FESC | 0xDD | FESC + TFESC represents 0xDB in data | + +### Byte Stuffing + +To send a data byte of 0xC0 (FEND), emit: 0xDB 0xDC (FESC TFEND). +To send a data byte of 0xDB (FESC), emit: 0xDB 0xDD (FESC TFESC). + +### Type Byte + +``` + Bit: 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+ + | Port (4) | Command (4) | + +---+---+---+---+---+---+---+---+ +``` + +Port is 0 for single-port TNC (standard for MeshCore). + +### Standard KISS Commands (Host to TNC) + +| Command | Value | Data | Description | +|---------|-------|------|-------------| +| DataFrame | 0x00 | Raw packet | Queue packet for transmission | +| TXDELAY | 0x01 | 1 byte | Transmitter keyup delay (× 10ms) | +| Persistence | 0x02 | 1 byte | CSMA persistence (0-255) | +| SlotTime | 0x03 | 1 byte | CSMA slot interval (× 10ms) | +| TXtail | 0x04 | 1 byte | Post-TX hold time (× 10ms) | +| FullDuplex | 0x05 | 1 byte | 0=half, nonzero=full | +| SetHardware | 0x06 | Sub-cmd + data | MeshCore extensions | +| Return | 0xFF | — | Exit KISS mode (no-op) | + +### TNC to Host + +| Type | Value | Data | Description | +|------|-------|------|-------------| +| DataFrame | 0x00 | Raw packet | Received packet from radio | + +Data frames carry raw MeshCore packets (up to 255 bytes, MAX_TRANS_UNIT). + +### SetHardware Extensions (0x06) + +MeshCore extends the KISS protocol via the SetHardware command. The first data +byte is a sub-command identifier. + +**Request Sub-commands (Host to TNC):** + +| Sub-cmd | Value | Data | Description | +|---------|-------|------|-------------| +| GetIdentity | 0x01 | — | Get node's Ed25519 public key | +| GetRandom | 0x02 | len(1) | Get random bytes (1-64) | +| VerifySignature | 0x03 | pubkey(32)+sig(64)+data | Verify Ed25519 signature | +| SignData | 0x04 | data | Ed25519 sign | +| EncryptData | 0x05 | key(32)+plaintext | AES-128 encrypt | +| DecryptData | 0x06 | key(32)+mac(2)+ciphertext | AES-128 decrypt with MAC | +| KeyExchange | 0x07 | remote_pub(32) | X25519 ECDH | +| Hash | 0x08 | data | SHA-256 hash | +| SetRadio | 0x09 | freq(4)+bw(4)+sf(1)+cr(1) | Set radio parameters | +| SetTxPower | 0x0A | power(1) | Set TX power (dBm) | +| GetRadio | 0x0B | — | Get radio parameters | +| GetTxPower | 0x0C | — | Get TX power | +| GetCurrentRssi | 0x0D | — | Get current RSSI | +| IsChannelBusy | 0x0E | — | Check if channel busy | +| GetAirtime | 0x0F | pkt_len(1) | Estimate air time | +| GetNoiseFloor | 0x10 | — | Get noise floor | +| GetVersion | 0x11 | — | Get firmware version | +| GetStats | 0x12 | — | Get RX/TX statistics | +| GetBattery | 0x13 | — | Get battery voltage | +| GetMCUTemp | 0x14 | — | Get MCU temperature | +| GetSensors | 0x15 | perms(1) | Get sensor data | +| GetDeviceName | 0x16 | — | Get device name | +| Ping | 0x17 | — | Ping | +| Reboot | 0x18 | — | Reboot device | +| SetSignalReport | 0x19 | enable(1) | Enable/disable RxMeta | +| GetSignalReport | 0x1A | — | Get signal report status | + +**Response Sub-commands (TNC to Host):** + +Response codes: `response = command | 0x80` + +| Sub-cmd | Value | Data | +|---------|-------|------| +| Identity | 0x81 | pubkey(32) | +| Random | 0x82 | random_bytes(1-64) | +| Verify | 0x83 | result(1): 0=invalid, 1=valid | +| Signature | 0x84 | signature(64) | +| Encrypted | 0x85 | mac(2)+ciphertext | +| Decrypted | 0x86 | plaintext | +| SharedSecret | 0x87 | secret(32) | +| HashResult | 0x88 | hash(32) | +| Radio | 0x8B | freq(4)+bw(4)+sf(1)+cr(1) | +| TxPower | 0x8C | power(1) | +| CurrentRssi | 0x8D | rssi(1, signed) | +| ChannelBusy | 0x8E | busy(1): 0=clear, 1=busy | +| Airtime | 0x8F | millis(4) | +| NoiseFloor | 0x90 | dBm(2, signed) | +| Version | 0x91 | version(1)+reserved(1) | +| Stats | 0x92 | rx(4)+tx(4)+errors(4) | +| Battery | 0x93 | millivolts(2) | +| MCUTemp | 0x94 | temp(2, signed, tenths °C) | +| Sensors | 0x95 | CayenneLPP data | +| DeviceName | 0x96 | name(UTF-8) | +| Pong | 0x97 | — | +| SignalReport | 0x9A | status(1) | +| OK | 0xF0 | — | +| Error | 0xF1 | error_code(1) | +| TxDone | 0xF8 | result(1): 0=fail, 1=success | +| RxMeta | 0xF9 | snr(1)+rssi(1) | + +### Error Codes + +| Code | Value | Description | +|------|-------|-------------| +| InvalidLength | 0x01 | Request data too short | +| InvalidParam | 0x02 | Invalid parameter value | +| NoCallback | 0x03 | Feature not available | +| MacFailed | 0x04 | MAC verification failed | +| UnknownCmd | 0x05 | Unknown sub-command | +| EncryptFailed | 0x06 | Encryption failed | + +### Unsolicited Events + +**TxDone (0xF8)**: Sent after packet transmission. 0x01 = success, 0x00 = fail. + +**RxMeta (0xF9)**: Sent after each received data frame. Contains: +- SNR: 1 byte, signed, × 4 for 0.25 dB precision +- RSSI: 1 byte, signed, dBm + +### Data Format Notes + +- Maximum payload per frame: 255 bytes (MAX_TRANS_UNIT) +- Frames larger than 255 unescaped bytes are silently dropped +- All multi-byte integers are little-endian +- Radio frequency is in Hz (e.g., 869618000) +- Battery voltage in millivolts +- MCU temperature in tenths of °C (e.g., 253 = 25.3°C) + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Packet format within data frames +- [Section 14: Cryptography](14-crypto.md) — Crypto operations via SetHardware +- Test vectors: `corpus/kiss/` + +### Reference Implementation + +- `docs/kiss_modem_protocol.md` in the MeshCore repository diff --git a/versions/v0.1.0/20-bridge-protocol.md b/versions/v0.1.0/20-bridge-protocol.md new file mode 100644 index 0000000..ea62622 --- /dev/null +++ b/versions/v0.1.0/20-bridge-protocol.md @@ -0,0 +1,69 @@ +# MeshCore Protocol Specification + +## Section 20: Bridge Protocol + +### Overview + +The bridge protocol enables MeshCore packet forwarding over non-LoRa transports +such as RS232 serial links and ESP-NOW WiFi. It adds a simple framing layer +with a magic number and checksum around the raw MeshCore packet. + +### Frame Format + +``` +┌──────────────┬──────────────┬──────────────┬──────────────┐ +│ Magic (2B) │ Payload │ Checksum (2B)│ │ +│ 0xC03E │ (variable) │ Fletcher-16 │ │ +└──────────────┴──────────────┴──────────────┴──────────────┘ +``` + +### Fields + +| Field | Size | Type | Description | +|-------|------|------|-------------| +| Magic | 2 bytes | uint16 | `0xC03E` — identifies the frame as a MeshCore bridge packet | +| Payload | variable | raw | Raw MeshCore packet (as produced by `Packet::writeTo()`) | +| Checksum | 2 bytes | Fletcher-16 | Fletcher-16 checksum over the payload | + +### RS232 Variant + +The RS232 variant adds a length field between the magic and payload: + +``` +┌──────────────┬──────────────┬──────────────┬──────────────┐ +│ Magic (2B) │ Length (2B) │ Payload │ Checksum (2B)│ +│ 0xC03E │ uint16_le │ (variable) │ Fletcher-16 │ +└──────────────┴──────────────┴──────────────┴──────────────┘ +``` + +| Field | Size | Type | Description | +|-------|------|------|-------------| +| Length | 2 bytes | uint16_le | Length of the payload in bytes | + +### Fletcher-16 Checksum + +The Fletcher-16 checksum is computed over the payload bytes: + +``` +function fletcher16(data): + sum1 = 0 + sum2 = 0 + for byte in data: + sum1 = (sum1 + byte) mod 255 + sum2 = (sum2 + sum1) mod 255 + return (sum2 << 8) | sum1 +``` + +### ESP-NOW Variant + +For ESP-NOW transport, the frame format is the same as the basic format (magic + +payload + checksum) without the length field, since ESP-NOW provides its own +length framing. + +### Cross-References + +- [Section 1: Wire Format](01-wire-format.md) — Packet format within payload + +### Reference Implementation + +- Bridge handling in MeshCore firmware (board-specific implementations) diff --git a/versions/v0.1.0/SUMMARY.md b/versions/v0.1.0/SUMMARY.md new file mode 100644 index 0000000..5df1005 --- /dev/null +++ b/versions/v0.1.0/SUMMARY.md @@ -0,0 +1,35 @@ +# Summary + +[Introduction](00-overview.md) + +# Wire Format + +- [Wire Format](01-wire-format.md) +- [Header](02-header.md) +- [Path](03-path.md) + +# Payloads + +- [ACK](04-payload-ack.md) +- [Advertisement](05-payload-advert.md) +- [Encrypted (REQ / RESPONSE / TXT_MSG)](06-payload-encrypted.md) +- [Anonymous Request](07-payload-anon-req.md) +- [Group (GRP_TXT / GRP_DATA)](08-payload-group.md) +- [Path Return](09-payload-path.md) +- [Trace](10-payload-trace.md) +- [Multipart](11-payload-multipart.md) +- [Control](12-payload-control.md) +- [Raw Custom](13-payload-raw-custom.md) + +# Cryptography & Identity + +- [Cryptography](14-crypto.md) +- [Identity](15-identity.md) +- [Packet Hash](16-packet-hash.md) + +# Routing & Higher-Layer Protocols + +- [Routing](17-routing.md) +- [Companion Protocol](18-companion-protocol.md) +- [KISS Modem Protocol](19-kiss-protocol.md) +- [Bridge Protocol](20-bridge-protocol.md)