Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 32 additions & 21 deletions rust/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
# rust/

The Rust implementation of wire-mesh: a Cargo workspace of three crates mirroring the TypeScript core's generated/ports/adapters/domain split.
The Rust implementation of wire-mesh: a Cargo workspace whose core crate mirrors the TypeScript core's generated/ports/adapters/domain split.

```
crates/
wire-mesh-wire/ The CDDL model + CDE codec. One hand-written module per CDDL rule family
(identity, handshake, tokens, transport, management, streaming, exec,
data, discovery, frame) plus the CborValue/CanonicalMap DOM for every
`any` and open-map position. alloc-only, no async/crypto/serde
dependencies.
wire-mesh-core/ Ports (Transport, KeyValueStorage, Identity, Clock), domain logic
(handshake negotiation, capability-token verification, revocation),
and adapters (in-memory storage, Tokio TCP transport, system clock,
Ed25519/ES256 node identity).
wire-mesh-conformance/ The conformance-check binary gating the frozen vectors in
../../conformance/*.v1.json.
wire-mesh-wire/ The CDDL model + CDE codec. One hand-written module per CDDL rule family
(identity, handshake, tokens, transport, management, streaming, exec,
data, discovery, frame, bulk, room, secure-channel, threshold, webrtc)
plus the CborValue/CanonicalMap DOM for every `any` and open-map
position. No async, crypto or serde dependencies.
wire-mesh-core/ Ports (Transport, KeyValueStorage, Identity, Clock), domain logic
(handshake negotiation, capability-token verification, revocation,
predicate conditions, peer-advert verification, rooms and room
paths, room content-key wrapping, erasure coding and shard
manifests, and a node session runtime that dispatches
manage-requests), and adapters (in-memory storage, Tokio TCP
transport, system clock, Ed25519/ES256 node identity).
wire-mesh-threshold/ FROST(Ed25519, SHA-512) threshold signing (RFC 9591) among a
person's own devices, wrapping the audited frost-ed25519 and
frost-core crates: distributed key generation, proactive resharing,
two-round signing, share envelopes, a nonce-store port, and
wire-driven coordinator and participant roles over manage-requests.
wire-mesh-threshold-wasm/ WebAssembly bindings for wire-mesh-threshold, so the TypeScript core
holds a real FROST key share without reimplementing the
cryptography. Byte-oriented wrappers only; no domain logic.
wire-mesh-conformance/ The conformance-check binary gating the frozen vectors in
../../conformance/*.v1.json.
```

## Codec guarantees

Every wire type implements `minicbor` `Encode`/`Decode` by hand (minicbor is pinned to exactly 2.2.2; the 2.x trait shapes differ from the widely documented 0.2x series) with two invariants:
Every wire type implements `minicbor` `Encode`/`Decode` by hand (minicbor is pinned to an exact version because its 2.x trait shapes differ from the widely documented 0.2x series) with two invariants:

- **CDE by construction** (RFC 8949 4.2 core deterministic encoding, the rules DAG-CBOR builds on): map keys are written in encoded-length-first-then-bytewise order, integer heads are minimal-length, lengths are always definite. Closed-key structs hard-code the order (pinned by the conformance vectors); open maps go through `CanonicalMap`; mixed typed-plus-tail maps through `CdeMapBuilder`, because an extension key can sort anywhere among the typed fields.
- **Strict decode**: unknown keys, wrong arity, indefinite lengths, floats, tags, duplicate keys, unsorted keys, non-minimal integer/string/array/map heads, non-32-byte device-ids, bad enum literals, and trailing bytes are all rejected — matching what cbor2's `cdeDecodeOptions` enforces on the TypeScript side, so both implementations accept exactly the same byte strings and any input that decodes also re-encodes to identical bytes.
Expand All @@ -36,19 +47,19 @@ cargo run --bin conformance-check

The root `justfile` already dispatches these (`just build`, `just test`, `just lint`, `just conformance`); `just conformance` runs this binary alongside the vector-generation gate in `conformance/`.

