@fedify/fedify 2.3.0-dev.1110 → 2.3.0-dev.1119

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 (97) hide show
  1. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DQP-q39h.mjs} +27 -2
  2. package/dist/{builder-B-Y6fwSu.mjs → builder-Ond_h57y.mjs} +3 -3
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/outgoing-jsonld.test.mjs +1 -1
  6. package/dist/compat/public-audience.test.mjs +1 -1
  7. package/dist/compat/transformers.test.mjs +2 -2
  8. package/dist/{context-C0C_sRha.d.cts → context-Ch-ZLyTQ.d.cts} +1 -1
  9. package/dist/{context-Dqgt8saU.d.ts → context-cSUMk2da.d.ts} +1 -1
  10. package/dist/{deno-hqC7tKJn.mjs → deno-DVsHS7rA.mjs} +1 -1
  11. package/dist/{docloader-BOEuuXkX.mjs → docloader-WsWfKaE5.mjs} +2 -2
  12. package/dist/federation/builder.test.mjs +3 -3
  13. package/dist/federation/collection.test.mjs +2 -2
  14. package/dist/federation/handler.test.mjs +8 -7
  15. package/dist/federation/idempotency.test.mjs +5 -5
  16. package/dist/federation/inbox.test.mjs +1 -1
  17. package/dist/federation/keycache.test.mjs +1 -1
  18. package/dist/federation/kv.test.mjs +2 -2
  19. package/dist/federation/metrics.test.d.mts +2 -0
  20. package/dist/federation/metrics.test.mjs +107 -0
  21. package/dist/federation/middleware.test.mjs +390 -10
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +2 -2
  24. package/dist/federation/mod.d.ts +2 -2
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +2 -2
  27. package/dist/federation/negotiation.test.mjs +2 -2
  28. package/dist/federation/router.test.mjs +2 -2
  29. package/dist/federation/send.test.mjs +11 -11
  30. package/dist/federation/webfinger.test.mjs +3 -3
  31. package/dist/{getMachineId-bsd-etIyxDet.mjs → getMachineId-bsd-BY01PL1n.mjs} +1 -1
  32. package/dist/{getMachineId-darwin-D23zTf4g.mjs → getMachineId-darwin-Dr1gkBkp.mjs} +1 -1
  33. package/dist/{getMachineId-win-Dpap6v5i.mjs → getMachineId-win-QEYwcJiy.mjs} +1 -1
  34. package/dist/{http-O8MYWwk8.js → http-CouJSFVK.js} +461 -37
  35. package/dist/{http-DV0il3vk.cjs → http-CubOB9wq.cjs} +513 -35
  36. package/dist/{http-BDZeS5om.d.ts → http-D6LP89UO.d.ts} +7 -1
  37. package/dist/{http-C87EWkO0.d.cts → http-D6aw3j2U.d.cts} +7 -1
  38. package/dist/{http-BLopFpvC.mjs → http-DUV8ysti.mjs} +86 -37
  39. package/dist/{key-DW1EVmtP.mjs → key-BoWaYRHm.mjs} +1 -1
  40. package/dist/{kv-cache-C3NWWiTg.js → kv-cache-DBNpsneh.js} +1 -1
  41. package/dist/{kv-cache-Dya-TWMe.cjs → kv-cache-Dz31ATUT.cjs} +1 -1
  42. package/dist/{ld-BNkk2Yal.mjs → ld-B5K1mSuG.mjs} +60 -9
  43. package/dist/{send-hokVCPu6.mjs → metrics-C4attqv0.mjs} +124 -224
  44. package/dist/{middleware-D6FbOjuK.mjs → middleware-BDKFRjue.mjs} +1 -1
  45. package/dist/{middleware-DUWeXjZR.cjs → middleware-CmsDtIHI.cjs} +75 -309
  46. package/dist/{middleware-CjzI3aYo.js → middleware-Dtjz-hSk.js} +46 -280
  47. package/dist/{middleware-DA2WTBr4.mjs → middleware-t0jC8I99.mjs} +59 -34
  48. package/dist/{mod-DXY9JF28.d.cts → mod-B-Lin9Sy.d.ts} +25 -2
  49. package/dist/{mod-DHO9lk3D.d.ts → mod-BDhgfjP7.d.cts} +25 -2
  50. package/dist/{mod-B0rWmfW5.d.cts → mod-BR_BB0bh.d.cts} +1 -1
  51. package/dist/{mod-Dx3-hqyo.d.ts → mod-C6E8rkcz.d.ts} +1 -1
  52. package/dist/{mod-BhU_H1I_.d.ts → mod-DLrRb0dx.d.ts} +1 -1
  53. package/dist/{mod-CLPnQPsv.d.cts → mod-P9tE2WmM.d.cts} +1 -1
  54. package/dist/mod.cjs +4 -4
  55. package/dist/mod.d.cts +5 -5
  56. package/dist/mod.d.ts +5 -5
  57. package/dist/mod.js +4 -4
  58. package/dist/nodeinfo/client.test.mjs +2 -2
  59. package/dist/nodeinfo/handler.test.mjs +3 -3
  60. package/dist/nodeinfo/types.test.mjs +2 -2
  61. package/dist/otel/exporter.test.mjs +2 -2
  62. package/dist/{outgoing-jsonld-BgFLCJQ_.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
  63. package/dist/{owner-jvJAtR5O.mjs → owner-hDxI0ufu.mjs} +2 -2
  64. package/dist/{proof-BD92WeqV.cjs → proof-BUWfVr6Q.cjs} +78 -11
  65. package/dist/{proof-mfmHH9j0.mjs → proof-DhVuz4bc.mjs} +25 -7
  66. package/dist/{proof-5kT7OUPV.js → proof-n60t8o9P.js} +78 -11
  67. package/dist/send-BPhyR5Oo.mjs +225 -0
  68. package/dist/sig/accept.test.mjs +1 -1
  69. package/dist/sig/http.test.mjs +212 -6
  70. package/dist/sig/key.test.mjs +4 -4
  71. package/dist/sig/ld.test.mjs +138 -5
  72. package/dist/sig/mod.cjs +2 -2
  73. package/dist/sig/mod.d.cts +2 -2
  74. package/dist/sig/mod.d.ts +2 -2
  75. package/dist/sig/mod.js +2 -2
  76. package/dist/sig/owner.test.mjs +4 -4
  77. package/dist/sig/proof.test.mjs +167 -6
  78. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BTEgfoJo.mjs} +2 -27
  79. package/dist/utils/docloader.test.mjs +5 -5
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.d.cts +1 -1
  83. package/dist/utils/mod.d.ts +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +5 -5
  86. /package/dist/{accept-CceiKpCy.mjs → accept-CgDcxvjV.mjs} +0 -0
  87. /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
  88. /package/dist/{client-B_A6mfn3.mjs → client-Bneh_DYR.mjs} +0 -0
  89. /package/dist/{collection-CA3V5zyK.mjs → collection-Cc3DVAhE.mjs} +0 -0
  90. /package/dist/{execAsync-DCBrgFiV.mjs → execAsync-Dxb7rNf3.mjs} +0 -0
  91. /package/dist/{getMachineId-linux-ObI47Hql.mjs → getMachineId-linux-Bbhofx-s.mjs} +0 -0
  92. /package/dist/{getMachineId-unsupported-Ddu-PFeh.mjs → getMachineId-unsupported-dIOte2Ct.mjs} +0 -0
  93. /package/dist/{keys-C3kae-6B.mjs → keys-CSYsOMFG.mjs} +0 -0
  94. /package/dist/{kv-x2IvBUyq.mjs → kv-QHE0oeM3.mjs} +0 -0
  95. /package/dist/{kv-cache-CiiNwT6W.mjs → kv-cache-DihufyAQ.mjs} +0 -0
  96. /package/dist/{public-audience-N3pyOx2p.mjs → public-audience-c9zmYKgA.mjs} +0 -0
  97. /package/dist/{types-BFowWFTT.mjs → types-D09GN0uZ.mjs} +0 -0
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { TracerProvider } from "@opentelemetry/api";
3
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
4
  import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
 
