open-standards/wai/wai-audit/web/blake3.js
Transaction Science e04d96eba2 wai-audit: the stream audit collector and the player integration
wai/wai-audit is the reference collector of stream-audit §12: it
verifies each bundle a player posts (wai-rs stream_audit), stores the
accepted ones one file per session (a different record of a stored
session is refused audit_session_duplicate), takes Event-mode CMCD
batches as advisory telemetry, and serves each channel's report signed
with its key. Limits per client (an IPv4 address or an IPv6 /64), per
route body limits, CORS for the configured page origins. The CLI also
verifies a bundle, recomputes a report from a data directory, and
checks a served report against its bundles. Its tests post every corpus
session and require the corpus's outcome, and require the stored
sessions to report exactly the corpus's served reports.

web/audit.js is the player integration: it hashes each object with
WebCrypto, signs the acknowledgements, claims and session record with
WebCrypto's Ed25519, computes the settlement tree root with a one-chunk
BLAKE3 of its own, records spans, ads, the log and its CMCD batch. Its
Node tests drive it with the corpus's inputs and require the corpus's
records byte for byte. adapter-tap.js wraps fetch and XMLHttpRequest for
any player; adapter-hlsjs.js wraps a player's loader class.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:41:51 -04:00

66 lines
2.6 KiB
JavaScript

// BLAKE3 in hash mode for an input of one chunk (at most 1024 bytes), written from the BLAKE3
// specification: the chunk's blocks compressed in turn from the IV, the first with CHUNK_START,
// the last with CHUNK_END and ROOT, and the root output's first 32 bytes. That is all the
// settlement tree root needs (jwp-receipts §2.2): a leaf hashes 52 bytes and a node 84.
// An input longer than one chunk is refused, never hashed wrongly.
const IV = Uint32Array.of(0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19);
const PERMUTE = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
const CHUNK_START = 1;
const CHUNK_END = 2;
const ROOT = 8;
const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0;
function g(s, a, b, c, d, x, y) {
s[a] = (s[a] + s[b] + x) >>> 0;
s[d] = rotr(s[d] ^ s[a], 16);
s[c] = (s[c] + s[d]) >>> 0;
s[b] = rotr(s[b] ^ s[c], 12);
s[a] = (s[a] + s[b] + y) >>> 0;
s[d] = rotr(s[d] ^ s[a], 8);
s[c] = (s[c] + s[d]) >>> 0;
s[b] = rotr(s[b] ^ s[c], 7);
}
/** The compression function: chaining value `cv`, 16 message words, counter 0, `len` bytes, `flags`. */
function compress(cv, m, len, flags) {
const s = Uint32Array.of(...cv, IV[0], IV[1], IV[2], IV[3], 0, 0, len, flags);
let w = Uint32Array.from(m);
for (let r = 0; r < 7; r++) {
g(s, 0, 4, 8, 12, w[0], w[1]);
g(s, 1, 5, 9, 13, w[2], w[3]);
g(s, 2, 6, 10, 14, w[4], w[5]);
g(s, 3, 7, 11, 15, w[6], w[7]);
g(s, 0, 5, 10, 15, w[8], w[9]);
g(s, 1, 6, 11, 12, w[10], w[11]);
g(s, 2, 7, 8, 13, w[12], w[13]);
g(s, 3, 4, 9, 14, w[14], w[15]);
if (r < 6) w = Uint32Array.from(PERMUTE, (i) => w[i]);
}
const out = new Uint32Array(8);
for (let i = 0; i < 8; i++) out[i] = (s[i] ^ s[i + 8]) >>> 0;
return out;
}
/** BLAKE3 of `input` (a Uint8Array of at most 1024 bytes), 32 bytes. */
export function blake3(input) {
if (input.length > 1024) throw new RangeError("this BLAKE3 hashes one chunk, at most 1024 bytes");
const blocks = Math.max(1, Math.ceil(input.length / 64));
let cv = IV;
for (let k = 0; k < blocks; k++) {
const part = input.subarray(64 * k, 64 * k + 64);
const block = new Uint8Array(64);
block.set(part);
const dv = new DataView(block.buffer);
const m = Array.from({ length: 16 }, (_, i) => dv.getUint32(4 * i, true));
let flags = 0;
if (k === 0) flags |= CHUNK_START;
if (k === blocks - 1) flags |= CHUNK_END | ROOT;
cv = compress(cv, m, part.length, flags);
}
const out = new Uint8Array(32);
const dv = new DataView(out.buffer);
for (let i = 0; i < 8; i++) dv.setUint32(4 * i, cv[i], true);
return out;
}