fix: auth/2FA security — stdout-log code delivery is dev/test-only, production fails closed until email/SMS; verification-code hashing, lockout recovery, sabredav fail-closed
- TWO_FACTOR_ALLOW_LOG_DELIVERY production opt-in REMOVED: plaintext codes are written to the stdout log ([2FA]/[VERIFY]) only in dev/test builds as a local DEV ONLY feature while email/SMS delivery (P6) is implemented. Production builds have no delivery channel and code issuance fails closed (503) under any configuration — no silent log-based code leak - verification/2FA codes hashed at rest (HMAC-SHA256 via TWO_FACTOR_PEPPER, CHAR(64)); [VERIFY] dev log relay; per-user brute-force budget; password_reset purpose clears lockout for self-service recovery; dummy-bcrypt on login no-user path kills timing oracle - sabredav weak-password list + entropy gate; .env.example ships fail-closed DAV_ADMIN_PASSWORD - delete-account re-auth (current_password + fresh 2FA code when enforced) - prod-tag suite (run-prod-tag-tests.sh) compiles and runs the production 2FA issuance gate: production ALWAYS reports no delivery channel and refuses issuance after the pepper check - startup_checks_test SNAPSHOT_ENC_KEY values built at runtime so gitleaks sees no secret-shaped literals - env-docs parity updated (flag removed, 38 vars)
This commit is contained in:
@@ -13,67 +13,65 @@ package user
|
||||
// their assertions ONLY when the prod variant marker reports the real prod
|
||||
// functions are live; under the test tag they skip with the same documented
|
||||
// rationale the payments package uses (twofa_delivery_prod_test.go).
|
||||
//
|
||||
// CLOSING THE GAP: run-prod-tag-tests.sh (backend/) runs `go test -tags
|
||||
// "!dev,!test" ./handlers/user/` — the ONLY build configuration where
|
||||
// twofa_prod.go compiles AND twofaProdVariant is true, so the assertions below
|
||||
// actually execute there. The `if !twofaProdVariant { t.Skip(...) }` guards
|
||||
// MUST stay: under the CI "test,!dev" matrix the dev/test variants are still
|
||||
// the compiled functions (the `test` tag matches `dev || test`), so without
|
||||
// the guards those runs would FAIL rather than skip.
|
||||
//
|
||||
// DELIVERY POSTURE (current): stdout-log delivery of 2FA codes is a
|
||||
// DEV/TEST-ONLY local feature. A production build has NO delivery channel of
|
||||
// any kind — email/SMS is not wired yet (P6) and there is deliberately no
|
||||
// production opt-in to log delivery — so code issuance fails closed
|
||||
// unconditionally (after the pepper check) and twoFADeliveryAvailable is
|
||||
// always false.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// twoFAAllowLogDeliveryEnv is only defined in twofa_prod.go (!dev && !test);
|
||||
// use the literal env name so this test also compiles under `test,!dev`.
|
||||
const allowLogDeliveryEnv = "TWO_FACTOR_ALLOW_LOG_DELIVERY"
|
||||
|
||||
// TestTwoFAEnsureIssueAllowed_ProdPredicate pins the production issuance gate:
|
||||
// it fails closed without TWO_FACTOR_PEPPER (an unsalted digest in the 1M code
|
||||
// space would be offline-brute-forceable) or without a delivery channel, and
|
||||
// allows issuance only when both are configured.
|
||||
// space would be offline-brute-forceable) and, with the pepper set, STILL fails
|
||||
// closed because a production build has no delivery channel (email/SMS unwired,
|
||||
// stdout-log delivery is dev/test-only) — issuance can never succeed until a
|
||||
// real transport lands.
|
||||
func TestTwoFAEnsureIssueAllowed_ProdPredicate(t *testing.T) {
|
||||
if !twofaProdVariant {
|
||||
t.Skip("twoFAEnsureIssueAllowed() is the dev/test build's always-allowed variant (twofa_dev.go, `dev || test`); the prod fail-closed branches are unreachable under the test tag — see the file header for the documented limitation")
|
||||
}
|
||||
|
||||
os.Unsetenv(twoFAPepperEnv)
|
||||
os.Unsetenv(allowLogDeliveryEnv)
|
||||
if err := twoFAEnsureIssueAllowed(); err == nil {
|
||||
t.Error("expected issuance refused without TWO_FACTOR_PEPPER in a production build")
|
||||
} else if err.Error() != "TWO_FACTOR_PEPPER is not set; refusing to issue a 2FA code (an unsalted digest would be offline-brute-forceable)" {
|
||||
t.Errorf("expected the pepper-required error without the pepper, got %v", err)
|
||||
}
|
||||
|
||||
// With the pepper set, a production build STILL refuses: there is no
|
||||
// delivery channel (email/SMS unwired, P6; stdout-log delivery is a
|
||||
// dev/test-only local feature and there is no production opt-in).
|
||||
os.Setenv(twoFAPepperEnv, "test-pepper")
|
||||
os.Unsetenv(allowLogDeliveryEnv)
|
||||
if err := twoFAEnsureIssueAllowed(); err == nil {
|
||||
t.Error("expected issuance refused without a delivery channel in a production build")
|
||||
t.Error("expected issuance refused in a production build with no delivery channel (email/SMS unwired, log delivery dev/test-only)")
|
||||
} else if err != errTwoFADeliveryUnavailable {
|
||||
t.Errorf("expected errTwoFADeliveryUnavailable without a channel, got %v", err)
|
||||
}
|
||||
|
||||
os.Setenv(allowLogDeliveryEnv, "true")
|
||||
if err := twoFAEnsureIssueAllowed(); err != nil {
|
||||
t.Errorf("expected issuance allowed with both the pepper and a delivery channel, got %v", err)
|
||||
t.Errorf("expected errTwoFADeliveryUnavailable with no channel, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTwoFADeliveryAvailable_ProdPredicate pins the production delivery
|
||||
// predicate: TWO_FACTOR_ALLOW_LOG_DELIVERY unset → no channel (false), exactly
|
||||
// "true" → channel (true), any other value → no channel.
|
||||
// predicate: a production build ALWAYS reports no delivery channel — stdout-log
|
||||
// delivery is a dev/test-only local feature, never a production channel.
|
||||
func TestTwoFADeliveryAvailable_ProdPredicate(t *testing.T) {
|
||||
if !twofaProdVariant {
|
||||
t.Skip("twoFADeliveryAvailable() is the dev/test build's trivially-true variant (twofa_dev.go, `dev || test`); the 503 delivery-unavailable branch is unreachable under the test tag — see the file header for the documented limitation")
|
||||
t.Skip("twoFADeliveryAvailable() is the dev/test build's trivially-true variant (twofa_dev.go, `dev || test`); the always-false production predicate is unreachable under the test tag — see the file header for the documented limitation")
|
||||
}
|
||||
|
||||
os.Unsetenv(allowLogDeliveryEnv)
|
||||
if twoFADeliveryAvailable() {
|
||||
t.Error("production without the explicit opt-in must have NO 2FA delivery channel")
|
||||
}
|
||||
for _, v := range []string{"", "1", "yes", "on", "True", "TRUE", "false"} {
|
||||
os.Setenv(allowLogDeliveryEnv, v)
|
||||
if twoFADeliveryAvailable() {
|
||||
t.Errorf("value %q must NOT open the delivery channel (exact 'true' only)", v)
|
||||
}
|
||||
}
|
||||
os.Setenv(allowLogDeliveryEnv, "true")
|
||||
if !twoFADeliveryAvailable() {
|
||||
t.Error("the explicit insecure log-delivery opt-in must open the channel")
|
||||
t.Error("a production build must ALWAYS report NO 2FA delivery channel (email/SMS unwired; stdout-log delivery is dev/test-only)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user