@primust/verifier 1.3.0 → 1.3.1

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/index.d.ts CHANGED
@@ -110,18 +110,19 @@ interface VerificationResult {
110
110
  */
111
111
  declare function verify(artifact: Record<string, unknown>, options?: VerifyOptions, upstreamRootResolver?: UpstreamRootResolver): Promise<VerificationResult>;
112
112
 
113
- /**
114
- * verify.html template generator for Evidence Pack output.
115
- *
116
- * Generates a self-contained HTML file that:
117
- * 1. Embeds the Ed25519 public key
118
- * 2. Pre-loads the Evidence Pack summary
119
- * 3. Verifies using WebCrypto API on page load
120
- * 4. Works completely offline
121
- * 5. Shows chain of VPECs with verification status
122
- */
123
113
  interface VerifyHtmlOptions {
124
114
  publicKeyB64: string;
115
+ /**
116
+ * SHA-256 fingerprints (lowercase hex of the raw 32-byte Ed25519 public key)
117
+ * of keys the *reader* trusts as genuine Primust trust anchors. The page only
118
+ * shows the green "verified by Primust" banner when the embedded key's
119
+ * fingerprint is on this list. OMITTED → defaults to the pinned Primust
120
+ * production anchors (PRIMUST_TRUST_ANCHOR_FINGERPRINTS) so genuinely
121
+ * Primust-signed packs verify green out of the box. Pass an explicit list
122
+ * (e.g. a BYOK org key confirmed out-of-band) to override; pass [] to trust
123
+ * no key, forcing the "authenticity unconfirmed" caution for every pack.
124
+ */
125
+ trustedKeyFingerprints?: string[];
125
126
  packData: {
126
127
  pack_id: string;
127
128
  org_id: string;
package/dist/index.js CHANGED
@@ -23,14 +23,53 @@ import {
23
23
  } from "./chunk-BWJUVMT7.js";
24
24
 
25
25
  // src/verify-html-template.ts
26
+ var PRIMUST_TRUST_ANCHOR_FINGERPRINTS = [
27
+ "3bc0af2e434432a68a8b2216c4e69165e7456d3f7ba584bf595b30da81e80218",
28
+ // US
29
+ "9a582df3ec7e83ddeaa0220cbb2741cb940925f9be09f04b45840fb07481174d"
30
+ // EU
31
+ ];
26
32
  function escapeHtml(value) {
27
33
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
28
34
  }
29
35
  function safePackJson(packData) {
30
36
  return JSON.stringify(packData).replace(/</g, "\\u003c");
31
37
  }
38
+ function bannerDecision(state) {
39
+ if (!state.allSigsValid) {
40
+ return {
41
+ cls: "status failed",
42
+ text: "\u2717 NOT VERIFIED \u2014 One or more credential signatures did not match the embedded key"
43
+ };
44
+ }
45
+ if (state.vpecsLength <= 0) {
46
+ return {
47
+ cls: "status failed",
48
+ text: "\u2717 NOT VERIFIED \u2014 this pack contains no credentials; there is nothing to verify"
49
+ };
50
+ }
51
+ if (state.vpecCount !== state.vpecsLength) {
52
+ return {
53
+ cls: "status failed",
54
+ text: "\u2717 NOT VERIFIED \u2014 credential count mismatch: pack declares " + state.vpecCount + " but carries " + state.vpecsLength + ". The pack may have been altered."
55
+ };
56
+ }
57
+ if (state.keyTrusted) {
58
+ return {
59
+ cls: "status verified",
60
+ text: "\u2713 VERIFIED \u2014 All " + state.vpecsLength + " credentials verified against a recognized Primust trust anchor"
61
+ };
62
+ }
63
+ return {
64
+ cls: "status caution",
65
+ text: "\u26A0 SIGNATURES CONSISTENT, KEY UNVERIFIED \u2014 all " + state.vpecsLength + " signatures match the embedded key, but that key is not a recognized Primust trust anchor. Confirm the fingerprint above before relying on this result; a self-signed pack reaches this same state."
66
+ };
67
+ }
32
68
  function generateVerifyHtml(options) {
33
69
  const { publicKeyB64, packData } = options;
70
+ const trustedKeyFingerprints = (options.trustedKeyFingerprints ?? PRIMUST_TRUST_ANCHOR_FINGERPRINTS).map(
71
+ (fp) => String(fp).toLowerCase().replace(/[^0-9a-f]/g, "")
72
+ );
34
73
  return `<!DOCTYPE html>
35
74
  <html lang="en">
36
75
  <head>
@@ -44,8 +83,13 @@ function generateVerifyHtml(options) {
44
83
  .subtitle { color: #666; font-size: 0.9rem; margin-bottom: 2rem; }
45
84
  .status { padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; font-weight: 600; font-size: 1.1rem; }
46
85
  .status.verified { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
86
+ .status.caution { background: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
47
87
  .status.failed { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
48
88
  .status.loading { background: #e2e3e5; color: #383d41; border: 1px solid #d6d8db; }
89
+ .trust-panel { background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; }
90
+ .trust-panel .label { font-size: 0.7rem; color: #888; text-transform: uppercase; letter-spacing: 0.03em; }
91
+ .trust-panel .fingerprint { font-family: 'SF Mono', Monaco, monospace; font-size: 0.8rem; word-break: break-all; margin-top: 0.2rem; }
92
+ .trust-panel .disclosure { color: #666; margin-top: 0.6rem; line-height: 1.4; }
49
93
  .summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
50
94
  .stat { background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; }
51
95
  .stat-label { font-size: 0.75rem; color: #888; text-transform: uppercase; }
@@ -71,6 +115,20 @@ function generateVerifyHtml(options) {
71
115
 
72
116
  <div id="status" class="status loading">Verifying...</div>
73
117
 
118
+ <div class="trust-panel">
119
+ <div class="label">Signing key fingerprint (SHA-256)</div>
120
+ <div class="fingerprint" id="key-fingerprint">computing\u2026</div>
121
+ <div class="disclosure" id="trust-disclosure">
122
+ This page checks each credential's signature against the public key embedded
123
+ in this file. That proves the credentials are internally consistent with
124
+ this key \u2014 it does <strong>not</strong>, on its own, prove the key belongs
125
+ to Primust. Before relying on a "verified" result, confirm the fingerprint
126
+ above matches Primust's published key at
127
+ <code>https://keys.primust.com/.well-known/primust-pubkeys/</code>, or use
128
+ <code>pip install primust-verify</code> (which only trusts that host).
129
+ </div>
130
+ </div>
131
+
74
132
  <div class="summary">
75
133
  <div class="stat">
76
134
  <div class="stat-label">VPECs</div>
@@ -113,11 +171,29 @@ function generateVerifyHtml(options) {
113
171
 
114
172
  <script>
115
173
  const PRIMUST_PUBLIC_KEY_B64 = "${escapeHtml(publicKeyB64)}";
174
+ // Fingerprints of keys the reader trusts as genuine Primust trust anchors.
175
+ // Empty by default \u2014 see the trust-model note in verify-html-template.ts.
176
+ const TRUSTED_KEY_FINGERPRINTS = ${safePackJson(trustedKeyFingerprints)};
116
177
  const PACK_DATA = ${safePackJson(packData)};
117
178
 
179
+ function toHex(buf) {
180
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
181
+ }
182
+
183
+ // Banner verdict logic \u2014 embedded verbatim from verify-html-template.ts so
184
+ // the page runs the exact function the test suite verifies behaviorally.
185
+ const bannerDecision = ${bannerDecision.toString()};
186
+
118
187
  async function importKey(b64) {
119
- const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
120
- return crypto.subtle.importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']);
188
+ // Accept base64 OR base64url (the JWKS 'x' field / SDK embeds base64url),
189
+ // so atob always yields the raw 32-byte key and the fingerprint is
190
+ // canonical (== sha256 of the raw key, matching the pinned anchors).
191
+ const std = String(b64).replace(/-/g, '+').replace(/_/g, '/').replace(/\\s/g, '');
192
+ const padded = std + '='.repeat((4 - (std.length % 4)) % 4);
193
+ const raw = Uint8Array.from(atob(padded), c => c.charCodeAt(0));
194
+ const fingerprint = toHex(await crypto.subtle.digest('SHA-256', raw));
195
+ const key = await crypto.subtle.importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']);
196
+ return { key, fingerprint };
121
197
  }
122
198
 
123
199
  async function verifySignature(key, signatureHex, payloadStr) {
@@ -132,7 +208,10 @@ function generateVerifyHtml(options) {
132
208
  let allValid = true;
133
209
 
134
210
  try {
135
- const key = await importKey(PRIMUST_PUBLIC_KEY_B64);
211
+ const { key, fingerprint } = await importKey(PRIMUST_PUBLIC_KEY_B64);
212
+ const fpEl = document.getElementById('key-fingerprint');
213
+ if (fpEl) fpEl.textContent = fingerprint;
214
+ const keyTrusted = TRUSTED_KEY_FINGERPRINTS.indexOf(fingerprint) !== -1;
136
215
 
137
216
  for (const vpec of PACK_DATA.vpecs) {
138
217
  let valid = false;
@@ -148,8 +227,10 @@ function generateVerifyHtml(options) {
148
227
 
149
228
  const statusTd = document.createElement('td');
150
229
  const statusBadge = document.createElement('span');
230
+ // Row badge reports signature-vs-embedded-key only. Whether that key
231
+ // is a genuine Primust anchor is the pack-level verdict in the banner.
151
232
  statusBadge.className = 'badge ' + (valid ? 'badge-pass' : 'badge-fail');
152
- statusBadge.textContent = valid ? 'VERIFIED' : 'NOT VERIFIED';
233
+ statusBadge.textContent = valid ? 'SIG OK' : 'SIG INVALID';
153
234
  statusTd.appendChild(statusBadge);
154
235
  row.appendChild(statusTd);
155
236
 
@@ -200,7 +281,7 @@ function generateVerifyHtml(options) {
200
281
 
201
282
  const badge = document.createElement('span');
202
283
  badge.className = 'badge ' + (uvValid ? 'badge-pass' : 'badge-fail');
203
- badge.textContent = uvValid ? 'VERIFIED' : 'NOT VERIFIED';
284
+ badge.textContent = uvValid ? 'SIG OK' : 'SIG INVALID';
204
285
  node.appendChild(badge);
205
286
  node.appendChild(document.createTextNode(' '));
206
287
 
@@ -240,10 +321,18 @@ function generateVerifyHtml(options) {
240
321
  container.appendChild(currentNode);
241
322
  }
242
323
 
243
- statusEl.className = 'status ' + (allValid ? 'verified' : 'failed');
244
- statusEl.textContent = allValid
245
- ? '\\u2713 VERIFIED \u2014 All ' + PACK_DATA.vpecs.length + ' credentials verified'
246
- : '\\u2717 NOT VERIFIED \u2014 Some credentials failed verification';
324
+ // Verdict: green requires valid sigs AND a recognized trust anchor AND
325
+ // a non-empty, count-consistent credential set. A consistent-but-
326
+ // unrecognized key (self-signed pack) \u2192 amber caution; empty/altered
327
+ // packs \u2192 failed. See bannerDecision() in verify-html-template.ts.
328
+ const verdict = bannerDecision({
329
+ allSigsValid: allValid,
330
+ keyTrusted: keyTrusted,
331
+ vpecCount: PACK_DATA.vpec_count,
332
+ vpecsLength: PACK_DATA.vpecs.length,
333
+ });
334
+ statusEl.className = verdict.cls;
335
+ statusEl.textContent = verdict.text;
247
336
  } catch (e) {
248
337
  statusEl.className = 'status failed';
249
338
  statusEl.textContent = 'Verification error: ' + e.message;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primust/verifier",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Offline and CLI verifier for Primust Governed-Execution Credentials. Free forever. No account required.",
5
5
  "homepage": "https://primust.com",
6
6
  "repository": {