6
6
  //#region src/sig/key.d.ts
@@ -464,6 +464,12 @@ interface VerifyRequestOptions {
464
464
  * @since 1.3.0
465
465
  */
466
466
  tracerProvider?: TracerProvider;
467
+ /**
468
+ * The OpenTelemetry meter provider. If omitted, the global meter provider
469
+ * is used.
470
+ * @since 2.3.0
471
+ */
472
+ meterProvider?: MeterProvider;
467
473
  }
468
474
  /**
469
475
  * The reason why {@link verifyRequestDetailed} could not verify a request.
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { CryptographicKey, Multikey } from "@fedify/vocab";
3
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
- import { TracerProvider } from "@opentelemetry/api";
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -464,6 +464,12 @@ interface VerifyRequestOptions {
464
464
  * @since 1.3.0
465
465
  */
466
466
  tracerProvider?: TracerProvider;
467
+ /**
468
+ * The OpenTelemetry meter provider. If omitted, the global meter provider
469
+ * is used.
470
+ * @since 2.3.0
471
+ */
472
+ meterProvider?: MeterProvider;
467
473
  }
468
474
  /**
469
475
  * The reason why {@link verifyRequestDetailed} could not verify a request.
@@ -1,9 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-hqC7tKJn.mjs";
5
- import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CceiKpCy.mjs";
6
- import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-DW1EVmtP.mjs";
4
+ import { n as version, t as name } from "./deno-DVsHS7rA.mjs";
5
+ import { a as measureSignatureKeyFetch, n as getFederationMetrics, t as getDurationMs } from "./metrics-C4attqv0.mjs";
6
+ import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CgDcxvjV.mjs";
7
+ import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-BoWaYRHm.mjs";
7
8
  import { getLogger } from "@logtape/logtape";
8
9
  import { CryptographicKey } from "@fedify/vocab";
9
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -317,6 +318,27 @@ function parseKeyId(value) {
317
318
  function getKeyFetchErrorName(error) {
318
319
  return error.name || error.constructor.name || "Error";
319
320
  }
321
+ /**
322
+ * Known draft-cavage `algorithm` parameter values, used to keep the
323
+ * `http_signatures.algorithm` metric attribute on a bounded set. The header
324
+ * field is attacker-controlled and not used to select the verification
325
+ * algorithm, so unknown values are dropped from the metric to prevent
326
+ * cardinality blow-up.
327
+ */
328
+ const DRAFT_KNOWN_ALGORITHMS = new Set([
329
+ "ecdsa-sha256",
330
+ "ecdsa-sha384",
331
+ "ecdsa-sha512",
332
+ "ed25519",
333
+ "hs2019",
334
+ "rsa-sha1",
335
+ "rsa-sha256",
336
+ "rsa-sha512"
337
+ ]);
338
+ function classifyHttpVerifyResult(result) {
339
+ if (result.verified) return "verified";
340
+ return result.reason.type === "noSignature" ? "missing" : "rejected";
341
+ }
320
342
  function recordVerificationResult(span, result) {
321
343
  span.setAttribute("http_signatures.verified", result.verified);
322
344
  if (result.verified === true) return;
@@ -360,27 +382,37 @@ async function verifyRequestDetailed(request, options = {}) {
360
382
  span.setAttribute(ATTR_URL_FULL, request.url);
361
383
  for (const [name, value] of request.headers) span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value);
362
384
  }
