Rename Tools → Theseus, trim /tlds/ self-ref, chipnet auth + state fixes
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.
This commit is contained in:
parent
abf21785bc
commit
9baa5db887
2 changed files with 33 additions and 7 deletions
|
|
@ -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() };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue