@fedify/fedify 2.2.12 → 2.2.14-dev.2082

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 (102) hide show
  1. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert-BhQd8QHP.mjs} +19 -1
  2. package/dist/{builder-TJzcpmDm.mjs → builder-Cwgo1nK5.mjs} +3 -3
  3. package/dist/{client-dx126nMS.mjs → client-CQ7zd8tW.mjs} +78 -4
  4. package/dist/compat/mod.d.cts +1 -1
  5. package/dist/compat/mod.d.ts +1 -1
  6. package/dist/compat/outgoing-jsonld.test.mjs +1 -1
  7. package/dist/compat/public-audience.test.mjs +1 -1
  8. package/dist/compat/transformers.test.mjs +3 -4
  9. package/dist/{context-BU-1O90h.d.ts → context-Bt2jnbC7.d.ts} +4 -3
  10. package/dist/{context-DVA8wHZ0.d.cts → context-Cls_M1HU.d.cts} +4 -3
  11. package/dist/{deno-DeYUdFal.mjs → deno-CV4wsQzy.mjs} +1 -1
  12. package/dist/{docloader-CXH2XqYP.mjs → docloader-DV9F08Dn.mjs} +2 -2
  13. package/dist/federation/builder.test.mjs +2 -2
  14. package/dist/federation/collection.test.mjs +1 -1
  15. package/dist/federation/handler.test.mjs +127 -10
  16. package/dist/federation/idempotency.test.mjs +4 -4
  17. package/dist/federation/inbox.test.mjs +1 -1
  18. package/dist/federation/keycache.test.mjs +44 -10
  19. package/dist/federation/kv.test.mjs +1 -1
  20. package/dist/federation/middleware.test.mjs +103 -15
  21. package/dist/federation/mod.cjs +1 -1
  22. package/dist/federation/mod.d.cts +2 -2
  23. package/dist/federation/mod.d.ts +2 -2
  24. package/dist/federation/mod.js +1 -1
  25. package/dist/federation/mq.test.mjs +1 -1
  26. package/dist/federation/negotiation.test.mjs +2 -2
  27. package/dist/federation/retry.test.mjs +1 -1
  28. package/dist/federation/router.test.mjs +1 -1
  29. package/dist/federation/send.test.mjs +138 -6
  30. package/dist/federation/temporal.test.mjs +2 -2
  31. package/dist/federation/webfinger.test.mjs +2 -2
  32. package/dist/{http-D6cIKrI1.cjs → http-BWNuy1sb.cjs} +166 -6
  33. package/dist/{http-CNsxTbVr.js → http-C5T-lWqB.js} +155 -7
  34. package/dist/{http-79lZeYEo.mjs → http-C9MzFrOj.mjs} +3 -3
  35. package/dist/{http-BDZeS5om.d.ts → http-Dl00HXr4.d.ts} +1 -1
  36. package/dist/{http-C87EWkO0.d.cts → http-DqZhyEso.d.cts} +1 -1
  37. package/dist/{key-B4kUuy3o.mjs → key-DiziIx9e.mjs} +154 -6
  38. package/dist/{keycache-EGATflN-.mjs → keycache-C1aVprwy.mjs} +12 -4
  39. package/dist/{kv-cache-CIZ1m-94.js → kv-cache-CM-zJoep.js} +1 -1
  40. package/dist/{kv-cache-BtXEUvHT.cjs → kv-cache-DGPEHc1T.cjs} +1 -1
  41. package/dist/{ld-2R9vZ_ym.mjs → ld-BAeuDKjx.mjs} +2 -2
  42. package/dist/{middleware-CjrIRAsQ.cjs → middleware-BbbKc-Tr.cjs} +90 -14
  43. package/dist/{middleware-D5NE6PkU.mjs → middleware-CkJOjZjS.mjs} +31 -20
  44. package/dist/{middleware-_qt3kHUU.mjs → middleware-DA2CvTQL.mjs} +1 -1
  45. package/dist/{middleware-DsEAPFhi.js → middleware-J32whqNP.js} +90 -14
  46. package/dist/{middleware-2bSnAJOQ.cjs → middleware-XNvq3NUZ.cjs} +1 -1
  47. package/dist/{mod-BhU_H1I_.d.ts → mod-0MWo2Nu5.d.ts} +1 -1
  48. package/dist/{mod-DXY9JF28.d.cts → mod-D2JyWtW3.d.cts} +1 -1
  49. package/dist/{mod-q-NFLW6B.d.cts → mod-DNvnDTS7.d.cts} +1 -1
  50. package/dist/{mod-BVt6iTmH.d.ts → mod-DaiJDJdQ.d.ts} +1 -1
  51. package/dist/{mod-B0rWmfW5.d.cts → mod-DzPMtIV3.d.cts} +1 -1
  52. package/dist/{mod-DHO9lk3D.d.ts → mod-ej_rLx8h.d.ts} +1 -1
  53. package/dist/mod.cjs +5 -5
  54. package/dist/mod.d.cts +6 -6
  55. package/dist/mod.d.ts +6 -6
  56. package/dist/mod.js +5 -5
  57. package/dist/nodeinfo/client.test.mjs +28 -1
  58. package/dist/nodeinfo/handler.test.mjs +2 -2
  59. package/dist/nodeinfo/mod.cjs +1 -1
  60. package/dist/nodeinfo/mod.js +1 -1
  61. package/dist/nodeinfo/types.test.mjs +1 -1
  62. package/dist/otel/exporter.test.mjs +1 -1
  63. package/dist/{outgoing-jsonld-CfUItkGW.mjs → outgoing-jsonld-Dc2OXyB-.mjs} +1 -1
  64. package/dist/{owner-CnngXDNJ.d.ts → owner-B3hNQCG8.d.cts} +12 -1
  65. package/dist/{owner-DEvZuyOE.d.cts → owner-CiBPcC6F.d.ts} +12 -1
  66. package/dist/owner-D08_JUSb.mjs +175 -0
  67. package/dist/{proof-CmtXtaul.cjs → proof-7T47tIu5.cjs} +112 -58
  68. package/dist/{proof-BhW_iARR.js → proof-BdRnLnh7.js} +112 -58
  69. package/dist/{proof-BzTt6nlk.mjs → proof-_S-wFUFM.mjs} +4 -4
  70. package/dist/{send-CZwOs7XD.mjs → send-DmcKKipr.mjs} +64 -6
  71. package/dist/sig/accept.test.mjs +1 -1
  72. package/dist/sig/http.test.mjs +4 -4
  73. package/dist/sig/key.test.mjs +170 -3
  74. package/dist/sig/ld.test.mjs +56 -4
  75. package/dist/sig/mod.cjs +2 -2
  76. package/dist/sig/mod.d.cts +3 -3
  77. package/dist/sig/mod.d.ts +3 -3
  78. package/dist/sig/mod.js +2 -2
  79. package/dist/sig/owner.test.mjs +105 -6
  80. package/dist/sig/proof.test.mjs +73 -5
  81. package/dist/{temporal-BYCnXIGk.mjs → temporal-CX_L28b-.mjs} +1 -1
  82. package/dist/{types-m8u9L86X.cjs → types-DWs-qZaY.cjs} +94 -2
  83. package/dist/{types-DTONwNqL.js → types-sXITXcWa.js} +78 -4
  84. package/dist/utils/docloader.test.mjs +28 -4
  85. package/dist/utils/kv-cache.test.mjs +1 -1
  86. package/dist/utils/mod.cjs +1 -1
  87. package/dist/utils/mod.d.cts +1 -1
  88. package/dist/utils/mod.d.ts +1 -1
  89. package/dist/utils/mod.js +1 -1
  90. package/package.json +5 -5
  91. package/dist/assert-DikXweDx.mjs +0 -23
  92. package/dist/owner-CqYsVXcd.mjs +0 -120
  93. /package/dist/{accept-CPkZzmGN.mjs → accept-Dd__NiUL.mjs} +0 -0
  94. /package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-IojJ9SVl.mjs} +0 -0
  95. /package/dist/{collection-D-HqUuA2.mjs → collection-DnoYNwBC.mjs} +0 -0
  96. /package/dist/{keys-DGu1NFwu.mjs → keys-BAK-tUlf.mjs} +0 -0
  97. /package/dist/{kv-rV3vodCc.mjs → kv-CX8qfaad.mjs} +0 -0
  98. /package/dist/{kv-cache-DYsF2MhP.mjs → kv-cache-FsT1ljhs.mjs} +0 -0
  99. /package/dist/{negotiation-SQvQgUqe.mjs → negotiation-BHtZN6Y1.mjs} +0 -0
  100. /package/dist/{public-audience-Ci-r-4Q-.mjs → public-audience-CS1udGkN.mjs} +0 -0
  101. /package/dist/{retry-v_sGLH1d.mjs → retry-bMXBL97A.mjs} +0 -0
  102. /package/dist/{types-J53Kw7so.mjs → types-s4mCTGkT.mjs} +0 -0