385
+ const start = performance.now();
386
+ const metricsContext = {};
387
+ let result;
388
+ let threw = false;
363
389
  try {
364
390
  let spec = options.spec;
365
391
  if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
366
- let result;
367
- if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, options);
368
- else result = await verifyRequestDraft(request, span, options);
392
+ if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
393
+ else result = await verifyRequestDraft(request, span, metricsContext, options);
369
394
  recordVerificationResult(span, result);
370
395
  if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
371
396
  return result;
372
397
  } catch (error) {
398
+ threw = true;
373
399
  span.setStatus({
374
400
  code: SpanStatusCode.ERROR,
375
401
  message: String(error)
376
402
  });
377
403
  throw error;
378
404
  } finally {
405
+ const classified = threw ? "error" : classifyHttpVerifyResult(result);
406
+ const failureReason = result != null && !result.verified && result.reason.type !== "noSignature" ? result.reason.type : void 0;
407
+ getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "http", classified, {
408
+ algorithm: metricsContext.algorithm,
409
+ failureReason
410
+ });
379
411
  span.end();
380
412
  }
381
413
  });
382
414
  }
383
- async function verifyRequestDraft(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) {
415
+ async function verifyRequestDraft(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
384
416
  const logger = getLogger([
385
417
  "fedify",
386
418
  "sig",
@@ -528,13 +560,17 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
528
560
  const keyIdUrl = parseKeyId(keyId);
529
561
  if (keyIdUrl == null) return invalidSignatureResult(null);
530
562
  span?.setAttribute("http_signatures.key_id", keyId);
531
- if ("algorithm" in sigValues) span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
532
- const { key, cached, fetchError } = await fetchKeyDetailed(keyIdUrl, CryptographicKey, {
563
+ if ("algorithm" in sigValues) {
564
+ span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
565
+ const normalizedAlgorithm = sigValues.algorithm.toLowerCase();
566
+ if (DRAFT_KNOWN_ALGORITHMS.has(normalizedAlgorithm)) metricsContext.algorithm = normalizedAlgorithm;
567
+ }
568
+ const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyIdUrl, CryptographicKey, {
533
569
  documentLoader,
534
570
  contextLoader,
535
571
  keyCache,
536
572
  tracerProvider
537
- });
573
+ }));
538
574
  if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
539
575
  if (key == null) return invalidSignatureResult(keyIdUrl);
540
576
  const headerNames = headers.split(/\s+/g);
@@ -556,7 +592,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
556
592
  signature,
557
593
  message
558
594
  });
