Handoff v0 verifier kit (SPEC r8.1a), JavaScript ================================================= Licence: Apache-2.0 (SPDX header in handoff-verify.mjs). handoff-verify.mjs checks a Handoff v0 record export offline, and runs the published conformance fixtures. It is one ES module with no dependencies: a JavaScript port of the Python reference implementation. The same file runs as a command with Node and loads as a module in a web browser (/verify/run.html uses it). It was tested with Node 24.21.0 and in Chrome 149. The verify path reads only the bytes it is given. It opens no network connection and reads no clock. Only the conformance runner loads files: from the directory or the URL you give it. Files ----- handoff-verify.mjs The verifier, the conformance runner and the command line, in one module. It holds canonical JSON (the G5 profile and RFC 8785), SHA-256, SHA-512 and HMAC-SHA256, strict Ed25519 verification in pure JavaScript with BigInt (RFC 8032, plus the reference's checks: canonical A and R, no small-order or mixed-order A or R, S < L), RFC 9162 Merkle inclusion and consistency proofs, the tip tree, the key history, the event rules and the verdict logic. README.txt This file. Verify an export (Node) ----------------------- node handoff-verify.mjs verify RECORD.json --keys keys.json node handoff-verify.mjs verify RECORD.json --keys keys.json --json node handoff-verify.mjs verify RECORD.json --mirror MIRROR_DIR [--require-anchored] node handoff-verify.mjs verify RECORD.json --keys keys.json --known-heads known-heads.jsonl [--update-known-heads FILE] The flags are those of section 13 of the specification. The key history comes from --keys, or from keys.json inside --mirror. The copy of the key history inside the export is never trusted, because an export cannot vouch for the key that signed it. Without --keys or --mirror the result is ERROR. Exit codes: 0 ACCEPT, 1 REJECT, 2 DETECT, 3 ERROR. With the files published under /verify/r8.1a/: node handoff-verify.mjs verify V01-1.export.json --keys keys.json (prints RESULT: ACCEPT, exit 0) node handoff-verify.mjs verify V02-1.export.json --keys keys.json (prints RESULT: REJECT, code SIG_W_INVALID, exit 1) Run the conformance fixtures (Node) ----------------------------------- node handoff-verify.mjs conformance https://weio.ai/spec/handoff/v0/fixtures/ node handoff-verify.mjs conformance FIXTURES_DIR [--dump FILE] The runner reads SHA256SUMS.txt and MANIFEST.json, checks the SHA-256 of every file SHA256SUMS.txt lists, and runs every fixture: a fixture whose file does not match is reported as FAIL and is not run. It prints PASS or FAIL per fixture, then the lines SUMMARY-BASE, SUMMARY-PROFILE and SUMMARY ("SUMMARY fixtures=N pass=P fail=F"). It exits 0 only when every fixture passes, 1 when any fails, and 3 when the fixtures cannot be loaded. With a URL it sends the User-Agent "weio-handoff-verify/r8.1a (+https://weio.ai/verify/)". --dump FILE writes what the verifier produced for each fixture, one JSON line per fixture. It applies the checks of the reference runner (run_fixtures.py) to every fixture kind of section 15: verify, canon, digest, text, render and ed25519, including the profile fixtures of section 15A. Like the reference runner, it compares the full code lists exactly; a verifier that differs there can still conform (see the fixtures' INDEX.txt). It does not run the server expectations of a fixture (expect.server, expect.server_code, expect.server_refused_seq): there is no server here, and the reference runner does not run them either. In a browser or another script ------------------------------ import { verifyRecord, runConformance } from "./handoff-verify.mjs"; const report = verifyRecord(exportBytes, { keysBytes }); // report.result is "ACCEPT", "REJECT", "DETECT" or "ERROR"; report.findings lists every finding. exportBytes and keysBytes may be a Uint8Array, an ArrayBuffer (what file.arrayBuffer() returns), any typed-array view or DataView, or a string. A string is encoded as UTF-8 the way the reference encodes it, so a lone surrogate in pasted text is refused (JSON_BAD_UTF8) instead of being replaced. Any other input type gives ERROR with the code USAGE. verifyRecord never throws: an unexpected fault gives ERROR with the code INTERNAL, never a verdict. Other options: knownLines (an array, one signed head per item), mirrorHeads (an array of head files, as bytes or strings), mirrorKeys (keys.json of the mirror), requireAnchored (boolean), localCaps ({export_bytes: n}, a local size cap). runConformance(load, {onLine, onCase, pause}) runs the fixtures; load(name) returns the bytes of a file of the fixtures directory by its relative name (a fetch in a page, a file read in Node). Character data -------------- Every rule that depends on Unicode character data (NFC, lower-casing of an email address, digits, printable characters in messages) uses Unicode 15.0, the version of the reference's Python 3.12. The module carries the table of code points assigned in Unicode 15.0. A code point that Unicode 15.0 does not assign is treated the way the reference treats it: no decomposition, no composition, no case mapping, no digit value. Under Unicode's normalization stability policy this gives the reference's result on any engine whose own data is Unicode 15.0 or later. An engine with older data cannot reproduce the reference, and the verifier then answers ERROR with the code ENGINE_UNICODE. What each verdict means ----------------------- ACCEPT Every check this verifier performs passed for the inputs given: the canonical form, the signatures by the key history you supplied, the hash chain, the event rules, the inclusion proofs against the signed heads, and the witness checks for any witness heads you supplied. REJECT The export breaks a rule or is incomplete in a way the export itself shows: a bad signature, a broken chain, an illegal event, a failed proof, a malformed field. The findings name each break. DETECT No rule inside the export is broken, but signed evidence shows a problem the export alone cannot settle: two different signed heads for one position, a head older than one you already hold, a record tip that moved backwards, a signed head that commits more events than the export carries, an anchoring head missing from the mirror. Each such finding carries the signed evidence and a class (conflict, freshness, completeness, anchoring). ERROR The verifier could not judge the export: no key history, a key history it refuses, an export above a local size cap (its content then goes unread), a witness chain it cannot prove, an input of the wrong type, or an engine whose Unicode data is older than 15.0. The verifier is tested against the 270 published fixtures. Passing them is evidence for those cases, not a certification that it implements every rule of the specification. Limits ------ - A signature proves that the holder of a key signed some bytes. It does not prove that what those bytes say is true, that a party is who its name says, or that the work was done. - Event times ("at") are declared by the record's writer. Head publication times are declared by the head's publisher. The kit reports both as declared. Inclusion in a head is proven; a wall-clock bound rests on witness heads you obtained yourself (--known-heads or --mirror). - Without witness heads, a split view or a rewritten log cannot be detected, and the report says so (NO_WITNESS). - The result covers only the inputs given. A fresh export or newer witness heads can change it. - An erasure is reported as what the server recorded. The kit cannot observe that any data was deleted. - The NAME_MIXED_SCRIPT display warning uses the Unicode Script property where the reference uses the word that starts each character's Unicode name. The two can list different scripts, and one can warn where the other does not, also for common names: for example the surname U+4F50 U+3005 U+6728 (the reference warns, the kit does not) and the word U+30B3 U+30FC U+30D2 U+30FC (the reference lists KATAKANA-HIRAGANA, the kit UNKNOWN). The warning never changes the verdict. - The command line's wording for malformed command lines differs from the reference's argparse wording. The verdicts, findings, exit codes and report output do not differ. - The kit does all its work in one synchronous call. In a page, an export with many events can take seconds, and the page waits for it.