oficina / home Public

§13 step 2: sign-in, the second factor, and recovery codes

The home can now sign a person in. Seven tasks from
docs/superpowers/plans/2026-09-03-home-signin.md, executed across 23
commits on step2/seal-and-secrets, and six adversarial review rounds.

## What is here

- internal/home/seal: values sealed at rest, ported from sheets-core
  unchanged apart from the HKDF info string, with vectors pinning the
  derivation.
- Four secrets loaded in one place, refusing rather than degrading,
  including all-zero keys — the canonical placeholder, well-formed
  enough to pass every other check.
- The schema: people (person ref, blind index, created_at — no address
  column) and signin_links (sealed address, no redeemed_at, taken by a
  single DELETE ... RETURNING so unknown, expired and already-used are
  one answer by construction).
- The person ref: opaque, minted, never derived from an address.
- Sign-in through keymaildev/signin, with the Keymail upgrade, driven
  end to end against a federation stub.
- The magic link redeemed on POST, never GET: a mail scanner's prefetch
  must not burn it, and an <img src=".../verify?token=…"> must not sign
  a victim into an attacker's account.
- Passkeys as an optional second factor, gated on BOTH sign-in paths,
  with ten single-use recovery codes redeemed by a plain form POST that
  needs no script — because recovery is exactly the moment WebAuthn is
  not working.
- Scriptless screens with ruling 21's carve-out named by template AND by
  src, the RP-ID guard, and the vendored stylesheets.
- Gates 1 and 2, and every gate on this branch watched failing on the
  thing it guards.

## What review caught that would otherwise have shipped

- A boot path that could not have started on the platform at all: it
  required a --db flag that carlos-app@.service never passes.
- The second factor removable from the keymail door by a one-line
  spelling change, because its only guard was a text grep.
- A magic-link expiry predicate with no test that could fail — the
  expired row was deleted by its own PutLink sweep before TakeLink saw
  it.
- A schema guard whose "sealed" exemption let unsealed_address through.
- A scriptless sweep blind to any <script> inside an {{if}} its fixture
  did not satisfy, and to </script > with a space.
- An oracle test that normalised away the difference it was looking for.
- An all-zero key accepted by every check.
- A stolen session cookie able to enrol its own passkey and, with no
  operator reset by design, lock the owner out permanently.
- A freshness window that bounced people to re-authenticate at the exact
  moment they held a passkey and zero recovery codes.

## Two things filed for Paul, not decided here

- §3.1 says the blind index "uses an expensive KDF rather than a bare
  HMAC"; ruling 3 says HMAC, and the code implements the ruling — so
  §3.1 is currently false about the code. Filed as a marked OPEN note in
  the spec with both sides argued.
- Enrolment now requires a fresh session. §5.2 says a passkey is
  "optional per person" and says nothing about how recently you must
  have proved yourself to add one; I took the safer side because the
  failure it prevents is unrecoverable.

## Not deployable yet, and deliberately so

cmd/home refuses to start without HOME_DEK, HOME_PEPPER,
HOME_SIGNIN_KEY, CARLOS_SMTP_PASS and the SMTP host and from address.
None is provisioned. Nothing in .amadan/ci.d deploys on merge, so this
is safe to land; deploying is what needs the secrets first, and the unit
is Restart=always, so deploying without them would 502 the site until a
rollback rather than leaving the old build serving.