@@ -5,13 +5,13 @@ let _logtape_logtape = require("@logtape/logtape");
5
5
  let _fedify_vocab = require("@fedify/vocab");
6
6
  let _opentelemetry_api = require("@opentelemetry/api");
7
7
  let byte_encodings_hex = require("byte-encodings/hex");
8
- let structured_field_values = require("structured-field-values");
9
8
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
9
+ let structured_field_values = require("structured-field-values");
10
10
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
11
11
  let byte_encodings_base64 = require("byte-encodings/base64");
12
12
  //#region deno.json
13
13
  var name = "@fedify/fedify";
14
- var version = "2.2.12";
14
+ var version = "2.2.14-dev.2082+79cd52e9";
15
15
  //#endregion
16
16
  //#region src/sig/accept.ts
17
17
  /**
@@ -231,6 +231,118 @@ async function importJwk(jwk, type) {
231
231
  validateCryptoKey(key, type);
232
232
  return key;
233
233
  }
234
+ /**
235
+ * Fetches the document that the given actor URI dereferences to, and returns
236
+ * the actor only if the host that served the document is authoritative for
237
+ * the id the document claims.
238
+ *
239
+ * Only the origin that serves an actor id can speak for it. Without that
240
+ * rule any host could serve a document describing somebody else's actor—and
241
+ * listing its own keys as that actor's.
242
+ *
243
+ * @param actorId The URI of the actor to fetch.
244
+ * @param options Options for fetching the document.
245
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
246
+ * actor, or belongs to another origin.
247
+ * @internal
248
+ */
249
+ async function fetchActorDocument(actorId, options = {}) {
250
+ const logger = (0, _logtape_logtape.getLogger)([
251
+ "fedify",
252
+ "sig",
253
+ "key"
254
+ ]);
255
+ const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
256
+ const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
257
+ const { tracerProvider } = options;
258
+ let document;
259
+ let documentUrl = actorId;
260
+ try {
261
+ const remoteDocument = await documentLoader(actorId.href);
262
+ document = remoteDocument.document;
263
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
264
+ } catch (error) {
265
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
266
+ actorId: actorId.href,
267
+ error
268
+ });
269
+ return null;
270
+ }
271
+ let object;
272
+ try {
273
+ object = await _fedify_vocab.Object.fromJsonLd(document, {
274
+ documentLoader,
275
+ contextLoader,
276
+ tracerProvider,
277
+ baseUrl: documentUrl
278
+ });
279
+ } catch (error) {
280
+ if (!(error instanceof TypeError)) throw error;
281
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
282
+ documentUrl: documentUrl.href,
283
+ error
284
+ });
285
+ return null;
286
+ }
287
+ if (!(0, _fedify_vocab.isActor)(object)) return null;
288
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
289
+ 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.", {
290
+ documentUrl: documentUrl.href,
291
+ actorId: object.id?.href
292
+ });
293
+ return null;
294
+ }
295
+ return object;
296
+ }
297
+ /**
298
+ * Resolves the owner that the given key claims, and returns it only if the
299
+ * claim holds.
300
+ *
301
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
302
+ * a {@link Multikey}, proves nothing on its own: the key document and the
303
+ * claim inside it are served by the same host, so anyone able to serve a key
304
+ * document can name any actor in the world as its owner. The claim becomes
305
+ * meaningful only once the named actor's own document is fetched and turns
306
+ * out to link back to the key. This function performs that mutual-link
307
+ * check, which is the only sound way to learn a fetched key's owner.
308
+ *
309
+ * The claimed owner is always dereferenced, never read out of the key
310
+ * document: an owner embedded there is written by whoever wrote the key.
311
+ *
312
+ * @param key The key whose ownership claim is to be verified. It must carry
313
+ * an `id`, as that is what the owner has to link back to.
314
+ * @param options Options for fetching the claimed owner.
315
+ * @returns The verified owner, or `null` if the key claims no owner, the
316
+ * owner cannot be fetched, or the owner does not link back to
317
+ * the key.
318
+ * @internal
319
+ */
320
+ async function verifyKeyOwnership(key, options = {}) {
321
+ const logger = (0, _logtape_logtape.getLogger)([
322
+ "fedify",
323
+ "sig",
324
+ "key"
325
+ ]);
326
+ const keyId = key.id;
327
+ if (keyId == null) return null;
328
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
329
+ if (claimedOwnerId == null) return null;
330
+ const owner = await fetchActorDocument(claimedOwnerId, options);
331
+ if (owner == null) {
332
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
333
+ keyId: keyId.href,
334
+ claimedOwnerId: claimedOwnerId.href
335
+ });
336
+ return null;
337
+ }
338
+ const linkedKeyIds = key instanceof _fedify_vocab.CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
339
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
340
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
341
+ keyId: keyId.href,
342
+ claimedOwnerId: claimedOwnerId.href
343
+ });
344
+ return null;
345
+ }
234
346
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
235
347
  tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
