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:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user