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

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 (94) hide show
  1. package/dist/{builder-B4zpKIFU.mjs → builder-D0DxgOV5.mjs} +2 -2
  2. package/dist/{client-CtxvmazV.mjs → client-DpnpgjyX.mjs} +78 -4
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/compound-proof-CJnWc6c-.mjs +495 -0
  7. package/dist/{context-COLwHogJ.d.cts → context-BeDnDuF3.d.cts} +4 -3
  8. package/dist/{context-DiiCt4S7.d.ts → context-uyRUWoZQ.d.ts} +4 -3
  9. package/dist/{deno-CtG93ohj.mjs → deno-CABhzCb_.mjs} +1 -1
  10. package/dist/{docloader-dk_k3S8d.mjs → docloader-BbGSL0Xq.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  13. package/dist/federation/handler-compound-proof.test.mjs +152 -0
  14. package/dist/federation/handler.test.mjs +122 -4
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +52 -9
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +111 -15
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/send.test.mjs +139 -4
  24. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  25. package/dist/federation/tasks/tasks.test.mjs +2 -2
  26. package/dist/federation/temporal.test.mjs +1 -1
  27. package/dist/federation/webfinger.test.mjs +1 -1
  28. package/dist/{federation-Drjlrrax.cjs → federation-DCEHQk4G.cjs} +632 -57
  29. package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
  30. package/dist/{http-Dj4s1UQ0.js → http-9WyuldYh.js} +170 -8
  31. package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
  32. package/dist/{http-B3skRvGo.cjs → http-CRFns-UE.cjs} +181 -7
  33. package/dist/{http-D7LX-BL9.mjs → http-D9urSO2D.mjs} +3 -3
  34. package/dist/{key-BVjgbs_9.mjs → key-Ccs8cnd7.mjs} +169 -7
  35. package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
  36. package/dist/{kv-cache-DtsjH_bY.cjs → kv-cache-DXT9vmIg.cjs} +2 -2
  37. package/dist/{kv-cache-tGNbvDCF.js → kv-cache-DtmXhPXG.js} +2 -2
  38. package/dist/{kv-cache-xRcC_xxP.mjs → kv-cache-lKyFn_Dn.mjs} +1 -1
  39. package/dist/{ld-OuCeQFny.mjs → ld-qup__MAW.mjs} +3 -3
  40. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  41. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  42. package/dist/{metrics-B9N27cCB.mjs → metrics-CISXCvB1.mjs} +1 -1
  43. package/dist/{middleware-TeLjzoxn.mjs → middleware-CzvJvPHK.mjs} +58 -15
  44. package/dist/{middleware-Cd_vPqDH.mjs → middleware-DkrU2WhY.mjs} +1 -1
  45. package/dist/{middleware-CPTDFMmJ.js → middleware-DsfWajaD.js} +593 -18
  46. package/dist/{mod-DypxPS9H.d.ts → mod-C0BYc0OU.d.ts} +1 -1
  47. package/dist/{mod-CnGLNoVw.d.cts → mod-CbKDOnCt.d.cts} +1 -1
  48. package/dist/{mod-CLHkCwLm.d.cts → mod-CyiSjRkk.d.cts} +1 -1
  49. package/dist/{mod-CSH1Ee-I.d.ts → mod-D882_Xsg.d.ts} +1 -1
  50. package/dist/{mod-DzRXACM5.d.ts → mod-UDwMjuHM.d.ts} +1 -1
  51. package/dist/{mod-BZldCEU0.d.cts → mod-x9W0d0Pk.d.cts} +1 -1
  52. package/dist/mod.cjs +20 -20
  53. package/dist/mod.d.cts +6 -6
  54. package/dist/mod.d.ts +6 -6
  55. package/dist/mod.js +5 -5
  56. package/dist/nodeinfo/client.test.mjs +28 -1
  57. package/dist/nodeinfo/handler.test.mjs +1 -1
  58. package/dist/nodeinfo/mod.cjs +1 -1
  59. package/dist/nodeinfo/mod.js +1 -1
  60. package/dist/{owner-DGSfntI-.d.ts → owner-BF0L9cMt.d.cts} +12 -1
  61. package/dist/{proof-BO_esQii.js → owner-BIPEZGiU.js} +1255 -1098
  62. package/dist/{owner-r_waTiQj.d.cts → owner-BSzr5-I8.d.ts} +12 -1
  63. package/dist/owner-CRoq8Spb.mjs +175 -0
  64. package/dist/{proof-Dua0d906.cjs → owner-CVA7L9MA.cjs} +1266 -1097
  65. package/dist/{proof-D5Ajk4DJ.mjs → proof-BDxNVZ3E.mjs} +186 -83
  66. package/dist/{send-DVXyUqhO.mjs → send-BzM5GLyn.mjs} +68 -11
  67. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  68. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  69. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  70. package/dist/sig/compound-proof-verification.test.mjs +465 -0
  71. package/dist/sig/compound-proof.test.d.mts +2 -0
  72. package/dist/sig/compound-proof.test.mjs +324 -0
  73. package/dist/sig/http.test.mjs +2 -2
  74. package/dist/sig/key.test.mjs +169 -2
  75. package/dist/sig/ld.test.mjs +55 -2
  76. package/dist/sig/mod.cjs +17 -17
  77. package/dist/sig/mod.d.cts +3 -3
  78. package/dist/sig/mod.d.ts +3 -3
  79. package/dist/sig/mod.js +2 -2
  80. package/dist/sig/owner.test.mjs +103 -4
  81. package/dist/sig/proof.test.mjs +120 -1
  82. package/dist/{temporal-BoDw51A-.mjs → temporal-Dv6Qv-8k.mjs} +1 -1
  83. package/dist/testing/kv.test.mjs +1 -1
  84. package/dist/testing/mod.d.mts +2 -1
  85. package/dist/{types-RXm5YxpC.cjs → types-Be2zbyeG.cjs} +94 -2
  86. package/dist/{types-Dtyf8lVj.js → types-C_cNlNVK.js} +78 -4
  87. package/dist/utils/docloader.test.mjs +27 -3
  88. package/dist/utils/kv-cache.test.mjs +1 -1
  89. package/dist/utils/mod.cjs +1 -1
  90. package/dist/utils/mod.d.cts +1 -1
  91. package/dist/utils/mod.d.ts +1 -1
  92. package/dist/utils/mod.js +1 -1
  93. package/package.json +6 -6
  94. package/dist/owner-DTEqTzSr.mjs +0 -120
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { DocumentLoader } from "@fedify/vocab-runtime";
2
+ import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
4
3
  import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
+ import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -59,6 +59,38 @@ interface FetchKeyOptions {
59
59
  * @since 2.3.0
60
60
  */
61
61
  meterProvider?: MeterProvider;
62
+ /**
63
+ * An assertion by the caller that it has already bound this key's id to
64
+ * whoever the key will be treated as speaking for, by means of its own that
65
+ * key resolution cannot see.
66
+ *
67
+ * Key resolution normally settles ownership itself: it dereferences the
68
+ * `owner`/`controller` a key claims and requires that actor's own document
69
+ * to link back to the key, because the claim and the key document come from
70
+ * the same host and so the claim proves nothing on its own. See
71
+ * GHSA-q9f8-5hc7-898f.
72
+ *
73
+ * That check presumes the ActivityPub shape of ownership, where the owner
74
+ * is an actor carrying a `publicKey`/`assertionMethod` list. It does not
75
+ * fit every caller. Portable objects (FEP-ef61) bind a key to an object
76
+ * structurally instead: {@link verifyPortableObjectProof} requires the
77
+ * verification method's FEP-ef34 origin to equal the object id's own
78
+ * authority, so the signer is pinned by the identifier being verified,
79
+ * before any key is fetched. Demanding the ActivityPub check as well would
80
+ * be impossible rather than merely redundant, since a DID dereferences to
81
+ * a DID document, which is not an actor.
82
+ *
83
+ * This is not a way to accept whatever a key says about itself. The claim
84
+ * still has to be corroborated, just from the key id rather than from
85
+ * a second document: a key may name an owner only when the key id is that
86
+ * owner's own (a `did:…#fragment` belongs to the DID it is a fragment of,
87
+ * exactly as a key embedded in an actor document belongs to that actor).
88
+ * A key naming anyone else falls back to the ordinary check and is rejected
89
+ * if it cannot pass it, so a key admitted here is as sound as any other and
90
+ * is cached like any other.
91
+ * @internal
92
+ */
93
+ keyIdBoundByCaller?: boolean;
62
94
  }
63
95
  /**
64
96
  * The result of {@link fetchKey}.
@@ -4,13 +4,13 @@ import { getLogger } from "@logtape/logtape";
4
4
  import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
5
5
  import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
6
6
  import { encodeHex } from "byte-encodings/hex";
7
- import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
8
7
  import { FetchError, getDocumentLoader, parseDidKeyVerificationMethod } from "@fedify/vocab-runtime";
9
- import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
8
+ import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
10
9
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
10
+ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
11
11
  //#region deno.json
12
12
  var name = "@fedify/fedify";
13
- var version = "2.4.0-dev.1964+1d28dc03";
13
+ var version = "2.4.0-dev.1995+618fd4e7";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -1040,6 +1040,118 @@ async function importJwk(jwk, type) {
1040
1040
  validateCryptoKey(key, type);
1041
1041
  return key;
1042
1042
  }
1043
+ /**
1044
+ * Fetches the document that the given actor URI dereferences to, and returns
1045
+ * the actor only if the host that served the document is authoritative for
1046
+ * the id the document claims.
1047
+ *
1048
+ * Only the origin that serves an actor id can speak for it. Without that
1049
+ * rule any host could serve a document describing somebody else's actor—and
1050
+ * listing its own keys as that actor's.
1051
+ *
1052
+ * @param actorId The URI of the actor to fetch.
1053
+ * @param options Options for fetching the document.
1054
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
1055
+ * actor, or belongs to another origin.
1056
+ * @internal
1057
+ */
1058
+ async function fetchActorDocument(actorId, options = {}) {
1059
+ const logger = getLogger([
1060
+ "fedify",
1061
+ "sig",
1062
+ "key"
1063
+ ]);
1064
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
1065
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
1066
+ const { tracerProvider } = options;
1067
+ let document;
1068
+ let documentUrl = actorId;
1069
+ try {
1070
+ const remoteDocument = await documentLoader(actorId.href);
1071
+ document = remoteDocument.document;
1072
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
1073
+ } catch (error) {
1074
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
1075
+ actorId: actorId.href,
1076
+ error
1077
+ });
1078
+ return null;
1079
+ }
1080
+ let object;
1081
+ try {
1082
+ object = await Object$1.fromJsonLd(document, {
1083
+ documentLoader,
1084
+ contextLoader,
1085
+ tracerProvider,
1086
+ baseUrl: documentUrl
1087
+ });
1088
+ } catch (error) {
1089
+ if (!(error instanceof TypeError)) throw error;
1090
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
1091
+ documentUrl: documentUrl.href,
1092
+ error
1093
+ });
1094
+ return null;
1095
+ }
1096
+ if (!isActor(object)) return null;
1097
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1098
+ logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
1099
+ documentUrl: documentUrl.href,
1100
+ actorId: object.id?.href
1101
+ });
1102
+ return null;
1103
+ }
1104
+ return object;
1105
+ }
1106
+ /**
1107
+ * Resolves the owner that the given key claims, and returns it only if the
1108
+ * claim holds.
1109
+ *
1110
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
1111
+ * a {@link Multikey}, proves nothing on its own: the key document and the
1112
+ * claim inside it are served by the same host, so anyone able to serve a key
1113
+ * document can name any actor in the world as its owner. The claim becomes
1114
+ * meaningful only once the named actor's own document is fetched and turns
1115
+ * out to link back to the key. This function performs that mutual-link
1116
+ * check, which is the only sound way to learn a fetched key's owner.
1117
+ *
1118
+ * The claimed owner is always dereferenced, never read out of the key
1119
+ * document: an owner embedded there is written by whoever wrote the key.
1120
+ *
1121
+ * @param key The key whose ownership claim is to be verified. It must carry
1122
+ * an `id`, as that is what the owner has to link back to.
1123
+ * @param options Options for fetching the claimed owner.
1124
+ * @returns The verified owner, or `null` if the key claims no owner, the
1125
+ * owner cannot be fetched, or the owner does not link back to
1126
+ * the key.
1127
+ * @internal
1128
+ */
1129
+ async function verifyKeyOwnership(key, options = {}) {
1130
+ const logger = getLogger([
1131
+ "fedify",
1132
+ "sig",
1133
+ "key"
1134
+ ]);
1135
+ const keyId = key.id;
1136
+ if (keyId == null) return null;
1137
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1138
+ if (claimedOwnerId == null) return null;
1139
+ const owner = await fetchActorDocument(claimedOwnerId, options);
1140
+ if (owner == null) {
1141
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
1142
+ keyId: keyId.href,
1143
+ claimedOwnerId: claimedOwnerId.href
1144
+ });
1145
+ return null;
1146
+ }
1147
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
1148
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
1149
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
1150
+ keyId: keyId.href,
1151
+ claimedOwnerId: claimedOwnerId.href
1152
+ });
1153
+ return null;
1154
+ }
1043
1155
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1044
1156
  tracerProvider ??= trace.getTracerProvider();
1045
1157
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -1173,6 +1285,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
1173
1285
  async function clearFetchErrorMetadata(keyId, keyCache) {
1174
1286
  await keyCache?.setFetchError?.(keyId, null);
1175
1287
  }
1288
+ /**
1289
+ * Returns the identifier a fragment-bearing DID URL is a fragment of, or
1290
+ * `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
1291
+ * traced back to `did:web:example.com`, the DID that resolving the key went
1292
+ * through in the first place.
1293
+ */
1294
+ function stripFragment(keyId) {
1295
+ if (keyId.protocol !== "did:") return null;
1296
+ const did = new URL(keyId.href);
1297
+ did.hash = "";
1298
+ return did;
1299
+ }
1176
1300
  function isDidKeyUrl(keyId) {
1177
1301
  return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
1178
1302
  }
@@ -1228,7 +1352,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
1228
1352
  cached: false
1229
1353
  };
1230
1354
  }
1231
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
1355
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
1232
1356
  let object;
1233
1357
  try {
1234
1358
  object = await Object$1.fromJsonLd(document, {
@@ -1258,8 +1382,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1258
1382
  }
1259
1383
  }
1260
1384
  let key = null;
1261
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
1385
+ let ownerDocument = null;
1386
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
1262
1387
  else if (isActor(object)) {
1388
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
1389
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
1390
+ keyId,
1391
+ documentUrl: documentUrl.href,
1392
+ actorId: object.id?.href
1393
+ });
1394
+ await keyCache?.set(cacheKey, null);
1395
+ return {
1396
+ key: null,
1397
+ cached: false
1398
+ };
1399
+ }
1400
+ ownerDocument = object;
1263
1401
  const keys = cls === CryptographicKey ? object.getPublicKeys({
1264
1402
  documentLoader,
1265
1403
  contextLoader,
@@ -1321,6 +1459,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
1321
1459
  cached: false
1322
1460
  };
1323
1461
  }
1462
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
1463
+ if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
1464
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
1465
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
1466
+ if (await verifyKeyOwnership(key, {
1467
+ documentLoader,
1468
+ contextLoader,
1469
+ tracerProvider
1470
+ }) == null) {
1471
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
1472
+ keyId,
1473
+ claimedOwnerId: claimedOwnerId.href
1474
+ });
1475
+ await keyCache?.set(cacheKey, null);
1476
+ return {
1477
+ key: null,
1478
+ cached: false
1479
+ };
1480
+ }
1481
+ }
1482
+ }
1324
1483
  if (keyCache != null) {
1325
1484
  await keyCache.set(cacheKey, key);
1326
1485
  logger.debug("Key {keyId} cached.", { keyId });
@@ -1359,15 +1518,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1359
1518
  }
1360
1519
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1361
1520
  let document;
1521
+ let documentUrl = cacheKey;
1362
1522
  try {
1363
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
1523
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
1524
+ document = remoteDocument.document;
1525
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
1364
1526
  } catch (error) {
1365
1527
  const classified = classifyFetchError(error);
1366
1528
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1367
1529
  outcome = classified;
1368
1530
  return errored;
1369
1531
  }
1370
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1532
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
1371
1533
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1372
1534
  return resolved;
1373
1535
  } finally {
@@ -2482,4 +2644,4 @@ function timingSafeEqual(a, b) {
2482
2644
  return result === 0;
2483
2645
  }
2484
2646
  //#endregion
2485
- export { formatAcceptSignature as A, recordDocumentCache as C, recordOutboxEnqueue as D, recordOutboxActivity as E, version as F, parseAcceptSignature as M, validateAcceptSignature as N, recordWebFingerHandle as O, name as P, recordCollectionTotalItems as S, recordInboxActivity as T, measureSignatureKeyFetch as _, verifyRequestDetailed as a, recordCollectionPageItems as b, fetchKeyDetailed as c, validateCryptoKey as d, getDurationMs as f, isAbortError$1 as g, instrumentDocumentLoader as h, verifyRequest as i, fulfillAcceptSignature as j, registerQueueDepthGauge as k, generateCryptoKeyPair as l, getRemoteHost as m, parseRfc9421SignatureInput as n, exportJwk as o, getFederationMetrics as p, signRequest as r, fetchKey as s, doubleKnock as t, importJwk as u, recordCircuitBreakerStateChange as v, recordFanoutRecipients as w, recordCollectionRequest as x, recordCollectionDispatchDuration as y };
2647
+ export { recordWebFingerHandle as A, recordCollectionRequest as C, recordInboxActivity as D, recordFanoutRecipients as E, validateAcceptSignature as F, name as I, version as L, formatAcceptSignature as M, fulfillAcceptSignature as N, recordOutboxActivity as O, parseAcceptSignature as P, recordCollectionPageItems as S, recordDocumentCache as T, instrumentDocumentLoader as _, verifyRequestDetailed as a, recordCircuitBreakerStateChange as b, fetchKey as c, importJwk as d, validateCryptoKey as f, getRemoteHost as g, getFederationMetrics as h, verifyRequest as i, registerQueueDepthGauge as j, recordOutboxEnqueue as k, fetchKeyDetailed as l, getDurationMs as m, parseRfc9421SignatureInput as n, exportJwk as o, verifyKeyOwnership as p, signRequest as r, fetchActorDocument as s, doubleKnock as t, generateCryptoKeyPair as u, isAbortError$1 as v, recordCollectionTotalItems as w, recordCollectionDispatchDuration as x, measureSignatureKeyFetch as y };
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { MeterProvider, TracerProvider } from "@opentelemetry/api";
2
+ import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
4
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -59,6 +59,38 @@ interface FetchKeyOptions {
59
59
  * @since 2.3.0
60
60
  */
61
61
  meterProvider?: MeterProvider;
62
+ /**
63
+ * An assertion by the caller that it has already bound this key's id to
64
+ * whoever the key will be treated as speaking for, by means of its own that
65
+ * key resolution cannot see.
66
+ *
67
+ * Key resolution normally settles ownership itself: it dereferences the
68
+ * `owner`/`controller` a key claims and requires that actor's own document
69
+ * to link back to the key, because the claim and the key document come from
70
+ * the same host and so the claim proves nothing on its own. See
71
+ * GHSA-q9f8-5hc7-898f.
72
+ *
73
+ * That check presumes the ActivityPub shape of ownership, where the owner
74
+ * is an actor carrying a `publicKey`/`assertionMethod` list. It does not
75
+ * fit every caller. Portable objects (FEP-ef61) bind a key to an object
76
+ * structurally instead: {@link verifyPortableObjectProof} requires the
77
+ * verification method's FEP-ef34 origin to equal the object id's own
78
+ * authority, so the signer is pinned by the identifier being verified,
79
+ * before any key is fetched. Demanding the ActivityPub check as well would
80
+ * be impossible rather than merely redundant, since a DID dereferences to
81
+ * a DID document, which is not an actor.
82
+ *
83
+ * This is not a way to accept whatever a key says about itself. The claim
84
+ * still has to be corroborated, just from the key id rather than from
85
+ * a second document: a key may name an owner only when the key id is that
86
+ * owner's own (a `did:…#fragment` belongs to the DID it is a fragment of,
87
+ * exactly as a key embedded in an actor document belongs to that actor).
88
+ * A key naming anyone else falls back to the ordinary check and is rejected
89
+ * if it cannot pass it, so a key admitted here is as sound as any other and
90
+ * is cached like any other.
91
+ * @internal
92
+ */
93
+ keyIdBoundByCaller?: boolean;
62
94
  }
63
95
  /**
64
96
  * The result of {@link fetchKey}.
@@ -3,13 +3,13 @@ let _logtape_logtape = require("@logtape/logtape");
3
3
  let _fedify_vocab = require("@fedify/vocab");
4
4
  let _opentelemetry_api = require("@opentelemetry/api");
5
5
  let byte_encodings_hex = require("byte-encodings/hex");
6
- let structured_field_values = require("structured-field-values");
7
6
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
8
- let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
7
+ let structured_field_values = require("structured-field-values");
9
8
  let byte_encodings_base64 = require("byte-encodings/base64");
9
+ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
10
10
  //#region deno.json
11
11
  var name = "@fedify/fedify";
12
- var version = "2.4.0-dev.1964+1d28dc03";
12
+ var version = "2.4.0-dev.1995+618fd4e7";
13
13
  //#endregion
14
14
  //#region ../../node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/root.js
15
15
  const NativeTemporal = globalThis.Temporal;
@@ -4603,6 +4603,118 @@ async function importJwk(jwk, type) {
4603
4603
  validateCryptoKey(key, type);
4604
4604
  return key;
4605
4605
  }
4606
+ /**
4607
+ * Fetches the document that the given actor URI dereferences to, and returns
4608
+ * the actor only if the host that served the document is authoritative for
4609
+ * the id the document claims.
4610
+ *
4611
+ * Only the origin that serves an actor id can speak for it. Without that
4612
+ * rule any host could serve a document describing somebody else's actor—and
4613
+ * listing its own keys as that actor's.
4614
+ *
4615
+ * @param actorId The URI of the actor to fetch.
4616
+ * @param options Options for fetching the document.
4617
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
4618
+ * actor, or belongs to another origin.
4619
+ * @internal
4620
+ */
4621
+ async function fetchActorDocument(actorId, options = {}) {
4622
+ const logger = (0, _logtape_logtape.getLogger)([
4623
+ "fedify",
4624
+ "sig",
4625
+ "key"
4626
+ ]);
4627
+ const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
4628
+ const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
4629
+ const { tracerProvider } = options;
4630
+ let document;
4631
+ let documentUrl = actorId;
4632
+ try {
4633
+ const remoteDocument = await documentLoader(actorId.href);
4634
+ document = remoteDocument.document;
4635
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
4636
+ } catch (error) {
4637
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
4638
+ actorId: actorId.href,
4639
+ error
4640
+ });
4641
+ return null;
4642
+ }
4643
+ let object;
4644
+ try {
4645
+ object = await _fedify_vocab.Object.fromJsonLd(document, {
4646
+ documentLoader,
4647
+ contextLoader,
4648
+ tracerProvider,
4649
+ baseUrl: documentUrl
4650
+ });
4651
+ } catch (error) {
4652
+ if (!(error instanceof TypeError)) throw error;
4653
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
4654
+ documentUrl: documentUrl.href,
4655
+ error
4656
+ });
4657
+ return null;
4658
+ }
4659
+ if (!(0, _fedify_vocab.isActor)(object)) return null;
4660
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
4661
+ logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
4662
+ documentUrl: documentUrl.href,
4663
+ actorId: object.id?.href
4664
+ });
4665
+ return null;
4666
+ }
4667
+ return object;
4668
+ }
4669
+ /**
4670
+ * Resolves the owner that the given key claims, and returns it only if the
4671
+ * claim holds.
4672
+ *
4673
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
4674
+ * a {@link Multikey}, proves nothing on its own: the key document and the
4675
+ * claim inside it are served by the same host, so anyone able to serve a key
4676
+ * document can name any actor in the world as its owner. The claim becomes
4677
+ * meaningful only once the named actor's own document is fetched and turns
4678
+ * out to link back to the key. This function performs that mutual-link
4679
+ * check, which is the only sound way to learn a fetched key's owner.
4680
+ *
4681
+ * The claimed owner is always dereferenced, never read out of the key
4682
+ * document: an owner embedded there is written by whoever wrote the key.
4683
+ *
4684
+ * @param key The key whose ownership claim is to be verified. It must carry
4685
+ * an `id`, as that is what the owner has to link back to.
4686
+ * @param options Options for fetching the claimed owner.
4687
+ * @returns The verified owner, or `null` if the key claims no owner, the
4688
+ * owner cannot be fetched, or the owner does not link back to
4689
+ * the key.
4690
+ * @internal
4691
+ */
4692
+ async function verifyKeyOwnership(key, options = {}) {
4693
+ const logger = (0, _logtape_logtape.getLogger)([
4694
+ "fedify",
4695
+ "sig",
4696
+ "key"
4697
+ ]);
4698
+ const keyId = key.id;
4699
+ if (keyId == null) return null;
4700
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
4701
+ if (claimedOwnerId == null) return null;
4702
+ const owner = await fetchActorDocument(claimedOwnerId, options);
4703
+ if (owner == null) {
4704
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
4705
+ keyId: keyId.href,
4706
+ claimedOwnerId: claimedOwnerId.href
4707
+ });
4708
+ return null;
4709
+ }
4710
+ const linkedKeyIds = key instanceof _fedify_vocab.CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
4711
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
4712
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
4713
+ keyId: keyId.href,
4714
+ claimedOwnerId: claimedOwnerId.href
4715
+ });
4716
+ return null;
4717
+ }
4606
4718
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
4607
4719
  tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
4608
4720
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -4736,6 +4848,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
4736
4848
  async function clearFetchErrorMetadata(keyId, keyCache) {
4737
4849
  await keyCache?.setFetchError?.(keyId, null);
4738
4850
  }
4851
+ /**
4852
+ * Returns the identifier a fragment-bearing DID URL is a fragment of, or
4853
+ * `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
4854
+ * traced back to `did:web:example.com`, the DID that resolving the key went
4855
+ * through in the first place.
4856
+ */
4857
+ function stripFragment(keyId) {
4858
+ if (keyId.protocol !== "did:") return null;
4859
+ const did = new URL(keyId.href);
4860
+ did.hash = "";
4861
+ return did;
4862
+ }
4739
4863
  function isDidKeyUrl(keyId) {
4740
4864
  return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
4741
4865
  }
@@ -4791,7 +4915,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
4791
4915
  cached: false
4792
4916
  };
4793
4917
  }
4794
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
4918
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
4795
4919
  let object;
4796
4920
  try {
4797
4921
  object = await _fedify_vocab.Object.fromJsonLd(document, {
@@ -4821,8 +4945,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
4821
4945
  }
4822
4946
  }
4823
4947
  let key = null;
4824
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
4948
+ let ownerDocument = null;
4949
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
4825
4950
  else if ((0, _fedify_vocab.isActor)(object)) {
4951
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
4952
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
4953
+ keyId,
4954
+ documentUrl: documentUrl.href,
4955
+ actorId: object.id?.href
4956
+ });
4957
+ await keyCache?.set(cacheKey, null);
4958
+ return {
4959
+ key: null,
4960
+ cached: false
4961
+ };
4962
+ }
4963
+ ownerDocument = object;
4826
4964
  const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
