@peac/protocol 0.11.1 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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, 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';
5
+ import { isValidPurposeToken, isCanonicalPurpose, isValidPurposeReason, isValidWorkflowContext, createWorkflowContextInvalidError, hasValidDagSemantics, createWorkflowDagInvalidError, WORKFLOW_EXTENSION_KEY, validateKernelConstraints, createConstraintViolationError, ReceiptClaims, createEvidenceNotJsonError, validateSubjectSnapshot, PEAC_ISSUER_CONFIG_MAX_BYTES, validateRevokedKeys, 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
 
@@ -237,6 +237,17 @@ function parseIssuerConfig(json) {
237
237
  throw new Error("payment_rails must be an array");
238
238
  }
239
239
  }
240
+ let revokedKeys;
241
+ if (obj.revoked_keys !== void 0) {
242
+ if (!Array.isArray(obj.revoked_keys)) {
243
+ throw new Error("revoked_keys must be an array");
244
+ }
245
+ const result = validateRevokedKeys(obj.revoked_keys);
246
+ if (!result.ok) {
247
+ throw new Error(`Invalid revoked_keys: ${result.error}`);
248
+ }
249
+ revokedKeys = result.value;
250
+ }
240
251
  return {
241
252
  version: obj.version,
242
253
  issuer: obj.issuer,
@@ -245,7 +256,8 @@ function parseIssuerConfig(json) {
245
256
  receipt_versions: obj.receipt_versions,
246
257
  algorithms: obj.algorithms,
247
258
  payment_rails: obj.payment_rails,
248
- security_contact: obj.security_contact
259
+ security_contact: obj.security_contact,
260
+ revoked_keys: revokedKeys
249
261
  };
250
262
  }
251
263
  async function fetchIssuerConfig(issuerUrl) {
@@ -932,6 +944,53 @@ async function fetchPointerSafe(pointerUrl, options) {
932
944
  var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
933
945
  var DEFAULT_MAX_CACHE_ENTRIES = 1e3;
934
946
  var jwksCache = /* @__PURE__ */ new Map();
947
+ var KID_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
948
+ var MAX_KID_THUMBPRINT_ENTRIES = 1e4;
949
+ var kidThumbprints = /* @__PURE__ */ new Map();
950
+ function pruneExpiredKidEntries(now) {
951
+ for (const [key, entry] of kidThumbprints) {
952
+ if (now - entry.firstSeen >= KID_RETENTION_MS) {
953
+ kidThumbprints.delete(key);
954
+ }
955
+ }
956
+ }
957
+ function evictOldestKidEntries() {
958
+ if (kidThumbprints.size <= MAX_KID_THUMBPRINT_ENTRIES) return;
959
+ const entries = Array.from(kidThumbprints.entries()).sort(
960
+ (a, b) => a[1].firstSeen - b[1].firstSeen
961
+ );
962
+ const toRemove = kidThumbprints.size - MAX_KID_THUMBPRINT_ENTRIES;
963
+ for (let i = 0; i < toRemove; i++) {
964
+ kidThumbprints.delete(entries[i][0]);
965
+ }
966
+ }
967
+ function checkKidReuse(issuer, jwks, now) {
968
+ pruneExpiredKidEntries(now);
969
+ for (const key of jwks.keys) {
970
+ if (!key.kid || !key.x) continue;
971
+ const mapKey = `${issuer}|${key.kid}`;
972
+ const existing = kidThumbprints.get(mapKey);
973
+ if (existing) {
974
+ if (now - existing.firstSeen < KID_RETENTION_MS) {
975
+ if (existing.thumbprint !== key.x) {
976
+ return `Kid reuse detected: kid=${key.kid} for issuer ${issuer} maps to different key material`;
977
+ }
978
+ } else {
979
+ kidThumbprints.set(mapKey, { thumbprint: key.x, firstSeen: now });
980
+ }
981
+ } else {
982
+ kidThumbprints.set(mapKey, { thumbprint: key.x, firstSeen: now });
983
+ }
984
+ }
985
+ evictOldestKidEntries();
986
+ return null;
987
+ }
988
+ function clearKidThumbprints() {
989
+ kidThumbprints.clear();
990
+ }
991
+ function getKidThumbprintSize() {
992
+ return kidThumbprints.size;
993
+ }
935
994
  function cacheGet(key, now) {
936
995
  const entry = jwksCache.get(key);
937
996
  if (!entry) return void 0;
@@ -1020,7 +1079,7 @@ async function resolveJWKS(issuerUrl, options) {
1020
1079
  if (!noCache) {
1021
1080
  const cached = cacheGet(normalizedIssuer, now);
1022
1081
  if (cached) {
1023
- return { ok: true, jwks: cached.jwks, fromCache: true };
1082
+ return { ok: true, jwks: cached.jwks, fromCache: true, revokedKeys: cached.revokedKeys };
1024
1083
  }
1025
1084
  }
1026
1085
  if (!normalizedIssuer.startsWith("https://")) {
@@ -1115,14 +1174,28 @@ async function resolveJWKS(issuerUrl, options) {
1115
1174
  message: `JWKS has too many keys: ${jwks.keys.length} > ${VERIFIER_LIMITS.maxJwksKeys}`
1116
1175
  };
1117
1176
  }
1177
+ const kidReuseError = checkKidReuse(normalizedIssuer, jwks, now);
1178
+ if (kidReuseError) {
1179
+ return {
1180
+ ok: false,
1181
+ code: "E_KID_REUSE_DETECTED",
1182
+ message: kidReuseError
1183
+ };
1184
+ }
1185
+ const revokedKeys = issuerConfig.revoked_keys?.map((entry) => ({
1186
+ kid: entry.kid,
1187
+ revoked_at: entry.revoked_at,
1188
+ reason: entry.reason
1189
+ }));
1118
1190
  if (!noCache) {
1119
- cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs }, maxCacheEntries);
1191
+ cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs, revokedKeys }, maxCacheEntries);
1120
1192
  }
