@peac/protocol 0.11.2 → 0.12.0-preview.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/README.md +68 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/index.cjs +311 -16
- 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 +309 -20
- package/dist/index.mjs.map +1 -1
- package/dist/issue.d.ts +60 -1
- package/dist/issue.d.ts.map +1 -1
- package/dist/jwks-resolver.d.ts +20 -0
- package/dist/jwks-resolver.d.ts.map +1 -1
- package/dist/policy-binding.d.ts +55 -0
- package/dist/policy-binding.d.ts.map +1 -0
- package/dist/verify-local.cjs +155 -12
- package/dist/verify-local.cjs.map +1 -1
- package/dist/verify-local.d.ts +94 -21
- package/dist/verify-local.d.ts.map +1 -1
- package/dist/verify-local.mjs +156 -14
- package/dist/verify-local.mjs.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @peac/protocol
|
|
2
2
|
|
|
3
|
-
PEAC protocol implementation
|
|
3
|
+
PEAC protocol implementation: receipt issuance, offline verification, and JWKS resolution.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,9 +8,74 @@ PEAC protocol implementation - receipt issuance and verification
|
|
|
8
8
|
pnpm add @peac/protocol
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## What It Does
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
`@peac/protocol` is Layer 3 of the PEAC stack. It provides `issue()` for signing receipts and `verifyLocal()` for offline verification with Ed25519 public keys. No network calls needed for verification.
|
|
14
|
+
|
|
15
|
+
## How Do I Issue a Receipt?
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { generateKeypair } from '@peac/crypto';
|
|
19
|
+
import { issue } from '@peac/protocol';
|
|
20
|
+
|
|
21
|
+
const { publicKey, privateKey } = await generateKeypair();
|
|
22
|
+
|
|
23
|
+
const { jws } = await issue({
|
|
24
|
+
iss: 'https://api.example.com',
|
|
25
|
+
aud: 'https://client.example.com',
|
|
26
|
+
amt: 100,
|
|
27
|
+
cur: 'USD',
|
|
28
|
+
rail: 'stripe',
|
|
29
|
+
reference: 'pi_abc123',
|
|
30
|
+
privateKey,
|
|
31
|
+
kid: 'key-2026-02',
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How Do I Verify a Receipt?
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { verifyLocal } from '@peac/protocol';
|
|
39
|
+
|
|
40
|
+
const result = await verifyLocal(jws, publicKey);
|
|
41
|
+
|
|
42
|
+
if (result.valid && result.variant === 'commerce') {
|
|
43
|
+
console.log(result.claims.iss); // issuer
|
|
44
|
+
console.log(result.claims.amt); // amount
|
|
45
|
+
console.log(result.claims.cur); // currency
|
|
46
|
+
} else if (!result.valid) {
|
|
47
|
+
console.log(result.code, result.message);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How Do I Verify with JWKS Discovery?
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { verifyReceipt } from '@peac/protocol';
|
|
55
|
+
|
|
56
|
+
// Resolves issuer's /.well-known/peac-issuer.json -> jwks_uri -> public key
|
|
57
|
+
const result = await verifyReceipt(jws);
|
|
58
|
+
|
|
59
|
+
if (result.ok) {
|
|
60
|
+
console.log('Issuer:', result.claims.iss);
|
|
61
|
+
} else {
|
|
62
|
+
console.log(result.reason, result.details);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Integrates With
|
|
67
|
+
|
|
68
|
+
- `@peac/crypto` (Layer 2): Ed25519 key generation and JWS encoding
|
|
69
|
+
- `@peac/kernel` (Layer 0): Error codes and wire format constants
|
|
70
|
+
- `@peac/schema` (Layer 1): Receipt claim validation
|
|
71
|
+
- `@peac/mcp-server` (Layer 5): MCP tool server using protocol functions
|
|
72
|
+
- `@peac/middleware-express` (Layer 3.5): Express middleware for automatic receipt issuance
|
|
73
|
+
|
|
74
|
+
## Security
|
|
75
|
+
|
|
76
|
+
- Verification is offline and deterministic: no network calls for `verifyLocal()`
|
|
77
|
+
- Fail-closed: invalid or missing evidence always produces a verification failure
|
|
78
|
+
- JWKS resolution (when used) is SSRF-hardened with HTTPS-only, private IP denial
|
|
14
79
|
|
|
15
80
|
## License
|
|
16
81
|
|
package/dist/discovery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EAOd,MAAM,cAAc,CAAC;AAMtB;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAoGzE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsCpF;AAiBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAoD1F;AAkFD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+CtF;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAkD1D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B9E"}
|
package/dist/index.cjs
CHANGED
|
@@ -179,6 +179,52 @@ async function issueJws(options) {
|
|
|
179
179
|
const result = await issue(options);
|
|
180
180
|
return result.jws;
|
|
181
181
|
}
|
|
182
|
+
async function issueWire02(options) {
|
|
183
|
+
if (!schema.isCanonicalIss(options.iss)) {
|
|
184
|
+
throw new IssueError({
|
|
185
|
+
code: "E_ISS_NOT_CANONICAL",
|
|
186
|
+
category: "validation",
|
|
187
|
+
severity: "error",
|
|
188
|
+
retryable: false,
|
|
189
|
+
http_status: 400,
|
|
190
|
+
details: {
|
|
191
|
+
message: `iss is not in canonical form: "${options.iss}". Use https:// origin or did: identifier.`
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const jti = options.jti ?? uuidv7.uuidv7();
|
|
196
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
197
|
+
const claims = {
|
|
198
|
+
peac_version: "0.2",
|
|
199
|
+
kind: options.kind,
|
|
200
|
+
type: options.type,
|
|
201
|
+
iss: options.iss,
|
|
202
|
+
iat,
|
|
203
|
+
jti,
|
|
204
|
+
...options.sub !== void 0 && { sub: options.sub },
|
|
205
|
+
...options.pillars !== void 0 && { pillars: options.pillars },
|
|
206
|
+
...options.occurred_at !== void 0 && { occurred_at: options.occurred_at },
|
|
207
|
+
...options.purpose_declared !== void 0 && { purpose_declared: options.purpose_declared },
|
|
208
|
+
...options.policy !== void 0 && { policy: options.policy },
|
|
209
|
+
...options.extensions !== void 0 && { extensions: options.extensions }
|
|
210
|
+
};
|
|
211
|
+
const parseResult = schema.Wire02ClaimsSchema.safeParse(claims);
|
|
212
|
+
if (!parseResult.success) {
|
|
213
|
+
const firstIssue = parseResult.error.issues[0];
|
|
214
|
+
throw new IssueError({
|
|
215
|
+
code: "E_INVALID_FORMAT",
|
|
216
|
+
category: "validation",
|
|
217
|
+
severity: "error",
|
|
218
|
+
retryable: false,
|
|
219
|
+
http_status: 400,
|
|
220
|
+
details: {
|
|
221
|
+
message: `Wire 0.2 claims schema validation failed: ${firstIssue?.message ?? "unknown"}`
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const jws = await crypto.signWire02(claims, options.privateKey, options.kid);
|
|
226
|
+
return { jws };
|
|
227
|
+
}
|
|
182
228
|
function parseIssuerConfig(json) {
|
|
183
229
|
let config;
|
|
184
230
|
if (typeof json === "string") {
|
|
@@ -238,6 +284,17 @@ function parseIssuerConfig(json) {
|
|
|
238
284
|
throw new Error("payment_rails must be an array");
|
|
239
285
|
}
|
|
240
286
|
}
|
|
287
|
+
let revokedKeys;
|
|
288
|
+
if (obj.revoked_keys !== void 0) {
|
|
289
|
+
if (!Array.isArray(obj.revoked_keys)) {
|
|
290
|
+
throw new Error("revoked_keys must be an array");
|
|
291
|
+
}
|
|
292
|
+
const result = schema.validateRevokedKeys(obj.revoked_keys);
|
|
293
|
+
if (!result.ok) {
|
|
294
|
+
throw new Error(`Invalid revoked_keys: ${result.error}`);
|
|
295
|
+
}
|
|
296
|
+
revokedKeys = result.value;
|
|
297
|
+
}
|
|
241
298
|
return {
|
|
242
299
|
version: obj.version,
|
|
243
300
|
issuer: obj.issuer,
|
|
@@ -246,7 +303,8 @@ function parseIssuerConfig(json) {
|
|
|
246
303
|
receipt_versions: obj.receipt_versions,
|
|
247
304
|
algorithms: obj.algorithms,
|
|
248
305
|
payment_rails: obj.payment_rails,
|
|
249
|
-
security_contact: obj.security_contact
|
|
306
|
+
security_contact: obj.security_contact,
|
|
307
|
+
revoked_keys: revokedKeys
|
|
250
308
|
};
|
|
251
309
|
}
|
|
252
310
|
async function fetchIssuerConfig(issuerUrl) {
|
|
@@ -933,6 +991,53 @@ async function fetchPointerSafe(pointerUrl, options) {
|
|
|
933
991
|
var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
934
992
|
var DEFAULT_MAX_CACHE_ENTRIES = 1e3;
|
|
935
993
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
994
|
+
var KID_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
995
|
+
var MAX_KID_THUMBPRINT_ENTRIES = 1e4;
|
|
996
|
+
var kidThumbprints = /* @__PURE__ */ new Map();
|
|
997
|
+
function pruneExpiredKidEntries(now) {
|
|
998
|
+
for (const [key, entry] of kidThumbprints) {
|
|
999
|
+
if (now - entry.firstSeen >= KID_RETENTION_MS) {
|
|
1000
|
+
kidThumbprints.delete(key);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function evictOldestKidEntries() {
|
|
1005
|
+
if (kidThumbprints.size <= MAX_KID_THUMBPRINT_ENTRIES) return;
|
|
1006
|
+
const entries = Array.from(kidThumbprints.entries()).sort(
|
|
1007
|
+
(a, b) => a[1].firstSeen - b[1].firstSeen
|
|
1008
|
+
);
|
|
1009
|
+
const toRemove = kidThumbprints.size - MAX_KID_THUMBPRINT_ENTRIES;
|
|
1010
|
+
for (let i = 0; i < toRemove; i++) {
|
|
1011
|
+
kidThumbprints.delete(entries[i][0]);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function checkKidReuse(issuer, jwks, now) {
|
|
1015
|
+
pruneExpiredKidEntries(now);
|
|
1016
|
+
for (const key of jwks.keys) {
|
|
1017
|
+
if (!key.kid || !key.x) continue;
|
|
1018
|
+
const mapKey = `${issuer}|${key.kid}`;
|
|
1019
|
+
const existing = kidThumbprints.get(mapKey);
|
|
1020
|
+
if (existing) {
|
|
1021
|
+
if (now - existing.firstSeen < KID_RETENTION_MS) {
|
|
1022
|
+
if (existing.thumbprint !== key.x) {
|
|
1023
|
+
return `Kid reuse detected: kid=${key.kid} for issuer ${issuer} maps to different key material`;
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
kidThumbprints.set(mapKey, { thumbprint: key.x, firstSeen: now });
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
kidThumbprints.set(mapKey, { thumbprint: key.x, firstSeen: now });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
evictOldestKidEntries();
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
function clearKidThumbprints() {
|
|
1036
|
+
kidThumbprints.clear();
|
|
1037
|
+
}
|
|
1038
|
+
function getKidThumbprintSize() {
|
|
1039
|
+
return kidThumbprints.size;
|
|
1040
|
+
}
|
|
936
1041
|
function cacheGet(key, now) {
|
|
937
1042
|
const entry = jwksCache.get(key);
|
|
938
1043
|
if (!entry) return void 0;
|
|
@@ -1021,7 +1126,7 @@ async function resolveJWKS(issuerUrl, options) {
|
|
|
1021
1126
|
if (!noCache) {
|
|
1022
1127
|
const cached = cacheGet(normalizedIssuer, now);
|
|
1023
1128
|
if (cached) {
|
|
1024
|
-
return { ok: true, jwks: cached.jwks, fromCache: true };
|
|
1129
|
+
return { ok: true, jwks: cached.jwks, fromCache: true, revokedKeys: cached.revokedKeys };
|
|
1025
1130
|
}
|
|
1026
1131
|
}
|
|
1027
1132
|
if (!normalizedIssuer.startsWith("https://")) {
|
|
@@ -1116,14 +1221,28 @@ async function resolveJWKS(issuerUrl, options) {
|
|
|
1116
1221
|
message: `JWKS has too many keys: ${jwks.keys.length} > ${kernel.VERIFIER_LIMITS.maxJwksKeys}`
|
|
1117
1222
|
};
|
|
1118
1223
|
}
|
|
1224
|
+
const kidReuseError = checkKidReuse(normalizedIssuer, jwks, now);
|
|
1225
|
+
if (kidReuseError) {
|
|
1226
|
+
return {
|
|
1227
|
+
ok: false,
|
|
1228
|
+
code: "E_KID_REUSE_DETECTED",
|
|
1229
|
+
message: kidReuseError
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
const revokedKeys = issuerConfig.revoked_keys?.map((entry) => ({
|
|
1233
|
+
kid: entry.kid,
|
|
1234
|
+
revoked_at: entry.revoked_at,
|
|
1235
|
+
reason: entry.reason
|
|
1236
|
+
}));
|
|
1119
1237
|
if (!noCache) {
|
|
1120
|
-
cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs }, maxCacheEntries);
|
|
1238
|
+
cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs, revokedKeys }, maxCacheEntries);
|
|
1121
1239
|
}
|
|
1122
1240
|
return {
|
|
1123
1241
|
ok: true,
|
|
1124
1242
|
jwks,
|
|
1125
1243
|
fromCache: false,
|
|
1126
|
-
rawBytes: jwksResult.rawBytes
|
|
1244
|
+
rawBytes: jwksResult.rawBytes,
|
|
1245
|
+
revokedKeys
|
|
1127
1246
|
};
|
|
1128
1247
|
}
|
|
1129
1248
|
|
|
@@ -1184,6 +1303,25 @@ async function verifyReceipt(optionsOrJws) {
|
|
|
1184
1303
|
if (!jwksResult.fromCache) {
|
|
1185
1304
|
jwksFetchTime = performance.now() - jwksFetchStart;
|
|
1186
1305
|
}
|
|
1306
|
+
if (jwksResult.revokedKeys) {
|
|
1307
|
+
const revokedEntry = jwksResult.revokedKeys.find((rk) => rk.kid === header.kid);
|
|
1308
|
+
if (revokedEntry) {
|
|
1309
|
+
const durationMs = performance.now() - startTime;
|
|
1310
|
+
fireTelemetryHook(telemetry?.onReceiptVerified, {
|
|
1311
|
+
receiptHash: hashReceipt(receiptJws),
|
|
1312
|
+
valid: false,
|
|
1313
|
+
reasonCode: "key_revoked",
|
|
1314
|
+
issuer: payload.iss,
|
|
1315
|
+
kid: header.kid,
|
|
1316
|
+
durationMs
|
|
1317
|
+
});
|
|
1318
|
+
return {
|
|
1319
|
+
ok: false,
|
|
1320
|
+
reason: "key_revoked",
|
|
1321
|
+
details: `Key kid=${header.kid} was revoked at ${revokedEntry.revoked_at}${revokedEntry.reason ? ` (reason: ${revokedEntry.reason})` : ""}`
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1187
1325
|
const jwk = jwksResult.jwks.keys.find((k) => k.kid === header.kid);
|
|
1188
1326
|
if (!jwk) {
|
|
1189
1327
|
const durationMs = performance.now() - startTime;
|
|
@@ -1261,6 +1399,13 @@ var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
1261
1399
|
"CRYPTO_INVALID_ALG",
|
|
1262
1400
|
"CRYPTO_INVALID_KEY_LENGTH"
|
|
1263
1401
|
]);
|
|
1402
|
+
var JOSE_CODE_MAP = {
|
|
1403
|
+
CRYPTO_JWS_EMBEDDED_KEY: "E_JWS_EMBEDDED_KEY",
|
|
1404
|
+
CRYPTO_JWS_CRIT_REJECTED: "E_JWS_CRIT_REJECTED",
|
|
1405
|
+
CRYPTO_JWS_MISSING_KID: "E_JWS_MISSING_KID",
|
|
1406
|
+
CRYPTO_JWS_B64_REJECTED: "E_JWS_B64_REJECTED",
|
|
1407
|
+
CRYPTO_JWS_ZIP_REJECTED: "E_JWS_ZIP_REJECTED"
|
|
1408
|
+
};
|
|
1264
1409
|
var MAX_PARSE_ISSUES = 25;
|
|
1265
1410
|
function sanitizeParseIssues(issues) {
|
|
1266
1411
|
if (!Array.isArray(issues)) return void 0;
|
|
@@ -1270,7 +1415,16 @@ function sanitizeParseIssues(issues) {
|
|
|
1270
1415
|
}));
|
|
1271
1416
|
}
|
|
1272
1417
|
async function verifyLocal(jws, publicKey, options = {}) {
|
|
1273
|
-
const {
|
|
1418
|
+
const {
|
|
1419
|
+
issuer,
|
|
1420
|
+
audience,
|
|
1421
|
+
subjectUri,
|
|
1422
|
+
rid,
|
|
1423
|
+
requireExp = false,
|
|
1424
|
+
maxClockSkew = 300,
|
|
1425
|
+
strictness = "strict",
|
|
1426
|
+
policyDigest
|
|
1427
|
+
} = options;
|
|
1274
1428
|
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
1275
1429
|
try {
|
|
1276
1430
|
const result = await crypto.verify(jws, publicKey);
|
|
@@ -1281,6 +1435,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
|
|
|
1281
1435
|
message: "Ed25519 signature verification failed"
|
|
1282
1436
|
};
|
|
1283
1437
|
}
|
|
1438
|
+
const accumulatedWarnings = [];
|
|
1439
|
+
if (result.header.typ === void 0) {
|
|
1440
|
+
if (strictness === "strict") {
|
|
1441
|
+
return {
|
|
1442
|
+
valid: false,
|
|
1443
|
+
code: "E_INVALID_FORMAT",
|
|
1444
|
+
message: "Missing JWS typ header: strict mode requires typ to be present"
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
accumulatedWarnings.push({
|
|
1448
|
+
code: schema.WARNING_TYP_MISSING,
|
|
1449
|
+
message: "JWS typ header is absent; accepted in interop mode"
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1284
1452
|
const constraintResult = schema.validateKernelConstraints(result.payload);
|
|
1285
1453
|
if (!constraintResult.valid) {
|
|
1286
1454
|
const v = constraintResult.violations[0];
|
|
@@ -1299,46 +1467,136 @@ async function verifyLocal(jws, publicKey, options = {}) {
|
|
|
1299
1467
|
details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
|
|
1300
1468
|
};
|
|
1301
1469
|
}
|
|
1302
|
-
if (
|
|
1470
|
+
if (pr.wireVersion === "0.2") {
|
|
1471
|
+
accumulatedWarnings.push(...pr.warnings);
|
|
1472
|
+
}
|
|
1473
|
+
if (pr.wireVersion === "0.2") {
|
|
1474
|
+
const claims = pr.claims;
|
|
1475
|
+
if (issuer !== void 0 && claims.iss !== issuer) {
|
|
1476
|
+
return {
|
|
1477
|
+
valid: false,
|
|
1478
|
+
code: "E_INVALID_ISSUER",
|
|
1479
|
+
message: `Issuer mismatch: expected "${issuer}", got "${claims.iss}"`
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
if (subjectUri !== void 0 && claims.sub !== subjectUri) {
|
|
1483
|
+
return {
|
|
1484
|
+
valid: false,
|
|
1485
|
+
code: "E_INVALID_SUBJECT",
|
|
1486
|
+
message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
if (claims.iat > now + maxClockSkew) {
|
|
1490
|
+
return {
|
|
1491
|
+
valid: false,
|
|
1492
|
+
code: "E_NOT_YET_VALID",
|
|
1493
|
+
message: `Receipt not yet valid: issued at ${new Date(claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
if (claims.kind === "evidence") {
|
|
1497
|
+
const skewResult = schema.checkOccurredAtSkew(claims.occurred_at, claims.iat, now, maxClockSkew);
|
|
1498
|
+
if (skewResult === "future_error") {
|
|
1499
|
+
return {
|
|
1500
|
+
valid: false,
|
|
1501
|
+
code: "E_OCCURRED_AT_FUTURE",
|
|
1502
|
+
message: `occurred_at is in the future beyond tolerance (${maxClockSkew}s)`
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
if (skewResult !== null) {
|
|
1506
|
+
accumulatedWarnings.push(skewResult);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (!schema.REGISTERED_RECEIPT_TYPES.has(claims.type)) {
|
|
1510
|
+
accumulatedWarnings.push({
|
|
1511
|
+
code: schema.WARNING_TYPE_UNREGISTERED,
|
|
1512
|
+
message: "Receipt type is not in the recommended type registry",
|
|
1513
|
+
pointer: "/type"
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
if (claims.extensions !== void 0) {
|
|
1517
|
+
for (const key of Object.keys(claims.extensions)) {
|
|
1518
|
+
if (!schema.REGISTERED_EXTENSION_GROUP_KEYS.has(key) && schema.isValidExtensionKey(key)) {
|
|
1519
|
+
const escapedKey = key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1520
|
+
accumulatedWarnings.push({
|
|
1521
|
+
code: schema.WARNING_UNKNOWN_EXTENSION,
|
|
1522
|
+
message: "Unknown extension key preserved without schema validation",
|
|
1523
|
+
pointer: `/extensions/${escapedKey}`
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (policyDigest !== void 0 && !kernel.HASH.pattern.test(policyDigest)) {
|
|
1529
|
+
return {
|
|
1530
|
+
valid: false,
|
|
1531
|
+
code: "E_INVALID_FORMAT",
|
|
1532
|
+
message: "policyDigest option must be in sha256:<64 lowercase hex> format"
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
const receiptPolicyDigest = claims.policy?.digest;
|
|
1536
|
+
const bindingStatus = receiptPolicyDigest === void 0 || policyDigest === void 0 ? "unavailable" : schema.verifyPolicyBinding(receiptPolicyDigest, policyDigest);
|
|
1537
|
+
if (bindingStatus === "failed") {
|
|
1538
|
+
return {
|
|
1539
|
+
valid: false,
|
|
1540
|
+
code: "E_POLICY_BINDING_FAILED",
|
|
1541
|
+
message: "Policy binding check failed: receipt policy digest does not match local policy",
|
|
1542
|
+
details: {
|
|
1543
|
+
receipt_policy_digest: receiptPolicyDigest,
|
|
1544
|
+
local_policy_digest: policyDigest,
|
|
1545
|
+
...claims.policy?.uri !== void 0 && { policy_uri: claims.policy.uri }
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
return {
|
|
1550
|
+
valid: true,
|
|
1551
|
+
variant: "wire-02",
|
|
1552
|
+
claims,
|
|
1553
|
+
kid: result.header.kid,
|
|
1554
|
+
wireVersion: "0.2",
|
|
1555
|
+
warnings: schema.sortWarnings(accumulatedWarnings),
|
|
1556
|
+
policy_binding: bindingStatus
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
const w01 = pr.claims;
|
|
1560
|
+
if (issuer !== void 0 && w01.iss !== issuer) {
|
|
1303
1561
|
return {
|
|
1304
1562
|
valid: false,
|
|
1305
1563
|
code: "E_INVALID_ISSUER",
|
|
1306
|
-
message: `Issuer mismatch: expected "${issuer}", got "${
|
|
1564
|
+
message: `Issuer mismatch: expected "${issuer}", got "${w01.iss}"`
|
|
1307
1565
|
};
|
|
1308
1566
|
}
|
|
1309
|
-
if (audience !== void 0 &&
|
|
1567
|
+
if (audience !== void 0 && w01.aud !== audience) {
|
|
1310
1568
|
return {
|
|
1311
1569
|
valid: false,
|
|
1312
1570
|
code: "E_INVALID_AUDIENCE",
|
|
1313
|
-
message: `Audience mismatch: expected "${audience}", got "${
|
|
1571
|
+
message: `Audience mismatch: expected "${audience}", got "${w01.aud}"`
|
|
1314
1572
|
};
|
|
1315
1573
|
}
|
|
1316
|
-
if (rid !== void 0 &&
|
|
1574
|
+
if (rid !== void 0 && w01.rid !== rid) {
|
|
1317
1575
|
return {
|
|
1318
1576
|
valid: false,
|
|
1319
1577
|
code: "E_INVALID_RECEIPT_ID",
|
|
1320
|
-
message: `Receipt ID mismatch: expected "${rid}", got "${
|
|
1578
|
+
message: `Receipt ID mismatch: expected "${rid}", got "${w01.rid}"`
|
|
1321
1579
|
};
|
|
1322
1580
|
}
|
|
1323
|
-
if (requireExp &&
|
|
1581
|
+
if (requireExp && w01.exp === void 0) {
|
|
1324
1582
|
return {
|
|
1325
1583
|
valid: false,
|
|
1326
1584
|
code: "E_MISSING_EXP",
|
|
1327
1585
|
message: "Receipt missing required exp claim"
|
|
1328
1586
|
};
|
|
1329
1587
|
}
|
|
1330
|
-
if (
|
|
1588
|
+
if (w01.iat > now + maxClockSkew) {
|
|
1331
1589
|
return {
|
|
1332
1590
|
valid: false,
|
|
1333
1591
|
code: "E_NOT_YET_VALID",
|
|
1334
|
-
message: `Receipt not yet valid: issued at ${new Date(
|
|
1592
|
+
message: `Receipt not yet valid: issued at ${new Date(w01.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
|
|
1335
1593
|
};
|
|
1336
1594
|
}
|
|
1337
|
-
if (
|
|
1595
|
+
if (w01.exp !== void 0 && w01.exp < now - maxClockSkew) {
|
|
1338
1596
|
return {
|
|
1339
1597
|
valid: false,
|
|
1340
1598
|
code: "E_EXPIRED",
|
|
1341
|
-
message: `Receipt expired at ${new Date(
|
|
1599
|
+
message: `Receipt expired at ${new Date(w01.exp * 1e3).toISOString()}`
|
|
1342
1600
|
};
|
|
1343
1601
|
}
|
|
1344
1602
|
if (pr.variant === "commerce") {
|
|
@@ -1355,6 +1613,8 @@ async function verifyLocal(jws, publicKey, options = {}) {
|
|
|
1355
1613
|
variant: "commerce",
|
|
1356
1614
|
claims,
|
|
1357
1615
|
kid: result.header.kid,
|
|
1616
|
+
wireVersion: "0.1",
|
|
1617
|
+
warnings: [],
|
|
1358
1618
|
policy_binding: "unavailable"
|
|
1359
1619
|
};
|
|
1360
1620
|
} else {
|
|
@@ -1371,11 +1631,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
|
|
|
1371
1631
|
variant: "attestation",
|
|
1372
1632
|
claims,
|
|
1373
1633
|
kid: result.header.kid,
|
|
1634
|
+
wireVersion: "0.1",
|
|
1635
|
+
warnings: [],
|
|
1374
1636
|
policy_binding: "unavailable"
|
|
1375
1637
|
};
|
|
1376
1638
|
}
|
|
1377
1639
|
} catch (err) {
|
|
1378
1640
|
if (isCryptoError(err)) {
|
|
1641
|
+
if (Object.prototype.hasOwnProperty.call(JOSE_CODE_MAP, err.code)) {
|
|
1642
|
+
return {
|
|
1643
|
+
valid: false,
|
|
1644
|
+
code: JOSE_CODE_MAP[err.code],
|
|
1645
|
+
message: err.message
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1379
1648
|
if (FORMAT_ERROR_CODES.has(err.code)) {
|
|
1380
1649
|
return {
|
|
1381
1650
|
valid: false,
|
|
@@ -1390,6 +1659,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
|
|
|
1390
1659
|
message: err.message
|
|
1391
1660
|
};
|
|
1392
1661
|
}
|
|
1662
|
+
if (err.code === "CRYPTO_WIRE_VERSION_MISMATCH") {
|
|
1663
|
+
return {
|
|
1664
|
+
valid: false,
|
|
1665
|
+
code: "E_WIRE_VERSION_MISMATCH",
|
|
1666
|
+
message: err.message
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1393
1669
|
}
|
|
1394
1670
|
if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
|
|
1395
1671
|
const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
|
|
@@ -1413,6 +1689,9 @@ function isCommerceResult(r) {
|
|
|
1413
1689
|
function isAttestationResult(r) {
|
|
1414
1690
|
return r.valid === true && r.variant === "attestation";
|
|
1415
1691
|
}
|
|
1692
|
+
function isWire02Result(r) {
|
|
1693
|
+
return r.valid === true && r.variant === "wire-02";
|
|
1694
|
+
}
|
|
1416
1695
|
function setReceiptHeader(headers, receiptJws) {
|
|
1417
1696
|
headers.set(schema.PEAC_RECEIPT_HEADER, receiptJws);
|
|
1418
1697
|
}
|
|
@@ -1454,6 +1733,16 @@ function setVaryPurposeHeader(headers) {
|
|
|
1454
1733
|
headers.set("Vary", schema.PEAC_PURPOSE_HEADER);
|
|
1455
1734
|
}
|
|
1456
1735
|
}
|
|
1736
|
+
async function computePolicyDigestJcs(policy) {
|
|
1737
|
+
const hex = await crypto.jcsHash(policy);
|
|
1738
|
+
return `${kernel.HASH.prefix}${hex}`;
|
|
1739
|
+
}
|
|
1740
|
+
function checkPolicyBinding(receiptDigest, localDigest) {
|
|
1741
|
+
if (receiptDigest === void 0 || localDigest === void 0) {
|
|
1742
|
+
return "unavailable";
|
|
1743
|
+
}
|
|
1744
|
+
return schema.verifyPolicyBinding(receiptDigest, localDigest);
|
|
1745
|
+
}
|
|
1457
1746
|
var DEFAULT_VERIFIER_LIMITS = {
|
|
1458
1747
|
max_receipt_bytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
|
|
1459
1748
|
max_jwks_bytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
|
|
@@ -2769,7 +3058,10 @@ exports.NON_DETERMINISTIC_ARTIFACT_KEYS = NON_DETERMINISTIC_ARTIFACT_KEYS;
|
|
|
2769
3058
|
exports.VerificationReportBuilder = VerificationReportBuilder;
|
|
2770
3059
|
exports.buildFailureReport = buildFailureReport;
|
|
2771
3060
|
exports.buildSuccessReport = buildSuccessReport;
|
|
3061
|
+
exports.checkPolicyBinding = checkPolicyBinding;
|
|
2772
3062
|
exports.clearJWKSCache = clearJWKSCache;
|
|
3063
|
+
exports.clearKidThumbprints = clearKidThumbprints;
|
|
3064
|
+
exports.computePolicyDigestJcs = computePolicyDigestJcs;
|
|
2773
3065
|
exports.computeReceiptDigest = computeReceiptDigest;
|
|
2774
3066
|
exports.createDefaultPolicy = createDefaultPolicy;
|
|
2775
3067
|
exports.createDigest = createDigest;
|
|
@@ -2782,14 +3074,17 @@ exports.fetchPointerSafe = fetchPointerSafe;
|
|
|
2782
3074
|
exports.fetchPointerWithDigest = fetchPointerWithDigest;
|
|
2783
3075
|
exports.fetchPolicyManifest = fetchPolicyManifest;
|
|
2784
3076
|
exports.getJWKSCacheSize = getJWKSCacheSize;
|
|
3077
|
+
exports.getKidThumbprintSize = getKidThumbprintSize;
|
|
2785
3078
|
exports.getPurposeHeader = getPurposeHeader;
|
|
2786
3079
|
exports.getReceiptHeader = getReceiptHeader;
|
|
2787
3080
|
exports.getSSRFCapabilities = getSSRFCapabilities;
|
|
2788
3081
|
exports.isAttestationResult = isAttestationResult;
|
|
2789
3082
|
exports.isBlockedIP = isBlockedIP;
|
|
2790
3083
|
exports.isCommerceResult = isCommerceResult;
|
|
3084
|
+
exports.isWire02Result = isWire02Result;
|
|
2791
3085
|
exports.issue = issue;
|
|
2792
3086
|
exports.issueJws = issueJws;
|
|
3087
|
+
exports.issueWire02 = issueWire02;
|
|
2793
3088
|
exports.parseBodyProfile = parseBodyProfile;
|
|
2794
3089
|
exports.parseDiscovery = parseDiscovery;
|
|
2795
3090
|
exports.parseHeaderProfile = parseHeaderProfile;
|