236
348
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -358,7 +470,7 @@ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
358
470
  async function clearFetchErrorMetadata(keyId, keyCache) {
359
471
  await keyCache?.setFetchError?.(keyId, null);
360
472
  }
361
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
473
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
362
474
  let object;
363
475
  try {
364
476
  object = await _fedify_vocab.Object.fromJsonLd(document, {
@@ -388,8 +500,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
388
500
  }
389
501
  }
390
502
  let key = null;
391
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
503
+ let ownerDocument = null;
504
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
392
505
  else if ((0, _fedify_vocab.isActor)(object)) {
506
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
507
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
508
+ keyId,
509
+ documentUrl: documentUrl.href,
510
+ actorId: object.id?.href
511
+ });
512
+ await keyCache?.set(cacheKey, null);
513
+ return {
514
+ key: null,
515
+ cached: false
516
+ };
517
+ }
518
+ ownerDocument = object;
393
519
  const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
394
520
  documentLoader,
395
521
  contextLoader,
@@ -451,6 +577,25 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
451
577
  cached: false
452
578
  };
453
579
  }
580
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
581
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
582
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
583
+ if (await verifyKeyOwnership(key, {
584
+ documentLoader,
585
+ contextLoader,
586
+ tracerProvider
587
+ }) == null) {
588
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
589
+ keyId,
590
+ claimedOwnerId: claimedOwnerId.href
591
+ });
592
+ await keyCache?.set(cacheKey, null);
593
+ return {
594
+ key: null,
595
+ cached: false
596
+ };
597
+ }
598
+ }
454
599
  if (keyCache != null) {
455
600
  await keyCache.set(cacheKey, key);
456
601
  logger.debug("Key {keyId} cached.", { keyId });
@@ -474,12 +619,15 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
474
619
  if (cached != null) return cached;
475
620
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
476
621
  let document;
622
+ let documentUrl = cacheKey;
477
623
  try {
478
- document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
624
+ const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
625
+ document = remoteDocument.document;
626
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
479
627
  } catch (error) {
480
628
  return await onFetchError(error, cacheKey, keyId, keyCache, logger);
481
629
  }
482
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
630
+ return await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
483
631
  }
