10 KiB
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.modexists with module pathgit.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 tidysucceeds;go build ./...succeeds on empty stubs
Step 0.2: .gitignore
Acceptance criteria:
.gitignoreexcludes: 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,PGCredentialstructs 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
Configstruct 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_envorkeyfileset - 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/randused 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 enabledMigrate(db *DB) errorapplies 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
- Header:
ValidateToken(key ed25519.PublicKey, tokenString string) (Claims, error)- Must check
algheader before any other processing; reject non-EdDSA - Validates
exp,iat,iss,jtipresence - Returns structured error types for: expired, invalid signature, wrong alg, missing claim
- Must check
RevokeToken(db *DB, jti string, reason string) errorPruneExpiredTokens(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 stringVerifyPassword(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-timeLogin(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, IPRequireAuth(key ed25519.PublicKey, db *DB)— extracts Bearer token, validates, checks revocation, injects claims into context; returns 401 on failureRequireRole(role string)— checks claims from context for role; returns 403 on failureRateLimit(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 toauth.Login; returns{token, expires_at}POST /v1/auth/logout— revokes current token from context; 204POST /v1/auth/renew— validates current token, issues new one, revokes oldPOST /v1/token/validate— validates submitted token; returns claimsDELETE /v1/token/{jti}— admin or role-scoped; revokes by JTIGET /v1/health— 200{"status":"ok"}GET /v1/keys/public— returns Ed25519 public key as JWKPOST /v1/accounts— admin; creates accountGET /v1/accounts— admin; lists accounts (no password hashes in response)GET /v1/accounts/{id}— admin; single accountPATCH /v1/accounts/{id}— admin; update status/fieldsDELETE /v1/accounts/{id}— admin; soft-delete + revoke all tokensGET|PUT /v1/accounts/{id}/roles— admin; get/replace role setPOST /v1/auth/totp/enroll— returns TOTP secret + otpauth URIPOST /v1/auth/totp/confirm— confirms TOTP enrollmentDELETE /v1/auth/totp— admin; removes TOTP from accountGET|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
--configflag - 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|systemmciasctl account listmciasctl account suspend --id UUIDmciasctl account delete --id UUIDmciasctl role grant --account UUID --role ROLEmciasctl role revoke --account UUID --role ROLEmciasctl token issue --account UUID(system accounts)mciasctl token revoke --jti JTImciasctl pgcreds set --account UUID --host H --port P --db D --user U --password Pmciasctl pgcreds get --account UUID
- CLI reads admin JWT from
MCIAS_ADMIN_TOKENenv var or--tokenflag - All commands make HTTPS requests to mciassrv (base URL from
--serverflag orMCIAS_SERVERenv 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 validTestE2EAdminFlow— create account via CLI, assign role, login as userTestE2ETOTPFlow— 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 warningsgo test -race ./...passes with zero race conditions- Manual review checklist:
- No password/token/secret in any log line (grep audit)
- All
crypto/rand— nomath/randusage - 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.mdreflects 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.