Prove an address before an account can use the relay #76

Merged
paulhitt merged 2 commits from feat/email-verification into main 2026-09-03 00:13:14 +00:00
paulhitt commented 2026-09-02 00:58:21 +00:00 (Migrated from gitlab.com)

Closes part of #40. Addresses were collected, used for password reset, and never
verified
— so an account could be created against an address nobody owns, and the reset
path could be aimed at one too.

The gate could not go where the ticket said

"Verify before you can send" is unimplementable. A sealed send is authorised by a
delivery key and records no sender (§14) — the relay does not know whose account to
check. Gating the send path would either break sealed sending or catch only the unsealed
minority, which is the wrong half.

The token route is the right chokepoint: authenticated, names the account, and everything
downstream needs one. An account that cannot mint a token cannot poll, register a device or
fetch a bundle. So this is verify before you can use the relay — enforceable, and
honest about what it does.

Two properties that keep it safe

Inert on a relay that cannot send mail. A self-hoster with no SMTP can never prove
anything; refusing them entry to their own server would be the worst possible reading of a
security feature. Same shape as enforceKeyLogIfServed. Two tests hold that line from both
sides.

The migration grandfathers every existing account. They signed up before there was
anything to prove — leaving them unproved would lock out every current user the moment this
went live.

Accounts authenticated by the shared service have no email here and are never gated.
Codes are spent on use, expire in a day, and are compared with the existing
constant-time helper. Sending cannot fail a sign-up that already succeeded.

Tests: 9 new, proven able to fail — disabling the gate turns 6 red. 479 tests in 41
suites pass, up from 470.

Not deployed, and SMTP is not configured on production, so every part of this is
currently inert on the live relay by design. Client work (rendering email_unverified and
offering a code entry screen) is a separate MR on each platform.

Closes part of #40. Addresses were collected, used for password reset, and **never verified** — so an account could be created against an address nobody owns, and the reset path could be aimed at one too. ## The gate could not go where the ticket said **"Verify before you can send" is unimplementable.** A sealed send is authorised by a delivery key and records **no sender** (§14) — the relay does not know whose account to check. Gating the send path would either break sealed sending or catch only the unsealed minority, which is the wrong half. The token route is the right chokepoint: authenticated, names the account, and everything downstream needs one. An account that cannot mint a token cannot poll, register a device or fetch a bundle. So this is **verify before you can use the relay** — enforceable, and honest about what it does. ## Two properties that keep it safe **Inert on a relay that cannot send mail.** A self-hoster with no SMTP can never prove anything; refusing them entry to their own server would be the worst possible reading of a security feature. Same shape as `enforceKeyLogIfServed`. Two tests hold that line from both sides. **The migration grandfathers every existing account.** They signed up before there was anything to prove — leaving them unproved would lock out every current user the moment this went live. Accounts authenticated by the shared service have no email here and are never gated. Codes are spent on use, expire in a day, and are compared with the existing constant-time helper. Sending cannot fail a sign-up that already succeeded. **Tests:** 9 new, proven able to fail — disabling the gate turns 6 red. 479 tests in 41 suites pass, up from 470. **Not deployed, and SMTP is not configured on production**, so every part of this is currently inert on the live relay by design. Client work (rendering `email_unverified` and offering a code entry screen) is a separate MR on each platform.
paulhitt commented 2026-09-02 01:06:25 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-android@1ad4719337a3d46961e047fe508063dfeb1805d2

mentioned in commit flamenet-messenger-android@1ad4719337a3d46961e047fe508063dfeb1805d2
paulhitt commented 2026-09-02 01:06:44 +00:00 (Migrated from gitlab.com)

mentioned in merge request flamenet-messenger-android!10

mentioned in merge request flamenet-messenger-android!10
paulhitt commented 2026-09-02 01:07:00 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-ios@5bdc4185385dfe922410ac9ace2dc4fc304cd50b

mentioned in commit flamenet-messenger-ios@5bdc4185385dfe922410ac9ace2dc4fc304cd50b
paulhitt commented 2026-09-02 01:07:12 +00:00 (Migrated from gitlab.com)

