# Datastore Auth: Development History **Branch:** `swactor-auth` **Base commit:** `3d5a539` (feat: distributed datastore primitives protocol) **Companion spec:** `DATASTORE_AUTH.md` (root) --- ## What Was Built An ed25519 authorization layer for the datastore, spanning the full stack from crypto primitives through actor enforcement to CLI/binary wiring. Three commits of protocol work, plus uncommitted binary integration. The auth system enforces binary access control (authorized or not) at the HTTP API boundary. Internal actors remain auth-unaware. Two auth paths exist: connection-level (iroh QUIC handshake proves NodeId) and per-request signed envelopes (for browser relay and HTTP API). This work implements Path 2 end-to-end. --- ## Commit-by-Commit ### `ebf778f` — fix: cli for datastore works Brought up the `store_node` and `store_cli` binaries as `[[bin]]` targets with feature-gated dependencies. The node binary spawns the actor runtime, wires up BlobStoreActor/MetadataActor/DatastoreNode, and serves the HTTP API. The CLI binary talks to the node over HTTP with `ureq`. Added `tiny_http` for the API server, `clap` for arg parsing, `ctrlc` for graceful shutdown, and the `runtime-dashboard` integration. Key files: `Cargo.toml` (features `node`/`cli`), `src/bin/store_node.rs`, `src/bin/store_cli.rs`, `src/api.rs` ### `7c1c2c1` — feat: mvp auth protocol Core auth implementation: - **`src/auth.rs`** — `DatastoreAction`, `SignedRequestPayload`, `SignedRequest`, `AccessControlList` (with JSON persistence), `AuthzEngine` (signature verification, timestamp window, nonce replay detection, ACL check), `sign_request()` / `verify_signed_request()` helpers, `DeniedReason` enum. - **`src/actors/gateway.rs`** — `GatewayActor` wrapping `AuthzEngine`. Handles `Authorize` (pure auth check), `HandleSignedRequest` (auth + dispatch), `CheckConnection` (Path 1), `Grant`/`Revoke` (owner-only ACL mutations), `NonceGcTick`. Translates `DatastoreAction` to `DatastoreNodeMsg` via `action_to_node_msg()`. - **`src/messages.rs`** — `GatewayMsg` enum, `DatastoreResponse::Denied` variant. - **`shared-types` crate** — Extracted `ContentHash` into its own crate so both `distribution` and `datastore` can depend on it without cycles. Tests added: - `auth_scenario_tests.rs` (12 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. ### `6366c6b` — 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` (not name). - `DatastoreAction::List` uses `name_filter`. - `GatewayActor::action_to_node_msg` maps actions to the existing `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. ### Uncommitted — Wire auth into node & CLI binaries The auth engine and HTTP gate existed but neither binary used them. This change connects them: **`store_node.rs`** — `--auth` and `--auth-dir ` flags: - When `--auth`: loads or generates owner keypair from `/owner.key.json` (JSON with hex-encoded keys, version field, public_key for inspection, ISO-8601 created_at). - Owner keypair's public key becomes the `NodeId` (deterministic identity across restarts). - Loads/creates `/acl.json` with owner as sole authorized key. - Spawns `GatewayActor` with the `AuthzEngine` and passes `Some(gateway_addr)` to `start_api_server`. - Sends `GatewayMsg::NonceGcTick` on the same cadence as the metadata GC tick. - Without `--auth`, behavior is unchanged (random NodeId, no gateway, `None` passed to API). **`store_cli.rs`** — `--key ` flag: - Loads keypair from the same JSON key file format. - Each command (put/get/delete/list) builds the appropriate `DatastoreAction`, creates a `SignedRequestPayload` with current timestamp + `getrandom` nonce, signs it, and sends the JSON as `X-Signed-Request` header. - `status` command never signs (always open per design). - Without `--key`, no header is sent (backward compatible with non-auth nodes). **`Cargo.toml`** — Added `getrandom = { version = "0.2", optional = true }` to the `cli` feature. --- ## Architecture ``` ┌──────────────────────────────────┐ │ HTTP API (api.rs) │ │ │ │ /api/status ──► handle_status │ (no auth) │ /api/put ──► check_auth ──► │ │ /api/get ──► check_auth ──► │ │ /api/data ──► check_auth ──► │ X-Signed-Request │ /api/delete ──► check_auth ──► │ header required │ /api/list ──► check_auth ──► │ when gateway_addr │ │ is Some └───────────┬───────────────────────┘ │ GatewayMsg::Authorize │ ┌───────────▼───────────┐ │ GatewayActor │ │ │ │ 1. verify signature │ │ 2. check timestamp │ │ 3. check nonce │ │ 4. check ACL │ │ │ │ DatastoreResponse:: │ │ Bool(true) or │ │ Denied { reason } │ └───────────────────────┘ ┌───────────────────────┐ │ CLI (store_cli) │ │ │ │ --key owner.key.json │ │ │ │ sign_action(): │ │ timestamp + nonce │ │ + DatastoreAction │ │ → ed25519 sign │ │ → JSON header │ └───────────────────────┘ ``` --- ## Key File Format `owner.key.json` / any client `key.json`: ```json { "version": 1, "secret_key": "...64 hex chars (32 bytes)...", "public_key": "...64 hex chars (32 bytes)...", "created_at": "2026-02-15T12:00:00Z" } ``` Shared between node and CLI. The node generates it on first `--auth` run; the CLI reads it with `--key`. --- ## Test Summary | Test File | Count | What | |-----------|-------|------| | `auth_scenario_tests.rs` | 12 | AuthzEngine: signing, verification, timestamp, nonce, ACL, grant/revoke | | `acl_persistence_tests.rs` | 2 | ACL JSON round-trip, create-on-missing | | `gateway_tests.rs` | 4 | GatewayActor: connection check, signed request flow, denial | | `http_auth_integration.rs` | 1 | Full HTTP stack: owner allowed, stranger gets 401/403 | | **Auth total** | **19** | | | **Overall total** | **107** | (93 pre-auth + 14 new auth + inherited datastore tests) | --- ## Design Decisions 1. **Key file format is JSON with hex encoding** — human-readable, inspectable with `cat`, foundation for future keystore without needing a binary format parser. 2. **Status endpoint stays open** — `/api/status` is not gated even when auth is enabled. This lets monitoring tools and health checks work without credentials. 3. **Node identity = owner keypair's public key** — when `--auth` is enabled, the keypair's `node_id()` replaces random generation. The node has a stable, cryptographic identity across restarts. 4. **Nonce source is `getrandom`** — cryptographically secure 16-byte random nonces. Already a transitive dependency via `ed25519-dalek` / `rand_core`. 5. **Backward compatible** — without `--auth` (node) or `--key` (CLI), everything works exactly as before. No breaking changes. 6. **Auth is opt-in per binary** — the auth engine, ACL, and gateway actor are always compiled (they're in the lib), but only activated when the binary flags are set. This keeps the default experience frictionless. --- ## Files Changed (Full Branch) | File | What | |------|------| | `crates/shared-types/` | New crate — extracted `ContentHash` to break dependency cycles | | `crates/datastore/src/auth.rs` | Auth engine, ACL, signing, verification | | `crates/datastore/src/actors/gateway.rs` | GatewayActor — auth enforcement point | | `crates/datastore/src/messages.rs` | `GatewayMsg`, `DatastoreResponse::Denied` | | `crates/datastore/src/api.rs` | HTTP API with `check_auth` gate | | `crates/datastore/src/bin/store_node.rs` | `--auth`, `--auth-dir`, keypair management, gateway spawn | | `crates/datastore/src/bin/store_cli.rs` | `--key`, per-request signing | | `crates/datastore/Cargo.toml` | `getrandom` dep, feature updates | | `DATASTORE_AUTH.md` | Auth specification document | | `tests/auth_scenario_tests.rs` | 12 auth engine tests | | `tests/acl_persistence_tests.rs` | 2 ACL persistence tests | | `tests/gateway_tests.rs` | 4 gateway actor tests | | `tests/http_auth_integration.rs` | 1 full-stack HTTP auth test |