# MuseHub Threat Model **Last updated:** 2026-04-07 **Author:** gabriel --- ## System Overview MuseHub is the remote server for the Muse version control system. It stores pushed commits, hosts issue tracking and merge proposals, and serves as the hub for multi-agent orchestration via agentception. Two types of principals interact with MuseHub: - **Human users** — authenticated via Ed25519 keypairs registered at account creation. - **Agent identities** — short-lived Ed25519 keypairs registered programmatically by agentception at runtime, carrying an explicit capability scope and an expiry timestamp. All communication is over HTTPS. There are no passwords, sessions, or bearer tokens. Every authenticated request carries a per-request Ed25519 signature. --- ## Authentication: MSign ### Protocol Every authenticated request carries: ``` Authorization: MSign handle="gabriel" ts=1712345678 sig="" ``` The signature covers the canonical message: ``` METHOD\n PATH_WITH_QUERY\n UNIX_TIMESTAMP\n SHA256_HEX(body_bytes) ``` The server verifies the signature against the registered public key(s) for the handle. Replay protection: requests are rejected when `|server_time - ts| > 30 seconds`. ### Key properties | Property | Value | |----------|-------| | Algorithm | Ed25519 (RFC 8032) | | Key storage (server) | Public key only, stored in `musehub_auth_keys` | | Key storage (client) | Private key in `~/.muse/keys/*.pem` | | Replay window | 30 seconds | | Clock skew tolerance | ±30 seconds | ### Threat mitigations | Threat | Mitigation | |--------|-----------| | Replay attack | Timestamp in canonical message; server rejects if `skew > 30s` | | Man-in-the-middle | HTTPS required; signature covers body hash, not just path | | Key compromise (human) | `muse auth keygen` + `revoke_identity`; soft-delete immediately visible | | Key compromise (agent) | Short TTL (2h by default); agentception revokes at run end | | Forged handle | Handle is looked up in DB; must match a row with a registered key | | Credential stuffing | No passwords; Ed25519 keys cannot be brute-forced | | Timing oracle | Signature verification iterates all keys; failure path is constant | --- ## Identity Hierarchy ``` gabriel (human, permanent) └── agentception-service (service identity, 24h TTL) └── agentception-{run_id} (ephemeral agent identity, 2h TTL) ``` ### Identity types | Type | `identity_type` | `scope` | `expires_at` | |------|-----------------|---------|--------------| | Human | `"human"` | `null` (unrestricted) | `null` (never expires) | | Service | `"agent"` | `["label:write", "issue:write", "proposal:write", "label:read", "issue:read"]` | 24h from registration | | Ephemeral agent | `"agent"` | Subset of service scope | 2h from dispatch | ### Identity lifecycle 1. **Registration:** `POST /api/v1/identities/agents` — requires a valid MSign token from the parent identity. Stores public key and scope. 2. **Request signing:** Agent signs requests with its private key. Server verifies against stored public key. 3. **Scope check:** `require_scope("X")` dependency rejects requests where `identity.scope` is non-null and does not contain `"X"`. 4. **Expiry check:** `_verify_msign` rejects requests for identities where `expires_at <= now`. Checked before signature verification. 5. **Revocation:** `DELETE /api/v1/identities/{handle}` — sets `deleted_at`, immediately visible to all subsequent requests. --- ## Capability Scopes Scope enforcement is implemented in `require_scope()` (`musehub/auth/request_signing.py`). Rules: - **`scope = null`** — human identity; no capability restrictions. All authenticated operations are permitted. - **`scope = [...]`** — agent identity; only listed tokens are permitted. Missing token → HTTP 403. - **Empty scope `[]`** — agent is blocked from every scoped operation. ### Scope taxonomy | Token | Endpoints | |-------|-----------| | `repo:write` | POST repos; DELETE repo; transfer ownership; PATCH settings; POST/stop sessions; rebuild symbol index | | `issue:read` | GET issues, GET comments (authenticated private repos) | | `issue:write` | POST/PATCH issues; POST/DELETE comments; set/remove milestone; reopen/close | | `label:read` | GET label assignments (authenticated private repos) | | `label:write` | POST/DELETE/PATCH labels; POST/DELETE label assignments on issues and proposals | | `proposal:read` | GET proposals, GET reviews (authenticated private repos) | | `proposal:write` | POST proposals; merge; POST comments; request/remove reviewers; submit reviews | | `release:write` | POST releases; POST/DELETE release assets | | `milestone:write` | POST/PATCH/DELETE milestones | ### Who holds which scopes | Principal | Scopes | |-----------|--------| | gabriel (human) | All (unrestricted, scope=null) | | agentception-service | `issue:write`, `issue:read`, `label:write`, `label:read`, `proposal:write` | | agentception-{run_id} | Subset of parent scope, decided at dispatch | Human identities carry `scope = null` and pass all scope checks unconditionally — there are no human-only capabilities. Any operation can be granted to an agent by including the relevant token in its scope list at registration time. --- ## Defence-in-Depth Layers ### Layer 1: Bot throttle (ASGI middleware) `BotThrottleMiddleware` runs before any route handler. It short-circuits at the ASGI level for known-bad User-Agent patterns (scrapers, scanners). Exempt paths: `/healthz`, `/static/`, `/mcp`, `/api/health/`. MSign-authenticated requests bypass the UA check entirely — cryptographic identity supersedes User-Agent heuristics. ### Layer 2: Request signing (MSign) Every route that mutates state requires a valid MSign token. Anonymous access is permitted only for read operations on public repos. ### Layer 3: Scope enforcement Write routes use `Depends(require_scope("X"))` instead of `Depends(require_valid_token)`. This ensures agent identities are limited to their registered capability set. ### Layer 4: Expiry enforcement `_verify_msign` checks `expires_at` before attempting signature verification. An expired identity is rejected even if its private key is still cryptographically valid. ### Layer 5: Soft-delete / revocation `deleted_at` is checked in every identity lookup. Revoked identities are immediately invisible without requiring key rotation or cache invalidation. ### Layer 6: Per-route slowapi rate limits Routes are rate-limited by IP + identity. Authentication failures do not count against the route limit but are tracked by `failure_limiter` to detect credential stuffing. --- ## Known Limitations and Future Work | Item | Status | |------|--------| | Scope enforcement on wire protocol (push/pull) | Not implemented. Wire operations use human keys; agents don't push. Tracked as future work. | | Scope audit log | Not implemented. Scope denials are logged at WARNING level but not stored in DB. | | Agent-to-agent delegation | Not implemented. An agent cannot grant sub-scope to another agent directly. | | Key rotation without downtime | Supported — multiple keys per identity; verification tries all. | | Scope changes without re-registration | Not supported. Scope is set at registration and cannot be modified. Revoke and re-register. | --- ## Incident Response ### Compromised human key 1. `muse auth keygen --hub ` — generate a new key. 2. `DELETE /api/v1/identities/{handle}/keys/{key_id}` — revoke the old key. 3. All subsequent requests using the old key are rejected immediately. ### Compromised agent key 1. `DELETE /api/v1/identities/{handle}` — soft-delete the agent identity. Takes effect immediately. 2. Generate a new ephemeral identity for the next run. ### Suspected replay attack 1. Check access logs for duplicate `(handle, ts)` pairs within the 30-second window. 2. If systematic, reduce `REPLAY_WINDOW_SECONDS` and rotate the affected identity's key.