mentioned in merge request flamenet-messenger-ios!46

mentioned in merge request flamenet-messenger-ios!46
paulhitt commented 2026-09-02 14:56:42 +00:00 (Migrated from gitlab.com)

mentioned in merge request !25

mentioned in merge request !25
paulhitt commented 2026-09-02 19:11:49 +00:00 (Migrated from gitlab.com)

🔴 Hold: this would lock out new sign-ups from the Android app that is already in the field.

The shipped Android release is v8 (versionName 0.8), published 2026-08-31. It has no screen for entering a verification code — that work is android!10, still open and therefore not in anybody's hands.

The gate here is canSendMail, which is config.smtp != nil. Production has SMTP configured, so on a real deploy the gate is live, not inert. A new account created from v8 would mint no relay token and get email_unverified (403) with nowhere to type the code. It could not poll, register a device, or fetch a bundle — a dead account with no way forward.

Two things are already right and worth keeping:

  • the migration backfills email_verified_at for every existing account, so nobody who already signed up is affected;
  • accounts with no email, and enclave pseudonyms, are correctly skipped.

The exposure is new sign-ups only, and only from a client that cannot answer.

Order this has to go in: android!10 and ios!45/!46 ship to users first, then this merges. Marking it draft so it cannot merge ahead of them by accident. Nothing here needs changing — the code is fine, it is the sequence that matters.

🔴 **Hold: this would lock out new sign-ups from the Android app that is already in the field.** The shipped Android release is **v8 (versionName 0.8)**, published 2026-08-31. It has no screen for entering a verification code — that work is android!10, still open and therefore not in anybody's hands. The gate here is `canSendMail`, which is `config.smtp != nil`. Production has SMTP configured, so on a real deploy the gate is **live, not inert**. A new account created from v8 would mint no relay token and get `email_unverified` (403) with nowhere to type the code. It could not poll, register a device, or fetch a bundle — a dead account with no way forward. Two things are already right and worth keeping: - the migration backfills `email_verified_at` for every existing account, so nobody who already signed up is affected; - accounts with no email, and enclave pseudonyms, are correctly skipped. The exposure is **new sign-ups only**, and only from a client that cannot answer. **Order this has to go in:** android!10 and ios!45/!46 ship to users first, then this merges. Marking it draft so it cannot merge ahead of them by accident. Nothing here needs changing — the code is fine, it is the sequence that matters.
paulhitt commented 2026-09-02 19:11:50 +00:00 (Migrated from gitlab.com)

marked this merge request as draft

marked this merge request as **draft**
paulhitt commented 2026-09-02 19:18:48 +00:00 (Migrated from gitlab.com)

added 1 commit

  • fe0497f9 - Switch address verification off by default

Compare with previous version

added 1 commit <ul><li>fe0497f9 - Switch address verification off by default</li></ul> [Compare with previous version](/paulhitt/flamenet-relay/-/merge_requests/13/diffs?diff_id=2000676847&start_sha=69f16d9cf3014ebfa0b5011ae31ef24a93a3d0fb)
paulhitt commented 2026-09-02 19:27:08 +00:00 (Migrated from gitlab.com)

Switched off by default, so this is safe to merge and inert on deploy.

Added FN_EMAIL_VERIFICATION_REQUIRED, off unless explicitly set to true, 1 or yes — a typo or an empty value leaves it off. Enforcement now needs that flag and SMTP, read off a single emailVerificationActive predicate so the issuing path and the gate cannot disagree about whether it is on.

With it off, no code is mailed either. The message tells somebody their account cannot be used until they enter the code, and while the gate is off that is untrue — mailing it would send a working account a demand it does not have to meet.

So on a production deploy today: nothing changes. v8 signs up and gets its token exactly as it does now. The hold I put on this yesterday is lifted — the sequence no longer matters, because turning it on is now a deliberate act rather than a consequence of deploying.

Documented in docs/CONFIGURATION.md under "Proving an address at sign-up", including the warning to check what clients are in the field before switching it on.

