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.
🔴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.
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)
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.
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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 bothsides.
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_unverifiedandoffering a code entry screen) is a separate MR on each platform.
mentioned in commit flamenet-messenger-android@1ad4719337a3d46961e047fe508063dfeb1805d2
mentioned in merge request flamenet-messenger-android!10
mentioned in commit flamenet-messenger-ios@5bdc4185385dfe922410ac9ace2dc4fc304cd50b
mentioned in merge request flamenet-messenger-ios!46
mentioned in merge request !25
🔴 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 isconfig.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 getemail_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:
email_verified_atfor every existing account, so nobody who already signed up is affected;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.
marked this merge request as draft
added 1 commit
Compare with previous version
Switched off by default, so this is safe to merge and inert on deploy.
Added
FN_EMAIL_VERIFICATION_REQUIRED, off unless explicitly set totrue,1oryes— a typo or an empty value leaves it off. Enforcement now needs that flag and SMTP, read off a singleemailVerificationActivepredicate 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.mdunder "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.
marked this merge request as ready
mentioned in commit flamenet-messenger-android@90850aa654e5e2304e3b73f5d1eaf53bb483b74e
mentioned in commit flamenet-messenger-ios@557db092e37e221b40afdec975bd04d756406a8b
mentioned in commit flamenet-messenger-ios@d91e1feb1358be10f4a58b8df098fbb09609683a
added 29 commits
main428c31ce- Prove an address before an account can use the relaya9bb2160- Switch address verification off by defaultCompare with previous version
mentioned in commit
28fe68dc82mentioned in commit flamenet-messenger-ios@27c5bde2f370729c8d419ba05b16161a984bc448
mentioned in merge request flamenet-messenger-ios!89
mentioned in commit flamenet-messenger-android@1d834f67fc30a3f5c41334401023153b49ef92ec
changed the description