@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
@@ -3,13 +3,13 @@ let _logtape_logtape = require("@logtape/logtape");
3
3
  let _fedify_vocab = require("@fedify/vocab");
4
4
  let _opentelemetry_api = require("@opentelemetry/api");
5
5
  let byte_encodings_hex = require("byte-encodings/hex");
6
- let structured_field_values = require("structured-field-values");
7
6
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
7
+ let structured_field_values = require("structured-field-values");
8
8
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
9
9
  let byte_encodings_base64 = require("byte-encodings/base64");
10
10
  //#region deno.json
11
11
  var name = "@fedify/fedify";
12
- var version = "2.4.0-dev.1964+1d28dc03";
12
+ var version = "2.4.0-dev.1988+c5c9d617";
13
13
  //#endregion
14
14
  //#region ../../node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/root.js
15
15
  const NativeTemporal = globalThis.Temporal;
@@ -4603,6 +4603,118 @@ async function importJwk(jwk, type) {
4603
4603
  validateCryptoKey(key, type);
4604
4604
  return key;
4605
4605
  }
4606
+ /**
4607
+ * Fetches the document that the given actor URI dereferences to, and returns
4608
+ * the actor only if the host that served the document is authoritative for
4609
+ * the id the document claims.
4610
+ *
4611
+ * Only the origin that serves an actor id can speak for it. Without that
4612
+ * rule any host could serve a document describing somebody else's actor—and
4613
+ * listing its own keys as that actor's.
4614
+ *
4615
+ * @param actorId The URI of the actor to fetch.
4616
+ * @param options Options for fetching the document.
4617
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
4618
+ * actor, or belongs to another origin.
4619
+ * @internal
4620
+ */
4621
+ async function fetchActorDocument(actorId, options = {}) {
4622
+ const logger = (0, _logtape_logtape.getLogger)([
4623
+ "fedify",
4624
+ "sig",
4625
+ "key"
4626
+ ]);
4627
+ const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
4628
+ const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
4629
+ const { tracerProvider } = options;
4630
+ let document;
4631
+ let documentUrl = actorId;
4632
+ try {
4633
+ const remoteDocument = await documentLoader(actorId.href);
4634
+ document = remoteDocument.document;
4635
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
4636
+ } catch (error) {
4637
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
4638
+ actorId: actorId.href,
4639
+ error
4640
+ });
4641
+ return null;
4642
+ }
4643
+ let object;
4644
+ try {
4645
+ object = await _fedify_vocab.Object.fromJsonLd(document, {
4646
+ documentLoader,
4647
+ contextLoader,
4648
+ tracerProvider,
4649
+ baseUrl: documentUrl
4650
+ });
4651
+ } catch (error) {
4652
+ if (!(error instanceof TypeError)) throw error;
4653
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
4654
+ documentUrl: documentUrl.href,
4655
+ error
4656
+ });
4657
+ return null;
4658
+ }
4659
+ if (!(0, _fedify_vocab.isActor)(object)) return null;
4660
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
4661
+ 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.", {
4662
+ documentUrl: documentUrl.href,
4663
+ actorId: object.id?.href
4664
+ });
4665
+ return null;
4666
+ }
4667
+ return object;
4668
+ }
4669
+ /**
4670
+ * Resolves the owner that the given key claims, and returns it only if the
4671
+ * claim holds.
4672
+ *
4673
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
4674
+ * a {@link Multikey}, proves nothing on its own: the key document and the
4675
+ * claim inside it are served by the same host, so anyone able to serve a key
4676
+ * document can name any actor in the world as its owner. The claim becomes
4677
+ * meaningful only once the named actor's own document is fetched and turns
4678
+ * out to link back to the key. This function performs that mutual-link
4679
+ * check, which is the only sound way to learn a fetched key's owner.
4680
+ *
4681
+ * The claimed owner is always dereferenced, never read out of the key
4682
+ * document: an owner embedded there is written by whoever wrote the key.
4683
+ *
4684
+ * @param key The key whose ownership claim is to be verified. It must carry
4685
+ * an `id`, as that is what the owner has to link back to.
4686
+ * @param options Options for fetching the claimed owner.
4687
+ * @returns The verified owner, or `null` if the key claims no owner, the
4688
+ * owner cannot be fetched, or the owner does not link back to
4689
+ * the key.
4690
+ * @internal
4691
+ */
4692
+ async function verifyKeyOwnership(key, options = {}) {
4693
+ const logger = (0, _logtape_logtape.getLogger)([
4694
+ "fedify",
4695
+ "sig",
4696
+ "key"
4697
+ ]);
4698
+ const keyId = key.id;
4699
+ if (keyId == null) return null;
4700
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
4701
+ if (claimedOwnerId == null) return null;
4702
+ const owner = await fetchActorDocument(claimedOwnerId, options);
4703
+ if (owner == null) {
4704
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
4705
+ keyId: keyId.href,
4706
+ claimedOwnerId: claimedOwnerId.href
4707
+ });
4708
+ return null;
4709
+ }
4710
+ const linkedKeyIds = key instanceof _fedify_vocab.CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
4711
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
4712
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
4713
+ keyId: keyId.href,
4714
+ claimedOwnerId: claimedOwnerId.href
4715
+ });
4716
+ return null;
4717
+ }
4606
4718
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
4607
4719
  tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
