Files
quicproquo/docs/src/wire-format/delivery-schema.md
Christian Nennemann f334ed3d43 feat: add post-quantum hybrid KEM + SQLCipher persistence
Feature 1 — Post-Quantum Hybrid KEM (X25519 + ML-KEM-768):
- Create hybrid_kem.rs with keygen, encrypt, decrypt + 11 unit tests
- Wire format: version(1) | x25519_eph_pk(32) | mlkem_ct(1088) | nonce(12) | ct
- Add uploadHybridKey/fetchHybridKey RPCs to node.capnp schema
- Server: hybrid key storage in FileBackedStore + RPC handlers
- Client: hybrid keypair in StoredState, auto-wrap/unwrap in send/recv/invite/join
- demo-group runs full hybrid PQ envelope round-trip

Feature 2 — SQLCipher Persistence:
- Extract Store trait from FileBackedStore API
- Create SqlStore (rusqlite + bundled-sqlcipher) with encrypted-at-rest SQLite
- Schema: key_packages, deliveries, hybrid_keys tables with indexes
- Server CLI: --store-backend=sql, --db-path, --db-key flags
- 5 unit tests for SqlStore (FIFO, round-trip, upsert, channel isolation)

Also includes: client lib.rs refactor, auth config, TOML config file support,
mdBook documentation, and various cleanups by user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 08:07:48 +01:00

9.3 KiB

Delivery Schema

Schema file: schemas/delivery.capnp File ID: @0xc5d9e2b4f1a83076

The DeliveryService interface defines the RPC contract for the store-and-forward message relay. The DS is intentionally MLS-unaware: it routes opaque byte strings by recipient key and optional channel ID without parsing or inspecting the content.


Full schema listing

# delivery.capnp -- Delivery Service RPC interface.
#
# The Delivery Service is a simple store-and-forward relay. It does not parse
# MLS messages -- all payloads are opaque byte strings routed by recipient key.
#
# Callers are responsible for:
#   - Routing Welcome messages to the correct new member after add_members().
#   - Routing Commit messages to any existing group members (other than self).
#   - Routing Application messages to the intended recipient(s).
#
# The DS indexes queues by the recipient's raw Ed25519 public key (32 bytes),
# matching the indexing scheme used by the Authentication Service.
#
# ID generated with: capnp id
@0xc5d9e2b4f1a83076;

interface DeliveryService {
  # Enqueue an opaque payload for delivery to a recipient.
  #
  # recipientKey : Ed25519 public key of the intended recipient (exactly 32 bytes).
  # payload      : Opaque byte string -- a TLS-encoded MlsMessageOut blob or any
  #                other framed data the application layer wants to deliver.
  # channelId    : Optional channel identifier (empty for legacy). A 16-byte UUID
  #                is recommended for 1:1 channels.
  # version      : Schema/wire version. Must be 0 (legacy) or 1 (this spec).
  #
  # The payload is appended to the recipient's FIFO queue. Returns immediately;
  # the recipient retrieves it via `fetch`.
  enqueue @0 (recipientKey :Data, payload :Data, channelId :Data, version :UInt16) -> ();

  # Fetch and atomically drain all queued payloads for a given recipient.
  #
  # recipientKey : Ed25519 public key of the caller (exactly 32 bytes).
  # channelId    : Optional channel identifier (empty for legacy).
  # version      : Schema/wire version. Must be 0 (legacy) or 1 (this spec).
  #
  # Returns the complete queue in FIFO order and clears it. Returns an empty
  # list if there are no pending messages.
  fetch @1 (recipientKey :Data, channelId :Data, version :UInt16) -> (payloads :List(Data));
}

Method-by-method analysis

enqueue @0

enqueue (recipientKey :Data, payload :Data, channelId :Data, version :UInt16) -> ()

Purpose: Append an opaque payload to a recipient's delivery queue. The DS stores the payload until the recipient fetches it. The call returns immediately after the payload is enqueued; it does not block until delivery.

Parameters:

Parameter Type Size Description
recipientKey Data Exactly 32 bytes Ed25519 public key of the intended recipient. Used as the primary queue index.
payload Data Variable (bounded by transport max) Opaque byte string. Typically a TLS-encoded MlsMessageOut blob, but the DS does not inspect it.
channelId Data 0 bytes (legacy) or 16 bytes (UUID) Channel identifier for channel-aware routing. Empty Data is treated as the legacy default channel.
version UInt16 2 bytes Schema/wire version. 0 = legacy (no channel routing), 1 = current spec (channel-aware).

Return value: Void. The method returns () on success. Errors are surfaced as Cap'n Proto RPC exceptions.

