Skip to content

logos-messaging/chat-store

Repository files navigation

Chat Store

Persistence for group-chat users' key packages — the chat-store HTTP service (formerly keypackage-registry), extracted from libchat so it can be deployed on its own.

Standalone HTTP service that caches MLS KeyPackages keyed by device_id, so a client can fetch a contact's keypackage without an out-of-band exchange. Throwaway by design: scheduled to be replaced by a λLEZ-based service in v0.3, so it intentionally has no overlap with the rest of libchat (axum + rusqlite only).

device_id is the hex-encoded 32-byte Ed25519 verifying key of a device.

It also runs a minimal account service: one signed blob per account_pub mapping an Account to its set of device (LocalIdentity) public keys, so clients can invite every LocalIdentity of an account. account_pub is the hex-encoded 32-byte Ed25519 AccountAddress verifying key. See Account device-list endpoints.

Trust model

A bundle is an opaque payload plus its signature, published under a device_id (the hex of the device's 32-byte Ed25519 verifying key). The signed bytes and the wire bytes are identical, so a verifier checks the signature over exactly what it received, no reconstruction.

The server treats payload as a black box: it never decodes it. It only verifies that signature over the payload bytes is valid under device_id's key, then stores it. A valid signature is proof-of-possession — only the holder of device_id's key can publish under it — so an adversary can't publish under a device_id it doesn't control, and junk is dropped before storage. The server is not a trusted authority, so consumers MUST also verify on retrieve, and a valid signature does not prove the device is authorized for any account (that binding arrives with λLEZ in v0.3).

Consumers define the payload layout. Today it is:

payload = timestamp_ms_le[8] || key_package[..]

Fixed-width field first with the variable key_package last makes it parse exactly one way — no delimiter, even though key_package is arbitrary bytes.

Building & running

cargo build --release
./target/release/chat-store   # binds 0.0.0.0:8080, db ./chat-store.db
Flag Default Description
--bind <addr> 0.0.0.0:8080 HTTP bind address
--db <path> chat-store.db SQLite database path
--max-per-identity <n> 100 Bundles retained per device_id
--retention-days <n> 30 Drop bundles older than this
--prune-interval-secs <n> 3600 How often the prune task runs

Logs via RUST_LOG (default info).

Docker

# Build the image
docker build -t chat-store .

# Run it, persisting the SQLite db on a named volume and exposing port 8080
docker run --rm -p 8080:8080 -v chat-store-data:/data chat-store

The image runs the binary with --bind 0.0.0.0:8080 --db /data/chat-store.db by default; override the CMD to change flags, e.g.:

docker run --rm -p 9000:9000 -v chat-store-data:/data chat-store \
  --bind 0.0.0.0:9000 --db /data/registry.db --retention-days 14

API

POST /v0/keypackage

{
  "device_id": "hex(32-byte ed25519 verifying key)",
  "payload":   "base64(opaque signed bytes)",
  "signature": "base64(64-byte ed25519 signature over payload)"
}

The server verifies signature over the (opaque) payload bytes under device_id's key before storing, keyed by device_id. It does not decode payload. Returns 204 on success, 400 on malformed input or a signature that fails to verify.

GET /v0/keypackage/{device_id}

Returns the most recently submitted bundle for that device_id, or 404:

{
  "payload":   "base64(...)",
  "signature": "base64(64-byte ed25519 signature)"
}

Consumers verify signature over the payload bytes using the key recovered from device_id, then read key_package out of the payload. A bundle that fails verification must be treated as not found.

Account device-list endpoints

The account service stores exactly one blob per account_pub mapping an Account to its LocalIdentity device keys. Same trust model as keypackages: the server verifies signature over payload under account_pub's key (proof-of-possession), and consumers MUST re-verify on retrieve. Clients encode a lamport-timestamped list of device public keys in payload; the rest of the payload stays opaque to the server.

Anti-replay: the server reads the lamport from the (signature-verified) payload and replaces the stored bundle only when the incoming lamport is strictly higher, returning 409 otherwise. Because the lamport is covered by the account signature it cannot be forged, so a replayed older-but-still-valid bundle cannot downgrade the device list, nor refresh the retention clock. Consumers should still compare lamports themselves as defence in depth.

POST /v0/account

Upsert the device-list bundle for an account; replaces any previous value.

{
  "account_pub": "hex(32-byte ed25519 AccountAddress verifying key)",
  "payload":     "base64(opaque signed bytes: lamport-ts + device pubkeys)",
  "signature":   "base64(64-byte ed25519 signature over payload by the account key)"
}

Returns 204 on success, 400 on malformed input or a signature that fails to verify, and 409 when the bundle's lamport is not newer than the stored one (replay / stale publish).

GET /v0/account/{account_pub}

Returns the stored bundle for that account, or 404:

{
  "payload":    "base64(...)",
  "signature":  "base64(64-byte ed25519 signature)",
  "updated_at": 1700000000000
}

updated_at is the server's last-upsert time in Unix ms. Consumers verify signature over payload under account_pub's key, then decode the device list.

Storage & retention

Two SQLite tables: keypackages keyed by device_id, and account_bundles (one row per account_pub). A background task runs every --prune-interval-secs, dropping keypackage bundles older than --retention-days (keeping at most --max-per-identity per device_id) and dropping account bundles not refreshed within --retention-days. The schema is an internal detail and may change.

Smoke test

The quickest end-to-end check is the bundled smoke_test example. It generates throwaway Ed25519 keys, signs and publishes a keypackage and an account bundle, fetches both back, and confirms the replay guard:

# Terminal 1 — start a server with a fresh db
cargo run -- --bind 127.0.0.1:8080 --db tmp/chat-store.db

# Terminal 2 — run the example against it (defaults to http://127.0.0.1:8080)
cargo run --example smoke_test
# or point it elsewhere:
cargo run --example smoke_test -- http://127.0.0.1:8080

Expected output:

POST /v0/keypackage        -> 204 No Content (expect 204)
GET  /v0/keypackage/<id>   -> 200 OK (expect 200) {"payload":...,"signature":...}
POST /v0/account           -> 204 No Content (expect 204)
GET  /v0/account/<id>      -> 200 OK (expect 200) {"payload":...,"signature":...,"updated_at":...}
POST /v0/account (replay)  -> 409 Conflict (expect 409)

You can also exercise it with the real chat-cli (which lives in the libchat repo) against a running server:

# In this repo: start the server on a test port with a fresh db
cargo run -- --bind 127.0.0.1:18080 --db tmp/registry.db

# In a libchat checkout: register two identities (--smoketest exits after registering)
cargo build -p chat-cli
./target/debug/chat-cli --name alice --transport file --data tmp/alice \
  --registry-url http://127.0.0.1:18080 --smoketest    # exits 0 on success
./target/debug/chat-cli --name bob   --transport file --data tmp/bob \
  --registry-url http://127.0.0.1:18080 --smoketest

# Confirm both bundles landed
sqlite3 tmp/registry.db "SELECT substr(device_id,1,12), length(payload) FROM keypackages;"

A non-zero exit from chat-cli means the server rejected the submission — e.g. the signature failed verification. GET /v0/keypackage/{device_id} returns 200 for a registered device and 404 otherwise.

Lifecycle

Exists to unblock contact-by-id flows on testnet; removed once λLEZ-based discovery lands in v0.3. The seam is the RegistrationService trait in libchat (core/conversations/src/service_traits.rs) — swapping implementations does not touch the chat protocol.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors