@fedify/fedify 2.3.7 → 2.3.9-dev.2048

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 (108) hide show
  1. package/dist/{assert_instance_of-DBC5X09g.mjs → assert-jSdZMYam.mjs} +19 -1
  2. package/dist/{builder-Bg7nkigw.mjs → builder-BfY2wZ4V.mjs} +3 -3
  3. package/dist/{client-D1KuWxjH.mjs → client-DYDoDpMa.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-DVoTs_wM.mjs → context-BepjCFHN.mjs} +1 -0
  10. package/dist/{context-BxVVO5Ro.d.ts → context-CAKnQy1x.d.ts} +4 -3
  11. package/dist/{context-BpmLyiPx.d.cts → context-CSIggG9_.d.cts} +4 -3
  12. package/dist/{deno-CTdCdRmf.mjs → deno-ayal9buz.mjs} +1 -1
  13. package/dist/{docloader-BVBWf8Bg.mjs → docloader-C-JrNL14.mjs} +2 -2
  14. package/dist/federation/builder.test.mjs +2 -2
  15. package/dist/federation/circuit-breaker.test.mjs +3 -3
  16. package/dist/federation/collection.test.mjs +1 -1
  17. package/dist/federation/handler.test.mjs +128 -11
  18. package/dist/federation/idempotency.test.mjs +4 -4
  19. package/dist/federation/inbox.test.mjs +1 -1
  20. package/dist/federation/keycache.test.mjs +44 -10
  21. package/dist/federation/kv.test.mjs +1 -1
  22. package/dist/federation/metrics.test.mjs +1 -1
  23. package/dist/federation/middleware.test.mjs +109 -19
  24. package/dist/federation/mod.cjs +1 -1
  25. package/dist/federation/mod.d.cts +2 -2
  26. package/dist/federation/mod.d.ts +2 -2
  27. package/dist/federation/mod.js +1 -1
  28. package/dist/federation/mq.test.mjs +1 -1
  29. package/dist/federation/negotiation.test.mjs +2 -2
  30. package/dist/federation/retry.test.mjs +1 -1
  31. package/dist/federation/router.test.mjs +1 -1
  32. package/dist/federation/send.test.mjs +141 -7
  33. package/dist/federation/temporal.test.mjs +2 -2
  34. package/dist/federation/webfinger.test.mjs +3 -3
  35. package/dist/{http-B3-zan5-.mjs → http-B2u6PvNX.mjs} +4 -4
  36. package/dist/{http-BjV0BTWd.cjs → http-BGP2QCbe.cjs} +166 -6
  37. package/dist/{http-DRVY9K4R.js → http-BPPCh0xN.js} +155 -7
  38. package/dist/{http-lf8Hsd91.d.ts → http-DQmNpfym.d.ts} +1 -1
  39. package/dist/{http-VyDTd4G3.d.cts → http-DiWQB8Mr.d.cts} +1 -1
  40. package/dist/{key-C_a6iE1-.mjs → key-C9DkrWwb.mjs} +155 -7
  41. package/dist/{keycache-BeU0LCII.mjs → keycache-B3qRQNLj.mjs} +12 -4
  42. package/dist/{kv-cache-OzUoHKnZ.cjs → kv-cache-C1TC_e9y.cjs} +2 -2
  43. package/dist/{kv-cache-b1dJZHUx.js → kv-cache-Cl1q1aA9.js} +2 -2
  44. package/dist/{kv-cache-BjjcmjtG.mjs → kv-cache-qJSjfLGm.mjs} +1 -1
  45. package/dist/{ld-COo05yJj.mjs → ld-DNl7KwLY.mjs} +3 -3
  46. package/dist/{metrics-V6vX0M13.mjs → metrics-Ce55disp.mjs} +1 -1
  47. package/dist/{middleware-DRk6xiYo.mjs → middleware-BCG2BX1n.mjs} +41 -30
  48. package/dist/{middleware-EDVv6ewB.cjs → middleware-BZojifnF.cjs} +101 -25
  49. package/dist/{middleware-DRGWEo0V.mjs → middleware-C96wm_7x.mjs} +1 -1
  50. package/dist/{middleware-K13LhN5w.js → middleware-C_utyVa_.js} +101 -25
  51. package/dist/{mod-yvIXFAEi.d.cts → mod-1TuoXkGs.d.cts} +1 -1
  52. package/dist/{mod-B0hW12_O.d.cts → mod-Bid79LFU.d.cts} +1 -1
  53. package/dist/{mod-Rpy4Uwkk.d.ts → mod-CFuyfw3c.d.ts} +1 -1
  54. package/dist/{mod-DFvNJcNb.d.ts → mod-DHgc4ygG.d.ts} +1 -1
  55. package/dist/{mod-CjERT09F.d.cts → mod-PVR75MeU.d.cts} +1 -1
  56. package/dist/{mod-COIAjwRS.d.ts → mod-TLEvgT1i.d.ts} +1 -1
  57. package/dist/mod.cjs +5 -5
  58. package/dist/mod.d.cts +6 -6
  59. package/dist/mod.d.ts +6 -6
  60. package/dist/mod.js +5 -5
  61. package/dist/nodeinfo/client.test.mjs +28 -1
  62. package/dist/nodeinfo/handler.test.mjs +3 -3
  63. package/dist/nodeinfo/mod.cjs +1 -1
  64. package/dist/nodeinfo/mod.js +1 -1
  65. package/dist/nodeinfo/types.test.mjs +1 -1
  66. package/dist/otel/exporter.test.mjs +1 -1
  67. package/dist/{outgoing-jsonld-OFoubetZ.mjs → outgoing-jsonld-HOAywiid.mjs} +1 -1
  68. package/dist/{owner-CnngXDNJ.d.ts → owner-B3hNQCG8.d.cts} +12 -1
  69. package/dist/{owner-DEvZuyOE.d.cts → owner-CiBPcC6F.d.ts} +12 -1
  70. package/dist/owner-DqqDBw-l.mjs +175 -0
  71. package/dist/{proof-kFq0IDK4.mjs → proof-D5vP7VIN.mjs} +5 -5
  72. package/dist/{proof-D6gZeewf.js → proof-Dz9ObvUq.js} +112 -58
  73. package/dist/{proof-Xk8lHNc0.cjs → proof-I6YcKRRq.cjs} +112 -58
  74. package/dist/{send-3lH63Nqf.mjs → send-CBtR8DQK.mjs} +68 -11
  75. package/dist/sig/accept.test.mjs +1 -1
  76. package/dist/sig/http.test.mjs +4 -4
  77. package/dist/sig/key.test.mjs +170 -3
  78. package/dist/sig/ld.test.mjs +56 -4
  79. package/dist/sig/mod.cjs +2 -2
  80. package/dist/sig/mod.d.cts +3 -3
  81. package/dist/sig/mod.d.ts +3 -3
  82. package/dist/sig/mod.js +2 -2
  83. package/dist/sig/owner.test.mjs +105 -6
  84. package/dist/sig/proof.test.mjs +73 -5
  85. package/dist/{temporal-Chwx9cEn.mjs → temporal-B9t2tY9A.mjs} +1 -1
  86. package/dist/testing/mod.mjs +1 -1
  87. package/dist/{types-m8u9L86X.cjs → types-DWs-qZaY.cjs} +94 -2
  88. package/dist/{types-DTONwNqL.js → types-sXITXcWa.js} +78 -4
  89. package/dist/utils/docloader.test.mjs +28 -4
  90. package/dist/utils/kv-cache.test.mjs +1 -1
  91. package/dist/utils/mod.cjs +1 -1
  92. package/dist/utils/mod.d.cts +1 -1
  93. package/dist/utils/mod.d.ts +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +6 -6
  96. package/dist/assert-OguE97r2.mjs +0 -23
  97. package/dist/owner-B3720pz3.mjs +0 -120
  98. /package/dist/{accept-CPkZzmGN.mjs → accept-Dd__NiUL.mjs} +0 -0
  99. /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-ell7W1s9.mjs} +0 -0
  100. /package/dist/{circuit-breaker-D1fxthDI.mjs → circuit-breaker-CDABzqVf.mjs} +0 -0
  101. /package/dist/{circuit-breaker-test-utils-CAdwjgMi.mjs → circuit-breaker-test-utils-Dm5MM6l9.mjs} +0 -0
  102. /package/dist/{collection-Cc3DVAhE.mjs → collection-CA3V5zyK.mjs} +0 -0
  103. /package/dist/{keys-DGu1NFwu.mjs → keys-BAK-tUlf.mjs} +0 -0
  104. /package/dist/{kv-rV3vodCc.mjs → kv-CX8qfaad.mjs} +0 -0
  105. /package/dist/{negotiation-DDstyBvc.mjs → negotiation-CDW-_gUU.mjs} +0 -0
  106. /package/dist/{public-audience-DZJAUUi8.mjs → public-audience-jwoSIGUw.mjs} +0 -0
  107. /package/dist/{retry-CXg_MBI-.mjs → retry-_VvV0h9f.mjs} +0 -0
  108. /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.3.7";
14
+ var version = "2.3.9-dev.2048+c88fcc8c";
15
15
  //#endregion
16
16
  //#region src/sig/accept.ts
17
17
  /**
@@ -1039,6 +1039,118 @@ async function importJwk(jwk, type) {
1039
1039
  validateCryptoKey(key, type);
1040
1040
  return key;
1041
1041
  }
1042
+ /**
1043
+ * Fetches the document that the given actor URI dereferences to, and returns
1044
+ * the actor only if the host that served the document is authoritative for
1045
+ * the id the document claims.
1046
+ *
1047
+ * Only the origin that serves an actor id can speak for it. Without that
1048
+ * rule any host could serve a document describing somebody else's actor—and
1049
+ * listing its own keys as that actor's.
1050
+ *
1051
+ * @param actorId The URI of the actor to fetch.
1052
+ * @param options Options for fetching the document.
1053
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
1054
+ * actor, or belongs to another origin.
1055
+ * @internal
1056
+ */
1057
+ async function fetchActorDocument(actorId, options = {}) {
1058
+ const logger = (0, _logtape_logtape.getLogger)([
1059
+ "fedify",
1060
+ "sig",
1061
+ "key"
1062
+ ]);
1063
+ const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
1064
+ const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
1065
+ const { tracerProvider } = options;
1066
+ let document;
1067
+ let documentUrl = actorId;
1068
+ try {
1069
+ const remoteDocument = await documentLoader(actorId.href);
1070
+ document = remoteDocument.document;
1071
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
1072
+ } catch (error) {
1073
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
1074
+ actorId: actorId.href,
1075
+ error
1076
+ });
1077
+ return null;
1078
+ }
1079
+ let object;
1080
+ try {
1081
+ object = await _fedify_vocab.Object.fromJsonLd(document, {
1082
+ documentLoader,
1083
+ contextLoader,
1084
+ tracerProvider,
1085
+ baseUrl: documentUrl
1086
+ });
1087
+ } catch (error) {
1088
+ if (!(error instanceof TypeError)) throw error;
1089
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
1090
+ documentUrl: documentUrl.href,
1091
+ error
1092
+ });
1093
+ return null;
1094
+ }
1095
+ if (!(0, _fedify_vocab.isActor)(object)) return null;
1096
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1097
+ 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.", {
1098
+ documentUrl: documentUrl.href,
1099
+ actorId: object.id?.href
1100
+ });
1101
+ return null;
1102
+ }
1103
+ return object;
1104
+ }
1105
+ /**
1106
+ * Resolves the owner that the given key claims, and returns it only if the
1107
+ * claim holds.
1108
+ *
1109
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
1110
+ * a {@link Multikey}, proves nothing on its own: the key document and the
1111
+ * claim inside it are served by the same host, so anyone able to serve a key
1112
+ * document can name any actor in the world as its owner. The claim becomes
1113
+ * meaningful only once the named actor's own document is fetched and turns
1114
+ * out to link back to the key. This function performs that mutual-link
1115
+ * check, which is the only sound way to learn a fetched key's owner.
1116
+ *
1117
+ * The claimed owner is always dereferenced, never read out of the key
1118
+ * document: an owner embedded there is written by whoever wrote the key.
1119
+ *
1120
+ * @param key The key whose ownership claim is to be verified. It must carry
1121
+ * an `id`, as that is what the owner has to link back to.
1122
+ * @param options Options for fetching the claimed owner.
1123
+ * @returns The verified owner, or `null` if the key claims no owner, the
1124
+ * owner cannot be fetched, or the owner does not link back to
1125
+ * the key.
1126
+ * @internal
1127
+ */
1128
+ async function verifyKeyOwnership(key, options = {}) {
1129
+ const logger = (0, _logtape_logtape.getLogger)([
1130
+ "fedify",
1131
+ "sig",
1132
+ "key"
1133
+ ]);
1134
+ const keyId = key.id;
1135
+ if (keyId == null) return null;
1136
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
1137
+ if (claimedOwnerId == null) return null;
1138
+ const owner = await fetchActorDocument(claimedOwnerId, options);
1139
+ if (owner == null) {
1140
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
1141
+ keyId: keyId.href,
1142
+ claimedOwnerId: claimedOwnerId.href
1143
+ });
1144
+ return null;
1145
+ }
1146
+ const linkedKeyIds = key instanceof _fedify_vocab.CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
1147
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
1148
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
1149
+ keyId: keyId.href,
1150
+ claimedOwnerId: claimedOwnerId.href
1151
+ });
1152
+ return null;
1153
+ }
1042
1154
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1043
1155
  tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
