@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 +11 -10
- package/dist/index.js +98 -9
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
120
|
-
|
|
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 ? '
|
|
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 ? '
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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