4608
4720
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -4736,6 +4848,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
4736
4848
  async function clearFetchErrorMetadata(keyId, keyCache) {
4737
4849
  await keyCache?.setFetchError?.(keyId, null);
4738
4850
  }
4851
+ /**
4852
+ * Returns the identifier a fragment-bearing DID URL is a fragment of, or
4853
+ * `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
4854
+ * traced back to `did:web:example.com`, the DID that resolving the key went
4855
+ * through in the first place.
4856
+ */
4857
+ function stripFragment(keyId) {
4858
+ if (keyId.protocol !== "did:") return null;
4859
+ const did = new URL(keyId.href);
4860
+ did.hash = "";
4861
+ return did;
4862
+ }
4739
4863
  function isDidKeyUrl(keyId) {
4740
4864
  return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
4741
4865
  }
@@ -4791,7 +4915,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
4791
4915
  cached: false
4792
4916
  };
4793
4917
  }
4794
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
4918
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
4795
4919
  let object;
4796
4920
  try {
4797
4921
  object = await _fedify_vocab.Object.fromJsonLd(document, {
@@ -4821,8 +4945,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
4821
4945
  }
4822
4946
  }
4823
4947
  let key = null;
4824
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
4948
+ let ownerDocument = null;
4949
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
4825
4950
  else if ((0, _fedify_vocab.isActor)(object)) {
4951
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
4952
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
4953
+ keyId,
4954
+ documentUrl: documentUrl.href,
4955
+ actorId: object.id?.href
4956
+ });
4957
+ await keyCache?.set(cacheKey, null);
4958
+ return {
4959
+ key: null,
4960
+ cached: false
4961
+ };
4962
+ }
4963
+ ownerDocument = object;
4826
4964
  const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
4827
4965
  documentLoader,
4828
4966
  contextLoader,
@@ -4884,6 +5022,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
4884
5022
  cached: false
4885
5023
  };
4886
5024
  }
5025
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
5026
+ if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
5027
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
5028
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
5029
+ if (await verifyKeyOwnership(key, {
5030
+ documentLoader,
5031
+ contextLoader,
5032
+ tracerProvider
5033
+ }) == null) {
5034
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
5035
+ keyId,
5036
+ claimedOwnerId: claimedOwnerId.href
5037
+ });
5038
+ await keyCache?.set(cacheKey, null);
5039
+ return {
5040
+ key: null,
5041
+ cached: false
5042
+ };
5043
+ }
5044
+ }
5045
+ }
4887
5046
  if (keyCache != null) {
4888
5047
  await keyCache.set(cacheKey, key);
4889
5048
  logger.debug("Key {keyId} cached.", { keyId });
@@ -4922,15 +5081,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
4922
5081
  }
4923
5082
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
4924
5083
  let document;
5084
+ let documentUrl = cacheKey;
4925
5085
  try {
4926
- document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
5086
+ const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
5087
+ document = remoteDocument.document;
5088
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
4927
5089
  } catch (error) {
4928
5090
  const classified = classifyFetchError(error);
4929
5091
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
4930
5092
  outcome = classified;
4931
5093
  return errored;
4932
5094
  }