1044
1156
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -1166,7 +1278,7 @@ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
1166
1278
  async function clearFetchErrorMetadata(keyId, keyCache) {
1167
1279
  await keyCache?.setFetchError?.(keyId, null);
1168
1280
  }
1169
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1281
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1170
1282
  let object;
1171
1283
  try {
1172
1284
  object = await _fedify_vocab.Object.fromJsonLd(document, {
@@ -1196,8 +1308,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1196
1308
  }
1197
1309
  }
1198
1310
  let key = null;
1199
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
1311
+ let ownerDocument = null;
1312
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
1200
1313
  else if ((0, _fedify_vocab.isActor)(object)) {
1314
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1315
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
1316
+ keyId,
1317
+ documentUrl: documentUrl.href,
1318
+ actorId: object.id?.href
1319
+ });
1320
+ await keyCache?.set(cacheKey, null);
1321
+ return {
1322
+ key: null,
1323
+ cached: false
1324
+ };
1325
+ }
1326
+ ownerDocument = object;
1201
1327
  const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
1202
1328
  documentLoader,
1203
1329
  contextLoader,
@@ -1259,6 +1385,25 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1259
1385
  cached: false
1260
1386
  };
1261
1387
  }
1388
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
1389
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
1390
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
1391
+ if (await verifyKeyOwnership(key, {
1392
+ documentLoader,
1393
+ contextLoader,
1394
+ tracerProvider
1395
+ }) == null) {
1396
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
1397
+ keyId,
1398
+ claimedOwnerId: claimedOwnerId.href
1399
+ });
1400
+ await keyCache?.set(cacheKey, null);
1401
+ return {
1402
+ key: null,
1403
+ cached: false
1404
+ };
1405
+ }
1406
+ }
1262
1407
  if (keyCache != null) {
1263
1408
  await keyCache.set(cacheKey, key);
1264
1409
  logger.debug("Key {keyId} cached.", { keyId });
@@ -1292,15 +1437,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1292
1437
  }