559
- return await verifyRequestDetailed(originalRequest, {
595
+ return await verifyRequestDraft(originalRequest, span, metricsContext, {
560
596
  documentLoader,
561
597
  contextLoader,
562
598
  timeWindow,
@@ -564,7 +600,9 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
564
600
  keyCache: {
565
601
  get: () => Promise.resolve(void 0),
566
602
  set: async (keyId, key) => await keyCache?.set(keyId, key)
567
- }
603
+ },
604
+ meterProvider,
605
+ tracerProvider
568
606
  });
569
607
  }
570
608
  logger.debug("Failed to verify with the fetched key {keyId}; signature {signature} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
@@ -640,7 +678,7 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
640
678
  }
641
679
  return false;
642
680
  }
643
- async function verifyRequestRfc9421(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) {
681
+ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
644
682
  const logger = getLogger([
645
683
  "fedify",
646
684
  "sig",
@@ -674,9 +712,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
674
712
  return invalidSignatureResult(null);
675
713
  }
676
714
  let failure = noSignatureResult();
715
+ let failureAlgorithm;
716
+ const setFailure = (result, algorithm) => {
717
+ failure = result;
718
+ failureAlgorithm = algorithm;
719
+ };
677
720
  for (const sigName of signatureNames) {
678
721
  if (!signatures[sigName]) {
679
- failure = invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId));
722
+ setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
680
723
  continue;
681
724
  }
682
725
  const sigInput = signatureInputs[sigName];
@@ -687,7 +730,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
687
730
  signatureName: sigName,
688
731
  signatureInput: signatureInputHeader
689
732
  });
690
- failure = invalidSignatureResult(null);
733
+ setFailure(invalidSignatureResult(null));
691
734
  continue;
692
735
  }
693
736
  if (!sigInput.created) {
@@ -695,7 +738,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
695
738
  signatureName: sigName,
696
739
  signatureInput: signatureInputHeader
697
740
  });
698
- failure = invalidSignatureResult(keyId);
741
+ setFailure(invalidSignatureResult(keyId));
699
742
  continue;
700
743
  }
701
744
  const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3);
@@ -707,14 +750,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
707
750
  created: signatureCreated.toString(),
708
751
  now: now.toString()
709
752
  });
710
- failure = invalidSignatureResult(keyId);
753
+ setFailure(invalidSignatureResult(keyId));
711
754
  continue;