1121
1193
  return {
1122
1194
  ok: true,
1123
1195
  jwks,
1124
1196
  fromCache: false,
1125
- rawBytes: jwksResult.rawBytes
1197
+ rawBytes: jwksResult.rawBytes,
1198
+ revokedKeys
1126
1199
  };
1127
1200
  }
1128
1201
 
@@ -1183,6 +1256,25 @@ async function verifyReceipt(optionsOrJws) {
1183
1256
  if (!jwksResult.fromCache) {
1184
1257
  jwksFetchTime = performance.now() - jwksFetchStart;
1185
1258
  }
1259
+ if (jwksResult.revokedKeys) {
1260
+ const revokedEntry = jwksResult.revokedKeys.find((rk) => rk.kid === header.kid);
1261
+ if (revokedEntry) {
1262
+ const durationMs = performance.now() - startTime;
1263
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1264
+ receiptHash: hashReceipt(receiptJws),
1265
+ valid: false,
1266
+ reasonCode: "key_revoked",
1267
+ issuer: payload.iss,
1268
+ kid: header.kid,
1269
+ durationMs
1270
+ });
1271
+ return {
1272
+ ok: false,
1273
+ reason: "key_revoked",
1274
+ details: `Key kid=${header.kid} was revoked at ${revokedEntry.revoked_at}${revokedEntry.reason ? ` (reason: ${revokedEntry.reason})` : ""}`
1275
+ };
1276
+ }
1277
+ }
1186
1278
  const jwk = jwksResult.jwks.keys.find((k) => k.kid === header.kid);
1187
1279
  if (!jwk) {
1188
1280
  const durationMs = performance.now() - startTime;
@@ -2728,6 +2820,6 @@ async function verifyAndFetchPointer(pointerHeader, fetchOptions) {
2728
2820
  });
2729
2821
  }
2730
2822
 
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 };
2823
+ export { CHECK_IDS, DEFAULT_NETWORK_SECURITY, DEFAULT_VERIFIER_LIMITS, IssueError, NON_DETERMINISTIC_ARTIFACT_KEYS, VerificationReportBuilder, buildFailureReport, buildSuccessReport, clearJWKSCache, clearKidThumbprints, computeReceiptDigest, createDefaultPolicy, createDigest, createEmptyReport, createReportBuilder, fetchDiscovery, fetchIssuerConfig, fetchJWKSSafe, fetchPointerSafe, fetchPointerWithDigest, fetchPolicyManifest, getJWKSCacheSize, getKidThumbprintSize, 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 };
2732
2824
  //# sourceMappingURL=index.mjs.map
2733
2825
  //# sourceMappingURL=index.mjs.map