@fedify/fedify 2.1.0-dev.503 → 2.1.0-dev.523

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.
Files changed (78) hide show
  1. package/dist/{builder-BHUnSQtB.js → builder-CJkMYxxc.js} +9 -3
  2. package/dist/compat/mod.d.cts +3 -3
  3. package/dist/compat/mod.d.ts +3 -3
  4. package/dist/compat/transformers.test.js +12 -12
  5. package/dist/{context-DZJhUmzF.d.ts → context--RwChtri.d.ts} +54 -2
  6. package/dist/{context-D3QkEtZd.d.cts → context-DL0cPpPV.d.cts} +54 -2
  7. package/dist/{deno-BYerLnry.js → deno-CQdJQjC5.js} +1 -1
  8. package/dist/{docloader-MSkogD2T.js → docloader-Cyl0-S8m.js} +2 -2
  9. package/dist/federation/builder.test.js +14 -3
  10. package/dist/federation/handler.test.js +97 -13
  11. package/dist/federation/idempotency.test.js +12 -12
  12. package/dist/federation/inbox.test.js +2 -2
  13. package/dist/federation/keycache.test.js +46 -2
  14. package/dist/federation/middleware.test.js +206 -12
  15. package/dist/federation/mod.cjs +4 -4
  16. package/dist/federation/mod.d.cts +4 -4
  17. package/dist/federation/mod.d.ts +4 -4
  18. package/dist/federation/mod.js +4 -4
  19. package/dist/federation/send.test.js +5 -5
  20. package/dist/federation/webfinger.test.js +12 -12
  21. package/dist/{http-DkHdFfrc.d.ts → http-BbfOqHGG.d.ts} +80 -8
  22. package/dist/{http-DJT6NciB.cjs → http-D6a6mMc0.cjs} +305 -99
  23. package/dist/{http-CSX1-Mgi.js → http-DJmytoC2.js} +295 -101
  24. package/dist/{http-S2U3qDwN.js → http-DK0CTomU.js} +153 -57
  25. package/dist/{http-Cz3MlXAZ.d.cts → http-DsqqmkXi.d.cts} +80 -8
  26. package/dist/{inbox-BaA0g5I_.js → inbox-CWa6sqsk.js} +1 -1
  27. package/dist/{key-DCdTVZiK.js → key-DRgvVevp.js} +145 -47
  28. package/dist/keycache-C7k8s1Bk.js +102 -0
  29. package/dist/{kv-cache-Vtxhbo1W.cjs → kv-cache-DPtsJ1sL.cjs} +1 -1
  30. package/dist/{kv-cache-CQPL_aGY.js → kv-cache-MPcS_mGG.js} +1 -1
  31. package/dist/{ld-CrX7pQda.js → ld-s9_8WfBc.js} +2 -2
  32. package/dist/{middleware-CfI9C9Xy.js → middleware-2XtoTBq0.js} +12 -12
  33. package/dist/{middleware-MlO5iUeZ.js → middleware-Ajnk9qHB.js} +158 -22
  34. package/dist/middleware-BgCIhb_C.cjs +12 -0
  35. package/dist/{middleware-D4S6i4A_.cjs → middleware-BoCzk7-G.cjs} +158 -22
  36. package/dist/{middleware-C8PKuPrm.js → middleware-DGUNDGCl.js} +4 -4
  37. package/dist/{middleware-BelSJK7m.js → middleware-Dn9UDJZP.js} +100 -24
  38. package/dist/{mod-CwZXZJ9d.d.ts → mod-BugwI0JN.d.ts} +1 -1
  39. package/dist/{mod-DPkRU3EK.d.cts → mod-CFBU2OT3.d.cts} +1 -1
  40. package/dist/{mod-DUWcVv49.d.ts → mod-CvxylbuV.d.ts} +1 -1
  41. package/dist/{mod-DVwHUI_x.d.cts → mod-DE8MYisy.d.cts} +1 -1
  42. package/dist/{mod-DXsQakeS.d.cts → mod-DKG0ovjR.d.cts} +1 -1
  43. package/dist/{mod-DnSsduJF.d.ts → mod-DcfFNgYf.d.ts} +1 -1
  44. package/dist/{mod-Di3W5OdP.d.cts → mod-Dp0kK0hO.d.cts} +1 -1
  45. package/dist/{mod-DosD6NsG.d.ts → mod-Z7lIaCfo.d.ts} +1 -1
  46. package/dist/mod.cjs +8 -4
  47. package/dist/mod.d.cts +8 -8
  48. package/dist/mod.d.ts +8 -8
  49. package/dist/mod.js +7 -5
  50. package/dist/nodeinfo/handler.test.js +12 -12
  51. package/dist/otel/exporter.test.js +43 -2
  52. package/dist/otel/mod.cjs +7 -1
  53. package/dist/otel/mod.d.cts +12 -0
  54. package/dist/otel/mod.d.ts +12 -0
  55. package/dist/otel/mod.js +7 -1
  56. package/dist/{owner-BAlnLKMO.js → owner-Cx8gV-j4.js} +1 -1
  57. package/dist/{proof-DMgHaXNJ.js → proof-CDr3NP3R.js} +2 -2
  58. package/dist/{proof-BgUVmaJz.js → proof-Le4DAkqb.js} +1 -1
  59. package/dist/{proof-CR5RUAmy.cjs → proof-qHcNgE5i.cjs} +1 -1
  60. package/dist/{send-B2aZYf9A.js → send-DreBSY1U.js} +2 -2
  61. package/dist/sig/http.test.js +85 -5
  62. package/dist/sig/key.test.js +70 -3
  63. package/dist/sig/ld.test.js +3 -3
  64. package/dist/sig/mod.cjs +4 -2
  65. package/dist/sig/mod.d.cts +3 -3
  66. package/dist/sig/mod.d.ts +3 -3
  67. package/dist/sig/mod.js +3 -3
  68. package/dist/sig/owner.test.js +3 -3
  69. package/dist/sig/proof.test.js +3 -3
  70. package/dist/testing/mod.d.ts +92 -0
  71. package/dist/utils/docloader.test.js +4 -4
  72. package/dist/utils/mod.cjs +2 -2
  73. package/dist/utils/mod.d.cts +2 -2
  74. package/dist/utils/mod.d.ts +2 -2
  75. package/dist/utils/mod.js +2 -2
  76. package/package.json +5 -5
  77. package/dist/keycache-DRxpZ5r9.js +0 -48
  78. package/dist/middleware-D4XcpSBG.cjs +0 -12
