// Publisher signatures on community extensions. // // A community extension's updates.json entry carries `publisherSig`: a // 65-byte BCH message signature (base64) over // sha256("silentmode.extension-v1||||") // made with the key that holds the publisher name's NFT. The gateway checks // it before storing the entry; Theseus checks it AGAIN against the owner it // reads from its own chain index, so a tampered catalog or a compromised // relay cannot hand the browser someone else's code under a trusted name. // // ESM because @bitauth/libauth is ESM-only; main.js loads it with import(). import { secp256k1, sha256, ripemd160, encodeCashAddress, base64ToBin, utf8ToBin } from "@bitauth/libauth"; export const EXT_SIG_DOMAIN = "silentmode.extension-v1"; export function entryMessage({ id, version, sha256: tarballSha256, publisher }) { return `${EXT_SIG_DOMAIN}|${id}|${version}|${String(tarballSha256).toLowerCase()}|${publisher}`; } // Recover the signer of `message` and return it as a CashAddress with the // same prefix as `likeAddress` (bitcoincash: / bchtest:). null on any error. export function recoverSigner(message, signatureBase64, likeAddress) { try { const sig = base64ToBin(String(signatureBase64 || "").trim()); if (!(sig instanceof Uint8Array) || sig.length !== 65) return null; const header = sig[0]; if (header < 27 || header > 34) return null; const digest = sha256.hash(utf8ToBin(message)); const recover = header >= 31 ? secp256k1.recoverPublicKeyCompressed : secp256k1.recoverPublicKeyUncompressed; const pub = recover(sig.slice(1), (header - 27) & 3, digest); if (typeof pub === "string") return null; const prefix = String(likeAddress || "bitcoincash:").split(":")[0] || "bitcoincash"; const enc = encodeCashAddress({ prefix, type: "p2pkh", payload: ripemd160.hash(sha256.hash(pub)) }); return typeof enc === "string" ? enc : (enc?.address ?? null); } catch { return null; } } // True when the entry was signed by `ownerAddress` (the publisher name's // current NFT holder as the caller's own index reports it). export function verifyPublisherEntry(entry, ownerAddress) { if (!entry || !ownerAddress || !entry.publisherSig) return false; const who = recoverSigner(entryMessage(entry), entry.publisherSig, ownerAddress); return !!who && who === ownerAddress; }