@primust/verifier 1.0.1 → 1.2.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.
@@ -2,6 +2,7 @@
2
2
  import { createHash, createPublicKey, verify as cryptoVerify } from "crypto";
3
3
  var ENVELOPE_VERSION = "v0.1";
4
4
  var ALLOWED_KINDS = /* @__PURE__ */ new Set([
5
+ // v1.0 kinds (DECOMPOSITION_PROOF_SPEC v1.0 — locked Apr 26):
5
6
  "governance_check",
6
7
  "outbound_action",
7
8
  "token_authority_snapshot",
@@ -10,8 +11,14 @@ var ALLOWED_KINDS = /* @__PURE__ */ new Set([
10
11
  "scope_claim_lifecycle",
11
12
  "scope_claim_emitted",
12
13
  "turn_context",
13
- "tool_execution"
14
+ "tool_execution",
15
+ // v1.1 additions (Controls Harness Execution Mode, mig 156).
16
+ // See docs/v29/foundation/CONTROLS_HARNESS_EXECUTION_SPEC_v0_1.md §2.4.
17
+ "enforcement_action",
18
+ "redaction",
19
+ "notification"
14
20
  ]);
21
+ var ALLOWED_PROVENANCE = /* @__PURE__ */ new Set(["host_captured", "customer_supplied"]);
15
22
  var ALLOWED_TRUST_EDGES = /* @__PURE__ */ new Set(["A2T", "A2M", "A2H", "A2A", "A2S", "A2D"]);
16
23
  var PROOF_TIER_HIERARCHY = [
17
24
  "attestation",
@@ -412,6 +419,7 @@ function verifyV29(opts) {
412
419
  export {
413
420
  ENVELOPE_VERSION,
414
421
  ALLOWED_KINDS,
422
+ ALLOWED_PROVENANCE,
415
423
  ALLOWED_TRUST_EDGES,
416
424
  PROOF_TIER_HIERARCHY,
417
425
  v29Pass,
@@ -1220,7 +1220,11 @@ function verify2(document, signatureEnvelope, publicKeyB64Url) {
1220
1220
  } catch {
1221
1221
  return false;
1222
1222
  }
1223
- for (const canonFn of [canonical, canonicalLegacy]) {
1223
+ const LEGACY_CUTOFF = "2026-04-19";
1224
+ const signedAt = signatureEnvelope.signed_at ?? "";
1225
+ const legacyAllowed = signedAt !== "" && signedAt.slice(0, 10) < LEGACY_CUTOFF;
1226
+ const candidates = legacyAllowed ? [canonical, canonicalLegacy] : [canonical];
1227
+ for (const canonFn of candidates) {
1224
1228
  try {
1225
1229
  const canonicalStr = canonFn(document);
1226
1230
  const hashBytes = sha2562(new TextEncoder().encode(canonicalStr));
@@ -2308,14 +2312,32 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2308
2312
  }
2309
2313
  const tsAnchor = artifact.timestamp_anchor;
2310
2314
  if (tsAnchor && tsAnchor.type === "rfc3161" && typeof tsAnchor.value === "string") {
2311
- result.timestamp_anchor_valid = verifyTimestampImprint(
2315
+ const imprintOk = verifyTimestampImprint(
2312
2316
  tsAnchor.value,
2313
2317
  timestampBody(artifact)
2314
2318
  );
2315
- if (result.timestamp_anchor_valid === false) {
2319
+ if (imprintOk === false) {
2320
+ result.timestamp_anchor_valid = false;
2316
2321
  result.warnings.push("rfc3161_imprint_mismatch");
2317
- } else if (result.timestamp_anchor_valid === true) {
2318
- result.warnings.push("rfc3161_tsa_cert_chain_not_verified");
2322
+ } else if (imprintOk === true) {
2323
+ let chain = null;
2324
+ try {
2325
+ const { verifyTsaChain } = await import("./tsa-chain-7KSQ5LAH.js");
2326
+ chain = verifyTsaChain(tsAnchor.value);
2327
+ } catch {
2328
+ chain = null;
2329
+ }
2330
+ if (chain === null) {
2331
+ result.timestamp_anchor_valid = true;
2332
+ result.warnings.push("rfc3161_tsa_chain_verifier_unavailable");
2333
+ } else if (chain.ok) {
2334
+ result.timestamp_anchor_valid = true;
2335
+ } else {
2336
+ result.timestamp_anchor_valid = false;
2337
+ result.warnings.push(`rfc3161_tsa_chain_invalid:${chain.reason}`);
2338
+ }
2339
+ } else {
2340
+ result.timestamp_anchor_valid = null;
2319
2341
  }
2320
2342
  } else {
2321
2343
  result.timestamp_anchor_valid = null;
@@ -2423,14 +2445,22 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2423
2445
  const failedArtifacts = proofArtifacts.filter(
2424
2446
  (artifactEntry) => artifactEntry.verification_status === "failed"
2425
2447
  ).length;
2448
+ const unresolvedArtifacts = proofArtifacts.filter(
2449
+ (artifactEntry) => artifactEntry.verification_status === "unresolved_at_seal"
2450
+ ).length;
2426
2451
  const verifiedArtifacts = proofArtifacts.filter(
2427
2452
  (artifactEntry) => artifactEntry.verification_status === "verified"
2428
2453
  ).length;
2454
+ if (failedArtifacts > 0) {
2455
+ result.errors.push(`proof_artifacts_failed:${failedArtifacts}`);
2456
+ }
2429
2457
  if (pendingArtifacts > 0) {
2430
- result.warnings.push(`proof_artifacts_pending:${pendingArtifacts}`);
2458
+ result.errors.push(`proof_artifacts_pending:${pendingArtifacts}`);
2431
2459
  }
2432
- if (failedArtifacts > 0) {
2433
- result.warnings.push(`proof_artifacts_failed:${failedArtifacts}`);
2460
+ if (unresolvedArtifacts > 0) {
2461
+ result.errors.push(
2462
+ `proof_artifacts_unresolved_at_seal:${unresolvedArtifacts}; proof unresolved at seal; counterparty must consult live API to determine status`
2463
+ );
2434
2464
  }
2435
2465
  if (!artifact.zk_proof) {
2436
2466
  result.warnings.push(`proof_artifacts_present:${verifiedArtifacts}`);
@@ -2459,7 +2489,7 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2459
2489
  }
2460
2490
  if (artifact.envelope_version != null && (artifact.run_header || artifact.records)) {
2461
2491
  try {
2462
- const { verifyV29 } = await import("./v29-envelope-GFVVA2S6.js");
2492
+ const { verifyV29 } = await import("./v29-envelope-JVSI5N3L.js");
2463
2493
  const shapeEnvelope = {
2464
2494
  envelope_version: artifact.envelope_version,
2465
2495
  run_header: artifact.run_header ?? {},
@@ -2485,9 +2515,38 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2485
2515
  result.warnings.push(`v29_conformance_error:${e.message}`);
2486
2516
  }
2487
2517
  }
2518
+ if (options.revocation_check_url && result.vpec_id) {
2519
+ await checkRevocationOnline(
2520
+ result,
2521
+ options.revocation_check_url.replace(/\/+$/, ""),
2522
+ options.revocation_check_timeout_ms ?? 5e3
2523
+ );
2524
+ }
2488
2525
  result.valid = result.errors.length === 0;
2489
2526
  return result;
2490
2527
  }
2528
+ async function checkRevocationOnline(result, baseUrl, timeoutMs) {
2529
+ const url = `${baseUrl}/api/v1/vpecs/${result.vpec_id}/revocation`;
2530
+ const controller = new AbortController();
2531
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2532
+ try {
2533
+ const resp = await fetch(url, { signal: controller.signal });
2534
+ if (!resp.ok) {
2535
+ result.warnings.push(`revocation_check_http_${resp.status}`);
2536
+ return;
2537
+ }
2538
+ const body = await resp.json();
2539
+ if (body.revoked === true) {
2540
+ const reason = body.revocation_reason ?? "unspecified";
2541
+ result.errors.push(`credential_revoked: ${reason}`);
2542
+ }
2543
+ } catch (e) {
2544
+ const msg = e.message ?? String(e);
2545
+ result.warnings.push(`revocation_check_unavailable: ${msg}`);
2546
+ } finally {
2547
+ clearTimeout(timer);
2548
+ }
2549
+ }
2491
2550
  function computeMerkleRoot(hashes) {
2492
2551
  let leaves = hashes.map((h2) => {
2493
2552
  const hex = h2.startsWith("sha256:") ? h2.slice(7) : h2;
@@ -2829,23 +2888,54 @@ function findBuffer(haystack, needle) {
2829
2888
  return -1;
2830
2889
  }
2831
2890
  var REKOR_API = "https://rekor.sigstore.dev/api/v1";
2891
+ var DEFAULT_REVOKED_KEYS_URLS = [
2892
+ "https://keys.primust.com/.well-known/primust-pubkeys/revoked.json",
2893
+ "https://keys.eu.primust.com/.well-known/primust-pubkeys/revoked.json"
2894
+ ];
2895
+ function revokedKeysUrls() {
2896
+ const env = globalThis.process?.env?.PRIMUST_REVOKED_KEYS_URLS;
2897
+ if (!env || !env.trim()) return DEFAULT_REVOKED_KEYS_URLS;
2898
+ return env.split(",").map((s) => s.trim()).filter(Boolean);
2899
+ }
2900
+ async function isKeyRevoked(fingerprintHex) {
2901
+ for (const url of revokedKeysUrls()) {
2902
+ try {
2903
+ const resp = await fetch(url, {
2904
+ headers: { Accept: "application/json" },
2905
+ signal: AbortSignal.timeout(3e3)
2906
+ });
2907
+ if (!resp.ok) continue;
2908
+ const body = await resp.json();
2909
+ const list = Array.isArray(body) ? body : Array.isArray(body.revoked) ? body.revoked : [];
2910
+ const needle = fingerprintHex.toLowerCase();
2911
+ for (const entry of list) {
2912
+ if (typeof entry === "string" && entry.toLowerCase().replace(/^sha256:/, "") === needle) {
2913
+ return true;
2914
+ }
2915
+ }
2916
+ return false;
2917
+ } catch {
2918
+ continue;
2919
+ }
2920
+ }
2921
+ return null;
2922
+ }
2832
2923
  async function checkRekor(publicKeyB64Url, kid) {
2924
+ void kid;
2833
2925
  try {
2834
2926
  const keyBytes = fromBase64Url(publicKeyB64Url);
2835
2927
  const fingerprint = createHash("sha256").update(Buffer.from(keyBytes)).digest("hex");
2928
+ const revoked = await isKeyRevoked(fingerprint);
2929
+ if (revoked === true) return "revoked";
2836
2930
  const resp = await fetch(`${REKOR_API}/index/retrieve`, {
2837
2931
  method: "POST",
2838
2932
  headers: { "Content-Type": "application/json" },
2839
2933
  body: JSON.stringify({ hash: `sha256:${fingerprint}` }),
2840
2934
  signal: AbortSignal.timeout(5e3)
2841
2935
  });
2842
- if (!resp.ok) {
2843
- return "unavailable";
2844
- }
2936
+ if (!resp.ok) return "unavailable";
2845
2937
  const entries = await resp.json();
2846
- if (!entries || entries.length === 0) {
2847
- return "not_found";
2848
- }
2938
+ if (!entries || entries.length === 0) return "not_found";
2849
2939
  return "active";
2850
2940
  } catch {
2851
2941
  return "unavailable";
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createUpstreamRootResolver,
4
4
  verify
5
- } from "./chunk-ZADQUKKN.js";
5
+ } from "./chunk-UB2YSMJ6.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { readFileSync } from "fs";
@@ -64,7 +64,7 @@ function formatHumanCertificate(artifact, result) {
64
64
  const ps = artifact.provable_surface;
65
65
  lines.push(` Provable surface: ${ps != null ? `${(ps * 100).toFixed(1)}%` : "\u2014"}`);
66
66
  const records = artifact.records ?? [];
67
- lines.push(` Checks run: ${records.length}`);
67
+ lines.push(` Controls run: ${records.length}`);
68
68
  lines.push(` Open gaps: ${result.gaps.length}`);
69
69
  const ac = artifact.action_coverage;
70
70
  if (ac) {
@@ -88,7 +88,7 @@ function formatHumanCertificate(artifact, result) {
88
88
  const tname = u.tool_name;
89
89
  let label = ` #${idx} ${atype}`;
90
90
  if (tname) label += `: ${tname}`;
91
- label += cuIndices.includes(idx) ? " \u26A0 consequential \u2014 no check ran" : " \u2014 no check ran";
91
+ label += cuIndices.includes(idx) ? " \u26A0 consequential \u2014 no control ran" : " \u2014 no control ran";
92
92
  lines.push(label);
93
93
  }
94
94
  }
@@ -106,14 +106,14 @@ function formatHumanCertificate(artifact, result) {
106
106
  const metadataMode = artifact.metadata_mode;
107
107
  if (records.length > 0 && metadataMode === "rich") {
108
108
  lines.push("");
109
- lines.push("GOVERNANCE CHECKS");
109
+ lines.push("CONTROLS RUN");
110
110
  const shown = records.slice(0, 10);
111
111
  for (const r of shown) {
112
- let checkId = r.manifest_id ?? r.check_id ?? "unknown";
113
- if (checkId.includes("@")) checkId = checkId.split("@")[0];
114
- const checkResult = r.check_result ?? "unknown";
112
+ let controlId = r.manifest_id ?? r.control_id ?? r.check_id ?? "unknown";
113
+ if (controlId.includes("@")) controlId = controlId.split("@")[0];
114
+ const controlResult = r.control_result ?? r.check_result ?? "unknown";
115
115
  const em = r.evidence_metadata;
116
- let line = ` ${checkId}: ${checkResult}`;
116
+ let line = ` ${controlId}: ${controlResult}`;
117
117
  if (em) {
118
118
  const details = [];
119
119
  if ("entity_count" in em) details.push(`${em.entity_count} entities`);
@@ -124,17 +124,17 @@ function formatHumanCertificate(artifact, result) {
124
124
  lines.push(line);
125
125
  }
126
126
  if (records.length > 10) {
127
- lines.push(` ... and ${records.length - 10} more checks`);
127
+ lines.push(` ... and ${records.length - 10} more controls`);
128
128
  }
129
129
  }
130
130
  lines.push("");
131
131
  lines.push("WHAT THIS PROVES");
132
- lines.push(" Governance checks ran on the actions listed in this credential");
133
- lines.push(" at the stated proof levels. This credential was issued at the");
134
- lines.push(" time of the governed run and has not been altered since.");
132
+ lines.push(" Controls ran on the actions listed in this credential at the");
133
+ lines.push(" stated proof levels. This credential was issued at the time");
134
+ lines.push(" of the governed run and has not been altered since.");
135
135
  lines.push("");
136
136
  lines.push("WHAT THIS DOES NOT PROVE");
137
- lines.push(" That the checks run constitute sufficient evidence for any");
137
+ lines.push(" That the controls run constitute sufficient evidence for any");
138
138
  lines.push(" specific regulatory requirement. That determination belongs");
139
139
  lines.push(" to the customer, their compliance function, and their auditors.");
140
140
  lines.push("");
package/dist/index.d.ts CHANGED
@@ -28,6 +28,20 @@ interface VerifyOptions {
28
28
  skip_network?: boolean;
29
29
  /** Path to custom public key PEM (for enterprise self-hosting). */
30
30
  trust_root?: string;
31
+ /**
32
+ * CRL: when set, the verifier consults the public revocation endpoint
33
+ * and treats revoked credentials as invalid. Default undefined preserves
34
+ * the offline-first invariant — verify() makes no network calls unless
35
+ * this is provided. Pass e.g. "https://primust-api.fly.dev" (no trailing
36
+ * slash); the verifier appends "/api/v1/vpecs/{vpec_id}/revocation".
37
+ */
38
+ revocation_check_url?: string;
39
+ /**
40
+ * Timeout for the CRL fetch in milliseconds. On timeout or network
41
+ * error the verifier adds a warning but does NOT fail (revocation
42
+ * check is advisory; absence of a record is the default state).
43
+ */
44
+ revocation_check_timeout_ms?: number;
31
45
  }
32
46
  interface VerificationResult {
33
47
  vpec_id: string;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createUpstreamRootResolver,
4
4
  seedKeyCache,
5
5
  verify
6
- } from "./chunk-ZADQUKKN.js";
6
+ } from "./chunk-UB2YSMJ6.js";
7
7
  import {
8
8
  ALLOWED_KINDS,
9
9
  ALLOWED_TRUST_EDGES,
@@ -20,9 +20,15 @@ import {
20
20
  validateRecordsAgainstHarness,
21
21
  validateRuntimeBindingHash,
22
22
  verifyV29
23
- } from "./chunk-LTWQK3HT.js";
23
+ } from "./chunk-BWJUVMT7.js";
24
24
 
25
25
  // src/verify-html-template.ts
26
+ function escapeHtml(value) {
27
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
28
+ }
29
+ function safePackJson(packData) {
30
+ return JSON.stringify(packData).replace(/</g, "\\u003c");
31
+ }
26
32
  function generateVerifyHtml(options) {
27
33
  const { publicKeyB64, packData } = options;
28
34
  return `<!DOCTYPE html>
@@ -61,7 +67,7 @@ function generateVerifyHtml(options) {
61
67
  </head>
62
68
  <body>
63
69
  <h1>Primust Evidence Pack Verification</h1>
64
- <div class="subtitle">Pack: ${packData.pack_id} &middot; Period: ${packData.period_start} to ${packData.period_end}</div>
70
+ <div class="subtitle">Pack: ${escapeHtml(packData.pack_id)} &middot; Period: ${escapeHtml(packData.period_start)} to ${escapeHtml(packData.period_end)}</div>
65
71
 
66
72
  <div id="status" class="status loading">Verifying...</div>
67
73
 
@@ -106,8 +112,8 @@ function generateVerifyHtml(options) {
106
112
  </div>
107
113
 
108
114
  <script>
109
- const PRIMUST_PUBLIC_KEY_B64 = "${publicKeyB64}";
110
- const PACK_DATA = ${JSON.stringify(packData)};
115
+ const PRIMUST_PUBLIC_KEY_B64 = "${escapeHtml(publicKeyB64)}";
116
+ const PACK_DATA = ${safePackJson(packData)};
111
117
 
112
118
  async function importKey(b64) {
113
119
  const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
@@ -139,20 +145,49 @@ function generateVerifyHtml(options) {
139
145
  if (!valid) allValid = false;
140
146
 
141
147
  const row = document.createElement('tr');
142
- row.innerHTML =
143
- '<td><span class="badge ' + (valid ? 'badge-pass' : 'badge-fail') + '">' + (valid ? 'VERIFIED' : 'NOT VERIFIED') + '</span></td>' +
144
- '<td><code>' + vpec.vpec_id + '</code></td>' +
145
- '<td>' + vpec.proof_level_floor + '</td>' +
146
- '<td>' + (vpec.provable_surface * 100).toFixed(0) + '%</td>' +
147
- '<td>' + vpec.gap_count + '</td>' +
148
- '<td>' + vpec.issued_at + '</td>';
148
+
149
+ const statusTd = document.createElement('td');
150
+ const statusBadge = document.createElement('span');
151
+ statusBadge.className = 'badge ' + (valid ? 'badge-pass' : 'badge-fail');
152
+ statusBadge.textContent = valid ? 'VERIFIED' : 'NOT VERIFIED';
153
+ statusTd.appendChild(statusBadge);
154
+ row.appendChild(statusTd);
155
+
156
+ const idTd = document.createElement('td');
157
+ const idCode = document.createElement('code');
158
+ idCode.textContent = vpec.vpec_id;
159
+ idTd.appendChild(idCode);
160
+ row.appendChild(idTd);
161
+
162
+ const proofTd = document.createElement('td');
163
+ proofTd.textContent = vpec.proof_level_floor;
164
+ row.appendChild(proofTd);
165
+
166
+ const surfaceTd = document.createElement('td');
167
+ surfaceTd.textContent = (vpec.provable_surface * 100).toFixed(0) + '%';
168
+ row.appendChild(surfaceTd);
169
+
170
+ const gapsTd = document.createElement('td');
171
+ gapsTd.textContent = String(vpec.gap_count);
172
+ row.appendChild(gapsTd);
173
+
174
+ const issuedTd = document.createElement('td');
175
+ issuedTd.textContent = vpec.issued_at;
176
+ row.appendChild(issuedTd);
177
+
149
178
  tbody.appendChild(row);
150
179
  }
151
180
 
152
181
  // Render upstream chain if present
153
182
  if (PACK_DATA.upstream_vpecs && PACK_DATA.upstream_vpecs.length > 0) {
154
183
  const container = document.getElementById('chain-container');
155
- container.innerHTML = '<h2 style="font-size:1.1rem;margin-bottom:1rem;">Cross-Organization Chain</h2>';
184
+
185
+ // Chain header (static, no PACK_DATA \u2014 safe to construct directly).
186
+ const heading = document.createElement('h2');
187
+ heading.style.fontSize = '1.1rem';
188
+ heading.style.marginBottom = '1rem';
189
+ heading.textContent = 'Cross-Organization Chain';
190
+ container.appendChild(heading);
156
191
 
157
192
  for (const uv of PACK_DATA.upstream_vpecs) {
158
193
  let uvValid = false;
@@ -162,9 +197,25 @@ function generateVerifyHtml(options) {
162
197
 
163
198
  const node = document.createElement('div');
164
199
  node.className = 'chain-node';
165
- node.innerHTML =
166
- '<span class="badge ' + (uvValid ? 'badge-pass' : 'badge-fail') + '">' + (uvValid ? 'VERIFIED' : 'NOT VERIFIED') + '</span> ' +
167
- '<strong>' + uv.org_id + '</strong> &mdash; <code>' + uv.vpec_id + '</code> (' + uv.proof_level_floor + ')';
200
+
201
+ const badge = document.createElement('span');
202
+ badge.className = 'badge ' + (uvValid ? 'badge-pass' : 'badge-fail');
203
+ badge.textContent = uvValid ? 'VERIFIED' : 'NOT VERIFIED';
204
+ node.appendChild(badge);
205
+ node.appendChild(document.createTextNode(' '));
206
+
207
+ const orgStrong = document.createElement('strong');
208
+ orgStrong.textContent = uv.org_id;
209
+ node.appendChild(orgStrong);
210
+
211
+ node.appendChild(document.createTextNode(' \\u2014 '));
212
+
213
+ const idCode = document.createElement('code');
214
+ idCode.textContent = uv.vpec_id;
215
+ node.appendChild(idCode);
216
+
217
+ node.appendChild(document.createTextNode(' (' + uv.proof_level_floor + ')'));
218
+
168
219
  container.appendChild(node);
169
220
 
170
221
  const arrow = document.createElement('div');
@@ -175,7 +226,17 @@ function generateVerifyHtml(options) {
175
226
 
176
227
  const currentNode = document.createElement('div');
177
228
  currentNode.className = 'chain-node current';
178
- currentNode.innerHTML = '<strong>This Pack</strong> &mdash; <code>' + PACK_DATA.pack_id + '</code>';
229
+
230
+ const labelStrong = document.createElement('strong');
231
+ labelStrong.textContent = 'This Pack';
232
+ currentNode.appendChild(labelStrong);
233
+
234
+ currentNode.appendChild(document.createTextNode(' \\u2014 '));
235
+
236
+ const packCode = document.createElement('code');
237
+ packCode.textContent = PACK_DATA.pack_id;
238
+ currentNode.appendChild(packCode);
239
+
179
240
  container.appendChild(currentNode);
180
241
  }
181
242
 
@@ -0,0 +1,235 @@
1
+ // src/tsa-chain.ts
2
+ import { X509Certificate, createHash } from "crypto";
3
+ var DIGICERT_TRUSTED_ROOT_G4_FPR_HEX = "552f7bdcf1a7af9e6ce672017f4f12abf77240c78e761ac203d1d9d20ac89988";
4
+ var TIME_STAMPING_EKU_OID = "1.3.6.1.5.5.7.3.8";
5
+ var DIGICERT_TRUSTED_ROOT_G4_PEM = `-----BEGIN CERTIFICATE-----
6
+ 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
36
+ -----END CERTIFICATE-----`;
37
+ function loadBundledRoots() {
38
+ const out = [];
39
+ try {
40
+ out.push(new X509Certificate(DIGICERT_TRUSTED_ROOT_G4_PEM));
41
+ } catch {
42
+ }
43
+ return out;
44
+ }
45
+ var BUNDLED_ROOTS = loadBundledRoots();
46
+ function trustedRootFprs() {
47
+ const env = globalThis.process?.env?.PRIMUST_TSA_ROOT_FPR;
48
+ if (!env || !env.trim()) {
49
+ return /* @__PURE__ */ new Set([DIGICERT_TRUSTED_ROOT_G4_FPR_HEX]);
50
+ }
51
+ return new Set(
52
+ env.split(",").map((f) => f.trim().toLowerCase().replace(/:/g, "")).filter(Boolean)
53
+ );
54
+ }
55
+ function verifyTsaChain(tsTokenB64) {
56
+ let tokenDer;
57
+ try {
58
+ tokenDer = Buffer.from(tsTokenB64, "base64");
59
+ } catch {
60
+ return { ok: false, reason: "parse_error" };
61
+ }
62
+ let certs;
63
+ try {
64
+ certs = extractCertificates(tokenDer);
65
+ } catch {
66
+ return { ok: false, reason: "parse_error" };
67
+ }
68
+ if (certs.length === 0) {
69
+ return { ok: false, reason: "no_certs" };
70
+ }
71
+ const trusted = trustedRootFprs();
72
+ let leaf = null;
73
+ for (const c of certs) {
74
+ if (hasTimeStampingEku(c)) {
75
+ leaf = c;
76
+ break;
77
+ }
78
+ }
79
+ if (!leaf) {
80
+ const subjects = new Set(certs.map((c) => c.subject));
81
+ const issuers = new Set(certs.map((c) => c.issuer));
82
+ for (const c of certs) {
83
+ if (subjects.has(c.subject) && !issuers.has(c.subject)) {
84
+ leaf = c;
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ if (!leaf) {
90
+ return { ok: false, reason: "leaf_not_found" };
91
+ }
92
+ if (!hasTimeStampingEku(leaf)) {
93
+ return { ok: false, reason: "missing_eku" };
94
+ }
95
+ if (!isWithinValidity(leaf)) {
96
+ return { ok: false, reason: "expired" };
97
+ }
98
+ const candidateRoots = BUNDLED_ROOTS.filter(
99
+ (r) => trusted.has(certFingerprint(r))
100
+ );
101
+ const opRootPem = globalThis.process?.env?.PRIMUST_TSA_ROOT_PEM;
102
+ if (opRootPem) {
103
+ try {
104
+ const opRoot = new X509Certificate(opRootPem);
105
+ if (trusted.has(certFingerprint(opRoot))) {
106
+ candidateRoots.push(opRoot);
107
+ }
108
+ } catch {
109
+ return { ok: false, reason: "parse_error" };
110
+ }
111
+ }
112
+ if (candidateRoots.length === 0) {
113
+ return { ok: false, reason: "root_not_trusted" };
114
+ }
115
+ const bySubject = /* @__PURE__ */ new Map();
116
+ for (const c of certs) bySubject.set(c.subject, c);
117
+ const byRootSubject = /* @__PURE__ */ new Map();
118
+ for (const r of candidateRoots) byRootSubject.set(r.subject, r);
119
+ let current = leaf;
120
+ const seen = /* @__PURE__ */ new Set();
121
+ for (let hop = 0; hop < 8; hop++) {
122
+ if (seen.has(current.subject)) {
123
+ return { ok: false, reason: "chain_incomplete" };
124
+ }
125
+ seen.add(current.subject);
126
+ const rootCert = byRootSubject.get(current.issuer);
127
+ if (rootCert) {
128
+ if (!current.verify(rootCert.publicKey)) {
129
+ return { ok: false, reason: "signature_invalid" };
130
+ }
131
+ if (!isWithinValidity(rootCert)) {
132
+ return { ok: false, reason: "expired" };
133
+ }
134
+ return { ok: true, reason: "ok" };
135
+ }
136
+ const issuerCert = bySubject.get(current.issuer);
137
+ if (issuerCert && issuerCert !== current) {
138
+ if (!current.verify(issuerCert.publicKey)) {
139
+ return { ok: false, reason: "signature_invalid" };
140
+ }
141
+ if (!isWithinValidity(issuerCert)) {
142
+ return { ok: false, reason: "expired" };
143
+ }
144
+ if (issuerCert.subject === issuerCert.issuer) {
145
+ if (trusted.has(certFingerprint(issuerCert))) {
146
+ return { ok: true, reason: "ok" };
147
+ }
148
+ return { ok: false, reason: "root_not_trusted" };
149
+ }
150
+ current = issuerCert;
151
+ continue;
152
+ }
153
+ return { ok: false, reason: "chain_incomplete" };
154
+ }
155
+ return { ok: false, reason: "chain_incomplete" };
156
+ }
157
+ function certFingerprint(cert) {
158
+ return createHash("sha256").update(cert.raw).digest("hex");
159
+ }
160
+ function hasTimeStampingEku(cert) {
161
+ const info = cert.toString();
162
+ return info.includes(TIME_STAMPING_EKU_OID) || info.includes("Time Stamping");
163
+ }
164
+ function isWithinValidity(cert) {
165
+ const now = Date.now();
166
+ const nb = Date.parse(cert.validFrom);
167
+ const na = Date.parse(cert.validTo);
168
+ if (Number.isNaN(nb) || Number.isNaN(na)) return false;
169
+ return nb <= now && now <= na;
170
+ }
171
+ function extractCertificates(tokenDer) {
172
+ const signedDataOid = Buffer.from([
173
+ 6,
174
+ 9,
175
+ 42,
176
+ 134,
177
+ 72,
178
+ 134,
179
+ 247,
180
+ 13,
181
+ 1,
182
+ 7,
183
+ 2
184
+ ]);
185
+ const oidIdx = tokenDer.indexOf(signedDataOid);
186
+ if (oidIdx === -1) return [];
187
+ const scanStart = oidIdx + signedDataOid.length;
188
+ const limit = tokenDer.length;
189
+ for (let i = scanStart; i < limit - 2; i++) {
190
+ if (tokenDer[i] === 160) {
191
+ const lenInfo = parseDerLength(tokenDer, i + 1);
192
+ if (!lenInfo) continue;
193
+ const { length, headerLen } = lenInfo;
194
+ const setStart = i + 1 + headerLen;
195
+ const setEnd = setStart + length;
196
+ if (setEnd > limit) continue;
197
+ const certs = [];
198
+ let p = setStart;
199
+ while (p < setEnd) {
200
+ if (tokenDer[p] !== 48) break;
201
+ const li = parseDerLength(tokenDer, p + 1);
202
+ if (!li) break;
203
+ const certLen = li.length;
204
+ const certHdr = li.headerLen;
205
+ const certEnd = p + 1 + certHdr + certLen;
206
+ if (certEnd > setEnd) break;
207
+ const certDer = tokenDer.subarray(p, certEnd);
208
+ try {
209
+ certs.push(new X509Certificate(certDer));
210
+ } catch {
211
+ }
212
+ p = certEnd;
213
+ }
214
+ if (certs.length > 0) return certs;
215
+ }
216
+ }
217
+ return [];
218
+ }
219
+ function parseDerLength(buf, offset) {
220
+ if (offset >= buf.length) return null;
221
+ const first = buf[offset];
222
+ if ((first & 128) === 0) {
223
+ return { length: first, headerLen: 1 };
224
+ }
225
+ const n = first & 127;
226
+ if (n === 0 || n > 4 || offset + n >= buf.length) return null;
227
+ let val = 0;
228
+ for (let i = 0; i < n; i++) {
229
+ val = val << 8 | buf[offset + 1 + i];
230
+ }
231
+ return { length: val, headerLen: 1 + n };
232
+ }
233
+ export {
234
+ verifyTsaChain
235
+ };
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ALLOWED_KINDS,
3
+ ALLOWED_PROVENANCE,
3
4
  ALLOWED_TRUST_EDGES,
4
5
  ENVELOPE_VERSION,
5
6
  PROOF_TIER_HIERARCHY,
@@ -18,9 +19,10 @@ import {
18
19
  validateRuntimeBindingHash,
19
20
  validateRuntimeBindingSignature,
20
21
  verifyV29
21
- } from "./chunk-LTWQK3HT.js";
22
+ } from "./chunk-BWJUVMT7.js";
22
23
  export {
23
24
  ALLOWED_KINDS,
25
+ ALLOWED_PROVENANCE,
24
26
  ALLOWED_TRUST_EDGES,
25
27
  ENVELOPE_VERSION,
26
28
  PROOF_TIER_HIERARCHY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primust/verifier",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Offline and CLI verifier for Primust VPECs. Free forever. No account required.",
5
5
  "homepage": "https://primust.com",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "tsx": "^4.19.0",
35
35
  "typescript": "^5.6.0",
36
36
  "vitest": "^3.0.0",
37
- "@primust/artifact-core": "^1.0.0",
37
+ "@primust/artifact-core": "workspace:*",
38
38
  "@noble/hashes": "^1.8.0",
39
39
  "@zkpassport/poseidon2": "^0.6.2"
40
40
  },
@@ -42,6 +42,9 @@
42
42
  "dist"
43
43
  ],
44
44
  "license": "Apache-2.0",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
45
48
  "exports": {
46
49
  ".": {
47
50
  "import": "./dist/index.js",