@@ -3,8 +3,8 @@
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
  globalThis.addEventListener = () => {};
5
5
 
6
- import { deno_default } from "./deno-BYerLnry.js";
7
- import { fetchKey, validateCryptoKey } from "./key-DCdTVZiK.js";
6
+ import { deno_default } from "./deno-CQdJQjC5.js";
7
+ import { fetchKeyDetailed, validateCryptoKey } from "./key-DRgvVevp.js";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey } from "@fedify/vocab";
10
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -240,6 +240,55 @@ const supportedHashAlgorithms = {
240
240
  "sha-256": "SHA-256",
241
241
  "sha-512": "SHA-512"
242
242
  };
243
+ function noSignatureResult() {
244
+ return {
245
+ verified: false,
246
+ reason: { type: "noSignature" }
247
+ };
248
+ }
249
+ function invalidSignatureResult(keyId) {
250
+ return keyId == null ? {
251
+ verified: false,
252
+ reason: { type: "invalidSignature" }
253
+ } : {
254
+ verified: false,
255
+ reason: {
256
+ type: "invalidSignature",
257
+ keyId
258
+ }
259
+ };
260
+ }
261
+ function keyFetchErrorResult(keyId, result) {
262
+ return {
263
+ verified: false,
264
+ reason: {
265
+ type: "keyFetchError",
266
+ keyId,
267
+ result
268
+ }
269
+ };
270
+ }
271
+ function parseKeyId(value) {
272
+ if (value == null) return null;
273
+ try {
274
+ return new URL(value);
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ function getKeyFetchErrorName(error) {
280
+ return error.name || error.constructor.name || "Error";
281
+ }
282
+ function recordVerificationResult(span, result) {
283
+ span.setAttribute("http_signatures.verified", result.verified);
284
+ if (result.verified === true) return;
285
+ const reason = result.reason;
286
+ span.setAttribute("http_signatures.failure_reason", reason.type);
287
+ if ("keyId" in reason && reason.keyId != null) span.setAttribute("http_signatures.key_id", reason.keyId.href);
288
+ if (reason.type !== "keyFetchError") return;
289
+ if ("status" in reason.result) span.setAttribute("http_signatures.key_fetch_status", reason.result.status);
290
+ else span.setAttribute("http_signatures.key_fetch_error", getKeyFetchErrorName(reason.result.error));
291
+ }
243
292
  /**
244
293
  * Verifies the signature of a request.
245
294
  *
@@ -254,6 +303,19 @@ const supportedHashAlgorithms = {
254
303
  * could not be verified.
255
304
  */
256
305
  async function verifyRequest(request, options = {}) {
306
+ const result = await verifyRequestDetailed(request, options);
307
+ return result.verified ? result.key : null;
308
+ }
309
+ /**
310
+ * Verifies the signature of a request and returns a structured failure reason
311
+ * when verification does not succeed.
312
+ *
313
+ * @param request The request to verify.
314
+ * @param options Options for verifying the request.
315
+ * @returns The verified public key, or a structured verification failure.
316
+ * @since 2.1.0
317
+ */
318
+ async function verifyRequestDetailed(request, options = {}) {
257
319
  const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
258
320
  const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
259
321
  return await tracer.startActiveSpan("http_signatures.verify", async (span) => {
@@ -265,11 +327,12 @@ async function verifyRequest(request, options = {}) {
265
327
  try {
266
328
  let spec = options.spec;
267
329
  if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
268
- let key;
269
- if (spec === "rfc9421") key = await verifyRequestRfc9421(request, span, options);
270
- else key = await verifyRequestDraft(request, span, options);
271
- if (key == null) span.setStatus({ code: SpanStatusCode.ERROR });
272
- return key;
330
+ let result;
331
+ if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, options);
332
+ else result = await verifyRequestDraft(request, span, options);
333
+ recordVerificationResult(span, result);
334
+ if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
335
+ return result;
273
336
  } catch (error) {
274
337
  span.setStatus({
275
338
  code: SpanStatusCode.ERROR,
@@ -289,27 +352,29 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
289
352
  ]);
290
353
  if (request.bodyUsed) {
291
354
  logger.error("Failed to verify; the request body is already consumed.", { url: request.url });
292
- return null;
355
+ return noSignatureResult();
293
356
  } else if (request.body?.locked) {
294
357
  logger.error("Failed to verify; the request body is locked.", { url: request.url });
295
- return null;
358
+ return noSignatureResult();
296
359
  }
297
360
  const originalRequest = request;
298
361
  request = request.clone();
299
- const dateHeader = request.headers.get("Date");
300
- if (dateHeader == null) {
301
- logger.debug("Failed to verify; no Date header found.", { headers: Object.fromEntries(request.headers.entries()) });
302
- return null;
303
- }
304
362
  const sigHeader = request.headers.get("Signature");
305
363
  if (sigHeader == null) {
306
364
  logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
307
- return null;
365
+ return noSignatureResult();
366
+ }
367
+ const sigValues = Object.fromEntries(sigHeader.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)=(?:"([^"]*)"|(\d+))\s*$/)).filter((m) => m != null).map((m) => [m[1], m[2] ?? m[3]]));
368
+ const parsedKeyId = parseKeyId(sigValues.keyId);
369
+ const dateHeader = request.headers.get("Date");
370
+ if (dateHeader == null) {
371
+ logger.debug("Failed to verify; no Date header found.", { headers: Object.fromEntries(request.headers.entries()) });
372
+ return invalidSignatureResult(parsedKeyId);
308
373
  }
309
374
  const digestHeader = request.headers.get("Digest");
310
375
  if (request.method !== "GET" && request.method !== "HEAD" && digestHeader == null) {
311
376
  logger.debug("Failed to verify; no Digest header found.", { headers: Object.fromEntries(request.headers.entries()) });
312
- return null;
377
+ return invalidSignatureResult(parsedKeyId);
313
378
  }
314
379
  let body = null;
315
380
  if (digestHeader != null) {
@@ -327,7 +392,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
327
392
  digest: digestBase64,
328
393
  error
329
394
  });
330
- return null;
395
+ return invalidSignatureResult(parsedKeyId);
331
396
  }
332
397
  if (span.isRecording()) span.setAttribute(`http_signatures.digest.${algo}`, encodeHex(digest));
333
398
  const expectedDigest = await crypto.subtle.digest(supportedHashAlgorithms[algo], body);
@@ -337,7 +402,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
337
402
  digest: digestBase64,
338
403
  expectedDigest: encodeBase64(expectedDigest)
339
404
  });
340
- return null;
405
+ return invalidSignatureResult(parsedKeyId);
341
406
  }
