Files
mcias/PROJECT_PLAN.md
Kyle Isom 25417b24f4 Add FIDO2/WebAuthn passkey authentication
Phase 14: Full WebAuthn support for passwordless passkey login and
hardware security key 2FA.

- go-webauthn/webauthn v0.16.1 dependency
- WebAuthnConfig with RPID/RPOrigin/DisplayName validation
- Migration 000009: webauthn_credentials table
- DB CRUD with ownership checks and admin operations
- internal/webauthn adapter: encrypt/decrypt at rest with AES-256-GCM
- REST: register begin/finish, login begin/finish, list, delete
- Web UI: profile enrollment, login passkey button, admin management
- gRPC: ListWebAuthnCredentials, RemoveWebAuthnCredential RPCs
- mciasdb: webauthn list/delete/reset subcommands
- OpenAPI: 6 new endpoints, WebAuthnCredentialInfo schema
- Policy: self-service enrollment rule, admin remove via wildcard
- Tests: DB CRUD, adapter round-trip, interface compliance
- Docs: ARCHITECTURE.md §22, PROJECT_PLAN.md Phase 14

Security: Credential IDs and public keys encrypted at rest with
AES-256-GCM via vault master key. Challenge ceremonies use 128-bit
nonces with 120s TTL in sync.Map. Sign counter validated on each
assertion to detect cloned authenticators. Password re-auth required
for registration (SEC-01 pattern). No credential material in API
responses or logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:12:59 -07:00

39 KiB
Raw Blame History

MCIAS Project Plan

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


Status

v1.0.0 tagged (2026-03-15). All phases complete.

All packages pass go test ./...; golangci-lint run ./... clean. See PROGRESS.md for the detailed development log.

Phases 09 match the original plan. Phases 1013 document significant features implemented beyond the original plan scope.


Phase 0 — Repository Bootstrap [COMPLETE]

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 [COMPLETE]

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 [COMPLETE]

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 [COMPLETE]

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
  • GET /v1/pgcreds — list all accessible credentials (owned + granted)
  • 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 [COMPLETE]

Step 4.1: cmd/mciasctl — admin CLI

Acceptance criteria:

  • Subcommands:
    • mciasctl account create -username NAME -type human|system
    • mciasctl account list
    • mciasctl account update -id UUID -status active|inactive
    • mciasctl account delete -id UUID
    • mciasctl account get -id UUID
    • mciasctl account set-password -id UUID
    • mciasctl role list -id UUID
    • mciasctl role set -id UUID -roles role1,role2
    • mciasctl role grant -id UUID -role ROLE
    • mciasctl role revoke -id UUID -role ROLE
    • mciasctl token issue -id UUID (system accounts)
    • mciasctl token revoke -jti JTI
    • mciasctl pgcreds list
    • mciasctl pgcreds set -id UUID -host H -port P -db D -user U
    • mciasctl pgcreds get -id UUID
    • mciasctl auth login
    • mciasctl auth change-password
    • mciasctl tag list -id UUID
    • mciasctl tag set -id UUID -tags tag1,tag2
    • mciasctl policy list|create|get|update|delete
  • CLI reads admin JWT from MCIAS_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 [COMPLETE]

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 [COMPLETE]

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

Phase 7 — gRPC Interface [COMPLETE]

See ARCHITECTURE.md §17 for full design rationale, proto definitions, and transport security requirements.

Step 7.1: Protobuf definitions and generated code

Acceptance criteria:

  • proto/mcias/v1/ directory contains .proto files for all service groups: auth.proto, token.proto, account.proto, policy.proto, admin.proto, common.proto
  • All RPC methods mirror the REST API surface (see ARCHITECTURE.md §8 and §17)
  • proto/generate.go contains a //go:generate protoc ... directive that produces Go stubs under gen/mcias/v1/ using protoc-gen-go and protoc-gen-go-grpc
  • Protobuf field conventions: snake_case field names, google.protobuf.Timestamp for all time fields, no credential fields in response messages (same exclusion rules as JSON API)
  • go generate ./... re-runs protoc idempotently
  • Tests: generated code compiles cleanly (go build ./... succeeds)

