// The page's Privacy Pass client against RFC 9578 Appendix A.2's first issuance vector, as WAI's // age-assurance corpus carries it (wai/live-conformance/age/vectors.json): from the vector's key, // challenge, nonce, blind and salt the client writes the vector's TokenRequest, and from the // issuer's TokenResponse the vector's token; and RFC 9577's header fields read and write. // // node --test web/room/privacy-pass.test.mjs import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import { tokenRequest, parseChallenges, readChallenge, acceptable, authorization, b64url, unb64url, readSpki, pssVerify, issue, directoryOf } from "./privacy-pass.js"; const corpus = JSON.parse(fs.readFileSync(new URL("../../../../../../wai/live-conformance/age/vectors.json", import.meta.url), "utf8")); const v = corpus.rfc9578_a2_vector1; const h = (s) => new Uint8Array(Buffer.from(s, "hex")); const hex = (b) => Buffer.from(b).toString("hex"); test("RFC 9578 A.2: the TokenRequest and the token, byte for byte", async () => { const { request, finalize } = await tokenRequest({ challenge: h(v.token_challenge), tokenKey: h(v.pkI), nonce: h(v.nonce), blind: h(v.blind), salt: h(v.salt) }); assert.equal(hex(request), v.token_request); const token = await finalize(h(v.token_response)); assert.equal(hex(token), v.token); // An answer changed by one bit is refused: it does not unblind to a signature. const bad = h(v.token_response); bad[100] ^= 1; await assert.rejects(finalize(bad), /invalid signature/); }); test("the token verifies under the key; a byte changed does not", async () => { const key = readSpki(h(v.pkI)); const tok = h(v.token); assert.equal(await pssVerify(key, tok.subarray(0, 98), tok.subarray(98)), true); tok[50] ^= 1; assert.equal(await pssVerify(key, tok.subarray(0, 98), tok.subarray(98)), false); }); test("RFC 9577: challenges read from WWW-Authenticate, checked, and the token's field", () => { const c = h(v.token_challenge); const k = h(v.pkI); const one = `PrivateToken challenge="${b64url(c)}", token-key="${b64url(k)}", max-age="600"`; const got = parseChallenges([`${one}, ${one.replace("600", "30")}`, "Bearer realm=x"]); assert.equal(got.length, 2); assert.deepEqual([hex(got[0].challenge), hex(got[0].tokenKey), got[0].maxAge, got[1].maxAge], [v.token_challenge, v.pkI, 600, 30]); const r = readChallenge(c); assert.deepEqual([r.tokenType, r.issuerName, r.redemptionContext.length, r.originInfo], [2, "issuer.example", 32, "origin.example"]); assert.equal(acceptable(r, "ORIGIN.example"), true); assert.equal(acceptable(r, "other.example"), false, "an origin_info that does not list the page's origin"); assert.equal(readChallenge(c.subarray(0, c.length - 1)), null, "cut short"); assert.equal(authorization(new Uint8Array([0, 1, 2])), 'PrivateToken token="AAEC"'); for (let n = 0; n < 20; n++) { const b = new Uint8Array(n).map((_, i) => (i * 37) & 255); assert.deepEqual(unb64url(b64url(b)), b); } assert.equal(directoryOf("issuer.example"), "https://issuer.example/.well-known/private-token-issuer-directory"); assert.equal(directoryOf("issuer-a.example", "http://127.0.0.1:9/"), "http://127.0.0.1:9/issuer-a.example/.well-known/private-token-issuer-directory"); }); test("issue: the directory, then the request, through a fetch the test answers as the vector's issuer", async () => { // An issuer that answers every request with the vector's response: the client, drawing its // own nonce and blind, finalizes it only if its request was the vector's, which it is not, // so the answer is refused; with the key it does not list, the client asks for nothing. const fetchFn = async (url, init) => { if (url.endsWith("/directory")) return { ok: true, json: async () => ({ "issuer-request-uri": "/request", "token-keys": [{ "token-type": 2, "token-key": b64url(h(v.pkI)) }] }) }; assert.equal(init.headers["content-type"], "application/private-token-request"); assert.equal(init.body.length, 259); return { status: 200, arrayBuffer: async () => h(v.token_response).buffer }; }; await assert.rejects(issue({ challenge: h(v.token_challenge), tokenKey: h(v.pkI), directory: "https://issuer.example/directory", fetchFn }), /invalid signature/); const other = h(v.pkI); other[300] ^= 1; await assert.rejects(issue({ challenge: h(v.token_challenge), tokenKey: other, directory: "https://issuer.example/directory", fetchFn }), /does not list/); });