@fedify/fedify 2.2.12 → 2.2.13

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-uTlVksLw.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-Cg_cZHcI.mjs} +1 -1
  12. package/dist/{docloader-CXH2XqYP.mjs → docloader-BwXP5fDH.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-79lZeYEo.mjs → http-B3V9unT4.mjs} +3 -3
  33. package/dist/{http-D6cIKrI1.cjs → http-BdbKIiay.cjs} +166 -6
  34. package/dist/{http-CNsxTbVr.js → http-DaLdtguq.js} +155 -7
  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-CLRfRmA_.mjs} +154 -6
  38. package/dist/{keycache-EGATflN-.mjs → keycache-C1aVprwy.mjs} +12 -4
  39. package/dist/{kv-cache-BtXEUvHT.cjs → kv-cache-K6eHO_YH.cjs} +1 -1
  40. package/dist/{kv-cache-CIZ1m-94.js → kv-cache-TPuR5wYg.js} +1 -1
  41. package/dist/{ld-2R9vZ_ym.mjs → ld-C0s4f-GU.mjs} +2 -2
  42. package/dist/{middleware-DsEAPFhi.js → middleware-BDRZkGPG.js} +90 -14
  43. package/dist/{middleware-CjrIRAsQ.cjs → middleware-BDc7xyOx.cjs} +90 -14
  44. package/dist/{middleware-D5NE6PkU.mjs → middleware-BsDiZP9H.mjs} +31 -20
  45. package/dist/{middleware-2bSnAJOQ.cjs → middleware-D6bGiZn6.cjs} +1 -1
  46. package/dist/{middleware-_qt3kHUU.mjs → middleware-uaOjlLEG.mjs} +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-DLbN_4jZ.mjs +175 -0
  67. package/dist/{proof-CmtXtaul.cjs → proof-CTIG9UEI.cjs} +112 -58
  68. package/dist/{proof-BzTt6nlk.mjs → proof-CW3Zrxmd.mjs} +4 -4
  69. package/dist/{proof-BhW_iARR.js → proof-CqyprEYB.js} +112 -58
  70. package/dist/{send-CZwOs7XD.mjs → send-B5QvdrZ1.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-CFLRXAcm.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 +6 -6
  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.13";
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.13";
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,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
 
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DeYUdFal.mjs";
4
+ import { n as version, t as name } from "./deno-Cg_cZHcI.mjs";
5
5
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
7
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
@@ -85,6 +85,118 @@ async function importJwk(jwk, type) {
85
85
  validateCryptoKey(key, type);
86
86
  return key;
87
87
  }
88
+ /**
89
+ * Fetches the document that the given actor URI dereferences to, and returns
90
+ * the actor only if the host that served the document is authoritative for
91
+ * the id the document claims.
92
+ *
93
+ * Only the origin that serves an actor id can speak for it. Without that
94
+ * rule any host could serve a document describing somebody else's actor—and
95
+ * listing its own keys as that actor's.
96
+ *
97
+ * @param actorId The URI of the actor to fetch.
98
+ * @param options Options for fetching the document.
99
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
100
+ * actor, or belongs to another origin.
101
+ * @internal
102
+ */
103
+ async function fetchActorDocument(actorId, options = {}) {
104
+ const logger = getLogger([
105
+ "fedify",
106
+ "sig",
107
+ "key"
108
+ ]);
109
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
110
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
111
+ const { tracerProvider } = options;
112
+ let document;
113
+ let documentUrl = actorId;
114
+ try {
115
+ const remoteDocument = await documentLoader(actorId.href);
116
+ document = remoteDocument.document;
117
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
118
+ } catch (error) {
119
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
120
+ actorId: actorId.href,
121
+ error
122
+ });
123
+ return null;
124
+ }
125
+ let object;
126
+ try {
127
+ object = await Object$1.fromJsonLd(document, {
128
+ documentLoader,
129
+ contextLoader,
130
+ tracerProvider,
131
+ baseUrl: documentUrl
132
+ });
133
+ } catch (error) {
134
+ if (!(error instanceof TypeError)) throw error;
135
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
136
+ documentUrl: documentUrl.href,
137
+ error
138
+ });
139
+ return null;
140
+ }
141
+ if (!isActor(object)) return null;
142
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
143
+ 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.", {
144
+ documentUrl: documentUrl.href,
145
+ actorId: object.id?.href
146
+ });
147
+ return null;
148
+ }
149
+ return object;
150
+ }
151
+ /**
152
+ * Resolves the owner that the given key claims, and returns it only if the
153
+ * claim holds.
154
+ *
155
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
156
+ * a {@link Multikey}, proves nothing on its own: the key document and the
157
+ * claim inside it are served by the same host, so anyone able to serve a key
158
+ * document can name any actor in the world as its owner. The claim becomes
159
+ * meaningful only once the named actor's own document is fetched and turns
160
+ * out to link back to the key. This function performs that mutual-link
161
+ * check, which is the only sound way to learn a fetched key's owner.
162
+ *
163
+ * The claimed owner is always dereferenced, never read out of the key
164
+ * document: an owner embedded there is written by whoever wrote the key.
165
+ *
166
+ * @param key The key whose ownership claim is to be verified. It must carry
167
+ * an `id`, as that is what the owner has to link back to.
168
+ * @param options Options for fetching the claimed owner.
169
+ * @returns The verified owner, or `null` if the key claims no owner, the
170
+ * owner cannot be fetched, or the owner does not link back to
171
+ * the key.
172
+ * @internal
173
+ */
174
+ async function verifyKeyOwnership(key, options = {}) {
175
+ const logger = getLogger([
176
+ "fedify",
177
+ "sig",
178
+ "key"
179
+ ]);
180
+ const keyId = key.id;
181
+ if (keyId == null) return null;
182
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
183
+ if (claimedOwnerId == null) return null;
184
+ const owner = await fetchActorDocument(claimedOwnerId, options);
185
+ if (owner == null) {
186
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
187
+ keyId: keyId.href,
188
+ claimedOwnerId: claimedOwnerId.href
189
+ });
190
+ return null;
191
+ }
192
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
193
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
194
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
195
+ keyId: keyId.href,
196
+ claimedOwnerId: claimedOwnerId.href
197
+ });
198
+ return null;
199
+ }
88
200
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
89
201
  tracerProvider ??= trace.getTracerProvider();