Step 7.2: internal/grpcserver — gRPC handler implementations

Acceptance criteria:

  • Package internal/grpcserver implements all generated gRPC service interfaces
  • Handlers delegate to the same internal/auth, internal/token, and internal/db packages used by the REST server — no duplicated logic
  • Authentication: Bearer token extracted from gRPC metadata key authorization, validated via internal/middleware logic (same JWT validation path)
  • Admin RPCs require the admin role (checked identically to REST middleware)
  • Errors mapped to canonical gRPC status codes:
    • Unauthenticated → codes.Unauthenticated
    • Forbidden → codes.PermissionDenied
    • Not found → codes.NotFound
    • Invalid input → codes.InvalidArgument
    • Internal error → codes.Internal (no internal detail leaked to caller)
  • Tests: unit tests for each RPC handler; unauthenticated calls rejected; credential fields absent from all response messages

Step 7.3: TLS and interceptors

Acceptance criteria:

  • gRPC server uses the same TLS certificate and key as the REST server (loaded from config); minimum TLS 1.2 enforced via tls.Config
  • Unary server interceptor chain:
    1. Sealed interceptor (blocks all RPCs when vault sealed, except Health)
    2. Request logger (method name, peer IP, status, duration)
    3. Auth interceptor (extracts Bearer token, validates, injects claims into context.Context)
    4. Rate-limit interceptor (per-IP token bucket, same parameters as REST)
  • No credential material logged by any interceptor
  • Tests: interceptor chain applied correctly; rate-limit triggers after burst

Step 7.4: cmd/mciassrv integration — dual-stack server

Acceptance criteria:

  • mciassrv starts both the REST listener and the gRPC listener on separate ports (gRPC port configurable: [server] grpc_addr; defaults to same host, port+1)
  • Both listeners share the same signing key, DB connection, and config
  • Graceful shutdown drains both servers within the configured drain window
  • Config: grpc_addr added to [server] section (optional; if absent, gRPC listener is disabled)
  • Integration test: start server, connect gRPC client, call Health RPC, assert OK response

Step 7.5: cmd/mciasgrpcctl — gRPC admin CLI (optional companion)

Acceptance criteria:

  • Binary at cmd/mciasgrpcctl/main.go with subcommands mirroring mciasctl
  • Uses generated gRPC client stubs; connects to --server address with TLS
  • Auth via --token flag or MCIAS_TOKEN env var (sent as gRPC metadata)
  • Custom CA cert support identical to mciasctl
  • Tests: flag parsing; missing required flags → error; help text complete

Step 7.6: Documentation and commit

Acceptance criteria:

  • ARCHITECTURE.md §17 written (proto service layout, transport security, interceptor chain, dual-stack operation)
  • README.md updated with gRPC section: enabling gRPC, connecting clients, example grpcurl invocations
  • .gitignore updated to exclude the mciasgrpcctl binary (using a root-anchored path /mciasgrpcctl); generated code in gen/ is committed to the repository so that consumers do not need the protoc toolchain
  • PROGRESS.md updated to reflect Phase 7 complete

Phase 8 — Operational Artifacts [COMPLETE]

See ARCHITECTURE.md §18 for full design rationale and artifact inventory.

Step 8.1: systemd service unit