484
632
  async function fetchKeyInternal(keyId, cls, options = {}) {
485
633
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -1545,6 +1693,12 @@ Object.defineProperty(exports, "exportJwk", {
1545
1693
  return exportJwk;
1546
1694
  }
1547
1695
  });
1696
+ Object.defineProperty(exports, "fetchActorDocument", {
1697
+ enumerable: true,
1698
+ get: function() {
1699
+ return fetchActorDocument;
1700
+ }
1701
+ });
1548
1702
  Object.defineProperty(exports, "fetchKey", {
1549
1703
  enumerable: true,
1550
1704
  get: function() {
@@ -1617,6 +1771,12 @@ Object.defineProperty(exports, "validateCryptoKey", {
1617
1771
  return validateCryptoKey;
1618
1772
  }
1619
1773
  });
1774
+ Object.defineProperty(exports, "verifyKeyOwnership", {
1775
+ enumerable: true,
1776
+ get: function() {
1777
+ return verifyKeyOwnership;
1778
+ }
1779
+ });
1620
1780
  Object.defineProperty(exports, "verifyRequest", {
1621
1781
  enumerable: true,
1622
1782
  get: function() {
@@ -4,13 +4,13 @@ import { getLogger } from "@logtape/logtape";
4
4
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
5
5
  import { SpanKind, SpanStatusCode, 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 } 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.2.12";
13
+ var version = "2.2.14-dev.2082+79cd52e9";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -230,6 +230,118 @@ async function importJwk(jwk, type) {
230
230
  validateCryptoKey(key, type);
231
231
  return key;
232
232
  }
233
+ /**
234
+ * Fetches the document that the given actor URI dereferences to, and returns
235
+ * the actor only if the host that served the document is authoritative for
236
+ * the id the document claims.
237
+ *
238
+ * Only the origin that serves an actor id can speak for it. Without that
239
+ * rule any host could serve a document describing somebody else's actor—and
240
+ * listing its own keys as that actor's.
241
+ *
242
+ * @param actorId The URI of the actor to fetch.
243
+ * @param options Options for fetching the document.
244
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
245
+ * actor, or belongs to another origin.
246
+ * @internal
247
+ */
248
+ async function fetchActorDocument(actorId, options = {}) {
249
+ const logger = getLogger([
250
+ "fedify",
251
+ "sig",
252
+ "key"
253
+ ]);
254
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
255
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
256
+ const { tracerProvider } = options;
257
+ let document;
258
+ let documentUrl = actorId;
259
+ try {
260
+ const remoteDocument = await documentLoader(actorId.href);
261
+ document = remoteDocument.document;
262
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
263
+ } catch (error) {
264
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
265
+ actorId: actorId.href,
266
+ error
267
+ });
268
+ return null;
269
+ }
270
+ let object;
271
+ try {
272
+ object = await Object$1.fromJsonLd(document, {
273
+ documentLoader,
274
+ contextLoader,
275
+ tracerProvider,
276
+ baseUrl: documentUrl
277
+ });
278
+ } catch (error) {
279
+ if (!(error instanceof TypeError)) throw error;
280
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
281
+ documentUrl: documentUrl.href,
282
+ error
283
+ });
284
+ return null;
285
+ }
286
+ if (!isActor(object)) return null;
287
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
288
+ 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.", {
289
+ documentUrl: documentUrl.href,
290
+ actorId: object.id?.href
291
+ });
292
+ return null;
293
+ }
294
+ return object;
295
+ }
296
+ /**
297
+ * Resolves the owner that the given key claims, and returns it only if the
298
+ * claim holds.
299
+ *
300
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
301
+ * a {@link Multikey}, proves nothing on its own: the key document and the
302
+ * claim inside it are served by the same host, so anyone able to serve a key
303
+ * document can name any actor in the world as its owner. The claim becomes
304
+ * meaningful only once the named actor's own document is fetched and turns
305
+ * out to link back to the key. This function performs that mutual-link
306
+ * check, which is the only sound way to learn a fetched key's owner.
307
+ *
308
+ * The claimed owner is always dereferenced, never read out of the key
309
+ * document: an owner embedded there is written by whoever wrote the key.
310
+ *
311
+ * @param key The key whose ownership claim is to be verified. It must carry
312
+ * an `id`, as that is what the owner has to link back to.
313
+ * @param options Options for fetching the claimed owner.
314
+ * @returns The verified owner, or `null` if the key claims no owner, the
315
+ * owner cannot be fetched, or the owner does not link back to
316
+ * the key.
317
+ * @internal
318
+ */
319
+ async function verifyKeyOwnership(key, options = {}) {
320
+ const logger = getLogger([
321
+ "fedify",
322
+ "sig",
323
+ "key"
324
+ ]);
325
+ const keyId = key.id;
326
+ if (keyId == null) return null;
327
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
328
+ if (claimedOwnerId == null) return null;
329
+ const owner = await fetchActorDocument(claimedOwnerId, options);
330
+ if (owner == null) {
331
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
332
+ keyId: keyId.href,
333
+ claimedOwnerId: claimedOwnerId.href
334
+ });
335
+ return null;
336
+ }
337
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
338
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
339
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
340
+ keyId: keyId.href,
341
+ claimedOwnerId: claimedOwnerId.href
342
+ });
343
+ return null;
344
+ }
233
345
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
234
346
  tracerProvider ??= trace.getTracerProvider();