Four tests added: default-off lets an unverified account through on a mail-capable relay; the gate needs both halves (all three failing combinations); the predicate is the conjunction and not either half; and only an explicit yes turns it on. Full suite 483 pass.

**Switched off by default, so this is safe to merge and inert on deploy.** Added `FN_EMAIL_VERIFICATION_REQUIRED`, off unless explicitly set to `true`, `1` or `yes` — a typo or an empty value leaves it off. Enforcement now needs that flag **and** SMTP, read off a single `emailVerificationActive` predicate so the issuing path and the gate cannot disagree about whether it is on. With it off, no code is mailed either. The message tells somebody their account cannot be used until they enter the code, and while the gate is off that is untrue — mailing it would send a working account a demand it does not have to meet. So on a production deploy today: nothing changes. v8 signs up and gets its token exactly as it does now. The hold I put on this yesterday is lifted — the sequence no longer matters, because turning it on is now a deliberate act rather than a consequence of deploying. Documented in `docs/CONFIGURATION.md` under "Proving an address at sign-up", including the warning to check what clients are in the field before switching it on. Four tests added: default-off lets an unverified account through on a mail-capable relay; the gate needs both halves (all three failing combinations); the predicate is the conjunction and not either half; and only an explicit yes turns it on. Full suite 483 pass.
paulhitt commented 2026-09-02 19:27:08 +00:00 (Migrated from gitlab.com)

marked this merge request as ready

marked this merge request as **ready**
paulhitt commented 2026-09-02 19:47:09 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-android@90850aa654e5e2304e3b73f5d1eaf53bb483b74e

mentioned in commit flamenet-messenger-android@90850aa654e5e2304e3b73f5d1eaf53bb483b74e
paulhitt commented 2026-09-02 20:10:20 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-ios@557db092e37e221b40afdec975bd04d756406a8b

mentioned in commit flamenet-messenger-ios@557db092e37e221b40afdec975bd04d756406a8b
paulhitt commented 2026-09-02 21:08:59 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-ios@d91e1feb1358be10f4a58b8df098fbb09609683a

mentioned in commit flamenet-messenger-ios@d91e1feb1358be10f4a58b8df098fbb09609683a
paulhitt commented 2026-09-03 00:13:02 +00:00 (Migrated from gitlab.com)

added 29 commits

  • fe0497f9...f1f2618b - 27 commits from branch main
  • 428c31ce - Prove an address before an account can use the relay
  • a9bb2160 - Switch address verification off by default

Compare with previous version

added 29 commits <ul><li>fe0497f9...f1f2618b - 27 commits from branch <code>main</code></li><li>428c31ce - Prove an address before an account can use the relay</li><li>a9bb2160 - Switch address verification off by default</li></ul> [Compare with previous version](/paulhitt/flamenet-relay/-/merge_requests/13/diffs?diff_id=2001153908&start_sha=fe0497f9c7dc6a6aabc112933261c005b7e4bfe8)
paulhitt commented 2026-09-03 00:13:15 +00:00 (Migrated from gitlab.com)

mentioned in commit 28fe68dc82

mentioned in commit 28fe68dc82f5915d493a25ac74da6d42d1c6bdd1
paulhitt (Migrated from gitlab.com) merged commit 28fe68dc82 into main 2026-09-03 00:13:15 +00:00
paulhitt commented 2026-09-03 19:38:21 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-ios@27c5bde2f370729c8d419ba05b16161a984bc448

mentioned in commit flamenet-messenger-ios@27c5bde2f370729c8d419ba05b16161a984bc448
paulhitt commented 2026-09-03 19:38:22 +00:00 (Migrated from gitlab.com)

mentioned in merge request flamenet-messenger-ios!89

mentioned in merge request flamenet-messenger-ios!89
paulhitt commented 2026-09-03 23:23:55 +00:00 (Migrated from gitlab.com)

mentioned in commit flamenet-messenger-android@1d834f67fc30a3f5c41334401023153b49ef92ec

mentioned in commit flamenet-messenger-android@1d834f67fc30a3f5c41334401023153b49ef92ec
paulhitt commented 2026-09-09 18:59:21 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
Sign in to join this conversation.