@peac/protocol 0.10.14 → 0.11.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/discovery.d.ts.map +1 -1
- package/dist/index.cjs +905 -783
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +906 -785
- package/dist/index.mjs.map +1 -1
- package/dist/issue.d.ts.map +1 -1
- package/dist/jwks-resolver.d.ts +88 -0
- package/dist/jwks-resolver.d.ts.map +1 -0
- package/dist/verifier-core.d.ts +0 -8
- package/dist/verifier-core.d.ts.map +1 -1
- package/dist/verify-local.cjs +9 -0
- package/dist/verify-local.cjs.map +1 -1
- package/dist/verify-local.d.ts +1 -1
- package/dist/verify-local.d.ts.map +1 -1
- package/dist/verify-local.mjs +10 -1
- package/dist/verify-local.mjs.map +1 -1
- package/dist/verify.d.ts +8 -1
- package/dist/verify.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { uuidv7 } from 'uuidv7';
|
|
|
2
2
|
import { sign, decode, verify, sha256Hex, computeJwkThumbprint, jwkToPublicKeyBytes, base64urlDecode } from '@peac/crypto';
|
|
3
3
|
export { base64urlDecode, base64urlEncode, computeJwkThumbprint, generateKeypair, jwkToPublicKeyBytes, sha256Bytes, sha256Hex, verify } from '@peac/crypto';
|
|
4
4
|
import { ZodError } from 'zod';
|
|
5
|
-
import { isValidPurposeToken, isCanonicalPurpose, isValidPurposeReason, isValidWorkflowContext, createWorkflowContextInvalidError, hasValidDagSemantics, createWorkflowDagInvalidError, WORKFLOW_EXTENSION_KEY, ReceiptClaims, createEvidenceNotJsonError, validateSubjectSnapshot,
|
|
5
|
+
import { isValidPurposeToken, isCanonicalPurpose, isValidPurposeReason, isValidWorkflowContext, createWorkflowContextInvalidError, hasValidDagSemantics, createWorkflowDagInvalidError, WORKFLOW_EXTENSION_KEY, validateKernelConstraints, createConstraintViolationError, ReceiptClaims, createEvidenceNotJsonError, validateSubjectSnapshot, PEAC_ISSUER_CONFIG_MAX_BYTES, PEAC_ISSUER_CONFIG_PATH, PEAC_POLICY_MAX_BYTES, PEAC_POLICY_PATH, PEAC_POLICY_FALLBACK_PATH, parseReceiptClaims, PEAC_RECEIPT_HEADER, PEAC_PURPOSE_HEADER, parsePurposeHeader, PEAC_PURPOSE_APPLIED_HEADER, PEAC_PURPOSE_REASON_HEADER } from '@peac/schema';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { VERIFIER_LIMITS, VERIFIER_NETWORK, VERIFIER_POLICY_VERSION, VERIFICATION_REPORT_VERSION, WIRE_TYPE } from '@peac/kernel';
|
|
8
8
|
|
|
@@ -139,6 +139,10 @@ async function issue(options) {
|
|
|
139
139
|
...options.purpose_enforced && { purpose_enforced: options.purpose_enforced },
|
|
140
140
|
...options.purpose_reason && { purpose_reason: options.purpose_reason }
|
|
141
141
|
};
|
|
142
|
+
const constraintResult = validateKernelConstraints(claims);
|
|
143
|
+
if (!constraintResult.valid) {
|
|
144
|
+
throw new IssueError(createConstraintViolationError(constraintResult.violations));
|
|
145
|
+
}
|
|
142
146
|
try {
|
|
143
147
|
ReceiptClaims.parse(claims);
|
|
144
148
|
} catch (err) {
|
|
@@ -147,7 +151,10 @@ async function issue(options) {
|
|
|
147
151
|
(issue2) => issue2.path.some((p) => p === "evidence" || p === "payment")
|
|
148
152
|
);
|
|
149
153
|
if (evidenceIssue && evidenceIssue.path.includes("evidence")) {
|
|
150
|
-
const peacError = createEvidenceNotJsonError(
|
|
154
|
+
const peacError = createEvidenceNotJsonError(
|
|
155
|
+
evidenceIssue.message,
|
|
156
|
+
evidenceIssue.path
|
|
157
|
+
);
|
|
151
158
|
throw new IssueError(peacError);
|
|
152
159
|
}
|
|
153
160
|
}
|
|
@@ -171,605 +178,253 @@ async function issueJws(options) {
|
|
|
171
178
|
const result = await issue(options);
|
|
172
179
|
return result.jws;
|
|
173
180
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
function parseIssuerConfig(json) {
|
|
182
|
+
let config;
|
|
183
|
+
if (typeof json === "string") {
|
|
184
|
+
const bytes = new TextEncoder().encode(json).length;
|
|
185
|
+
if (bytes > PEAC_ISSUER_CONFIG_MAX_BYTES) {
|
|
186
|
+
throw new Error(`Issuer config exceeds ${PEAC_ISSUER_CONFIG_MAX_BYTES} bytes (got ${bytes})`);
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
config = JSON.parse(json);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error("Issuer config is not valid JSON");
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
config = json;
|
|
195
|
+
}
|
|
196
|
+
if (typeof config !== "object" || config === null) {
|
|
197
|
+
throw new Error("Issuer config must be an object");
|
|
198
|
+
}
|
|
199
|
+
const obj = config;
|
|
200
|
+
if (typeof obj.version !== "string" || !obj.version) {
|
|
201
|
+
throw new Error("Missing required field: version");
|
|
202
|
+
}
|
|
203
|
+
if (typeof obj.issuer !== "string" || !obj.issuer) {
|
|
204
|
+
throw new Error("Missing required field: issuer");
|
|
205
|
+
}
|
|
206
|
+
if (typeof obj.jwks_uri !== "string" || !obj.jwks_uri) {
|
|
207
|
+
throw new Error("Missing required field: jwks_uri");
|
|
208
|
+
}
|
|
209
|
+
if (!obj.issuer.startsWith("https://")) {
|
|
210
|
+
throw new Error("issuer must be an HTTPS URL");
|
|
179
211
|
}
|
|
180
|
-
const discoveryUrl = `${issuerUrl}/.well-known/peac.txt`;
|
|
181
212
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
throw new Error(
|
|
213
|
+
new URL(obj.jwks_uri);
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error("jwks_uri must be a valid URL");
|
|
216
|
+
}
|
|
217
|
+
if (obj.verify_endpoint !== void 0) {
|
|
218
|
+
if (typeof obj.verify_endpoint !== "string") {
|
|
219
|
+
throw new Error("verify_endpoint must be a string");
|
|
220
|
+
}
|
|
221
|
+
if (!obj.verify_endpoint.startsWith("https://")) {
|
|
222
|
+
throw new Error("verify_endpoint must be an HTTPS URL");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (obj.receipt_versions !== void 0) {
|
|
226
|
+
if (!Array.isArray(obj.receipt_versions)) {
|
|
227
|
+
throw new Error("receipt_versions must be an array");
|
|
189
228
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!
|
|
193
|
-
throw new Error("
|
|
229
|
+
}
|
|
230
|
+
if (obj.algorithms !== void 0) {
|
|
231
|
+
if (!Array.isArray(obj.algorithms)) {
|
|
232
|
+
throw new Error("algorithms must be an array");
|
|
194
233
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
}
|
|
235
|
+
if (obj.payment_rails !== void 0) {
|
|
236
|
+
if (!Array.isArray(obj.payment_rails)) {
|
|
237
|
+
throw new Error("payment_rails must be an array");
|
|
198
238
|
}
|
|
199
|
-
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
version: obj.version,
|
|
242
|
+
issuer: obj.issuer,
|
|
243
|
+
jwks_uri: obj.jwks_uri,
|
|
244
|
+
verify_endpoint: obj.verify_endpoint,
|
|
245
|
+
receipt_versions: obj.receipt_versions,
|
|
246
|
+
algorithms: obj.algorithms,
|
|
247
|
+
payment_rails: obj.payment_rails,
|
|
248
|
+
security_contact: obj.security_contact
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function fetchIssuerConfig(issuerUrl) {
|
|
252
|
+
if (!issuerUrl.startsWith("https://")) {
|
|
253
|
+
throw new Error("Issuer URL must be https://");
|
|
254
|
+
}
|
|
255
|
+
const baseUrl = issuerUrl.replace(/\/$/, "");
|
|
256
|
+
const configUrl = `${baseUrl}${PEAC_ISSUER_CONFIG_PATH}`;
|
|
257
|
+
try {
|
|
258
|
+
const resp = await fetch(configUrl, {
|
|
200
259
|
headers: { Accept: "application/json" },
|
|
201
|
-
signal: AbortSignal.timeout(
|
|
260
|
+
signal: AbortSignal.timeout(1e4)
|
|
202
261
|
});
|
|
203
|
-
if (!
|
|
204
|
-
throw new Error(`
|
|
262
|
+
if (!resp.ok) {
|
|
263
|
+
throw new Error(`Issuer config fetch failed: ${resp.status}`);
|
|
264
|
+
}
|
|
265
|
+
const text = await resp.text();
|
|
266
|
+
const config = parseIssuerConfig(text);
|
|
267
|
+
const normalizedExpected = baseUrl.replace(/\/$/, "");
|
|
268
|
+
const normalizedActual = config.issuer.replace(/\/$/, "");
|
|
269
|
+
if (normalizedActual !== normalizedExpected) {
|
|
270
|
+
throw new Error(`Issuer mismatch: expected ${normalizedExpected}, got ${normalizedActual}`);
|
|
205
271
|
}
|
|
206
|
-
|
|
207
|
-
return jwks;
|
|
272
|
+
return config;
|
|
208
273
|
} catch (err) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
|
|
211
|
-
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Failed to fetch issuer config from ${issuerUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
276
|
+
{ cause: err }
|
|
277
|
+
);
|
|
212
278
|
}
|
|
213
279
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const jwks = await fetchJWKS(issuerUrl);
|
|
221
|
-
jwksCache.set(issuerUrl, {
|
|
222
|
-
keys: jwks,
|
|
223
|
-
expiresAt: now + CACHE_TTL_MS
|
|
224
|
-
});
|
|
225
|
-
return { jwks, fromCache: false };
|
|
280
|
+
function isJsonContent(text, contentType) {
|
|
281
|
+
if (contentType?.includes("application/json")) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
const firstChar = text.trimStart()[0];
|
|
285
|
+
return firstChar === "{";
|
|
226
286
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
287
|
+
function parsePolicyManifest(text, contentType) {
|
|
288
|
+
const bytes = new TextEncoder().encode(text).length;
|
|
289
|
+
if (bytes > PEAC_POLICY_MAX_BYTES) {
|
|
290
|
+
throw new Error(`Policy manifest exceeds ${PEAC_POLICY_MAX_BYTES} bytes (got ${bytes})`);
|
|
230
291
|
}
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
292
|
+
let manifest;
|
|
293
|
+
if (isJsonContent(text, contentType)) {
|
|
294
|
+
try {
|
|
295
|
+
manifest = JSON.parse(text);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error("Policy manifest is not valid JSON");
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
manifest = parseSimpleYaml(text);
|
|
234
301
|
}
|
|
235
|
-
|
|
302
|
+
if (typeof manifest.version !== "string" || !manifest.version) {
|
|
303
|
+
throw new Error("Missing required field: version");
|
|
304
|
+
}
|
|
305
|
+
if (!manifest.version.startsWith("peac-policy/")) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Invalid version format: "${manifest.version}". Must start with "peac-policy/" (e.g., "peac-policy/0.1")`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (manifest.usage !== "open" && manifest.usage !== "conditional") {
|
|
311
|
+
throw new Error('Missing or invalid field: usage (must be "open" or "conditional")');
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
version: manifest.version,
|
|
315
|
+
usage: manifest.usage,
|
|
316
|
+
purposes: manifest.purposes,
|
|
317
|
+
receipts: manifest.receipts,
|
|
318
|
+
attribution: manifest.attribution,
|
|
319
|
+
rate_limit: manifest.rate_limit,
|
|
320
|
+
daily_limit: manifest.daily_limit,
|
|
321
|
+
negotiate: manifest.negotiate,
|
|
322
|
+
contact: manifest.contact,
|
|
323
|
+
license: manifest.license,
|
|
324
|
+
price: manifest.price,
|
|
325
|
+
currency: manifest.currency,
|
|
326
|
+
payment_methods: manifest.payment_methods,
|
|
327
|
+
payment_endpoint: manifest.payment_endpoint
|
|
328
|
+
};
|
|
236
329
|
}
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
ok: false,
|
|
258
|
-
reason: "expired",
|
|
259
|
-
details: `Receipt expired at ${new Date(payload.exp * 1e3).toISOString()}`
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
const jwksFetchStart = performance.now();
|
|
263
|
-
const { jwks, fromCache } = await getJWKS(payload.iss);
|
|
264
|
-
if (!fromCache) {
|
|
265
|
-
jwksFetchTime = performance.now() - jwksFetchStart;
|
|
330
|
+
function parseSimpleYaml(text) {
|
|
331
|
+
const lines = text.split("\n");
|
|
332
|
+
const result = {};
|
|
333
|
+
if (text.includes("<<:")) {
|
|
334
|
+
throw new Error("YAML merge keys are not allowed");
|
|
335
|
+
}
|
|
336
|
+
if (text.includes("&") || text.includes("*")) {
|
|
337
|
+
throw new Error("YAML anchors and aliases are not allowed");
|
|
338
|
+
}
|
|
339
|
+
if (/!\w+/.test(text)) {
|
|
340
|
+
throw new Error("YAML custom tags are not allowed");
|
|
341
|
+
}
|
|
342
|
+
const docSeparators = text.match(/^---$/gm);
|
|
343
|
+
if (docSeparators && docSeparators.length > 1) {
|
|
344
|
+
throw new Error("Multi-document YAML is not allowed");
|
|
345
|
+
}
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
const trimmed = line.trim();
|
|
348
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed === "---") {
|
|
349
|
+
continue;
|
|
266
350
|
}
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
351
|
+
const colonIndex = trimmed.indexOf(":");
|
|
352
|
+
if (colonIndex === -1) continue;
|
|
353
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
354
|
+
let value = trimmed.slice(colonIndex + 1).trim();
|
|
355
|
+
if (value === "") {
|
|
356
|
+
value = void 0;
|
|
357
|
+
} else if (typeof value === "string") {
|
|
358
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
359
|
+
value = value.slice(1, -1);
|
|
360
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
361
|
+
const inner = value.slice(1, -1);
|
|
362
|
+
value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
363
|
+
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
364
|
+
value = parseFloat(value);
|
|
365
|
+
} else if (value === "true") {
|
|
366
|
+
value = true;
|
|
367
|
+
} else if (value === "false") {
|
|
368
|
+
value = false;
|
|
369
|
+
}
|
|
283
370
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (!result.valid) {
|
|
287
|
-
const durationMs = performance.now() - startTime;
|
|
288
|
-
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
289
|
-
receiptHash: hashReceipt(receiptJws),
|
|
290
|
-
valid: false,
|
|
291
|
-
reasonCode: "invalid_signature",
|
|
292
|
-
issuer: payload.iss,
|
|
293
|
-
kid: header.kid,
|
|
294
|
-
durationMs
|
|
295
|
-
});
|
|
296
|
-
return {
|
|
297
|
-
ok: false,
|
|
298
|
-
reason: "invalid_signature",
|
|
299
|
-
details: "Ed25519 signature verification failed"
|
|
300
|
-
};
|
|
371
|
+
if (value !== void 0) {
|
|
372
|
+
result[key] = value;
|
|
301
373
|
}
|
|
302
|
-
const validatedSnapshot = validateSubjectSnapshot(inputSnapshot);
|
|
303
|
-
const verifyTime = performance.now() - startTime;
|
|
304
|
-
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
305
|
-
receiptHash: hashReceipt(receiptJws),
|
|
306
|
-
valid: true,
|
|
307
|
-
issuer: payload.iss,
|
|
308
|
-
kid: header.kid,
|
|
309
|
-
durationMs: verifyTime
|
|
310
|
-
});
|
|
311
|
-
return {
|
|
312
|
-
ok: true,
|
|
313
|
-
claims: payload,
|
|
314
|
-
...validatedSnapshot && { subject_snapshot: validatedSnapshot },
|
|
315
|
-
perf: {
|
|
316
|
-
verify_ms: verifyTime,
|
|
317
|
-
...jwksFetchTime && { jwks_fetch_ms: jwksFetchTime }
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
} catch (err) {
|
|
321
|
-
const durationMs = performance.now() - startTime;
|
|
322
|
-
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
323
|
-
receiptHash: hashReceipt(receiptJws),
|
|
324
|
-
valid: false,
|
|
325
|
-
reasonCode: "verification_error",
|
|
326
|
-
durationMs
|
|
327
|
-
});
|
|
328
|
-
return {
|
|
329
|
-
ok: false,
|
|
330
|
-
reason: "verification_error",
|
|
331
|
-
details: err instanceof Error ? err.message : String(err)
|
|
332
|
-
};
|
|
333
374
|
}
|
|
375
|
+
return result;
|
|
334
376
|
}
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"CRYPTO_INVALID_KEY_LENGTH"
|
|
343
|
-
]);
|
|
344
|
-
var MAX_PARSE_ISSUES = 25;
|
|
345
|
-
function sanitizeParseIssues(issues) {
|
|
346
|
-
if (!Array.isArray(issues)) return void 0;
|
|
347
|
-
return issues.slice(0, MAX_PARSE_ISSUES).map((issue2) => ({
|
|
348
|
-
path: Array.isArray(issue2?.path) ? issue2.path.join(".") : "",
|
|
349
|
-
message: typeof issue2?.message === "string" ? issue2.message : String(issue2)
|
|
350
|
-
}));
|
|
351
|
-
}
|
|
352
|
-
async function verifyLocal(jws, publicKey, options = {}) {
|
|
353
|
-
const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
|
|
354
|
-
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
377
|
+
async function fetchPolicyManifest(baseUrl) {
|
|
378
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
379
|
+
throw new Error("Base URL must be https://");
|
|
380
|
+
}
|
|
381
|
+
const normalizedBase = baseUrl.replace(/\/$/, "");
|
|
382
|
+
const primaryUrl = `${normalizedBase}${PEAC_POLICY_PATH}`;
|
|
383
|
+
const fallbackUrl = `${normalizedBase}${PEAC_POLICY_FALLBACK_PATH}`;
|
|
355
384
|
try {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const pr = parseReceiptClaims(result.payload);
|
|
365
|
-
if (!pr.ok) {
|
|
366
|
-
return {
|
|
367
|
-
valid: false,
|
|
368
|
-
code: "E_INVALID_FORMAT",
|
|
369
|
-
message: `Receipt schema validation failed: ${pr.error.message}`,
|
|
370
|
-
details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
if (issuer !== void 0 && pr.claims.iss !== issuer) {
|
|
374
|
-
return {
|
|
375
|
-
valid: false,
|
|
376
|
-
code: "E_INVALID_ISSUER",
|
|
377
|
-
message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
if (audience !== void 0 && pr.claims.aud !== audience) {
|
|
381
|
-
return {
|
|
382
|
-
valid: false,
|
|
383
|
-
code: "E_INVALID_AUDIENCE",
|
|
384
|
-
message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
if (rid !== void 0 && pr.claims.rid !== rid) {
|
|
388
|
-
return {
|
|
389
|
-
valid: false,
|
|
390
|
-
code: "E_INVALID_RECEIPT_ID",
|
|
391
|
-
message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
if (requireExp && pr.claims.exp === void 0) {
|
|
395
|
-
return {
|
|
396
|
-
valid: false,
|
|
397
|
-
code: "E_MISSING_EXP",
|
|
398
|
-
message: "Receipt missing required exp claim"
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
if (pr.claims.iat > now + maxClockSkew) {
|
|
402
|
-
return {
|
|
403
|
-
valid: false,
|
|
404
|
-
code: "E_NOT_YET_VALID",
|
|
405
|
-
message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
|
|
409
|
-
return {
|
|
410
|
-
valid: false,
|
|
411
|
-
code: "E_EXPIRED",
|
|
412
|
-
message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
|
|
413
|
-
};
|
|
385
|
+
const resp = await fetch(primaryUrl, {
|
|
386
|
+
headers: { Accept: "text/plain, application/json" },
|
|
387
|
+
signal: AbortSignal.timeout(5e3)
|
|
388
|
+
});
|
|
389
|
+
if (resp.ok) {
|
|
390
|
+
const text = await resp.text();
|
|
391
|
+
const contentType = resp.headers.get("content-type") || void 0;
|
|
392
|
+
return parsePolicyManifest(text, contentType);
|
|
414
393
|
}
|
|
415
|
-
if (
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
valid: true,
|
|
426
|
-
variant: "commerce",
|
|
427
|
-
claims,
|
|
428
|
-
kid: result.header.kid,
|
|
429
|
-
policy_binding: "unavailable"
|
|
430
|
-
};
|
|
431
|
-
} else {
|
|
432
|
-
const claims = pr.claims;
|
|
433
|
-
if (subjectUri !== void 0 && claims.sub !== subjectUri) {
|
|
434
|
-
return {
|
|
435
|
-
valid: false,
|
|
436
|
-
code: "E_INVALID_SUBJECT",
|
|
437
|
-
message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
|
|
438
|
-
};
|
|
394
|
+
if (resp.status === 404) {
|
|
395
|
+
const fallbackResp = await fetch(fallbackUrl, {
|
|
396
|
+
headers: { Accept: "text/plain, application/json" },
|
|
397
|
+
signal: AbortSignal.timeout(5e3)
|
|
398
|
+
});
|
|
399
|
+
if (fallbackResp.ok) {
|
|
400
|
+
const text = await fallbackResp.text();
|
|
401
|
+
const contentType = fallbackResp.headers.get("content-type") || void 0;
|
|
402
|
+
return parsePolicyManifest(text, contentType);
|
|
439
403
|
}
|
|
440
|
-
|
|
441
|
-
valid: true,
|
|
442
|
-
variant: "attestation",
|
|
443
|
-
claims,
|
|
444
|
-
kid: result.header.kid,
|
|
445
|
-
policy_binding: "unavailable"
|
|
446
|
-
};
|
|
404
|
+
throw new Error("Policy manifest not found at primary or fallback location");
|
|
447
405
|
}
|
|
406
|
+
throw new Error(`Policy manifest fetch failed: ${resp.status}`);
|
|
448
407
|
} catch (err) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
code: "E_INVALID_FORMAT",
|
|
454
|
-
message: err.message
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
if (err.code === "CRYPTO_INVALID_SIGNATURE") {
|
|
458
|
-
return {
|
|
459
|
-
valid: false,
|
|
460
|
-
code: "E_INVALID_SIGNATURE",
|
|
461
|
-
message: err.message
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
|
|
466
|
-
const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
|
|
467
|
-
return {
|
|
468
|
-
valid: false,
|
|
469
|
-
code: "E_INVALID_FORMAT",
|
|
470
|
-
message: `Invalid receipt payload: ${syntaxMessage}`
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
474
|
-
return {
|
|
475
|
-
valid: false,
|
|
476
|
-
code: "E_INTERNAL",
|
|
477
|
-
message: `Unexpected verification error: ${message}`
|
|
478
|
-
};
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Failed to fetch policy manifest from ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
410
|
+
{ cause: err }
|
|
411
|
+
);
|
|
479
412
|
}
|
|
480
413
|
}
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return r.valid === true && r.variant === "attestation";
|
|
486
|
-
}
|
|
487
|
-
function setReceiptHeader(headers, receiptJws) {
|
|
488
|
-
headers.set(PEAC_RECEIPT_HEADER, receiptJws);
|
|
489
|
-
}
|
|
490
|
-
function getReceiptHeader(headers) {
|
|
491
|
-
return headers.get(PEAC_RECEIPT_HEADER);
|
|
492
|
-
}
|
|
493
|
-
function setVaryHeader(headers) {
|
|
494
|
-
const existing = headers.get("Vary");
|
|
495
|
-
if (existing) {
|
|
496
|
-
const varies = existing.split(",").map((v) => v.trim());
|
|
497
|
-
if (!varies.includes(PEAC_RECEIPT_HEADER)) {
|
|
498
|
-
headers.set("Vary", `${existing}, ${PEAC_RECEIPT_HEADER}`);
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
headers.set("Vary", PEAC_RECEIPT_HEADER);
|
|
414
|
+
function parseDiscovery(text) {
|
|
415
|
+
const bytes = new TextEncoder().encode(text).length;
|
|
416
|
+
if (bytes > 2e3) {
|
|
417
|
+
throw new Error(`Discovery manifest exceeds 2000 bytes (got ${bytes})`);
|
|
502
418
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (!value) {
|
|
507
|
-
return [];
|
|
419
|
+
const lines = text.trim().split("\n");
|
|
420
|
+
if (lines.length > 20) {
|
|
421
|
+
throw new Error(`Discovery manifest exceeds 20 lines (got ${lines.length})`);
|
|
508
422
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
function setPurposeReasonHeader(headers, reason) {
|
|
515
|
-
headers.set(PEAC_PURPOSE_REASON_HEADER, reason);
|
|
516
|
-
}
|
|
517
|
-
function setVaryPurposeHeader(headers) {
|
|
518
|
-
const existing = headers.get("Vary");
|
|
519
|
-
if (existing) {
|
|
520
|
-
const varies = existing.split(",").map((v) => v.trim());
|
|
521
|
-
if (!varies.includes(PEAC_PURPOSE_HEADER)) {
|
|
522
|
-
headers.set("Vary", `${existing}, ${PEAC_PURPOSE_HEADER}`);
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
headers.set("Vary", PEAC_PURPOSE_HEADER);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
function parseIssuerConfig(json) {
|
|
529
|
-
let config;
|
|
530
|
-
if (typeof json === "string") {
|
|
531
|
-
const bytes = new TextEncoder().encode(json).length;
|
|
532
|
-
if (bytes > PEAC_ISSUER_CONFIG_MAX_BYTES) {
|
|
533
|
-
throw new Error(`Issuer config exceeds ${PEAC_ISSUER_CONFIG_MAX_BYTES} bytes (got ${bytes})`);
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
config = JSON.parse(json);
|
|
537
|
-
} catch {
|
|
538
|
-
throw new Error("Issuer config is not valid JSON");
|
|
539
|
-
}
|
|
540
|
-
} else {
|
|
541
|
-
config = json;
|
|
542
|
-
}
|
|
543
|
-
if (typeof config !== "object" || config === null) {
|
|
544
|
-
throw new Error("Issuer config must be an object");
|
|
545
|
-
}
|
|
546
|
-
const obj = config;
|
|
547
|
-
if (typeof obj.version !== "string" || !obj.version) {
|
|
548
|
-
throw new Error("Missing required field: version");
|
|
549
|
-
}
|
|
550
|
-
if (typeof obj.issuer !== "string" || !obj.issuer) {
|
|
551
|
-
throw new Error("Missing required field: issuer");
|
|
552
|
-
}
|
|
553
|
-
if (typeof obj.jwks_uri !== "string" || !obj.jwks_uri) {
|
|
554
|
-
throw new Error("Missing required field: jwks_uri");
|
|
555
|
-
}
|
|
556
|
-
if (!obj.issuer.startsWith("https://")) {
|
|
557
|
-
throw new Error("issuer must be an HTTPS URL");
|
|
558
|
-
}
|
|
559
|
-
if (!obj.jwks_uri.startsWith("https://")) {
|
|
560
|
-
throw new Error("jwks_uri must be an HTTPS URL");
|
|
561
|
-
}
|
|
562
|
-
if (obj.verify_endpoint !== void 0) {
|
|
563
|
-
if (typeof obj.verify_endpoint !== "string") {
|
|
564
|
-
throw new Error("verify_endpoint must be a string");
|
|
565
|
-
}
|
|
566
|
-
if (!obj.verify_endpoint.startsWith("https://")) {
|
|
567
|
-
throw new Error("verify_endpoint must be an HTTPS URL");
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (obj.receipt_versions !== void 0) {
|
|
571
|
-
if (!Array.isArray(obj.receipt_versions)) {
|
|
572
|
-
throw new Error("receipt_versions must be an array");
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (obj.algorithms !== void 0) {
|
|
576
|
-
if (!Array.isArray(obj.algorithms)) {
|
|
577
|
-
throw new Error("algorithms must be an array");
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
if (obj.payment_rails !== void 0) {
|
|
581
|
-
if (!Array.isArray(obj.payment_rails)) {
|
|
582
|
-
throw new Error("payment_rails must be an array");
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
version: obj.version,
|
|
587
|
-
issuer: obj.issuer,
|
|
588
|
-
jwks_uri: obj.jwks_uri,
|
|
589
|
-
verify_endpoint: obj.verify_endpoint,
|
|
590
|
-
receipt_versions: obj.receipt_versions,
|
|
591
|
-
algorithms: obj.algorithms,
|
|
592
|
-
payment_rails: obj.payment_rails,
|
|
593
|
-
security_contact: obj.security_contact
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
async function fetchIssuerConfig(issuerUrl) {
|
|
597
|
-
if (!issuerUrl.startsWith("https://")) {
|
|
598
|
-
throw new Error("Issuer URL must be https://");
|
|
599
|
-
}
|
|
600
|
-
const baseUrl = issuerUrl.replace(/\/$/, "");
|
|
601
|
-
const configUrl = `${baseUrl}${PEAC_ISSUER_CONFIG_PATH}`;
|
|
602
|
-
try {
|
|
603
|
-
const resp = await fetch(configUrl, {
|
|
604
|
-
headers: { Accept: "application/json" },
|
|
605
|
-
signal: AbortSignal.timeout(1e4)
|
|
606
|
-
});
|
|
607
|
-
if (!resp.ok) {
|
|
608
|
-
throw new Error(`Issuer config fetch failed: ${resp.status}`);
|
|
609
|
-
}
|
|
610
|
-
const text = await resp.text();
|
|
611
|
-
const config = parseIssuerConfig(text);
|
|
612
|
-
const normalizedExpected = baseUrl.replace(/\/$/, "");
|
|
613
|
-
const normalizedActual = config.issuer.replace(/\/$/, "");
|
|
614
|
-
if (normalizedActual !== normalizedExpected) {
|
|
615
|
-
throw new Error(`Issuer mismatch: expected ${normalizedExpected}, got ${normalizedActual}`);
|
|
616
|
-
}
|
|
617
|
-
return config;
|
|
618
|
-
} catch (err) {
|
|
619
|
-
throw new Error(
|
|
620
|
-
`Failed to fetch issuer config from ${issuerUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
621
|
-
{ cause: err }
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
function isJsonContent(text, contentType) {
|
|
626
|
-
if (contentType?.includes("application/json")) {
|
|
627
|
-
return true;
|
|
628
|
-
}
|
|
629
|
-
const firstChar = text.trimStart()[0];
|
|
630
|
-
return firstChar === "{";
|
|
631
|
-
}
|
|
632
|
-
function parsePolicyManifest(text, contentType) {
|
|
633
|
-
const bytes = new TextEncoder().encode(text).length;
|
|
634
|
-
if (bytes > PEAC_POLICY_MAX_BYTES) {
|
|
635
|
-
throw new Error(`Policy manifest exceeds ${PEAC_POLICY_MAX_BYTES} bytes (got ${bytes})`);
|
|
636
|
-
}
|
|
637
|
-
let manifest;
|
|
638
|
-
if (isJsonContent(text, contentType)) {
|
|
639
|
-
try {
|
|
640
|
-
manifest = JSON.parse(text);
|
|
641
|
-
} catch {
|
|
642
|
-
throw new Error("Policy manifest is not valid JSON");
|
|
643
|
-
}
|
|
644
|
-
} else {
|
|
645
|
-
manifest = parseSimpleYaml(text);
|
|
646
|
-
}
|
|
647
|
-
if (typeof manifest.version !== "string" || !manifest.version) {
|
|
648
|
-
throw new Error("Missing required field: version");
|
|
649
|
-
}
|
|
650
|
-
if (!manifest.version.startsWith("peac-policy/")) {
|
|
651
|
-
throw new Error(
|
|
652
|
-
`Invalid version format: "${manifest.version}". Must start with "peac-policy/" (e.g., "peac-policy/0.1")`
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
if (manifest.usage !== "open" && manifest.usage !== "conditional") {
|
|
656
|
-
throw new Error('Missing or invalid field: usage (must be "open" or "conditional")');
|
|
657
|
-
}
|
|
658
|
-
return {
|
|
659
|
-
version: manifest.version,
|
|
660
|
-
usage: manifest.usage,
|
|
661
|
-
purposes: manifest.purposes,
|
|
662
|
-
receipts: manifest.receipts,
|
|
663
|
-
attribution: manifest.attribution,
|
|
664
|
-
rate_limit: manifest.rate_limit,
|
|
665
|
-
daily_limit: manifest.daily_limit,
|
|
666
|
-
negotiate: manifest.negotiate,
|
|
667
|
-
contact: manifest.contact,
|
|
668
|
-
license: manifest.license,
|
|
669
|
-
price: manifest.price,
|
|
670
|
-
currency: manifest.currency,
|
|
671
|
-
payment_methods: manifest.payment_methods,
|
|
672
|
-
payment_endpoint: manifest.payment_endpoint
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
function parseSimpleYaml(text) {
|
|
676
|
-
const lines = text.split("\n");
|
|
677
|
-
const result = {};
|
|
678
|
-
if (text.includes("<<:")) {
|
|
679
|
-
throw new Error("YAML merge keys are not allowed");
|
|
680
|
-
}
|
|
681
|
-
if (text.includes("&") || text.includes("*")) {
|
|
682
|
-
throw new Error("YAML anchors and aliases are not allowed");
|
|
683
|
-
}
|
|
684
|
-
if (/!\w+/.test(text)) {
|
|
685
|
-
throw new Error("YAML custom tags are not allowed");
|
|
686
|
-
}
|
|
687
|
-
const docSeparators = text.match(/^---$/gm);
|
|
688
|
-
if (docSeparators && docSeparators.length > 1) {
|
|
689
|
-
throw new Error("Multi-document YAML is not allowed");
|
|
690
|
-
}
|
|
691
|
-
for (const line of lines) {
|
|
692
|
-
const trimmed = line.trim();
|
|
693
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed === "---") {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
const colonIndex = trimmed.indexOf(":");
|
|
697
|
-
if (colonIndex === -1) continue;
|
|
698
|
-
const key = trimmed.slice(0, colonIndex).trim();
|
|
699
|
-
let value = trimmed.slice(colonIndex + 1).trim();
|
|
700
|
-
if (value === "") {
|
|
701
|
-
value = void 0;
|
|
702
|
-
} else if (typeof value === "string") {
|
|
703
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
704
|
-
value = value.slice(1, -1);
|
|
705
|
-
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
706
|
-
const inner = value.slice(1, -1);
|
|
707
|
-
value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
708
|
-
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
709
|
-
value = parseFloat(value);
|
|
710
|
-
} else if (value === "true") {
|
|
711
|
-
value = true;
|
|
712
|
-
} else if (value === "false") {
|
|
713
|
-
value = false;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (value !== void 0) {
|
|
717
|
-
result[key] = value;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return result;
|
|
721
|
-
}
|
|
722
|
-
async function fetchPolicyManifest(baseUrl) {
|
|
723
|
-
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
724
|
-
throw new Error("Base URL must be https://");
|
|
725
|
-
}
|
|
726
|
-
const normalizedBase = baseUrl.replace(/\/$/, "");
|
|
727
|
-
const primaryUrl = `${normalizedBase}${PEAC_POLICY_PATH}`;
|
|
728
|
-
const fallbackUrl = `${normalizedBase}${PEAC_POLICY_FALLBACK_PATH}`;
|
|
729
|
-
try {
|
|
730
|
-
const resp = await fetch(primaryUrl, {
|
|
731
|
-
headers: { Accept: "text/plain, application/json" },
|
|
732
|
-
signal: AbortSignal.timeout(5e3)
|
|
733
|
-
});
|
|
734
|
-
if (resp.ok) {
|
|
735
|
-
const text = await resp.text();
|
|
736
|
-
const contentType = resp.headers.get("content-type") || void 0;
|
|
737
|
-
return parsePolicyManifest(text, contentType);
|
|
738
|
-
}
|
|
739
|
-
if (resp.status === 404) {
|
|
740
|
-
const fallbackResp = await fetch(fallbackUrl, {
|
|
741
|
-
headers: { Accept: "text/plain, application/json" },
|
|
742
|
-
signal: AbortSignal.timeout(5e3)
|
|
743
|
-
});
|
|
744
|
-
if (fallbackResp.ok) {
|
|
745
|
-
const text = await fallbackResp.text();
|
|
746
|
-
const contentType = fallbackResp.headers.get("content-type") || void 0;
|
|
747
|
-
return parsePolicyManifest(text, contentType);
|
|
748
|
-
}
|
|
749
|
-
throw new Error("Policy manifest not found at primary or fallback location");
|
|
750
|
-
}
|
|
751
|
-
throw new Error(`Policy manifest fetch failed: ${resp.status}`);
|
|
752
|
-
} catch (err) {
|
|
753
|
-
throw new Error(
|
|
754
|
-
`Failed to fetch policy manifest from ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
755
|
-
{ cause: err }
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function parseDiscovery(text) {
|
|
760
|
-
const bytes = new TextEncoder().encode(text).length;
|
|
761
|
-
if (bytes > 2e3) {
|
|
762
|
-
throw new Error(`Discovery manifest exceeds 2000 bytes (got ${bytes})`);
|
|
763
|
-
}
|
|
764
|
-
const lines = text.trim().split("\n");
|
|
765
|
-
if (lines.length > 20) {
|
|
766
|
-
throw new Error(`Discovery manifest exceeds 20 lines (got ${lines.length})`);
|
|
767
|
-
}
|
|
768
|
-
const discovery = {};
|
|
769
|
-
for (const line of lines) {
|
|
770
|
-
const trimmed = line.trim();
|
|
771
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
772
|
-
continue;
|
|
423
|
+
const discovery = {};
|
|
424
|
+
for (const line of lines) {
|
|
425
|
+
const trimmed = line.trim();
|
|
426
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
427
|
+
continue;
|
|
773
428
|
}
|
|
774
429
|
if (trimmed.includes(":")) {
|
|
775
430
|
const [key, ...valueParts] = trimmed.split(":");
|
|
@@ -821,116 +476,6 @@ async function fetchDiscovery(issuerUrl) {
|
|
|
821
476
|
);
|
|
822
477
|
}
|
|
823
478
|
}
|
|
824
|
-
var DEFAULT_VERIFIER_LIMITS = {
|
|
825
|
-
max_receipt_bytes: VERIFIER_LIMITS.maxReceiptBytes,
|
|
826
|
-
max_jwks_bytes: VERIFIER_LIMITS.maxJwksBytes,
|
|
827
|
-
max_jwks_keys: VERIFIER_LIMITS.maxJwksKeys,
|
|
828
|
-
max_redirects: VERIFIER_LIMITS.maxRedirects,
|
|
829
|
-
fetch_timeout_ms: VERIFIER_LIMITS.fetchTimeoutMs,
|
|
830
|
-
max_extension_bytes: VERIFIER_LIMITS.maxExtensionBytes
|
|
831
|
-
};
|
|
832
|
-
var DEFAULT_NETWORK_SECURITY = {
|
|
833
|
-
https_only: VERIFIER_NETWORK.httpsOnly,
|
|
834
|
-
block_private_ips: VERIFIER_NETWORK.blockPrivateIps,
|
|
835
|
-
allow_redirects: VERIFIER_NETWORK.allowRedirects,
|
|
836
|
-
allow_cross_origin_redirects: true,
|
|
837
|
-
// Allow for CDN compatibility
|
|
838
|
-
dns_failure_behavior: "block"
|
|
839
|
-
// Fail-closed by default
|
|
840
|
-
};
|
|
841
|
-
function createDefaultPolicy(mode) {
|
|
842
|
-
return {
|
|
843
|
-
policy_version: VERIFIER_POLICY_VERSION,
|
|
844
|
-
mode,
|
|
845
|
-
limits: { ...DEFAULT_VERIFIER_LIMITS },
|
|
846
|
-
network: { ...DEFAULT_NETWORK_SECURITY }
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
var CHECK_IDS = [
|
|
850
|
-
"jws.parse",
|
|
851
|
-
"limits.receipt_bytes",
|
|
852
|
-
"jws.protected_header",
|
|
853
|
-
"claims.schema_unverified",
|
|
854
|
-
"issuer.trust_policy",
|
|
855
|
-
"issuer.discovery",
|
|
856
|
-
"key.resolve",
|
|
857
|
-
"jws.signature",
|
|
858
|
-
"claims.time_window",
|
|
859
|
-
"extensions.limits",
|
|
860
|
-
"transport.profile_binding",
|
|
861
|
-
"policy.binding"
|
|
862
|
-
];
|
|
863
|
-
var NON_DETERMINISTIC_ARTIFACT_KEYS = [
|
|
864
|
-
"issuer_jwks_digest"
|
|
865
|
-
];
|
|
866
|
-
function createDigest(hexValue) {
|
|
867
|
-
return {
|
|
868
|
-
alg: "sha-256",
|
|
869
|
-
value: hexValue.toLowerCase()
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
function createEmptyReport(policy) {
|
|
873
|
-
return {
|
|
874
|
-
report_version: VERIFICATION_REPORT_VERSION,
|
|
875
|
-
policy
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
function ssrfErrorToReasonCode(ssrfReason, fetchType) {
|
|
879
|
-
const prefix = fetchType === "key" ? "key_fetch" : "pointer_fetch";
|
|
880
|
-
switch (ssrfReason) {
|
|
881
|
-
case "not_https":
|
|
882
|
-
case "private_ip":
|
|
883
|
-
case "loopback":
|
|
884
|
-
case "link_local":
|
|
885
|
-
case "cross_origin_redirect":
|
|
886
|
-
case "dns_failure":
|
|
887
|
-
return `${prefix}_blocked`;
|
|
888
|
-
case "timeout":
|
|
889
|
-
return `${prefix}_timeout`;
|
|
890
|
-
case "response_too_large":
|
|
891
|
-
return fetchType === "pointer" ? "pointer_fetch_too_large" : "jwks_too_large";
|
|
892
|
-
case "jwks_too_many_keys":
|
|
893
|
-
return "jwks_too_many_keys";
|
|
894
|
-
case "too_many_redirects":
|
|
895
|
-
case "scheme_downgrade":
|
|
896
|
-
case "network_error":
|
|
897
|
-
case "invalid_url":
|
|
898
|
-
default:
|
|
899
|
-
return `${prefix}_failed`;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
function reasonCodeToSeverity(reason) {
|
|
903
|
-
if (reason === "ok") return "info";
|
|
904
|
-
return "error";
|
|
905
|
-
}
|
|
906
|
-
function reasonCodeToErrorCode(reason) {
|
|
907
|
-
const mapping = {
|
|
908
|
-
ok: "",
|
|
909
|
-
receipt_too_large: "E_VERIFY_RECEIPT_TOO_LARGE",
|
|
910
|
-
malformed_receipt: "E_VERIFY_MALFORMED_RECEIPT",
|
|
911
|
-
signature_invalid: "E_VERIFY_SIGNATURE_INVALID",
|
|
912
|
-
issuer_not_allowed: "E_VERIFY_ISSUER_NOT_ALLOWED",
|
|
913
|
-
key_not_found: "E_VERIFY_KEY_NOT_FOUND",
|
|
914
|
-
key_fetch_blocked: "E_VERIFY_KEY_FETCH_BLOCKED",
|
|
915
|
-
key_fetch_failed: "E_VERIFY_KEY_FETCH_FAILED",
|
|
916
|
-
key_fetch_timeout: "E_VERIFY_KEY_FETCH_TIMEOUT",
|
|
917
|
-
pointer_fetch_blocked: "E_VERIFY_POINTER_FETCH_BLOCKED",
|
|
918
|
-
pointer_fetch_failed: "E_VERIFY_POINTER_FETCH_FAILED",
|
|
919
|
-
pointer_fetch_timeout: "E_VERIFY_POINTER_FETCH_TIMEOUT",
|
|
920
|
-
pointer_fetch_too_large: "E_VERIFY_POINTER_FETCH_TOO_LARGE",
|
|
921
|
-
pointer_digest_mismatch: "E_VERIFY_POINTER_DIGEST_MISMATCH",
|
|
922
|
-
jwks_too_large: "E_VERIFY_JWKS_TOO_LARGE",
|
|
923
|
-
jwks_too_many_keys: "E_VERIFY_JWKS_TOO_MANY_KEYS",
|
|
924
|
-
expired: "E_VERIFY_EXPIRED",
|
|
925
|
-
not_yet_valid: "E_VERIFY_NOT_YET_VALID",
|
|
926
|
-
audience_mismatch: "E_VERIFY_AUDIENCE_MISMATCH",
|
|
927
|
-
schema_invalid: "E_VERIFY_SCHEMA_INVALID",
|
|
928
|
-
policy_violation: "E_VERIFY_POLICY_VIOLATION",
|
|
929
|
-
extension_too_large: "E_VERIFY_EXTENSION_TOO_LARGE",
|
|
930
|
-
invalid_transport: "E_VERIFY_INVALID_TRANSPORT"
|
|
931
|
-
};
|
|
932
|
-
return mapping[reason] || "E_VERIFY_POLICY_VIOLATION";
|
|
933
|
-
}
|
|
934
479
|
var cachedCapabilities = null;
|
|
935
480
|
function getSSRFCapabilities() {
|
|
936
481
|
if (cachedCapabilities) {
|
|
@@ -1338,49 +883,685 @@ async function ssrfSafeFetch(url, options = {}) {
|
|
|
1338
883
|
}, new Uint8Array());
|
|
1339
884
|
const body = new TextDecoder().decode(rawBytes);
|
|
1340
885
|
return {
|
|
1341
|
-
ok: true,
|
|
1342
|
-
status: response.status,
|
|
1343
|
-
body,
|
|
1344
|
-
rawBytes,
|
|
1345
|
-
contentType: response.headers.get("content-type") ?? void 0
|
|
886
|
+
ok: true,
|
|
887
|
+
status: response.status,
|
|
888
|
+
body,
|
|
889
|
+
rawBytes,
|
|
890
|
+
contentType: response.headers.get("content-type") ?? void 0
|
|
891
|
+
};
|
|
892
|
+
} catch (err) {
|
|
893
|
+
if (err instanceof Error) {
|
|
894
|
+
if (err.name === "AbortError" || err.message.includes("timeout")) {
|
|
895
|
+
return {
|
|
896
|
+
ok: false,
|
|
897
|
+
reason: "timeout",
|
|
898
|
+
message: `Fetch timeout after ${timeoutMs}ms: ${currentUrl}`
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
ok: false,
|
|
904
|
+
reason: "network_error",
|
|
905
|
+
message: `Network error: ${err instanceof Error ? err.message : String(err)}`
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async function fetchJWKSSafe(jwksUrl, options) {
|
|
911
|
+
return ssrfSafeFetch(jwksUrl, {
|
|
912
|
+
...options,
|
|
913
|
+
maxBytes: VERIFIER_LIMITS.maxJwksBytes,
|
|
914
|
+
headers: {
|
|
915
|
+
Accept: "application/json",
|
|
916
|
+
...options?.headers
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
async function fetchPointerSafe(pointerUrl, options) {
|
|
921
|
+
return ssrfSafeFetch(pointerUrl, {
|
|
922
|
+
...options,
|
|
923
|
+
maxBytes: VERIFIER_LIMITS.maxReceiptBytes,
|
|
924
|
+
headers: {
|
|
925
|
+
Accept: "application/jose, application/json",
|
|
926
|
+
...options?.headers
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/jwks-resolver.ts
|
|
932
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
933
|
+
var DEFAULT_MAX_CACHE_ENTRIES = 1e3;
|
|
934
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
935
|
+
function cacheGet(key, now) {
|
|
936
|
+
const entry = jwksCache.get(key);
|
|
937
|
+
if (!entry) return void 0;
|
|
938
|
+
if (entry.expiresAt <= now) {
|
|
939
|
+
jwksCache.delete(key);
|
|
940
|
+
return void 0;
|
|
941
|
+
}
|
|
942
|
+
jwksCache.delete(key);
|
|
943
|
+
jwksCache.set(key, entry);
|
|
944
|
+
return entry;
|
|
945
|
+
}
|
|
946
|
+
function cacheSet(key, entry, maxEntries) {
|
|
947
|
+
if (jwksCache.has(key)) jwksCache.delete(key);
|
|
948
|
+
jwksCache.set(key, entry);
|
|
949
|
+
while (jwksCache.size > maxEntries) {
|
|
950
|
+
const oldestKey = jwksCache.keys().next().value;
|
|
951
|
+
if (oldestKey !== void 0) jwksCache.delete(oldestKey);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function clearJWKSCache() {
|
|
955
|
+
jwksCache.clear();
|
|
956
|
+
}
|
|
957
|
+
function getJWKSCacheSize() {
|
|
958
|
+
return jwksCache.size;
|
|
959
|
+
}
|
|
960
|
+
function canonicalizeIssuerOrigin(issuerUrl) {
|
|
961
|
+
try {
|
|
962
|
+
const origin = new URL(issuerUrl).origin;
|
|
963
|
+
if (origin === "null") {
|
|
964
|
+
return { ok: false, message: `Issuer URL has no valid origin: ${issuerUrl}` };
|
|
965
|
+
}
|
|
966
|
+
return { ok: true, origin };
|
|
967
|
+
} catch {
|
|
968
|
+
return { ok: false, message: `Issuer URL is not a valid URL: ${issuerUrl}` };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function mapSSRFError(reason, context) {
|
|
972
|
+
let code;
|
|
973
|
+
switch (reason) {
|
|
974
|
+
case "not_https":
|
|
975
|
+
code = "E_VERIFY_INSECURE_SCHEME_BLOCKED";
|
|
976
|
+
break;
|
|
977
|
+
case "private_ip":
|
|
978
|
+
case "loopback":
|
|
979
|
+
case "link_local":
|
|
980
|
+
code = "E_VERIFY_KEY_FETCH_BLOCKED";
|
|
981
|
+
break;
|
|
982
|
+
case "timeout":
|
|
983
|
+
code = "E_VERIFY_KEY_FETCH_TIMEOUT";
|
|
984
|
+
break;
|
|
985
|
+
case "response_too_large":
|
|
986
|
+
code = context === "jwks" ? "E_VERIFY_JWKS_TOO_LARGE" : "E_VERIFY_KEY_FETCH_FAILED";
|
|
987
|
+
break;
|
|
988
|
+
case "dns_failure":
|
|
989
|
+
case "network_error":
|
|
990
|
+
case "too_many_redirects":
|
|
991
|
+
case "scheme_downgrade":
|
|
992
|
+
case "cross_origin_redirect":
|
|
993
|
+
case "invalid_url":
|
|
994
|
+
code = context === "issuer_config" ? "E_VERIFY_ISSUER_CONFIG_MISSING" : "E_VERIFY_KEY_FETCH_FAILED";
|
|
995
|
+
break;
|
|
996
|
+
case "jwks_too_many_keys":
|
|
997
|
+
code = "E_VERIFY_JWKS_TOO_MANY_KEYS";
|
|
998
|
+
break;
|
|
999
|
+
default:
|
|
1000
|
+
code = "E_VERIFY_KEY_FETCH_FAILED";
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
return { code, reason };
|
|
1004
|
+
}
|
|
1005
|
+
async function resolveJWKS(issuerUrl, options) {
|
|
1006
|
+
const cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1007
|
+
const maxCacheEntries = options?.maxCacheEntries ?? DEFAULT_MAX_CACHE_ENTRIES;
|
|
1008
|
+
const noCache = options?.noCache ?? false;
|
|
1009
|
+
const normalized = canonicalizeIssuerOrigin(issuerUrl);
|
|
1010
|
+
if (!normalized.ok) {
|
|
1011
|
+
return {
|
|
1012
|
+
ok: false,
|
|
1013
|
+
code: "E_VERIFY_ISSUER_CONFIG_INVALID",
|
|
1014
|
+
message: normalized.message,
|
|
1015
|
+
blockedUrl: issuerUrl
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
const normalizedIssuer = normalized.origin;
|
|
1019
|
+
const now = Date.now();
|
|
1020
|
+
if (!noCache) {
|
|
1021
|
+
const cached = cacheGet(normalizedIssuer, now);
|
|
1022
|
+
if (cached) {
|
|
1023
|
+
return { ok: true, jwks: cached.jwks, fromCache: true };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (!normalizedIssuer.startsWith("https://")) {
|
|
1027
|
+
return {
|
|
1028
|
+
ok: false,
|
|
1029
|
+
code: "E_VERIFY_INSECURE_SCHEME_BLOCKED",
|
|
1030
|
+
message: `Issuer URL must be HTTPS: ${normalizedIssuer}`,
|
|
1031
|
+
blockedUrl: normalizedIssuer
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const configUrl = `${normalizedIssuer}/.well-known/peac-issuer.json`;
|
|
1035
|
+
const configResult = await ssrfSafeFetch(configUrl, {
|
|
1036
|
+
maxBytes: PEAC_ISSUER_CONFIG_MAX_BYTES,
|
|
1037
|
+
headers: { Accept: "application/json" }
|
|
1038
|
+
});
|
|
1039
|
+
if (!configResult.ok) {
|
|
1040
|
+
const mapped = mapSSRFError(configResult.reason, "issuer_config");
|
|
1041
|
+
return {
|
|
1042
|
+
ok: false,
|
|
1043
|
+
code: mapped.code,
|
|
1044
|
+
message: `Failed to fetch peac-issuer.json: ${configResult.message}`,
|
|
1045
|
+
reason: mapped.reason,
|
|
1046
|
+
blockedUrl: configResult.blockedUrl
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
let issuerConfig;
|
|
1050
|
+
try {
|
|
1051
|
+
issuerConfig = parseIssuerConfig(configResult.body);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
return {
|
|
1054
|
+
ok: false,
|
|
1055
|
+
code: "E_VERIFY_ISSUER_CONFIG_INVALID",
|
|
1056
|
+
message: `Invalid peac-issuer.json: ${err instanceof Error ? err.message : String(err)}`
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const configIssuerResult = canonicalizeIssuerOrigin(issuerConfig.issuer);
|
|
1060
|
+
const configIssuer = configIssuerResult.ok ? configIssuerResult.origin : issuerConfig.issuer;
|
|
1061
|
+
if (configIssuer !== normalizedIssuer) {
|
|
1062
|
+
return {
|
|
1063
|
+
ok: false,
|
|
1064
|
+
code: "E_VERIFY_ISSUER_MISMATCH",
|
|
1065
|
+
message: `Issuer mismatch: expected ${normalizedIssuer}, got ${configIssuer}`
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
if (!issuerConfig.jwks_uri) {
|
|
1069
|
+
return {
|
|
1070
|
+
ok: false,
|
|
1071
|
+
code: "E_VERIFY_JWKS_URI_INVALID",
|
|
1072
|
+
message: "peac-issuer.json missing required jwks_uri field"
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
if (!issuerConfig.jwks_uri.startsWith("https://")) {
|
|
1076
|
+
return {
|
|
1077
|
+
ok: false,
|
|
1078
|
+
code: "E_VERIFY_JWKS_URI_INVALID",
|
|
1079
|
+
message: `jwks_uri must be HTTPS: ${issuerConfig.jwks_uri}`,
|
|
1080
|
+
blockedUrl: issuerConfig.jwks_uri
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
const jwksResult = await fetchJWKSSafe(issuerConfig.jwks_uri);
|
|
1084
|
+
if (!jwksResult.ok) {
|
|
1085
|
+
const mapped = mapSSRFError(jwksResult.reason, "jwks");
|
|
1086
|
+
return {
|
|
1087
|
+
ok: false,
|
|
1088
|
+
code: mapped.code,
|
|
1089
|
+
message: `Failed to fetch JWKS from ${issuerConfig.jwks_uri}: ${jwksResult.message}`,
|
|
1090
|
+
reason: mapped.reason,
|
|
1091
|
+
blockedUrl: jwksResult.blockedUrl
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
let jwks;
|
|
1095
|
+
try {
|
|
1096
|
+
jwks = JSON.parse(jwksResult.body);
|
|
1097
|
+
} catch {
|
|
1098
|
+
return {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
code: "E_VERIFY_JWKS_INVALID",
|
|
1101
|
+
message: "JWKS response is not valid JSON"
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (!jwks.keys || !Array.isArray(jwks.keys)) {
|
|
1105
|
+
return {
|
|
1106
|
+
ok: false,
|
|
1107
|
+
code: "E_VERIFY_JWKS_INVALID",
|
|
1108
|
+
message: "JWKS missing required keys array"
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (jwks.keys.length > VERIFIER_LIMITS.maxJwksKeys) {
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
code: "E_VERIFY_JWKS_TOO_MANY_KEYS",
|
|
1115
|
+
message: `JWKS has too many keys: ${jwks.keys.length} > ${VERIFIER_LIMITS.maxJwksKeys}`
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
if (!noCache) {
|
|
1119
|
+
cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs }, maxCacheEntries);
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
ok: true,
|
|
1123
|
+
jwks,
|
|
1124
|
+
fromCache: false,
|
|
1125
|
+
rawBytes: jwksResult.rawBytes
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/verify.ts
|
|
1130
|
+
function jwkToPublicKey(jwk) {
|
|
1131
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
|
|
1132
|
+
throw new Error("Only Ed25519 keys (OKP/Ed25519) are supported");
|
|
1133
|
+
}
|
|
1134
|
+
const xBytes = Buffer.from(jwk.x, "base64url");
|
|
1135
|
+
if (xBytes.length !== 32) {
|
|
1136
|
+
throw new Error("Ed25519 public key must be 32 bytes");
|
|
1137
|
+
}
|
|
1138
|
+
return new Uint8Array(xBytes);
|
|
1139
|
+
}
|
|
1140
|
+
async function verifyReceipt(optionsOrJws) {
|
|
1141
|
+
const receiptJws = typeof optionsOrJws === "string" ? optionsOrJws : optionsOrJws.receiptJws;
|
|
1142
|
+
const inputSnapshot = typeof optionsOrJws === "string" ? void 0 : optionsOrJws.subject_snapshot;
|
|
1143
|
+
const telemetry = typeof optionsOrJws === "string" ? void 0 : optionsOrJws.telemetry;
|
|
1144
|
+
const startTime = performance.now();
|
|
1145
|
+
let jwksFetchTime;
|
|
1146
|
+
try {
|
|
1147
|
+
const { header, payload } = decode(receiptJws);
|
|
1148
|
+
const constraintResult = validateKernelConstraints(payload);
|
|
1149
|
+
if (!constraintResult.valid) {
|
|
1150
|
+
const v = constraintResult.violations[0];
|
|
1151
|
+
return {
|
|
1152
|
+
ok: false,
|
|
1153
|
+
reason: "constraint_violation",
|
|
1154
|
+
details: `Kernel constraint violated: ${v.constraint} (actual: ${v.actual}, limit: ${v.limit})`
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
ReceiptClaims.parse(payload);
|
|
1158
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) {
|
|
1159
|
+
const durationMs = performance.now() - startTime;
|
|
1160
|
+
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
1161
|
+
receiptHash: hashReceipt(receiptJws),
|
|
1162
|
+
valid: false,
|
|
1163
|
+
reasonCode: "expired",
|
|
1164
|
+
issuer: payload.iss,
|
|
1165
|
+
kid: header.kid,
|
|
1166
|
+
durationMs
|
|
1167
|
+
});
|
|
1168
|
+
return {
|
|
1169
|
+
ok: false,
|
|
1170
|
+
reason: "expired",
|
|
1171
|
+
details: `Receipt expired at ${new Date(payload.exp * 1e3).toISOString()}`
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const jwksFetchStart = performance.now();
|
|
1175
|
+
const jwksResult = await resolveJWKS(payload.iss);
|
|
1176
|
+
if (!jwksResult.ok) {
|
|
1177
|
+
return {
|
|
1178
|
+
ok: false,
|
|
1179
|
+
reason: jwksResult.code,
|
|
1180
|
+
details: jwksResult.message
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (!jwksResult.fromCache) {
|
|
1184
|
+
jwksFetchTime = performance.now() - jwksFetchStart;
|
|
1185
|
+
}
|
|
1186
|
+
const jwk = jwksResult.jwks.keys.find((k) => k.kid === header.kid);
|
|
1187
|
+
if (!jwk) {
|
|
1188
|
+
const durationMs = performance.now() - startTime;
|
|
1189
|
+
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
1190
|
+
receiptHash: hashReceipt(receiptJws),
|
|
1191
|
+
valid: false,
|
|
1192
|
+
reasonCode: "unknown_key",
|
|
1193
|
+
issuer: payload.iss,
|
|
1194
|
+
kid: header.kid,
|
|
1195
|
+
durationMs
|
|
1196
|
+
});
|
|
1197
|
+
return {
|
|
1198
|
+
ok: false,
|
|
1199
|
+
reason: "unknown_key",
|
|
1200
|
+
details: `No key found with kid=${header.kid}`
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
const publicKey = jwkToPublicKey(jwk);
|
|
1204
|
+
const result = await verify(receiptJws, publicKey);
|
|
1205
|
+
if (!result.valid) {
|
|
1206
|
+
const durationMs = performance.now() - startTime;
|
|
1207
|
+
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
1208
|
+
receiptHash: hashReceipt(receiptJws),
|
|
1209
|
+
valid: false,
|
|
1210
|
+
reasonCode: "invalid_signature",
|
|
1211
|
+
issuer: payload.iss,
|
|
1212
|
+
kid: header.kid,
|
|
1213
|
+
durationMs
|
|
1214
|
+
});
|
|
1215
|
+
return {
|
|
1216
|
+
ok: false,
|
|
1217
|
+
reason: "invalid_signature",
|
|
1218
|
+
details: "Ed25519 signature verification failed"
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const validatedSnapshot = validateSubjectSnapshot(inputSnapshot);
|
|
1222
|
+
const verifyTime = performance.now() - startTime;
|
|
1223
|
+
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
1224
|
+
receiptHash: hashReceipt(receiptJws),
|
|
1225
|
+
valid: true,
|
|
1226
|
+
issuer: payload.iss,
|
|
1227
|
+
kid: header.kid,
|
|
1228
|
+
durationMs: verifyTime
|
|
1229
|
+
});
|
|
1230
|
+
return {
|
|
1231
|
+
ok: true,
|
|
1232
|
+
claims: payload,
|
|
1233
|
+
...validatedSnapshot && { subject_snapshot: validatedSnapshot },
|
|
1234
|
+
perf: {
|
|
1235
|
+
verify_ms: verifyTime,
|
|
1236
|
+
...jwksFetchTime && { jwks_fetch_ms: jwksFetchTime }
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
const durationMs = performance.now() - startTime;
|
|
1241
|
+
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
1242
|
+
receiptHash: hashReceipt(receiptJws),
|
|
1243
|
+
valid: false,
|
|
1244
|
+
reasonCode: "verification_error",
|
|
1245
|
+
durationMs
|
|
1246
|
+
});
|
|
1247
|
+
return {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
reason: "verification_error",
|
|
1250
|
+
details: err instanceof Error ? err.message : String(err)
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function isCryptoError(err) {
|
|
1255
|
+
return err !== null && typeof err === "object" && "name" in err && err.name === "CryptoError" && "code" in err && typeof err.code === "string" && err.code.startsWith("CRYPTO_") && "message" in err && typeof err.message === "string";
|
|
1256
|
+
}
|
|
1257
|
+
var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1258
|
+
"CRYPTO_INVALID_JWS_FORMAT",
|
|
1259
|
+
"CRYPTO_INVALID_TYP",
|
|
1260
|
+
"CRYPTO_INVALID_ALG",
|
|
1261
|
+
"CRYPTO_INVALID_KEY_LENGTH"
|
|
1262
|
+
]);
|
|
1263
|
+
var MAX_PARSE_ISSUES = 25;
|
|
1264
|
+
function sanitizeParseIssues(issues) {
|
|
1265
|
+
if (!Array.isArray(issues)) return void 0;
|
|
1266
|
+
return issues.slice(0, MAX_PARSE_ISSUES).map((issue2) => ({
|
|
1267
|
+
path: Array.isArray(issue2?.path) ? issue2.path.join(".") : "",
|
|
1268
|
+
message: typeof issue2?.message === "string" ? issue2.message : String(issue2)
|
|
1269
|
+
}));
|
|
1270
|
+
}
|
|
1271
|
+
async function verifyLocal(jws, publicKey, options = {}) {
|
|
1272
|
+
const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
|
|
1273
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
1274
|
+
try {
|
|
1275
|
+
const result = await verify(jws, publicKey);
|
|
1276
|
+
if (!result.valid) {
|
|
1277
|
+
return {
|
|
1278
|
+
valid: false,
|
|
1279
|
+
code: "E_INVALID_SIGNATURE",
|
|
1280
|
+
message: "Ed25519 signature verification failed"
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const constraintResult = validateKernelConstraints(result.payload);
|
|
1284
|
+
if (!constraintResult.valid) {
|
|
1285
|
+
const v = constraintResult.violations[0];
|
|
1286
|
+
return {
|
|
1287
|
+
valid: false,
|
|
1288
|
+
code: "E_CONSTRAINT_VIOLATION",
|
|
1289
|
+
message: `Kernel constraint violated: ${v.constraint} (actual: ${v.actual}, limit: ${v.limit})`
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
const pr = parseReceiptClaims(result.payload);
|
|
1293
|
+
if (!pr.ok) {
|
|
1294
|
+
return {
|
|
1295
|
+
valid: false,
|
|
1296
|
+
code: "E_INVALID_FORMAT",
|
|
1297
|
+
message: `Receipt schema validation failed: ${pr.error.message}`,
|
|
1298
|
+
details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
if (issuer !== void 0 && pr.claims.iss !== issuer) {
|
|
1302
|
+
return {
|
|
1303
|
+
valid: false,
|
|
1304
|
+
code: "E_INVALID_ISSUER",
|
|
1305
|
+
message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
if (audience !== void 0 && pr.claims.aud !== audience) {
|
|
1309
|
+
return {
|
|
1310
|
+
valid: false,
|
|
1311
|
+
code: "E_INVALID_AUDIENCE",
|
|
1312
|
+
message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
if (rid !== void 0 && pr.claims.rid !== rid) {
|
|
1316
|
+
return {
|
|
1317
|
+
valid: false,
|
|
1318
|
+
code: "E_INVALID_RECEIPT_ID",
|
|
1319
|
+
message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (requireExp && pr.claims.exp === void 0) {
|
|
1323
|
+
return {
|
|
1324
|
+
valid: false,
|
|
1325
|
+
code: "E_MISSING_EXP",
|
|
1326
|
+
message: "Receipt missing required exp claim"
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
if (pr.claims.iat > now + maxClockSkew) {
|
|
1330
|
+
return {
|
|
1331
|
+
valid: false,
|
|
1332
|
+
code: "E_NOT_YET_VALID",
|
|
1333
|
+
message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
|
|
1337
|
+
return {
|
|
1338
|
+
valid: false,
|
|
1339
|
+
code: "E_EXPIRED",
|
|
1340
|
+
message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
if (pr.variant === "commerce") {
|
|
1344
|
+
const claims = pr.claims;
|
|
1345
|
+
if (subjectUri !== void 0 && claims.subject?.uri !== subjectUri) {
|
|
1346
|
+
return {
|
|
1347
|
+
valid: false,
|
|
1348
|
+
code: "E_INVALID_SUBJECT",
|
|
1349
|
+
message: `Subject mismatch: expected "${subjectUri}", got "${claims.subject?.uri ?? "undefined"}"`
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return {
|
|
1353
|
+
valid: true,
|
|
1354
|
+
variant: "commerce",
|
|
1355
|
+
claims,
|
|
1356
|
+
kid: result.header.kid,
|
|
1357
|
+
policy_binding: "unavailable"
|
|
1358
|
+
};
|
|
1359
|
+
} else {
|
|
1360
|
+
const claims = pr.claims;
|
|
1361
|
+
if (subjectUri !== void 0 && claims.sub !== subjectUri) {
|
|
1362
|
+
return {
|
|
1363
|
+
valid: false,
|
|
1364
|
+
code: "E_INVALID_SUBJECT",
|
|
1365
|
+
message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
valid: true,
|
|
1370
|
+
variant: "attestation",
|
|
1371
|
+
claims,
|
|
1372
|
+
kid: result.header.kid,
|
|
1373
|
+
policy_binding: "unavailable"
|
|
1346
1374
|
};
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1375
|
+
}
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
if (isCryptoError(err)) {
|
|
1378
|
+
if (FORMAT_ERROR_CODES.has(err.code)) {
|
|
1379
|
+
return {
|
|
1380
|
+
valid: false,
|
|
1381
|
+
code: "E_INVALID_FORMAT",
|
|
1382
|
+
message: err.message
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
if (err.code === "CRYPTO_INVALID_SIGNATURE") {
|
|
1386
|
+
return {
|
|
1387
|
+
valid: false,
|
|
1388
|
+
code: "E_INVALID_SIGNATURE",
|
|
1389
|
+
message: err.message
|
|
1390
|
+
};
|
|
1356
1391
|
}
|
|
1392
|
+
}
|
|
1393
|
+
if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
|
|
1394
|
+
const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
|
|
1357
1395
|
return {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
message: `
|
|
1396
|
+
valid: false,
|
|
1397
|
+
code: "E_INVALID_FORMAT",
|
|
1398
|
+
message: `Invalid receipt payload: ${syntaxMessage}`
|
|
1361
1399
|
};
|
|
1362
1400
|
}
|
|
1401
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1402
|
+
return {
|
|
1403
|
+
valid: false,
|
|
1404
|
+
code: "E_INTERNAL",
|
|
1405
|
+
message: `Unexpected verification error: ${message}`
|
|
1406
|
+
};
|
|
1363
1407
|
}
|
|
1364
1408
|
}
|
|
1365
|
-
|
|
1366
|
-
return
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1409
|
+
function isCommerceResult(r) {
|
|
1410
|
+
return r.valid === true && r.variant === "commerce";
|
|
1411
|
+
}
|
|
1412
|
+
function isAttestationResult(r) {
|
|
1413
|
+
return r.valid === true && r.variant === "attestation";
|
|
1414
|
+
}
|
|
1415
|
+
function setReceiptHeader(headers, receiptJws) {
|
|
1416
|
+
headers.set(PEAC_RECEIPT_HEADER, receiptJws);
|
|
1417
|
+
}
|
|
1418
|
+
function getReceiptHeader(headers) {
|
|
1419
|
+
return headers.get(PEAC_RECEIPT_HEADER);
|
|
1420
|
+
}
|
|
1421
|
+
function setVaryHeader(headers) {
|
|
1422
|
+
const existing = headers.get("Vary");
|
|
1423
|
+
if (existing) {
|
|
1424
|
+
const varies = existing.split(",").map((v) => v.trim());
|
|
1425
|
+
if (!varies.includes(PEAC_RECEIPT_HEADER)) {
|
|
1426
|
+
headers.set("Vary", `${existing}, ${PEAC_RECEIPT_HEADER}`);
|
|
1372
1427
|
}
|
|
1373
|
-
}
|
|
1428
|
+
} else {
|
|
1429
|
+
headers.set("Vary", PEAC_RECEIPT_HEADER);
|
|
1430
|
+
}
|
|
1374
1431
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1432
|
+
function getPurposeHeader(headers) {
|
|
1433
|
+
const value = headers.get(PEAC_PURPOSE_HEADER);
|
|
1434
|
+
if (!value) {
|
|
1435
|
+
return [];
|
|
1436
|
+
}
|
|
1437
|
+
return parsePurposeHeader(value);
|
|
1438
|
+
}
|
|
1439
|
+
function setPurposeAppliedHeader(headers, purpose) {
|
|
1440
|
+
headers.set(PEAC_PURPOSE_APPLIED_HEADER, purpose);
|
|
1441
|
+
}
|
|
1442
|
+
function setPurposeReasonHeader(headers, reason) {
|
|
1443
|
+
headers.set(PEAC_PURPOSE_REASON_HEADER, reason);
|
|
1444
|
+
}
|
|
1445
|
+
function setVaryPurposeHeader(headers) {
|
|
1446
|
+
const existing = headers.get("Vary");
|
|
1447
|
+
if (existing) {
|
|
1448
|
+
const varies = existing.split(",").map((v) => v.trim());
|
|
1449
|
+
if (!varies.includes(PEAC_PURPOSE_HEADER)) {
|
|
1450
|
+
headers.set("Vary", `${existing}, ${PEAC_PURPOSE_HEADER}`);
|
|
1382
1451
|
}
|
|
1383
|
-
}
|
|
1452
|
+
} else {
|
|
1453
|
+
headers.set("Vary", PEAC_PURPOSE_HEADER);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
var DEFAULT_VERIFIER_LIMITS = {
|
|
1457
|
+
max_receipt_bytes: VERIFIER_LIMITS.maxReceiptBytes,
|
|
1458
|
+
max_jwks_bytes: VERIFIER_LIMITS.maxJwksBytes,
|
|
1459
|
+
max_jwks_keys: VERIFIER_LIMITS.maxJwksKeys,
|
|
1460
|
+
max_redirects: VERIFIER_LIMITS.maxRedirects,
|
|
1461
|
+
fetch_timeout_ms: VERIFIER_LIMITS.fetchTimeoutMs,
|
|
1462
|
+
max_extension_bytes: VERIFIER_LIMITS.maxExtensionBytes
|
|
1463
|
+
};
|
|
1464
|
+
var DEFAULT_NETWORK_SECURITY = {
|
|
1465
|
+
https_only: VERIFIER_NETWORK.httpsOnly,
|
|
1466
|
+
block_private_ips: VERIFIER_NETWORK.blockPrivateIps,
|
|
1467
|
+
allow_redirects: VERIFIER_NETWORK.allowRedirects,
|
|
1468
|
+
allow_cross_origin_redirects: true,
|
|
1469
|
+
// Allow for CDN compatibility
|
|
1470
|
+
dns_failure_behavior: "block"
|
|
1471
|
+
// Fail-closed by default
|
|
1472
|
+
};
|
|
1473
|
+
function createDefaultPolicy(mode) {
|
|
1474
|
+
return {
|
|
1475
|
+
policy_version: VERIFIER_POLICY_VERSION,
|
|
1476
|
+
mode,
|
|
1477
|
+
limits: { ...DEFAULT_VERIFIER_LIMITS },
|
|
1478
|
+
network: { ...DEFAULT_NETWORK_SECURITY }
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
var CHECK_IDS = [
|
|
1482
|
+
"jws.parse",
|
|
1483
|
+
"limits.receipt_bytes",
|
|
1484
|
+
"jws.protected_header",
|
|
1485
|
+
"claims.schema_unverified",
|
|
1486
|
+
"issuer.trust_policy",
|
|
1487
|
+
"issuer.discovery",
|
|
1488
|
+
"key.resolve",
|
|
1489
|
+
"jws.signature",
|
|
1490
|
+
"claims.time_window",
|
|
1491
|
+
"extensions.limits",
|
|
1492
|
+
"transport.profile_binding",
|
|
1493
|
+
"policy.binding"
|
|
1494
|
+
];
|
|
1495
|
+
var NON_DETERMINISTIC_ARTIFACT_KEYS = [
|
|
1496
|
+
"issuer_jwks_digest"
|
|
1497
|
+
];
|
|
1498
|
+
function createDigest(hexValue) {
|
|
1499
|
+
return {
|
|
1500
|
+
alg: "sha-256",
|
|
1501
|
+
value: hexValue.toLowerCase()
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function createEmptyReport(policy) {
|
|
1505
|
+
return {
|
|
1506
|
+
report_version: VERIFICATION_REPORT_VERSION,
|
|
1507
|
+
policy
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
function ssrfErrorToReasonCode(ssrfReason, fetchType) {
|
|
1511
|
+
const prefix = fetchType === "key" ? "key_fetch" : "pointer_fetch";
|
|
1512
|
+
switch (ssrfReason) {
|
|
1513
|
+
case "not_https":
|
|
1514
|
+
case "private_ip":
|
|
1515
|
+
case "loopback":
|
|
1516
|
+
case "link_local":
|
|
1517
|
+
case "cross_origin_redirect":
|
|
1518
|
+
case "dns_failure":
|
|
1519
|
+
return `${prefix}_blocked`;
|
|
1520
|
+
case "timeout":
|
|
1521
|
+
return `${prefix}_timeout`;
|
|
1522
|
+
case "response_too_large":
|
|
1523
|
+
return fetchType === "pointer" ? "pointer_fetch_too_large" : "jwks_too_large";
|
|
1524
|
+
case "jwks_too_many_keys":
|
|
1525
|
+
return "jwks_too_many_keys";
|
|
1526
|
+
case "too_many_redirects":
|
|
1527
|
+
case "scheme_downgrade":
|
|
1528
|
+
case "network_error":
|
|
1529
|
+
case "invalid_url":
|
|
1530
|
+
default:
|
|
1531
|
+
return `${prefix}_failed`;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function reasonCodeToSeverity(reason) {
|
|
1535
|
+
if (reason === "ok") return "info";
|
|
1536
|
+
return "error";
|
|
1537
|
+
}
|
|
1538
|
+
function reasonCodeToErrorCode(reason) {
|
|
1539
|
+
const mapping = {
|
|
1540
|
+
ok: "",
|
|
1541
|
+
receipt_too_large: "E_VERIFY_RECEIPT_TOO_LARGE",
|
|
1542
|
+
malformed_receipt: "E_VERIFY_MALFORMED_RECEIPT",
|
|
1543
|
+
signature_invalid: "E_VERIFY_SIGNATURE_INVALID",
|
|
1544
|
+
issuer_not_allowed: "E_VERIFY_ISSUER_NOT_ALLOWED",
|
|
1545
|
+
key_not_found: "E_VERIFY_KEY_NOT_FOUND",
|
|
1546
|
+
key_fetch_blocked: "E_VERIFY_KEY_FETCH_BLOCKED",
|
|
1547
|
+
key_fetch_failed: "E_VERIFY_KEY_FETCH_FAILED",
|
|
1548
|
+
key_fetch_timeout: "E_VERIFY_KEY_FETCH_TIMEOUT",
|
|
1549
|
+
pointer_fetch_blocked: "E_VERIFY_POINTER_FETCH_BLOCKED",
|
|
1550
|
+
pointer_fetch_failed: "E_VERIFY_POINTER_FETCH_FAILED",
|
|
1551
|
+
pointer_fetch_timeout: "E_VERIFY_POINTER_FETCH_TIMEOUT",
|
|
1552
|
+
pointer_fetch_too_large: "E_VERIFY_POINTER_FETCH_TOO_LARGE",
|
|
1553
|
+
pointer_digest_mismatch: "E_VERIFY_POINTER_DIGEST_MISMATCH",
|
|
1554
|
+
jwks_too_large: "E_VERIFY_JWKS_TOO_LARGE",
|
|
1555
|
+
jwks_too_many_keys: "E_VERIFY_JWKS_TOO_MANY_KEYS",
|
|
1556
|
+
expired: "E_VERIFY_EXPIRED",
|
|
1557
|
+
not_yet_valid: "E_VERIFY_NOT_YET_VALID",
|
|
1558
|
+
audience_mismatch: "E_VERIFY_AUDIENCE_MISMATCH",
|
|
1559
|
+
schema_invalid: "E_VERIFY_SCHEMA_INVALID",
|
|
1560
|
+
policy_violation: "E_VERIFY_POLICY_VIOLATION",
|
|
1561
|
+
extension_too_large: "E_VERIFY_EXTENSION_TOO_LARGE",
|
|
1562
|
+
invalid_transport: "E_VERIFY_INVALID_TRANSPORT"
|
|
1563
|
+
};
|
|
1564
|
+
return mapping[reason] || "E_VERIFY_POLICY_VIOLATION";
|
|
1384
1565
|
}
|
|
1385
1566
|
var VerificationReportBuilder = class {
|
|
1386
1567
|
state;
|
|
@@ -1649,8 +1830,6 @@ async function buildSuccessReport(policy, receiptBytes, issuer, kid, checkDetail
|
|
|
1649
1830
|
}
|
|
1650
1831
|
|
|
1651
1832
|
// src/verifier-core.ts
|
|
1652
|
-
var jwksCache2 = /* @__PURE__ */ new Map();
|
|
1653
|
-
var CACHE_TTL_MS2 = 5 * 60 * 1e3;
|
|
1654
1833
|
function normalizeIssuer(issuer) {
|
|
1655
1834
|
try {
|
|
1656
1835
|
const url = new URL(issuer);
|
|
@@ -1676,75 +1855,23 @@ function findPinnedKey(issuer, kid, pinnedKeys) {
|
|
|
1676
1855
|
const normalizedIssuer = normalizeIssuer(issuer);
|
|
1677
1856
|
return pinnedKeys.find((pk) => normalizeIssuer(pk.issuer) === normalizedIssuer && pk.kid === kid);
|
|
1678
1857
|
}
|
|
1679
|
-
async function fetchIssuerConfig2(issuerOrigin) {
|
|
1680
|
-
const configUrl = `${issuerOrigin}/.well-known/peac-issuer.json`;
|
|
1681
|
-
const result = await ssrfSafeFetch(configUrl, {
|
|
1682
|
-
maxBytes: 65536,
|
|
1683
|
-
// 64 KB
|
|
1684
|
-
headers: { Accept: "application/json" }
|
|
1685
|
-
});
|
|
1686
|
-
if (!result.ok) {
|
|
1687
|
-
return null;
|
|
1688
|
-
}
|
|
1689
|
-
try {
|
|
1690
|
-
return JSON.parse(result.body);
|
|
1691
|
-
} catch {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
1858
|
async function fetchIssuerJWKS(issuerOrigin) {
|
|
1696
|
-
const
|
|
1697
|
-
const cached = jwksCache2.get(issuerOrigin);
|
|
1698
|
-
if (cached && cached.expiresAt > now) {
|
|
1699
|
-
return { jwks: cached.jwks, fromCache: true };
|
|
1700
|
-
}
|
|
1701
|
-
const config = await fetchIssuerConfig2(issuerOrigin);
|
|
1702
|
-
if (!config?.jwks_uri) {
|
|
1703
|
-
const fallbackUrl = `${issuerOrigin}/.well-known/jwks.json`;
|
|
1704
|
-
const result2 = await fetchJWKSSafe(fallbackUrl);
|
|
1705
|
-
if (!result2.ok) {
|
|
1706
|
-
return { error: result2 };
|
|
1707
|
-
}
|
|
1708
|
-
try {
|
|
1709
|
-
const jwks = JSON.parse(result2.body);
|
|
1710
|
-
jwksCache2.set(issuerOrigin, { jwks, expiresAt: now + CACHE_TTL_MS2 });
|
|
1711
|
-
return { jwks, fromCache: false, rawBytes: result2.rawBytes };
|
|
1712
|
-
} catch {
|
|
1713
|
-
return {
|
|
1714
|
-
error: {
|
|
1715
|
-
ok: false,
|
|
1716
|
-
reason: "network_error",
|
|
1717
|
-
message: "Invalid JWKS JSON"
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
const result = await fetchJWKSSafe(config.jwks_uri);
|
|
1859
|
+
const result = await resolveJWKS(issuerOrigin);
|
|
1723
1860
|
if (!result.ok) {
|
|
1724
|
-
return { error: result };
|
|
1725
|
-
}
|
|
1726
|
-
try {
|
|
1727
|
-
const jwks = JSON.parse(result.body);
|
|
1728
|
-
if (jwks.keys.length > VERIFIER_LIMITS.maxJwksKeys) {
|
|
1729
|
-
return {
|
|
1730
|
-
error: {
|
|
1731
|
-
ok: false,
|
|
1732
|
-
reason: "jwks_too_many_keys",
|
|
1733
|
-
message: `JWKS has too many keys: ${jwks.keys.length} > ${VERIFIER_LIMITS.maxJwksKeys}`
|
|
1734
|
-
}
|
|
1735
|
-
};
|
|
1736
|
-
}
|
|
1737
|
-
jwksCache2.set(issuerOrigin, { jwks, expiresAt: now + CACHE_TTL_MS2 });
|
|
1738
|
-
return { jwks, fromCache: false, rawBytes: result.rawBytes };
|
|
1739
|
-
} catch {
|
|
1740
1861
|
return {
|
|
1741
1862
|
error: {
|
|
1742
1863
|
ok: false,
|
|
1743
|
-
reason: "network_error",
|
|
1744
|
-
message:
|
|
1864
|
+
reason: result.reason ?? "network_error",
|
|
1865
|
+
message: result.message,
|
|
1866
|
+
blockedUrl: result.blockedUrl
|
|
1745
1867
|
}
|
|
1746
1868
|
};
|
|
1747
1869
|
}
|
|
1870
|
+
return {
|
|
1871
|
+
jwks: result.jwks,
|
|
1872
|
+
fromCache: result.fromCache,
|
|
1873
|
+
rawBytes: result.rawBytes
|
|
1874
|
+
};
|
|
1748
1875
|
}
|
|
1749
1876
|
async function verifyReceiptCore(options) {
|
|
1750
1877
|
const {
|
|
@@ -2081,12 +2208,6 @@ async function verifyReceiptCore(options) {
|
|
|
2081
2208
|
claims: parsedClaims
|
|
2082
2209
|
};
|
|
2083
2210
|
}
|
|
2084
|
-
function clearJWKSCache() {
|
|
2085
|
-
jwksCache2.clear();
|
|
2086
|
-
}
|
|
2087
|
-
function getJWKSCacheSize() {
|
|
2088
|
-
return jwksCache2.size;
|
|
2089
|
-
}
|
|
2090
2211
|
|
|
2091
2212
|
// src/transport-profiles.ts
|
|
2092
2213
|
function parseHeaderProfile(headerValue) {
|
|
@@ -2607,6 +2728,6 @@ async function verifyAndFetchPointer(pointerHeader, fetchOptions) {
|
|
|
2607
2728
|
});
|
|
2608
2729
|
}
|
|
2609
2730
|
|
|
2610
|
-
export { CHECK_IDS, DEFAULT_NETWORK_SECURITY, DEFAULT_VERIFIER_LIMITS, IssueError, NON_DETERMINISTIC_ARTIFACT_KEYS, VerificationReportBuilder, buildFailureReport, buildSuccessReport, clearJWKSCache, computeReceiptDigest, createDefaultPolicy, createDigest, createEmptyReport, createReportBuilder, fetchDiscovery, fetchIssuerConfig, fetchJWKSSafe, fetchPointerSafe, fetchPointerWithDigest, fetchPolicyManifest, getJWKSCacheSize, getPurposeHeader, getReceiptHeader, getSSRFCapabilities, isAttestationResult, isBlockedIP, isCommerceResult, issue, issueJws, parseBodyProfile, parseDiscovery, parseHeaderProfile, parseIssuerConfig, parsePointerProfile, parsePolicyManifest, parseTransportProfile, reasonCodeToErrorCode, reasonCodeToSeverity, resetSSRFCapabilitiesCache, setPurposeAppliedHeader, setPurposeReasonHeader, setReceiptHeader, setVaryHeader, setVaryPurposeHeader, ssrfErrorToReasonCode, ssrfSafeFetch, verifyAndFetchPointer, verifyLocal, verifyReceipt, verifyReceiptCore };
|
|
2731
|
+
export { CHECK_IDS, DEFAULT_NETWORK_SECURITY, DEFAULT_VERIFIER_LIMITS, IssueError, NON_DETERMINISTIC_ARTIFACT_KEYS, VerificationReportBuilder, buildFailureReport, buildSuccessReport, clearJWKSCache, computeReceiptDigest, createDefaultPolicy, createDigest, createEmptyReport, createReportBuilder, fetchDiscovery, fetchIssuerConfig, fetchJWKSSafe, fetchPointerSafe, fetchPointerWithDigest, fetchPolicyManifest, getJWKSCacheSize, getPurposeHeader, getReceiptHeader, getSSRFCapabilities, isAttestationResult, isBlockedIP, isCommerceResult, issue, issueJws, parseBodyProfile, parseDiscovery, parseHeaderProfile, parseIssuerConfig, parsePointerProfile, parsePolicyManifest, parseTransportProfile, reasonCodeToErrorCode, reasonCodeToSeverity, resetSSRFCapabilitiesCache, resolveJWKS, setPurposeAppliedHeader, setPurposeReasonHeader, setReceiptHeader, setVaryHeader, setVaryPurposeHeader, ssrfErrorToReasonCode, ssrfSafeFetch, verifyAndFetchPointer, verifyLocal, verifyReceipt, verifyReceiptCore };
|
|
2611
2732
|
//# sourceMappingURL=index.mjs.map
|
|
2612
2733
|
//# sourceMappingURL=index.mjs.map
|