open-standards/joule-code/joule-code-rs/crates/joule-moq-web/e2e/issuer.mjs
2026-10-21 19:18:09 -04:00

79 lines
4.1 KiB
JavaScript

// 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 /<issuer name>/.well-known/private-token-issuer-directory the directory (§4)
// POST /<issuer name>/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)) };
}