1293
1438
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1294
1439
  let document;
1440
+ let documentUrl = cacheKey;
1295
1441
  try {
1296
- document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
1442
+ const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
1443
+ document = remoteDocument.document;
1444
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
1297
1445
  } catch (error) {
1298
1446
  const classified = classifyFetchError(error);
1299
1447
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1300
1448
  outcome = classified;
1301
1449
  return errored;
1302
1450
  }
1303
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1451
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
1304
1452
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1305
1453
  return resolved;
1306
1454
  } finally {
@@ -2427,6 +2575,12 @@ Object.defineProperty(exports, "exportJwk", {
2427
2575
  return exportJwk;
2428
2576
  }
2429
2577
  });
2578
+ Object.defineProperty(exports, "fetchActorDocument", {
2579
+ enumerable: true,
2580
+ get: function() {
2581
+ return fetchActorDocument;
2582
+ }
2583
+ });
2430
2584
  Object.defineProperty(exports, "fetchKey", {
2431
2585
  enumerable: true,
2432
2586
  get: function() {
@@ -2607,6 +2761,12 @@ Object.defineProperty(exports, "validateCryptoKey", {
2607
2761
  return validateCryptoKey;
2608
2762
  }
2609
2763
  });
2764
+ Object.defineProperty(exports, "verifyKeyOwnership", {
2765
+ enumerable: true,
2766
+ get: function() {
2767
+ return verifyKeyOwnership;
2768
+ }
2769
+ });
2610
2770
  Object.defineProperty(exports, "verifyRequest", {
2611
2771
  enumerable: true,
2612
2772
  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, metrics, trace } from "@opentelemetry/api";
6
6
  import { encodeHex } from "byte-encodings/hex";
7
- import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
8
7
  import { FetchError, getDocumentLoader } 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.3.7";
13
+ var version = "2.3.9-dev.2048+c88fcc8c";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -1038,6 +1038,118 @@ async function importJwk(jwk, type) {
1038
1038
  validateCryptoKey(key, type);
1039
1039
  return key;
1040
1040
  }
1041
+ /**
1042
+ * Fetches the document that the given actor URI dereferences to, and returns
1043
+ * the actor only if the host that served the document is authoritative for
1044
+ * the id the document claims.
1045
+ *
1046
+ * Only the origin that serves an actor id can speak for it. Without that
1047
+ * rule any host could serve a document describing somebody else's actor—and
1048
+ * listing its own keys as that actor's.
1049
+ *
1050
+ * @param actorId The URI of the actor to fetch.
1051
+ * @param options Options for fetching the document.
1052
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
1053
+ * actor, or belongs to another origin.
1054
+ * @internal
1055
+ */
1056
+ async function fetchActorDocument(actorId, options = {}) {
1057
+ const logger = getLogger([
1058
+ "fedify",
1059
+ "sig",
1060
+ "key"
1061
+ ]);
1062
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
1063
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
1064
+ const { tracerProvider } = options;
1065
+ let document;
1066
+ let documentUrl = actorId;
1067
+ try {
1068
+ const remoteDocument = await documentLoader(actorId.href);
1069
+ document = remoteDocument.document;
1070
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
1071
+ } catch (error) {
1072
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
1073
+ actorId: actorId.href,
1074
+ error
1075
+ });
1076
+ return null;
1077
+ }
1078
+ let object;
1079
+ try {
1080
+ object = await Object$1.fromJsonLd(document, {
1081
+ documentLoader,
1082
+ contextLoader,
1083
+ tracerProvider,
1084
+ baseUrl: documentUrl
1085
+ });
1086
+ } catch (error) {
1087
+ if (!(error instanceof TypeError)) throw error;
1088
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
1089
+ documentUrl: documentUrl.href,
1090
+ error
1091
+ });
1092
+ return null;
1093
+ }
1094
+ if (!isActor(object)) return null;
1095
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1096
+ 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.", {
1097
+ documentUrl: documentUrl.href,
1098
+ actorId: object.id?.href
1099
+ });
1100
+ return null;
1101
+ }
1102
+ return object;
1103
+ }
1104
+ /**
1105
+ * Resolves the owner that the given key claims, and returns it only if the
1106
+ * claim holds.
1107
+ *
1108
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
1109
+ * a {@link Multikey}, proves nothing on its own: the key document and the
1110
+ * claim inside it are served by the same host, so anyone able to serve a key
1111
+ * document can name any actor in the world as its owner. The claim becomes
1112
+ * meaningful only once the named actor's own document is fetched and turns
1113
+ * out to link back to the key. This function performs that mutual-link
1114
+ * check, which is the only sound way to learn a fetched key's owner.
1115
+ *
1116
+ * The claimed owner is always dereferenced, never read out of the key
1117
+ * document: an owner embedded there is written by whoever wrote the key.
1118
+ *
1119
+ * @param key The key whose ownership claim is to be verified. It must carry
1120
+ * an `id`, as that is what the owner has to link back to.
1121
+ * @param options Options for fetching the claimed owner.
1122
+ * @returns The verified owner, or `null` if the key claims no owner, the
1123
+ * owner cannot be fetched, or the owner does not link back to
1124
+ * the key.
1125
+ * @internal
1126
+ */
1127
+ async function verifyKeyOwnership(key, options = {}) {
1128
+ const logger = getLogger([
1129
+ "fedify",
1130
+ "sig",
1131
+ "key"
1132
+ ]);
1133
+ const keyId = key.id;
1134
+ if (keyId == null) return null;
1135
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1136
+ if (claimedOwnerId == null) return null;
1137
+ const owner = await fetchActorDocument(claimedOwnerId, options);
1138
+ if (owner == null) {
1139
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
1140
+ keyId: keyId.href,
1141
+ claimedOwnerId: claimedOwnerId.href
1142
+ });
1143
+ return null;
1144
+ }
1145
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
1146
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
1147
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
1148
+ keyId: keyId.href,
1149
+ claimedOwnerId: claimedOwnerId.href
1150
+ });
1151
+ return null;
1152
+ }
1041
1153
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1042
1154
  tracerProvider ??= trace.getTracerProvider();