342
407
  matched = true;
343
408
  }
@@ -346,7 +411,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
346
411
  supportedAlgorithms: Object.keys(supportedHashAlgorithms),
347
412
  algorithms: digests.map(([algo]) => algo)
348
413
  });
349
- return null;
414
+ return invalidSignatureResult(parsedKeyId);
350
415
  }
351
416
  }
352
417
  const date = Temporal.Instant.from(new Date(dateHeader).toISOString());
@@ -358,25 +423,24 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
358
423
  date: date.toString(),
359
424
  now: now.toString()
360
425
  });
361
- return null;
426
+ return invalidSignatureResult(parsedKeyId);
362
427
  } else if (Temporal.Instant.compare(date, now.subtract(tw)) < 0) {
363
428
  logger.debug("Failed to verify; Date is too far in the past.", {
364
429
  date: date.toString(),
365
430
  now: now.toString()
366
431
  });
367
- return null;
432
+ return invalidSignatureResult(parsedKeyId);
368
433
  }
369
434
  }
370
- const sigValues = Object.fromEntries(sigHeader.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)=(?:"([^"]*)"|(\d+))\s*$/)).filter((m) => m != null).map((m) => [m[1], m[2] ?? m[3]]));
371
435
  if (!("keyId" in sigValues)) {
372
436
  logger.debug("Failed to verify; no keyId field found in the Signature header.", { signature: sigHeader });
373
- return null;
437
+ return invalidSignatureResult(null);
374
438
  } else if (!("headers" in sigValues)) {
375
439
  logger.debug("Failed to verify; no headers field found in the Signature header.", { signature: sigHeader });
376
- return null;
440
+ return invalidSignatureResult(parsedKeyId);
377
441
  } else if (!("signature" in sigValues)) {
378
442
  logger.debug("Failed to verify; no signature field found in the Signature header.", { signature: sigHeader });
379
- return null;
443
+ return invalidSignatureResult(parsedKeyId);
380
444
  }
