79 lines
4.1 KiB
JavaScript
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)) };
|
|
}
|