// A Privacy Pass issuer for the end-to-end tests alone (RFC 9578 §4, §6.2), on loopback, over // the published test keys of WAI's age-assurance corpus: it signs every TokenRequest it is // sent, blindly, under the key it names, attesting nothing. A deployment's issuer and its // attester establish a person's age; this one stands in for them so that the page's own client // (web/room/privacy-pass.js) runs its whole path: the directory, the blinded request, the // unblinding and the token. // // GET //.well-known/private-token-issuer-directory the directory (§4) // POST //request a TokenRequest (§6.1) → // its TokenResponse (§6.2) // // Cross-origin: the page fetches it from its own origin, so it answers CORS preflights. import http from "node:http"; import fs from "node:fs"; import { b64url, readSpki, modpow } from "../web/room/privacy-pass.js"; import { PRIVATE_EXPONENTS } from "./rfc-test-keys.mjs"; const big = (b) => b.reduce((n, x) => (n << 8n) | BigInt(x), 0n); function bytes(n, len) { const out = new Uint8Array(len); for (let i = len - 1; i >= 0; i--) { out[i] = Number(n & 255n); n >>= 8n; } return out; } // The issuer keys of the corpus at `corpusFile`: each its issuer, threshold, SubjectPublicKeyInfo // (hexadecimal) and private exponent. export function testKeys(corpusFile) { const doc = JSON.parse(fs.readFileSync(corpusFile, "utf8")); return doc.issuer_keys.map((k) => ({ issuer: k.issuer, threshold: k.threshold, spki: k.key, d: PRIVATE_EXPONENTS[k.source], source: k.source })); } export async function startIssuer(keys) { const byIssuer = new Map(); for (const k of keys) { const spki = new Uint8Array(Buffer.from(k.spki, "hex")); const { n } = readSpki(spki); const id = new Uint8Array(await crypto.subtle.digest("SHA-256", spki)); const list = byIssuer.get(k.issuer) || []; list.push({ ...k, spkiBytes: spki, n, d: BigInt(`0x${k.d}`), truncated: id[31] }); byIssuer.set(k.issuer, list); } const issued = []; const cors = { "access-control-allow-origin": "*", "access-control-allow-headers": "content-type, accept", "access-control-allow-methods": "GET, POST, OPTIONS", "cache-control": "no-store" }; const server = http.createServer((req, res) => { const u = new URL(req.url, "http://127.0.0.1"); const [, name, ...rest] = u.pathname.split("/"); const list = byIssuer.get(decodeURIComponent(name || "")); if (req.method === "OPTIONS") { res.writeHead(204, cors); return res.end(); } if (!list) { res.writeHead(404, cors); return res.end(); } const tail = rest.join("/"); if (req.method === "GET" && tail === ".well-known/private-token-issuer-directory") { res.writeHead(200, { ...cors, "content-type": "application/private-token-issuer-directory" }); return res.end(JSON.stringify({ "issuer-request-uri": `/${name}/request`, "token-keys": list.map((k) => ({ "token-type": 2, "token-key": b64url(k.spkiBytes) })) })); } if (req.method === "POST" && tail === "request") { const chunks = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => { const b = new Uint8Array(Buffer.concat(chunks)); // §6.2: a supported type, a known truncated key id, a blinded message of Nk bytes. const k = b.length === 259 && b[0] === 0 && b[1] === 2 ? list.find((x) => x.truncated === b[2]) : null; if (!k) { res.writeHead(422, cors); return res.end(); } const m = big(b.subarray(3)); if (m >= k.n) { res.writeHead(422, cors); return res.end(); } // RFC 9474 §4.3, BlindSign: s = m^d mod n. const s = modpow(m, k.d, k.n); issued.push({ issuer: name, threshold: k.threshold }); res.writeHead(200, { ...cors, "content-type": "application/private-token-response" }); res.end(Buffer.from(bytes(s, 256))); }); return; } res.writeHead(404, cors); res.end(); }); await new Promise((ok) => server.listen(0, "127.0.0.1", ok)); return { url: `http://127.0.0.1:${server.address().port}`, issued, close: () => new Promise((r) => server.close(r)) }; }