712
755
  } else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) {
713
756
  logger.debug("Failed to verify; signature created time is too far in the past.", {
714
757
  created: signatureCreated.toString(),
715
758
  now: now.toString()
716
759
  });
717
- failure = invalidSignatureResult(keyId);
760
+ setFailure(invalidSignatureResult(keyId));
718
761
  continue;
719
762
  }
720
763
  }
@@ -722,34 +765,34 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
722
765
  const contentDigestHeader = request.headers.get("Content-Digest");
723
766
  if (!contentDigestHeader) {
724
767
  logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
725
- failure = invalidSignatureResult(keyId);
768
+ setFailure(invalidSignatureResult(keyId));
726
769
  continue;
727
770
  }
728
771
  if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
729
772
  logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
730
- failure = invalidSignatureResult(keyId);
773
+ setFailure(invalidSignatureResult(keyId));
731
774
  continue;
732
775
  }
733
776
  }
734
777
  span?.setAttribute("http_signatures.key_id", sigInput.keyId);
735
778
  span?.setAttribute("http_signatures.created", sigInput.created.toString());
736
779
  if (keyId == null) {
737
- failure = invalidSignatureResult(null);
780
+ setFailure(invalidSignatureResult(null));
738
781
  continue;
739
782
  }
740
- const { key, cached, fetchError } = await fetchKeyDetailed(keyId, CryptographicKey, {
783
+ const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
741
784
  documentLoader,
742
785
  contextLoader,
743
786
  keyCache,
744
787
  tracerProvider
745
- });
788
+ }));
746
789
  if (fetchError != null) {
747
- failure = keyFetchErrorResult(keyId, fetchError);
790
+ setFailure(keyFetchErrorResult(keyId, fetchError));
748
791
  continue;
749
792
  }
750
793
  if (!key) {
751
794
  logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
752
- failure = invalidSignatureResult(keyId);
795
+ setFailure(invalidSignatureResult(keyId));
753
796
  continue;
754
797
  }
755
798
  let alg = sigInput.alg?.toLowerCase();
@@ -761,12 +804,13 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
761
804
  }
762
805
  if (alg) span?.setAttribute("http_signatures.algorithm", alg);
763
806
  const algorithm = alg && rfc9421AlgorithmMap[alg];
807
+ const candidateAlgorithm = algorithm ? alg : void 0;
764
808
  if (!algorithm) {
765
809
  logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
766
810
  algorithm: sigInput.alg,
767
811
  supported: Object.keys(rfc9421AlgorithmMap)
768
812
  });
769
- failure = invalidSignatureResult(keyId);
813
+ setFailure(invalidSignatureResult(keyId));
770
814
  continue;
771
815
  }
772
816
  let signatureBase;
@@ -777,20 +821,22 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
777
821
  error,
778
822
  signatureInput: sigInput
779
823
  });
780
- failure = invalidSignatureResult(keyId);
824
+ setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
781
825
  continue;
782
826
  }
783
827
  const signatureBaseBytes = new TextEncoder().encode(signatureBase);
784
828
  span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
785
829
  try {
786
- if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) return {
787
- verified: true,
788
- key,
789
- signatureLabel: sigName
790
- };
791
- else if (cached) {
830
+ if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
831
+ metricsContext.algorithm = candidateAlgorithm;
832
+ return {
833
+ verified: true,
834
+ key,
835
+ signatureLabel: sigName
836
+ };
837
+ } else if (cached) {
792
838
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
793
- return await verifyRequestDetailed(originalRequest, {
839
+ return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
794
840
  documentLoader,
795
841
  contextLoader,
796
842
  timeWindow,
@@ -799,14 +845,16 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
799
845
  get: () => Promise.resolve(void 0),
800
846
  set: async (keyId, key) => await keyCache?.set(keyId, key)
801
847
  },
802
- spec: "rfc9421"
848
+ spec: "rfc9421",
849
+ meterProvider,
850
+ tracerProvider
803
851
  });
804
852
  } else {
805
853
  logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
806
854
  keyId: sigInput.keyId,
807
855
  signatureBase
808
856
  });
