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
< ? php
/**
* Signing in .
*
* The whole substitution happens in one filter . WordPress asks its
* `authenticate` chain " who is this? " , and this plugin answers first : if the
* request carries a wallet signature over a challenge this site issued , the
* signer ' s address identifies the account , and a `WP_User` comes back . Core
* then sets its ordinary auth cookie , and from that point on every
* capability , role check , nonce and REST permission works exactly as it does
* on stock WordPress . Nothing downstream knows the login was different .
*
* The form degrades honestly . With JavaScript , a button signs the challenge
* in the page or hands it to the browser ' s own wallet . Without it , the
* challenge is printed in a box next to a field for the signature — which is
* precisely the " Sign message " workflow every desktop BCH wallet already has .
* That path is not a courtesy to the JavaScript - averse ; it is the path for
* someone whose keys live on a machine that never touches this site .
*
* @ package SiriusPress
*/
defined ( 'ABSPATH' ) || exit ;
final class SPA_Login {
const OPT_ALLOW_PASSWORDS = 'sirius_press_allow_password_login' ;
public static function hooks () {
add_filter ( 'authenticate' , array ( __CLASS__ , 'authenticate' ), 5 , 3 );
add_action ( 'login_enqueue_scripts' , array ( __CLASS__ , 'enqueue' ) );
add_action ( 'login_form' , array ( __CLASS__ , 'render_form' ) );
add_filter ( 'login_message' , array ( __CLASS__ , 'login_message' ) );
add_action ( 'login_footer' , array ( __CLASS__ , 'footer_config' ) );
if ( ! self :: passwords_allowed () ) {
// Pull core's password checks out of the chain entirely rather
// than letting them run and fail: a site that has turned
// passwords off should not have a password oracle on its login
// page at all.
remove_filter ( 'authenticate' , 'wp_authenticate_username_password' , 20 );
remove_filter ( 'authenticate' , 'wp_authenticate_email_password' , 20 );
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
add_action ( 'login_head' , array ( __CLASS__ , 'hide_credential_fields' ) );
add_filter ( 'wp_login_errors' , array ( __CLASS__ , 'quiet_empty_form_error' ) );
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
}
}
/** Whether username+password sign-in is still accepted. */
public static function passwords_allowed () {
return ( bool ) get_option ( self :: OPT_ALLOW_PASSWORDS , true );
}
/**
* The substitution itself .
*
* @ param null | WP_User | WP_Error $user
* @ param string $username
* @ param string $password
* @ return null | WP_User | WP_Error
*/
public static function authenticate ( $user , $username , $password ) {
if ( $user instanceof WP_User ) {
return $user ;
}
// Nonce checking is not applicable here: the signed challenge *is* the
// anti-forgery token, and it is stronger than one — it is bound to a
// key, single-use, and expires.
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( empty ( $_POST [ 'sirius_signature' ] ) || empty ( $_POST [ 'sirius_nonce' ] ) ) {
return $user ;
}
$signature = sanitize_text_field ( wp_unslash ( $_POST [ 'sirius_signature' ] ) );
$nonce = sanitize_text_field ( wp_unslash ( $_POST [ 'sirius_nonce' ] ) );
fix(sirius-press): recovering a key is not the same as verifying a signature
Running the fork against a live WordPress found a real hole in registration,
and it is the kind that only shows up when you actually try it.
ECDSA public-key recovery always succeeds. Given any well-formed signature
and any digest it returns a key — just not the signer's, unless the digest is
the one that was signed. The auth flow leaned on that as if a wrong message
would fail. It does not; it quietly yields a stranger's address.
At sign-in this was harmless, because the wrong address matches no account
and the attempt fails. Registration and wallet-linking were another matter:
both took the recovered address and bound it to an account, so a signature
over slightly different text — a challenge copied without its blank line, a
wallet that rewrote the text, a login signature replayed at the registration
form — created an account keyed to an address nobody could sign for. The
person would see "success" and discover the truth the next time they tried to
get in. Wallet-linking was worse still: it would move an existing account onto
a dead address and lock its owner out of their own site.
Both paths now require the address the signer claims and compare it to the
recovered one, which is what verification actually means. Sign-in accepts the
claim when the page sends it and uses it to turn "no account uses that wallet"
into the more useful "that signature is not over the text we asked for".
Also from running it:
URL rewriting mangled every link on a site whose URL carries a port. The
protocol-relative pass matched inside absolute URLs and gave each one a second
scheme, and matching the host without its port left the port stranded as
`//host:8760:8760/`. Local and staging installs would have exported a site of
broken links.
Plain permalinks silently collapse an entire site onto one exported file,
because every post's URL is `/?p=N` and its path is `/`. The queue looks
healthy the whole time. The Publishing screen now says so.
Translations loaded on `plugins_loaded`, which WordPress 6.7 warns about on
every request — the kind of noise that trains people to stop reading logs.
And one deletion: an `is_email()` filter written on the assumption that
WordPress rejects `.invalid` addresses. It does not — `is_email()` validates
syntax, not whether a domain could exist — so the filter never fired. A filter
that appears to relax a rule but does not is worse than no filter, because
someone later reasons from it. The documentation made the same claim and has
been corrected.
Verification added rather than asserted: tests/live.mjs drives a real instance
over HTTP (40 checks), and tests/mock-gateway.mjs answers uploads with the
signature check transcribed from the gateway's own source, so the publishing
path can be exercised without a registered name.
2026-09-21 02:35:32 +02:00
// Optional at sign-in: the recovered address is looked up against real
// accounts, so a wrong one simply finds nothing. When the page does
// send it, checking it turns "no account uses that wallet" into the
// more useful "that signature is not over the text we asked for".
$claimed = isset ( $_POST [ 'sirius_address' ] ) ? sanitize_text_field ( wp_unslash ( $_POST [ 'sirius_address' ] ) ) : '' ;
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
// phpcs:enable WordPress.Security.NonceVerification.Missing
$limited = SPA_Challenge :: check_rate_limit ( 'login' );
if ( is_wp_error ( $limited ) ) {
return $limited ;
}
fix(sirius-press): recovering a key is not the same as verifying a signature
Running the fork against a live WordPress found a real hole in registration,
and it is the kind that only shows up when you actually try it.
ECDSA public-key recovery always succeeds. Given any well-formed signature
and any digest it returns a key — just not the signer's, unless the digest is
the one that was signed. The auth flow leaned on that as if a wrong message
would fail. It does not; it quietly yields a stranger's address.
At sign-in this was harmless, because the wrong address matches no account
and the attempt fails. Registration and wallet-linking were another matter:
both took the recovered address and bound it to an account, so a signature
over slightly different text — a challenge copied without its blank line, a
wallet that rewrote the text, a login signature replayed at the registration
form — created an account keyed to an address nobody could sign for. The
person would see "success" and discover the truth the next time they tried to
get in. Wallet-linking was worse still: it would move an existing account onto
a dead address and lock its owner out of their own site.
Both paths now require the address the signer claims and compare it to the
recovered one, which is what verification actually means. Sign-in accepts the
claim when the page sends it and uses it to turn "no account uses that wallet"
into the more useful "that signature is not over the text we asked for".
Also from running it:
URL rewriting mangled every link on a site whose URL carries a port. The
protocol-relative pass matched inside absolute URLs and gave each one a second
scheme, and matching the host without its port left the port stranded as
`//host:8760:8760/`. Local and staging installs would have exported a site of
broken links.
Plain permalinks silently collapse an entire site onto one exported file,
because every post's URL is `/?p=N` and its path is `/`. The queue looks
healthy the whole time. The Publishing screen now says so.
Translations loaded on `plugins_loaded`, which WordPress 6.7 warns about on
every request — the kind of noise that trains people to stop reading logs.
And one deletion: an `is_email()` filter written on the assumption that
WordPress rejects `.invalid` addresses. It does not — `is_email()` validates
syntax, not whether a domain could exist — so the filter never fired. A filter
that appears to relax a rule but does not is worse than no filter, because
someone later reasons from it. The documentation made the same claim and has
been corrected.
Verification added rather than asserted: tests/live.mjs drives a real instance
over HTTP (40 checks), and tests/mock-gateway.mjs answers uploads with the
signature check transcribed from the gateway's own source, so the publishing
path can be exercised without a registered name.
2026-09-21 02:35:32 +02:00
$address = SPA_Challenge :: verify ( $nonce , $signature , SPA_Challenge :: PURPOSE_LOGIN , $claimed );
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
if ( is_wp_error ( $address ) ) {
return $address ;
}
$found = SP_Identity :: user_by_address ( $address );
if ( ! $found ) {
if ( SP_Settings :: open_registration () ) {
return new WP_Error (
'sirius_no_account' ,
sprintf (
/* translators: %s: URL of the registration page. */
__ ( 'No account here uses that wallet yet. <a href="%s">Create one</a> — it takes one more signature.' , 'sirius-press' ),
esc_url ( SPA_Register :: url () )
)
);
}
return new WP_Error (
'sirius_no_account' ,
__ ( 'No account on this site uses that wallet, and registration is closed.' , 'sirius-press' )
);
}
/**
* Fires after a wallet signature has been accepted for a user .
*
* A plugin that wants a second factor can return a WP_Error from the
* `authenticate` chain at a later priority ; this is the hook that
* tells it a wallet proof already succeeded .
*
* @ param WP_User $found
* @ param string $address
*/
do_action ( 'sirius_press_wallet_authenticated' , $found , $address );
return $found ;
}
// ------------------------------------------------------------------- UI
public static function enqueue () {
self :: enqueue_wallet ();
wp_enqueue_style (
'sirius-press-login' ,
SIRIUS_PRESS_AUTH_URL . 'assets/login.css' ,
array (),
SIRIUS_PRESS_AUTH_VERSION
);
}
/** Shared by the login screen, the registration screen and the profile. */
public static function enqueue_wallet () {
wp_enqueue_script (
'sirius-press-bip39' ,
SIRIUS_PRESS_AUTH_URL . 'assets/bip39-en.js' ,
array (),
SIRIUS_PRESS_AUTH_VERSION ,
true
);
wp_enqueue_script (
'sirius-press-wallet' ,
SIRIUS_PRESS_AUTH_URL . 'assets/wallet.js' ,
array ( 'sirius-press-bip39' ),
SIRIUS_PRESS_AUTH_VERSION ,
true
);
wp_enqueue_script (
'sirius-press-login-js' ,
SIRIUS_PRESS_AUTH_URL . 'assets/login.js' ,
array ( 'sirius-press-wallet' ),
SIRIUS_PRESS_AUTH_VERSION ,
true
);
}
/** Configuration the scripts need, printed once. */
public static function footer_config () {
self :: print_config ();
}
public static function print_config () {
static $printed = false ;
if ( $printed ) {
return ;
}
$printed = true ;
printf (
'<script>window.SIRIUS_PRESS = %s;</script>' ,
wp_json_encode (
array (
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
'prefix' => SP_Settings :: prefix (),
'path' => SP_Settings :: derivation_path (),
'network' => SP_Settings :: network (),
'site' => home_url ( '/' ),
// So the page can stop core's username and password inputs
// from blocking a submit they are no longer part of.
'passwords' => self :: passwords_allowed (),
'strings' => array (
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
'signing' => __ ( 'Signing…' , 'sirius-press' ),
'noWallet' => __ ( 'No wallet found in this browser.' , 'sirius-press' ),
'generated' => __ ( 'Write these words down. They are the only way back into this account — nobody, including this site, can reset them for you.' , 'sirius-press' ),
),
)
)
);
}
/**
* The wallet block on wp - login . php .
*
* Rendered by `login_form` , which puts it directly under the password
* field , so the wallet path reads as the primary one and passwords as the
* leftover they are .
*/
public static function render_form () {
$nonce = SPA_Challenge :: issue ();
$message = SPA_Challenge :: message ( $nonce , SPA_Challenge :: PURPOSE_LOGIN );
self :: render_signing_block ( $nonce , $message , SPA_Challenge :: PURPOSE_LOGIN , __ ( 'Sign in with your wallet' , 'sirius-press' ) );
}
/**
* The signing widget , shared by login and registration .
*
* @ param string $nonce
* @ param string $message Exact text to be signed .
* @ param string $purpose
* @ param string $button Label for the primary action .
*/
public static function render_signing_block ( $nonce , $message , $purpose , $button ) {
?>
< div class = " sirius-wallet " data - sirius - purpose = " <?php echo esc_attr( $purpose ); ?> " >
< input type = " hidden " name = " sirius_nonce " value = " <?php echo esc_attr( $nonce ); ?> " />
< input type = " hidden " name = " sirius_purpose " value = " <?php echo esc_attr( $purpose ); ?> " />
fix(sirius-press): recovering a key is not the same as verifying a signature
Running the fork against a live WordPress found a real hole in registration,
and it is the kind that only shows up when you actually try it.
ECDSA public-key recovery always succeeds. Given any well-formed signature
and any digest it returns a key — just not the signer's, unless the digest is
the one that was signed. The auth flow leaned on that as if a wrong message
would fail. It does not; it quietly yields a stranger's address.
At sign-in this was harmless, because the wrong address matches no account
and the attempt fails. Registration and wallet-linking were another matter:
both took the recovered address and bound it to an account, so a signature
over slightly different text — a challenge copied without its blank line, a
wallet that rewrote the text, a login signature replayed at the registration
form — created an account keyed to an address nobody could sign for. The
person would see "success" and discover the truth the next time they tried to
get in. Wallet-linking was worse still: it would move an existing account onto
a dead address and lock its owner out of their own site.
Both paths now require the address the signer claims and compare it to the
recovered one, which is what verification actually means. Sign-in accepts the
claim when the page sends it and uses it to turn "no account uses that wallet"
into the more useful "that signature is not over the text we asked for".
Also from running it:
URL rewriting mangled every link on a site whose URL carries a port. The
protocol-relative pass matched inside absolute URLs and gave each one a second
scheme, and matching the host without its port left the port stranded as
`//host:8760:8760/`. Local and staging installs would have exported a site of
broken links.
Plain permalinks silently collapse an entire site onto one exported file,
because every post's URL is `/?p=N` and its path is `/`. The queue looks
healthy the whole time. The Publishing screen now says so.
Translations loaded on `plugins_loaded`, which WordPress 6.7 warns about on
every request — the kind of noise that trains people to stop reading logs.
And one deletion: an `is_email()` filter written on the assumption that
WordPress rejects `.invalid` addresses. It does not — `is_email()` validates
syntax, not whether a domain could exist — so the filter never fired. A filter
that appears to relax a rule but does not is worse than no filter, because
someone later reasons from it. The documentation made the same claim and has
been corrected.
Verification added rather than asserted: tests/live.mjs drives a real instance
over HTTP (40 checks), and tests/mock-gateway.mjs answers uploads with the
signature check transcribed from the gateway's own source, so the publishing
path can be exercised without a registered name.
2026-09-21 02:35:32 +02:00
< ? php
/*
* The address the signer claims . Filled in by the script after
* signing ; typed by hand on the paste - a - signature path .
*
* Not decoration : recovering a key from a signature always
* succeeds , so without something to compare against , a signature
* over the wrong text yields a stranger ' s address instead of an
* error . See the note in class - spa - challenge . php .
*/
?>
< input type = " hidden " name = " sirius_address " class = " sirius-wallet__address " value = " " />
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
< textarea
class = " sirius-wallet__message "
readonly
rows = " 8 "
aria - label = " <?php esc_attr_e( 'The exact text to sign', 'sirius-press' ); ?> "
>< ? php echo esc_textarea ( $message ); ?> </textarea>
< div class = " sirius-wallet__actions " >
< button type = " button " class = " button button-primary sirius-wallet__sign " hidden >
< ? php echo esc_html ( $button ); ?>
</ button >
< button type = " button " class = " button sirius-wallet__external " hidden >
< ? php esc_html_e ( 'Use the browser wallet' , 'sirius-press' ); ?>
</ button >
</ div >
< div class = " sirius-wallet__phrase " hidden >
< label for = " sirius_phrase_<?php echo esc_attr( $purpose ); ?> " >
< ? php esc_html_e ( 'Recovery phrase' , 'sirius-press' ); ?>
</ label >
< textarea
id = " sirius_phrase_<?php echo esc_attr( $purpose ); ?> "
class = " sirius-wallet__phrase-input "
rows = " 2 "
autocomplete = " off "
autocapitalize = " none "
spellcheck = " false "
></ textarea >
< p class = " sirius-wallet__hint " >
< ? php esc_html_e ( 'Typed here, the phrase never leaves this page — only the signature is sent. If you would rather not type it, sign the text above in your own wallet and paste the result below.' , 'sirius-press' ); ?>
</ p >
</ div >
< details class = " sirius-wallet__manual " >
< summary >< ? php esc_html_e ( 'Paste a signature instead' , 'sirius-press' ); ?> </summary>
< p class = " sirius-wallet__hint " >
< ? php esc_html_e ( 'Sign the text above in any Bitcoin Cash wallet — Electron Cash calls it “Sign message” — and paste what it gives you.' , 'sirius-press' ); ?>
</ p >
< textarea
name = " sirius_signature "
class = " sirius-wallet__signature "
rows = " 3 "
autocomplete = " off "
spellcheck = " false "
placeholder = " <?php esc_attr_e( 'base64 signature', 'sirius-press' ); ?> "
></ textarea >
fix(sirius-press): recovering a key is not the same as verifying a signature
Running the fork against a live WordPress found a real hole in registration,
and it is the kind that only shows up when you actually try it.
ECDSA public-key recovery always succeeds. Given any well-formed signature
and any digest it returns a key — just not the signer's, unless the digest is
the one that was signed. The auth flow leaned on that as if a wrong message
would fail. It does not; it quietly yields a stranger's address.
At sign-in this was harmless, because the wrong address matches no account
and the attempt fails. Registration and wallet-linking were another matter:
both took the recovered address and bound it to an account, so a signature
over slightly different text — a challenge copied without its blank line, a
wallet that rewrote the text, a login signature replayed at the registration
form — created an account keyed to an address nobody could sign for. The
person would see "success" and discover the truth the next time they tried to
get in. Wallet-linking was worse still: it would move an existing account onto
a dead address and lock its owner out of their own site.
Both paths now require the address the signer claims and compare it to the
recovered one, which is what verification actually means. Sign-in accepts the
claim when the page sends it and uses it to turn "no account uses that wallet"
into the more useful "that signature is not over the text we asked for".
Also from running it:
URL rewriting mangled every link on a site whose URL carries a port. The
protocol-relative pass matched inside absolute URLs and gave each one a second
scheme, and matching the host without its port left the port stranded as
`//host:8760:8760/`. Local and staging installs would have exported a site of
broken links.
Plain permalinks silently collapse an entire site onto one exported file,
because every post's URL is `/?p=N` and its path is `/`. The queue looks
healthy the whole time. The Publishing screen now says so.
Translations loaded on `plugins_loaded`, which WordPress 6.7 warns about on
every request — the kind of noise that trains people to stop reading logs.
And one deletion: an `is_email()` filter written on the assumption that
WordPress rejects `.invalid` addresses. It does not — `is_email()` validates
syntax, not whether a domain could exist — so the filter never fired. A filter
that appears to relax a rule but does not is worse than no filter, because
someone later reasons from it. The documentation made the same claim and has
been corrected.
Verification added rather than asserted: tests/live.mjs drives a real instance
over HTTP (40 checks), and tests/mock-gateway.mjs answers uploads with the
signature check transcribed from the gateway's own source, so the publishing
path can be exercised without a registered name.
2026-09-21 02:35:32 +02:00
< label class = " sirius-wallet__address-label " for = " sirius_address_<?php echo esc_attr( $purpose ); ?> " >
< ? php esc_html_e ( 'The address you signed with' , 'sirius-press' ); ?>
</ label >
< input
type = " text "
id = " sirius_address_<?php echo esc_attr( $purpose ); ?> "
class = " sirius-wallet__address-input "
autocomplete = " off "
autocapitalize = " none "
spellcheck = " false "
placeholder = " <?php echo esc_attr( SP_Settings::prefix() ); ?>:… "
/>
< p class = " sirius-wallet__hint " >
< ? php esc_html_e ( 'Your wallet shows this next to the signature. It is checked against the signature, so a mistyped address is caught rather than quietly creating the wrong account.' , 'sirius-press' ); ?>
</ p >
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
< ? php
/*
* This path ' s own submit button .
*
* It used to borrow core ' s , which is wrong twice over : on
* wp - login . php that button is hidden once passwords are off ,
* leaving a pasted signature with no way to be sent ; and on
* every other screen this block appears on there is no core
* login button to borrow . Plain markup , so it still works with
* JavaScript switched off — which is the whole reason this
* path exists .
*/
?>
< button type = " submit " class = " button button-primary sirius-wallet__submit " >
< ? php esc_html_e ( 'Use this signature' , 'sirius-press' ); ?>
</ button >
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
</ details >
< p class = " sirius-wallet__status " role = " status " aria - live = " polite " ></ p >
</ div >
< ? php
}
public static function login_message ( $message ) {
if ( ! empty ( $_GET [ 'action' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $message ;
}
$intro = '<p class="message sirius-intro">'
. esc_html__ ( 'This site has no passwords to forget and no email to confirm. Your wallet is the account.' , 'sirius-press' )
. '</p>' ;
return $intro . $message ;
}
/**
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
* Stop the login screen greeting every visitor with a failed sign - in .
*
* `wp-login.php` calls `wp_signon()` on its way to drawing the form , even
* for a plain GET with nothing submitted . Core tolerates that by throwing
* the result away — but only when the error codes are exactly the pair its
* own two password filters produce for an empty form . Removing those
* filters , which is the entire point of turning passwords off , leaves the
* generic `authentication_failed` instead . Core does not recognise it , so
* it prints it : a red " Invalid username, email address or incorrect
* password " above a form that has neither field, on a site that has no
* passwords .
*
* @ param WP_Error $errors
* @ return WP_Error
*/
public static function quiet_empty_form_error ( $errors ) {
// Nothing was submitted, so there is nothing to forge: this only reads
// whether the request carried a body at all.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! empty ( $_POST ) ) {
return $errors ;
}
if ( ! is_wp_error ( $errors ) || array ( 'authentication_failed' ) !== $errors -> get_error_codes () ) {
return $errors ;
}
return new WP_Error ();
}
/**
* Visually retire the username and password fields when passwords are off .
*
* Both of them , not just the password . With passwords off , `authenticate()`
* never reads the username either — the account is found from the address
* recovered out of the signature — so leaving " Username or Email Address "
* at the top of the form puts the exact thing this fork exists to remove
* in the first position a visitor looks .
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
*
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
* Core ' s own submit button goes too . The paste - a - signature path — the one
* for people whose keys are on another machine — carries its own , so it no
* longer depends on a control that belongs to the password form it is
* replacing . The `required` attributes those two inputs carry are cleared
* in login . js ,
* because a hidden required field does not fail validation quietly : it
* refuses the submit and points an error bubble at a control nobody can
* see .
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
*/
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
public static function hide_credential_fields () {
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
echo '<style>'
sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords
were actually turned off — which until now nobody had done on a live
instance.
wp-login.php runs wp_signon() on its way to drawing the form, even for a
plain GET. Core tolerates that by discarding the result, but only when
the error codes are exactly the pair its own password filters produce.
Removing those filters is the whole point of turning passwords off, so
what was left behind was the generic authentication_failed — printed, in
red, above a form with no username and no password, on a site that has
neither. Every visitor was told their sign-in had failed before they
tried.
The paste-a-signature path had no way to submit. It borrowed core's
login button, and the stylesheet that retires the password form hid that
button too. The one route for people whose keys live on another machine
— sign in Electron Cash, paste the result — was therefore broken exactly
on the sites most committed to not having passwords. It now carries its
own plain submit button, so it also works on the screens where there is
no core login form to borrow from, and with JavaScript off.
And the username field outlived the password field, although
authenticate() never reads it either. "Username or Email Address" sat at
the top of a wallet login form: the one thing this fork exists to
remove, in the first place anyone looks. Both go now, with their
required attributes cleared, because a hidden required field does not
fail quietly — it refuses the submit and points an error bubble at a
control nobody can see.
2026-10-03 00:57:34 +02:00
. '#user_login, label[for="user_login"],'
. '#user_pass, label[for="user_pass"], .user-pass-wrap,'
. '.forgetmenot + p { display:none !important; }'
// Core wraps each field in a bare <p>, so hiding the input alone
// leaves its margins behind as a gap above the wallet block.
. 'form p:has( > #user_login ), form p:has( .user-pass-wrap ) { display:none !important; }'
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
. '</style>' ;
}
}