verifyhash 0.1.0

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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
@@ -0,0 +1,482 @@
1
+ "use strict";
2
+
3
+ // cli/core/timestamp.js — the GENERIC detached-timestamp CONTAINER engine for the product family.
4
+ //
5
+ // WHY THIS EXISTS (T-20.2, EPIC-20)
6
+ // cli/core/attestation.js wraps a canonical UNSIGNED attestation in a detached SIGNATURE — "the
7
+ // publisher SAYS this payload existed". The honestly-stronger claim a due-diligence / EU-AI-Act reviewer
8
+ // ultimately wants is "an INDEPENDENT third party attests this exact payload existed by time T". RFC-3161
9
+ // delivers that: you send a hash (the messageImprint) to a Time-Stamping Authority (TSA), it returns a
10
+ // signed TimeStampToken whose embedded TSTInfo binds that hash to a genTime. This module is the
11
+ // wrap-don't-edit CONTAINER for that token — the EXACT sibling of `signAttestation`'s envelope, but for a
12
+ // timestamp instead of a signature: it embeds the canonical UNSIGNED attestation bytes verbatim and
13
+ // attaches the TSA's RFC-3161 token, bound to the SHA-256 digest OF THOSE EXACT BYTES.
14
+ //
15
+ // PARAMETERIZED BY THE PRODUCT'S FRAMING — exactly like attestation.js.
16
+ // Each product (DataLedger, ProofParcel) supplies ONLY its container `kind`/`schemaVersion`/`note` plus
17
+ // its OWN unsigned-payload codec (`validateUnsigned` + `serializeUnsigned`). The core does the shared
18
+ // machinery: assemble the container, re-validate the embedded canonical attestation (the SAME
19
+ // wrap-don't-edit invariant the signed envelope enforces), parse the token via cli/core/rfc3161.js, and
20
+ // confirm `bindsDigest(token, digest)` AND `digest === sha256(canonical attestation bytes)`. No back-edge
21
+ // (the core never requires a product module).
22
+ //
23
+ // THE DIGEST IS SHA-256 — NOT the project's keccak256 manifestDigest. (Load-bearing.)
24
+ // RFC-3161 TSAs stamp a `messageImprint` over a STANDARD hash. SHA-256 is universal; keccak256 is
25
+ // non-standard and most TSAs will reject it. So the timestamp digest is a FRESH `sha256(utf8(canonical
26
+ // attestation string))` computed via Node's crypto.createHash("sha256") — it is the digest the BUYER can
27
+ // re-derive from the embedded canonical bytes with any standard tool, and the digest the human submits to
28
+ // their TSA. We do NOT reuse the keccak `manifestDigest` (which lives INSIDE the attestation payload and
29
+ // is non-standard).
30
+ //
31
+ // SCOPE / TRUST (honest about it). The token's AUTHENTICITY — that the bytes really came from a TSA you
32
+ // trust and weren't forged — is the HUMAN out-of-band trust anchor (validate the TSA cert chain / CMS
33
+ // signature with `openssl ts -verify`, exactly as the signed envelope pins the signer ADDRESS out of
34
+ // band). This module proves the BINDING (which digest/genTime the token asserts over the buyer's own
35
+ // re-derivable bytes), NOT the authenticity of the asserting party. It performs NO network and holds NO
36
+ // key.
37
+
38
+ const crypto = require("crypto");
39
+ const rfc3161 = require("./rfc3161");
40
+
41
+ // The detached timestamp schemes this build understands. EXACTLY one — RFC-3161 — and the hash the
42
+ // messageImprint is computed under is SHA-256 (universal across TSAs). Frozen so they can't drift.
43
+ const TIMESTAMP_SCHEMES = Object.freeze(["rfc3161"]);
44
+ const TIMESTAMP_HASH_ALGORITHMS = Object.freeze(["sha256"]);
45
+
46
+ // The dotted-decimal OID a SHA-256 messageImprint hashAlgorithm carries, sourced from the rfc3161 reader so
47
+ // the OID can never drift between the reader and this container.
48
+ const SHA256_OID = rfc3161.OID.sha256;
49
+
50
+ // A 0x-OPTIONAL, lowercase, EVEN-length, 32-byte (64-hex-char) digest. SHA-256 is exactly 32 bytes. The
51
+ // container stores the digest WITHOUT a 0x prefix (it is a standard hash imprint, the form a TSA query/
52
+ // `openssl ts` speaks), lowercase, for byte-determinism (an indexer keys on the exact bytes).
53
+ const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
54
+
55
+ // A base64 string (the DER token). Permissive on read (whitespace tolerated by the rfc3161 reader's toBuf);
56
+ // the container stores it as canonical base64 with NO whitespace so two structurally identical containers
57
+ // over the same token serialize identically.
58
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
59
+
60
+ /**
61
+ * Compute the FRESH SHA-256 digest (lowercase hex, no 0x) over the canonical UNSIGNED attestation bytes.
62
+ * This is the digest a human submits to their TSA and the digest the token must bind. PURE.
63
+ *
64
+ * @param {string} canonicalBytes the exact canonical UNSIGNED attestation string (serializeUnsigned output)
65
+ * @returns {string} lowercase 64-char hex SHA-256 digest (no 0x)
66
+ */
67
+ function sha256Hex(canonicalBytes) {
68
+ if (typeof canonicalBytes !== "string") {
69
+ throw new Error("sha256Hex requires the canonical attestation bytes as a string");
70
+ }
71
+ return crypto.createHash("sha256").update(canonicalBytes, "utf8").digest("hex");
72
+ }
73
+
74
+ /**
75
+ * Internal: assert a product passed a structurally complete timestamp-container config. The injected
76
+ * `validateUnsigned`/`serializeUnsigned` are how the core enforces the wrap-don't-edit invariant WITHOUT
77
+ * knowing anything product-specific (no `require("../dataset")` back-edge) — IDENTICAL discipline to
78
+ * attestation.js's `_requireCfg`.
79
+ */
80
+ function _requireCfg(cfg) {
81
+ if (!cfg || typeof cfg !== "object") {
82
+ throw new Error(
83
+ "timestamp core requires a { kind, schemaVersion, supportedSchemaVersions, note, validateUnsigned, serializeUnsigned } config"
84
+ );
85
+ }
86
+ if (typeof cfg.kind !== "string" || cfg.kind.length === 0) {
87
+ throw new Error("timestamp core config requires a non-empty string `kind`");
88
+ }
89
+ if (!Array.isArray(cfg.supportedSchemaVersions) || cfg.supportedSchemaVersions.length === 0) {
90
+ throw new Error("timestamp core config requires a non-empty `supportedSchemaVersions` array");
91
+ }
92
+ if (typeof cfg.note !== "string") {
93
+ throw new Error("timestamp core config requires a string `note` (the in-band trust caveat)");
94
+ }
95
+ if (typeof cfg.validateUnsigned !== "function" || typeof cfg.serializeUnsigned !== "function") {
96
+ throw new Error(
97
+ "timestamp core config requires `validateUnsigned` and `serializeUnsigned` functions (the product's UNSIGNED payload codec)"
98
+ );
99
+ }
100
+ if (typeof cfg.label !== "string" && cfg.label !== undefined) {
101
+ throw new Error("timestamp core config `label`, when present, must be a string");
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Strictly validate a parsed DETACHED-TIMESTAMP container against a product's framing. Throws an Error
107
+ * describing the FIRST problem; never mutates and never fills defaults. REJECTS: a wrong kind/schemaVersion/
108
+ * note, a non-string embedded `attestation`, an embedded attestation that does not re-validate as a sound
109
+ * UNSIGNED payload OR is not byte-for-byte canonical (the wrap-don't-edit invariant), a malformed
110
+ * `timestamp` block (wrong scheme/hashAlgorithm, non-hex digest, non-base64 token), a token that does not
111
+ * PARSE as RFC-3161, a token whose messageImprint does not BIND the recorded digest, or a recorded `digest`
112
+ * that is NOT sha256(canonical attestation bytes). It NEVER half-accepts.
113
+ *
114
+ * @param {any} obj
115
+ * @param {object} cfg the product's timestamp-container framing (see buildTimestampContainer)
116
+ * @returns {object} the same object, if valid
117
+ */
118
+ function validateTimestampContainer(obj, cfg) {
119
+ _requireCfg(cfg);
120
+ const label = cfg.label || "timestamped dataset attestation";
121
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
122
+ throw new Error(`${label} must be a JSON object`);
123
+ }
124
+ if (obj.kind !== cfg.kind) {
125
+ throw new Error(
126
+ `not a verifyhash ${label} (kind: ${JSON.stringify(obj.kind)}; expected ${JSON.stringify(cfg.kind)})`
127
+ );
128
+ }
129
+ if (!cfg.supportedSchemaVersions.includes(obj.schemaVersion)) {
130
+ throw new Error(
131
+ `unsupported ${label} schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
132
+ `(this build understands ${JSON.stringify(cfg.supportedSchemaVersions)})`
133
+ );
134
+ }
135
+ if (obj.note !== cfg.note) {
136
+ throw new Error(`${label} note must be the standing TIMESTAMP_TRUST_NOTE`);
137
+ }
138
+
139
+ // The embedded UNSIGNED payload is carried as the EXACT canonical bytes serializeUnsigned emits — a
140
+ // STRING, so the timestamped-over bytes are unambiguous. Re-parse and re-validate it with the PRODUCT's
141
+ // strict unsigned validator: it must STILL be signed:false/signature:null. This is the wrap-don't-edit
142
+ // invariant — a timestamp container can never smuggle an edited or already-"signed" payload.
143
+ if (typeof obj.attestation !== "string") {
144
+ throw new Error(`${label} must embed the canonical UNSIGNED attestation as a string \`attestation\``);
145
+ }
146
+ let embedded;
147
+ try {
148
+ embedded = JSON.parse(obj.attestation);
149
+ } catch (e) {
150
+ throw new Error(`embedded attestation is not valid JSON: ${e.message}`);
151
+ }
152
+ cfg.validateUnsigned(embedded);
153
+ const canonical = cfg.serializeUnsigned(embedded);
154
+ if (obj.attestation !== canonical) {
155
+ throw new Error(
156
+ "embedded attestation is not in canonical form (the timestamped-over bytes must be byte-for-byte " +
157
+ "serializeAttestation's output)"
158
+ );
159
+ }
160
+
161
+ // The timestamp block.
162
+ const ts = obj.timestamp;
163
+ if (ts == null || typeof ts !== "object" || Array.isArray(ts)) {
164
+ throw new Error(`${label} timestamp must be a { scheme, hashAlgorithm, digest, token } object`);
165
+ }
166
+ if (!TIMESTAMP_SCHEMES.includes(ts.scheme)) {
167
+ throw new Error(
168
+ `unknown timestamp scheme: ${JSON.stringify(ts.scheme)} ` +
169
+ `(this build understands ${JSON.stringify(TIMESTAMP_SCHEMES)})`
170
+ );
171
+ }
172
+ if (!TIMESTAMP_HASH_ALGORITHMS.includes(ts.hashAlgorithm)) {
173
+ throw new Error(
174
+ `unsupported timestamp hashAlgorithm: ${JSON.stringify(ts.hashAlgorithm)} ` +
175
+ `(this build understands ${JSON.stringify(TIMESTAMP_HASH_ALGORITHMS)}; RFC-3161 TSAs stamp a ` +
176
+ "standard hash — SHA-256 — NOT the project's internal keccak256 manifestDigest)"
177
+ );
178
+ }
179
+ if (typeof ts.digest !== "string" || !SHA256_HEX_RE.test(ts.digest)) {
180
+ throw new Error(
181
+ `timestamp digest must be a 32-byte lowercase SHA-256 hex string (no 0x), got: ${String(ts.digest)}`
182
+ );
183
+ }
184
+ if (typeof ts.token !== "string" || !BASE64_RE.test(ts.token) || ts.token.length === 0) {
185
+ throw new Error(
186
+ `timestamp token must be a non-empty base64 string (the DER-encoded RFC-3161 TimeStampToken), got: ${String(
187
+ ts.token
188
+ )}`
189
+ );
190
+ }
191
+
192
+ // The digest MUST be sha256 of the EXACT embedded canonical bytes — the digest the buyer re-derives. A
193
+ // container whose `digest` does not match the bytes it carries is rejected (it could otherwise point a
194
+ // genuine TSA token at a digest unrelated to the payload).
195
+ const expectedDigest = sha256Hex(canonical);
196
+ if (ts.digest !== expectedDigest) {
197
+ throw new Error(
198
+ "timestamp digest does NOT equal sha256(canonical attestation bytes) — the digest must be over the " +
199
+ `EXACT embedded bytes (expected ${expectedDigest}, got ${ts.digest})`
200
+ );
201
+ }
202
+
203
+ // The token must PARSE as RFC-3161 (a malformed/non-TSTInfo token is rejected here, clearly) AND its
204
+ // messageImprint must BIND the recorded digest under SHA-256. parseTimeStampToken throws on malformed
205
+ // DER; bindsDigest returns false (never throws) for a valid token that simply binds a different digest.
206
+ let parsed;
207
+ try {
208
+ parsed = rfc3161.parseTimeStampToken(ts.token);
209
+ } catch (e) {
210
+ throw new Error(`timestamp token is not a parseable RFC-3161 TimeStampToken: ${e.message}`);
211
+ }
212
+ const bound = rfc3161.bindsDigest({
213
+ token: parsed,
214
+ expectedDigestHex: ts.digest,
215
+ expectedHashOID: SHA256_OID,
216
+ });
217
+ if (!bound) {
218
+ throw new Error(
219
+ "timestamp token does NOT bind the digest: its messageImprint does not stamp " +
220
+ `${ts.digest} under SHA-256 (the TSA stamped a different digest, or a different hash algorithm)`
221
+ );
222
+ }
223
+
224
+ return obj;
225
+ }
226
+
227
+ /**
228
+ * Assemble + validate a DETACHED-TIMESTAMP container from a validated UNSIGNED payload and an RFC-3161
229
+ * token, PARAMETERIZED by the product's container framing. PURE: NO network, NO key. It embeds the EXACT
230
+ * canonical unsigned bytes (cfg.serializeUnsigned(attestation)) as a string, computes the FRESH SHA-256
231
+ * digest OVER those bytes, attaches { scheme:"rfc3161", hashAlgorithm:"sha256", digest, token } (with the
232
+ * token canonicalized to base64), and strictly validates the whole container — so a token that does not
233
+ * bind the re-derived digest is rejected HERE (the build never produces an unbinding container).
234
+ *
235
+ * @param {object} params
236
+ * @param {object} params.attestation a validated UNSIGNED payload (re-validated via cfg.validateUnsigned)
237
+ * @param {Buffer|Uint8Array|string} params.token the RFC-3161 TimeStampToken (raw DER bytes or hex/base64)
238
+ * @param {object} cfg the product's timestamp-container framing
239
+ * @returns {object} a validated detached-timestamp container
240
+ */
241
+ function buildTimestampContainer(params, cfg) {
242
+ _requireCfg(cfg);
243
+ if (!params || typeof params !== "object") {
244
+ throw new Error("buildTimestampContainer requires { attestation, token }");
245
+ }
246
+ const { attestation, token } = params;
247
+ // The embedded payload must itself be a sound UNSIGNED payload before we wrap it (re-validate so a
248
+ // programmatic caller that hand-built one is checked too). validateUnsigned rejects signed:true.
249
+ cfg.validateUnsigned(attestation);
250
+ const canonical = cfg.serializeUnsigned(attestation);
251
+ const digest = sha256Hex(canonical);
252
+
253
+ // Normalize the token to canonical base64 (no whitespace) so the container is byte-deterministic. toBuf
254
+ // accepts a Buffer/Uint8Array OR a hex/base64 string; a non-token throws here clearly. We re-encode the
255
+ // EXACT DER bytes — never a re-DER'd or mutated form.
256
+ if (token == null) throw new Error("buildTimestampContainer requires a `token` (RFC-3161 DER bytes)");
257
+ const der = rfc3161._internal.toBuf(token);
258
+ const tokenB64 = der.toString("base64");
259
+
260
+ const container = {
261
+ kind: cfg.kind,
262
+ schemaVersion: cfg.schemaVersion,
263
+ note: cfg.note,
264
+ attestation: canonical,
265
+ timestamp: {
266
+ scheme: "rfc3161",
267
+ hashAlgorithm: "sha256",
268
+ digest,
269
+ token: tokenB64,
270
+ },
271
+ };
272
+ // Strict validation (re-derives the digest, parses the token, confirms bindsDigest) — a token that does
273
+ // not bind the re-derived digest hard-errors HERE, so a bad handoff never lands an unbinding container.
274
+ validateTimestampContainer(container, cfg);
275
+ return container;
276
+ }
277
+
278
+ /**
279
+ * Serialize a detached-timestamp container to its canonical, byte-deterministic bytes: a FIXED top-level
280
+ * (and timestamp-block) key order, NO insignificant whitespace, a single trailing newline. Two runs over
281
+ * the same inputs produce an identical string.
282
+ * @param {object} container a validated detached-timestamp container
283
+ * @param {object} cfg the product's timestamp-container framing
284
+ * @returns {string} the canonical serialization (newline-terminated)
285
+ */
286
+ function serializeTimestampContainer(container, cfg) {
287
+ validateTimestampContainer(container, cfg);
288
+ const canonical = {
289
+ kind: container.kind,
290
+ schemaVersion: container.schemaVersion,
291
+ note: container.note,
292
+ attestation: container.attestation,
293
+ timestamp: {
294
+ scheme: container.timestamp.scheme,
295
+ hashAlgorithm: container.timestamp.hashAlgorithm,
296
+ digest: container.timestamp.digest,
297
+ token: container.timestamp.token,
298
+ },
299
+ };
300
+ return JSON.stringify(canonical) + "\n";
301
+ }
302
+
303
+ /**
304
+ * Read, parse, and STRICTLY validate the detached-timestamp container at `containerPath` against a
305
+ * product's framing. Round-trips with serializeTimestampContainer. Throws on a missing file or invalid JSON
306
+ * too. The `label` (default "timestamped dataset attestation") parameterizes only the human noun in the I/O
307
+ * error messages so each product's strings stay byte-identical.
308
+ *
309
+ * @param {string} containerPath
310
+ * @param {object} cfg the product's timestamp-container framing
311
+ * @returns {object} the validated container
312
+ */
313
+ function readTimestampContainer(containerPath, cfg) {
314
+ _requireCfg(cfg);
315
+ const fs = require("fs");
316
+ const label = cfg.label || "timestamped dataset attestation";
317
+ if (!containerPath || typeof containerPath !== "string") {
318
+ throw new Error("readTimestampContainer requires a timestamped attestation file path");
319
+ }
320
+ let raw;
321
+ try {
322
+ raw = fs.readFileSync(containerPath, "utf8");
323
+ } catch (e) {
324
+ throw new Error(`cannot read ${label} at ${containerPath}: ${e.message}`);
325
+ }
326
+ let obj;
327
+ try {
328
+ obj = JSON.parse(raw);
329
+ } catch (e) {
330
+ throw new Error(`${label} at ${containerPath} is not valid JSON: ${e.message}`);
331
+ }
332
+ return validateTimestampContainer(obj, cfg);
333
+ }
334
+
335
+ // Possible verify-timestamp verdicts. ACCEPTED = the container is structurally sound, the digest IS
336
+ // sha256(canonical bytes), the token parses + BINDS that digest, and (when a manifest is given) the
337
+ // embedded attestation is byte-identical to the buyer's own re-derived canonical bytes. REJECTED = at
338
+ // least one check failed.
339
+ const VERIFY_TIMESTAMP_VERDICT = Object.freeze({ ACCEPTED: "ACCEPTED", REJECTED: "REJECTED" });
340
+
341
+ /**
342
+ * Verify (purely, OFFLINE) a DETACHED-TIMESTAMP container against a product's framing — the read-only
343
+ * sibling of `verifySignedAttestation`. PARAMETERIZED by `cfg` exactly like the validate/build path. It
344
+ * answers, with NO key and NO network: (1) does the container re-derive the canonical attestation bytes
345
+ * from the embedded UNSIGNED payload, with `digest === sha256(those bytes)`; (2) does the token PARSE as
346
+ * RFC-3161 and BIND that digest under SHA-256; (3) — OPTIONALLY, when `expectedManifestCanonical` is
347
+ * provided — are the embedded canonical bytes byte-identical to the buyer's OWN re-derived canonical bytes
348
+ * (binding the token to the buyer's data, exactly like verify-attest's `--manifest`).
349
+ *
350
+ * The structural + binding checks (1) and (2) are precisely what `validateTimestampContainer` enforces; we
351
+ * reuse it VERBATIM (never a re-impl) so a tampered token / mismatched digest / edited embedded attestation
352
+ * REJECTS for the same reason the build/read path rejects. A structural failure is a clean REJECTED that
353
+ * NAMES the failing reason — NEVER a false ACCEPT, and never a thrown error from a malformed-but-parseable
354
+ * container. (A non-JSON / unreadable file is still an I/O error at the read boundary, handled by the CLI.)
355
+ *
356
+ * @param {object} params
357
+ * @param {any} params.container the parsed container object (from readTimestampContainer or JSON.parse)
358
+ * @param {string} [params.expectedManifestCanonical] OPTIONAL: the buyer's OWN canonical UNSIGNED bytes
359
+ * (serializeUnsigned(buildUnsigned(theirManifest))); when present, the embedded attestation must
360
+ * equal it byte-for-byte
361
+ * @param {object} cfg the product's timestamp-container framing (see buildTimestampContainer)
362
+ * @returns {{
363
+ * verdict: "ACCEPTED"|"REJECTED",
364
+ * accepted: boolean,
365
+ * checks: { structureAndBinding: boolean, manifestBindsAttestation: boolean|null },
366
+ * manifestChecked: boolean,
367
+ * failedChecks: string[],
368
+ * reason: string|null,
369
+ * genTime: string|null,
370
+ * genTimeEpochMs: number|null,
371
+ * serialNumber: {hex:string,decimal:string}|null,
372
+ * policyOID: string|null,
373
+ * hashAlgorithmOID: string|null,
374
+ * digest: string|null,
375
+ * }}
376
+ */
377
+ function verifyTimestampContainer(params, cfg) {
378
+ _requireCfg(cfg);
379
+ if (!params || typeof params !== "object") {
380
+ throw new Error("verifyTimestampContainer requires { container, [expectedManifestCanonical] }");
381
+ }
382
+ const { container, expectedManifestCanonical } = params;
383
+ const manifestChecked =
384
+ expectedManifestCanonical !== undefined && expectedManifestCanonical !== null;
385
+
386
+ const result = {
387
+ verdict: VERIFY_TIMESTAMP_VERDICT.REJECTED,
388
+ accepted: false,
389
+ checks: { structureAndBinding: false, manifestBindsAttestation: manifestChecked ? false : null },
390
+ manifestChecked,
391
+ failedChecks: [],
392
+ reason: null,
393
+ genTime: null,
394
+ genTimeEpochMs: null,
395
+ serialNumber: null,
396
+ policyOID: null,
397
+ hashAlgorithmOID: null,
398
+ digest: null,
399
+ };
400
+
401
+ // Check 1 + 2 (structure + binding): run the SAME strict validator the build/read path uses. It
402
+ // re-derives sha256(canonical bytes), confirms digest equality, parses the token, and confirms
403
+ // bindsDigest — so an edited embedded attestation, a mismatched digest, or a token binding a different
404
+ // digest all throw HERE with a descriptive message. We turn that throw into a clean, named REJECTED
405
+ // (never a false ACCEPT, never a leaked exception).
406
+ let validated;
407
+ try {
408
+ validated = validateTimestampContainer(container, cfg);
409
+ } catch (e) {
410
+ result.failedChecks.push("structureAndBinding");
411
+ result.reason = e.message;
412
+ return result;
413
+ }
414
+ result.checks.structureAndBinding = true;
415
+
416
+ // Surface what the (now-confirmed-binding) token ASSERTS — the same honest scope as readTimestampFacts.
417
+ const facts = readTimestampFacts(validated);
418
+ result.genTime = facts.genTime;
419
+ result.genTimeEpochMs = facts.genTimeEpochMs;
420
+ result.serialNumber = facts.serialNumber;
421
+ result.policyOID = facts.policyOID;
422
+ result.hashAlgorithmOID = facts.hashAlgorithmOID;
423
+ result.digest = facts.digest;
424
+
425
+ // Check 3 (OPTIONAL): bind the token to the BUYER's own data. The embedded canonical bytes must equal
426
+ // the buyer's re-derived canonical bytes byte-for-byte. A DIFFERENT manifest -> a different attestation
427
+ // -> a byte mismatch -> REJECTED (the token timestamped a DIFFERENT dataset/parcel identity).
428
+ if (manifestChecked) {
429
+ if (typeof expectedManifestCanonical !== "string") {
430
+ throw new Error("verifyTimestampContainer: expectedManifestCanonical must be a string when provided");
431
+ }
432
+ const binds = validated.attestation === expectedManifestCanonical;
433
+ result.checks.manifestBindsAttestation = binds;
434
+ if (!binds) {
435
+ result.failedChecks.push("manifestBindsAttestation");
436
+ result.reason =
437
+ "the timestamped attestation does NOT match YOUR manifest — the token stamped a DIFFERENT " +
438
+ "dataset/parcel identity than the one you hold";
439
+ }
440
+ }
441
+
442
+ result.accepted = result.failedChecks.length === 0;
443
+ result.verdict = result.accepted
444
+ ? VERIFY_TIMESTAMP_VERDICT.ACCEPTED
445
+ : VERIFY_TIMESTAMP_VERDICT.REJECTED;
446
+ return result;
447
+ }
448
+
449
+ /**
450
+ * Read (purely, OFFLINE) the timestamp facts a container ASSERTS: the asserted genTime / TSA serial /
451
+ * policy OID + the bound digest, with the SAME honest scope as cli/core/rfc3161.js — it does NOT validate
452
+ * the TSA cert chain / the CMS signature (that is the human out-of-band trust anchor). Used by the read
453
+ * side to surface what the token claims without re-deciding the binding the validator already confirmed.
454
+ *
455
+ * @param {object} container a validated detached-timestamp container
456
+ * @returns {{ digest: string, genTime: string, genTimeEpochMs: number, serialNumber: {hex,decimal}, policyOID: string, hashAlgorithmOID: string }}
457
+ */
458
+ function readTimestampFacts(container) {
459
+ const parsed = rfc3161.parseTimeStampToken(container.timestamp.token);
460
+ return {
461
+ digest: container.timestamp.digest,
462
+ genTime: parsed.genTime,
463
+ genTimeEpochMs: parsed.genTimeEpochMs,
464
+ serialNumber: parsed.serialNumber,
465
+ policyOID: parsed.policyOID,
466
+ hashAlgorithmOID: parsed.messageImprint.hashAlgorithmOID,
467
+ };
468
+ }
469
+
470
+ module.exports = {
471
+ TIMESTAMP_SCHEMES,
472
+ TIMESTAMP_HASH_ALGORITHMS,
473
+ SHA256_OID,
474
+ sha256Hex,
475
+ validateTimestampContainer,
476
+ buildTimestampContainer,
477
+ serializeTimestampContainer,
478
+ readTimestampContainer,
479
+ readTimestampFacts,
480
+ VERIFY_TIMESTAMP_VERDICT,
481
+ verifyTimestampContainer,
482
+ };