235
347
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -357,7 +469,7 @@ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
357
469
  async function clearFetchErrorMetadata(keyId, keyCache) {
358
470
  await keyCache?.setFetchError?.(keyId, null);
359
471
  }
360
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
472
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
361
473
  let object;
362
474
  try {
363
475
  object = await Object$1.fromJsonLd(document, {
@@ -387,8 +499,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
387
499
  }
388
500
  }
389
501
  let key = null;
390
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
502
+ let ownerDocument = null;
503
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
391
504
  else if (isActor(object)) {
505
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
506
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
507
+ keyId,
508
+ documentUrl: documentUrl.href,
509
+ actorId: object.id?.href
510
+ });
511
+ await keyCache?.set(cacheKey, null);
512
+ return {
513
+ key: null,
514
+ cached: false
515
+ };
516
+ }
517
+ ownerDocument = object;
392
518
  const keys = cls === CryptographicKey ? object.getPublicKeys({
393
519
  documentLoader,
394
520
  contextLoader,
@@ -450,6 +576,25 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
450
576
  cached: false
451
577
  };
452
578
  }
579
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
580
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
581
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
582
+ if (await verifyKeyOwnership(key, {
583
+ documentLoader,
584
+ contextLoader,
585
+ tracerProvider
586
+ }) == null) {
587
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
588
+ keyId,
589
+ claimedOwnerId: claimedOwnerId.href
590
+ });
591
+ await keyCache?.set(cacheKey, null);
592
+ return {
593
+ key: null,
594
+ cached: false
595
+ };
596
+ }
597
+ }
453
598
  if (keyCache != null) {
454
599
  await keyCache.set(cacheKey, key);
455
600
  logger.debug("Key {keyId} cached.", { keyId });
@@ -473,12 +618,15 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
473
618
  if (cached != null) return cached;
474
619
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
475
620
  let document;
621
+ let documentUrl = cacheKey;
476
622
  try {
477
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
623
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
624
+ document = remoteDocument.document;
625
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
478
626
  } catch (error) {
479
627
  return await onFetchError(error, cacheKey, keyId, keyCache, logger);
480
628
  }
481
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
629
+ return await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
482
630
  }
