# Datastore Auth: Development History **Branch:** `swactor-auth` **Base commit:** `af15416` (pre-auth baseline) **5 commits + uncommitted working tree changes** --- ## What Was Built A complete ed25519 authorization layer for the distributed datastore, spanning: - **Auth engine** — `AuthzEngine` with ACL, signed request verification, replay protection, nonce GC - **GatewayActor** — actor-level enforcement point with grant/revoke, access requests, key listing - **Browser auth flow** — WASM Ed25519 crypto, device key generation, access request/grant/deny lifecycle - **Admin page** — owner key upload, pending request management, manual key grant, authorized key list - **Expanded CLI** — full CRUD + auth subcommands (`grant`, `revoke`, `requests`, `keys`, `deny`) with name resolution - **Storage persistence** — entry/manifest persistence to filesystem, startup bulk-load - **xtask** — `node`, `cli`, `wasm` subcommands with `config.toml` support - **WASM crypto crate** — `crates/crypto-wasm/`, a `no_std` cdylib exporting `ed25519_sign()`, `get_public_key()`, `buffer_ptr()` The auth system enforces binary access control (authorized or not) at the HTTP API boundary. Internal actors remain auth-unaware. Two auth paths: connection-level (iroh QUIC handshake proves NodeId) and per-request signed envelopes (for browser/HTTP API). This branch implements Path 2 end-to-end, including the browser UX. --- ## Architecture ``` ┌──────────────────────────────────────────────────────┐ │ HTTP API (api.rs) │ │ │ │ Ungated: │ │ GET / → browser UI (access page) │ │ GET /admin → admin page │ │ GET /crypto.wasm → WASM Ed25519 module │ │ GET /api/status → node identity │ │ │ │ Auth-gated (X-Signed-Request header): │ │ POST /api/put → check_auth → handle_put │ │ GET /api/get → check_auth → handle_get │ │ GET /api/data → check_auth → handle_data │ │ POST /api/delete → check_auth → handle_delete│ │ GET /api/list → check_auth → handle_list │ │ │ │ Auth management (owner-only): │ │ POST /api/auth/grant → check_auth_identity │ │ POST /api/auth/revoke → check_auth_identity │ │ GET /api/auth/requests→ check_auth_identity │ │ GET /api/auth/keys → check_auth_identity │ │ POST /api/auth/deny → check_auth_identity │ │ │ │ Signature-only (proves key, no ACL check): │ │ POST /api/auth/request → check_auth_sig_only │ └───────────────┬─────────────────────────────────────┘ │ GatewayMsg (various) │ ┌───────────────▼───────────────┐ │ GatewayActor │ │ │ │ AuthzEngine: │ │ 1. verify ed25519 signature │ │ 2. check timestamp ±300s │ │ 3. check nonce uniqueness │ │ 4. check ACL │ │ │ │ Access request management: │ │ pending_requests HashMap │ │ grant resolves label from │ │ pending request name │ │ │ │ ACL persistence: │ │ persist_acl() on grant/ │ │ revoke │ └───────────────┬───────────────┘ │ ┌───────────────▼───────────────┐ │ DatastoreNode │ │ │ │ MetadataActor ◄──► BlobStore │ │ (auth-unaware) │ └───────────────────────────────┘ ┌───────────────────────────────┐ │ Browser (WASM Ed25519) │ │ │ │ /crypto.wasm → initCrypto() │ │ deviceKeySeed in localStorage │ │ signBytes() per request │ │ Access action for all ops │ │ → X-Signed-Request header │ └───────────────────────────────┘ ┌───────────────────────────────┐ │ CLI (store_cli) │ │ │ │ --key owner.key.json │ │ Per-action signing: │ │ Put/Get/Delete/List/Access │ │ Name resolution for │ │ grant/revoke/deny │ └───────────────────────────────┘ ``` --- ## Commit-by-Commit ### `863185e` — feat: distributed datastore primitives protocol Foundation commit establishing the distributed datastore protocol. Defined the protocol messages (`GetChunkRequest`, `FindObjectRequest`, `StoreObjectRequest`, `ListObjectsRequest` and their responses), all implementing `NetworkMessage` with stable `type_tag()` strings. This is the wire protocol for inter-node communication over iroh/QUIC. **Key files:** `src/messages.rs` (inter-node message types) ### `84c4408` — fix: cli for datastore works Brought up the `store_node` and `store_cli` binaries as `[[bin]]` targets with feature-gated dependencies (`node` and `cli` features). The node binary spawns the actor runtime, wires up BlobStoreActor/MetadataActor/DatastoreNode, and serves the HTTP API via `tiny_http`. The CLI binary talks to the node over HTTP with `ureq`. Added `clap` for arg parsing, `ctrlc` for graceful shutdown, and `runtime-dashboard` integration. **Key files:** `Cargo.toml` (features `node`/`cli`), `src/bin/store_node.rs`, `src/bin/store_cli.rs`, `src/api.rs` ### `ebee109` — feat: mvp auth protocol Core auth implementation: - **`src/auth.rs`** — `DatastoreAction` enum, `SignedRequestPayload`, `SignedRequest` envelope, `AccessControlList` (with JSON persistence via `save()`/`load_or_create()`), `AuthzEngine` (4-step verification: signature, timestamp, nonce, ACL), `sign_request()`/`verify_signed_request()` helpers, `AuthzResult`/`DeniedReason` enums. - **`src/actors/gateway.rs`** — `GatewayActor` wrapping `AuthzEngine`. Handles `Authorize` (pure auth check), `HandleSignedRequest` (auth + dispatch via `action_to_node_msg()`), `CheckConnection` (Path 1), `Grant`/`Revoke` (owner-only ACL mutations), `NonceGcTick`. - **`src/messages.rs`** — `GatewayMsg` enum, `DatastoreResponse::Denied` variant. - **`crates/shared-types/`** — Extracted `ContentHash` into its own crate to break dependency cycles between `distribution` and `datastore`. Tests added (18 total): - `auth_scenario_tests.rs` (10 tests) — owner access, stranger denial, grant/revoke lifecycle, signed request happy path, tampered signature, stale timestamp, replayed nonce, nonce GC, non-owner grant/revoke rejection. - `acl_persistence_tests.rs` (2 tests) — save/load round-trip, create-on-missing. - `gateway_tests.rs` (4 tests) — connection allow/deny, signed request flow-through, unauthorized signed request denial. ### `8ac45e5` — fix: adjust auth protocol to datastore protocol Aligned the auth types with the content-hash-first datastore protocol: - `DatastoreAction::Put` carries `content_hash`, `size_bytes`, and `tags` (not raw data). - `DatastoreAction::Get`/`Delete` use `content_hash`. - `DatastoreAction::List` uses `name_filter`. - `GatewayActor::action_to_node_msg()` maps actions to `DatastoreNodeMsg` variants. - Wired `check_auth()` into the HTTP API handlers (put, get, data, delete, list) — reads `X-Signed-Request` header, sends `GatewayMsg::Authorize` to the gateway actor, denies with 401/403/504 on failure. - `handle_status` intentionally left ungated. ### `e549eef` — feat: auth MVP with integrated tests Wired auth into both binaries: **`store_node.rs`** — `--auth` and `--auth-dir ` flags: - Loads or generates owner keypair from `/owner.key.json`. - Owner keypair's public key becomes the `NodeId` (deterministic identity across restarts). - Loads/creates `/acl.json` with owner as sole authorized key. - Spawns `GatewayActor` and passes `Some(gateway_addr)` to `start_api_server`. - Sends `GatewayMsg::NonceGcTick` on the same cadence as the metadata GC tick. **`store_cli.rs`** — `--key ` flag: - Each command builds the appropriate `DatastoreAction`, signs it, sends as `X-Signed-Request` header. - `status` never signs (always open by design). **`http_auth_integration.rs`** — Full-stack integration test: spins up the actor runtime with GatewayActor, starts the HTTP server, proves owner is allowed (PUT/GET/LIST/DELETE), stranger gets 403, missing header gets 401. --- ## Uncommitted Working Tree Changes The uncommitted changes represent the bulk of the user-facing work: browser UI, admin page, WASM crypto, expanded CLI, storage persistence, and xtask. ### Browser UI (`ui_html.rs` — `DATASTORE_UI_HTML`) Complete browser access page served at `/`: - **Upload panel** — file input + optional name, PUT via `authFetch()` - **Object table** — list all objects with hash, name, size; download and delete buttons - **Detail modal** — click a row to see full metadata, chunks, tags - **Auth detection** — on load, `detectAuth()` fetches `/api/list`; if 401, enables auth mode - **WASM crypto integration** — `initCrypto()` fetches `/crypto.wasm`, `initKeys()` generates or loads device seed from `localStorage`, derives public key via WASM - **Auth banner** — shown when user is not authorized, with access request form (name + optional message) - **Pending state** — after submitting request, shows "waiting for operator approval" with 5-second polling; auto-refreshes when granted - **Device key display** — shows truncated public key hex in header when auth is active - **JWK migration** — handles legacy `localStorage.deviceKey` (JWK format) by extracting the `d` parameter as seed ### Admin Page (`ui_html.rs` — `DATASTORE_ADMIN_HTML`) Owner administration page served at `/admin`: - **Owner key upload** — file input for `key.json`, loads secret/public key hex, derives via WASM to verify, test call to `/api/auth/requests` to confirm ownership - **Pending access requests table** — name, message, key (truncated), grant/deny buttons - **Authorized keys table** — name (label), key (truncated), revoke button - **Manual grant form** — input for 64-char hex public key + optional name - **Name disambiguation** — when multiple entries share the same name, appends `(key_prefix)` suffix - **`ownerAuthFetch()`** — signs all admin API calls with `DatastoreAction::Access` ### WASM Ed25519 Crypto (`crates/crypto-wasm/`) New `no_std` Rust crate compiled to `wasm32-unknown-unknown`: - **`Cargo.toml`** — `swactor-crypto-wasm`, `cdylib` crate type, depends on `ed25519-dalek` (no default features) - **`src/lib.rs`** — Three exported functions: - `buffer_ptr()` → pointer to 8192-byte shared buffer - `get_public_key()` — reads 32-byte seed from `BUF[0..32]`, writes public key to `BUF[32..64]` - `ed25519_sign(msg_len)` — reads seed from `BUF[0..32]`, message from `BUF[128..128+msg_len]`, writes 64-byte signature to `BUF[64..128]` - **`crypto_wasm.wasm`** — pre-built binary embedded in the datastore via `include_bytes!("crypto_wasm.wasm")` - Served at `/crypto.wasm` endpoint (ungated) - Replaces the earlier Web Crypto API approach — Web Crypto's Ed25519 support is inconsistent across browsers; WASM provides deterministic behavior using the same `ed25519-dalek` crate as the Rust backend ### Expanded GatewayActor (`actors/gateway.rs`) New message handlers beyond the original `Authorize`/`HandleSignedRequest`/`CheckConnection`/`Grant`/`Revoke`: - **`VerifySignature`** — calls `check_signature_only()` (no ACL check). Used for access request submissions where the caller needs to prove key ownership without being in the ACL. - **`SubmitAccessRequest`** — stores `AccessRequestInfo { key, name, message, requested_at }` in `pending_requests: HashMap`. - **`ListAccessRequests`** — owner-only; returns all pending requests. - **`DenyAccessRequest`** — owner-only; removes a pending request. - **`ListAuthorizedKeys`** — owner-only; returns `Vec` with labels. Grant now resolves labels: when granting a key that has a pending request, the request's `name` field becomes the key's label (unless an explicit label is provided). ### Expanded Auth Types (`auth.rs`) - **`AccessRequestInfo`** — `{ key: NodeId, name: String, message: String, requested_at: u64 }` - **`AuthorizedKeyInfo`** — `{ key: NodeId, label: String }` - **`DatastoreAction::Access`** — new variant for browser-originated requests that prove identity without binding to specific content. The browser uses `Access` for all operations (auth is at the HTTP layer). - **`key_labels: HashMap`** added to `AccessControlList` — maps hex public key to human-readable name. Populated by `grant()`, removed by `revoke()`. - **`check_signature_only()`** on `AuthzEngine` — verifies signature, timestamp, and nonce but skips ACL check. - **`authorized_key_list()`** on `AuthzEngine` — returns all authorized keys with their labels. ### Storage Persistence (`storage/mod.rs`, `storage/in_memory.rs`) Extended `StorageBackend` trait with entry persistence: - **`write_entry()`** / **`read_entry()`** / **`delete_entry()`** / **`list_entries()`** — persist `ObjectEntry` JSON to disk - **`FilesystemBackend`** layout extended: ``` {root}/ ├── chunks/{hex[0..2]}/{hex[2..4]}/{full_hex_hash} ├── manifests/{hex[0..2]}/{hex[2..4]}/{full_hex_hash} └── entries/{hex[0..2]}/{hex[2..4]}/{full_hex_hash} ``` - **`BlobStoreMsg::WriteEntry`** / **`DeleteEntry`** — fire-and-forget messages for entry persistence - **`BlobStoreMsg::LoadAll`** — startup bulk-load of all entries + their manifests - **`MetadataMsg::BulkLoad`** — injects loaded entries into MetadataActor's index - **`store_node.rs` startup sequence** — sends `LoadAll` to BlobStoreActor, polls for `LoadedAll` response, sends `BulkLoad` to MetadataActor ### Expanded CLI (`store_cli.rs`) Full CRUD + auth management subcommands: | Subcommand | Auth | Description | |------------|------|-------------| | `put [--name]` | `--key` signs `DatastoreAction::Put` | Upload a file | | `get [--output]` | `--key` signs `DatastoreAction::Get` | Metadata or download | | `delete ` | `--key` signs `DatastoreAction::Delete` | Delete an object | | `list [--name] [--all]` | `--key` signs `DatastoreAction::List` | List objects | | `status` | Never signed | Node identity | | `grant [--name]` | `--key` signs `Access` | Authorize a key (owner-only) | | `revoke ` | `--key` signs `Access` | Revoke a key (owner-only) | | `requests` | `--key` signs `Access` | List pending access requests | | `keys` | `--key` signs `Access` | List authorized keys | | `deny ` | `--key` signs `Access` | Deny a pending request | **Name resolution:** `grant`, `revoke`, and `deny` accept either a 64-char hex key or a human-readable name. When given a name, the CLI fetches the relevant list from the API and resolves the name to a key. Disambiguated names (`"alice (c9d0e1f2)"`) are supported. ### xtask (`xtask/src/main.rs`) Development task runner with three new subcommands beyond the existing `test`: - **`cargo xtask node`** — builds and runs `swactor-store-node`. Flags: `--port`, `--storage-path`, `--auth` (default: true), `--auth-dir`. Builds with `--features node` first, then runs the binary directly (not via `cargo run`) to avoid SIGINT issues. Ignores SIGINT in the xtask process so the child handles Ctrl-C. - **`cargo xtask cli`** — builds and runs `swactor-store`. Flags: `--url`, `--key`. Auto-detects `./auth/owner.key.json` if present. Passes extra args through. - **`cargo xtask wasm`** — builds `swactor-crypto-wasm` for `wasm32-unknown-unknown --release`, copies the output to `crates/datastore/src/crypto_wasm.wasm`, optionally runs `wasm-strip`. - **`config.toml` support** — reads `xtask/config.toml` for default values (node port, storage path, auth settings, CLI url/key). **`xtask/Cargo.toml`** — added `toml`, `serde`, `libc` dependencies. ### HTTP API Expansion (`api.rs`) New endpoints: | Method | Path | Auth | Handler | |--------|------|------|---------| | `POST` | `/api/auth/grant?key=[&name=