@peac/protocol 0.11.3 → 0.12.0-preview.2

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.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './verify-local';
8
8
  export * from './headers';
9
9
  export * from './discovery';
10
10
  export * from './jwks-resolver';
11
+ export { computePolicyDigestJcs, checkPolicyBinding } from './policy-binding';
11
12
  export * from './verifier-types';
12
13
  export * from './verifier-core';
13
14
  export * from './verification-report';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAGhC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,SAAS,EACT,MAAM,GACP,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG9E,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,SAAS,EACT,MAAM,GACP,MAAM,cAAc,CAAC"}
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { uuidv7 } from 'uuidv7';
2
- import { sign, decode, verify, sha256Hex, computeJwkThumbprint, jwkToPublicKeyBytes, base64urlDecode } from '@peac/crypto';
2
+ import { sign, signWire02, decode, verify, jcsHash, 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, 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';
5
+ import { isValidPurposeToken, isCanonicalPurpose, isValidPurposeReason, isValidWorkflowContext, createWorkflowContextInvalidError, hasValidDagSemantics, createWorkflowDagInvalidError, WORKFLOW_EXTENSION_KEY, validateKernelConstraints, createConstraintViolationError, ReceiptClaims, createEvidenceNotJsonError, validateSubjectSnapshot, isCanonicalIss, Wire02ClaimsSchema, PEAC_ISSUER_CONFIG_MAX_BYTES, validateRevokedKeys, PEAC_ISSUER_CONFIG_PATH, PEAC_POLICY_MAX_BYTES, PEAC_POLICY_PATH, PEAC_POLICY_FALLBACK_PATH, WARNING_TYP_MISSING, parseReceiptClaims, checkOccurredAtSkew, REGISTERED_RECEIPT_TYPES, WARNING_TYPE_UNREGISTERED, REGISTERED_EXTENSION_GROUP_KEYS, isValidExtensionKey, WARNING_UNKNOWN_EXTENSION, verifyPolicyBinding, sortWarnings, 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
- import { VERIFIER_LIMITS, VERIFIER_NETWORK, VERIFIER_POLICY_VERSION, VERIFICATION_REPORT_VERSION, WIRE_TYPE } from '@peac/kernel';
7
+ import { VERIFIER_LIMITS, VERIFIER_NETWORK, HASH, VERIFIER_POLICY_VERSION, VERIFICATION_REPORT_VERSION, WIRE_TYPE } from '@peac/kernel';
8
8
 
9
9
  // src/issue.ts
10
10
  function fireTelemetryHook(fn, input) {
@@ -178,6 +178,52 @@ async function issueJws(options) {
178
178
  const result = await issue(options);
179
179
  return result.jws;
180
180
  }
181
+ async function issueWire02(options) {
182
+ if (!isCanonicalIss(options.iss)) {
183
+ throw new IssueError({
184
+ code: "E_ISS_NOT_CANONICAL",
185
+ category: "validation",
186
+ severity: "error",
187
+ retryable: false,
188
+ http_status: 400,
189
+ details: {
190
+ message: `iss is not in canonical form: "${options.iss}". Use an https://<origin> or did:<method> identifier.`
191
+ }
192
+ });
193
+ }
194
+ const jti = options.jti ?? uuidv7();
195
+ const iat = Math.floor(Date.now() / 1e3);
196
+ const claims = {
197
+ peac_version: "0.2",
198
+ kind: options.kind,
199
+ type: options.type,
200
+ iss: options.iss,
201
+ iat,
202
+ jti,
203
+ ...options.sub !== void 0 && { sub: options.sub },
204
+ ...options.pillars !== void 0 && { pillars: options.pillars },
205
+ ...options.occurred_at !== void 0 && { occurred_at: options.occurred_at },
206
+ ...options.purpose_declared !== void 0 && { purpose_declared: options.purpose_declared },
207
+ ...options.policy !== void 0 && { policy: options.policy },
208
+ ...options.extensions !== void 0 && { extensions: options.extensions }
209
+ };
210
+ const parseResult = Wire02ClaimsSchema.safeParse(claims);
211
+ if (!parseResult.success) {
212
+ const firstIssue = parseResult.error.issues[0];
213
+ throw new IssueError({
214
+ code: "E_INVALID_FORMAT",
215
+ category: "validation",
216
+ severity: "error",
217
+ retryable: false,
218
+ http_status: 400,
219
+ details: {
220
+ message: `Wire 0.2 claims schema validation failed: ${firstIssue?.message ?? "unknown"}`
221
+ }
222
+ });
223
+ }
224
+ const jws = await signWire02(claims, options.privateKey, options.kid);
225
+ return { jws };
226
+ }
181
227
  function parseIssuerConfig(json) {
182
228
  let config;
183
229
  if (typeof json === "string") {
@@ -1352,6 +1398,13 @@ var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
1352
1398
  "CRYPTO_INVALID_ALG",
1353
1399
  "CRYPTO_INVALID_KEY_LENGTH"
1354
1400
  ]);
1401
+ var JOSE_CODE_MAP = {
1402
+ CRYPTO_JWS_EMBEDDED_KEY: "E_JWS_EMBEDDED_KEY",
1403
+ CRYPTO_JWS_CRIT_REJECTED: "E_JWS_CRIT_REJECTED",
1404
+ CRYPTO_JWS_MISSING_KID: "E_JWS_MISSING_KID",
1405
+ CRYPTO_JWS_B64_REJECTED: "E_JWS_B64_REJECTED",
1406
+ CRYPTO_JWS_ZIP_REJECTED: "E_JWS_ZIP_REJECTED"
1407
+ };
1355
1408
  var MAX_PARSE_ISSUES = 25;
1356
1409
  function sanitizeParseIssues(issues) {
1357
1410
  if (!Array.isArray(issues)) return void 0;
@@ -1361,7 +1414,7 @@ function sanitizeParseIssues(issues) {
1361
1414
  }));
1362
1415
  }
1363
1416
  async function verifyLocal(jws, publicKey, options = {}) {
1364
- const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
1417
+ const { issuer, subjectUri, maxClockSkew = 300, strictness = "strict", policyDigest } = options;
1365
1418
  const now = options.now ?? Math.floor(Date.now() / 1e3);
1366
1419
  try {
1367
1420
  const result = await verify(jws, publicKey);
@@ -1372,6 +1425,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
1372
1425
  message: "Ed25519 signature verification failed"
1373
1426
  };
1374
1427
  }
1428
+ const accumulatedWarnings = [];
1429
+ if (result.header.typ === void 0) {
1430
+ if (strictness === "strict") {
1431
+ return {
1432
+ valid: false,
1433
+ code: "E_INVALID_FORMAT",
1434
+ message: "Missing JWS typ header: strict mode requires typ to be present"
1435
+ };
1436
+ }
1437
+ accumulatedWarnings.push({
1438
+ code: WARNING_TYP_MISSING,
1439
+ message: "JWS typ header is absent; accepted in interop mode"
1440
+ });
1441
+ }
1375
1442
  const constraintResult = validateKernelConstraints(result.payload);
1376
1443
  if (!constraintResult.valid) {
1377
1444
  const v = constraintResult.violations[0];
@@ -1390,66 +1457,18 @@ async function verifyLocal(jws, publicKey, options = {}) {
1390
1457
  details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
1391
1458
  };
1392
1459
  }
1393
- if (issuer !== void 0 && pr.claims.iss !== issuer) {
1394
- return {
1395
- valid: false,
1396
- code: "E_INVALID_ISSUER",
1397
- message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
1398
- };
1460
+ if (pr.wireVersion === "0.2") {
1461
+ accumulatedWarnings.push(...pr.warnings);
1399
1462
  }
1400
- if (audience !== void 0 && pr.claims.aud !== audience) {
1401
- return {
1402
- valid: false,
1403
- code: "E_INVALID_AUDIENCE",
1404
- message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
1405
- };
1406
- }
1407
- if (rid !== void 0 && pr.claims.rid !== rid) {
1408
- return {
1409
- valid: false,
1410
- code: "E_INVALID_RECEIPT_ID",
1411
- message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
1412
- };
1413
- }
1414
- if (requireExp && pr.claims.exp === void 0) {
1415
- return {
1416
- valid: false,
1417
- code: "E_MISSING_EXP",
1418
- message: "Receipt missing required exp claim"
1419
- };
1420
- }
1421
- if (pr.claims.iat > now + maxClockSkew) {
1422
- return {
1423
- valid: false,
1424
- code: "E_NOT_YET_VALID",
1425
- message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1426
- };
1427
- }
1428
- if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
1429
- return {
1430
- valid: false,
1431
- code: "E_EXPIRED",
1432
- message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
1433
- };
1434
- }
1435
- if (pr.variant === "commerce") {
1463
+ if (pr.wireVersion === "0.2") {
1436
1464
  const claims = pr.claims;
1437
- if (subjectUri !== void 0 && claims.subject?.uri !== subjectUri) {
1465
+ if (issuer !== void 0 && claims.iss !== issuer) {
1438
1466
  return {
1439
1467
  valid: false,
1440
- code: "E_INVALID_SUBJECT",
1441
- message: `Subject mismatch: expected "${subjectUri}", got "${claims.subject?.uri ?? "undefined"}"`
1468
+ code: "E_INVALID_ISSUER",
1469
+ message: `Issuer mismatch: expected "${issuer}", got "${claims.iss}"`
1442
1470
  };
1443
1471
  }
1444
- return {
1445
- valid: true,
1446
- variant: "commerce",
1447
- claims,
1448
- kid: result.header.kid,
1449
- policy_binding: "unavailable"
1450
- };
1451
- } else {
1452
- const claims = pr.claims;
1453
1472
  if (subjectUri !== void 0 && claims.sub !== subjectUri) {
1454
1473
  return {
1455
1474
  valid: false,
@@ -1457,16 +1476,90 @@ async function verifyLocal(jws, publicKey, options = {}) {
1457
1476
  message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
1458
1477
  };
1459
1478
  }
1479
+ if (claims.iat > now + maxClockSkew) {
1480
+ return {
1481
+ valid: false,
1482
+ code: "E_NOT_YET_VALID",
1483
+ message: `Receipt not yet valid: issued at ${new Date(claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1484
+ };
1485
+ }
1486
+ if (claims.kind === "evidence") {
1487
+ const skewResult = checkOccurredAtSkew(claims.occurred_at, claims.iat, now, maxClockSkew);
1488
+ if (skewResult === "future_error") {
1489
+ return {
1490
+ valid: false,
1491
+ code: "E_OCCURRED_AT_FUTURE",
1492
+ message: `occurred_at is in the future beyond tolerance (${maxClockSkew}s)`
1493
+ };
1494
+ }
1495
+ if (skewResult !== null) {
1496
+ accumulatedWarnings.push(skewResult);
1497
+ }
1498
+ }
1499
+ if (!REGISTERED_RECEIPT_TYPES.has(claims.type)) {
1500
+ accumulatedWarnings.push({
1501
+ code: WARNING_TYPE_UNREGISTERED,
1502
+ message: "Receipt type is not in the recommended type registry",
1503
+ pointer: "/type"
1504
+ });
1505
+ }
1506
+ if (claims.extensions !== void 0) {
1507
+ for (const key of Object.keys(claims.extensions)) {
1508
+ if (!REGISTERED_EXTENSION_GROUP_KEYS.has(key) && isValidExtensionKey(key)) {
1509
+ const escapedKey = key.replace(/~/g, "~0").replace(/\//g, "~1");
1510
+ accumulatedWarnings.push({
1511
+ code: WARNING_UNKNOWN_EXTENSION,
1512
+ message: "Unknown extension key preserved without schema validation",
1513
+ pointer: `/extensions/${escapedKey}`
1514
+ });
1515
+ }
1516
+ }
1517
+ }
1518
+ if (policyDigest !== void 0 && !HASH.pattern.test(policyDigest)) {
1519
+ return {
1520
+ valid: false,
1521
+ code: "E_INVALID_FORMAT",
1522
+ message: "policyDigest option must be in sha256:<64 lowercase hex> format"
1523
+ };
1524
+ }
1525
+ const receiptPolicyDigest = claims.policy?.digest;
1526
+ const bindingStatus = receiptPolicyDigest === void 0 || policyDigest === void 0 ? "unavailable" : verifyPolicyBinding(receiptPolicyDigest, policyDigest);
1527
+ if (bindingStatus === "failed") {
1528
+ return {
1529
+ valid: false,
1530
+ code: "E_POLICY_BINDING_FAILED",
1531
+ message: "Policy binding check failed: receipt policy digest does not match local policy",
1532
+ details: {
1533
+ receipt_policy_digest: receiptPolicyDigest,
1534
+ local_policy_digest: policyDigest,
1535
+ ...claims.policy?.uri !== void 0 && { policy_uri: claims.policy.uri }
1536
+ }
1537
+ };
1538
+ }
1460
1539
  return {
1461
1540
  valid: true,
1462
- variant: "attestation",
1541
+ variant: "wire-02",
1463
1542
  claims,
1464
1543
  kid: result.header.kid,
1465
- policy_binding: "unavailable"
1544
+ wireVersion: "0.2",
1545
+ warnings: sortWarnings(accumulatedWarnings),
1546
+ policy_binding: bindingStatus
1466
1547
  };
1467
1548
  }
1549
+ return {
1550
+ valid: false,
1551
+ code: "E_UNSUPPORTED_WIRE_VERSION",
1552
+ message: "Wire 0.1 receipts are not supported. Re-issue as Wire 0.2 using issueWire02()."
1553
+ };
1468
1554
  } catch (err) {
1469
1555
  if (isCryptoError(err)) {
1556
+ if (Object.prototype.hasOwnProperty.call(JOSE_CODE_MAP, err.code)) {
1557
+ return {
1558
+ valid: false,
1559
+ code: JOSE_CODE_MAP[err.code],
1560
+ message: err.message
1561
+ };
1562
+ }
1470
1563
  if (FORMAT_ERROR_CODES.has(err.code)) {
1471
1564
  return {
1472
1565
  valid: false,
@@ -1481,6 +1574,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
1481
1574
  message: err.message
1482
1575
  };
1483
1576
  }
1577
+ if (err.code === "CRYPTO_WIRE_VERSION_MISMATCH") {
1578
+ return {
1579
+ valid: false,
1580
+ code: "E_WIRE_VERSION_MISMATCH",
1581
+ message: err.message
1582
+ };
1583
+ }
1484
1584
  }
1485
1585
  if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
1486
1586
  const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
@@ -1499,10 +1599,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
1499
1599
  }
1500
1600
  }
1501
1601
  function isCommerceResult(r) {
1502
- return r.valid === true && r.variant === "commerce";
1602
+ return false;
1503
1603
  }
1504
1604
  function isAttestationResult(r) {
1505
- return r.valid === true && r.variant === "attestation";
1605
+ return false;
1606
+ }
1607
+ function isWire02Result(r) {
1608
+ return r.valid === true && r.variant === "wire-02";
1506
1609
  }
1507
1610
  function setReceiptHeader(headers, receiptJws) {
1508
1611
  headers.set(PEAC_RECEIPT_HEADER, receiptJws);
@@ -1545,6 +1648,16 @@ function setVaryPurposeHeader(headers) {
1545
1648
  headers.set("Vary", PEAC_PURPOSE_HEADER);
1546
1649
  }
1547
1650
  }
1651
+ async function computePolicyDigestJcs(policy) {
1652
+ const hex = await jcsHash(policy);
1653
+ return `${HASH.prefix}${hex}`;
1654
+ }
1655
+ function checkPolicyBinding(receiptDigest, localDigest) {
1656
+ if (receiptDigest === void 0 || localDigest === void 0) {
1657
+ return "unavailable";
1658
+ }
1659
+ return verifyPolicyBinding(receiptDigest, localDigest);
1660
+ }
1548
1661
  var DEFAULT_VERIFIER_LIMITS = {
1549
1662
  max_receipt_bytes: VERIFIER_LIMITS.maxReceiptBytes,
1550
1663
  max_jwks_bytes: VERIFIER_LIMITS.maxJwksBytes,
@@ -2820,6 +2933,6 @@ async function verifyAndFetchPointer(pointerHeader, fetchOptions) {
2820
2933
  });
2821
2934
  }
2822
2935
 
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 };
2936
+ export { CHECK_IDS, DEFAULT_NETWORK_SECURITY, DEFAULT_VERIFIER_LIMITS, IssueError, NON_DETERMINISTIC_ARTIFACT_KEYS, VerificationReportBuilder, buildFailureReport, buildSuccessReport, checkPolicyBinding, clearJWKSCache, clearKidThumbprints, computePolicyDigestJcs, computeReceiptDigest, createDefaultPolicy, createDigest, createEmptyReport, createReportBuilder, fetchDiscovery, fetchIssuerConfig, fetchJWKSSafe, fetchPointerSafe, fetchPointerWithDigest, fetchPolicyManifest, getJWKSCacheSize, getKidThumbprintSize, getPurposeHeader, getReceiptHeader, getSSRFCapabilities, isAttestationResult, isBlockedIP, isCommerceResult, isWire02Result, issue, issueJws, issueWire02, parseBodyProfile, parseDiscovery, parseHeaderProfile, parseIssuerConfig, parsePointerProfile, parsePolicyManifest, parseTransportProfile, reasonCodeToErrorCode, reasonCodeToSeverity, resetSSRFCapabilitiesCache, resolveJWKS, setPurposeAppliedHeader, setPurposeReasonHeader, setReceiptHeader, setVaryHeader, setVaryPurposeHeader, ssrfErrorToReasonCode, ssrfSafeFetch, verifyAndFetchPointer, verifyLocal, verifyReceipt, verifyReceiptCore };
2824
2937
  //# sourceMappingURL=index.mjs.map
2825
2938
  //# sourceMappingURL=index.mjs.map