@peac/protocol 0.11.2 → 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/README.md +68 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/index.cjs +98 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +98 -6
- package/dist/index.mjs.map +1 -1
- package/dist/jwks-resolver.d.ts +20 -0
- package/dist/jwks-resolver.d.ts.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/package.json +4 -4
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
|