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.
- package/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- 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
|
+
};
|