1043
1155
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -1165,7 +1277,7 @@ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
1165
1277
  async function clearFetchErrorMetadata(keyId, keyCache) {
1166
1278
  await keyCache?.setFetchError?.(keyId, null);
1167
1279
  }
1168
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1280
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1169
1281
  let object;
1170
1282
  try {
1171
1283
  object = await Object$1.fromJsonLd(document, {
@@ -1195,8 +1307,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1195
1307
  }
1196
1308
  }
1197
1309
  let key = null;
1198
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
1310
+ let ownerDocument = null;
1311
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
1199
1312
  else if (isActor(object)) {
1313
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1314
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
1315
+ keyId,
1316
+ documentUrl: documentUrl.href,
1317
+ actorId: object.id?.href
1318
+ });
1319
+ await keyCache?.set(cacheKey, null);
1320
+ return {
1321
+ key: null,
1322
+ cached: false
1323
+ };
1324
+ }
1325
+ ownerDocument = object;
1200
1326
  const keys = cls === CryptographicKey ? object.getPublicKeys({
1201
1327
  documentLoader,
1202
1328
  contextLoader,
@@ -1258,6 +1384,25 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1258
1384
  cached: false
1259
1385
  };
1260
1386
  }
1387
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1388
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
1389
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
1390
+ if (await verifyKeyOwnership(key, {
1391
+ documentLoader,
1392
+ contextLoader,
1393
+ tracerProvider
1394
+ }) == null) {
1395
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
1396
+ keyId,
1397
+ claimedOwnerId: claimedOwnerId.href
1398
+ });
1399
+ await keyCache?.set(cacheKey, null);
1400
+ return {
1401
+ key: null,
1402
+ cached: false
1403
+ };
1404
+ }
1405
+ }
1261
1406
  if (keyCache != null) {
1262
1407
  await keyCache.set(cacheKey, key);
1263
1408
  logger.debug("Key {keyId} cached.", { keyId });
@@ -1291,15 +1436,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1291
1436
  }
