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,389 @@
1
+ "use strict";
2
+
3
+ // cli/core/rfc3161.js — a small, PURE, dependency-free RFC-3161 TimeStampToken reader.
4
+ //
5
+ // WHY THIS EXISTS (T-20.1, EPIC-20)
6
+ // verifyhash's signed-attestation envelope (cli/core/attestation.js) proves "the publisher SAYS this
7
+ // payload existed" — a SELF-managed signature. The honestly-stronger claim a due-diligence / EU-AI-Act
8
+ // reviewer ultimately wants is "an INDEPENDENT third party attests this exact digest existed by time T."
9
+ // RFC-3161 is the standard way to get that: you send a hash (the messageImprint) to a Time-Stamping
10
+ // Authority (TSA) and it returns a signed TimeStampToken whose embedded TSTInfo binds that hash to a
11
+ // genTime. This module is the OFFLINE half: given a token's bytes, parse the TSTInfo and confirm it
12
+ // binds EXACTLY the buyer's own digest. Obtaining a token requires a network call to a real (possibly
13
+ // paid) TSA — a HUMAN step; this loop never makes it. The only tokens this module ever sees in-suite
14
+ // are self-minted throwaway TEST tokens (see __testutil / the test file), the timestamp analogue of
15
+ // Wallet.createRandom().
16
+ //
17
+ // SCOPE — DELIBERATELY MINIMAL, AND HONEST ABOUT IT.
18
+ // IN : a bounded, pure DER reader for EXACTLY the types on the TSTInfo path (SEQUENCE, OID, INTEGER,
19
+ // OCTET STRING, GeneralizedTime, and the [0] EXPLICIT context tags it must traverse); reaching the
20
+ // embedded TSTInfo through the CMS SignedData wrapper; extracting version/policy/messageImprint/
21
+ // serialNumber/genTime; and a bindsDigest() check.
22
+ // OUT: NO X.509 certificate-chain parsing, NO verification of the TSA's signature OVER the TSTInfo, NO
23
+ // network. Trusting that the TOKEN itself is authentic (that the bytes really came from your TSA
24
+ // and weren't forged) is the HUMAN TRUST-ANCHOR step: you validate the TSA's cert chain / the CMS
25
+ // signature out-of-band (e.g. `openssl ts -verify`, a CMS verifier), exactly as Option A pins the
26
+ // signer ADDRESS out-of-band. This module proves the BINDING (which digest/genTime the token
27
+ // asserts), not the AUTHENTICITY of the asserting party. The reader REJECTS malformed/truncated
28
+ // DER with a clear error — it never silently returns a partial or wrong field.
29
+ //
30
+ // IMPORTANT: zero dependencies, zero I/O, zero global state. Operates on a Buffer/Uint8Array of DER bytes.
31
+
32
+ // ---------------------------------------------------------------------------------------------------
33
+ // OIDs we care about (dotted-decimal strings). Frozen so they can't drift.
34
+ // ---------------------------------------------------------------------------------------------------
35
+ const OID = Object.freeze({
36
+ // CMS ContentInfo contentType for SignedData (PKCS#7 / RFC 5652).
37
+ signedData: "1.2.840.113549.1.7.2",
38
+ // RFC-3161 id-ct-TSTInfo: the eContentType whose eContent is the DER-encoded TSTInfo.
39
+ tstInfo: "1.2.840.113549.1.9.16.1.4",
40
+ // Common digest algorithms a messageImprint hashAlgorithm may name. SHA-256 is the default we expect.
41
+ sha256: "2.16.840.1.101.3.4.2.1",
42
+ sha384: "2.16.840.1.101.3.4.2.2",
43
+ sha512: "2.16.840.1.101.3.4.2.3",
44
+ sha1: "1.3.14.3.2.26",
45
+ });
46
+
47
+ // DER universal tag numbers (class 0, the low 5 bits of the identifier octet for primitive forms).
48
+ const TAG = Object.freeze({
49
+ INTEGER: 0x02,
50
+ BIT_STRING: 0x03,
51
+ OCTET_STRING: 0x04,
52
+ OID: 0x06,
53
+ SEQUENCE: 0x30, // constructed
54
+ SET: 0x31, // constructed
55
+ GENERALIZED_TIME: 0x18,
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------------------------------
59
+ // A tiny, BOUNDED DER cursor. Every read is length-checked against `end`; any over-read throws. This is
60
+ // the single place that turns "DER bytes" into "a value", so truncation/garbage can NEVER leak past it
61
+ // as a silently-wrong field — a partial read is always a thrown error.
62
+ // ---------------------------------------------------------------------------------------------------
63
+ class DerError extends Error {
64
+ constructor(msg) {
65
+ super("RFC-3161 DER: " + msg);
66
+ this.name = "DerError";
67
+ }
68
+ }
69
+
70
+ function toBuf(der) {
71
+ if (Buffer.isBuffer(der)) return der;
72
+ if (der instanceof Uint8Array) return Buffer.from(der);
73
+ if (typeof der === "string") {
74
+ // Accept hex (0x-optional) or base64. Hex if it's all hex of even length; else try base64.
75
+ const s = der.trim();
76
+ const hex = s.replace(/^0x/i, "");
77
+ if (/^[0-9a-fA-F]+$/.test(hex) && hex.length % 2 === 0) return Buffer.from(hex, "hex");
78
+ if (/^[A-Za-z0-9+/=\s]+$/.test(s)) {
79
+ const b = Buffer.from(s.replace(/\s+/g, ""), "base64");
80
+ if (b.length > 0) return b;
81
+ }
82
+ throw new DerError("input string is neither valid hex nor base64");
83
+ }
84
+ throw new DerError("input must be a Buffer, Uint8Array, or hex/base64 string");
85
+ }
86
+
87
+ // Read one TLV at offset `pos` within [0,end). Returns { tag, valStart, valEnd, next } where the value
88
+ // occupies bytes [valStart, valEnd) and the NEXT TLV (if any) starts at `next` (== valEnd here, since DER
89
+ // is definite-length). Rejects indefinite length (0x80) — forbidden in DER — and any length that runs
90
+ // past `end`.
91
+ function readTLV(buf, pos, end) {
92
+ if (pos >= end) throw new DerError("unexpected end of input while reading tag");
93
+ const tag = buf[pos];
94
+ // Multi-byte (high-tag-number) identifiers (low 5 bits all 1) are not used on the TSTInfo path; reject.
95
+ if ((tag & 0x1f) === 0x1f) throw new DerError("high-tag-number form is unsupported");
96
+ let p = pos + 1;
97
+ if (p >= end) throw new DerError("unexpected end of input while reading length");
98
+ let lenByte = buf[p++];
99
+ let len;
100
+ if (lenByte < 0x80) {
101
+ len = lenByte; // short form
102
+ } else if (lenByte === 0x80) {
103
+ throw new DerError("indefinite length is forbidden in DER");
104
+ } else {
105
+ const numBytes = lenByte & 0x7f;
106
+ if (numBytes > 4) throw new DerError("length field too large");
107
+ if (p + numBytes > end) throw new DerError("truncated long-form length");
108
+ len = 0;
109
+ for (let i = 0; i < numBytes; i++) len = len * 256 + buf[p++];
110
+ // DER requires the minimal length encoding; a long form encoding a value < 0x80 is malformed, and a
111
+ // leading zero byte is non-minimal.
112
+ if (numBytes === 1 && len < 0x80) throw new DerError("non-minimal long-form length");
113
+ if (buf[p - numBytes] === 0x00) throw new DerError("non-minimal long-form length (leading zero)");
114
+ }
115
+ const valStart = p;
116
+ const valEnd = p + len;
117
+ if (valEnd > end) throw new DerError("value length runs past end of input");
118
+ return { tag, valStart, valEnd, next: valEnd };
119
+ }
120
+
121
+ // Read a TLV and assert it carries the expected tag; otherwise a clear error naming both.
122
+ function expectTLV(buf, pos, end, wantTag, what) {
123
+ const t = readTLV(buf, pos, end);
124
+ if (t.tag !== wantTag) {
125
+ throw new DerError(
126
+ `expected ${what} (tag 0x${wantTag.toString(16).padStart(2, "0")}) but found tag 0x${t.tag
127
+ .toString(16)
128
+ .padStart(2, "0")}`
129
+ );
130
+ }
131
+ return t;
132
+ }
133
+
134
+ // Decode a DER OBJECT IDENTIFIER value (the bytes between valStart/valEnd) to dotted-decimal.
135
+ function decodeOID(buf, valStart, valEnd) {
136
+ if (valEnd <= valStart) throw new DerError("empty OID");
137
+ const parts = [];
138
+ // First byte encodes the first two arcs: X*40 + Y.
139
+ const first = buf[valStart];
140
+ parts.push(Math.floor(first / 40));
141
+ parts.push(first % 40);
142
+ let val = 0;
143
+ let started = false;
144
+ for (let i = valStart + 1; i < valEnd; i++) {
145
+ const b = buf[i];
146
+ // base-128, high bit = continuation. Reject non-minimal (leading 0x80 in a sub-identifier).
147
+ if (!started && b === 0x80) throw new DerError("non-minimal OID sub-identifier");
148
+ started = true;
149
+ val = val * 128 + (b & 0x7f);
150
+ if ((b & 0x80) === 0) {
151
+ parts.push(val);
152
+ val = 0;
153
+ started = false;
154
+ }
155
+ }
156
+ if (started) throw new DerError("truncated OID (last sub-identifier unterminated)");
157
+ return parts.join(".");
158
+ }
159
+
160
+ // Decode a DER INTEGER value to BOTH a lowercase hex string (no 0x, magnitude, leading zero stripped to a
161
+ // single byte minimum) and a decimal string. Used for serialNumber, which RFC-3161 allows to be large
162
+ // (>53 bits) — so we go through BigInt for the decimal and keep the raw hex for byte-exactness. We only
163
+ // support NON-NEGATIVE integers here (serialNumber and version are non-negative); a negative DER INTEGER
164
+ // (high bit set after the minimal-zero rule) is rejected.
165
+ function decodeUInt(buf, valStart, valEnd, what) {
166
+ if (valEnd <= valStart) throw new DerError(`empty INTEGER (${what})`);
167
+ // DER minimal-encoding rules for INTEGER.
168
+ if (valEnd - valStart >= 2) {
169
+ if (buf[valStart] === 0x00 && (buf[valStart + 1] & 0x80) === 0)
170
+ throw new DerError(`non-minimal INTEGER encoding (${what})`);
171
+ if (buf[valStart] === 0xff && (buf[valStart + 1] & 0x80) !== 0)
172
+ throw new DerError(`non-minimal negative INTEGER encoding (${what})`);
173
+ }
174
+ if (buf[valStart] & 0x80) throw new DerError(`negative INTEGER not supported (${what})`);
175
+ // Strip the single leading 0x00 sign byte (if present) for the magnitude.
176
+ let start = valStart;
177
+ if (buf[start] === 0x00 && valEnd - start > 1) start += 1;
178
+ let hex = "";
179
+ let big = 0n;
180
+ for (let i = start; i < valEnd; i++) {
181
+ hex += buf[i].toString(16).padStart(2, "0");
182
+ big = (big << 8n) | BigInt(buf[i]);
183
+ }
184
+ if (hex === "") hex = "00";
185
+ return { hex, decimal: big.toString(10) };
186
+ }
187
+
188
+ // Decode an RFC-3161 GeneralizedTime to a canonical ISO-8601 UTC string. RFC-3161 §2.4.2 constrains
189
+ // genTime to the form YYYYMMDDHHMMSS[.fff]Z — ALWAYS UTC (trailing Z), with optional fractional seconds
190
+ // and NO trailing zeros in the fraction. We REJECT zoneless or offset (+hhmm) forms: a timestamp whose
191
+ // instant is ambiguous must never be silently coerced.
192
+ function decodeGeneralizedTime(buf, valStart, valEnd) {
193
+ const raw = buf.toString("ascii", valStart, valEnd);
194
+ // Must end in Z (UTC). Anything else (no zone, or a +/- offset) is rejected.
195
+ const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.(\d+))?Z$/.exec(raw);
196
+ if (!m) {
197
+ if (/[+-]\d{2,4}$/.test(raw))
198
+ throw new DerError(`genTime must be UTC (trailing Z); got offset form "${raw}"`);
199
+ if (!/Z$/.test(raw)) throw new DerError(`genTime must be UTC (trailing Z); got zoneless "${raw}"`);
200
+ throw new DerError(`malformed GeneralizedTime "${raw}"`);
201
+ }
202
+ const [, Y, Mo, D, H, Mi, S, frac] = m;
203
+ const year = Number(Y);
204
+ const month = Number(Mo);
205
+ const day = Number(D);
206
+ const hour = Number(H);
207
+ const min = Number(Mi);
208
+ const sec = Number(S);
209
+ if (month < 1 || month > 12) throw new DerError(`genTime month out of range "${raw}"`);
210
+ if (day < 1 || day > 31) throw new DerError(`genTime day out of range "${raw}"`);
211
+ if (hour > 23 || min > 59 || sec > 60) throw new DerError(`genTime time-of-day out of range "${raw}"`);
212
+ // Build a canonical ISO string. Use Date.UTC for the instant; preserve fractional seconds (trimmed of
213
+ // trailing zeros, max 3 digits surfaced as milliseconds in the ISO form) when present.
214
+ let ms = 0;
215
+ let fracStr = "";
216
+ if (frac !== undefined) {
217
+ const trimmed = frac.replace(/0+$/, "");
218
+ if (trimmed === "") throw new DerError(`genTime fractional seconds must not be all-zero "${raw}"`);
219
+ if (trimmed !== frac) throw new DerError(`genTime fractional seconds must not have trailing zeros "${raw}"`);
220
+ fracStr = trimmed;
221
+ ms = Math.round(Number("0." + trimmed) * 1000);
222
+ }
223
+ const epoch = Date.UTC(year, month - 1, day, hour, min, sec, ms);
224
+ if (!Number.isFinite(epoch)) throw new DerError(`genTime is not a valid date "${raw}"`);
225
+ // Canonical ISO-8601 UTC. We surface the original fractional precision (not just milliseconds) so a
226
+ // sub-millisecond stamp isn't silently rounded away in the human/text form.
227
+ const iso =
228
+ `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}T${pad(hour, 2)}:${pad(min, 2)}:${pad(sec, 2)}` +
229
+ (fracStr ? "." + fracStr : "") +
230
+ "Z";
231
+ return { iso, epochMs: epoch };
232
+ }
233
+
234
+ function pad(n, w) {
235
+ return String(n).padStart(w, "0");
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------------------------------
239
+ // Reach the embedded TSTInfo through the CMS SignedData wrapper, then parse it.
240
+ // ---------------------------------------------------------------------------------------------------
241
+
242
+ // Given the value-range of a [0] EXPLICIT context tag (the outermost ContentInfo's content, and the
243
+ // encapContentInfo's eContent), return the inner TLV range. EXPLICIT context tags wrap exactly one inner
244
+ // element. Tag byte for [0] constructed-explicit is 0xA0.
245
+ function CONTEXT(n) {
246
+ return 0xa0 + n; // constructed context-specific [n]
247
+ }
248
+
249
+ // Parse a CMS ContentInfo (SignedData) and return the byte range of the embedded TSTInfo OCTET STRING's
250
+ // VALUE (the DER-encoded TSTInfo). Throws if eContentType is not id-ct-TSTInfo (so a non-timestamp CMS
251
+ // blob errors clearly instead of producing nonsense).
252
+ function reachTSTInfo(buf) {
253
+ const end = buf.length;
254
+ // ContentInfo ::= SEQUENCE { contentType OID, content [0] EXPLICIT ANY }
255
+ const ci = expectTLV(buf, 0, end, TAG.SEQUENCE, "ContentInfo SEQUENCE");
256
+ if (ci.next !== end) throw new DerError("trailing bytes after ContentInfo");
257
+ let pos = ci.valStart;
258
+ const ct = expectTLV(buf, pos, ci.valEnd, TAG.OID, "ContentInfo.contentType OID");
259
+ const ctOid = decodeOID(buf, ct.valStart, ct.valEnd);
260
+ if (ctOid !== OID.signedData)
261
+ throw new DerError(`ContentInfo.contentType is ${ctOid}, expected signedData (${OID.signedData})`);
262
+ pos = ct.next;
263
+ const content = expectTLV(buf, pos, ci.valEnd, CONTEXT(0), "ContentInfo.content [0] EXPLICIT");
264
+ // SignedData ::= SEQUENCE { version, digestAlgorithms SET, encapContentInfo SEQUENCE {...}, ... }
265
+ const sd = expectTLV(buf, content.valStart, content.valEnd, TAG.SEQUENCE, "SignedData SEQUENCE");
266
+ let sp = sd.valStart;
267
+ const sdVer = expectTLV(buf, sp, sd.valEnd, TAG.INTEGER, "SignedData.version");
268
+ sp = sdVer.next;
269
+ const digestAlgs = expectTLV(buf, sp, sd.valEnd, TAG.SET, "SignedData.digestAlgorithms SET");
270
+ sp = digestAlgs.next;
271
+ // encapContentInfo ::= SEQUENCE { eContentType OID, eContent [0] EXPLICIT OCTET STRING OPTIONAL }
272
+ const enc = expectTLV(buf, sp, sd.valEnd, TAG.SEQUENCE, "encapContentInfo SEQUENCE");
273
+ let ep = enc.valStart;
274
+ const eContentType = expectTLV(buf, ep, enc.valEnd, TAG.OID, "eContentType OID");
275
+ const eOid = decodeOID(buf, eContentType.valStart, eContentType.valEnd);
276
+ if (eOid !== OID.tstInfo)
277
+ throw new DerError(
278
+ `eContentType is ${eOid}, not id-ct-TSTInfo (${OID.tstInfo}) — this is not an RFC-3161 TimeStampToken`
279
+ );
280
+ ep = eContentType.next;
281
+ const eContent = expectTLV(buf, ep, enc.valEnd, CONTEXT(0), "eContent [0] EXPLICIT");
282
+ // eContent wraps an OCTET STRING whose bytes ARE the DER-encoded TSTInfo.
283
+ const octet = expectTLV(buf, eContent.valStart, eContent.valEnd, TAG.OCTET_STRING, "TSTInfo OCTET STRING");
284
+ return { start: octet.valStart, end: octet.valEnd };
285
+ }
286
+
287
+ // Parse the TSTInfo at [start,end) into the extracted field set.
288
+ function parseTSTInfo(buf, start, end) {
289
+ // TSTInfo ::= SEQUENCE { version INTEGER, policy OID, messageImprint SEQUENCE,
290
+ // serialNumber INTEGER, genTime GeneralizedTime, ... (optionals ignored) }
291
+ const seq = expectTLV(buf, start, end, TAG.SEQUENCE, "TSTInfo SEQUENCE");
292
+ let p = seq.valStart;
293
+ const limit = seq.valEnd;
294
+
295
+ const ver = expectTLV(buf, p, limit, TAG.INTEGER, "TSTInfo.version");
296
+ const version = Number(decodeUInt(buf, ver.valStart, ver.valEnd, "TSTInfo.version").decimal);
297
+ if (version !== 1) throw new DerError(`unsupported TSTInfo version ${version} (expected 1)`);
298
+ p = ver.next;
299
+
300
+ const pol = expectTLV(buf, p, limit, TAG.OID, "TSTInfo.policy OID");
301
+ const policyOID = decodeOID(buf, pol.valStart, pol.valEnd);
302
+ p = pol.next;
303
+
304
+ // messageImprint ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage OCTET STRING }
305
+ const mi = expectTLV(buf, p, limit, TAG.SEQUENCE, "messageImprint SEQUENCE");
306
+ let mp = mi.valStart;
307
+ const alg = expectTLV(buf, mp, mi.valEnd, TAG.SEQUENCE, "hashAlgorithm AlgorithmIdentifier SEQUENCE");
308
+ const algOid = expectTLV(buf, alg.valStart, alg.valEnd, TAG.OID, "hashAlgorithm OID");
309
+ const hashAlgorithmOID = decodeOID(buf, algOid.valStart, algOid.valEnd);
310
+ // (Any AlgorithmIdentifier parameters after the OID — e.g. an explicit NULL — are intentionally ignored.)
311
+ mp = alg.next;
312
+ const hm = expectTLV(buf, mp, mi.valEnd, TAG.OCTET_STRING, "hashedMessage OCTET STRING");
313
+ const hashedMessage = buf.toString("hex", hm.valStart, hm.valEnd);
314
+ p = mi.next;
315
+
316
+ const ser = expectTLV(buf, p, limit, TAG.INTEGER, "TSTInfo.serialNumber");
317
+ const serial = decodeUInt(buf, ser.valStart, ser.valEnd, "TSTInfo.serialNumber");
318
+ p = ser.next;
319
+
320
+ const gt = expectTLV(buf, p, limit, TAG.GENERALIZED_TIME, "TSTInfo.genTime");
321
+ const time = decodeGeneralizedTime(buf, gt.valStart, gt.valEnd);
322
+
323
+ return {
324
+ version,
325
+ policyOID,
326
+ messageImprint: {
327
+ hashAlgorithmOID,
328
+ hashedMessage, // lowercase hex, no 0x
329
+ },
330
+ serialNumber: { hex: serial.hex, decimal: serial.decimal },
331
+ genTime: time.iso,
332
+ genTimeEpochMs: time.epochMs,
333
+ };
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------------------------------
337
+ // Public API.
338
+ // ---------------------------------------------------------------------------------------------------
339
+
340
+ // parseTimeStampToken(der) -> the extracted TSTInfo field set (see parseTSTInfo). `der` may be a Buffer,
341
+ // Uint8Array, or a hex/base64 string. Throws a DerError on any malformed/truncated/non-TSTInfo input.
342
+ function parseTimeStampToken(der) {
343
+ const buf = toBuf(der);
344
+ if (buf.length === 0) throw new DerError("empty input");
345
+ const range = reachTSTInfo(buf);
346
+ return parseTSTInfo(buf, range.start, range.end);
347
+ }
348
+
349
+ // bindsDigest({ token, expectedDigestHex, expectedHashOID }) -> boolean.
350
+ // `token` is EITHER raw DER (Buffer/Uint8Array/string) OR an already-parsed object from
351
+ // parseTimeStampToken. Returns true ONLY when the token's messageImprint.hashedMessage equals
352
+ // expectedDigestHex (compared lowercased, exact) AND messageImprint.hashAlgorithmOID equals
353
+ // expectedHashOID. Any mismatch — a different digest, or the same digest under a different hash
354
+ // algorithm — returns false. This is the OFFLINE binding check: it proves WHICH digest the token
355
+ // asserts existed, NOT that the asserting TSA is authentic (that is the out-of-band human trust anchor).
356
+ //
357
+ // Returns false (never throws) for a structurally-parsed token that simply doesn't match. It DOES throw
358
+ // if asked to parse raw DER that is malformed (so callers can distinguish "valid token, wrong digest"
359
+ // from "not a token at all").
360
+ function bindsDigest({ token, expectedDigestHex, expectedHashOID } = {}) {
361
+ if (token == null) throw new DerError("bindsDigest: token is required");
362
+ if (typeof expectedDigestHex !== "string" || expectedDigestHex.length === 0)
363
+ throw new DerError("bindsDigest: expectedDigestHex (hex string) is required");
364
+ if (typeof expectedHashOID !== "string" || expectedHashOID.length === 0)
365
+ throw new DerError("bindsDigest: expectedHashOID (dotted-decimal string) is required");
366
+
367
+ const parsed =
368
+ token && typeof token === "object" && token.messageImprint
369
+ ? token
370
+ : parseTimeStampToken(token);
371
+
372
+ const want = expectedDigestHex.trim().replace(/^0x/i, "").toLowerCase();
373
+ if (!/^[0-9a-f]*$/.test(want) || want.length === 0) return false;
374
+
375
+ const got = String(parsed.messageImprint.hashedMessage).toLowerCase();
376
+ if (got !== want) return false;
377
+ if (parsed.messageImprint.hashAlgorithmOID !== expectedHashOID) return false;
378
+ return true;
379
+ }
380
+
381
+ module.exports = {
382
+ OID,
383
+ TAG,
384
+ DerError,
385
+ parseTimeStampToken,
386
+ bindsDigest,
387
+ // Exposed for the test-only minter + potential reuse; not part of the command path's required surface.
388
+ _internal: { readTLV, decodeOID, decodeUInt, decodeGeneralizedTime, reachTSTInfo, parseTSTInfo, toBuf },
389
+ };