From 9baa5db887776895d7722b495eb5c887c4229d54 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Mon, 31 Aug 2026 04:16:42 +0200 Subject: [PATCH] =?UTF-8?q?Rename=20Tools=20=E2=86=92=20Theseus,=20trim=20?= =?UTF-8?q?/tlds/=20self-ref,=20chipnet=20auth=20+=20state=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Site: - Rename the "Tools" nav label to "Theseus" on every subpage. URL slug /tools/ is kept intact so bookmarks and cross-links survive; the page's own title + h1 rename to Theseus; homepage product card retitled. - Remove the /tlds/ self-nav entry so its 7-item header matches every other subpage (was 8 items, causing the same "header rearranges" glitch Hephaestus had before). Auth-proxy (fixes reported by a concurrent session working with a chipnet wallet): - verify.ts now auto-detects the CashAddr prefix (bitcoincash, bchtest, bchreg) instead of hardcoding "bitcoincash". Chipnet wallets sign the same message with the same key but display bchtest: addresses; the old code compared their claim against a re-encoded bitcoincash: version and always rejected. Same key material, different envelope. - /challenge accepts any of the three known prefixes at the input gate. - /verify accepts an optional `state` param in the request body — used as a fallback if the challenge record didn't carry one (which happens when a scripted caller runs the /verify step without passing state through /challenge). Ensures RFC 6749 §4.1.2 compliance: the OAuth callback URL always echoes state when the client provided one. --- auth-proxy/src/index.ts | 23 +++++++++++++++++------ auth-proxy/src/verify.ts | 17 ++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/auth-proxy/src/index.ts b/auth-proxy/src/index.ts index a87e380..d2faa4b 100644 --- a/auth-proxy/src/index.ts +++ b/auth-proxy/src/index.ts @@ -109,8 +109,14 @@ app.post<{ Body: { cashaddr: string; state?: string; redirect_uri?: string }; }>("/challenge", async (req, reply) => { const { cashaddr, state, redirect_uri } = req.body ?? {}; - if (!cashaddr || !cashaddr.startsWith("bitcoincash:")) { - return reply.code(400).send({ error: "cashaddr must start with bitcoincash:" }); + // Accept any CashAddr prefix the chain actually uses: + // bitcoincash: (mainnet) + // bchtest: (chipnet / testnet — what Silent Mode's whole stack runs on today) + // bchreg: (regtest) + if (!cashaddr || !/^(bitcoincash|bchtest|bchreg):/.test(cashaddr)) { + return reply.code(400).send({ + error: "cashaddr must start with bitcoincash:, bchtest: or bchreg:", + }); } const rec = createChallenge(cashaddr, state, redirect_uri); const message = buildChallenge({ @@ -123,10 +129,14 @@ app.post<{ }); // --- Signature verification & OAuth code mint --- +// `state` in the body is a fallback so scripted callers who bypass /challenge's +// state-passing (or use their own OAuth kickoff wrapper) can still get RFC 6749 +// §4.1.2 compliant callbacks. If state was stored via /challenge it's used +// automatically; explicit body override wins if both are present. app.post<{ - Body: { nonce: string; signature: string }; + Body: { nonce: string; signature: string; state?: string; redirect_uri?: string }; }>("/verify", async (req, reply) => { - const { nonce, signature } = req.body ?? {}; + const { nonce, signature, state: stateOverride, redirect_uri: redirectOverride } = req.body ?? {}; if (!nonce || !signature) { return reply.code(400).send({ error: "nonce and signature required" }); } @@ -142,11 +152,12 @@ app.post<{ const recovered = verifyBchSignedMessage(rec.cashaddr, message, signature); if (!recovered) return reply.code(401).send({ error: "signature mismatch" }); - const redirectUri = rec.redirectUri ?? FORGEJO_REDIRECT_URI; + const redirectUri = redirectOverride ?? rec.redirectUri ?? FORGEJO_REDIRECT_URI; const code = mintAuthCode(recovered, CLIENT_ID, redirectUri); const url = new URL(redirectUri); url.searchParams.set("code", code); - if (rec.clientState) url.searchParams.set("state", rec.clientState); + const stateToEcho = stateOverride ?? rec.clientState; + if (stateToEcho) url.searchParams.set("state", stateToEcho); return { redirect: url.toString() }; }); diff --git a/auth-proxy/src/verify.ts b/auth-proxy/src/verify.ts index a55d384..76e266b 100644 --- a/auth-proxy/src/verify.ts +++ b/auth-proxy/src/verify.ts @@ -62,13 +62,28 @@ function magicHash(message: string): Uint8Array { /** * Verify a base64 recoverable-ECDSA signature against a message and cashaddr. * Returns the recovered cashaddr on success, or null on any mismatch. + * + * The prefix is auto-detected from the claimed cashaddr, so both + * bitcoincash:qq... (mainnet) + * bchtest:qq... (chipnet / testnet) + * bchreg:qq... (regtest) + * work with identical key material. This matters because chipnet wallets + * (which the whole Silent Mode stack runs on today) sign the same message + * with the same key, but display their address with the bchtest: prefix. */ +const KNOWN_PREFIXES = ["bitcoincash", "bchtest", "bchreg"] as const; +type Prefix = (typeof KNOWN_PREFIXES)[number]; + export function verifyBchSignedMessage( claimedCashaddr: string, message: string, signatureBase64: string, - prefix: "bitcoincash" | "bchtest" = "bitcoincash", ): string | null { + const prefixMatch = claimedCashaddr.match(/^([a-z]+):/); + if (!prefixMatch) return null; + const prefix = prefixMatch[1] as Prefix; + if (!KNOWN_PREFIXES.includes(prefix)) return null; + const sig = base64ToBin(signatureBase64); if (typeof sig === "string" || sig.length !== 65) return null;