Files
mcias/PROJECT_PLAN.md
2026-03-11 11:26:47 -07:00

10 KiB

MCIAS Project Plan

Discrete implementation steps with acceptance criteria. See ARCHITECTURE.md for design rationale.


Phase 0 — Repository Bootstrap

Step 0.1: Go module and dependency setup

Acceptance criteria:

  • go.mod exists with module path git.wntrmute.dev/kyle/mcias
  • Required dependencies declared: modernc.org/sqlite (CGo-free SQLite), golang.org/x/crypto (Argon2, Ed25519 helpers), github.com/golang-jwt/jwt/v5, github.com/pelletier/go-toml/v2, github.com/google/uuid, git.wntrmute.dev/kyle/goutils
  • go mod tidy succeeds; go build ./... succeeds on empty stubs

Step 0.2: .gitignore

Acceptance criteria:

  • .gitignore excludes: build output (mciassrv, mciasctl), *.db, *.db-wal, *.db-shm, test coverage files, editor artifacts

Phase 1 — Foundational Packages

Step 1.1: internal/model — shared data types

Acceptance criteria:

  • Account, Role, Token, AuditEvent, PGCredential structs defined
  • Account types and statuses as typed constants
  • No external dependencies (pure data definitions)
  • Tests: struct instantiation and constant validation

Step 1.2: internal/config — configuration loading

Acceptance criteria:

  • TOML config parsed into a Config struct matching ARCHITECTURE.md §11
  • Validation: required fields present, listen_addr parseable, TLS paths non-empty, Argon2 params within safe bounds (memory >= 64MB, time >= 2)
  • Master key config: exactly one of passphrase_env or keyfile set
  • Tests: valid config round-trips; missing required field → error; unsafe Argon2 params rejected

Step 1.3: internal/crypto — key management and encryption helpers

