diff --git a/.gitea/workflows/wk-recovery.yml b/.gitea/workflows/wk-recovery.yml new file mode 100644 index 0000000..d9bcaf3 --- /dev/null +++ b/.gitea/workflows/wk-recovery.yml @@ -0,0 +1,50 @@ +name: wk-recovery +# Sets (or clears, with recovery=none) the recovery key on our own well-known names. +# Signs with the name's main key inside Actions: factory = SNAIL_KNOWN, well-known = WK_NSEC. Keys never leave the runner. +on: + workflow_dispatch: + inputs: + key: { description: "which main key signs: factory or well-known", required: true, default: factory } + names: { description: comma-separated names, required: true } + recovery: { description: "recovery pubkey (64 hex) or none", required: true } +jobs: + set: + runs-on: ubuntu-latest + steps: + - name: run + env: + K_FACTORY: ${{ secrets.SNAIL_KNOWN }} + K_WK: ${{ secrets.WK_NSEC }} + WHICH: ${{ inputs.key }} + NAMES: ${{ inputs.names }} + REC: ${{ inputs.recovery }} + run: | + mkdir -p /tmp/rk && cd /tmp/rk + npm init -y >/dev/null && npm i --silent @noble/curves@1.9.7 >/dev/null + cat > t.mjs <<'JS' + import { schnorr } from '@noble/curves/secp256k1'; + import { createHash } from 'node:crypto'; + const CH = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; + const toHex = (s) => { s = s.trim().toLowerCase(); if (/^[0-9a-f]{64}$/.test(s)) return s; + const d = [...s.slice(s.lastIndexOf('1') + 1)].map((c) => CH.indexOf(c)).slice(0, -6); let a = 0, b = 0; const o = []; + for (const v of d) { a = (a << 5) | v; b += 5; while (b >= 8) { b -= 8; o.push((a >> b) & 255); } } return Buffer.from(o).toString('hex'); }; + const raw = process.env.WHICH === 'well-known' ? process.env.K_WK : process.env.WHICH === 'factory' ? process.env.K_FACTORY : ''; + if (!raw) { console.log('key must be factory or well-known'); process.exit(2); } + const rec = process.env.REC.trim().toLowerCase(); + if (rec !== 'none' && !/^[0-9a-f]{64}$/.test(rec)) { console.log('recovery must be 64 hex or none'); process.exit(2); } + const sk = toHex(raw), pk = Buffer.from(schnorr.getPublicKey(sk)).toString('hex'); + let fails = 0, n = 0; + for (const name of process.env.NAMES.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)) { + const ev = { pubkey: pk, created_at: Math.floor(Date.now() / 1000), kind: 27235, content: String(n++), + tags: [['u', 'https://well-known.online/recovery'], ['method', 'POST'], ['name', name], ...(rec === 'none' ? [] : [['p', rec]])] }; + ev.id = createHash('sha256').update(JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content])).digest('hex'); + ev.sig = Buffer.from(schnorr.sign(ev.id, sk)).toString('hex'); + const r = await fetch('https://well-known.online/recovery', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ event: ev }) }); + const j = await r.json().catch(() => ({})); + const ok = r.status === 200 && (rec === 'none' ? j.recovery === null : j.recovery === rec); + console.log(`${ok ? 'PASS' : 'FAIL'} ${name} ${r.status} ${JSON.stringify(j)}`); + if (!ok) fails++; + } + process.exit(fails ? 1 : 0); + JS + node t.mjs