Reviewed by Fable (rounds 1-2) and Opus (rounds 3-6, after Fable hit a
session rate limit); mutation testing throughout, which is what found
most of the above. Second opinion from gpt-6-astra via codex on the
step-3 signing decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev bb953990e05c12da4c5acb2a4bbb8c3625eb5cbb parent c1784e8
52 files changed, +7513 −78
  • .amadan/ci.d/50-race +8 −0
  • AGENTS.md +9 −5
  • Makefile +19 −8
  • cmd/home/main.go +80 −22
  • docs/superpowers/plans/2026-09-03-home-signin.md +2 −1
  • docs/superpowers/plans/2026-09-07-home-assertion.md +355 −0
  • docs/superpowers/specs/2026-08-30-home-design.md +21 −0
  • go.mod +9 −2
  • go.sum +30 −8
  • internal/guardstest/logtest_containment_test.go +22 −11
  • internal/guardstest/noaddress_test.go +164 −0
  • internal/guardstest/repo_test.go +39 −0
  • internal/home/app.go +169 −18
  • internal/home/app_test.go +2 −2
  • internal/home/db.go +64 −0
  • internal/home/db_test.go +170 −0
  • internal/home/keymail_test.go +195 −0
  • internal/home/keymailstub_test.go +185 −0
  • internal/home/keys.go +197 −0
  • internal/home/keys_test.go +215 −0
  • internal/home/links.go +163 −0
  • internal/home/links_test.go +327 −0
  • internal/home/logs_test.go +24 −1
  • internal/home/migrations/0001_people.sql +13 −0
  • internal/home/migrations/0002_signin_links.sql +24 −0
  • internal/home/models.go +39 −0
  • internal/home/noscript_test.go +449 −0
  • internal/home/passkey.go +231 −0
  • internal/home/passkey_test.go +412 −0
  • internal/home/personref.go +74 −0
  • internal/home/personref_test.go +80 −0
  • internal/home/render.go +93 −0
  • internal/home/rpid_test.go +89 −0
  • internal/home/seal/seal.go +106 −0
  • internal/home/seal/seal_test.go +53 −0
  • internal/home/seal/vectors_test.go +79 −0
  • internal/home/sealtest/sealtest.go +107 −0
  • internal/home/sealtest/sealtest_test.go +136 −0
  • internal/home/signin.go +385 −0
  • internal/home/signin_test.go +535 −0
  • internal/home/static/passkey.js +83 −0
  • internal/home/static/theme.css +174 −0
  • internal/home/static/tokens.css +1550 −0
  • internal/home/templates/check_your_mail.html +6 −0
  • internal/home/templates/layout.html +17 −0
  • internal/home/templates/passkey_confirm.html +20 −0
  • internal/home/templates/passkey_enrol.html +11 −0
  • internal/home/templates/recovery_codes.html +17 −0
  • internal/home/templates/signin.html +10 −0
  • internal/home/templates/signin_confirm.html +9 −0
  • internal/home/templates/signin_failed.html +6 −0
  • internal/home/testapp_test.go +236 −0

CI passed — run details

.amadan/ci.d/50-race

Loading diff…

AGENTS.md

Loading diff…

Makefile

Loading diff…

cmd/home/main.go

Loading diff…

docs/superpowers/plans/2026-09-03-home-signin.md

Loading diff…

docs/superpowers/plans/2026-09-07-home-assertion.md

Loading diff…

docs/superpowers/specs/2026-08-30-home-design.md

Loading diff…

go.mod

Loading diff…

go.sum

Loading diff…

internal/guardstest/logtest_containment_test.go

Loading diff…

internal/guardstest/noaddress_test.go

Loading diff…

internal/guardstest/repo_test.go

Loading diff…

internal/home/app.go

Loading diff…

internal/home/app_test.go

Loading diff…

internal/home/db.go

Loading diff…

internal/home/db_test.go

Loading diff…

internal/home/keymail_test.go

Loading diff…

internal/home/keymailstub_test.go

Loading diff…

internal/home/keys.go

Loading diff…

internal/home/keys_test.go

Loading diff…

internal/home/links.go

Loading diff…

internal/home/links_test.go

Loading diff…

internal/home/logs_test.go

Loading diff…

internal/home/migrations/0001_people.sql

Loading diff…

internal/home/migrations/0002_signin_links.sql

Loading diff…

internal/home/models.go

Loading diff…

internal/home/noscript_test.go

Loading diff…

internal/home/passkey.go

Loading diff…

internal/home/passkey_test.go

Loading diff…

internal/home/personref.go

Loading diff…

internal/home/personref_test.go

Loading diff…

internal/home/render.go

Loading diff…

internal/home/rpid_test.go

Loading diff…

internal/home/seal/seal.go

Loading diff…

internal/home/seal/seal_test.go

Loading diff…

internal/home/seal/vectors_test.go

Loading diff…

internal/home/sealtest/sealtest.go

Loading diff…

internal/home/sealtest/sealtest_test.go

Loading diff…

internal/home/signin.go

Loading diff…

internal/home/signin_test.go

Loading diff…

internal/home/static/passkey.js

Loading diff…

internal/home/static/theme.css

Loading diff…

internal/home/static/tokens.css

Loading diff…

internal/home/templates/check_your_mail.html

Loading diff…

internal/home/templates/layout.html

Loading diff…

internal/home/templates/passkey_confirm.html

Loading diff…

internal/home/templates/passkey_enrol.html

Loading diff…

internal/home/templates/recovery_codes.html

Loading diff…

internal/home/templates/signin.html

Loading diff…

internal/home/templates/signin_confirm.html

Loading diff…

internal/home/templates/signin_failed.html

Loading diff…

internal/home/testapp_test.go

Loading diff…