Acceptance criteria:

  • GenerateEd25519KeyPair() (crypto/ed25519.PublicKey, crypto/ed25519.PrivateKey, error)
  • MarshalPrivateKeyPEM(key crypto/ed25519.PrivateKey) ([]byte, error) (PKCS#8)
  • ParsePrivateKeyPEM(pemData []byte) (crypto/ed25519.PrivateKey, error)
  • SealAESGCM(key, plaintext []byte) (ciphertext, nonce []byte, err error)
  • OpenAESGCM(key, nonce, ciphertext []byte) ([]byte, error)
  • DeriveKey(passphrase string, salt []byte) ([]byte, error) — Argon2id KDF for master key derivation (separate from password hashing)
  • Key is always 32 bytes (AES-256)
  • crypto/rand used for all nonce/salt generation
  • Tests: seal/open round-trip; open with wrong key → error; PEM round-trip; nonces are unique across calls

Step 1.4: internal/db — database layer

Acceptance criteria:

  • Open(path string) (*DB, error) opens/creates SQLite DB with WAL mode and foreign keys enabled
  • Migrate(db *DB) error applies schema from ARCHITECTURE.md §9 idempotently (version table tracks applied migrations)
  • CRUD for accounts, account_roles, token_revocation, system_tokens, pg_credentials, audit_log, server_config
  • All queries use prepared statements (no string concatenation)
  • Tests: in-memory SQLite (:memory:); each CRUD operation; FK constraint enforcement; migration idempotency

Phase 2 — Authentication Core

Step 2.1: internal/token — JWT issuance and validation

Acceptance criteria:

  • IssueToken(key ed25519.PrivateKey, claims Claims) (string, error)
    • Header: {"alg":"EdDSA","typ":"JWT"}
    • Claims: iss, sub, iat, exp, jti (UUID), roles
  • ValidateToken(key ed25519.PublicKey, tokenString string) (Claims, error)
    • Must check alg header before any other processing; reject non-EdDSA
    • Validates exp, iat, iss, jti presence
    • Returns structured error types for: expired, invalid signature, wrong alg, missing claim
  • RevokeToken(db *DB, jti string, reason string) error
  • PruneExpiredTokens(db *DB) (int64, error) — removes rows past expiry
  • Tests: happy path; expired token rejected; wrong alg (none/HS256/RS256) rejected; tampered signature rejected; missing jti rejected; revoked token checked by caller; pruning removes only expired rows

Step 2.2: internal/auth — login and credential verification

Acceptance criteria:

  • HashPassword(password string, params ArgonParams) (string, error) — returns PHC-format string
  • VerifyPassword(password, hash string) (bool, error) — constant-time; re-hashes if params differ (upgrade path)
  • ValidateTOTP(secret []byte, code string) (bool, error) — RFC 6238, 1-window tolerance, constant-time
  • Login(ctx, db, key, cfg, req LoginRequest) (LoginResponse, error)
    • Orchestrates: load account → verify status → verify password → verify TOTP (if required) → issue JWT → write token_revocation row → write audit_log
    • On any failure: write audit_log (login_fail or login_totp_fail); return generic error to caller (no information about which step failed)
  • Tests: successful login; wrong password (timing: compare duration against a threshold — at minimum assert no short-circuit); wrong TOTP; suspended account; deleted account; TOTP enrolled but not provided; constant-time comparison verified (both branches take comparable time)

Phase 3 — HTTP Server

Step 3.1: internal/middleware — HTTP middleware

Acceptance criteria:

  • RequestLogger — logs method, path, status, duration, IP
  • RequireAuth(key ed25519.PublicKey, db *DB) — extracts Bearer token, validates, checks revocation, injects claims into context; returns 401 on failure
  • RequireRole(role string) — checks claims from context for role; returns 403 on failure
  • RateLimit(rps float64, burst int) — per-IP token bucket; returns 429 on limit exceeded
  • Tests: logger captures fields; RequireAuth rejects missing/invalid/revoked tokens; RequireRole rejects insufficient roles; RateLimit triggers after burst exceeded

Step 3.2: internal/server — HTTP handlers and router

Acceptance criteria:

  • Router wired per ARCHITECTURE.md §8 (all endpoints listed)
  • POST /v1/auth/login — delegates to auth.Login; returns {token, expires_at}
  • POST /v1/auth/logout — revokes current token from context; 204
  • POST /v1/auth/renew — validates current token, issues new one, revokes old
  • POST /v1/token/validate — validates submitted token; returns claims
  • DELETE /v1/token/{jti} — admin or role-scoped; revokes by JTI
  • GET /v1/health — 200 {"status":"ok"}
  • GET /v1/keys/public — returns Ed25519 public key as JWK
  • POST /v1/accounts — admin; creates account
  • GET /v1/accounts — admin; lists accounts (no password hashes in response)
  • GET /v1/accounts/{id} — admin; single account
  • PATCH /v1/accounts/{id} — admin; update status/fields
  • DELETE /v1/accounts/{id} — admin; soft-delete + revoke all tokens
  • GET|PUT /v1/accounts/{id}/roles — admin; get/replace role set
  • POST /v1/auth/totp/enroll — returns TOTP secret + otpauth URI
  • POST /v1/auth/totp/confirm — confirms TOTP enrollment
  • DELETE /v1/auth/totp — admin; removes TOTP from account
  • GET|PUT /v1/accounts/{id}/pgcreds — get/set Postgres credentials
  • Credential fields (password hash, TOTP secret, Postgres password) are never included in any API response
  • Tests: each endpoint happy path; auth middleware applied correctly; invalid JSON body → 400; credential fields absent from all responses

Step 3.3: cmd/mciassrv — server binary

Acceptance criteria:

  • Reads config file path from --config flag
  • Loads config, derives master key, opens DB, runs migrations, loads/generates signing key
  • Starts HTTPS listener on configured address
  • Graceful shutdown on SIGINT/SIGTERM (30s drain)
  • If no signing key exists in DB, generates one and stores it encrypted
  • Integration test: start server on random port, hit /v1/health, assert 200

Phase 4 — Admin CLI

Step 4.1: cmd/mciasctl — admin CLI

Acceptance criteria:

  • Subcommands:
    • mciasctl account create --username NAME --type human|system
    • mciasctl account list
    • mciasctl account suspend --id UUID
    • mciasctl account delete --id UUID
    • mciasctl role grant --account UUID --role ROLE
    • mciasctl role revoke --account UUID --role ROLE
    • mciasctl token issue --account UUID (system accounts)
    • mciasctl token revoke --jti JTI
    • mciasctl pgcreds set --account UUID --host H --port P --db D --user U --password P
    • mciasctl pgcreds get --account UUID
  • CLI reads admin JWT from MCIAS_ADMIN_TOKEN env var or --token flag
  • All commands make HTTPS requests to mciassrv (base URL from --server flag or MCIAS_SERVER env var)
  • Tests: flag parsing; missing required flags → error; help text complete

Phase 5 — End-to-End Tests and Hardening

Step 5.1: End-to-end test suite

Acceptance criteria:

  • TestE2ELoginLogout — create account, login, validate token, logout, validate token again (must fail)
  • TestE2ETokenRenewal — login, renew, old token rejected, new token valid
  • TestE2EAdminFlow — create account via CLI, assign role, login as user
  • TestE2ETOTPFlow — enroll TOTP, login without code (fail), login with code (succeed)
  • TestE2ESystemAccount — create system account, issue token, validate token, rotate token (old token rejected)
  • TestE2EAlgConfusion — attempt login, forge token with alg=HS256/none, submit to validate endpoint, assert 401

Step 5.2: Security hardening review

Acceptance criteria:

  • golangci-lint run ./... passes with zero warnings
  • go test -race ./... passes with zero race conditions
  • Manual review checklist:
    • No password/token/secret in any log line (grep audit)
    • All crypto/rand — no math/rand usage
    • All token comparisons use crypto/subtle
    • Argon2id params meet OWASP minimums
    • JWT alg validated before signature check in all code paths
    • All DB queries use parameterized statements
    • TLS min version enforced in server config

Step 5.3: Documentation and commit

Acceptance criteria:

  • README.md updated with: build instructions, config file example, first-run steps
  • Each phase committed separately with appropriate commit messages
  • PROGRESS.md reflects completed state

Implementation Order

Phase 0 → Phase 1 (1.1, 1.2, 1.3, 1.4 in parallel or sequence)
        → Phase 2 (2.1 then 2.2)
        → Phase 3 (3.1, 3.2, 3.3 in sequence)
        → Phase 4
        → Phase 5

Each step must have passing tests before the next step begins.