483
631
  async function fetchKeyInternal(keyId, cls, options = {}) {
484
632
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -1532,4 +1680,4 @@ function timingSafeEqual(a, b) {
1532
1680
  return result === 0;
1533
1681
  }
1534
1682
  //#endregion
1535
- export { version as _, verifyRequestDetailed as a, fetchKeyDetailed as c, validateCryptoKey as d, formatAcceptSignature as f, name as g, validateAcceptSignature as h, verifyRequest as i, generateCryptoKeyPair as l, parseAcceptSignature as m, parseRfc9421SignatureInput as n, exportJwk as o, fulfillAcceptSignature as p, signRequest as r, fetchKey as s, doubleKnock as t, importJwk as u };
1683
+ export { validateAcceptSignature as _, verifyRequestDetailed as a, fetchKey as c, importJwk as d, validateCryptoKey as f, parseAcceptSignature as g, fulfillAcceptSignature as h, verifyRequest as i, fetchKeyDetailed as l, formatAcceptSignature as m, parseRfc9421SignatureInput as n, exportJwk as o, verifyKeyOwnership as p, signRequest as r, fetchActorDocument as s, doubleKnock as t, generateCryptoKeyPair as u, name as v, version as y };
@@ -1,9 +1,9 @@
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-DeYUdFal.mjs";
5
- import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPkZzmGN.mjs";
6
- import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-B4kUuy3o.mjs";
4
+ import { n as version, t as name } from "./deno-CV4wsQzy.mjs";
5
+ import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-Dd__NiUL.mjs";
6
+ import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-DiziIx9e.mjs";
7
7
  import { CryptographicKey } from "@fedify/vocab";
8
8
  import { SpanStatusCode, trace } from "@opentelemetry/api";
9
9
  import { FetchError } from "@fedify/vocab-runtime";
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { CryptographicKey, Multikey } from "@fedify/vocab";
2
+ import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
3
  import { TracerProvider } from "@opentelemetry/api";
4
4
  import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
 
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { CryptographicKey, Multikey } from "@fedify/vocab";
2
+ import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
4
  import { TracerProvider } from "@opentelemetry/api";
5
5