4827
4965
  documentLoader,
4828
4966
  contextLoader,
@@ -4884,6 +5022,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
4884
5022
  cached: false
4885
5023
  };
4886
5024
  }
5025
+ const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
5026
+ if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
5027
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
5028
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
5029
+ if (await verifyKeyOwnership(key, {
5030
+ documentLoader,
5031
+ contextLoader,
5032
+ tracerProvider
5033
+ }) == null) {
5034
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
5035
+ keyId,
5036
+ claimedOwnerId: claimedOwnerId.href
5037
+ });
5038
+ await keyCache?.set(cacheKey, null);
5039
+ return {
5040
+ key: null,
5041
+ cached: false
5042
+ };
5043
+ }
5044
+ }
5045
+ }
4887
5046
  if (keyCache != null) {
4888
5047
  await keyCache.set(cacheKey, key);
4889
5048
  logger.debug("Key {keyId} cached.", { keyId });
@@ -4922,15 +5081,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
4922
5081
  }
4923
5082
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
4924
5083
  let document;
5084
+ let documentUrl = cacheKey;
4925
5085
  try {
4926
- document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
5086
+ const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
5087
+ document = remoteDocument.document;
5088
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
4927
5089
  } catch (error) {
4928
5090
  const classified = classifyFetchError(error);
4929
5091
  const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
4930
5092
  outcome = classified;
4931
5093
  return errored;
4932
5094
  }