809
- failure = invalidSignatureResult(keyId);
857
+ setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
810
858
  }
811
859
  } catch (error) {
812
860
  logger.debug("Error during signature verification: {error}", {
@@ -814,9 +862,10 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
814
862
  keyId: sigInput.keyId,
815
863
  algorithm: sigInput.alg
816
864
  });
817
- failure = invalidSignatureResult(keyId);
865
+ setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
818
866
  }
819
867
  }
868
+ metricsContext.algorithm = failureAlgorithm;
820
869
  return failure;
821
870
  }
822
871
  /**
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-hqC7tKJn.mjs";
4
+ import { n as version, t as name } from "./deno-DVsHS7rA.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
7
7
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { d as validateCryptoKey, t as doubleKnock } from "./http-O8MYWwk8.js";
3
+ import { d as validateCryptoKey, t as doubleKnock } from "./http-CouJSFVK.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { curry } from "es-toolkit";
6
6
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
@@ -1,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-DV0il3vk.cjs");
4
+ const require_http = require("./http-CubOB9wq.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let es_toolkit = require("es-toolkit");
7
7
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
@@ -1,8 +1,9 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-hqC7tKJn.mjs";
5
- import { n as fetchKey, o as validateCryptoKey } from "./key-DW1EVmtP.mjs";
4
+ import { n as version, t as name } from "./deno-DVsHS7rA.mjs";
5
+ import { a as measureSignatureKeyFetch, n as getFederationMetrics, t as getDurationMs } from "./metrics-C4attqv0.mjs";
6
+ import { n as fetchKey, o as validateCryptoKey } from "./key-BoWaYRHm.mjs";
6
7
  import { getLogger } from "@logtape/logtape";
7
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
8
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -146,6 +147,17 @@ function detachSignature(jsonLd) {
146
147
  }
147
148
  /**
148
149
  * Verifies Linked Data Signatures of the given JSON-LD document.
150
+ *
151
+ * This is a low-level utility that only checks the cryptographic signature
152
+ * and (optionally) the cached key. It does not run the JSON-LD parsing,
153
+ * attribution, and owner checks that a complete inbound LD verification
154
+ * needs. For incoming activities, prefer {@link verifyJsonLd}, which is
155
+ * the public verification entry point and the one that emits the
156
+ * `activitypub.signature.verification.duration` metric for the LD path.
157
+ * `verifySignature` itself only emits
158
+ * `activitypub.signature.key_fetch.duration`, since the rest of the work
159
+ * that the verification-duration metric is meant to cover happens in
160
+ * `verifyJsonLd`.
149
161
  * @param jsonLd The JSON-LD document to verify.
150
162
  * @param options Options for verifying the signature.
151
163
  * @returns The public key that signed the document or `null` if the signature
@@ -165,7 +177,7 @@ async function verifySignature(jsonLd, options = {}) {
165
177
  });
166
178
  return null;
167
179
  }
168
- const { key, cached } = await fetchKey(new URL(sig.creator), CryptographicKey, options);
180
+ const { key, cached } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, options));
169
181
  if (key == null) return null;
170
182
  const sigOpts = {
171
183
  ...sig,
@@ -205,13 +217,13 @@ async function verifySignature(jsonLd, options = {}) {
205
217
  keyId: sig.creator,
206
218
  ...sig
207
219
  });
208
- const { key } = await fetchKey(new URL(sig.creator), CryptographicKey, {
220
+ const { key } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, {
209
221
  ...options,
210
222
  keyCache: {
211
223
  get: () => Promise.resolve(void 0),
212
224
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
213
225
  }
214
- });
226
+ }));
215
227
  if (key == null) return null;
216
228
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
217
229
  }
@@ -223,6 +235,33 @@ async function verifySignature(jsonLd, options = {}) {
223
235
  return null;
224
236
  }
225
237
  /**
238
+ * Known Linked Data Signature `type` values, used to keep
239
+ * `ld_signatures.type` on a bounded set of spec-defined string values.
240
+ * Fedify only signs and verifies `RsaSignature2017`; other values come in
241
+ * only from external documents and are dropped from the metric attribute to
242
+ * avoid attacker-controlled cardinality.
243
+ */
244
+ const LD_KNOWN_SIGNATURE_TYPES = new Set(["RsaSignature2017"]);
245
+ /**
246
+ * Reports only whether a `signature` key is present on the document, with
247
+ * no shape check on its value. This is intentionally looser than
248
+ * {@link hasSignature} (which validates a full `RsaSignature2017` shape)
249
+ * and {@link hasSignatureLike} (which structurally accepts several known
250
+ * suites): `verifyJsonLd` needs to tell a document with a malformed or
251
+ * unsupported signature payload (classified as `rejected`) apart from a
252
+ * truly unsigned document (classified as `missing`), and only this
253
+ * presence-only check captures both cases.
254
+ */
255
+ function hasLdSignatureProperty(jsonLd) {
256
+ return typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd;
257
+ }
258
+ function getLdSignatureObject(jsonLd) {
259
+ if (!hasLdSignatureProperty(jsonLd)) return void 0;
260
+ const { signature } = jsonLd;
261
+ if (typeof signature !== "object" || signature == null || Array.isArray(signature)) return;
262
+ return signature;
263
+ }
264
+ /**
226
265
  * Verify the authenticity of the given JSON-LD document using Linked Data
227
266
  * Signatures. If the document is signed, this function verifies the signature
228
267
  * and checks if the document is attributed to the owner of the public key.
@@ -233,14 +272,22 @@ async function verifySignature(jsonLd, options = {}) {
233
272
  */
