Files
mcias/PROJECT_PLAN.md
Kyle Isom 14083b82b4 Fix linting: golangci-lint v2 config, nolint annotations
* Rewrite .golangci.yaml to v2 schema: linters-settings ->
  linters.settings, issues.exclude-rules -> issues.exclusions.rules,
  issues.exclude-dirs -> issues.exclusions.paths
* Drop deprecated revive exported/package-comments rules: personal
  project, not a public library; godoc completeness is not a CI req
* Add //nolint:gosec G101 on PassphraseEnv default in config.go:
  environment variable name is not a credential value
* Add //nolint:gosec G101 on EventPGCredUpdated in model.go:
  audit event type string, not a credential

Security: no logic changes. gosec G101 suppressions are false
positives confirmed by code inspection: neither constant holds a
credential value.
2026-03-11 12:53:25 -07:00

14 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

Phase 6 — mciasdb: Database Maintenance Tool

See ARCHITECTURE.md §16 for full design rationale, trust model, and command surface.

Step 6.1: cmd/mciasdb — binary skeleton and config loading

Acceptance criteria:

  • Binary at cmd/mciasdb/main.go parses --config flag
  • Loads [database] and [master_key] sections from mciassrv config format
  • Derives master key (passphrase or keyfile, identical logic to mciassrv)
  • Opens SQLite DB with WAL mode and FK enforcement (reuses internal/db)
  • Exits with error if DB cannot be opened (e.g., busy-timeout exceeded)
  • Help text lists all subcommands

Step 6.2: Schema subcommands

Acceptance criteria:

  • mciasdb schema verify — opens DB, reports current schema version, exits 0 if up-to-date, exits 1 if migrations pending
  • mciasdb schema migrate — applies any pending migrations, reports each one applied, exits 0
  • Tests: verify on fresh DB reports version 0; migrate advances to current version; verify after migrate exits 0

Step 6.3: Account and role subcommands

Acceptance criteria:

  • mciasdb account list — prints uuid, username, type, status for all accounts
  • mciasdb account get --id UUID — prints single account record
  • mciasdb account create --username NAME --type human|system — inserts row, prints new UUID
  • mciasdb account set-password --id UUID — prompts twice (confirm), re-hashes with Argon2id, updates row; no --password flag permitted
  • mciasdb account set-status --id UUID --status STATUS — updates status
  • mciasdb account reset-totp --id UUID — clears totp_required and totp_secret_enc
  • mciasdb role list --id UUID — prints roles
  • mciasdb role grant --id UUID --role ROLE — inserts role row
  • mciasdb role revoke --id UUID --role ROLE — deletes role row
  • All write operations append an audit log row with actor tagged mciasdb
  • Tests: each subcommand happy path against in-memory SQLite; unknown UUID returns error; set-password with mismatched confirmation returns error

Step 6.4: Token subcommands

Acceptance criteria:

  • mciasdb token list --id UUID — prints jti, issued_at, expires_at, revoked_at for account
  • mciasdb token revoke --jti JTI — sets revoked_at = now on the row
  • mciasdb token revoke-all --id UUID — revokes all non-revoked tokens for account
  • mciasdb prune tokens — deletes rows from token_revocation where expires_at < now; prints count removed
  • All write operations append an audit log row
  • Tests: revoke on unknown JTI returns error; revoke-all on account with no active tokens is a no-op (exits 0); prune removes only expired rows

Step 6.5: Audit log subcommands

Acceptance criteria:

  • mciasdb audit tail [--n N] — prints last N events (default 50), newest last
  • mciasdb audit query --account UUID — filters by actor_id or target_id
  • mciasdb audit query --type EVENT_TYPE — filters by event_type
  • mciasdb audit query --since TIMESTAMP — filters by event_time >= RFC-3339 timestamp
  • Flags are combinable (AND semantics)
  • --json flag on any audit subcommand emits newline-delimited JSON
  • Tests: tail returns correct count; query filters correctly; --json output is valid JSON

Step 6.6: Postgres credentials subcommands

Acceptance criteria:

  • mciasdb pgcreds get --id UUID — decrypts and prints host, port, db, username, password with a warning header that output is sensitive
  • mciasdb pgcreds set --id UUID --host H --port P --db D --user U — prompts for password interactively (no --password flag), encrypts with AES-256-GCM, stores row
  • All write operations append an audit log row
  • Tests: get on account with no pgcreds returns error; set then get round-trips correctly (decrypted value matches original)

Step 6.7: .gitignore and documentation

Acceptance criteria:

  • .gitignore updated to exclude mciasdb binary
  • README.md updated with mciasdb usage section (when to use vs mciasctl, config requirements, example commands)
  • PROGRESS.md updated to reflect Phase 6 complete

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
        → Phase 6 (6.1 → 6.2 → 6.3 → 6.4 → 6.5 → 6.6 → 6.7)

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