Files
mcias/PROGRESS.md
Kyle Isom 59d51a1d38 Implement Phase 7: gRPC dual-stack interface
- proto/mcias/v1/: AdminService, AuthService, TokenService,
  AccountService, CredentialService; generated Go stubs in gen/
- internal/grpcserver: full handler implementations sharing all
  business logic (auth, token, db, crypto) with REST server;
  interceptor chain: logging -> auth (JWT alg-first + revocation) ->
  rate-limit (token bucket, 10 req/s, burst 10, per-IP)
- internal/config: optional grpc_addr field in [server] section
- cmd/mciassrv: dual-stack startup; gRPC/TLS listener on grpc_addr
  when configured; graceful shutdown of both servers in 15s window
- cmd/mciasgrpcctl: companion gRPC CLI mirroring mciasctl commands
  (health, pubkey, account, role, token, pgcreds) using TLS with
  optional custom CA cert
- internal/grpcserver/grpcserver_test.go: 20 tests via bufconn covering
  public RPCs, auth interceptor (no token, invalid, revoked -> 401),
  non-admin -> 403, Login/Logout/RenewToken/ValidateToken flows,
  AccountService CRUD, SetPGCreds/GetPGCreds AES-GCM round-trip,
  credential fields absent from all responses
Security:
  JWT validation path identical to REST: alg header checked before
  signature, alg:none rejected, revocation table checked after sig.
  Authorization metadata value never logged by any interceptor.
  Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from
  all proto response messages — enforced by proto design and confirmed
  by test TestCredentialFieldsAbsentFromAccountResponse.
  Login dummy-Argon2 timing guard preserves timing uniformity for
  unknown users (same as REST handleLogin).
  TLS required at listener level; cmd/mciassrv uses
  credentials.NewServerTLSFromFile; no h2c offered.
137 tests pass, zero race conditions (go test -race ./...)
2026-03-11 14:38:47 -07:00

11 KiB
Raw Blame History

MCIAS Progress

Source of truth for current development state.


Current Status: Phase 7 Complete — Phases 89 Planned

137 tests pass with zero race conditions. Phase 7 (gRPC dual-stack) is complete. Phases 89 are designed and documented; implementation not yet started.

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
  • Phase 6: mciasdb — direct SQLite maintenance tool
  • Phase 7: gRPC interface (alternate transport; dual-stack with REST)

Planned Phases

  • Phase 8: Operational artifacts (systemd unit, man pages, Makefile, install script)
  • Phase 9: Client libraries (Go, Rust, Common Lisp, Python)

Implementation Log

2026-03-11 — Phase 7: gRPC dual-stack

proto/mcias/v1/

  • common.proto — shared types: Account, TokenInfo, PGCreds, Error
  • admin.proto — AdminService: Health (public), GetPublicKey (public)
  • auth.proto — AuthService: Login (public), Logout, RenewToken, EnrollTOTP, ConfirmTOTP, RemoveTOTP (admin)
  • token.proto — TokenService: ValidateToken (public), IssueServiceToken (admin), RevokeToken (admin)
  • account.proto — AccountService (CRUD + roles, all admin) + CredentialService (GetPGCreds, SetPGCreds, all admin)
  • proto/generate.go — go:generate directive for protoc regeneration
  • Generated Go stubs in gen/mcias/v1/ via protoc + protoc-gen-go-grpc

internal/grpcserver

  • grpcserver.go — Server struct, interceptor chain (loggingInterceptor → authInterceptor → rateLimitInterceptor), GRPCServer() / GRPCServerWithCreds(creds) / buildServer() helpers, per-IP token-bucket rate limiter (same parameters as REST: 10 req/s, burst 10), extractBearerFromMD, requireAdmin
  • admin.go — Health, GetPublicKey implementations
  • auth.go — Login (with dummy-Argon2 timing guard), Logout, RenewToken, EnrollTOTP, ConfirmTOTP, RemoveTOTP
  • tokenservice.go — ValidateToken (returns valid=false on error, never an RPC error), IssueServiceToken, RevokeToken
  • accountservice.go — ListAccounts, CreateAccount, GetAccount, UpdateAccount, DeleteAccount, GetRoles, SetRoles
  • credentialservice.go — GetPGCreds (AES-GCM decrypt), SetPGCreds (AES-GCM encrypt)