234
273
  async function verifyJsonLd(jsonLd, options = {}) {
235
274
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("ld_signatures.verify", async (span) => {
275
+ const start = performance.now();
276
+ let verified = false;
277
+ let threw = false;
278
+ let signatureType;
236
279
  try {
237
280
  const object = await Object$1.fromJsonLd(jsonLd, options);
238
281
  if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
239
282
  span.setAttribute("activitypub.object.type", getTypeId(object).href);
240
- if (typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd && typeof jsonLd.signature === "object" && jsonLd.signature != null) {
241
- if ("creator" in jsonLd.signature && typeof jsonLd.signature.creator === "string") span.setAttribute("ld_signatures.key_id", jsonLd.signature.creator);
242
- if ("signatureValue" in jsonLd.signature && typeof jsonLd.signature.signatureValue === "string") span.setAttribute("ld_signatures.signature", jsonLd.signature.signatureValue);
243
- if ("type" in jsonLd.signature && typeof jsonLd.signature.type === "string") span.setAttribute("ld_signatures.type", jsonLd.signature.type);
283
+ const sig = getLdSignatureObject(jsonLd);
284
+ if (sig != null) {
285
+ if (typeof sig.creator === "string") span.setAttribute("ld_signatures.key_id", sig.creator);
286
+ if (typeof sig.signatureValue === "string") span.setAttribute("ld_signatures.signature", sig.signatureValue);
287
+ if (typeof sig.type === "string") {
288
+ span.setAttribute("ld_signatures.type", sig.type);
289
+ if (LD_KNOWN_SIGNATURE_TYPES.has(sig.type)) signatureType = sig.type;
290
+ }
244
291
  }
245
292
  const attributions = new Set(object.attributionIds.map((uri) => uri.href));
246
293
  if (object instanceof Activity) for (const uri of object.actorIds) attributions.add(uri.href);
@@ -255,14 +302,18 @@ async function verifyJsonLd(jsonLd, options = {}) {
255
302
  logger.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
256
303
  return false;
257
304
  }
305
+ verified = true;
258
306
  return true;
259
307
  } catch (error) {
308
+ threw = true;
260
309
  span.setStatus({
261
310
  code: SpanStatusCode.ERROR,
262
311
  message: String(error)
263
312
  });
264
313
  throw error;
265
314
  } finally {
315
+ const classified = threw ? "error" : verified ? "verified" : hasLdSignatureProperty(jsonLd) ? "rejected" : "missing";
316
+ getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "linked_data", classified, { ldType: signatureType });
266
317
  span.end();
267
318
  }
268
319
  });