381
445
  if ("expires" in sigValues) {
382
446
  const expiresSeconds = parseInt(sigValues.expires);
@@ -385,7 +449,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
385
449
  expires: sigValues.expires,
386
450
  signature: sigHeader
387
451
  });
388
- return null;
452
+ return invalidSignatureResult(parsedKeyId);
389
453
  }
390
454
  const expires = Temporal.Instant.fromEpochMilliseconds(expiresSeconds * 1e3);
391
455
  if (Temporal.Instant.compare(now, expires) > 0) {
@@ -394,7 +458,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
394
458
  now: now.toString(),
395
459
  signature: sigHeader
396
460
  });
397
- return null;
461
+ return invalidSignatureResult(parsedKeyId);
398
462
  }
399
463
  }
400
464
  if ("created" in sigValues) {
@@ -404,7 +468,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
404
468
  created: sigValues.created,
405
469
  signature: sigHeader
406
470
  });
407
- return null;
471
+ return invalidSignatureResult(parsedKeyId);
408
472
  }
409
473
  if (timeWindow !== false) {
410
474
  const created = Temporal.Instant.fromEpochMilliseconds(createdSeconds * 1e3);
@@ -414,34 +478,37 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
414
478
  created: created.toString(),
415
479
  now: now.toString()
416
480
  });
417
- return null;
481
+ return invalidSignatureResult(parsedKeyId);
418
482
  } else if (Temporal.Instant.compare(created, now.subtract(tw)) < 0) {
419
483
  logger.debug("Failed to verify; created is too far in the past.", {
420
484
  created: created.toString(),
421
485
  now: now.toString()
422
486
  });
423
- return null;
487
+ return invalidSignatureResult(parsedKeyId);
424
488
  }
