@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,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  const require_chunk = 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 _fedify_vocab = require("@fedify/vocab");
7
7
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -148,6 +148,17 @@ function detachSignature(jsonLd) {
148
148
  }
149
149
  /**
150
150
  * Verifies Linked Data Signatures of the given JSON-LD document.
151
+ *
152
+ * This is a low-level utility that only checks the cryptographic signature
153
+ * and (optionally) the cached key. It does not run the JSON-LD parsing,
154
+ * attribution, and owner checks that a complete inbound LD verification
155
+ * needs. For incoming activities, prefer {@link verifyJsonLd}, which is
156
+ * the public verification entry point and the one that emits the
157
+ * `activitypub.signature.verification.duration` metric for the LD path.
158
+ * `verifySignature` itself only emits
159
+ * `activitypub.signature.key_fetch.duration`, since the rest of the work
160
+ * that the verification-duration metric is meant to cover happens in
161
+ * `verifyJsonLd`.
151
162
  * @param jsonLd The JSON-LD document to verify.
152
163
  * @param options Options for verifying the signature.
153
164
  * @returns The public key that signed the document or `null` if the signature
@@ -167,7 +178,7 @@ async function verifySignature(jsonLd, options = {}) {
167
178
  });
168
179
  return null;
169
180
  }
170
- const { key, cached } = await require_http.fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, options);
181
+ const { key, cached } = await require_http.measureSignatureKeyFetch(options.meterProvider, "linked_data", () => require_http.fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, options));
171
182
  if (key == null) return null;
172
183
  const sigOpts = {
173
184
  ...sig,
@@ -207,13 +218,13 @@ async function verifySignature(jsonLd, options = {}) {
207
218
  keyId: sig.creator,
208
219
  ...sig
209
220
  });
210
- const { key } = await require_http.fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, {
221
+ const { key } = await require_http.measureSignatureKeyFetch(options.meterProvider, "linked_data", () => require_http.fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, {
211
222
  ...options,
212
223
  keyCache: {
213
224
  get: () => Promise.resolve(void 0),
214
225
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
215
226
  }
216
- });
227
+ }));
217
228
  if (key == null) return null;
218
229
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
219
230
  }
@@ -225,6 +236,33 @@ async function verifySignature(jsonLd, options = {}) {
225
236
  return null;
226
237
  }
227
238
  /**
239
+ * Known Linked Data Signature `type` values, used to keep
240
+ * `ld_signatures.type` on a bounded set of spec-defined string values.
241
+ * Fedify only signs and verifies `RsaSignature2017`; other values come in
242
+ * only from external documents and are dropped from the metric attribute to
243
+ * avoid attacker-controlled cardinality.
244
+ */
245
+ const LD_KNOWN_SIGNATURE_TYPES = new Set(["RsaSignature2017"]);
246
+ /**
247
+ * Reports only whether a `signature` key is present on the document, with
248
+ * no shape check on its value. This is intentionally looser than
249
+ * {@link hasSignature} (which validates a full `RsaSignature2017` shape)
250
+ * and {@link hasSignatureLike} (which structurally accepts several known
251
+ * suites): `verifyJsonLd` needs to tell a document with a malformed or
252
+ * unsupported signature payload (classified as `rejected`) apart from a
253
+ * truly unsigned document (classified as `missing`), and only this
254
+ * presence-only check captures both cases.
255
+ */
256
+ function hasLdSignatureProperty(jsonLd) {
257
+ return typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd;
258
+ }
259
+ function getLdSignatureObject(jsonLd) {
260
+ if (!hasLdSignatureProperty(jsonLd)) return void 0;
261
+ const { signature } = jsonLd;
262
+ if (typeof signature !== "object" || signature == null || Array.isArray(signature)) return;
263
+ return signature;
264
+ }
265
+ /**
228
266
  * Verify the authenticity of the given JSON-LD document using Linked Data
229
267
  * Signatures. If the document is signed, this function verifies the signature
230
268
  * and checks if the document is attributed to the owner of the public key.
@@ -235,14 +273,22 @@ async function verifySignature(jsonLd, options = {}) {
235
273
  */
