// The com.fedfork.exec.attest pseudo-call in the browser: network.attested.signature entries, // byte for byte as socialweb-computer-emdash src/attest.ts (atlibs-host-calls) and kcp // internal/atproto/market/attest.go. The secp256k1 implementation is injected // (@noble/curves 2.x `secp256k1`: getPublicKey, sign with sha256 prehash, utils.randomSecretKey). import { base58, dagCbor, toBase32 } from "../encoding/index.js"; export const ATTEST_NSID = "com.fedfork.exec.attest"; const SIGNATURE = "network.attested.signature"; // CIDv1 dag-cbor sha2-256 of the record without signatures, with $sig = entry metadata + repository. export async function attestationCid(record, metadata, repository) { if (typeof record.$type !== "string" || !record.$type) throw new Error("Attested record needs a $type"); const { cid: _cid, signature: _signature, ...sig } = metadata; const { signatures: _signatures, ...prepared } = record; const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(dagCbor({ ...prepared, $sig: { ...sig, repository } })))); const bytes = Uint8Array.from([0x01, 0x71, 0x12, 0x20, ...digest]); return { bytes, cid: `b${toBase32(bytes)}` }; } // did:key of a compressed secp256k1 public key (multicodec 0xe7 0x01). export const didKey = (publicKey) => `did:key:z${base58(Uint8Array.from([0xe7, 0x01, ...publicKey]))}`; async function signInline(secp256k1, secretKey, record, repository) { const metadata = { $type: SIGNATURE, key: didKey(secp256k1.getPublicKey(secretKey, true)), issuer: repository }; const { bytes, cid } = await attestationCid(record, metadata, repository); return { ...metadata, cid, signature: { $bytes: (await secp256k1.sign(bytes, secretKey)).toBase64({ omitPadding: true }) } }; } const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); const unhex = (text) => Uint8Array.from(text.match(/../g), (h) => parseInt(h, 16)); // call(nsid, request) handler for ATTEST_NSID: one key per account DID, in the secret setting // `attestKeys` ({did: hex}) of ctx (createPluginContext with settingsSchema {attestKeys: {type: "secret"}}). // The key never enters wasm; the output is the input record plus one signatures[] entry. // ponytail: get-then-set, two tabs generating at once can race; use compareAndSet if that matters. export function attestHost(ctx, secp256k1, account) { const key = async () => { const keys = (await ctx.settings.get("attestKeys")) ?? {}; if (!keys[account]) await ctx.settings.set("attestKeys", { ...keys, [account]: hex(secp256k1.utils.randomSecretKey()) }); return unhex((await ctx.settings.get("attestKeys"))[account]); }; return async ({ repository, record } = {}) => { if (!account || repository !== account) throw new Error("attest repository must be the session DID"); const secretKey = await key(); if (record === undefined) return { key: didKey(secp256k1.getPublicKey(secretKey, true)) }; if (typeof record !== "object" || record === null || Array.isArray(record)) throw new Error("attest record must be an object"); const entry = await signInline(secp256k1, secretKey, record, account); return { key: entry.key, record: { ...record, signatures: [...(Array.isArray(record.signatures) ? record.signatures : []), entry] } }; }; }