425
489
  }
426
490
  }
427
491
  const { keyId, headers, signature } = sigValues;
492
+ const keyIdUrl = parseKeyId(keyId);
493
+ if (keyIdUrl == null) return invalidSignatureResult(null);
428
494
  span?.setAttribute("http_signatures.key_id", keyId);
429
495
  if ("algorithm" in sigValues) span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
430
- const { key, cached } = await fetchKey(new URL(keyId), CryptographicKey, {
496
+ const { key, cached, fetchError } = await fetchKeyDetailed(keyIdUrl, CryptographicKey, {
431
497
  documentLoader,
432
498
  contextLoader,
433
499
  keyCache,
434
500
  tracerProvider
435
501
  });
436
- if (key == null) return null;
502
+ if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
503
+ if (key == null) return invalidSignatureResult(keyIdUrl);
437
504
  const headerNames = headers.split(/\s+/g);
438
505
  if (!headerNames.includes("(request-target)") || !headerNames.includes("date")) {
439
506
  logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers });
440
- return null;
507
+ return invalidSignatureResult(keyIdUrl);
441
508
  }
442
509
  if (body != null && !headerNames.includes("digest")) {
443
510
  logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers });
444
- return null;
511
+ return invalidSignatureResult(keyIdUrl);
445
512
  }
446
513
  const message = headerNames.map((name) => `${name}: ` + (name === "(request-target)" ? `${request.method.toLowerCase()} ${new URL(request.url).pathname}` : name === "(created)" ? sigValues.created ?? "" : name === "(expires)" ? sigValues.expires ?? "" : name === "host" ? request.headers.get("host") ?? new URL(request.url).host : request.headers.get(name))).join("\n");
447
514
  const sig = decodeBase64(signature);
@@ -454,7 +521,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
454
521
  signature,
455
522
  message
456
523
  });
457
- return await verifyRequest(originalRequest, {
524
+ return await verifyRequestDetailed(originalRequest, {
458
525
  documentLoader,
459
526
  contextLoader,
460
527
  timeWindow,
@@ -470,9 +537,12 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
470
537
  signature,
471
538
  message
472
539
  });
473
- return null;
540
+ return invalidSignatureResult(keyIdUrl);
474
541
  }
475
- return key;
542
+ return {
543
+ verified: true,
544
+ key
545
+ };
476
546
  }