Acceptance criteria:

  • dist/mcias.service — a hardened systemd unit for mciassrv:
    • Type=notify with sd_notify support (or Type=simple with explicit ExecStart if notify is not implemented)
    • User=mcias / Group=mcias — dedicated service account
    • CapabilityBoundingSet= — no capabilities required (port > 1024 assumed; document this assumption)
    • ProtectSystem=strict, ProtectHome=true, PrivateTmp=true
    • ReadWritePaths=/var/lib/mcias for the SQLite file
    • EnvironmentFile=/etc/mcias/env for MCIAS_MASTER_PASSPHRASE
    • Restart=on-failure, RestartSec=5
    • LimitNOFILE=65536
  • dist/mcias.env.example — template environment file with comments explaining each variable
  • Tests: systemd-analyze verify dist/mcias.service exits 0 (run in CI if systemd available; skip gracefully if not)

Step 8.2: Default configuration file

Acceptance criteria:

  • dist/mcias.conf.example — fully-commented TOML config covering all sections from ARCHITECTURE.md §11, including the new grpc_addr field from Phase 7
  • Comments explain every field, acceptable values, and security implications
  • Defaults match the ARCHITECTURE.md §11 reference config
  • A separate dist/mcias-dev.conf.example suitable for local development (localhost address, relaxed timeouts, short token expiry for testing)

Step 8.3: Installation script

Acceptance criteria:

  • dist/install.sh — POSIX shell script (no bash-isms) that:
    • Creates mcias system user and group (idempotent)
    • Copies binaries to /usr/local/bin/
    • Creates /etc/mcias/ and /var/lib/mcias/ with correct ownership/perms
    • Installs dist/mcias.service to /etc/systemd/system/
    • Prints post-install instructions (key generation, first-run steps)
    • Does not start the service automatically
  • Script is idempotent (safe to re-run after upgrades)
  • Tests: shellcheck passes with zero warnings

Step 8.4: Man pages

