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.
This commit is contained in:
2026-03-11 11:54:14 -07:00
parent d75a1d6fd3
commit f02eff21b4
10 changed files with 779 additions and 114 deletions

View File

@@ -1,15 +1,15 @@
// Package auth implements login, TOTP verification, and credential management.
//
// Security design:
// - All credential comparisons use constant-time operations to resist timing
// side-channels. crypto/subtle.ConstantTimeCompare is used wherever secrets
// are compared.
// - On any login failure the error returned to the caller is always generic
// ("invalid credentials"), regardless of which step failed, to prevent
// user enumeration.
// - TOTP uses a ±1 time-step window (±30s) per RFC 6238 recommendation.
// - PHC string format is used for password hashes, enabling transparent
// parameter upgrades without re-migration.
// - All credential comparisons use constant-time operations to resist timing
// side-channels. crypto/subtle.ConstantTimeCompare is used wherever secrets
// are compared.
// - On any login failure the error returned to the caller is always generic
// ("invalid credentials"), regardless of which step failed, to prevent
// user enumeration.
// - TOTP uses a ±1 time-step window (±30s) per RFC 6238 recommendation.
// - PHC string format is used for password hashes, enabling transparent
// parameter upgrades without re-migration.
package auth
import (
@@ -168,10 +168,10 @@ func parsePHC(phc string) (ArgonParams, []byte, []byte, error) {
// A ±1 time-step window (±30s) is allowed to accommodate clock skew.
//
// Security:
// - Comparison uses crypto/subtle.ConstantTimeCompare to resist timing attacks.
// - Only RFC 6238-compliant HOTP (HMAC-SHA1) is implemented; no custom crypto.
// - A ±1 window is the RFC 6238 recommendation; wider windows increase
// exposure to code interception between generation and submission.
// - Comparison uses crypto/subtle.ConstantTimeCompare to resist timing attacks.
// - Only RFC 6238-compliant HOTP (HMAC-SHA1) is implemented; no custom crypto.
// - A ±1 window is the RFC 6238 recommendation; wider windows increase
// exposure to code interception between generation and submission.
func ValidateTOTP(secret []byte, code string) (bool, error) {
if len(code) != 6 {
return false, nil

View File

@@ -35,10 +35,10 @@ type DatabaseConfig struct {
// TokensConfig holds JWT issuance settings.
type TokensConfig struct {
Issuer string `toml:"issuer"`
DefaultExpiry duration `toml:"default_expiry"`
AdminExpiry duration `toml:"admin_expiry"`
ServiceExpiry duration `toml:"service_expiry"`
Issuer string `toml:"issuer"`
DefaultExpiry duration `toml:"default_expiry"`
AdminExpiry duration `toml:"admin_expiry"`
ServiceExpiry duration `toml:"service_expiry"`
}
// Argon2Config holds Argon2id password hashing parameters.
@@ -46,7 +46,7 @@ type TokensConfig struct {
// We enforce these minimums to prevent accidental weakening.
type Argon2Config struct {
Time uint32 `toml:"time"`
Memory uint32 `toml:"memory"` // KiB
Memory uint32 `toml:"memory"` // KiB
Threads uint8 `toml:"threads"`
}

View File

@@ -1,13 +1,13 @@
// Package crypto provides key management and encryption helpers for MCIAS.
//
// Security design:
// - All random material (keys, nonces, salts) comes from crypto/rand.
// - AES-256-GCM is used for symmetric encryption; the 256-bit key size
// provides 128-bit post-quantum security margin.
// - Ed25519 is used for JWT signing; it has no key-size or parameter
// malleability issues that affect RSA/ECDSA.
// - The master key KDF uses Argon2id (separate parameterisation from
// password hashing) to derive a 256-bit key from a passphrase.
// - All random material (keys, nonces, salts) comes from crypto/rand.
// - AES-256-GCM is used for symmetric encryption; the 256-bit key size
// provides 128-bit post-quantum security margin.
// - Ed25519 is used for JWT signing; it has no key-size or parameter
// malleability issues that affect RSA/ECDSA.
// - The master key KDF uses Argon2id (separate parameterisation from
// password hashing) to derive a 256-bit key from a passphrase.
package crypto
import (

View File

@@ -35,14 +35,14 @@ func (db *DB) CreateAccount(username string, accountType model.AccountType, pass
}
return &model.Account{
ID: rowID,
UUID: id,
Username: username,
AccountType: accountType,
Status: model.AccountStatusActive,
ID: rowID,
UUID: id,
Username: username,
AccountType: accountType,
Status: model.AccountStatusActive,
PasswordHash: passwordHash,
CreatedAt: createdAt,
UpdatedAt: createdAt,
CreatedAt: createdAt,
UpdatedAt: createdAt,
}, nil
}

View File

@@ -1,14 +1,14 @@
// Package middleware provides HTTP middleware for the MCIAS server.
//
// Security design:
// - RequireAuth extracts the Bearer token from the Authorization header,
// validates it (alg check, signature, expiry, issuer), and checks revocation
// against the database before injecting claims into the request context.
// - RequireRole checks claims from context for the required role.
// No role implies no access; the check fails closed.
// - RateLimit implements a per-IP token bucket to limit login brute-force.
// - RequestLogger logs request metadata but never logs the Authorization
// header value (which contains credential tokens).
// - RequireAuth extracts the Bearer token from the Authorization header,
// validates it (alg check, signature, expiry, issuer), and checks revocation
// against the database before injecting claims into the request context.
// - RequireRole checks claims from context for the required role.
// No role implies no access; the check fails closed.
// - RateLimit implements a per-IP token bucket to limit login brute-force.
// - RequestLogger logs request metadata but never logs the Authorization
// header value (which contains credential tokens).
package middleware
import (

View File

@@ -2,11 +2,11 @@
// for the MCIAS authentication server.
//
// Security design:
// - All endpoints use HTTPS (enforced at the listener level in cmd/mciassrv).
// - Authentication state is carried via JWT; no cookies or server-side sessions.
// - Credential fields (password hash, TOTP secret, Postgres password) are
// never included in any API response.
// - All JSON parsing uses strict decoders that reject unknown fields.
// - All endpoints use HTTPS (enforced at the listener level in cmd/mciassrv).
// - Authentication state is carried via JWT; no cookies or server-side sessions.
// - Credential fields (password hash, TOTP secret, Postgres password) are
// never included in any API response.
// - All JSON parsing uses strict decoders that reject unknown fields.
package server
import (
@@ -610,7 +610,7 @@ func (s *Server) handleSetRoles(w http.ResponseWriter, r *http.Request) {
// ---- TOTP endpoints ----
type totpEnrollResponse struct {
Secret string `json:"secret"` // base32-encoded
Secret string `json:"secret"` // base32-encoded
OTPAuthURI string `json:"otpauth_uri"`
}
@@ -652,7 +652,7 @@ func (s *Server) handleTOTPEnroll(w http.ResponseWriter, r *http.Request) {
// Security: return the secret for display to the user. It is only shown
// once; subsequent reads are not possible (only the encrypted form is stored).
writeJSON(w, http.StatusOK, totpEnrollResponse{
Secret: b32Secret,
Secret: b32Secret,
OTPAuthURI: otpURI,
})
}

View File

@@ -1,13 +1,13 @@
// Package token handles JWT issuance, validation, and revocation for MCIAS.
//
// Security design:
// - Algorithm header is checked FIRST, before any signature verification.
// This prevents algorithm-confusion attacks (CVE-2022-21449 class).
// - Only "EdDSA" is accepted; "none", HS*, RS*, ES* are all rejected.
// - The signing key is taken from the server's keystore, never from the token.
// - All standard claims (exp, iat, iss, jti) are required and validated.
// - JTIs are UUIDs generated from crypto/rand (via google/uuid).
// - Token values are never stored; only JTIs are recorded for revocation.
// - Algorithm header is checked FIRST, before any signature verification.
// This prevents algorithm-confusion attacks (CVE-2022-21449 class).
// - Only "EdDSA" is accepted; "none", HS*, RS*, ES* are all rejected.
// - The signing key is taken from the server's keystore, never from the token.
// - All standard claims (exp, iat, iss, jti) are required and validated.
// - JTIs are UUIDs generated from crypto/rand (via google/uuid).
// - Token values are never stored; only JTIs are recorded for revocation.
package token
import (