477
547
  /**
478
548
  * RFC 9421 map of algorithm identifiers to WebCrypto algorithms
@@ -543,22 +613,22 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
543
613
  ]);
544
614
  if (request.bodyUsed) {
545
615
  logger.error("Failed to verify; the request body is already consumed.", { url: request.url });
546
- return null;
616
+ return noSignatureResult();
547
617
  } else if (request.body?.locked) {
548
618
  logger.error("Failed to verify; the request body is locked.", { url: request.url });
549
- return null;
619
+ return noSignatureResult();
550
620
  }
551
621
  const originalRequest = request;
552
622
  request = request.clone();
553
623
  const signatureInputHeader = request.headers.get("Signature-Input");
554
624
  if (!signatureInputHeader) {
555
625
  logger.debug("Failed to verify; no Signature-Input header found.", { headers: Object.fromEntries(request.headers.entries()) });
556
- return null;
626
+ return noSignatureResult();
557
627
  }
558
628
  const signatureHeader = request.headers.get("Signature");
559
629
  if (!signatureHeader) {
560
630
  logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
561
- return null;
631
+ return noSignatureResult();
562
632
  }
563
633
  const signatureInputs = parseRfc9421SignatureInput(signatureInputHeader);
564
634
  logger.debug("Parsed Signature-Input header: {signatureInputs}", { signatureInputs });
@@ -566,18 +636,23 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
566
636
  const signatureNames = Object.keys(signatureInputs);
567
637
  if (signatureNames.length === 0) {
568
638
  logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
569
- return null;
639
+ return invalidSignatureResult(null);
570
640
  }
571
- let validKey = null;
641
+ let failure = noSignatureResult();
572
642
  for (const sigName of signatureNames) {
573
- if (!signatures[sigName]) continue;
643
+ if (!signatures[sigName]) {
644
+ failure = invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId));
645
+ continue;
646
+ }
574
647
  const sigInput = signatureInputs[sigName];
575
648
  const sigBytes = signatures[sigName];
649
+ const keyId = parseKeyId(sigInput.keyId);
576
650
  if (!sigInput.keyId) {
577
651
  logger.debug("Failed to verify; missing keyId in signature {signatureName}.", {
578
652
  signatureName: sigName,
579
653
  signatureInput: signatureInputHeader
580
654
  });
655
+ failure = invalidSignatureResult(null);
581
656
  continue;
582
657
  }
583
658
  if (!sigInput.created) {
@@ -585,6 +660,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
585
660
  signatureName: sigName,
586
661
  signatureInput: signatureInputHeader
587
662
  });
663
+ failure = invalidSignatureResult(keyId);
588
664
  continue;
589
665
  }
590
666
  const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3);
@@ -596,12 +672,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
596
672
  created: signatureCreated.toString(),
597
673
  now: now.toString()
598
674
  });
675
+ failure = invalidSignatureResult(keyId);
599
676
  continue;
600
677
  } else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) {
601
678
  logger.debug("Failed to verify; signature created time is too far in the past.", {
602
679
  created: signatureCreated.toString(),
603
680
  now: now.toString()
604
681
  });
682
+ failure = invalidSignatureResult(keyId);
605
683
  continue;
606
684
  }
607
685
  }
@@ -609,25 +687,36 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
609
687
  const contentDigestHeader = request.headers.get("Content-Digest");
610
688
  if (!contentDigestHeader) {
611
689
  logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
690
+ failure = invalidSignatureResult(keyId);
612
691
  continue;
613
692
  }
614
693
  const body = await request.arrayBuffer();
615
694
  const digestValid = await verifyRfc9421ContentDigest(contentDigestHeader, body);
616
695
  if (!digestValid) {
617
696
  logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
697
+ failure = invalidSignatureResult(keyId);
618
698
  continue;
619
699
  }
620
700
  }
621
701
  span?.setAttribute("http_signatures.key_id", sigInput.keyId);
622
702
  span?.setAttribute("http_signatures.created", sigInput.created.toString());
623
- const { key, cached } = await fetchKey(new URL(sigInput.keyId), CryptographicKey, {
703
+ if (keyId == null) {
704
+ failure = invalidSignatureResult(null);
705
+ continue;
706
+ }
707
+ const { key, cached, fetchError } = await fetchKeyDetailed(keyId, CryptographicKey, {
624
708
  documentLoader,
625
709
  contextLoader,
626
710
  keyCache,
627
711
  tracerProvider
628
712
  });
713
+ if (fetchError != null) {
714
+ failure = keyFetchErrorResult(keyId, fetchError);
715
+ continue;
716
+ }
629
717
  if (!key) {
630
718
  logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
719
+ failure = invalidSignatureResult(keyId);
631
720
  continue;
632
721
  }
633
722
  let alg = sigInput.alg?.toLowerCase();
@@ -644,6 +733,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
644
733
  algorithm: sigInput.alg,
645
734
  supported: Object.keys(rfc9421AlgorithmMap)
646
735
  });
736
+ failure = invalidSignatureResult(keyId);
647
737
  continue;
648
738
  }
649
739
  let signatureBase;
@@ -654,41 +744,47 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
654
744
  error,
655
745
  signatureInput: sigInput
656
746
  });
747
+ failure = invalidSignatureResult(keyId);
657
748
  continue;
658
749
  }
659
750
  const signatureBaseBytes = new TextEncoder().encode(signatureBase);
660
751
  span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
661
752
  try {
662
753
  const verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
663
- if (verified) {
664
- validKey = key;
665
- break;
666
- } else if (cached) {
754
+ if (verified) return {
755
+ verified: true,
756
+ key
757
+ };
758
+ else if (cached) {
667
759
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
668
- return await verifyRequest(originalRequest, {
760
+ return await verifyRequestDetailed(originalRequest, {
669
761
  documentLoader,
670
762
  contextLoader,
671
763
  timeWindow,
672
764
  currentTime,
673
765
  keyCache: {
674
766
  get: () => Promise.resolve(void 0),
675
- set: async (keyId, key$1) => await keyCache?.set(keyId, key$1)
767
+ set: async (keyId$1, key$1) => await keyCache?.set(keyId$1, key$1)
676
768
  },
677
769
  spec: "rfc9421"
678
770
  });
679
- } else logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
680
- keyId: sigInput.keyId,
681
- signatureBase
682
- });
771
+ } else {
772
+ logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
773
+ keyId: sigInput.keyId,
774
+ signatureBase
775
+ });
776
+ failure = invalidSignatureResult(keyId);
777
+ }
683
778
  } catch (error) {
684
779
  logger.debug("Error during signature verification: {error}", {
685
780
  error,
686
781
  keyId: sigInput.keyId,
687
782
  algorithm: sigInput.alg
688
783
  });
784
+ failure = invalidSignatureResult(keyId);
689
785
  }
690
786
  }
691
- return validKey;
787
+ return failure;
692
788
  }
693
789
  /**
694
790
  * Helper function to create a new Request for redirect handling.
@@ -806,4 +902,4 @@ function timingSafeEqual(a, b) {
806
902
  }
807
903
 
808
904
  //#endregion
809
- export { createRfc9421SignatureBase, doubleKnock, formatRfc9421Signature, formatRfc9421SignatureParameters, parseRfc9421Signature, parseRfc9421SignatureInput, signRequest, timingSafeEqual, verifyRequest };
905
+ export { createRfc9421SignatureBase, doubleKnock, formatRfc9421Signature, formatRfc9421SignatureParameters, parseRfc9421Signature, parseRfc9421SignatureInput, signRequest, timingSafeEqual, verifyRequest, verifyRequestDetailed };
@@ -70,6 +70,34 @@ interface FetchKeyResult<T extends CryptographicKey | Multikey> {
70
70
  readonly cached: boolean;
71
71
  }
72
72
  /**
73
+ * Detailed fetch failure information from {@link fetchKeyDetailed}.
74
+ * @since 2.1.0
75
+ */
76
+ type FetchKeyErrorResult = {
77
+ readonly status: number;
78
+ readonly response: Response;
79
+ } | {
80
+ readonly error: Error;
81
+ };
82
+ /**
83
+ * The result of {@link fetchKeyDetailed}.
84
+ * @since 2.1.0
85
+ */
86
+ interface FetchKeyDetailedResult<T extends CryptographicKey | Multikey> extends FetchKeyResult<T> {
87
+ /**
88
+ * The error that occurred while fetching the key, if fetching failed before
89
+ * a document could be parsed.
90
+ */
91
+ readonly fetchError?: FetchKeyErrorResult;
92
+ }
93
+ type FetchableKeyClass<T extends CryptographicKey | Multikey> = (new (...args: any[]) => T) & {
94
+ fromJsonLd(jsonLd: unknown, options: {
95
+ documentLoader?: DocumentLoader;
96
+ contextLoader?: DocumentLoader;
97
+ tracerProvider?: TracerProvider;
98
+ }): Promise<T>;
99
+ };
100
+ /**
73
101
  * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL.
74
102
  * If the given URL contains an {@link Actor} object, it tries to find
75
103
  * the corresponding key in the `publicKey` or `assertionMethod` property.
@@ -82,13 +110,22 @@ interface FetchKeyResult<T extends CryptographicKey | Multikey> {
82
110
  * @returns The fetched key or `null` if the key is not found.
83
111
  * @since 1.3.0
84
112
  */
