sirius-press/docs/accounts.md

179 lines
6.9 KiB
Markdown
Raw Normal View History

feat(sirius-press): a WordPress where the account is a key, not a mailbox WordPress makes two assumptions this project cannot accept: that identity comes from an email address, and that a site lives at one server. Both are things somebody else can take away — a mailbox is rented from a provider who can close it or be compelled to open it, and a server is one seizure from being gone. Sirius Press replaces the first and hedges the second. Signing in means signing a challenge with the key that controls a CashAddress. The address is recovered from the signature, so nothing is typed but the signature itself, and the result is an ordinary WordPress session cookie — roles, capabilities, nonces and the REST API never learn the login was different. Three ways to produce one: a wallet the browser already exposes, a phrase used once in the page and wiped, or a signature pasted in from any BIP-137 wallet, which needs no JavaScript and lets the key stay on a machine that never touches the web. There is no password reset, and the recovery page says so plainly rather than offering a form that cannot work. A reset mechanism is by construction a way to take an account from its owner, and it is always easier to attack than the cryptography it bypasses. Publishing a post also exports it as static HTML to the name's storage on Sia, signed by the key that owns the name, so the site keeps answering when the server does not. Email as a feature is untouched. wp_mail() still works, SMTP still sends, and contact forms still deliver to addresses real people typed. Only mail to the site's own unroutable placeholder addresses is diverted to an in-app inbox. The objection was to email as identity, not to email. Core is pinned and patched rather than vendored. WordPress 7.1.1 is 149 MB and 5,008 files; the fork's entire core diff is 75 lines in wp-admin/install.php. Carrying the former to express the latter would bury the patch where nobody reviews it and make every clone of the monorepo pay for it. Upstream releases still merge through tools/update-wordpress.sh, which reapplies the series and says exactly which hunk needs a human. The cryptography is implemented twice — PHP on the server, JavaScript in the page — because the server must verify and the browser must sign. Both are pinned against libauth, the library the Sirius portal wallet and the BNS gateway already use, so a disagreement of one byte fails the test suite rather than presenting as a rejected login at three in the morning. 132 checks, no framework, about a second.
2026-09-21 01:39:38 +02:00
# Accounts, sign-in and recovery
## What an account is
A CashAddress, and a WordPress user row attached to it.
The address lives in user meta, not in a new column on `wp_users`. That is a
deliberate departure from the original design sketch, which proposed replacing
`user_email` with a `wallet_addr` column: a schema change to a core table makes
every future upstream merge and every `dbDelta()` run a negotiation, and it
buys nothing here. Meta lookups are indexed, and a plugin reading
`$user->user_email` keeps getting a string instead of crashing.
Everything else about the user is ordinary WordPress. Roles, capabilities,
nonces, sessions, the REST API — none of it knows the login was different.
---
## Signing in
The login page issues a challenge: a few lines of readable text containing the
site, the purpose, a nonce and a timestamp. You sign it; the server recovers
the public key from the signature, derives the address, and finds the account.
You never type your address. It comes out of the signature.
### The three ways to sign
**A wallet the browser already has.** Theseus exposes `window.bitcoincash` on
`.x` origins. The button appears when it is there. The key stays in the
browser's wallet and never touches the page.
**A phrase typed into the form.** Derived in the page, used once, wiped. The
phrase is never transmitted — only the signature is. This is the path most
people will use, and it is the one worth being clear-eyed about: you are typing
a recovery phrase into a web page. It is the same trust you place in the Sirius
portal, and less than you place in a browser extension, but it is not nothing.
**A signature made somewhere else.** The challenge is a BIP-137 message — the
format Electron Cash's "Sign message" box has produced for a decade. Copy the
text out, sign it on a machine that never touches this site, paste the result
back. **This path needs no JavaScript at all**, which is why the paste box is
in the markup from the start and the buttons are revealed by script rather than
the other way round.
### The challenge
```
Sign in to Example Site
Site: https://example.bch/
Purpose: sign in
Nonce: 1758412800000.3f9ab2c1d4e5f607.9a1b…
Issued: 2026-09-21T10:00:00Z
Signing this proves you control this wallet. It moves no coins.
```
Written to be readable in a wallet's approval dialog, because that dialog is
the only place a user gets to check what they are agreeing to. Anything a
wallet renders as a wall of hex is a security control the user cannot exercise.
Issuing one writes nothing: the nonce carries its own timestamp and an HMAC
under the site's salts, so a forged nonce fails arithmetic rather than a
database lookup. Only a *successful* verification writes — a short-lived marker
that burns the nonce so the signature cannot be replayed.
The purpose is inside the signed bytes, which is what stops a login signature
being replayed to create an account.
---
## Registration
One step. Sign the challenge, optionally pick a username, done — there is no
confirmation link because the signature *is* the confirmation.
No pending state, no expiring link, no way for a typo to strand an account.
Turn it on or off at **Sirius Press → Sign-in**.
---
## Recovery, and the absence of it
**If you lose your phrase, the account is gone.** Nobody can reset it, and
nothing in the software is trying to.
This is not an oversight to be worked around later. A reset mechanism is, by
construction, a way to take an account from its owner — and every such
mechanism ends up being the thing attacked, because it is easier than the
cryptography it bypasses. Password reset by email is exactly that: a promise
that whoever controls a mailbox controls the account.
The fork's "lost password" page says this to your users in those terms, and
offers the two things that genuinely help: if they still have a phrase, they
are not locked out at all and should just sign in; and if they have several
phrases and do not know which is which, the page derives the address from a
phrase in the browser so they can check.
### The one route back
An administrator can point an account at a different address from **Users →
Edit**.
That is intentionally a human decision about a person somebody recognises,
rather than a capability an attacker triggers. It also means an administrator
can take any account on the site, which is true of WordPress administrators
generally and is worth knowing rather than discovering.
If nobody has an administrator account any more, nothing in the software can
help — but the database can. Attach a wallet to user 1 directly:
```sql
INSERT INTO wp_usermeta (user_id, meta_key, meta_value)
VALUES (1, 'sirius_wallet_address', 'bitcoincash:qq…')
ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value);
```
Or with WP-CLI, which handles the prefix for you:
```bash
wp user meta update 1 sirius_wallet_address 'bitcoincash:qq…'
```
---
## Passwords
Left on by default, and the reason is unglamorous: a fresh install has exactly
one account, created by the installer, with no wallet attached yet. Shipping
with passwords off would mean the first thing a new site does is lock out its
own administrator.
Turn them off at **Sirius Press → Sign-in** once every account that needs
access has a wallet. That screen refuses to do it until *your* account has one,
and lists every account that would be stranded.
With passwords off, core's password checks are removed from the `authenticate`
chain entirely rather than left to run and fail — a site that has turned
passwords off should not still have a password oracle on its login page.
Application passwords and WP-CLI are unaffected either way. They do not go
through the interactive login path.
---
## Asking for a fresh signature
A capability check proves what a session is allowed to do. It cannot prove the
person holding the key is still at the keyboard. For something irreversible —
transferring a name, moving money, deleting a site — a plugin can demand a new
signature:
```php
$challenge = sirius_press_confirmation_challenge();
// Render $challenge['message'] for the user to sign, then POST
// { nonce, signature } to /wp-json/sirius-press/v1/confirm
```
It returns 403 if the signature belongs to a different wallet than the one
signed in, so a caller that forgets to check the response body still fails
closed.
---
## The REST surface
| Endpoint | Method | Auth | Does |
|---|---|---|---|
| `/sirius-press/v1/challenge` | GET | none | Hands out something to sign. `?purpose=login\|register\|link\|confirm` |
| `/sirius-press/v1/login` | POST | none | `{nonce, signature}` → sets a session cookie |
| `/sirius-press/v1/register` | POST | none | `{nonce, signature, user_login?}` → creates an account |
| `/sirius-press/v1/confirm` | POST | logged in | `{nonce, signature}` → re-proves the current user's key |
Everything that does curve maths is rate limited per client, because signature
recovery is the most expensive thing a stranger can make the server do — a few
hundred milliseconds of CPU each on a BCMath host.