open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/privacy-pass.test.mjs
Transaction Science c93aa4f7a9 The room page's Privacy Pass client (RFC 9577, RFC 9578 §6) and ratings on its tracks; every grant request in a room that admits by age carries a token
web/room/privacy-pass.js reads RFC 9577's challenges, checks them (§2.1.3), and obtains a
token by blind issuance (RFC 9474 §4.2 to §4.4) from the issuer's directory (RFC 9578 §4); it
reproduces RFC 9578 A.2's TokenRequest and token from the corpus's vector. The page asks
again with the token on a 401, carries the room's rating (and, with rate=, its own rating of
its video, signed in wasm) on every PUBLISH, and records a relay's below_threshold refusal
as withheld. The service now challenges publishers as it does viewers: every track of a rated
room is held to its rating, the room's events among them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:15:49 -04:00

72 lines
4.4 KiB
JavaScript

// 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/);
});