Acceptance criteria:

  • man/man1/mciassrv.1 — man page for the server binary; covers synopsis, description, options, config file format, signals, exit codes, and files
  • man/man1/mciasctl.1 — man page for the admin CLI; covers all subcommands with examples
  • man/man1/mciasdb.1 — man page for the DB maintenance tool; includes trust model and safety warnings
  • man/man1/mciasgrpcctl.1 — man page for the gRPC CLI (Phase 7)
  • Man pages written in mdoc format (BSD/macOS compatible) or troff (Linux)
  • make man target in Makefile generates compressed .gz versions
  • Tests: man --warnings -l man/man1/*.1 exits 0 (or equivalent lint)

Step 8.5: Makefile

Acceptance criteria:

  • Makefile at repository root with targets:
    • build — compile all binaries to bin/
    • testgo test -race ./...
    • lintgolangci-lint run ./...
    • generatego generate ./... (proto stubs from Phase 7)
    • man — build compressed man pages
    • install — run dist/install.sh
    • dockerdocker build -t mcias:$(VERSION) -t mcias:latest .
    • docker-clean — remove local mcias:$(VERSION) and mcias:latest images; prune dangling images with the mcias label
    • clean — remove bin/, compressed man pages, and local Docker images
    • dist — build release tarballs for linux/amd64 and linux/arm64 (using GOOS/GOARCH cross-compilation)
  • make build works from a clean checkout after go mod download
  • Tests: make build produces binaries; make test passes; make lint passes

Step 8.6: Dockerfile

Acceptance criteria:

  • Dockerfile at repository root using a multi-stage build:
    • Build stage: golang:1.26-bookworm — compiles all four binaries with CGO_ENABLED=1 (required for SQLite via modernc.org/sqlite) and -trimpath -ldflags="-s -w" to strip debug info
    • Runtime stage: debian:bookworm-slim — installs only ca-certificates and libc6; copies binaries from the build stage
    • Final image runs as a non-root user (uid=10001, gid=10001; named mcias)
    • EXPOSE 8443 (REST) and EXPOSE 9443 (gRPC); both are overridable via env
    • VOLUME /data — operator mounts the SQLite database here
    • ENTRYPOINT ["mciassrv"] with CMD ["-config", "/etc/mcias/mcias.conf"]
    • Image must not contain the Go toolchain, source code, or build cache
  • dist/mcias.conf.docker.example — config template suitable for container deployment: listen_addr = "0.0.0.0:8443", grpc_addr = "0.0.0.0:9443", db_path = "/data/mcias.db", TLS cert/key paths under /etc/mcias/
  • Tests:
    • docker build . completes without error (run in CI if Docker available; skip gracefully if not)
    • docker run --rm mcias:latest mciassrv --help exits 0

Step 8.7: Documentation

Acceptance criteria:

  • README.md updated with: quick-start section referencing both the install script and the Docker image, links to man pages, configuration walkthrough
  • ARCHITECTURE.md §18 updated to include the Dockerfile in the artifact inventory and document the container deployment model
  • PROGRESS.md updated to reflect Phase 8 complete

Phase 9 — Client Libraries [COMPLETE]

See ARCHITECTURE.md §19 for full design rationale, API surface, and per-language implementation notes.

Step 9.1: API surface definition and shared test fixtures

Acceptance criteria:

  • clients/README.md — defines the canonical client API surface that all language implementations must expose:
    • Client type: configured with server URL, optional CA cert, optional token
    • Client.Login(username, password, totp_code) → (token, expires_at, error)
    • Client.Logout() → error
    • Client.RenewToken() → (token, expires_at, error)
    • Client.ValidateToken(token) → (claims, error)
    • Client.GetPublicKey() → (public_key_jwk, error)
    • Client.Health() → error
    • Account management methods (admin only): CreateAccount, ListAccounts, GetAccount, UpdateAccount, DeleteAccount
    • Role management: GetRoles, SetRoles
    • Token management: IssueServiceToken, RevokeToken
    • PG credentials: GetPGCreds, SetPGCreds
  • clients/testdata/ — shared JSON fixtures for mock server responses (used by all language test suites)
  • A mock MCIAS server (test/mock/) in Go that can be started by integration tests in any language via subprocess or a pre-built binary

Step 9.2: Go client library

Acceptance criteria:

  • clients/go/ — Go module git.wntrmute.dev/kyle/mcias/clients/go
  • Package mciasgoclient exposes the canonical API surface from Step 9.1
  • Uses net/http with crypto/tls; custom CA cert supported via x509.CertPool
  • Token stored in-memory; Client.Token() accessor returns current token
  • Thread-safe: concurrent calls from multiple goroutines are safe
  • All JSON decoding uses DisallowUnknownFields
  • Tests:
    • Unit tests with httptest.Server for all methods
    • Integration test against mock server (Step 9.1) covering full login → validate → logout flow
    • go test -race ./... passes with zero race conditions
  • go doc comments on all exported types and methods

Step 9.3: Rust client library

Acceptance criteria:

  • clients/rust/ — Rust crate mcias-client
  • Uses reqwest (async, TLS-enabled) and serde / serde_json
  • Exposes the canonical API surface from Step 9.1 as an async Rust API (tokio-compatible)
  • Custom CA cert supported via reqwest::Certificate
  • Token stored in Arc<Mutex<Option<String>>> for async-safe sharing
  • Errors: typed MciasError enum covering Unauthenticated, Forbidden, NotFound, InvalidInput, Transport, Server
  • Tests:
    • Unit tests with wiremock or mockito for all methods
    • Integration test against mock server covering full login → validate → logout
    • cargo test passes; cargo clippy -- -D warnings passes
  • cargo doc with #[doc] comments on all public items

Step 9.4: Common Lisp client library

Acceptance criteria:

  • clients/lisp/ — ASDF system mcias-client
  • Uses dexador for HTTP and yason (or cl-json) for JSON
  • TLS handled by the underlying Dexador/Usocket stack; custom CA cert documented (platform-specific)
  • Exposes the canonical API surface from Step 9.1 as synchronous functions with keyword arguments
  • Errors signalled as conditions: mcias-error, mcias-unauthenticated, mcias-forbidden, mcias-not-found
  • Token stored as a slot on the mcias-client CLOS object
  • Tests:
    • Unit tests using fiveam with mock HTTP responses (via dexador mocking or a local test server)
    • Integration test against mock server covering full login → validate → logout
    • Tests run with SBCL; (asdf:test-system :mcias-client) passes
  • Docstrings on all exported symbols; (describe 'mcias-client) is informative

Step 9.5: Python client library

Acceptance criteria:

  • clients/python/ — Python package mcias_client; supports Python 3.11+
  • Uses httpx (sync and async variants) or requests (sync-only acceptable for v1)
  • Exposes the canonical API surface from Step 9.1 as a MciasClient class
  • Custom CA cert supported via ssl.create_default_context() with cafile
  • Token stored as an instance attribute; client.token property
  • Errors: MciasError base class with subclasses MciasAuthError, MciasForbiddenError, MciasNotFoundError
  • Typed: full py.typed marker; all public symbols annotated with PEP 526 type annotations; mypy --strict passes
  • Tests:
    • Unit tests with pytest and respx (httpx mock) or responses (requests) for all methods
    • Integration test against mock server covering full login → validate → logout
    • pytest passes; ruff check and mypy --strict pass
  • Docstrings on all public classes and methods; help(MciasClient) is informative

Step 9.6: Documentation and commit

Acceptance criteria:

  • Each client library has its own README.md with: installation instructions, quickstart example, API reference summary, error handling guide
  • ARCHITECTURE.md §19 written (client library design, per-language notes, versioning strategy)
  • PROGRESS.md updated to reflect Phase 9 complete

Phase 10 — Web UI (HTMX) [COMPLETE]

Not in the original plan. Implemented alongside and after Phase 3.

See ARCHITECTURE.md §8 (Web Management UI) for design details.

Step 10.1: internal/ui — HTMX web interface

Acceptance criteria:

  • Go html/template pages embedded at compile time via web/embed.go
  • CSRF protection: HMAC-signed double-submit cookie (mcias_csrf)
  • Session: JWT stored as HttpOnly; Secure; SameSite=Strict cookie
  • Security headers: Content-Security-Policy: default-src 'self', X-Frame-Options: DENY, Referrer-Policy: strict-origin
  • Pages: login, dashboard, account list/detail, role editor, tag editor, pgcreds, audit log viewer, policy rules, user profile, service-accounts
  • HTMX partial-page updates for mutations (role updates, tag edits, policy toggles, access grants)
  • Empty-state handling on all list pages (zero records case tested)

Step 10.2: Swagger UI at /docs

Acceptance criteria:

  • GET /docs serves Swagger UI for openapi.yaml
  • swagger-ui-bundle.js and swagger-ui.css bundled locally in web/static/ (CDN blocked by CSP default-src 'self')
  • GET /docs/openapi.yaml serves the OpenAPI spec
  • openapi.yaml kept in sync with REST API surface

Phase 11 — Authorization Policy Engine [COMPLETE]

Not in the original plan (CLI subcommands for policy were planned in Phase 4, but the engine itself was not a discrete plan phase).

See ARCHITECTURE.md §20 for full design, evaluation algorithm, and built-in default rules.

Step 11.1: internal/policy — in-process ABAC engine

Acceptance criteria:

  • Pure evaluation: Evaluate(input PolicyInput, rules []Rule) (Effect, *Rule)
  • Deny-wins: any explicit deny overrides all allows
  • Default-deny: no matching rule → deny
  • Built-in default rules (IDs -1 … -7) compiled in; reproduce previous binary admin/non-admin behavior exactly; cannot be disabled via API
  • Match fields: roles, account types, subject UUID, actions, resource type, owner-matches-subject, service names, required tags (all ANDed; zero value = wildcard)
  • Temporal constraints on DB-backed rules: not_before, expires_at
  • Engine wrapper: caches rule set in memory; reloads on policy mutations
  • Tests: all built-in rules; deny-wins over allow; default-deny fallback; temporal filtering; concurrent access

Step 11.2: Middleware and REST integration

Acceptance criteria:

  • RequirePolicy(engine, action, resourceType) middleware replaces RequireRole("admin") where policy-gated
  • Every explicit deny produces a policy_deny audit event
  • REST endpoints: GET|POST /v1/policy/rules, GET|PATCH|DELETE /v1/policy/rules/{id}
  • DB schema: policy_rules and account_tags tables (migrations 000004, 000006)
  • PATCH /v1/policy/rules/{id} supports updating priority, enabled, not_before, expires_at

Phase 12 — Vault Seal/Unseal Lifecycle [COMPLETE]

Not in the original plan.

See ARCHITECTURE.md §8 (Vault Endpoints) for the API surface.

Step 12.1: internal/vault — master key lifecycle

Acceptance criteria:

  • Thread-safe Vault struct with sync.RWMutex-protected state
  • Methods: IsSealed(), Unseal(passphrase), Seal(), MasterKey(), PrivKey(), PubKey()
  • Seal() zeroes all key material before nilling (memguard-style cleanup)
  • DeriveFromPassphrase() and DecryptSigningKey() extracted to derive.go for reuse by unseal handlers
  • Tests: state transitions; key zeroing verified; concurrent read/write safety

Step 12.2: REST and UI integration

Acceptance criteria:

  • POST /v1/vault/unseal — rate-limited (3/s burst 5); derives key, unseals
  • GET /v1/vault/status — always accessible; returns {"sealed": bool}
  • POST /v1/vault/seal — admin only; zeroes key material
  • GET /v1/health returns {"status":"sealed"} when sealed
  • All other /v1/* endpoints return 503 vault_sealed when sealed
  • UI redirects all paths to /unseal when sealed (except /static/)
  • gRPC: sealedInterceptor first in chain; blocks all RPCs except Health
  • Startup: server may start in sealed state if passphrase env var is absent
  • Audit events: vault_sealed, vault_unsealed

Phase 13 — Token Delegation and pgcred Access Grants [COMPLETE]

Not in the original plan.

See ARCHITECTURE.md §21 (Token Issuance Delegation) for design details.

Step 13.1: Service account token delegation

Acceptance criteria:

  • DB migration 000008: service_account_delegates table
  • POST /accounts/{id}/token/delegates — admin grants delegation
  • DELETE /accounts/{id}/token/delegates/{grantee} — admin revokes delegation
  • POST /accounts/{id}/token — accepts admin or delegate (not admin-only)
  • One-time token download: nonce stored in sync.Map with 5-minute TTL; GET /token/download/{nonce} serves token as attachment, deletes nonce
  • /service-accounts page for non-admin delegates
  • Audit events: token_delegate_granted, token_delegate_revoked

Step 13.2: pgcred fine-grained access grants

Acceptance criteria:

  • DB migration 000005: pgcred_access_grants table
  • POST /accounts/{id}/pgcreds/access — owner grants read access to grantee
  • DELETE /accounts/{id}/pgcreds/access/{grantee} — owner revokes access
  • GET /v1/pgcreds — lists all credentials accessible to caller (owned + granted); includes credential ID for reference
  • Grantees may view connection metadata; password is never decrypted for them
  • Audit events: pgcred_access_granted, pgcred_access_revoked

Phase 14 — FIDO2/WebAuthn and Passkey Authentication

Goal: Add FIDO2/WebAuthn support for passwordless passkey login and hardware security key 2FA. Discoverable credentials enable passwordless login; non-discoverable credentials serve as 2FA. Either WebAuthn or TOTP satisfies the 2FA requirement.

Step 14.1: Dependency, config, and model types

Acceptance criteria:

  • github.com/go-webauthn/webauthn dependency added
  • WebAuthnConfig struct in config with RPID, RPOrigin, DisplayName
  • Validation: if any field set, RPID+RPOrigin required; RPOrigin must be HTTPS
  • WebAuthnCredential model type with encrypted-at-rest fields
  • Audit events: webauthn_enrolled, webauthn_removed, webauthn_login_ok, webauthn_login_fail
  • Policy actions: ActionEnrollWebAuthn, ActionRemoveWebAuthn

Step 14.2: Database migration and CRUD

Acceptance criteria:

  • Migration 000009: webauthn_credentials table with encrypted credential fields
  • Full CRUD: Create, Get (by ID, by account), Delete (ownership-checked and admin), DeleteAll, UpdateSignCount, UpdateLastUsed, Has, Count
  • DB tests for all operations including ownership checks and cascade behavior

Step 14.3: WebAuthn adapter package

Acceptance criteria:

  • internal/webauthn/ package with adapter, user, and converter
  • NewWebAuthn(cfg) factory wrapping library initialization
  • AccountUser implementing webauthn.User interface
  • EncryptCredential/DecryptCredential/DecryptCredentials round-trip encryption
  • Tests for encrypt/decrypt, interface compliance, wrong-key rejection

Step 14.4: REST endpoints

Acceptance criteria:

  • POST /v1/auth/webauthn/register/begin — password re-auth, returns creation options
  • POST /v1/auth/webauthn/register/finish — completes registration, encrypts credential
  • POST /v1/auth/webauthn/login/begin — discoverable and username-scoped flows
  • POST /v1/auth/webauthn/login/finish — validates assertion, issues JWT
  • GET /v1/accounts/{id}/webauthn — admin, returns metadata only
  • DELETE /v1/accounts/{id}/webauthn/{credentialId} — admin remove
  • Challenge store: sync.Map with 120s TTL, background cleanup

Step 14.5: Web UI

Acceptance criteria:

  • Profile page: passkey enrollment form, credential list with delete
  • Login page: "Sign in with passkey" button with discoverable flow
  • Account detail page: passkey section with admin remove
  • CSP-compliant webauthn.js (external script, base64url helpers)
  • Empty state handling for zero credentials

Step 14.6: gRPC handlers

Acceptance criteria:

  • Proto messages and RPCs: ListWebAuthnCredentials, RemoveWebAuthnCredential
  • gRPC handler implementation delegating to shared packages
  • Regenerated protobuf stubs

Step 14.7: mciasdb offline management

Acceptance criteria:

  • mciasdb webauthn list --id UUID
  • mciasdb webauthn delete --id UUID --credential-id N
  • mciasdb webauthn reset --id UUID (deletes all)
  • mciasdb account reset-webauthn --id UUID alias
  • All operations write audit events

Step 14.8: OpenAPI and documentation

Acceptance criteria:

  • All 6 REST endpoints documented in openapi.yaml
  • WebAuthnCredentialInfo schema, webauthn_enabled/webauthn_count on Account
  • ARCHITECTURE.md §22 with design details
  • PROJECT_PLAN.md Phase 14
  • PROGRESS.md updated

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)
        → Phase 7 (7.1 → 7.2 → 7.3 → 7.4 → 7.5 → 7.6)
        → Phase 8 (8.1 → 8.2 → 8.3 → 8.4 → 8.5 → 8.6)
        → Phase 9 (9.1 → 9.2 → 9.3 → 9.4 → 9.5 → 9.6)
        → Phase 10 (interleaved with Phase 3 and later phases)
        → Phase 11 (interleaved with Phase 34)
        → Phase 12 (post Phase 3)
        → Phase 13 (post Phase 3 and 11)
        → Phase 14 (post v1.0.0)

Each step must have passing tests before the next step begins. Phases 013 complete as of v1.0.0 (2026-03-15). Phase 14 complete as of 2026-03-16.