verifyhash 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/cli/agent-hook.js +431 -0
- package/docs/ADOPT.md +15 -5
- package/docs/AGENT-HOOK.md +111 -0
- package/docs/ANCHORING.md +43 -22
- package/docs/PUBLISH-VERIFY-VH.md +45 -0
- package/examples/README.md +185 -0
- package/examples/policy.lenient.json +5 -0
- package/examples/policy.strict.json +6 -0
- package/examples/run.js +366 -0
- package/examples/sample-dataset/README.txt +10 -0
- package/examples/sample-dataset/corpus/cc-by-poem.txt +8 -0
- package/examples/sample-dataset/corpus/mit-notes.txt +4 -0
- package/examples/sample-dataset/data/unlabeled.txt +5 -0
- package/examples/sample-dataset/vendored/gpl-snippet.txt +5 -0
- package/examples/sample-dataset.hints.json +7 -0
- package/examples/sample-parcel/data/manifest-of-contents.txt +7 -0
- package/examples/sample-parcel/data/records.csv +4 -0
- package/examples/sample-parcel/delivery-note.txt +9 -0
- package/package.json +26 -3
- package/verifier/README.md +584 -0
- package/verifier/action/README.md +87 -0
- package/verifier/action/action.yml +146 -0
- package/verifier/build-standalone-html.js +1287 -0
- package/verifier/build-standalone.js +989 -0
- package/verifier/ci/journal.generic.sh +96 -0
- package/verifier/ci/journal.github-actions.yml +99 -0
- package/verifier/ci/reproduce-vh.generic.sh +59 -0
- package/verifier/ci/reproduce-vh.github-actions.yml +49 -0
- package/verifier/ci/verify-service.generic.sh +96 -0
- package/verifier/ci/verify-service.github-actions.yml +88 -0
- package/verifier/ci/verify-vh.generic.sh +75 -0
- package/verifier/ci/verify-vh.github-actions.yml +56 -0
- package/verifier/dist/BUILD-PROVENANCE.json +210 -0
- package/verifier/dist/seal-vh-standalone.js +876 -0
- package/verifier/dist/seal-vh-standalone.js.sha256 +1 -0
- package/verifier/dist/verify-vh-standalone.html +3373 -0
- package/verifier/dist/verify-vh-standalone.html.sha256 +1 -0
- package/verifier/dist/verify-vh-standalone.js +5123 -0
- package/verifier/dist/verify-vh-standalone.js.sha256 +1 -0
- package/verifier/lib/canonical.js +141 -0
- package/verifier/lib/keccak.js +30 -0
- package/verifier/lib/keccak256-vendored.js +206 -0
- package/verifier/lib/merkle.js +145 -0
- package/verifier/lib/revocation-core.js +606 -0
- package/verifier/lib/revocation.js +200 -0
- package/verifier/lib/seal-cli.js +374 -0
- package/verifier/lib/seal-evidence.js +237 -0
- package/verifier/lib/secp256k1-recover.js +249 -0
- package/verifier/package.json +39 -0
- package/verifier/verify-vh.js +3376 -0
- package/docs/ADOPTION.json +0 -11
- package/docs/AUDIT.md +0 -55
- package/docs/DECIDE.md +0 -47
- package/docs/DECISIONS-PENDING.md +0 -27
- package/docs/DEPLOY-PUBLIC-SITE.md +0 -301
- package/docs/ENGINE-LEDGER.json +0 -12
- package/docs/LOOP-AUDIT-2026-07-03.json +0 -580
- package/docs/LOOP-HARDENING-PLAN.md +0 -44
- package/docs/METRICS.jsonl +0 -31
- package/docs/MORNING.md +0 -204
- package/docs/STRATEGY-ARCHIVE.md +0 -5055
- package/docs/SUPERVISOR-RUNBOOK.md +0 -52
- package/docs/USAGE-BUDGET.json +0 -121
|
@@ -0,0 +1 @@
|
|
|
1
|
+
6de719e11e3885c733aca329a10e4d25f0dff1e5f24d85934a43b3f766d4c0b0 verify-vh-standalone.js
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// verifier/lib/canonical.js — INDEPENDENT canonical UNSIGNED serialization.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS
|
|
6
|
+
// For the independent `verifier/` to sign/hash over BYTE-IDENTICAL input to the production path, it must
|
|
7
|
+
// reproduce the family's canonical UNSIGNED serialization itself — WITHOUT importing the producer code in
|
|
8
|
+
// `cli/`. If the verifier imported `cli/dataset.js`'s `serializeAttestation`, a cross-check would be
|
|
9
|
+
// circular (it would be comparing a function to itself). So this file re-derives the SAME byte string,
|
|
10
|
+
// from first principles, and the cross-check test asserts it equals what the producer emits.
|
|
11
|
+
//
|
|
12
|
+
// THE CANONICAL CONVENTION (must match cli/core/attestation.js + cli/dataset.js#serializeAttestation)
|
|
13
|
+
// * A FIXED key order — NOT JSON.stringify's insertion order by accident, but an EXPLICIT ordered key
|
|
14
|
+
// list per object shape. We emit keys in that exact order.
|
|
15
|
+
// * NO insignificant whitespace (separators ",", ":").
|
|
16
|
+
// * A SINGLE trailing newline ("\n") terminating the document.
|
|
17
|
+
// The result is byte-deterministic: the same logical value always serializes to the same bytes.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Serialize a value to canonical JSON with an EXPLICIT key order, no insignificant whitespace, and NO
|
|
21
|
+
* trailing newline (the newline is the document-level convention added by the envelope serializers below).
|
|
22
|
+
*
|
|
23
|
+
* Key order: when `keyOrder[<path-or-shape>]` is provided we use it; otherwise keys are emitted in the
|
|
24
|
+
* object's own insertion order (matching the producer's explicit object literals, which V8 preserves).
|
|
25
|
+
* Because the producers always build their canonical objects via explicit ordered literals, reproducing
|
|
26
|
+
* that same ordered literal here yields byte-identical output WITHOUT a generic key-sorting pass.
|
|
27
|
+
*
|
|
28
|
+
* This is a minimal, dependency-free JSON emitter that matches JSON.stringify's escaping for the value
|
|
29
|
+
* shapes this family uses (strings, integers, booleans, null, nested objects/arrays).
|
|
30
|
+
*
|
|
31
|
+
* @param {*} value
|
|
32
|
+
* @returns {string} canonical JSON (no trailing newline)
|
|
33
|
+
*/
|
|
34
|
+
function canonicalJson(value) {
|
|
35
|
+
// JSON.stringify with no spacing already emits ","/":" separators and standard string escaping with no
|
|
36
|
+
// insignificant whitespace. The ONLY thing it does not do for us is reorder keys — but the family's
|
|
37
|
+
// canonical objects are built as explicit ordered literals, so insertion order IS the canonical order.
|
|
38
|
+
// We therefore use JSON.stringify directly on a value whose keys are already in canonical order. This is
|
|
39
|
+
// intentionally the SAME primitive the producer uses, but driven from an INDEPENDENTLY constructed,
|
|
40
|
+
// explicitly-ordered object here (so the bytes are reproduced, not imported).
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reproduce the canonical UNSIGNED dataset-attestation bytes, byte-for-byte identical to
|
|
46
|
+
* `cli/dataset.js#serializeAttestation` — WITHOUT importing it.
|
|
47
|
+
*
|
|
48
|
+
* Canonical top-level key order (from the producer's explicit object literal):
|
|
49
|
+
* kind, schemaVersion, note, root, fileCount, manifestDigest, signed, signature
|
|
50
|
+
* then a single trailing newline.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} env a validated UNSIGNED attestation envelope
|
|
53
|
+
* @returns {string} the canonical serialization (newline-terminated)
|
|
54
|
+
*/
|
|
55
|
+
function serializeUnsignedDatasetAttestation(env) {
|
|
56
|
+
if (env == null || typeof env !== "object" || Array.isArray(env)) {
|
|
57
|
+
throw new Error("serializeUnsignedDatasetAttestation requires an attestation envelope object");
|
|
58
|
+
}
|
|
59
|
+
// Build the canonical object via an EXPLICIT ordered literal — independently of the producer.
|
|
60
|
+
const canonical = {
|
|
61
|
+
kind: env.kind,
|
|
62
|
+
schemaVersion: env.schemaVersion,
|
|
63
|
+
note: env.note,
|
|
64
|
+
root: env.root,
|
|
65
|
+
fileCount: env.fileCount,
|
|
66
|
+
manifestDigest: env.manifestDigest,
|
|
67
|
+
signed: env.signed,
|
|
68
|
+
signature: env.signature,
|
|
69
|
+
};
|
|
70
|
+
return canonicalJson(canonical) + "\n";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generic canonical envelope serializer for the family's signed-attestation containers, reproducing
|
|
75
|
+
* `cli/core/attestation.js#serializeSignedAttestation` byte-for-byte WITHOUT importing it.
|
|
76
|
+
*
|
|
77
|
+
* Canonical key order: kind, schemaVersion, note, attestation, signature{scheme,signer,signature}
|
|
78
|
+
* then a single trailing newline.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} container a signed-attestation container
|
|
81
|
+
* @returns {string} the canonical serialization (newline-terminated)
|
|
82
|
+
*/
|
|
83
|
+
function serializeSignedContainer(container) {
|
|
84
|
+
if (container == null || typeof container !== "object" || Array.isArray(container)) {
|
|
85
|
+
throw new Error("serializeSignedContainer requires a signed-attestation container object");
|
|
86
|
+
}
|
|
87
|
+
const sig = container.signature || {};
|
|
88
|
+
const canonical = {
|
|
89
|
+
kind: container.kind,
|
|
90
|
+
schemaVersion: container.schemaVersion,
|
|
91
|
+
note: container.note,
|
|
92
|
+
attestation: container.attestation,
|
|
93
|
+
signature: {
|
|
94
|
+
scheme: sig.scheme,
|
|
95
|
+
signer: sig.signer,
|
|
96
|
+
signature: sig.signature,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return canonicalJson(canonical) + "\n";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The reserved relPath of the synthetic HEADER leaf a reconciliation seal binds its verdict + input
|
|
103
|
+
// role partition into, byte-identical to trustledger/seal.js#SEAL_HEADER_RELPATH. A real file may not
|
|
104
|
+
// occupy it; the verifier folds the header content in under this relPath when re-deriving the root.
|
|
105
|
+
const TRUST_SEAL_HEADER_RELPATH = "__trustledger.seal-header__v1";
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reproduce the canonical "content" bytes of a reconciliation seal's verdict/role HEADER entry, byte-for-
|
|
109
|
+
* byte identical to trustledger/seal.js#_headerBytes — WITHOUT importing it. The header binds the recorded
|
|
110
|
+
* verdict (pass/reportDate/period) + each input's logical role→relPath into the SAME committed Merkle
|
|
111
|
+
* root as the files, so a verdict/role edit changes the header content -> its leaf -> the root.
|
|
112
|
+
*
|
|
113
|
+
* Canonical layout (FIXED key order, no insignificant whitespace, roles sorted by role):
|
|
114
|
+
* { v: 1, verdict: { pass, reportDate, period }, roles: [{ role, relPath }, ...] }
|
|
115
|
+
*
|
|
116
|
+
* @param {object} verdict { pass, reportDate, period }
|
|
117
|
+
* @param {{role:string, relPath:string}[]} inputs the seal's input role bindings
|
|
118
|
+
* @returns {Buffer} the canonical UTF-8 header content
|
|
119
|
+
*/
|
|
120
|
+
function trustSealHeaderBytes(verdict, inputs) {
|
|
121
|
+
const canonical = {
|
|
122
|
+
v: 1,
|
|
123
|
+
verdict: {
|
|
124
|
+
pass: verdict.pass,
|
|
125
|
+
reportDate: verdict.reportDate,
|
|
126
|
+
period: verdict.period == null ? null : String(verdict.period),
|
|
127
|
+
},
|
|
128
|
+
roles: inputs
|
|
129
|
+
.map((i) => ({ role: i.role, relPath: i.relPath }))
|
|
130
|
+
.sort((a, b) => (a.role < b.role ? -1 : a.role > b.role ? 1 : 0)),
|
|
131
|
+
};
|
|
132
|
+
return Buffer.from(JSON.stringify(canonical), "utf8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
canonicalJson,
|
|
137
|
+
serializeUnsignedDatasetAttestation,
|
|
138
|
+
serializeSignedContainer,
|
|
139
|
+
TRUST_SEAL_HEADER_RELPATH,
|
|
140
|
+
trustSealHeaderBytes,
|
|
141
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// verifier/lib/keccak.js — the keccak256 used by the INDEPENDENT verifier core.
|
|
4
|
+
//
|
|
5
|
+
// WHY A SHIM (and not a re-implementation)
|
|
6
|
+
// The whole point of `verifier/` is to be an INDEPENDENT re-derivation of the family's crypto that can
|
|
7
|
+
// be CROSS-CHECKED against the production (ethers) path, so the two can never silently diverge. keccak256
|
|
8
|
+
// is a fixed, standardized permutation; re-rolling it by hand would add risk, not independence. So we
|
|
9
|
+
// take it from `js-sha3` — an audited, dependency-free implementation that is ALREADY a direct dependency
|
|
10
|
+
// of this project (package.json), and is the SAME primitive ethers itself uses under the hood. The
|
|
11
|
+
// independence that matters for the anti-divergence guard is the EIP-191 framing + secp256k1 recovery +
|
|
12
|
+
// canonical serialization, all of which `verifier/` implements WITHOUT ethers/hardhat. This file has NO
|
|
13
|
+
// dependency on `cli/` or `trustledger/`.
|
|
14
|
+
|
|
15
|
+
const { keccak256: keccakHex } = require("js-sha3");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* keccak256 over a byte buffer, returning a 32-byte Buffer.
|
|
19
|
+
* @param {Buffer|Uint8Array} bytes
|
|
20
|
+
* @returns {Buffer} the 32-byte digest
|
|
21
|
+
*/
|
|
22
|
+
function keccak256(bytes) {
|
|
23
|
+
if (!(bytes instanceof Uint8Array) && !Buffer.isBuffer(bytes)) {
|
|
24
|
+
throw new TypeError("keccak256 requires a Buffer/Uint8Array of input bytes");
|
|
25
|
+
}
|
|
26
|
+
// js-sha3's keccak256 accepts a byte array and returns a lowercase hex string (no 0x).
|
|
27
|
+
return Buffer.from(keccakHex.create().update(bytes).hex(), "hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { keccak256 };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// verifier/lib/keccak256-vendored.js — a PURE-JS, ZERO-DEPENDENCY keccak256 (T-35.1).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS FILE EXISTS
|
|
6
|
+
// The whole point of the free verifier is "save ONE file, run it with `node`, no `npm install`, audit it
|
|
7
|
+
// in one sitting." The in-tree verifier core takes keccak256 from `js-sha3` (verifier/lib/keccak.js) — an
|
|
8
|
+
// audited, dependency-free package that is already a project dependency — which is correct for the
|
|
9
|
+
// IN-TREE path. But `js-sha3` is still a RUNTIME dependency: a third party handed a single sealed packet
|
|
10
|
+
// would have to `npm install` it. This module is the LAST piece needed to inline the verifier into one
|
|
11
|
+
// self-contained file: a from-scratch keccak256 that `require`s NOTHING (no `js-sha3`, no Node core, no
|
|
12
|
+
// relative module). It is ADDITIVE — keccak.js and verifier/package.json's `dependencies: ["js-sha3"]`
|
|
13
|
+
// are deliberately left UNCHANGED so the existing tree + isolation test stay green.
|
|
14
|
+
//
|
|
15
|
+
// CORRECTNESS, NOT NOVELTY
|
|
16
|
+
// keccak256 is the FIXED, standardized Keccak[c=512] sponge over the Keccak-f[1600] permutation with the
|
|
17
|
+
// ORIGINAL Keccak padding (a single 0x01 domain byte, NOT SHA3's 0x06) and a 256-bit squeeze — exactly
|
|
18
|
+
// what Ethereum/ethers and `js-sha3.keccak256` compute. This is a textbook implementation of FIPS-202's
|
|
19
|
+
// Keccak-f (theta, rho, pi, chi, iota) done with 32-bit lane halves (lo/hi) so it runs on plain JS numbers
|
|
20
|
+
// with no BigInt and no 64-bit-int dependency. test/verifier.keccak-vendored.test.js proves byte-identical
|
|
21
|
+
// output vs BOTH `js-sha3` AND the production `ethers` keccak path across the empty input, the known
|
|
22
|
+
// vectors, and ≥500 random buffers — a single mismatch FAILS. So this is independent CODE but never an
|
|
23
|
+
// independent ALGORITHM: it cannot silently diverge from the standard.
|
|
24
|
+
//
|
|
25
|
+
// REQUIRES NOTHING: a grep of this source finds no CommonJS require call and no bare-name import.
|
|
26
|
+
// (Intentional — this property is asserted by test/verifier.keccak-vendored.test.js.)
|
|
27
|
+
|
|
28
|
+
// ---- Keccak-f[1600] round constants, split into 32-bit (hi, lo) halves --------------------------------
|
|
29
|
+
// The 24 RC[i] are the canonical Keccak iota constants; here each 64-bit constant is pre-split so we never
|
|
30
|
+
// need a 64-bit integer type. RC_HI[i] is bits 63..32, RC_LO[i] is bits 31..0.
|
|
31
|
+
const RC_HI = [
|
|
32
|
+
0x00000000, 0x00000000, 0x80000000, 0x80000000, 0x00000000, 0x00000000, 0x80000000, 0x80000000,
|
|
33
|
+
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x80000000, 0x80000000, 0x80000000,
|
|
34
|
+
0x80000000, 0x80000000, 0x00000000, 0x80000000, 0x80000000, 0x80000000, 0x00000000, 0x80000000,
|
|
35
|
+
];
|
|
36
|
+
const RC_LO = [
|
|
37
|
+
0x00000001, 0x00008082, 0x0000808a, 0x80008000, 0x0000808b, 0x80000001, 0x80008081, 0x00008009,
|
|
38
|
+
0x0000008a, 0x00000088, 0x80008009, 0x8000000a, 0x8000808b, 0x0000008b, 0x00008089, 0x00008003,
|
|
39
|
+
0x00008002, 0x00000080, 0x0000800a, 0x8000000a, 0x80008081, 0x00008080, 0x80000001, 0x80008008,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Rotation offsets r[x,y] for the rho step, indexed by lane number (x + 5*y). Lane 0 is never rotated.
|
|
43
|
+
const RHO = [
|
|
44
|
+
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
|
|
45
|
+
];
|
|
46
|
+
// pi permutation: destination lane for each source lane. pi maps (x,y) -> (y, 2x+3y), so source lane
|
|
47
|
+
// (x + 5y) is written to lane (y + 5*((2x+3y) mod 5)); PI[src] = dst.
|
|
48
|
+
const PI = [
|
|
49
|
+
0, 10, 20, 5, 15, 16, 1, 11, 21, 6, 7, 17, 2, 12, 22, 23, 8, 18, 3, 13, 14, 24, 9, 19, 4,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// The state is 25 lanes; we hold each lane as two 32-bit halves in parallel arrays sLo/sHi (index = lane).
|
|
53
|
+
|
|
54
|
+
// Keccak-f[1600] permutation, in place, on (sLo, sHi). 24 rounds of theta, rho+pi, chi, iota.
|
|
55
|
+
function keccakF(sLo, sHi) {
|
|
56
|
+
const bcLo = new Array(5);
|
|
57
|
+
const bcHi = new Array(5);
|
|
58
|
+
const tLo = new Array(25);
|
|
59
|
+
const tHi = new Array(25);
|
|
60
|
+
|
|
61
|
+
for (let round = 0; round < 24; round++) {
|
|
62
|
+
// --- theta ---
|
|
63
|
+
for (let x = 0; x < 5; x++) {
|
|
64
|
+
bcLo[x] = sLo[x] ^ sLo[x + 5] ^ sLo[x + 10] ^ sLo[x + 15] ^ sLo[x + 20];
|
|
65
|
+
bcHi[x] = sHi[x] ^ sHi[x + 5] ^ sHi[x + 10] ^ sHi[x + 15] ^ sHi[x + 20];
|
|
66
|
+
}
|
|
67
|
+
for (let x = 0; x < 5; x++) {
|
|
68
|
+
// d = bc[x-1] XOR rotl1(bc[x+1])
|
|
69
|
+
const x1 = (x + 1) % 5;
|
|
70
|
+
const x4 = (x + 4) % 5;
|
|
71
|
+
const rotLo = ((bcLo[x1] << 1) | (bcHi[x1] >>> 31)) >>> 0;
|
|
72
|
+
const rotHi = ((bcHi[x1] << 1) | (bcLo[x1] >>> 31)) >>> 0;
|
|
73
|
+
const dLo = (bcLo[x4] ^ rotLo) >>> 0;
|
|
74
|
+
const dHi = (bcHi[x4] ^ rotHi) >>> 0;
|
|
75
|
+
for (let y = 0; y < 25; y += 5) {
|
|
76
|
+
sLo[x + y] = (sLo[x + y] ^ dLo) >>> 0;
|
|
77
|
+
sHi[x + y] = (sHi[x + y] ^ dHi) >>> 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- rho + pi --- (write permuted, rotated lanes into t)
|
|
82
|
+
for (let i = 0; i < 25; i++) {
|
|
83
|
+
const r = RHO[i];
|
|
84
|
+
const dest = PI[i];
|
|
85
|
+
let outLo, outHi;
|
|
86
|
+
if (r === 0) {
|
|
87
|
+
outLo = sLo[i];
|
|
88
|
+
outHi = sHi[i];
|
|
89
|
+
} else if (r < 32) {
|
|
90
|
+
outLo = ((sLo[i] << r) | (sHi[i] >>> (32 - r))) >>> 0;
|
|
91
|
+
outHi = ((sHi[i] << r) | (sLo[i] >>> (32 - r))) >>> 0;
|
|
92
|
+
} else if (r === 32) {
|
|
93
|
+
outLo = sHi[i];
|
|
94
|
+
outHi = sLo[i];
|
|
95
|
+
} else {
|
|
96
|
+
const rr = r - 32;
|
|
97
|
+
outLo = ((sHi[i] << rr) | (sLo[i] >>> (32 - rr))) >>> 0;
|
|
98
|
+
outHi = ((sLo[i] << rr) | (sHi[i] >>> (32 - rr))) >>> 0;
|
|
99
|
+
}
|
|
100
|
+
tLo[dest] = outLo;
|
|
101
|
+
tHi[dest] = outHi;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- chi --- a[x] = t[x] XOR ((NOT t[x+1]) AND t[x+2]), per row
|
|
105
|
+
for (let y = 0; y < 25; y += 5) {
|
|
106
|
+
for (let x = 0; x < 5; x++) {
|
|
107
|
+
const x1 = y + ((x + 1) % 5);
|
|
108
|
+
const x2 = y + ((x + 2) % 5);
|
|
109
|
+
sLo[y + x] = (tLo[y + x] ^ (~tLo[x1] & tLo[x2])) >>> 0;
|
|
110
|
+
sHi[y + x] = (tHi[y + x] ^ (~tHi[x1] & tHi[x2])) >>> 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- iota ---
|
|
115
|
+
sLo[0] = (sLo[0] ^ RC_LO[round]) >>> 0;
|
|
116
|
+
sHi[0] = (sHi[0] ^ RC_HI[round]) >>> 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// keccak256 over `bytes` (a Uint8Array/Buffer or array of byte values), returning a 32-byte Uint8Array.
|
|
121
|
+
// Rate r = 1088 bits = 136 bytes (c = 512), original Keccak padding (0x01 .. 0x80), 256-bit output.
|
|
122
|
+
function keccak256Bytes(bytes) {
|
|
123
|
+
const RATE = 136; // bytes absorbed per permutation
|
|
124
|
+
const sLo = new Array(25).fill(0);
|
|
125
|
+
const sHi = new Array(25).fill(0);
|
|
126
|
+
|
|
127
|
+
// Build the padded message: append a single 0x01 domain/pad start byte, zero-fill, set the high bit
|
|
128
|
+
// (0x80) of the final rate block. (If the 0x01 lands on the last byte of a block, it merges to 0x81.)
|
|
129
|
+
const inLen = bytes.length;
|
|
130
|
+
const padLen = RATE - (inLen % RATE); // 1..RATE, guarantees room for the 0x01 and 0x80 markers
|
|
131
|
+
const total = inLen + padLen;
|
|
132
|
+
const msg = new Uint8Array(total);
|
|
133
|
+
for (let i = 0; i < inLen; i++) msg[i] = bytes[i] & 0xff;
|
|
134
|
+
msg[inLen] = 0x01; // start of the original-Keccak pad (NOT SHA3's 0x06)
|
|
135
|
+
msg[total - 1] = (msg[total - 1] | 0x80) & 0xff; // final-block high bit
|
|
136
|
+
|
|
137
|
+
// Absorb: XOR each RATE-byte block into the state (little-endian lanes) and permute.
|
|
138
|
+
for (let off = 0; off < total; off += RATE) {
|
|
139
|
+
for (let i = 0; i < RATE; i += 8) {
|
|
140
|
+
const lane = i >> 3; // lane index within the rate region (0..16), block-relative
|
|
141
|
+
const b = off + i;
|
|
142
|
+
const lo =
|
|
143
|
+
((msg[b] | (msg[b + 1] << 8) | (msg[b + 2] << 16) | (msg[b + 3] << 24)) >>> 0);
|
|
144
|
+
const hi =
|
|
145
|
+
((msg[b + 4] | (msg[b + 5] << 8) | (msg[b + 6] << 16) | (msg[b + 7] << 24)) >>> 0);
|
|
146
|
+
sLo[lane] = (sLo[lane] ^ lo) >>> 0;
|
|
147
|
+
sHi[lane] = (sHi[lane] ^ hi) >>> 0;
|
|
148
|
+
}
|
|
149
|
+
keccakF(sLo, sHi);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Squeeze 256 bits = 32 bytes = the first 4 lanes (little-endian), no further permutation needed.
|
|
153
|
+
const out = new Uint8Array(32);
|
|
154
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
155
|
+
const lo = sLo[lane];
|
|
156
|
+
const hi = sHi[lane];
|
|
157
|
+
const base = lane * 8;
|
|
158
|
+
out[base] = lo & 0xff;
|
|
159
|
+
out[base + 1] = (lo >>> 8) & 0xff;
|
|
160
|
+
out[base + 2] = (lo >>> 16) & 0xff;
|
|
161
|
+
out[base + 3] = (lo >>> 24) & 0xff;
|
|
162
|
+
out[base + 4] = hi & 0xff;
|
|
163
|
+
out[base + 5] = (hi >>> 8) & 0xff;
|
|
164
|
+
out[base + 6] = (hi >>> 16) & 0xff;
|
|
165
|
+
out[base + 7] = (hi >>> 24) & 0xff;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Lowercase hex (no 0x prefix) of a byte array — used by the hex-string entry point.
|
|
171
|
+
function toHex(bytes) {
|
|
172
|
+
let s = "";
|
|
173
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
174
|
+
const b = bytes[i] & 0xff;
|
|
175
|
+
s += (b < 16 ? "0" : "") + b.toString(16);
|
|
176
|
+
}
|
|
177
|
+
return s;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* keccak256 over a byte buffer.
|
|
182
|
+
* @param {Uint8Array|Buffer|number[]} bytes input bytes
|
|
183
|
+
* @returns {Uint8Array} the 32-byte digest
|
|
184
|
+
*/
|
|
185
|
+
function keccak256(bytes) {
|
|
186
|
+
if (
|
|
187
|
+
!(bytes instanceof Uint8Array) &&
|
|
188
|
+
!Array.isArray(bytes) &&
|
|
189
|
+
!(typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(bytes))
|
|
190
|
+
) {
|
|
191
|
+
throw new TypeError("keccak256 requires a Uint8Array/Buffer/byte-array of input bytes");
|
|
192
|
+
}
|
|
193
|
+
return keccak256Bytes(bytes);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* keccak256 over a byte buffer, returned as a lowercase hex string WITHOUT a 0x prefix
|
|
198
|
+
* (matching `js-sha3`'s keccak256().hex() output, for drop-in cross-checking).
|
|
199
|
+
* @param {Uint8Array|Buffer|number[]} bytes input bytes
|
|
200
|
+
* @returns {string} 64-char lowercase hex
|
|
201
|
+
*/
|
|
202
|
+
function keccak256Hex(bytes) {
|
|
203
|
+
return toHex(keccak256(bytes));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { keccak256, keccak256Hex };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// verifier/lib/merkle.js — INDEPENDENT re-derivation of the family's path-bound, domain-separated
|
|
4
|
+
// Merkle convention, using ONLY ./keccak (js-sha3). NO ethers, NO hardhat, NO require back into cli/.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS
|
|
7
|
+
// To verify an evidence seal / reconciliation seal / proof bundle OFFLINE without the producer stack,
|
|
8
|
+
// the independent verifier must RE-DERIVE the same per-file leaves and the same Merkle root the
|
|
9
|
+
// producer (cli/hash.js) computes. cli/hash.js uses `ethers` (keccak256/concat/toUtf8Bytes), which the
|
|
10
|
+
// verifier explicitly refuses to depend on. So this file reproduces the EXACT byte composition of
|
|
11
|
+
// pathLeaf / leafHash / nodeHash / buildTree from first principles — and test/verifier.cli.test.js
|
|
12
|
+
// cross-checks the result is byte-identical to the producer's. The two can never silently diverge.
|
|
13
|
+
//
|
|
14
|
+
// THE CONVENTION (must match cli/hash.js VERBATIM)
|
|
15
|
+
// * content digest c = keccak256(file bytes)
|
|
16
|
+
// * DIR_LEAF_DOMAIN = keccak256("verifyhash/dir-leaf/v1") (a fixed 32-byte prefix)
|
|
17
|
+
// * path-bound leaf pathLeaf = keccak256(DIR_LEAF_DOMAIN ++ utf8(relPath) ++ 0x00 ++ c)
|
|
18
|
+
// * tagged leaf leafHash = keccak256(0x00 ++ leaf)
|
|
19
|
+
// * interior node nodeHash = keccak256(0x01 ++ min(a,b) ++ max(a,b)) (sorted 32-byte pair)
|
|
20
|
+
// * tree sorted-leaf, "duplicate the lone odd node" pairing (OpenZeppelin style)
|
|
21
|
+
// relPath is normalized with no leading "./", exactly as the producer's toPosixRel does. CRUCIALLY
|
|
22
|
+
// this must be BYTE-FOR-BYTE the producer's normalization (cli/hash.js#toPosixRel) — see toPosixRel
|
|
23
|
+
// below — or the verifier would re-derive a DIFFERENT root than the producer sealed for some input
|
|
24
|
+
// class and would either falsely reject a genuine artifact or falsely accept the wrong one.
|
|
25
|
+
|
|
26
|
+
const { keccak256 } = require("./keccak");
|
|
27
|
+
|
|
28
|
+
// Domain tags, byte-identical to ContributionRegistry / cli/hash.js LEAF_TAG / NODE_TAG.
|
|
29
|
+
const LEAF_TAG = Buffer.from([0x00]);
|
|
30
|
+
const NODE_TAG = Buffer.from([0x01]);
|
|
31
|
+
const PATH_SEP = Buffer.from([0x00]);
|
|
32
|
+
|
|
33
|
+
// The fixed, versioned domain prefix for path-bound directory leaves: keccak256 of the ASCII tag.
|
|
34
|
+
const DIR_LEAF_DOMAIN_STR = "verifyhash/dir-leaf/v1";
|
|
35
|
+
const DIR_LEAF_DOMAIN = keccak256(Buffer.from(DIR_LEAF_DOMAIN_STR, "utf8")); // 32-byte Buffer
|
|
36
|
+
|
|
37
|
+
const HEX32_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
38
|
+
|
|
39
|
+
// 0x-hex string (no 0x, lowercase) <-> 32-byte Buffer.
|
|
40
|
+
function hexToBuf32(hex) {
|
|
41
|
+
if (typeof hex !== "string" || !HEX32_RE.test(hex)) {
|
|
42
|
+
throw new Error(`expected a 0x-prefixed 32-byte hex string, got: ${String(hex)}`);
|
|
43
|
+
}
|
|
44
|
+
return Buffer.from(hex.slice(2), "hex");
|
|
45
|
+
}
|
|
46
|
+
function bufToHex(buf) {
|
|
47
|
+
return "0x" + Buffer.from(buf).toString("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** keccak256 of raw bytes, returned as a 0x-prefixed 32-byte hex string (matches cli/hash.js hashBytes). */
|
|
51
|
+
function hashBytes(bytes) {
|
|
52
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
53
|
+
return bufToHex(keccak256(buf));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize a relPath EXACTLY as the producer (cli/hash.js#toPosixRel) does, so the verifier
|
|
58
|
+
* re-derives the IDENTICAL root the producer sealed. The producer is `split(path.sep).join("/")`
|
|
59
|
+
* then `.replace(/^\.\//, "")`. The artifacts the verifier reads carry relPaths the producer wrote,
|
|
60
|
+
* and those are produced on POSIX hosts (cli/evidence.js#loadDirEntries does the same `path.sep`
|
|
61
|
+
* split) — where `path.sep === "/"`, so the split/join is a no-op and a literal backslash byte is a
|
|
62
|
+
* CONTENT byte that survives into the hash. We therefore must NOT collapse backslashes: a previous
|
|
63
|
+
* version unconditionally mapped "\\"->"/", which made the verifier hash `a/b.txt` while the producer
|
|
64
|
+
* hashed `a\b.txt` — a silent root divergence that could falsely REJECT a genuine backslash-named
|
|
65
|
+
* directory or falsely ACCEPT one where `a/b.txt` and `a\b.txt` collide. All we strip is the leading
|
|
66
|
+
* "./", which the producer also strips on every host. (Windows-authored relPaths, if ever needed,
|
|
67
|
+
* must be converted to "/" on BOTH the producer and verifier sides identically — not only here.)
|
|
68
|
+
*/
|
|
69
|
+
function toPosixRel(relPath) {
|
|
70
|
+
return String(relPath).replace(/^\.\//, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* pathLeaf(relPath, contentDigest) = keccak256(DIR_LEAF_DOMAIN ++ utf8(relPath) ++ 0x00 ++ c).
|
|
75
|
+
* @param {string} relPath
|
|
76
|
+
* @param {string} contentDigest 0x bytes32
|
|
77
|
+
* @returns {string} 0x bytes32
|
|
78
|
+
*/
|
|
79
|
+
function pathLeaf(relPath, contentDigest) {
|
|
80
|
+
const relBytes = Buffer.from(toPosixRel(relPath), "utf8");
|
|
81
|
+
const c = hexToBuf32(contentDigest);
|
|
82
|
+
return bufToHex(keccak256(Buffer.concat([DIR_LEAF_DOMAIN, relBytes, PATH_SEP, c])));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** leafHash(c) = keccak256(LEAF_TAG ++ c). */
|
|
86
|
+
function leafHash(c) {
|
|
87
|
+
return bufToHex(keccak256(Buffer.concat([LEAF_TAG, hexToBuf32(c)])));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** nodeHash(a,b) = keccak256(NODE_TAG ++ min(a,b) ++ max(a,b)) comparing as 32-byte big-endian values. */
|
|
91
|
+
function nodeHash(a, b) {
|
|
92
|
+
const A = hexToBuf32(a);
|
|
93
|
+
const B = hexToBuf32(b);
|
|
94
|
+
const [lo, hi] = Buffer.compare(A, B) <= 0 ? [A, B] : [B, A];
|
|
95
|
+
return bufToHex(keccak256(Buffer.concat([NODE_TAG, lo, hi])));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the sorted-leaf, domain-separated Merkle root from an array of per-file PATH-BOUND leaves
|
|
100
|
+
* (the same values pathLeaf produces). Leaves are sorted ascending by their 32-byte value, tagged via
|
|
101
|
+
* leafHash, then folded with nodeHash, pairing a lone odd node with itself — byte-identical to
|
|
102
|
+
* cli/hash.js buildTree's root.
|
|
103
|
+
* @param {string[]} leaves array of 0x bytes32 path-bound leaves
|
|
104
|
+
* @returns {string} the 0x bytes32 root
|
|
105
|
+
*/
|
|
106
|
+
function rootFromLeaves(leaves) {
|
|
107
|
+
if (!Array.isArray(leaves) || leaves.length === 0) {
|
|
108
|
+
throw new Error("cannot build a Merkle tree from zero leaves");
|
|
109
|
+
}
|
|
110
|
+
const sorted = leaves
|
|
111
|
+
.slice()
|
|
112
|
+
.sort((a, b) => Buffer.compare(hexToBuf32(a), hexToBuf32(b)));
|
|
113
|
+
let layer = sorted.map((c) => leafHash(c));
|
|
114
|
+
while (layer.length > 1) {
|
|
115
|
+
const next = [];
|
|
116
|
+
for (let i = 0; i < layer.length; i += 2) {
|
|
117
|
+
const right = i + 1 < layer.length ? layer[i + 1] : layer[i];
|
|
118
|
+
next.push(nodeHash(layer[i], right));
|
|
119
|
+
}
|
|
120
|
+
layer = next;
|
|
121
|
+
}
|
|
122
|
+
return layer[0];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Re-derive the top-level root from a flat list of { relPath, contentHash } — the SAME computation the
|
|
127
|
+
* seal cores use: pathLeaf each, then rootFromLeaves. PURE.
|
|
128
|
+
* @param {{relPath:string, contentHash:string}[]} flat
|
|
129
|
+
* @returns {string} 0x bytes32 root
|
|
130
|
+
*/
|
|
131
|
+
function rootFromFlat(flat) {
|
|
132
|
+
return rootFromLeaves(flat.map((e) => pathLeaf(e.relPath, e.contentHash)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
HEX32_RE,
|
|
137
|
+
DIR_LEAF_DOMAIN_STR,
|
|
138
|
+
hashBytes,
|
|
139
|
+
toPosixRel,
|
|
140
|
+
pathLeaf,
|
|
141
|
+
leafHash,
|
|
142
|
+
nodeHash,
|
|
143
|
+
rootFromLeaves,
|
|
144
|
+
rootFromFlat,
|
|
145
|
+
};
|