45 lines
2.3 KiB
JavaScript
45 lines
2.3 KiB
JavaScript
|
|
// 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|<id>|<version>|<tarball-sha256>|<publisher-name>")
|
||
|
|
// 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;
|
||
|
|
}
|