4933
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
5095
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
4934
5096
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
4935
5097
  return resolved;
4936
5098
  } finally {
@@ -6064,6 +6226,12 @@ Object.defineProperty(exports, "exportJwk", {
6064
6226
  return exportJwk;
6065
6227
  }
6066
6228
  });
6229
+ Object.defineProperty(exports, "fetchActorDocument", {
6230
+ enumerable: true,
6231
+ get: function() {
6232
+ return fetchActorDocument;
6233
+ }
6234
+ });
6067
6235
  Object.defineProperty(exports, "fetchKey", {
6068
6236
  enumerable: true,
6069
6237
  get: function() {
@@ -6244,6 +6412,12 @@ Object.defineProperty(exports, "validateCryptoKey", {
6244
6412
  return validateCryptoKey;
6245
6413
  }
6246
6414
  });
6415
+ Object.defineProperty(exports, "verifyKeyOwnership", {
6416
+ enumerable: true,
6417
+ get: function() {
6418
+ return verifyKeyOwnership;
6419
+ }
6420
+ });
6247
6421
  Object.defineProperty(exports, "verifyRequest", {
6248
6422
  enumerable: true,
6249
6423
  get: function() {
@@ -1,8 +1,8 @@
1
1
  import "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 { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-B9N27cCB.mjs";
4
+ import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-CrqsHP0l.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -86,6 +86,118 @@ async function importJwk(jwk, type) {
86
86
  validateCryptoKey(key, type);
87
87
  return key;
88
88
  }
89
+ /**
90
+ * Fetches the document that the given actor URI dereferences to, and returns
91
+ * the actor only if the host that served the document is authoritative for
92
+ * the id the document claims.
93
+ *
94
+ * Only the origin that serves an actor id can speak for it. Without that
95
+ * rule any host could serve a document describing somebody else's actor—and
96
+ * listing its own keys as that actor's.
97
+ *
98
+ * @param actorId The URI of the actor to fetch.
99
+ * @param options Options for fetching the document.
100
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
101
+ * actor, or belongs to another origin.
102
+ * @internal
103
+ */
104
+ async function fetchActorDocument(actorId, options = {}) {
105
+ const logger = getLogger([
106
+ "fedify",
107
+ "sig",
108
+ "key"
109
+ ]);
110
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
111
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
112
+ const { tracerProvider } = options;
113
+ let document;
114
+ let documentUrl = actorId;
115
+ try {
116
+ const remoteDocument = await documentLoader(actorId.href);
117
+ document = remoteDocument.document;
118
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
119
+ } catch (error) {
120
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
121
+ actorId: actorId.href,
122
+ error
123
+ });
124
+ return null;
125
+ }
126
+ let object;
127
+ try {
128
+ object = await Object$1.fromJsonLd(document, {
129
+ documentLoader,
130
+ contextLoader,
131
+ tracerProvider,
132
+ baseUrl: documentUrl
133
+ });
134
+ } catch (error) {
135
+ if (!(error instanceof TypeError)) throw error;
136
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
137
+ documentUrl: documentUrl.href,
138
+ error
139
+ });
140
+ return null;
141
+ }
142
+ if (!isActor(object)) return null;
143
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
144
+ 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.", {
145
+ documentUrl: documentUrl.href,
146
+ actorId: object.id?.href
147
+ });
148
+ return null;
149
+ }
150
+ return object;
151
+ }
152
+ /**
153
+ * Resolves the owner that the given key claims, and returns it only if the
154
+ * claim holds.
155
+ *
156
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
157
+ * a {@link Multikey}, proves nothing on its own: the key document and the
158
+ * claim inside it are served by the same host, so anyone able to serve a key
159
+ * document can name any actor in the world as its owner. The claim becomes
160
+ * meaningful only once the named actor's own document is fetched and turns
161
+ * out to link back to the key. This function performs that mutual-link
162
+ * check, which is the only sound way to learn a fetched key's owner.
163
+ *
164
+ * The claimed owner is always dereferenced, never read out of the key
165
+ * document: an owner embedded there is written by whoever wrote the key.
166
+ *
167
+ * @param key The key whose ownership claim is to be verified. It must carry
168
+ * an `id`, as that is what the owner has to link back to.
169
+ * @param options Options for fetching the claimed owner.
170
+ * @returns The verified owner, or `null` if the key claims no owner, the
171
+ * owner cannot be fetched, or the owner does not link back to
172
+ * the key.
173
+ * @internal
174
+ */
175
+ async function verifyKeyOwnership(key, options = {}) {
176
+ const logger = getLogger([
177
+ "fedify",
178
+ "sig",
179
+ "key"
180
+ ]);
181
+ const keyId = key.id;
182
+ if (keyId == null) return null;
183
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
184
+ if (claimedOwnerId == null) return null;
185
+ const owner = await fetchActorDocument(claimedOwnerId, options);
186
+ if (owner == null) {
187
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
188
+ keyId: keyId.href,
189
+ claimedOwnerId: claimedOwnerId.href
190
+ });
191
+ return null;
192
+ }
193
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
194
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
195
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
196
+ keyId: keyId.href,
197
+ claimedOwnerId: claimedOwnerId.href
198
+ });
199
+ return null;
200
+ }
89
201
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
90
202
  tracerProvider ??= trace.getTracerProvider();