1292
1437
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1293
1438
  let document;
1439
+ let documentUrl = cacheKey;
1294
1440
  try {
1295
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
1441
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
1442
+ document = remoteDocument.document;
1443
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
1296
1444
  } catch (error) {
1297
1445
  const classified = classifyFetchError(error);
1298
1446
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1299
1447
  outcome = classified;
1300
1448
  return errored;
1301
1449
  }
1302
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1450
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
1303
1451
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1304
1452
  return resolved;
1305
1453
  } finally {
@@ -2414,4 +2562,4 @@ function timingSafeEqual(a, b) {
2414
2562
  return result === 0;
2415
2563
  }
2416
2564
  //#endregion
2417
- export { formatAcceptSignature as A, recordDocumentCache as C, recordOutboxEnqueue as D, recordOutboxActivity as E, version as F, parseAcceptSignature as M, validateAcceptSignature as N, recordWebFingerHandle as O, name as P, recordCollectionTotalItems as S, recordInboxActivity as T, measureSignatureKeyFetch as _, verifyRequestDetailed as a, recordCollectionPageItems as b, fetchKeyDetailed as c, validateCryptoKey as d, getDurationMs as f, isAbortError$1 as g, instrumentDocumentLoader as h, verifyRequest as i, fulfillAcceptSignature as j, registerQueueDepthGauge as k, generateCryptoKeyPair as l, getRemoteHost as m, parseRfc9421SignatureInput as n, exportJwk as o, getFederationMetrics as p, signRequest as r, fetchKey as s, doubleKnock as t, importJwk as u, recordCircuitBreakerStateChange as v, recordFanoutRecipients as w, recordCollectionRequest as x, recordCollectionDispatchDuration as y };
2565
+ export { recordWebFingerHandle as A, recordCollectionRequest as C, recordInboxActivity as D, recordFanoutRecipients as E, validateAcceptSignature as F, name as I, version as L, formatAcceptSignature as M, fulfillAcceptSignature as N, recordOutboxActivity as O, parseAcceptSignature as P, recordCollectionPageItems as S, recordDocumentCache as T, instrumentDocumentLoader as _, verifyRequestDetailed as a, recordCircuitBreakerStateChange as b, fetchKey as c, importJwk as d, validateCryptoKey as f, getRemoteHost as g, getFederationMetrics as h, verifyRequest as i, registerQueueDepthGauge as j, recordOutboxEnqueue as k, fetchKeyDetailed as l, getDurationMs as m, parseRfc9421SignatureInput as n, exportJwk as o, verifyKeyOwnership as p, signRequest as r, fetchActorDocument as s, doubleKnock as t, generateCryptoKeyPair as u, isAbortError$1 as v, recordCollectionTotalItems as w, recordCollectionDispatchDuration as x, measureSignatureKeyFetch as y };
@@ -1,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 { MeterProvider, 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 { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5