236
274
  async function verifyJsonLd(jsonLd, options = {}) {
237
275
  return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(require_http.name, require_http.version).startActiveSpan("ld_signatures.verify", async (span) => {
276
+ const start = performance.now();
277
+ let verified = false;
278
+ let threw = false;
279
+ let signatureType;
238
280
  try {
239
281
  const object = await _fedify_vocab.Object.fromJsonLd(jsonLd, options);
240
282
  if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
241
283
  span.setAttribute("activitypub.object.type", (0, _fedify_vocab.getTypeId)(object).href);
242
- if (typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd && typeof jsonLd.signature === "object" && jsonLd.signature != null) {
243
- if ("creator" in jsonLd.signature && typeof jsonLd.signature.creator === "string") span.setAttribute("ld_signatures.key_id", jsonLd.signature.creator);
244
- if ("signatureValue" in jsonLd.signature && typeof jsonLd.signature.signatureValue === "string") span.setAttribute("ld_signatures.signature", jsonLd.signature.signatureValue);
245
- if ("type" in jsonLd.signature && typeof jsonLd.signature.type === "string") span.setAttribute("ld_signatures.type", jsonLd.signature.type);
284
+ const sig = getLdSignatureObject(jsonLd);
285
+ if (sig != null) {
286
+ if (typeof sig.creator === "string") span.setAttribute("ld_signatures.key_id", sig.creator);
287
+ if (typeof sig.signatureValue === "string") span.setAttribute("ld_signatures.signature", sig.signatureValue);
288
+ if (typeof sig.type === "string") {
289
+ span.setAttribute("ld_signatures.type", sig.type);
290
+ if (LD_KNOWN_SIGNATURE_TYPES.has(sig.type)) signatureType = sig.type;
291
+ }
246
292
  }
247
293
  const attributions = new Set(object.attributionIds.map((uri) => uri.href));
248
294
  if (object instanceof _fedify_vocab.Activity) for (const uri of object.actorIds) attributions.add(uri.href);
@@ -257,14 +303,18 @@ async function verifyJsonLd(jsonLd, options = {}) {
257
303
  logger$3.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
258
304
  return false;
259
305
  }
306
+ verified = true;
260
307
  return true;
261
308
  } catch (error) {
309
+ threw = true;
262
310
  span.setStatus({
263
311
  code: _opentelemetry_api.SpanStatusCode.ERROR,
264
312
  message: String(error)
265
313
  });
266
314
  throw error;
267
315
  } finally {
316
+ const classified = threw ? "error" : verified ? "verified" : hasLdSignatureProperty(jsonLd) ? "rejected" : "missing";
317
+ require_http.getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(require_http.getDurationMs(start), "linked_data", classified, { ldType: signatureType });
268
318
  span.end();
269
319
  }
270
320
  });
@@ -769,6 +819,15 @@ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) {
769
819
  }
770
820
  //#endregion
771
821
  //#region src/sig/proof.ts
822
+ /**
823
+ * Known Object Integrity Proof `cryptosuite` values, used to keep
824
+ * `object_integrity_proofs.cryptosuite` on a bounded set of spec-defined
825
+ * string values. Fedify currently signs and verifies only
826
+ * `eddsa-jcs-2022`; other values come in only from external proofs and are
827
+ * dropped from the metric attribute to avoid attacker-controlled
828
+ * cardinality.
829
+ */
830
+ const OIP_KNOWN_CRYPTOSUITES = new Set(["eddsa-jcs-2022"]);
772
831
  const logger = (0, _logtape_logtape.getLogger)([
773
832
  "fedify",
774
833
  "sig",
@@ -895,6 +954,10 @@ async function signObject(object, privateKey, keyId, options = {}) {
895
954
  */
896
955
  async function verifyProof(jsonLd, proof, options = {}) {
897
956
  return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(require_http.name, require_http.version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
957
+ const start = performance.now();
958
+ let verified = false;
959
+ let threw = false;
960
+ const cryptosuite = proof.cryptosuite != null && OIP_KNOWN_CRYPTOSUITES.has(proof.cryptosuite) ? proof.cryptosuite : void 0;
898
961
  if (span.isRecording()) {
899
962
  if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
900
963
  if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
@@ -903,21 +966,25 @@ async function verifyProof(jsonLd, proof, options = {}) {
903
966
  try {
904
967
  const key = await verifyProofInternal(jsonLd, proof, options);
905
968
  if (key == null) span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
969
+ else verified = true;
906
970
  return key;
907
971
  } catch (error) {
972
+ threw = true;
908
973
  span.setStatus({
909
974
  code: _opentelemetry_api.SpanStatusCode.ERROR,
910
975
  message: String(error)
911
976
  });
912
977
  throw error;
913
978
  } finally {
979
+ const classified = threw ? "error" : verified ? "verified" : "rejected";
980
+ require_http.getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(require_http.getDurationMs(start), "object_integrity", classified, { cryptosuite });
914
981
  span.end();
915
982
  }
916
983
  });
917
984
  }
918
985
  async function verifyProofInternal(jsonLd, proof, options) {
919
986
  if (typeof jsonLd !== "object" || jsonLd == null || Array.isArray(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
920
- const publicKeyPromise = require_http.fetchKey(proof.verificationMethodId, _fedify_vocab.Multikey, options);
987
+ const publicKeyPromise = require_http.measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => require_http.fetchKey(proof.verificationMethodId, _fedify_vocab.Multikey, options));
921
988
  const proofConfig = {
922
989
  "@context": jsonLd["@context"],
923
990
  type: "DataIntegrityProof",
@@ -957,7 +1024,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
957
1024
  proof,
958
1025
  keyId: proof.verificationMethodId.href
959
1026
  });
960
- return await verifyProof(jsonLd, proof, {
1027
+ return await verifyProofInternal(jsonLd, proof, {
961
1028
  ...options,
962
1029
  keyCache: {
963
1030
  get: () => Promise.resolve(void 0),
@@ -988,7 +1055,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
988
1055
  keyId: proof.verificationMethodId.href,
989
1056
  proof
990
1057
  });
991
- return await verifyProof(jsonLd, proof, {
1058
+ return await verifyProofInternal(jsonLd, proof, {
992
1059
  ...options,
993
1060
  keyCache: {
994
1061
  get: () => Promise.resolve(void 0),
@@ -1,16 +1,26 @@
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 { n as fetchKey, o as validateCryptoKey } from "./key-DW1EVmtP.mjs";
6
- import { n as preloadedOnlyDocumentLoader } from "./public-audience-N3pyOx2p.mjs";
7
- import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BgFLCJQ_.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";
7
+ import { n as preloadedOnlyDocumentLoader } from "./public-audience-c9zmYKgA.mjs";
8
+ import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BNL8AC14.mjs";
8
9
  import { getLogger } from "@logtape/logtape";
9
10
  import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
10
11
  import { SpanStatusCode, trace } from "@opentelemetry/api";
11
12
  import { encodeHex } from "byte-encodings/hex";
12
13
  import serialize from "json-canon";
13
14
  //#region src/sig/proof.ts
15
+ /**
16
+ * Known Object Integrity Proof `cryptosuite` values, used to keep
17
+ * `object_integrity_proofs.cryptosuite` on a bounded set of spec-defined
18
+ * string values. Fedify currently signs and verifies only
19
+ * `eddsa-jcs-2022`; other values come in only from external proofs and are
20
+ * dropped from the metric attribute to avoid attacker-controlled
21
+ * cardinality.
22
+ */
23
+ const OIP_KNOWN_CRYPTOSUITES = new Set(["eddsa-jcs-2022"]);
14
24
  const logger = getLogger([
15
25
  "fedify",
16
26
  "sig",
@@ -137,6 +147,10 @@ async function signObject(object, privateKey, keyId, options = {}) {
137
147
  */
138
148
  async function verifyProof(jsonLd, proof, options = {}) {
139
149
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
150
+ const start = performance.now();
151
+ let verified = false;
152
+ let threw = false;
153
+ const cryptosuite = proof.cryptosuite != null && OIP_KNOWN_CRYPTOSUITES.has(proof.cryptosuite) ? proof.cryptosuite : void 0;
140
154
  if (span.isRecording()) {
141
155
  if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
142
156
  if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
@@ -145,21 +159,25 @@ async function verifyProof(jsonLd, proof, options = {}) {
145
159
  try {
146
160
  const key = await verifyProofInternal(jsonLd, proof, options);
147
161
  if (key == null) span.setStatus({ code: SpanStatusCode.ERROR });
162
+ else verified = true;
148
163
  return key;
149
164
  } catch (error) {
165
+ threw = true;
150
166
  span.setStatus({
151
167
  code: SpanStatusCode.ERROR,
152
168
  message: String(error)
153
169
  });
154
170
  throw error;
155
171
  } finally {
172
+ const classified = threw ? "error" : verified ? "verified" : "rejected";
173
+ getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "object_integrity", classified, { cryptosuite });
156
174
  span.end();
157
175
  }
158
176
  });
159
177
  }
160
178
  async function verifyProofInternal(jsonLd, proof, options) {
161
179
  if (typeof jsonLd !== "object" || jsonLd == null || Array.isArray(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
162
- const publicKeyPromise = fetchKey(proof.verificationMethodId, Multikey, options);
180
+ const publicKeyPromise = measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => fetchKey(proof.verificationMethodId, Multikey, options));
163
181
  const proofConfig = {
164
182
  "@context": jsonLd["@context"],
165
183
  type: "DataIntegrityProof",
@@ -199,7 +217,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
199
217
  proof,
200
218
  keyId: proof.verificationMethodId.href
201
219
  });
202
- return await verifyProof(jsonLd, proof, {
220
+ return await verifyProofInternal(jsonLd, proof, {
203
221
  ...options,
204
222
  keyCache: {
205
223
  get: () => Promise.resolve(void 0),
@@ -230,7 +248,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
230
248
  keyId: proof.verificationMethodId.href,
231
249
  proof
232
250
  });
233
- return await verifyProof(jsonLd, proof, {
251
+ return await verifyProofInternal(jsonLd, proof, {
234
252
  ...options,
235
253
  keyCache: {
236
254
  get: () => Promise.resolve(void 0),
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { _ as version, d as validateCryptoKey, g as name, s as fetchKey } from "./http-O8MYWwk8.js";
3
+ import { E as version, T as name, d as validateCryptoKey, f as getDurationMs, g as measureSignatureKeyFetch, p as getFederationMetrics, s as fetchKey } from "./http-CouJSFVK.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { Activity, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1, PUBLIC_COLLECTION, getTypeId, isActor } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -145,6 +145,17 @@ function detachSignature(jsonLd) {
145
145
  }
146
146
  /**
147
147
  * Verifies Linked Data Signatures of the given JSON-LD document.
148
+ *
149
+ * This is a low-level utility that only checks the cryptographic signature
150
+ * and (optionally) the cached key. It does not run the JSON-LD parsing,
151
+ * attribution, and owner checks that a complete inbound LD verification
152
+ * needs. For incoming activities, prefer {@link verifyJsonLd}, which is
153
+ * the public verification entry point and the one that emits the
154
+ * `activitypub.signature.verification.duration` metric for the LD path.
155
+ * `verifySignature` itself only emits
156
+ * `activitypub.signature.key_fetch.duration`, since the rest of the work
157
+ * that the verification-duration metric is meant to cover happens in
158
+ * `verifyJsonLd`.
148
159
  * @param jsonLd The JSON-LD document to verify.
149
160
  * @param options Options for verifying the signature.
150
161
  * @returns The public key that signed the document or `null` if the signature
@@ -164,7 +175,7 @@ async function verifySignature(jsonLd, options = {}) {
164
175
  });
165
176
  return null;
166
177
  }
167
- const { key, cached } = await fetchKey(new URL(sig.creator), CryptographicKey, options);
178
+ const { key, cached } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, options));
168
179
  if (key == null) return null;
169
180
  const sigOpts = {
170
181
  ...sig,
@@ -204,13 +215,13 @@ async function verifySignature(jsonLd, options = {}) {
204
215
  keyId: sig.creator,
205
216
  ...sig
206
217
  });
207
- const { key } = await fetchKey(new URL(sig.creator), CryptographicKey, {
218
+ const { key } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, {
208
219
  ...options,
209
220
  keyCache: {
210
221
  get: () => Promise.resolve(void 0),
211
222
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
212
223
  }
213
- });
224
+ }));
214
225
  if (key == null) return null;
215
226
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
216
227
  }
@@ -222,6 +233,33 @@ async function verifySignature(jsonLd, options = {}) {
222
233
  return null;
223
234
  }
224
235
  /**
236
+ * Known Linked Data Signature `type` values, used to keep
237
+ * `ld_signatures.type` on a bounded set of spec-defined string values.
238
+ * Fedify only signs and verifies `RsaSignature2017`; other values come in
239
+ * only from external documents and are dropped from the metric attribute to
240
+ * avoid attacker-controlled cardinality.
241
+ */
242
+ const LD_KNOWN_SIGNATURE_TYPES = new Set(["RsaSignature2017"]);
243
+ /**
244
+ * Reports only whether a `signature` key is present on the document, with
245
+ * no shape check on its value. This is intentionally looser than
246
+ * {@link hasSignature} (which validates a full `RsaSignature2017` shape)
247
+ * and {@link hasSignatureLike} (which structurally accepts several known
248
+ * suites): `verifyJsonLd` needs to tell a document with a malformed or
249
+ * unsupported signature payload (classified as `rejected`) apart from a
250
+ * truly unsigned document (classified as `missing`), and only this
251
+ * presence-only check captures both cases.
252
+ */
253
+ function hasLdSignatureProperty(jsonLd) {
254
+ return typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd;
255
+ }
256
+ function getLdSignatureObject(jsonLd) {
257
+ if (!hasLdSignatureProperty(jsonLd)) return void 0;
258
+ const { signature } = jsonLd;
259
+ if (typeof signature !== "object" || signature == null || Array.isArray(signature)) return;
260
+ return signature;
261
+ }
262
+ /**
225
263
  * Verify the authenticity of the given JSON-LD document using Linked Data
226
264
  * Signatures. If the document is signed, this function verifies the signature
227
265
  * and checks if the document is attributed to the owner of the public key.
@@ -232,14 +270,22 @@ async function verifySignature(jsonLd, options = {}) {
232
270
  */
233
271
  async function verifyJsonLd(jsonLd, options = {}) {
234
272
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("ld_signatures.verify", async (span) => {
273
+ const start = performance.now();
274
+ let verified = false;
275
+ let threw = false;
276
+ let signatureType;
235
277
  try {
236
278
  const object = await Object$1.fromJsonLd(jsonLd, options);
237
279
  if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
238
280
  span.setAttribute("activitypub.object.type", getTypeId(object).href);
239
- if (typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd && typeof jsonLd.signature === "object" && jsonLd.signature != null) {
240
- if ("creator" in jsonLd.signature && typeof jsonLd.signature.creator === "string") span.setAttribute("ld_signatures.key_id", jsonLd.signature.creator);
241
- if ("signatureValue" in jsonLd.signature && typeof jsonLd.signature.signatureValue === "string") span.setAttribute("ld_signatures.signature", jsonLd.signature.signatureValue);
242
- if ("type" in jsonLd.signature && typeof jsonLd.signature.type === "string") span.setAttribute("ld_signatures.type", jsonLd.signature.type);
281
+ const sig = getLdSignatureObject(jsonLd);
282
+ if (sig != null) {
283
+ if (typeof sig.creator === "string") span.setAttribute("ld_signatures.key_id", sig.creator);
284
+ if (typeof sig.signatureValue === "string") span.setAttribute("ld_signatures.signature", sig.signatureValue);
285
+ if (typeof sig.type === "string") {
286
+ span.setAttribute("ld_signatures.type", sig.type);
287
+ if (LD_KNOWN_SIGNATURE_TYPES.has(sig.type)) signatureType = sig.type;
288
+ }
243
289
  }
244
290
  const attributions = new Set(object.attributionIds.map((uri) => uri.href));
245
291
  if (object instanceof Activity) for (const uri of object.actorIds) attributions.add(uri.href);
@@ -254,14 +300,18 @@ async function verifyJsonLd(jsonLd, options = {}) {
254
300
  logger$3.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
255
301
  return false;
256
302
  }
303
+ verified = true;
257
304
  return true;
258
305
  } catch (error) {
306
+ threw = true;
259
307
  span.setStatus({
260
308
  code: SpanStatusCode.ERROR,
261
309
  message: String(error)
262
310
  });
263
311
  throw error;
264
312
  } finally {
313
+ const classified = threw ? "error" : verified ? "verified" : hasLdSignatureProperty(jsonLd) ? "rejected" : "missing";
314
+ getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "linked_data", classified, { ldType: signatureType });
265
315
  span.end();
266
316
  }
267
317
  });
@@ -766,6 +816,15 @@ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) {
766
816
  }
767
817
  //#endregion
768
818
  //#region src/sig/proof.ts
819
+ /**
820
+ * Known Object Integrity Proof `cryptosuite` values, used to keep
821
+ * `object_integrity_proofs.cryptosuite` on a bounded set of spec-defined
822
+ * string values. Fedify currently signs and verifies only
823
+ * `eddsa-jcs-2022`; other values come in only from external proofs and are
824
+ * dropped from the metric attribute to avoid attacker-controlled
825
+ * cardinality.
826
+ */
827
+ const OIP_KNOWN_CRYPTOSUITES = new Set(["eddsa-jcs-2022"]);
769
828
  const logger = getLogger([
770
829
  "fedify",
771
830
  "sig",
@@ -892,6 +951,10 @@ async function signObject(object, privateKey, keyId, options = {}) {
892
951
  */
893
952
  async function verifyProof(jsonLd, proof, options = {}) {
894
953
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
954
+ const start = performance.now();
955
+ let verified = false;
956
+ let threw = false;
957
+ const cryptosuite = proof.cryptosuite != null && OIP_KNOWN_CRYPTOSUITES.has(proof.cryptosuite) ? proof.cryptosuite : void 0;
895
958
  if (span.isRecording()) {
896
959
  if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
897
960
  if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
@@ -900,21 +963,25 @@ async function verifyProof(jsonLd, proof, options = {}) {
900
963
  try {
901
964
  const key = await verifyProofInternal(jsonLd, proof, options);
902
965
  if (key == null) span.setStatus({ code: SpanStatusCode.ERROR });
966
+ else verified = true;
903
967
  return key;
904
968
  } catch (error) {
969
+ threw = true;
905
970
  span.setStatus({
906
971
  code: SpanStatusCode.ERROR,
907
972
  message: String(error)
908
973
  });
909
974
  throw error;
910
975
  } finally {
976
+ const classified = threw ? "error" : verified ? "verified" : "rejected";
977
+ getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "object_integrity", classified, { cryptosuite });
911
978
  span.end();
912
979
  }
913
980
  });
914
981
  }
915
982
  async function verifyProofInternal(jsonLd, proof, options) {
916
983
  if (typeof jsonLd !== "object" || jsonLd == null || Array.isArray(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
917
- const publicKeyPromise = fetchKey(proof.verificationMethodId, Multikey, options);
984
+ const publicKeyPromise = measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => fetchKey(proof.verificationMethodId, Multikey, options));
918
985
  const proofConfig = {
919
986
  "@context": jsonLd["@context"],
920
987
  type: "DataIntegrityProof",
@@ -954,7 +1021,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
954
1021
  proof,
955
1022
  keyId: proof.verificationMethodId.href
956
1023
  });
957
- return await verifyProof(jsonLd, proof, {
1024
+ return await verifyProofInternal(jsonLd, proof, {
958
1025
  ...options,
959
1026
  keyCache: {
960
1027
  get: () => Promise.resolve(void 0),
@@ -985,7 +1052,7 @@ async function verifyProofInternal(jsonLd, proof, options) {
985
1052
  keyId: proof.verificationMethodId.href,
986
1053
  proof
987
1054
  });
988
- return await verifyProof(jsonLd, proof, {
1055
+ return await verifyProofInternal(jsonLd, proof, {
989
1056
  ...options,
990
1057
  keyCache: {
991
1058
  get: () => Promise.resolve(void 0),