Queue semantics: Payloads are appended in FIFO order. The DS does not deduplicate, reorder, or inspect payloads. Multiple enqueue calls for the same recipient and channel ID are simply appended to the queue in the order they arrive.

fetch @1

fetch (recipientKey :Data, channelId :Data, version :UInt16) -> (payloads :List(Data))

Purpose: Fetch and atomically drain all queued payloads for a given recipient on a given channel. This is the "pull" side of the store-and-forward relay.

Parameters:

Parameter Type Size Description
recipientKey Data Exactly 32 bytes Ed25519 public key of the caller. Must match the key used in the enqueue calls.
channelId Data 0 bytes (legacy) or 16 bytes (UUID) Channel identifier. Must match the channelId used during enqueue.
version UInt16 2 bytes Schema/wire version. Must match the version used during enqueue.

Return value:

Field Type Description
payloads List(Data) All queued payloads in FIFO order. Empty list if no messages are pending.

Atomic drain: The fetch operation returns the entire queue and clears it in a single atomic operation. There is no "peek" or partial fetch. This simplifies the concurrency model: the client processes all returned payloads and does not need to track which ones it has already seen.


Channel-aware routing

The channelId field enables per-channel queue separation. Each unique (recipientKey, channelId) pair maps to an independent FIFO queue on the server.

Compound key structure

Queue Key = recipientKey (32 bytes) || channelId (0 or 16 bytes)

When channelId is empty (0 bytes), the queue key degenerates to just the recipientKey, preserving backward compatibility with legacy clients that do not use channels.

Channel ID format

The recommended format for channelId is a 16-byte UUID (128-bit, typically UUID v4). The DS treats the channel ID as an opaque byte string and does not parse its structure. Using UUIDs provides:

  1. Collision resistance -- 2^122 random bits (for UUID v4) makes accidental collision negligible.
  2. Privacy -- The channel ID reveals no information about the channel's participants or purpose.
  3. Fixed size -- 16 bytes is compact and predictable for indexing.

Use cases

Scenario channelId recipientKey Result
Legacy client, no channels Empty (0 bytes) Alice's Ed25519 key Single queue for all of Alice's messages
1:1 channel between Alice and Bob UUID of the 1:1 channel Alice's Ed25519 key Separate queue for this specific channel
Group channel UUID of the group channel Alice's Ed25519 key Separate queue for this group's messages to Alice

Version field

The version field provides a mechanism for wire-level schema evolution without breaking existing clients.

Version Semantics
0 Legacy mode. channelId is ignored (treated as empty). Behaves like the pre-channel DeliveryService.
1 Current specification. channelId is used for channel-aware routing.

The server validates the version field and rejects unknown versions as protocol errors. Clients must set the version field to match the schema revision they implement.


FIFO queue semantics

The Delivery Service provides strict FIFO ordering within each (recipientKey, channelId) queue:

  1. Enqueue order is preserved. Payloads are returned by fetch in the exact order they were enqueued.
  2. Atomic drain. Each fetch call returns all pending payloads and clears the queue. There is no risk of partial reads or interleaving.
  3. No persistence guarantees (current implementation). The in-memory queue is lost on server restart. Persistent storage is planned for a future milestone.
  4. No redelivery. Once a payload is returned by fetch, it is permanently removed. If the client crashes before processing it, the payload is lost. Reliable delivery with acknowledgments is a future enhancement.

MLS-unaware design

The DS intentionally does not parse, validate, or inspect MLS messages. All payloads are opaque Data blobs. This design has several consequences:

  • Security: The server cannot extract plaintext from MLS ciphertext, even if compromised.
  • Simplicity: The DS has no dependency on openmls or any MLS library.
  • Flexibility: The same DS can carry non-MLS payloads (e.g., signaling, metadata) without modification.
  • No server-side optimization: The DS cannot optimize delivery based on MLS message type (e.g., fanning out a Commit to all group members). The client must enqueue separately for each recipient.

For the full design rationale, see ADR-004: MLS-Unaware Delivery Service.


Relationship to NodeService

In the current unified architecture, the Delivery Service methods are exposed as part of the NodeService interface with additional methods:

DeliveryService Method NodeService Method Additional Parameters
enqueue @0 enqueue @2 auth :Auth
fetch @1 fetch @3 auth :Auth
(none) fetchWait @4 auth :Auth, timeoutMs :UInt64

The fetchWait method is a NodeService extension that provides long-polling semantics: it blocks until either new payloads arrive or the timeout expires. This avoids the latency and bandwidth overhead of repeated fetch polling.


Further reading