sirius-press/plugins/sirius-press-auth/includes/class-spa-login.php

317 lines
12 KiB
PHP
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
<?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 );
add_action( 'login_head', array( __CLASS__, 'hide_password_fields' ) );
}
}
/** 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(
'prefix' => SP_Settings::prefix(),
'path' => SP_Settings::derivation_path(),
'network' => SP_Settings::network(),
'site' => home_url( '/' ),
'strings' => array(
'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>
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;
}
/**
* Visually retire the password field when passwords are off.
*
* The submit button stays. It is what the paste-a-signature path the one
* for people whose keys are on another machine uses to send the form,
* and hiding it would leave that path with no way to submit at all.
*/
public static function hide_password_fields() {
echo '<style>'
. '#user_pass, label[for="user_pass"], .user-pass-wrap, .forgetmenot + p { display:none !important; }'
. '</style>';
}
}