4933
- const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
5095
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
4934
5096
  outcome = { result: resolved.key != null ? "fetched" : "invalid" };
4935
5097
  return resolved;
4936
5098
  } finally {
@@ -6064,6 +6226,12 @@ Object.defineProperty(exports, "exportJwk", {
6064
6226
  return exportJwk;
6065
6227
  }
6066
6228
  });
6229
+ Object.defineProperty(exports, "fetchActorDocument", {
6230
+ enumerable: true,
6231
+ get: function() {
6232
+ return fetchActorDocument;
6233
+ }
6234
+ });
6067
6235
  Object.defineProperty(exports, "fetchKey", {
6068
6236
  enumerable: true,
6069
6237
  get: function() {
@@ -6244,6 +6412,12 @@ Object.defineProperty(exports, "validateCryptoKey", {
6244
6412
  return validateCryptoKey;
6245
6413
  }
6246
6414
  });
6415
+ Object.defineProperty(exports, "verifyKeyOwnership", {
6416
+ enumerable: true,
6417
+ get: function() {
6418
+ return verifyKeyOwnership;
6419
+ }
6420
+ });
6247
6421
  Object.defineProperty(exports, "verifyRequest", {
6248
6422
  enumerable: true,
6249
6423
  get: function() {
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CtG93ohj.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-B9N27cCB.mjs";
4
+ import { n as version, t as name } from "./deno-CABhzCb_.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CISXCvB1.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
7
- import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-BVjgbs_9.mjs";
7
+ import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-Ccs8cnd7.mjs";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey } from "@fedify/vocab";
10
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";