90
202
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -212,7 +324,7 @@ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
212
324
  async function clearFetchErrorMetadata(keyId, keyCache) {
213
325
  await keyCache?.setFetchError?.(keyId, null);
214
326
  }
215
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
327
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
216
328
  let object;
217
329
  try {
218
330
  object = await Object$1.fromJsonLd(document, {
@@ -242,8 +354,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
242
354
  }
243
355
  }
244
356
  let key = null;
245
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
357
+ let ownerDocument = null;
358
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
246
359
  else if (isActor(object)) {
360
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
361
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
362
+ keyId,
363
+ documentUrl: documentUrl.href,
364
+ actorId: object.id?.href
365
+ });
366
+ await keyCache?.set(cacheKey, null);
367
+ return {
368
+ key: null,
369
+ cached: false
370
+ };
371
+ }
372
+ ownerDocument = object;
247
373
  const keys = cls === CryptographicKey ? object.getPublicKeys({
248
374
  documentLoader,
249
375
  contextLoader,
@@ -305,6 +431,25 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
305
431
  cached: false
306
432
  };
307
433
  }
434
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
435
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
436
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
437
+ if (await verifyKeyOwnership(key, {
438
+ documentLoader,
439
+ contextLoader,
440
+ tracerProvider
441
+ }) == null) {
442
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
443
+ keyId,
444
+ claimedOwnerId: claimedOwnerId.href
445
+ });
446
+ await keyCache?.set(cacheKey, null);
447
+ return {
448
+ key: null,
449
+ cached: false
450
+ };
451
+ }
452
+ }
308
453
  if (keyCache != null) {
309
454
  await keyCache.set(cacheKey, key);
310
455
  logger.debug("Key {keyId} cached.", { keyId });
@@ -328,12 +473,15 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
328
473
  if (cached != null) return cached;
329
474
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
330
475
  let document;
476
+ let documentUrl = cacheKey;
331
477
  try {
332
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
478
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
479
+ document = remoteDocument.document;
480
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
333
481
  } catch (error) {
334
482
  return await onFetchError(error, cacheKey, keyId, keyCache, logger);
335
483
  }
336
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
484
+ return await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
337
485
  }
338
486
  async function fetchKeyInternal(keyId, cls, options = {}) {
339
487
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -359,4 +507,4 @@ async function fetchKeyInternal(keyId, cls, options = {}) {
359
507
  });
360
508
  }
361
509
  //#endregion
362
- export { importJwk as a, generateCryptoKeyPair as i, fetchKey as n, validateCryptoKey as o, fetchKeyDetailed as r, exportJwk as t };
510
+ 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 };