oficina / memoria Public

🤖 fix: make the wiring mistake unsayable, and pin main's path

Two more from the adversarial pass, both about production wiring that no
test observed because every test builds its own app.

M6: main.go could hand UseAuth a different predicate from the one NewAuth
got. The guard then admits everyone while verification still refuses — the
dial reopens, the SSRF surface with it, and the whole suite stays green.

The fix is not another gate. NewAuth now returns a Signin{Auth, Admits}
and UseAuth takes it, so there is one value and no second argument to get
wrong. A source gate would have caught the mutation; this makes it
unsayable, which is better.

M62: main.go could build its own chi router, register POST /auth/begin on
it bare, and hand it to the mux — bypassing MountAuth, where the guard is
attached. Gate 17's walk follows receivers named r and mux and states that
limit honestly, so it did not see it. TestProductionMountsAuthThroughTheGuardedPath
is a source check that main still calls UseAuth and MountAuth. It buys one
thing: production cannot quietly stop being the thing gate 27 tested. It
does not prove the guard works; gate 27 does that.

Mutation-tested: deleting either call from main.go fails it. Reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3VKtXnq6fSmM2F11Bjcsy
Paul Campbell pushed by paul@keymail.dev 44230d60fea1b8d6d0a7c749868708a302f3808f parent abdab35
6 files changed, +73 −20
  • cmd/memoria/main.go +1 −1
  • internal/memoria/auth.go +25 −3
  • internal/memoria/handlers.go +12 −12
  • internal/memoriatest/admission_test.go +2 −2
  • internal/memoriatest/routes_test.go +30 −0
  • internal/memoriatest/signindial_test.go +3 −2
diff --git a/cmd/memoria/main.go b/cmd/memoria/main.go
index 90dc3ae..5001c06 100644
--- a/cmd/memoria/main.go
+++ b/cmd/memoria/main.go
@@ -69,7 +69,7 @@ func main() {
logger.Error("wire sign-in", "err", err)
os.Exit(1)
}
- app.UseAuth(a, memoria.Admits(cfg.Owners))
+ app.UseAuth(a)
app.MountAuth(mux, cfg.Origin)
opts.Mux = mux
diff --git a/internal/memoria/auth.go b/internal/memoria/auth.go
index bdd4a4a..6cbac81 100644
--- a/internal/memoria/auth.go
+++ b/internal/memoria/auth.go
@@ -163,18 +163,36 @@ func Admits(owners []string) func(string) bool {
}
}
+// Signin is auth and the admission predicate it was built with, as one
+// value.
+//
+// They travel together because they must be the same predicate: rastrillo
+// consults it at verification, and beginGuard consults it before the
+// classifier dials. A review mutated `main.go` to pass a different
+// predicate to UseAuth than the one NewAuth got — the guard admitted
+// everyone while verification still refused, so the SSRF surface reopened
+// and every test stayed green, because the tests build their own app and
+// nothing observed how production wired it.
+//
+// The fix is not another gate. It is having one value, so there is no
+// second argument to get wrong.
+type Signin struct {
+ Auth *auth.Auth
+ Admits func(string) bool
+}
+
// NewAuth wires the sign-in flow. Exactly one per process: auth carries
// rate-limiter state on the value, so one per request would get a fresh
// empty budget every time and rate-limit nothing, silently.
-func NewAuth(sqlDB *sql.DB, env AuthEnv, logger *slog.Logger, authorize func(string) bool) (*auth.Auth, error) {
+func NewAuth(sqlDB *sql.DB, env AuthEnv, logger *slog.Logger, authorize func(string) bool) (Signin, error) {
if authorize == nil {
// A nil Authorize admits every verified address, which on a
// public hostname means anyone who can receive mail. Refusing
// beats defaulting, because a caller that forgot to pass one has
// a bug either way and this is the version that says so.
- return nil, errors.New("sign-in: refusing to wire auth with no admission gate")
+ return Signin{}, errors.New("sign-in: refusing to wire auth with no admission gate")
}
- return auth.New(auth.Config{
+ a, err := auth.New(auth.Config{
DB: sqlDB,
Origin: env.Origin,
InstanceKey: env.InstanceKey,
@@ -185,4 +203,8 @@ func NewAuth(sqlDB *sql.DB, env AuthEnv, logger *slog.Logger, authorize func(str
Subject: "Your memoria sign-in link",
Logger: logger,
})
+ if err != nil {
+ return Signin{}, err
+ }
+ return Signin{Auth: a, Admits: authorize}, nil
}
diff --git a/internal/memoria/handlers.go b/internal/memoria/handlers.go
index da4f33c..95d7582 100644
--- a/internal/memoria/handlers.go
+++ b/internal/memoria/handlers.go
@@ -29,10 +29,9 @@ type app struct {
// admits is the same predicate auth.Config.Authorize gets, held here
// as well because beginGuard has to consult it BEFORE rastrillo does
- // — the whole point is to run it earlier than auth.Begin would.
- // Duplicating a reference is the cost of that; the two must be the
- // same function value, which UseAuth's signature enforces by taking
- // them together.
+ // — the whole point is to run it earlier than auth.Begin would. They
+ // must be the same function value, which is why UseAuth takes a
+ // Signin: one value, so there is no second argument to get wrong.
admits func(string) bool
// allowUnauthenticated is set only by AppForTest. Production cannot
@@ -45,14 +44,15 @@ type app struct {
// the router needs auth's handlers, but auth needs the admission gate
// that only App can supply — so the knot is untied by attaching late.
//
-// It takes the admission predicate alongside, rather than letting a
-// caller set it separately, because beginGuard fails closed on a nil one:
-// a two-call version would compile with the second call missing and the
-// sign-in route would then refuse everybody, loudly but pointlessly. One
-// call means the compiler asks for both.
-func (a *app) UseAuth(x *auth.Auth, admits func(string) bool) {
- a.auth = x
- a.admits = admits
+// It takes a Signin — auth and its predicate as one value — rather than
+// two arguments. A review mutated main.go to hand UseAuth a different
+// predicate from the one NewAuth got: the guard then admitted everyone
+// while verification still refused, reopening the dial, and every test
+// stayed green because the tests build their own app. One value makes
+// that unsayable.
+func (a *app) UseAuth(s Signin) {
+ a.auth = s.Auth
+ a.admits = s.Admits
}
// page is the data every template gets. Chunks and Signed are on it
diff --git a/internal/memoriatest/admission_test.go b/internal/memoriatest/admission_test.go
index 304100c..56c9c7a 100644
--- a/internal/memoriatest/admission_test.go
+++ b/internal/memoriatest/admission_test.go
@@ -60,7 +60,7 @@ func admissionApp(t *testing.T, dbPath string, owners []string) (http.Handler, *
t.Fatalf("App: %v", err)
}
mailer := &capturingMailer{}
- au, err := memoria.NewAuth(d.Writer(), memoria.AuthEnv{
+ signin, err := memoria.NewAuth(d.Writer(), memoria.AuthEnv{
Origin: testOrigin,
InstanceKey: "admission-gate-instance-key",
Owners: owners,
@@ -69,7 +69,7 @@ func admissionApp(t *testing.T, dbPath string, owners []string) (http.Handler, *
if err != nil {
t.Fatalf("NewAuth: %v", err)
}
- a.UseAuth(au, memoria.Admits(owners))
+ a.UseAuth(signin)
a.MountAuth(mux, testOrigin)
// No test may reach the network. The classifier resolves the submitted
diff --git a/internal/memoriatest/routes_test.go b/internal/memoriatest/routes_test.go
index b31fa7b..9e03b7b 100644
--- a/internal/memoriatest/routes_test.go
+++ b/internal/memoriatest/routes_test.go
@@ -211,3 +211,33 @@ func TestNoUndeclaredRoutes(t *testing.T) {
t.Error(p)
}
}
+
+// Production mounts auth through MountAuth, which is where the sign-in
+// guard is attached.
+//
+// An adversarial pass mutated `main.go` to build its own chi router,
+// register `POST /auth/begin` on it bare, and hand it to the mux — and
+// every test stayed green, because the tests build their own app and
+// nothing observed how production wires itself. Gate 17's route walk did
+// not catch it either: it follows receivers named r and mux, and states
+// that limit honestly.
+//
+// This is a source check and it buys one thing: main cannot quietly stop
+// going through MountAuth. It does not prove the guard works — gate 27
+// does that — only that production is the thing gate 27 tested.
+//
+// MUTATION-TESTED 2026-09-05: deleting either call from main.go fails
+// here. Reverted.
+func TestProductionMountsAuthThroughTheGuardedPath(t *testing.T) {
+ src, err := os.ReadFile(filepath.Join("..", "..", "cmd", "memoria", "main.go"))
+ if err != nil {
+ t.Fatalf("read main.go: %v", err)
+ }
+ for _, want := range []string{"UseAuth(", "MountAuth("} {
+ if !strings.Contains(string(src), want) {
+ t.Errorf("cmd/memoria/main.go does not call %s — auth is being wired "+
+ "some other way, and the sign-in guard is attached inside "+
+ "MountAuth", want)
+ }
+ }
+}
diff --git a/internal/memoriatest/signindial_test.go b/internal/memoriatest/signindial_test.go
index 807667c..d47021b 100644
--- a/internal/memoriatest/signindial_test.go
+++ b/internal/memoriatest/signindial_test.go
@@ -95,7 +95,7 @@ func signinAppWithPredicate(t *testing.T, admits func(string) bool) (http.Handle
t.Fatalf("App: %v", err)
}
owners := []string{dialTestOwner}
- au, err := memoria.NewAuth(d.Writer(), memoria.AuthEnv{
+ signin, err := memoria.NewAuth(d.Writer(), memoria.AuthEnv{
Origin: testOrigin,
InstanceKey: "signin-dial-gate-instance-key",
Owners: owners,
@@ -104,7 +104,8 @@ func signinAppWithPredicate(t *testing.T, admits func(string) bool) (http.Handle
if err != nil {
t.Fatalf("NewAuth: %v", err)
}
- a.UseAuth(au, admits)
+ signin.Admits = admits // only the guard's copy varies; see Signin
+ a.UseAuth(signin)
a.MountAuth(mux, testOrigin)
rec := &dialRecorder{}