85
- declare function fetchKey<T extends CryptographicKey | Multikey>(keyId: URL | string, cls: (new (...args: any[]) => T) & {
86
- fromJsonLd(jsonLd: unknown, options: {
87
- documentLoader?: DocumentLoader;
88
- contextLoader?: DocumentLoader;
89
- tracerProvider?: TracerProvider;
90
- }): Promise<T>;
91
- }, options?: FetchKeyOptions): Promise<FetchKeyResult<T>>;
113
+ declare function fetchKey<T extends CryptographicKey | Multikey>(keyId: URL | string, cls: FetchableKeyClass<T>, options?: FetchKeyOptions): Promise<FetchKeyResult<T>>;
114
+ /**
115
+ * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL,
116
+ * preserving transport-level fetch failures for callers that need to inspect
117
+ * why the key could not be loaded.
118
+ *
119
+ * @template T The type of the key to fetch. Either {@link CryptographicKey}
120
+ * or {@link Multikey}.
121
+ * @param keyId The URL of the key.
122
+ * @param cls The class of the key to fetch. Either {@link CryptographicKey}
123
+ * or {@link Multikey}.
124
+ * @param options Options for fetching the key.
125
+ * @returns The fetched key, or detailed fetch failure information.
126
+ * @since 2.1.0
127
+ */
128
+ declare function fetchKeyDetailed<T extends CryptographicKey | Multikey>(keyId: URL | string, cls: FetchableKeyClass<T>, options?: FetchKeyOptions): Promise<FetchKeyDetailedResult<T>>;
92
129
  /**
93
130
  * A cache for storing cryptographic keys.
94
131
  * @since 0.12.0
@@ -202,6 +239,31 @@ interface VerifyRequestOptions {
202
239
  tracerProvider?: TracerProvider;
203
240
  }
204
241
  /**
242
+ * The reason why {@link verifyRequestDetailed} could not verify a request.
243
+ * @since 2.1.0
244
+ */
245
+ type VerifyRequestFailureReason = {
246
+ readonly type: "keyFetchError";
247
+ readonly keyId: URL;
248
+ readonly result: FetchKeyErrorResult;
249
+ } | {
250
+ readonly type: "invalidSignature";
251
+ readonly keyId?: URL;
252
+ } | {
253
+ readonly type: "noSignature";
254
+ };
255
+ /**
256
+ * The detailed result of {@link verifyRequestDetailed}.
257
+ * @since 2.1.0
258
+ */
259
+ type VerifyRequestDetailedResult = {
260
+ readonly verified: true;
261
+ readonly key: CryptographicKey;
262
+ } | {
263
+ readonly verified: false;
264
+ readonly reason: VerifyRequestFailureReason;
265
+ };
266
+ /**
205
267
  * Verifies the signature of a request.
206
268
  *
207
269
  * Note that this function consumes the request body, so it should not be used
@@ -216,6 +278,16 @@ interface VerifyRequestOptions {
216
278
  */