Security invariants (same as REST server):

  • Authorization metadata value never logged by any interceptor
  • Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from all proto response messages by proto design + grpcserver enforcement
  • JWT validation: alg-first, then signature, then revocation table lookup
  • Public RPCs bypass auth: Health, GetPublicKey, ValidateToken, Login
  • Admin-only RPCs checked in-handler via requireAdmin(ctx)
  • Dummy Argon2 in Login for unknown users prevents timing enumeration

internal/config additions

  • GRPCAddr string field in ServerConfig (optional; omit to disable gRPC)

cmd/mciassrv updates

  • Dual-stack: starts both HTTPS (REST) and gRPC/TLS listeners when grpc_addr is configured in [server] section
  • gRPC listener uses same TLS cert/key as REST; credentials passed at server-construction time via GRPCServerWithCreds
  • Graceful shutdown drains both listeners within 15s window

cmd/mciasgrpcctl

  • New companion CLI for gRPC management
  • Global flags: -server (host:port), -token (or MCIAS_TOKEN), -cacert
  • Commands: health, pubkey, account (list/create/get/update/delete), role (list/set), token (validate/issue/revoke), pgcreds (get/set)
  • Connects with TLS; custom CA cert support for self-signed certs

Tests

  • internal/grpcserver/grpcserver_test.go: 20 tests using bufconn (in-process, no network sockets); covers:
    • Health and GetPublicKey (public RPCs, no auth)
    • Auth interceptor: no token, invalid token, revoked token all → 401
    • Non-admin calling admin RPC → 403
    • Login: success, wrong password, unknown user
    • Logout and RenewToken
    • ValidateToken: good token → valid=true; garbage → valid=false (no error)
    • IssueServiceToken requires admin
    • ListAccounts: non-admin → 403, admin → OK
    • CreateAccount, GetAccount, UpdateAccount, SetRoles, GetRoles lifecycle
    • SetPGCreds + GetPGCreds with AES-GCM round-trip verification
    • PGCreds requires admin
    • Credential fields absent from account responses (structural enforcement)

Dependencies added

  • google.golang.org/grpc v1.68.0
  • google.golang.org/protobuf v1.36.0
  • google.golang.org/grpc/test/bufconn (test only, included in grpc module)

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

2026-03-11 — Phase 6: mciasdb

cmd/mciasdb

  • Binary skeleton: config loading, master key derivation (identical to mciassrv for key compatibility), DB open + migrate on startup
  • schema verify / schema migrate — reports and applies pending migrations
  • account list/get/create/set-password/set-status/reset-totp — offline account management; set-password prompts interactively (no --password flag)
  • role list/grant/revoke — direct role management
  • token list/revoke/revoke-all + prune tokens — token maintenance
  • audit tail/query — audit log inspection with --json output flag
  • pgcreds get/set — decrypt/encrypt Postgres credentials with master key; set prompts interactively; get prints warning before sensitive output
  • All write operations emit audit log entries tagged actor:"mciasdb"

internal/db additions

  • ListTokensForAccount(accountID) — newest-first token list for an account
  • ListAuditEvents(AuditQueryParams) — filtered audit query (account, type, since, limit)
  • TailAuditEvents(n) — last n events, returned oldest-first
  • SchemaVersion(db) / LatestSchemaVersion — exported for mciasdb verify

Dependencies

  • Added golang.org/x/term v0.29.0 for interactive password prompting (no-echo terminal reads); pinned to version compatible with local module cache
  • golang.org/x/crypto pinned at v0.33.0 (compatible with term@v0.29.0)

Tests

  • internal/db/mciasdb_test.go: 4 tests covering ListTokensForAccount, ListAuditEvents filtering, TailAuditEvents ordering, combined filters
  • cmd/mciasdb/mciasdb_test.go: 20 tests covering all subcommands via in-memory SQLite and stdout capture

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

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