import crypto from 'node:crypto' import { question } from 'zx' import { ffetch } from '../../utils/fetch.ts' import { getEnv } from '../../utils/misc.ts' function bigintToBase64Url(bn) { let hex = bn.toString(16) if (hex.length % 2) hex = `0${hex}` // eslint-disable-next-line no-restricted-globals const buf = Buffer.from(hex, 'hex') return buf.toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '') } interface GoPrivateKey { N: bigint E: bigint D: bigint Primes: [bigint, bigint] Precomputed?: { Dp?: bigint Dq?: bigint Qinv?: bigint } } function goKeyToPEM(goKey: GoPrivateKey) { const jwk = { kty: 'RSA', n: bigintToBase64Url(goKey.N), e: bigintToBase64Url(goKey.E), d: bigintToBase64Url(goKey.D), p: bigintToBase64Url(goKey.Primes[0]), q: bigintToBase64Url(goKey.Primes[1]), dp: goKey.Precomputed?.Dp ? bigintToBase64Url(goKey.Precomputed.Dp) : undefined, dq: goKey.Precomputed?.Dq ? bigintToBase64Url(goKey.Precomputed.Dq) : undefined, qi: goKey.Precomputed?.Qinv ? bigintToBase64Url(goKey.Precomputed.Qinv) : undefined, } // Remove undefined fields (dp/dq/qi could be missing if not precomputed) Object.keys(jwk).forEach(k => jwk[k] === undefined && delete jwk[k]) const keyObject = crypto.createPrivateKey({ key: jwk, format: 'jwk' }) return keyObject.export({ type: 'pkcs8', format: 'pem' }) } // ! currently only supports gts privkey format, but should be easy enough to support other key formats const privKey = goKeyToPEM(JSON.parse(getEnv('AP_PRIVKEY'), ((key, value, ctx) => { // go privkey json stores long numbers as just numbers so we need to convert them to bigints. requires node 20+ i think if (typeof value === 'number') return BigInt(ctx.source) return value }) as any)) const actor = getEnv('AP_ACTOR') const url = new URL(process.argv[2] ?? (await question('url > '))) const body = (process.argv[3] ?? (await question('body (empty for GET) > '))).trim() const host = url.host const path = url.pathname const method = body ? 'POST' : 'GET' const date = new Date().toUTCString() const digest = body ? `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}` : undefined let toSign = `(request-target): ${method.toLowerCase()} ${path} host: ${host} date: ${date}` if (body) { toSign += `\ndigest: ${digest}` } const signature = crypto.createSign('RSA-SHA256').update(toSign).sign(privKey, 'base64') const headers: Record = { 'Date': date, 'Signature': `keyId="${actor}#main-key",headers="(request-target) host date${body ? ' digest' : ''}",algorithm="rsa-sha256",signature="${signature}"`, 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', } if (body) { headers.Digest = digest! } const res = await ffetch(`https://${host}${path}`, { headers, method, body: body || undefined, validateResponse: false, }) console.log(res.status) const resText = await res.text() if (resText[0] !== '{') { console.error('bad response:', resText) process.exit(1) } const json = JSON.parse(resText) console.dir(json, { depth: null })