91
203
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -219,6 +331,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
219
331
  async function clearFetchErrorMetadata(keyId, keyCache) {
220
332
  await keyCache?.setFetchError?.(keyId, null);
221
333
  }
334
+ /**
335
+ * Returns the identifier a fragment-bearing DID URL is a fragment of, or
336
+ * `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
337
+ * traced back to `did:web:example.com`, the DID that resolving the key went
338
+ * through in the first place.
339
+ */
340
+ function stripFragment(keyId) {
341
+ if (keyId.protocol !== "did:") return null;
342
+ const did = new URL(keyId.href);
343
+ did.hash = "";
344
+ return did;
345
+ }
222
346
  function isDidKeyUrl(keyId) {
223
347
  return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
224
348
  }
@@ -274,7 +398,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
274
398
  cached: false
275
399
  };
276
400
  }
277
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
401
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
278
402
  let object;
279
403
  try {
280
404
  object = await Object$1.fromJsonLd(document, {
@@ -304,8 +428,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
304
428
  }
305
429
  }
306
430
  let key = null;
307
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
431
+ let ownerDocument = null;
432
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
308
433
  else if (isActor(object)) {
434
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
435
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
436
+ keyId,
437
+ documentUrl: documentUrl.href,
438
+ actorId: object.id?.href
439
+ });
440
+ await keyCache?.set(cacheKey, null);
441
+ return {
442
+ key: null,
443
+ cached: false
444
+ };
445
+ }
446
+ ownerDocument = object;
309
447
  const keys = cls === CryptographicKey ? object.getPublicKeys({
310
448
  documentLoader,
311
449
  contextLoader,
@@ -367,6 +505,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
367
505
  cached: false
368
506
  };
369
507
  }
508
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
509
+ if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
510
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
511
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
512
+ if (await verifyKeyOwnership(key, {
513
+ documentLoader,
514
+ contextLoader,
515
+ tracerProvider
516
+ }) == null) {
517
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
518
+ keyId,
519
+ claimedOwnerId: claimedOwnerId.href
520
+ });
521
+ await keyCache?.set(cacheKey, null);
522
+ return {
523
+ key: null,
524
+ cached: false
525
+ };
526
+ }
527
+ }
528
+ }
370
529
  if (keyCache != null) {
371
530
  await keyCache.set(cacheKey, key);
372
531
  logger.debug("Key {keyId} cached.", { keyId });
@@ -405,15 +564,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
405
564
  }
406
565
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
407
566
  let document;
567
+ let documentUrl = cacheKey;
408
568
  try {
409
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
569
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
570
+ document = remoteDocument.document;
571
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
410
572
  } catch (error) {
411
573
  const classified = classifyFetchError(error);
412
574
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
413
575
  outcome = classified;
414
576
  return errored;
415
577
  }
416
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
578
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
417
579
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
418
580
  return resolved;
419
581
  } finally {
@@ -450,4 +612,4 @@ async function fetchKeyInternal(keyId, cls, options = {}) {
450
612
  });
451
613
  }
452
614
  //#endregion
