@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.
- package/dist/{chunk-LTWQK3HT.js → chunk-BWJUVMT7.js} +9 -1
- package/dist/{chunk-ZADQUKKN.js → chunk-UB2YSMJ6.js} +105 -15
- package/dist/cli.js +13 -13
- package/dist/index.d.ts +14 -0
- package/dist/index.js +78 -17
- package/dist/tsa-chain-7KSQ5LAH.js +235 -0
- package/dist/{v29-envelope-GFVVA2S6.js → v29-envelope-JVSI5N3L.js} +3 -1
- package/package.json +5 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
2315
|
+
const imprintOk = verifyTimestampImprint(
|
|
2312
2316
|
tsAnchor.value,
|
|
2313
2317
|
timestampBody(artifact)
|
|
2314
2318
|
);
|
|
2315
|
-
if (
|
|
2319
|
+
if (imprintOk === false) {
|
|
2320
|
+
result.timestamp_anchor_valid = false;
|
|
2316
2321
|
result.warnings.push("rfc3161_imprint_mismatch");
|
|
2317
|
-
} else if (
|
|
2318
|
-
|
|
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.
|
|
2458
|
+
result.errors.push(`proof_artifacts_pending:${pendingArtifacts}`);
|
|
2431
2459
|
}
|
|
2432
|
-
if (
|
|
2433
|
-
result.
|
|
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-
|
|
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-
|
|
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(`
|
|
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
|
|
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("
|
|
109
|
+
lines.push("CONTROLS RUN");
|
|
110
110
|
const shown = records.slice(0, 10);
|
|
111
111
|
for (const r of shown) {
|
|
112
|
-
let
|
|
113
|
-
if (
|
|
114
|
-
const
|
|
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 = ` ${
|
|
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
|
|
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("
|
|
133
|
-
lines.push("
|
|
134
|
-
lines.push("
|
|
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
|
|
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-
|
|
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-
|
|
23
|
+
} from "./chunk-BWJUVMT7.js";
|
|
24
24
|
|
|
25
25
|
// src/verify-html-template.ts
|
|
26
|
+
function escapeHtml(value) {
|
|
27
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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} · Period: ${packData.period_start} to ${packData.period_end}</div>
|
|
70
|
+
<div class="subtitle">Pack: ${escapeHtml(packData.pack_id)} · 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 = ${
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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": "
|
|
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",
|