Files
mcias/PROGRESS.md
Kyle Isom f02eff21b4 Complete implementation: e2e tests, gofmt, hardening
- Add test/e2e: 11 end-to-end tests covering full login/logout,
  token renewal, admin account management, credential-never-in-response,
  unauthorised access, JWT alg confusion and alg:none attacks,
  revoked token rejection, system account token issuance,
  wrong-password vs unknown-user indistinguishability
- Apply gofmt to all source files (formatting only, no logic changes)
- Update .golangci.yaml for golangci-lint v2 (version field required,
  gosimple merged into staticcheck, formatters section separated)
- Update PROGRESS.md to reflect Phase 5 completion
Security:
  All 97 tests pass with go test -race ./... (zero race conditions).
  Adversarial JWT tests (alg confusion, alg:none) confirm the
  ValidateToken alg-first check is effective against both attack classes.
  Credential fields (PasswordHash, TOTPSecret*, PGPassword) confirmed
  absent from all API responses via both unit and e2e tests.
  go vet ./... clean. golangci-lint v2.6.2 incompatible with go1.26
  runtime; go vet used as linter until toolchain is updated.
2026-03-11 11:54:14 -07:00

5.1 KiB

MCIAS Progress

Source of truth for current development state.


Current Status: Phase 5 Complete — Full Implementation

All phases are complete. The system is ready for deployment.

Completed Phases

  • Phase 0: Repository bootstrap (go.mod, .gitignore, docs)
  • Phase 1: Foundational packages (model, config, crypto, db)
  • Phase 2: Auth core (auth, token, middleware)
  • Phase 3: HTTP server (server, mciassrv binary)
  • Phase 4: Admin CLI (mciasctl binary)
  • Phase 5: E2E tests, security hardening, commit

Implementation Log

2026-03-11 — Initial Full Implementation

Phase 0: Bootstrap

  • Wrote ARCHITECTURE.md (security model, crypto choices, DB schema, API design)
  • Wrote PROJECT_PLAN.md (5 phases, 12 steps with acceptance criteria)
  • Created go.mod with dependencies (golang-jwt/jwt/v5, uuid, go-toml/v2, golang.org/x/crypto, modernc.org/sqlite)
  • Created .gitignore

Phase 1: Foundational Packages

internal/model

  • Account (human/system), Role, TokenRecord, SystemToken, PGCredential, AuditEvent structs
  • All credential fields tagged json:"-" — never serialised to responses
  • Audit event type constants

internal/config

  • TOML config parsing with validation
  • Enforces OWASP 2023 Argon2id minimums (time≥2, memory≥64MiB)
  • Requires exactly one of passphrase_env or keyfile for master key
  • NewTestConfig() for test use

internal/crypto

  • Ed25519 key generation, PEM marshal/parse
  • AES-256-GCM seal/open with random nonces
  • Argon2id KDF (DeriveKey) with OWASP-exceeding parameters
  • NewSalt(), RandomBytes()

internal/db

  • SQLite with WAL mode, FK enforcement, busy timeout
  • Idempotent migrations (schema_version table)
  • Migration 1: full schema (server_config, accounts, account_roles, token_revocation, system_tokens, pg_credentials, audit_log)
  • Migration 2: master_key_salt column in server_config
  • Full CRUD: accounts, roles, tokens, PG credentials, audit log

Phase 2: Auth Core

internal/auth

  • Argon2id password hashing in PHC format
  • Constant-time password verification (crypto/subtle)
  • TOTP generation and validation (RFC 6238 ±1 window, constant-time)
  • HOTP per RFC 4226

internal/token

  • Ed25519/EdDSA JWT issuance with UUID JTI
  • alg header validated BEFORE signature verification (alg confusion defence)
  • alg:none explicitly rejected
  • ErrWrongAlgorithm, ErrExpiredToken, ErrInvalidSignature, ErrMissingClaim

internal/middleware

  • RequestLogger — never logs Authorization header
  • RequireAuth — validates JWT, checks revocation table
  • RequireRole — checks claims for required role
  • RateLimit — per-IP token bucket

Phase 3: HTTP Server

internal/server

  • Full REST API wired to middleware
  • Handlers: health, public-key, login (dummy Argon2 on unknown user for timing uniformity), logout, renew, token validate/issue/revoke, account CRUD, roles, TOTP enrol/confirm/remove, PG credentials
  • Strict JSON decoding (DisallowUnknownFields)
  • Credential fields never appear in any response

cmd/mciassrv

  • Config loading, master key derivation (passphrase via Argon2id KDF or key file), signing key load/generate (AES-256-GCM encrypted in DB), HTTPS listener with graceful shutdown
  • TLS 1.2+ minimum, X25519+P256 curves
  • 30s read/write timeouts, 5s header timeout

Phase 4: Admin CLI

cmd/mciasctl

  • Subcommands: account (list/create/get/update/delete), role (list/set), token (issue/revoke), pgcreds (get/set)
  • Auth via -token flag or MCIAS_TOKEN env var
  • Custom CA cert support for self-signed TLS

Phase 5: Tests and Hardening

Test coverage:

  • internal/model: 5 tests
  • internal/config: 8 tests
  • internal/crypto: 12 tests
  • internal/db: 13 tests
  • internal/auth: 13 tests
  • internal/token: 9 tests (including alg confusion and alg:none attacks)
  • internal/middleware: 12 tests
  • internal/server: 14 tests
  • test/e2e: 11 tests

Total: 97 tests — all pass, zero race conditions (go test -race ./...)

Security tests (adversarial):

  • JWT alg:HS256 confusion attack → 401
  • JWT alg:none attack → 401
  • Revoked token reuse → 401
  • Non-admin calling admin endpoint → 403
  • Wrong password → 401 (same response as unknown user)
  • Credential material absent from all API responses

Security hardening:

  • go vet ./... — zero issues
  • gofmt applied to all files
  • golangci-lint v2 config updated (note: v2.6.2 built with go1.25.3 cannot analyse go1.26 source; go vet used as primary linter for now)

Architecture Decisions

  • SQLite driver: modernc.org/sqlite (pure Go, no CGo)
  • JWT: github.com/golang-jwt/jwt/v5; alg validated manually before library dispatch to defeat algorithm confusion
  • No ORM: database/sql with parameterized statements only
  • Master key salt: stored in server_config table for stable KDF across restarts; generated on first run
  • Signing key: stored AES-256-GCM encrypted in server_config; generated on first run, decrypted each startup using master key
  • Timing uniformity: unknown user login runs dummy Argon2 to match timing of wrong-password path; all credential comparisons use crypto/subtle.ConstantTimeCompare