453
- export { importJwk as a, generateCryptoKeyPair as i, fetchKey as n, validateCryptoKey as o, fetchKeyDetailed as r, exportJwk as t };
615
+ export { generateCryptoKeyPair as a, verifyKeyOwnership as c, fetchKeyDetailed as i, fetchActorDocument as n, importJwk as o, fetchKey as r, validateCryptoKey as s, exportJwk as t };
@@ -3,6 +3,7 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { CryptographicKey, Multikey } from "@fedify/vocab";
5
5
  //#region src/federation/keycache.ts
6
+ const KEY_CACHE_GENERATION = "2";
6
7
  var KvKeyCache = class {
7
8
  kv;
8
9
  prefix;
@@ -25,13 +26,20 @@ var KvKeyCache = class {
25
26
  keyId.href
26
27
  ];
27
28
  }
29
+ #entryKey(keyId) {
30
+ return [
31
+ ...this.prefix,
32
+ KEY_CACHE_GENERATION,
33
+ keyId.href
34
+ ];
35
+ }
28
36
  async get(keyId) {
29
37
  const negativeExpiration = this.nullKeys.get(keyId.href);
30
38
  if (negativeExpiration != null) {
31
39
  if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
32
40
  this.nullKeys.delete(keyId.href);
33
41
  }
34
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
42
+ const serialized = await this.kv.get(this.#entryKey(keyId));
35
43
  if (serialized === void 0) return void 0;
36
44
  if (serialized === null) {
37
45
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
@@ -43,7 +51,7 @@ var KvKeyCache = class {
43
51
  try {
44
52
  return await Multikey.fromJsonLd(serialized, this.options);
45
53
  } catch {
46
- await this.kv.delete([...this.prefix, keyId.href]);
54
+ await this.kv.delete(this.#entryKey(keyId));
47
55
  return;
48
56
  }
49
57
  }
@@ -51,12 +59,12 @@ var KvKeyCache = class {
51
59
  async set(keyId, key) {
52
60
  if (key == null) {
53
61
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
54
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
62
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
55
63
  return;
56
64
  }
57
65
  this.nullKeys.delete(keyId.href);
58
66
  const serialized = await key.toJsonLd(this.options);
59
- await this.kv.set([...this.prefix, keyId.href], serialized, { ttl: this.keyTtl });
67
+ await this.kv.set(this.#entryKey(keyId), serialized, { ttl: this.keyTtl });
60
68
  }
61
69
  async getFetchError(keyId) {
62
70
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -1,8 +1,8 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
- const require_http = require("./http-B3skRvGo.cjs");
2
+ const require_http = require("./http-Z3anvCd-.cjs");
3
3
  let _logtape_logtape = require("@logtape/logtape");
4
- let es_toolkit = require("es-toolkit");
5
4
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
5
+ let es_toolkit = require("es-toolkit");
6
6
  //#region src/utils/docloader.ts
7
7
  const logger$1 = (0, _logtape_logtape.getLogger)([
8
8
  "fedify",
@@ -1,9 +1,9 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { C as recordDocumentCache, d as validateCryptoKey, t as doubleKnock } from "./http-Dj4s1UQ0.js";
3
+ import { T as recordDocumentCache, f as validateCryptoKey, t as doubleKnock } from "./http-CgwqtxAy.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
- import { curry } from "es-toolkit";
6
5
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
6
+ import { curry } from "es-toolkit";
7
7
  //#region src/utils/docloader.ts
8
8
  const logger$1 = getLogger([
9
9
  "fedify",
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-B9N27cCB.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-CrqsHP0l.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/kv-cache.ts
@@ -1,9 +1,9 @@
1
1
  import "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";
6
- import { n as fetchKey, o as validateCryptoKey } from "./key-BVjgbs_9.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
+ import { r as fetchKey, s as validateCryptoKey } from "./key-Cwbg5ggV.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
9
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CtG93ohj.mjs";
4
+ import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { FetchError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/metrics.ts
@@ -1,5 +1,5 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as FederationImpl } from "./middleware-TeLjzoxn.mjs";
4
+ import { n as FederationImpl } from "./middleware-DCpiAsZi.mjs";
5
5
  export { FederationImpl };