`conformance-check` proves all 26 frozen vectors two ways per vector: a typed decode → validate → re-encode → byte-compare loop (with a first-divergence hex diff on mismatch), and a reverse message-JSON → `CborValue` DOM → CDE encode → byte-compare loop that pins the `{"hex": ...}` byte-string convention and rejects non-integral JSON numbers rather than truncating them. For the three COSE vectors it additionally round-trips the nested protected headers and claims through the on-demand accessors, proving those re-encode identically too.
`conformance-check` proves every frozen vector two ways per vector: a typed decode → validate → re-encode → byte-compare loop (with a first-divergence hex diff on mismatch), and a reverse message-JSON → `CborValue` DOM → CDE encode → byte-compare loop that pins the `{"hex": ...}` byte-string convention and rejects non-integral JSON numbers rather than truncating them. For the three COSE vectors it additionally round-trips the nested protected headers and claims through the on-demand accessors, proving those re-encode identically too.

## Known upstream quirk (flagged, worked around)

The frozen `tokens.v1.json` protected headers encode the RFC 9052 `alg`/`kid` labels as *text* keys `"1"`/`"4"` (bytes `0x61 0x31`/`0x61 0x34`), not the CDDL's integer labels 1/4 (`0x01`/`0x04`) — a JS-object-key quirk from the vector generator. The wire crate accepts both forms on decode (`HeaderLabel::Int` vs `HeaderLabel::Text`), but only the integer form is canonical for *emitted* tokens: `CoseTokenHeaders`'s typed `alg`/`kid` fields populate from integer labels and re-emit as integer labels, while a text form decodes into `extra` verbatim so byte-exact round-tripping still holds. The vector generator should eventually be fixed to emit true integer labels and re-frozen; worth doing before more implementations hard-code the quirk.

## Scope and deferrals

Real domain logic exists for the three areas the plan prioritised — transport (TCP adapter with 4-byte big-endian length-prefixed framing, matching the TS adapter), handshake negotiation (version min + order-independent domain intersection, retired `core/federation` rejected distinctly), and capability tokens (COSE_Sign1 per RFC 9052 Sig_structure, self-certifying sha256(device-id) check, expiry/not-before, delegation-chain narrowing with expiry clamping, and the revocation obligations from `management.cddl`: entries verified as signed claims, only a token's own issuer may revoke it, and every ancestor token-id swept against the view, not just the leaf's). Everything else is wire types plus schema validation only. Deferred, deliberately:
Real domain logic exists for transport (a TCP adapter with 4-byte big-endian length-prefixed framing, matching the TS adapter), handshake negotiation (version min + order-independent domain intersection, retired `core/federation` rejected distinctly), capability tokens (COSE_Sign1 per RFC 9052 Sig_structure, self-certifying sha256(device-id) check, expiry/not-before, delegation-chain narrowing with expiry clamping, and the revocation obligations from `management.cddl`: entries verified as signed claims, only a token's own issuer may revoke it, and every ancestor token-id swept against the view, not just the leaf's), gossiped peer-advert verification, rooms, erasure coding and shard manifests byte-identical with the TS core, and threshold signing. The Rust side has no relay hub (the session runtime deliberately has no relay or hub role), no secure channel (the frames have a codec only, with no handshake, key schedule or sealing), no bulk transfer, no data sync and no exec or streaming engine: those frame families decode, encode and validate, but no code drives them. Deferred, deliberately:

- **TLS transport adapter** — the TCP adapter implements the same `Transport` port a TLS adapter will; no TLS code exists yet.
- **Exec/data/discovery services** — the frame families decode, encode, and validate, but no process spawning, oplog storage, or handle-resolution client/server exists. `handle-record` verification (signature + `sha256(identity-key) == device-id` + expiry) is available through the same COSE machinery as tokens, but nothing performs DNS fetches of `/.well-known/wire-mesh/<local-part>`.
- **Coordinator election logic** — the frame type exists; the term/lowest-device-id tiebreak is a receiver obligation documented in `transport.cddl` but no gossip engine implements it.
- **Revocation persistence wiring** — `RevocationView` verifies and admits entries in memory; a service that stores raw entries via the `KeyValueStorage` port and rebuilds the view on startup is not built.
- **Streaming backpressure enforcement** — the credit-window frames exist; no producer/consumer engine enforces `ack-seq + window` yet.
- **CDRL-to-Rust drift mitigation** — there is no CDDL-to-Rust generator (cddl.js emits TS/Zod only), so beyond the 26 vectors, drift is caught by the per-family unit tests derived from the CDDL text (key order, regex tiers over the registry's examples, arity, and unknown-key rejection). Vectors should be regenerated and this gate re-run whenever `spec/*.cddl` changes.
- **TLS transport adapter**: the TCP adapter implements the same `Transport` port a TLS adapter will; no TLS code exists on the Rust side.
- **Exec, data and discovery services**: no process spawning, oplog storage or handle-resolution client or server exists, and nothing performs DNS fetches of `/.well-known/wire-mesh/<local-part>`. `handle-record` verification (signature + `sha256(identity-key) == device-id` + expiry) is available through the same COSE machinery as tokens.
- **Coordinator election logic**: the frame type exists; the term/lowest-device-id tiebreak is a receiver obligation documented in `transport.cddl` but no gossip engine implements it.
- **Revocation persistence wiring**: `RevocationView` verifies and admits entries in memory; a service that stores raw entries via the `KeyValueStorage` port and rebuilds the view on startup is not built.
- **Streaming backpressure enforcement**: the credit-window frames exist; no producer/consumer engine enforces `ack-seq + window` yet.
- **CDDL-to-Rust drift mitigation**: there is no CDDL-to-Rust generator (cddl.js emits TS/Zod only), so beyond the frozen vectors, drift is caught by the per-family unit tests derived from the CDDL text (key order, regex tiers over the registry's examples, arity, and unknown-key rejection). Vectors should be regenerated and this gate re-run whenever `spec/*.cddl` changes.
15 changes: 11 additions & 4 deletions ts/packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom

## Ports

- **Transport** (`src/ports/transport.ts`) -- an async contract for sending/receiving `Frame` values over a connection. Adapter: `src/adapters/tcp-transport.ts` (length-prefixed CBOR frames over plain `node:net`, matching Cascade's own transport shape).
- **Storage** (`src/ports/storage.ts`) -- an async key/value contract. Adapter: `src/adapters/memory-storage.ts` (in-process, for tests and single-process nodes).
- **Transport** (`src/ports/transport.ts`): an async contract for sending/receiving `Frame` values over a connection. Adapters: `src/adapters/tcp-transport.ts` (length-prefixed CBOR frames over plain `node:net`, matching Cascade's own transport shape) and `src/adapters/tls-transport.ts` (the same framing over `node:tls`).
- **Storage** (`src/ports/storage.ts`): an async key/value contract. Adapters: `src/adapters/memory-storage.ts` (in-process, for tests and single-process nodes) and `src/adapters/node-fs-storage.ts` (filesystem).
- **Identity** (`src/ports/identity.ts`) -- device-id derivation and COSE signing/verification. Adapter: `src/adapters/node-identity.ts` (Web Crypto, ECDSA P-256 and Ed25519 -- the two algorithms the spec's own conformance vectors use).
- **Clock** (`src/ports/clock.ts`) -- injected time, never `Date.now()` pulled directly into domain logic. Adapter: `src/adapters/system-clock.ts`.

Expand All @@ -15,10 +15,17 @@ The TypeScript implementation of wire-mesh's protocol, built ports/adapters: dom
- **Capability-token verification** (`src/domain/tokens.ts`) -- the full chain tokens.cddl documents: COSE_Sign1 signature verification, the self-certifying issuer-key check (`sha256(issuer-key.public-key) == issuer`), expiry/not-before, issuer-matched revocation (only a token's own issuer's signed revocation-entry counts, checked across every ancestor in the delegation chain, not just the leaf), and recursive delegation-chain narrowing across all three axes of authority: a delegated token's issuer must be its parent's bearer (the chain is unbroken), its expiry must not exceed its parent's, and its scope must narrow its parent's (identical kind, equal-or-descendant path when the parent carries one) with an identical capability verb (the verb grammar has no sub-verb relation, so a different verb is different authority, not narrower). Also exports `verifyRevocationEntry` for ingesting gossiped revocation-announce frames: each entry is itself a signed, self-certifying COSE_Sign1 over revocation-claims, verified before it may enter the revocation view.
- **Peer-advert verification** (`src/domain/peer-advert.ts`): the two checks transport.cddl states for a gossiped advert, applied by the relay hub before it registers or forwards one and by every session before one reaches its directory: the embedded identity-key self-certifies (`sha256(identity-key.public-key) == device`), and the signature verifies under that same key over the domain-separated canonical encoding of the advert with its signature removed, which covers the open extension tail as well. Deliberately not tokens.cddl's COSE Sig_structure: an advert's open tail lets an attacker craft one whose encoding also satisfies token-claims, so the prefixed construction keeps the two byte strings disjoint. Reaches a verdict for every input rather than throwing, since an advert's algorithm and key bytes are attacker-chosen.
- **Secure channel** (`src/domain/secure-channel.ts`, `src/domain/relay-channels.ts`): the end-to-end channel spec/secure-channel.cddl defines between two devices that reach each other through a relay. A session opens one with each peer it addresses through a relay pairing, seals every manage-request, manage-response and core/data frame in it, drops any that arrives as plain relay-data, and attributes a request to the identity the peer's handshake proved rather than to the `from-device` a hub stamps. There is no plaintext fallback, so a peer that does not speak it cannot be reached through a relay.
- **Relay hub** (`src/domain/relay-hub.ts`, `src/domain/relay-pairing.ts`, `src/domain/hub-mailbox.ts`): the hub's relay role against the Transport port alone. It verifies every gossiped advert before registering, forwarding or replaying it, pairs devices for relayed traffic, and, as a mailbox, replicates the `core/data` logs of devices that are offline.
- **Bulk transfer** (`src/domain/bulk.ts`): resumable, flow-controlled `core/bulk` transfer, with a byte-credit window that the receiver's acknowledgements move. `src/domain/sharded-delivery.ts` composes it with erasure coding (`src/domain/erasure-coding.ts`) and shard manifests (`src/domain/shard-manifest.ts`) to spread a payload across holder devices.
- **Data sync** (`src/domain/data-sync.ts`): the have, request and entries primitive over a storage-backed oplog, which never accepts a gap. What to broadcast, whose logs to track and which holder to ask are left to the application.
- **Coordinator election** (`src/domain/coordinator-election.ts`): the gossiped, term-based election over `coordinator-frame`, where a higher term supersedes a lower one and equal terms are broken by lowest device-id.
- **Rooms** (`src/domain/room.ts`, `src/domain/room-path.ts`, `src/domain/room-token-verification.ts`, `src/domain/room-rekey.ts`, `src/domain/group-key.ts`, `src/domain/notice-board.ts`): self-certifying room-notice verification, owner-named and direct-message room paths, the room verifier obligations on top of token verification, content-key distribution and rotation, and the encrypted noticeboard that composes them with data sync.
- **Threshold signing** (`src/domain/threshold-network.ts`, `src/adapters/threshold-*.ts`): the `exadev.io/threshold` manage-commands for FROST(Ed25519) key generation, resharing and two-round signing, with the cryptography supplied by the WebAssembly build of the Rust threshold crate.
- **Mesh session** (`src/domain/mesh-session.ts`): the connection session that owns the handshake exchange, the gossip-built peer directory, manage-request dispatch and reconnection, and that the hub, LAN node and web console build on.

## Deliberately deferred
## Not implemented

Every other frame family (management/exec, streaming, data-domain, discovery, coordinator election) is covered by schema validation only -- `conformance-check` proves the generated schemas decode and re-encode every golden vector byte-exactly, including these families, but no domain-level business logic (dispatch, session bookkeeping, PTY/proc lifecycle, oplog replication, coordinator term tracking) exists for them yet. This first pass deliberately scopes domain logic to transport + handshake + tokens (the families with real business rules worth pinning down before the others), because the conformance suite already covers every family's wire shape either way -- nothing here is unverified, only unimplemented. (The federation link protocol no longer exists in the spec at all: cross-scope sharing is ordinary capability-token delegation, and `src/domain/tokens.ts`'s verification chain is exactly the mechanism that governs it.)
Exec and streaming are schema only: `conformance-check` proves the generated schemas decode and re-encode their golden vectors byte-exactly, but no code spawns a process or PTY (`proc.*`, `pty.*`) or sends, receives or enforces the credit window of a stream (`stream-data` and its acknowledgements). Tracked in [#341](https://github.com/ExaDev/wire-mesh/issues/341). Direct-path NAT traversal is not implemented either, so a connection across a NAT goes through the relay; tracked in [#343](https://github.com/ExaDev/wire-mesh/issues/343). Nothing resolves DNS-anchored handles. The federation link protocol no longer exists in the spec: cross-scope sharing is ordinary capability-token delegation, and the verification chain in `src/domain/tokens.ts` is the mechanism that governs it.

## Building from a fresh checkout

Expand Down