@primust/verifier 1.0.1 → 1.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.
@@ -2308,14 +2308,32 @@ async function verify3(artifact, options = {}, upstreamRootResolver) {
2308
2308
  }
2309
2309
  const tsAnchor = artifact.timestamp_anchor;
2310
2310
  if (tsAnchor && tsAnchor.type === "rfc3161" && typeof tsAnchor.value === "string") {
2311
- result.timestamp_anchor_valid = verifyTimestampImprint(
2311
+ const imprintOk = verifyTimestampImprint(
2312
2312
  tsAnchor.value,
2313
2313
  timestampBody(artifact)
2314
2314
  );
2315
- if (result.timestamp_anchor_valid === false) {
2315
+ if (imprintOk === false) {
2316
+ result.timestamp_anchor_valid = false;
2316
2317
  result.warnings.push("rfc3161_imprint_mismatch");
2317
- } else if (result.timestamp_anchor_valid === true) {
2318
- result.warnings.push("rfc3161_tsa_cert_chain_not_verified");
2318
+ } else if (imprintOk === true) {
2319
+ let chain = null;
2320
+ try {
2321
+ const { verifyTsaChain } = await import("./tsa-chain-7KSQ5LAH.js");
2322
+ chain = verifyTsaChain(tsAnchor.value);
2323
+ } catch {
2324
+ chain = null;
2325
+ }
2326
+ if (chain === null) {
2327
+ result.timestamp_anchor_valid = true;
2328
+ result.warnings.push("rfc3161_tsa_chain_verifier_unavailable");
2329
+ } else if (chain.ok) {
2330
+ result.timestamp_anchor_valid = true;
2331
+ } else {
2332
+ result.timestamp_anchor_valid = false;
2333
+ result.warnings.push(`rfc3161_tsa_chain_invalid:${chain.reason}`);
2334
+ }
2335
+ } else {
2336
+ result.timestamp_anchor_valid = null;
2319
2337
  }
2320
2338
  } else {
2321
2339
  result.timestamp_anchor_valid = null;
@@ -2829,23 +2847,54 @@ function findBuffer(haystack, needle) {
2829
2847
  return -1;
2830
2848
  }
2831
2849
  var REKOR_API = "https://rekor.sigstore.dev/api/v1";
2850
+ var DEFAULT_REVOKED_KEYS_URLS = [
2851
+ "https://keys.primust.com/.well-known/primust-pubkeys/revoked.json",
2852
+ "https://keys.eu.primust.com/.well-known/primust-pubkeys/revoked.json"
2853
+ ];
2854
+ function revokedKeysUrls() {
2855
+ const env = globalThis.process?.env?.PRIMUST_REVOKED_KEYS_URLS;
2856
+ if (!env || !env.trim()) return DEFAULT_REVOKED_KEYS_URLS;
2857
+ return env.split(",").map((s) => s.trim()).filter(Boolean);
2858
+ }
2859
+ async function isKeyRevoked(fingerprintHex) {
2860
+ for (const url of revokedKeysUrls()) {
2861
+ try {
2862
+ const resp = await fetch(url, {
2863
+ headers: { Accept: "application/json" },
2864
+ signal: AbortSignal.timeout(3e3)
2865
+ });
2866
+ if (!resp.ok) continue;
2867
+ const body = await resp.json();
2868
+ const list = Array.isArray(body) ? body : Array.isArray(body.revoked) ? body.revoked : [];
2869
+ const needle = fingerprintHex.toLowerCase();
2870
+ for (const entry of list) {
2871
+ if (typeof entry === "string" && entry.toLowerCase().replace(/^sha256:/, "") === needle) {
2872
+ return true;
2873
+ }
2874
+ }
2875
+ return false;
2876
+ } catch {
2877
+ continue;
2878
+ }
2879
+ }
2880
+ return null;
2881
+ }
2832
2882
  async function checkRekor(publicKeyB64Url, kid) {
2883
+ void kid;
2833
2884
  try {
2834
2885
  const keyBytes = fromBase64Url(publicKeyB64Url);
2835
2886
  const fingerprint = createHash("sha256").update(Buffer.from(keyBytes)).digest("hex");
2887
+ const revoked = await isKeyRevoked(fingerprint);
2888
+ if (revoked === true) return "revoked";
2836
2889
  const resp = await fetch(`${REKOR_API}/index/retrieve`, {
2837
2890
  method: "POST",
2838
2891
  headers: { "Content-Type": "application/json" },
2839
2892
  body: JSON.stringify({ hash: `sha256:${fingerprint}` }),
2840
2893
  signal: AbortSignal.timeout(5e3)
2841
2894
  });
2842
- if (!resp.ok) {
2843
- return "unavailable";
2844
- }
2895
+ if (!resp.ok) return "unavailable";
2845
2896
  const entries = await resp.json();
2846
- if (!entries || entries.length === 0) {
2847
- return "not_found";
2848
- }
2897
+ if (!entries || entries.length === 0) return "not_found";
2849
2898
  return "active";
2850
2899
  } catch {
2851
2900
  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-NOADQWB6.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { readFileSync } from "fs";
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-NOADQWB6.js";
7
7
  import {
8
8
  ALLOWED_KINDS,
9
9
  ALLOWED_TRUST_EDGES,
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primust/verifier",
3
- "version": "1.0.1",
3
+ "version": "1.1.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": {