Commit Graph

6 Commits

Author SHA1 Message Date
6e690c4435 Fix F-07: pre-compute real Argon2 dummy hash via sync.Once
- auth/auth.go: add DummyHash() which uses sync.Once to compute
  HashPassword("dummy-password-for-timing-only", DefaultArgonParams())
  on first call; subsequent calls return the cached PHC string;
  add sync to imports
- auth/auth_test.go: TestDummyHashIsValidPHC verifies the hash
  parses and verifies correctly; TestDummyHashIsCached verifies
  sync.Once behaviour; TestDummyHashMatchesDefaultParams verifies
  embedded m/t/p match DefaultArgonParams()
- server/server.go, grpcserver/auth.go, ui/ui.go: replace five
  hardcoded PHC strings with auth.DummyHash() calls
- AUDIT.md: mark F-07 as fixed
Security: the previous hardcoded hash used a 6-byte salt and
  6-byte output ("testsalt"/"testhash" in base64), which Argon2id
  verifies faster than a real 16-byte-salt / 32-byte-output hash.
  This timing gap was measurable and could aid user enumeration.
  auth.DummyHash() uses identical parameters and full-length salt
  and output, so dummy verification timing matches real timing
  exactly, regardless of future parameter changes.
2026-03-11 20:37:27 -07:00
005e734842 Fix F-16: revoke old system token before issuing new one
- ui/handlers_accounts.go (handleIssueSystemToken): call
  GetSystemToken before issuing; if one exists, call
  RevokeToken(existing.JTI, "rotated") before TrackToken
  and SetSystemToken for the new token; mirrors the pattern
  in REST handleTokenIssue and gRPC IssueServiceToken
- db/db_test.go: TestSystemTokenRotationRevokesOld verifies
  the full rotation flow: old JTI revoked with reason
  "rotated", new JTI tracked and active, GetSystemToken
  returns the new JTI
- AUDIT.md: mark F-16 as fixed
Security: without this fix an old system token remained valid
  after rotation until its natural expiry, giving a leaked or
  stolen old token extra lifetime. With the revocation the old
  JTI is immediately marked in token_revocation so any validator
  checking revocation status rejects it.
2026-03-11 20:34:57 -07:00
d42f51fc83 Fix F-02: replace password-in-hidden-field with nonce
- ui/ui.go: add pendingLogin struct and pendingLogins sync.Map
  to UIServer; add issueTOTPNonce (generates 128-bit random nonce,
  stores accountID with 90s TTL) and consumeTOTPNonce (single-use,
  expiry-checked LoadAndDelete); add dummyHash() method
- ui/handlers_auth.go: split handleLoginPost into step 1
  (password verify → issue nonce) and step 2 (handleTOTPStep,
  consume nonce → validate TOTP) via a new finishLogin helper;
  password never transmitted or stored after step 1
- ui/ui_test.go: refactor newTestMux to reuse new
  newTestUIServer; add TestTOTPNonceIssuedAndConsumed,
  TestTOTPNonceUnknownRejected, TestTOTPNonceExpired, and
  TestLoginPostPasswordNotInTOTPForm; 11/11 tests pass
- web/templates/fragments/totp_step.html: replace
  'name=password' hidden field with 'name=totp_nonce'
- db/accounts.go: add GetAccountByID for TOTP step lookup
- AUDIT.md: mark F-02 as fixed
Security: the plaintext password previously survived two HTTP
  round-trips and lived in the browser DOM during the TOTP step.
  The nonce approach means the password is verified once and
  immediately discarded; only an opaque random token tied to an
  account ID (never a credential) crosses the wire on step 2.
  Nonces are single-use and expire after 90 seconds to limit
  the window if one is captured.
2026-03-11 20:33:04 -07:00
4da39475cc Fix F-04 + F-11; add AUDIT.md
- AUDIT.md: security audit report with 16 findings (F-01..F-16)
- F-04 (server.go): wire loginRateLimit (10 req/s, burst 10) to
  POST /v1/auth/login and POST /v1/token/validate; no limit on
  /v1/health or public-key endpoints
- F-04 (server_test.go): TestLoginRateLimited uses concurrent
  goroutines (sync.WaitGroup) to fire burst+1 requests before
  Argon2id completes, sidestepping token-bucket refill timing;
  TestTokenValidateRateLimited; TestHealthNotRateLimited
- F-11 (ui.go): refactor Register() so all UI routes are mounted
  on a child mux wrapped with securityHeaders middleware; five
  headers set on every response: Content-Security-Policy,
  X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy
- F-11 (ui_test.go): 7 new tests covering login page, dashboard
  redirect, root redirect, static assets, CSP directives,
  HSTS min-age, and middleware unit behaviour
Security: rate limiter on login prevents brute-force credential
  stuffing; security headers mitigate clickjacking (X-Frame-Options
  DENY), MIME sniffing (nosniff), and protocol downgrade (HSTS)
2026-03-11 20:18:09 -07:00
4596ea08ab Fix grpcserver rate limiter: move to Server field
The package-level defaultRateLimiter drained its token bucket
across all test cases, causing later tests to hit ResourceExhausted.
Move rateLimiter from a package-level var to a *grpcRateLimiter field
on Server; New() allocates a fresh instance (10 req/s, burst 10) per
server. Each test's newTestEnv() constructs its own Server, so tests
no longer share limiter state.

Production behaviour is unchanged: a single Server is constructed at
startup and lives for the process lifetime.
2026-03-11 19:23:34 -07:00
a80242ae3e Add HTMX-based UI templates and handlers for account and audit management
- Introduced `web/templates/` for HTMX-fragmented pages (`dashboard`, `accounts`, `account_detail`, `error_fragment`, etc.).
- Implemented UI routes for account CRUD, audit log display, and login/logout with CSRF protection.
- Added `internal/ui/` package for handlers, CSRF manager, session validation, and token issuance.
- Updated documentation to include new UI features and templates directory structure.
- Security: Double-submit CSRF cookies, constant-time HMAC validation, login password/Argon2id re-verification at all steps to prevent bypass.
2026-03-11 18:02:53 -07:00