@fedify/fedify 2.4.0-dev.1964 → 2.4.0-dev.1988

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 (83) hide show
  1. package/dist/{builder-B4zpKIFU.mjs → builder-Cts78G9N.mjs} +2 -2
  2. package/dist/{client-CtxvmazV.mjs → client-DpnpgjyX.mjs} +78 -4
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{context-COLwHogJ.d.cts → context-BeDnDuF3.d.cts} +4 -3
  7. package/dist/{context-DiiCt4S7.d.ts → context-uyRUWoZQ.d.ts} +4 -3
  8. package/dist/{deno-CtG93ohj.mjs → deno-BgMEefEr.mjs} +1 -1
  9. package/dist/{docloader-dk_k3S8d.mjs → docloader-DEYs99FZ.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/handler.test.mjs +122 -4
  12. package/dist/federation/idempotency.test.mjs +2 -2
  13. package/dist/federation/keycache.test.mjs +52 -9
  14. package/dist/federation/metrics.test.mjs +1 -1
  15. package/dist/federation/middleware.test.mjs +111 -15
  16. package/dist/federation/mod.cjs +1 -1
  17. package/dist/federation/mod.d.cts +2 -2
  18. package/dist/federation/mod.d.ts +2 -2
  19. package/dist/federation/mod.js +1 -1
  20. package/dist/federation/send.test.mjs +139 -4
  21. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  22. package/dist/federation/tasks/tasks.test.mjs +2 -2
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{federation-Drjlrrax.cjs → federation-DzPUG_AP.cjs} +93 -17
  26. package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
  27. package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
  28. package/dist/{http-Dj4s1UQ0.js → http-CgwqtxAy.js} +169 -7
  29. package/dist/{http-D7LX-BL9.mjs → http-CtidZS_t.mjs} +3 -3
  30. package/dist/{http-B3skRvGo.cjs → http-Z3anvCd-.cjs} +180 -6
  31. package/dist/{key-BVjgbs_9.mjs → key-Cwbg5ggV.mjs} +169 -7
  32. package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
  33. package/dist/{kv-cache-DtsjH_bY.cjs → kv-cache-BM2-w-Wq.cjs} +2 -2
  34. package/dist/{kv-cache-tGNbvDCF.js → kv-cache-CykuL0ya.js} +2 -2
  35. package/dist/{kv-cache-xRcC_xxP.mjs → kv-cache-DHiR8yTf.mjs} +1 -1
  36. package/dist/{ld-OuCeQFny.mjs → ld-CzhdWyTt.mjs} +3 -3
  37. package/dist/{metrics-B9N27cCB.mjs → metrics-CrqsHP0l.mjs} +1 -1
  38. package/dist/{middleware-Cd_vPqDH.mjs → middleware-CZpYxSKD.mjs} +1 -1
  39. package/dist/{middleware-CPTDFMmJ.js → middleware-CeCj5caM.js} +101 -25
  40. package/dist/{middleware-TeLjzoxn.mjs → middleware-DCpiAsZi.mjs} +34 -23
  41. package/dist/{mod-DypxPS9H.d.ts → mod-C0BYc0OU.d.ts} +1 -1
  42. package/dist/{mod-CnGLNoVw.d.cts → mod-CbKDOnCt.d.cts} +1 -1
  43. package/dist/{mod-CLHkCwLm.d.cts → mod-CyiSjRkk.d.cts} +1 -1
  44. package/dist/{mod-CSH1Ee-I.d.ts → mod-D882_Xsg.d.ts} +1 -1
  45. package/dist/{mod-DzRXACM5.d.ts → mod-UDwMjuHM.d.ts} +1 -1
  46. package/dist/{mod-BZldCEU0.d.cts → mod-x9W0d0Pk.d.cts} +1 -1
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +28 -1
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.js +1 -1
  55. package/dist/{owner-DGSfntI-.d.ts → owner-BF0L9cMt.d.cts} +12 -1
  56. package/dist/{owner-r_waTiQj.d.cts → owner-BSzr5-I8.d.ts} +12 -1
  57. package/dist/owner-BqVXJLW8.mjs +175 -0
  58. package/dist/{proof-BO_esQii.js → proof-CUghqhAf.js} +122 -65
  59. package/dist/{proof-Dua0d906.cjs → proof-DHu5zI0Z.cjs} +122 -65
  60. package/dist/{proof-D5Ajk4DJ.mjs → proof-DL8Iid3a.mjs} +14 -11
  61. package/dist/{send-DVXyUqhO.mjs → send-6jS4xFsX.mjs} +68 -11
  62. package/dist/sig/http.test.mjs +2 -2
  63. package/dist/sig/key.test.mjs +169 -2
  64. package/dist/sig/ld.test.mjs +55 -2
  65. package/dist/sig/mod.cjs +2 -2
  66. package/dist/sig/mod.d.cts +3 -3
  67. package/dist/sig/mod.d.ts +3 -3
  68. package/dist/sig/mod.js +2 -2
  69. package/dist/sig/owner.test.mjs +103 -4
  70. package/dist/sig/proof.test.mjs +120 -1
  71. package/dist/{temporal-BoDw51A-.mjs → temporal-DcyzeytP.mjs} +1 -1
  72. package/dist/testing/kv.test.mjs +1 -1
  73. package/dist/testing/mod.d.mts +2 -1
  74. package/dist/{types-RXm5YxpC.cjs → types-Be2zbyeG.cjs} +94 -2
  75. package/dist/{types-Dtyf8lVj.js → types-C_cNlNVK.js} +78 -4
  76. package/dist/utils/docloader.test.mjs +27 -3
  77. package/dist/utils/kv-cache.test.mjs +1 -1
  78. package/dist/utils/mod.cjs +1 -1
  79. package/dist/utils/mod.d.cts +1 -1
  80. package/dist/utils/mod.d.ts +1 -1
  81. package/dist/utils/mod.js +1 -1
  82. package/package.json +6 -6
  83. package/dist/owner-DTEqTzSr.mjs +0 -120
@@ -1,17 +1,17 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-B3skRvGo.cjs");
5
- const require_proof = require("./proof-Dua0d906.cjs");
6
- const require_types = require("./types-RXm5YxpC.cjs");
7
- const require_kv_cache = require("./kv-cache-DtsjH_bY.cjs");
4
+ const require_http = require("./http-Z3anvCd-.cjs");
5
+ const require_types = require("./types-Be2zbyeG.cjs");
6
+ const require_proof = require("./proof-DHu5zI0Z.cjs");
7
+ const require_kv_cache = require("./kv-cache-BM2-w-Wq.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
11
11
  let _opentelemetry_api = require("@opentelemetry/api");
12
12
  let byte_encodings_hex = require("byte-encodings/hex");
13
- let es_toolkit = require("es-toolkit");
14
13
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
14
+ let es_toolkit = require("es-toolkit");
15
15
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
16
16
  let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
17
17
  _fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
@@ -1458,6 +1458,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
1458
1458
  //#endregion
1459
1459
  //#region src/federation/keycache.ts
1460
1460
  const Temporal$8 = require_http.Temporal;
1461
+ const KEY_CACHE_GENERATION = "2";
1461
1462
  var KvKeyCache = class {
1462
1463
  kv;
1463
1464
  prefix;
@@ -1480,13 +1481,20 @@ var KvKeyCache = class {
1480
1481
  keyId.href
1481
1482
  ];
1482
1483
  }
1484
+ #entryKey(keyId) {
1485
+ return [
1486
+ ...this.prefix,
1487
+ KEY_CACHE_GENERATION,
1488
+ keyId.href
1489
+ ];
1490
+ }
1483
1491
  async get(keyId) {
1484
1492
  const negativeExpiration = this.nullKeys.get(keyId.href);
1485
1493
  if (negativeExpiration != null) {
1486
1494
  if (Temporal$8.Now.instant().until(negativeExpiration).sign >= 0) return null;
1487
1495
  this.nullKeys.delete(keyId.href);
1488
1496
  }
1489
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
1497
+ const serialized = await this.kv.get(this.#entryKey(keyId));
1490
1498
  if (serialized === void 0) return void 0;
1491
1499
  if (serialized === null) {
1492
1500
  this.nullKeys.set(keyId.href, Temporal$8.Now.instant().add(this.unavailableKeyTtl));
@@ -1498,7 +1506,7 @@ var KvKeyCache = class {
1498
1506
  try {
1499
1507
  return await _fedify_vocab.Multikey.fromJsonLd(serialized, this.options);
1500
1508
  } catch {
1501
- await this.kv.delete([...this.prefix, keyId.href]);
1509
+ await this.kv.delete(this.#entryKey(keyId));
1502
1510
  return;
1503
1511
  }
1504
1512
  }
@@ -1506,12 +1514,12 @@ var KvKeyCache = class {
1506
1514
  async set(keyId, key) {
1507
1515
  if (key == null) {
1508
1516
  this.nullKeys.set(keyId.href, Temporal$8.Now.instant().add(this.unavailableKeyTtl));
1509
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
1517
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
1510
1518
  return;
1511
1519
  }
1512
1520
  this.nullKeys.delete(keyId.href);
1513
1521
  const serialized = await key.toJsonLd(this.options);
1514
- await this.kv.set([...this.prefix, keyId.href], serialized, { ttl: this.keyTtl });
1522
+ await this.kv.set(this.#entryKey(keyId), serialized, { ttl: this.keyTtl });
1515
1523
  }
1516
1524
  async getFetchError(keyId) {
1517
1525
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -2516,8 +2524,16 @@ async function handleInboxInternal(request, parameters, span) {
2516
2524
  }
2517
2525
  let json;
2518
2526
  try {
2519
- json = await request.clone().json();
2527
+ json = JSON.parse(await require_types.readBoundedText(request.clone(), require_types.MAX_BODY_SIZE, request.url));
2520
2528
  } catch (error) {
2529
+ if (error instanceof require_types.BodyTooLargeError) {
2530
+ request.body?.cancel(error).catch(() => {});
2531
+ span.setStatus({
2532
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
2533
+ message: String(error)
2534
+ });
2535
+ return new Response("Inbox body too large.", { status: 413 });
2536
+ }
2521
2537
  logger.error("Failed to parse JSON:\n{error}", {
2522
2538
  recipient,
2523
2539
  error
@@ -3737,7 +3753,7 @@ async function readLimitedResponseBody(response, maxBytes) {
3737
3753
  if (truncated) result += "… (truncated)";
3738
3754
  return result;
3739
3755
  }
3740
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
3756
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
3741
3757
  const logger = (0, _logtape_logtape.getLogger)([
3742
3758
  "fedify",
3743
3759
  "federation",
@@ -3746,12 +3762,25 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3746
3762
  const federationMetrics = require_http.getFederationMetrics(meterProvider);
3747
3763
  const started = performance.now();
3748
3764
  let deliverySuccess = false;
3765
+ async function validateUrl(url) {
3766
+ if (!allowPrivateAddress) try {
3767
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
3768
+ } catch (error) {
3769
+ if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
3770
+ url,
3771
+ error
3772
+ });
3773
+ throw error;
3774
+ }
3775
+ }
3776
+ await validateUrl(inbox.href);
3749
3777
  headers = new Headers(headers);
3750
3778
  headers.set("Content-Type", "application/activity+json");
3751
3779
  const request = new Request(inbox, {
3752
3780
  method: "POST",
3753
3781
  headers,
3754
- body: JSON.stringify(activity)
3782
+ body: JSON.stringify(activity),
3783
+ redirect: "manual"
3755
3784
  });
3756
3785
  let rsaKey = null;
3757
3786
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
@@ -3767,19 +3796,20 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3767
3796
  });
3768
3797
  let response;
3769
3798
  try {
3770
- response = rsaKey == null ? await fetch(request) : await require_http.doubleKnock(request, rsaKey, {
3799
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
3771
3800
  tracerProvider,
3772
- specDeterminer
3801
+ specDeterminer,
3802
+ validateRedirect: validateUrl
3773
3803
  });
3774
3804
  } catch (error) {
3775
- const transportError = error instanceof _fedify_vocab_runtime.FetchError ? error : createFetchError(inbox.href, error);
3805
+ const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
3776
3806
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
3777
3807
  activityId,
3778
3808
  inbox: inbox.href,
3779
- error: transportError
3809
+ error: failure
3780
3810
  });
3781
3811
  federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
3782
- throw transportError;
3812
+ throw failure;
3783
3813
  }
3784
3814
  try {
3785
3815
  if (!response.ok) {
@@ -3816,6 +3846,49 @@ function createFetchError(url, cause) {
3816
3846
  error.cause = cause;
3817
3847
  return error;
3818
3848
  }
3849
+ async function fetchWithValidatedRedirects(request, validateUrl) {
3850
+ let body = await request.clone().text();
3851
+ for (let redirects = 0;; redirects++) {
3852
+ const response = await fetch(request, { redirect: "manual" });
3853
+ const location = response.headers.get("Location");
3854
+ if (![
3855
+ 301,
3856
+ 302,
3857
+ 303,
3858
+ 307,
3859
+ 308
3860
+ ].includes(response.status) || location == null) return response;
3861
+ await response.body?.cancel();
3862
+ if (redirects >= 20) throw new TypeError("Too many redirects");
3863
+ const url = new URL(location, request.url);
3864
+ await validateUrl(url.href);
3865
+ const headers = new Headers(request.headers);
3866
+ if (url.origin !== new URL(request.url).origin) {
3867
+ headers.delete("Authorization");
3868
+ headers.delete("Proxy-Authorization");
3869
+ headers.delete("Cookie");
3870
+ headers.delete("Host");
3871
+ }
3872
+ let method = request.method;
3873
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
3874
+ method = "GET";
3875
+ body = void 0;
3876
+ for (const name of [
3877
+ "Content-Encoding",
3878
+ "Content-Language",
3879
+ "Content-Length",
3880
+ "Content-Location",
3881
+ "Content-Type"
3882
+ ]) headers.delete(name);
3883
+ }
3884
+ request = new Request(url, {
3885
+ method,
3886
+ headers,
3887
+ body,
3888
+ redirect: "manual"
3889
+ });
3890
+ }
3891
+ }
3819
3892
  /**
3820
3893
  * An error that is thrown when an activity fails to send to a remote inbox.
3821
3894
  * It contains structured information about the failure, including the HTTP
@@ -5309,6 +5382,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5309
5382
  if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
5310
5383
  }
5311
5384
  await sendActivity({
5385
+ allowPrivateAddress: this.allowPrivateAddress,
5312
5386
  keys,
5313
5387
  activity: message.activity,
5314
5388
  activityId: message.activityId,
@@ -5930,6 +6004,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5930
6004
  });
5931
6005
  const promises = [];
5932
6006
  for (const inbox in inboxes) promises.push(sendActivity({
6007
+ allowPrivateAddress: this.allowPrivateAddress,
5933
6008
  keys,
5934
6009
  activity: jsonLd,
5935
6010
  activityId: activity.id?.href,
@@ -7264,6 +7339,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
7264
7339
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
7265
7340
  const promises = [];
7266
7341
  for (const inbox in inboxes) promises.push(sendActivity({
7342
+ allowPrivateAddress: ctx.federation.allowPrivateAddress,
7267
7343
  keys,
7268
7344
  activity: ctx.activity,
7269
7345
  activityId: ctx.activityId,
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { DocumentLoader } from "@fedify/vocab-runtime";
2
+ import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
4
3
  import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
+ import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -59,6 +59,38 @@ interface FetchKeyOptions {
59
59
  * @since 2.3.0
60
60
  */
61
61
  meterProvider?: MeterProvider;
62
+ /**
63
+ * An assertion by the caller that it has already bound this key's id to
64
+ * whoever the key will be treated as speaking for, by means of its own that
65
+ * key resolution cannot see.
66
+ *
67
+ * Key resolution normally settles ownership itself: it dereferences the
68
+ * `owner`/`controller` a key claims and requires that actor's own document
69
+ * to link back to the key, because the claim and the key document come from
70
+ * the same host and so the claim proves nothing on its own. See
71
+ * GHSA-q9f8-5hc7-898f.
72
+ *
73
+ * That check presumes the ActivityPub shape of ownership, where the owner
74
+ * is an actor carrying a `publicKey`/`assertionMethod` list. It does not
75
+ * fit every caller. Portable objects (FEP-ef61) bind a key to an object
76
+ * structurally instead: {@link verifyPortableObjectProof} requires the
77
+ * verification method's FEP-ef34 origin to equal the object id's own
78
+ * authority, so the signer is pinned by the identifier being verified,
79
+ * before any key is fetched. Demanding the ActivityPub check as well would
80
+ * be impossible rather than merely redundant, since a DID dereferences to
81
+ * a DID document, which is not an actor.
82
+ *
83
+ * This is not a way to accept whatever a key says about itself. The claim
84
+ * still has to be corroborated, just from the key id rather than from
85
+ * a second document: a key may name an owner only when the key id is that
86
+ * owner's own (a `did:…#fragment` belongs to the DID it is a fragment of,
87
+ * exactly as a key embedded in an actor document belongs to that actor).
88
+ * A key naming anyone else falls back to the ordinary check and is rejected
89
+ * if it cannot pass it, so a key admitted here is as sound as any other and
90
+ * is cached like any other.
91
+ * @internal
92
+ */
93
+ keyIdBoundByCaller?: boolean;
62
94
  }
63
95
  /**
64
96
  * The result of {@link fetchKey}.
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { MeterProvider, TracerProvider } from "@opentelemetry/api";
2
+ import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
4
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -59,6 +59,38 @@ interface FetchKeyOptions {
59
59
  * @since 2.3.0
60
60
  */
61
61
  meterProvider?: MeterProvider;
62
+ /**
63
+ * An assertion by the caller that it has already bound this key's id to
64
+ * whoever the key will be treated as speaking for, by means of its own that
65
+ * key resolution cannot see.
66
+ *
67
+ * Key resolution normally settles ownership itself: it dereferences the
68
+ * `owner`/`controller` a key claims and requires that actor's own document
69
+ * to link back to the key, because the claim and the key document come from
70
+ * the same host and so the claim proves nothing on its own. See
71
+ * GHSA-q9f8-5hc7-898f.
72
+ *
73
+ * That check presumes the ActivityPub shape of ownership, where the owner
74
+ * is an actor carrying a `publicKey`/`assertionMethod` list. It does not
75
+ * fit every caller. Portable objects (FEP-ef61) bind a key to an object
76
+ * structurally instead: {@link verifyPortableObjectProof} requires the
77
+ * verification method's FEP-ef34 origin to equal the object id's own
78
+ * authority, so the signer is pinned by the identifier being verified,
79
+ * before any key is fetched. Demanding the ActivityPub check as well would
80
+ * be impossible rather than merely redundant, since a DID dereferences to
81
+ * a DID document, which is not an actor.
82
+ *
83
+ * This is not a way to accept whatever a key says about itself. The claim
84
+ * still has to be corroborated, just from the key id rather than from
85
+ * a second document: a key may name an owner only when the key id is that
86
+ * owner's own (a `did:…#fragment` belongs to the DID it is a fragment of,
87
+ * exactly as a key embedded in an actor document belongs to that actor).
88
+ * A key naming anyone else falls back to the ordinary check and is rejected
89
+ * if it cannot pass it, so a key admitted here is as sound as any other and
90
+ * is cached like any other.
91
+ * @internal
92
+ */
93
+ keyIdBoundByCaller?: boolean;
62
94
  }
63
95
  /**
64
96
  * The result of {@link fetchKey}.
@@ -4,13 +4,13 @@ import { getLogger } from "@logtape/logtape";
4
4
  import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
5
5
  import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
6
6
  import { encodeHex } from "byte-encodings/hex";
7
- import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
8
7
  import { FetchError, getDocumentLoader, parseDidKeyVerificationMethod } from "@fedify/vocab-runtime";
8
+ import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
9
9
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
10
10
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
11
11
  //#region deno.json
12
12
  var name = "@fedify/fedify";
13
- var version = "2.4.0-dev.1964+1d28dc03";
13
+ var version = "2.4.0-dev.1988+c5c9d617";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -1040,6 +1040,118 @@ async function importJwk(jwk, type) {
1040
1040
  validateCryptoKey(key, type);
1041
1041
  return key;
1042
1042
  }
1043
+ /**
1044
+ * Fetches the document that the given actor URI dereferences to, and returns
1045
+ * the actor only if the host that served the document is authoritative for
1046
+ * the id the document claims.
1047
+ *
1048
+ * Only the origin that serves an actor id can speak for it. Without that
1049
+ * rule any host could serve a document describing somebody else's actor—and
1050
+ * listing its own keys as that actor's.
1051
+ *
1052
+ * @param actorId The URI of the actor to fetch.
1053
+ * @param options Options for fetching the document.
1054
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
1055
+ * actor, or belongs to another origin.
1056
+ * @internal
1057
+ */
1058
+ async function fetchActorDocument(actorId, options = {}) {
1059
+ const logger = getLogger([
1060
+ "fedify",
1061
+ "sig",
1062
+ "key"
1063
+ ]);
1064
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
1065
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
1066
+ const { tracerProvider } = options;
1067
+ let document;
1068
+ let documentUrl = actorId;
1069
+ try {
1070
+ const remoteDocument = await documentLoader(actorId.href);
1071
+ document = remoteDocument.document;
1072
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
1073
+ } catch (error) {
1074
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
1075
+ actorId: actorId.href,
1076
+ error
1077
+ });
1078
+ return null;
1079
+ }
1080
+ let object;
1081
+ try {
1082
+ object = await Object$1.fromJsonLd(document, {
1083
+ documentLoader,
1084
+ contextLoader,
1085
+ tracerProvider,
1086
+ baseUrl: documentUrl
1087
+ });
1088
+ } catch (error) {
1089
+ if (!(error instanceof TypeError)) throw error;
1090
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
1091
+ documentUrl: documentUrl.href,
1092
+ error
1093
+ });
1094
+ return null;
1095
+ }
1096
+ if (!isActor(object)) return null;
1097
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1098
+ logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
1099
+ documentUrl: documentUrl.href,
1100
+ actorId: object.id?.href
1101
+ });
1102
+ return null;
1103
+ }
1104
+ return object;
1105
+ }
1106
+ /**
1107
+ * Resolves the owner that the given key claims, and returns it only if the
1108
+ * claim holds.
1109
+ *
1110
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
1111
+ * a {@link Multikey}, proves nothing on its own: the key document and the
1112
+ * claim inside it are served by the same host, so anyone able to serve a key
1113
+ * document can name any actor in the world as its owner. The claim becomes
1114
+ * meaningful only once the named actor's own document is fetched and turns
1115
+ * out to link back to the key. This function performs that mutual-link
1116
+ * check, which is the only sound way to learn a fetched key's owner.
1117
+ *
1118
+ * The claimed owner is always dereferenced, never read out of the key
1119
+ * document: an owner embedded there is written by whoever wrote the key.
1120
+ *
1121
+ * @param key The key whose ownership claim is to be verified. It must carry
1122
+ * an `id`, as that is what the owner has to link back to.
1123
+ * @param options Options for fetching the claimed owner.
1124
+ * @returns The verified owner, or `null` if the key claims no owner, the
1125
+ * owner cannot be fetched, or the owner does not link back to
1126
+ * the key.
1127
+ * @internal
1128
+ */
1129
+ async function verifyKeyOwnership(key, options = {}) {
1130
+ const logger = getLogger([
1131
+ "fedify",
1132
+ "sig",
1133
+ "key"
1134
+ ]);
1135
+ const keyId = key.id;
1136
+ if (keyId == null) return null;
1137
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1138
+ if (claimedOwnerId == null) return null;
1139
+ const owner = await fetchActorDocument(claimedOwnerId, options);
1140
+ if (owner == null) {
1141
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
1142
+ keyId: keyId.href,
1143
+ claimedOwnerId: claimedOwnerId.href
1144
+ });
1145
+ return null;
1146
+ }
1147
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
1148
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
1149
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
1150
+ keyId: keyId.href,
1151
+ claimedOwnerId: claimedOwnerId.href
1152
+ });
1153
+ return null;
1154
+ }
1043
1155
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1044
1156
  tracerProvider ??= trace.getTracerProvider();
1045
1157
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -1173,6 +1285,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
1173
1285
  async function clearFetchErrorMetadata(keyId, keyCache) {
1174
1286
  await keyCache?.setFetchError?.(keyId, null);
1175
1287
  }
1288
+ /**
1289
+ * Returns the identifier a fragment-bearing DID URL is a fragment of, or
1290
+ * `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
1291
+ * traced back to `did:web:example.com`, the DID that resolving the key went
1292
+ * through in the first place.
1293
+ */
1294
+ function stripFragment(keyId) {
1295
+ if (keyId.protocol !== "did:") return null;
1296
+ const did = new URL(keyId.href);
1297
+ did.hash = "";
1298
+ return did;
1299
+ }
1176
1300
  function isDidKeyUrl(keyId) {
1177
1301
  return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
1178
1302
  }
@@ -1228,7 +1352,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
1228
1352
  cached: false
1229
1353
  };
1230
1354
  }
1231
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1355
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
1232
1356
  let object;
1233
1357
  try {
1234
1358
  object = await Object$1.fromJsonLd(document, {
@@ -1258,8 +1382,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1258
1382
  }
1259
1383
  }
1260
1384
  let key = null;
1261
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
1385
+ let ownerDocument = null;
1386
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
1262
1387
  else if (isActor(object)) {
1388
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1389
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
1390
+ keyId,
1391
+ documentUrl: documentUrl.href,
1392
+ actorId: object.id?.href
1393
+ });
1394
+ await keyCache?.set(cacheKey, null);
1395
+ return {
1396
+ key: null,
1397
+ cached: false
1398
+ };
1399
+ }
1400
+ ownerDocument = object;
1263
1401
  const keys = cls === CryptographicKey ? object.getPublicKeys({
1264
1402
  documentLoader,
1265
1403
  contextLoader,
@@ -1321,6 +1459,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1321
1459
  cached: false
1322
1460
  };
1323
1461
  }
1462
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1463
+ if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
1464
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
1465
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
1466
+ if (await verifyKeyOwnership(key, {
1467
+ documentLoader,
1468
+ contextLoader,
1469
+ tracerProvider
1470
+ }) == null) {
1471
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
1472
+ keyId,
1473
+ claimedOwnerId: claimedOwnerId.href
1474
+ });
1475
+ await keyCache?.set(cacheKey, null);
1476
+ return {
1477
+ key: null,
1478
+ cached: false
1479
+ };
1480
+ }
1481
+ }
1482
+ }
1324
1483
  if (keyCache != null) {
1325
1484
  await keyCache.set(cacheKey, key);
1326
1485
  logger.debug("Key {keyId} cached.", { keyId });
@@ -1359,15 +1518,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1359
1518
  }
1360
1519
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1361
1520
  let document;
1521
+ let documentUrl = cacheKey;
1362
1522
  try {
1363
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
1523
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
1524
+ document = remoteDocument.document;
1525
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
1364
1526
  } catch (error) {
1365
1527
  const classified = classifyFetchError(error);
1366
1528
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1367
1529
  outcome = classified;
1368
1530
  return errored;
1369
1531
  }
1370
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1532
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
1371
1533
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1372
1534
  return resolved;
1373
1535
  } finally {
@@ -2482,4 +2644,4 @@ function timingSafeEqual(a, b) {
2482
2644
  return result === 0;
2483
2645
  }
2484
2646
  //#endregion
2485
- export { formatAcceptSignature as A, recordDocumentCache as C, recordOutboxEnqueue as D, recordOutboxActivity as E, version as F, parseAcceptSignature as M, validateAcceptSignature as N, recordWebFingerHandle as O, name as P, recordCollectionTotalItems as S, recordInboxActivity as T, measureSignatureKeyFetch as _, verifyRequestDetailed as a, recordCollectionPageItems as b, fetchKeyDetailed as c, validateCryptoKey as d, getDurationMs as f, isAbortError$1 as g, instrumentDocumentLoader as h, verifyRequest as i, fulfillAcceptSignature as j, registerQueueDepthGauge as k, generateCryptoKeyPair as l, getRemoteHost as m, parseRfc9421SignatureInput as n, exportJwk as o, getFederationMetrics as p, signRequest as r, fetchKey as s, doubleKnock as t, importJwk as u, recordCircuitBreakerStateChange as v, recordFanoutRecipients as w, recordCollectionRequest as x, recordCollectionDispatchDuration as y };
2647
+ export { recordWebFingerHandle as A, recordCollectionRequest as C, recordInboxActivity as D, recordFanoutRecipients as E, validateAcceptSignature as F, name as I, version as L, formatAcceptSignature as M, fulfillAcceptSignature as N, recordOutboxActivity as O, parseAcceptSignature as P, recordCollectionPageItems as S, recordDocumentCache as T, instrumentDocumentLoader as _, verifyRequestDetailed as a, recordCircuitBreakerStateChange as b, fetchKey as c, importJwk as d, validateCryptoKey as f, getRemoteHost as g, getFederationMetrics as h, verifyRequest as i, registerQueueDepthGauge as j, recordOutboxEnqueue as k, fetchKeyDetailed as l, getDurationMs as m, parseRfc9421SignatureInput as n, exportJwk as o, verifyKeyOwnership as p, signRequest as r, fetchActorDocument as s, doubleKnock as t, generateCryptoKeyPair as u, isAbortError$1 as v, recordCollectionTotalItems as w, recordCollectionDispatchDuration as x, measureSignatureKeyFetch as y };
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CtG93ohj.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-B9N27cCB.mjs";
4
+ import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CrqsHP0l.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
7
- import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-BVjgbs_9.mjs";
7
+ import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-Cwbg5ggV.mjs";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey } from "@fedify/vocab";
10
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";