217
279
  declare function verifyRequest(request: Request, options?: VerifyRequestOptions): Promise<CryptographicKey | null>;
218
280
  /**
281
+ * Verifies the signature of a request and returns a structured failure reason
282
+ * when verification does not succeed.
283
+ *
284
+ * @param request The request to verify.
285
+ * @param options Options for verifying the request.
286
+ * @returns The verified public key, or a structured verification failure.
287
+ * @since 2.1.0
288
+ */
289
+ declare function verifyRequestDetailed(request: Request, options?: VerifyRequestOptions): Promise<VerifyRequestDetailedResult>;
290
+ /**
219
291
  * A spec determiner for HTTP Message Signatures.
220
292
  * It determines the spec to use for signing requests.
221
293
  * It is used for double-knocking
@@ -241,4 +313,4 @@ interface HttpMessageSignaturesSpecDeterminer {
241
313
  * @since 1.6.0
242
314
  */
243
315
  //#endregion
244
- export { FetchKeyOptions, FetchKeyResult, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, SignRequestOptions, VerifyRequestOptions, exportJwk, fetchKey, generateCryptoKeyPair, importJwk, signRequest, verifyRequest };
316
+ export { FetchKeyDetailedResult, FetchKeyErrorResult, FetchKeyOptions, FetchKeyResult, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, SignRequestOptions, VerifyRequestDetailedResult, VerifyRequestFailureReason, VerifyRequestOptions, exportJwk, fetchKey, fetchKeyDetailed, generateCryptoKeyPair, importJwk, signRequest, verifyRequest, verifyRequestDetailed };
@@ -3,7 +3,7 @@
3
3
  import { URLPattern } from "urlpattern-polyfill";
4
4
  globalThis.addEventListener = () => {};
5
5
 
6
- import { deno_default } from "./deno-BYerLnry.js";
6
+ import { deno_default } from "./deno-CQdJQjC5.js";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Activity, getTypeId } from "@fedify/vocab";
9
9
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";