@fedify/fedify 2.4.0-dev.1962 → 2.4.0-dev.1988

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/{builder-CWYQXC2W.mjs → builder-Cts78G9N.mjs} +2 -2
  2. package/dist/{client-CtxvmazV.mjs → client-DpnpgjyX.mjs} +78 -4
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{context-COLwHogJ.d.cts → context-BeDnDuF3.d.cts} +4 -3
  7. package/dist/{context-DiiCt4S7.d.ts → context-uyRUWoZQ.d.ts} +4 -3
  8. package/dist/{deno-CfLMX4HF.mjs → deno-BgMEefEr.mjs} +1 -1
  9. package/dist/{docloader-CDmmYxYb.mjs → docloader-DEYs99FZ.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/handler.test.mjs +122 -4
  12. package/dist/federation/idempotency.test.mjs +2 -2
  13. package/dist/federation/keycache.test.mjs +52 -9
  14. package/dist/federation/metrics.test.mjs +1 -1
  15. package/dist/federation/middleware.test.mjs +111 -15
  16. package/dist/federation/mod.cjs +1 -1
  17. package/dist/federation/mod.d.cts +2 -2
  18. package/dist/federation/mod.d.ts +2 -2
  19. package/dist/federation/mod.js +1 -1
  20. package/dist/federation/send.test.mjs +139 -4
  21. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  22. package/dist/federation/tasks/tasks.test.mjs +2 -2
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{federation-CIeDMHxZ.cjs → federation-DzPUG_AP.cjs} +93 -17
  26. package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
  27. package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
  28. package/dist/{http-DWZZicb2.js → http-CgwqtxAy.js} +169 -7
  29. package/dist/{http-BR190IDB.mjs → http-CtidZS_t.mjs} +3 -3
  30. package/dist/{http-C3kkrvAE.cjs → http-Z3anvCd-.cjs} +180 -6
  31. package/dist/{key-BcA-AzPp.mjs → key-Cwbg5ggV.mjs} +169 -7
  32. package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
  33. package/dist/{kv-cache-DCItgZUM.cjs → kv-cache-BM2-w-Wq.cjs} +2 -2
  34. package/dist/{kv-cache-5d2HgB_-.js → kv-cache-CykuL0ya.js} +2 -2
  35. package/dist/{kv-cache-m962_cok.mjs → kv-cache-DHiR8yTf.mjs} +1 -1
  36. package/dist/{ld-QpPkmUjy.mjs → ld-CzhdWyTt.mjs} +3 -3
  37. package/dist/{metrics-Dkyzq7ml.mjs → metrics-CrqsHP0l.mjs} +1 -1
  38. package/dist/{middleware-BdKtWJK1.mjs → middleware-CZpYxSKD.mjs} +1 -1
  39. package/dist/{middleware-BNktRUnj.js → middleware-CeCj5caM.js} +101 -25
  40. package/dist/{middleware-BaZ32nCi.mjs → middleware-DCpiAsZi.mjs} +34 -23
  41. package/dist/{mod-DypxPS9H.d.ts → mod-C0BYc0OU.d.ts} +1 -1
  42. package/dist/{mod-CnGLNoVw.d.cts → mod-CbKDOnCt.d.cts} +1 -1
  43. package/dist/{mod-CLHkCwLm.d.cts → mod-CyiSjRkk.d.cts} +1 -1
  44. package/dist/{mod-CSH1Ee-I.d.ts → mod-D882_Xsg.d.ts} +1 -1
  45. package/dist/{mod-DzRXACM5.d.ts → mod-UDwMjuHM.d.ts} +1 -1
  46. package/dist/{mod-BZldCEU0.d.cts → mod-x9W0d0Pk.d.cts} +1 -1
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +28 -1
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.js +1 -1
  55. package/dist/{owner-r_waTiQj.d.cts → owner-BF0L9cMt.d.cts} +11 -0
  56. package/dist/{owner-DGSfntI-.d.ts → owner-BSzr5-I8.d.ts} +11 -0
  57. package/dist/owner-BqVXJLW8.mjs +175 -0
  58. package/dist/{proof-DrWB_vel.js → proof-CUghqhAf.js} +122 -65
  59. package/dist/{proof-CYdLcTFg.cjs → proof-DHu5zI0Z.cjs} +122 -65
  60. package/dist/{proof-Mov6-gSw.mjs → proof-DL8Iid3a.mjs} +14 -11
  61. package/dist/{send-DB1mkFKO.mjs → send-6jS4xFsX.mjs} +68 -11
  62. package/dist/sig/http.test.mjs +2 -2
  63. package/dist/sig/key.test.mjs +169 -2
  64. package/dist/sig/ld.test.mjs +55 -2
  65. package/dist/sig/mod.cjs +2 -2
  66. package/dist/sig/mod.d.cts +3 -3
  67. package/dist/sig/mod.d.ts +3 -3
  68. package/dist/sig/mod.js +2 -2
  69. package/dist/sig/owner.test.mjs +103 -4
  70. package/dist/sig/proof.test.mjs +143 -2
  71. package/dist/{temporal-Yi0JAV9z.mjs → temporal-DcyzeytP.mjs} +1 -1
  72. package/dist/testing/kv.test.mjs +1 -1
  73. package/dist/testing/mod.d.mts +2 -1
  74. package/dist/{types-RXm5YxpC.cjs → types-Be2zbyeG.cjs} +94 -2
  75. package/dist/{types-Dtyf8lVj.js → types-C_cNlNVK.js} +78 -4
  76. package/dist/utils/docloader.test.mjs +27 -3
  77. package/dist/utils/kv-cache.test.mjs +1 -1
  78. package/dist/utils/mod.cjs +1 -1
  79. package/dist/utils/mod.d.cts +1 -1
  80. package/dist/utils/mod.d.ts +1 -1
  81. package/dist/utils/mod.js +1 -1
  82. package/package.json +7 -7
  83. package/dist/owner-B795xXwU.mjs +0 -120
@@ -1,6 +1,6 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
- const require_http = require("./http-C3kkrvAE.cjs");
3
+ const require_http = require("./http-Z3anvCd-.cjs");
4
4
  let _logtape_logtape = require("@logtape/logtape");
5
5
  let _fedify_vocab = require("@fedify/vocab");
6
6
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -12,7 +12,7 @@ _fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_ru
12
12
  let json_canon = require("json-canon");
13
13
  json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
14
14
  //#region src/sig/ld.ts
15
- const logger$3 = (0, _logtape_logtape.getLogger)([
15
+ const logger$4 = (0, _logtape_logtape.getLogger)([
16
16
  "fedify",
17
17
  "sig",
18
18
  "ld"
@@ -455,7 +455,7 @@ async function verifySignature(jsonLd, options = {}) {
455
455
  try {
456
456
  signature = (0, byte_encodings_base64.decodeBase64)(sig.signatureValue);
457
457
  } catch (error) {
458
- logger$3.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
458
+ logger$4.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
459
459
  ...sig,
460
460
  error
461
461
  });
@@ -474,7 +474,7 @@ async function verifySignature(jsonLd, options = {}) {
474
474
  try {
475
475
  sigOptsHash = await hashJsonLd(sigOpts, options.contextLoader);
476
476
  } catch (error) {
477
- logger$3.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
477
+ logger$4.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
478
478
  signatureOptions: sigOpts,
479
479
  error
480
480
  });
@@ -486,7 +486,7 @@ async function verifySignature(jsonLd, options = {}) {
486
486
  try {
487
487
  docHash = await hashJsonLd(document, options.contextLoader);
488
488
  } catch (error) {
489
- logger$3.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
489
+ logger$4.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
490
490
  document,
491
491
  error
492
492
  });
@@ -497,7 +497,7 @@ async function verifySignature(jsonLd, options = {}) {
497
497
  const messageBytes = encoder.encode(message);
498
498
  if (await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes)) return key;
499
499
  if (cached) {
500
- logger$3.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
500
+ logger$4.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
501
501
  keyId: sig.creator,
502
502
  ...sig
503
503
  });
@@ -511,7 +511,7 @@ async function verifySignature(jsonLd, options = {}) {
511
511
  if (key == null) return null;
512
512
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
513
513
  }
514
- logger$3.debug("Failed to verify with the fetched key {keyId}; signature {signatureValue} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
514
+ logger$4.debug("Failed to verify with the fetched key {keyId}; signature {signatureValue} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
515
515
  keyId: sig.creator,
516
516
  ...sig,
517
517
  message
@@ -590,12 +590,12 @@ async function verifyJsonLdInternal(jsonLd, options, compact) {
590
590
  const key = await verifySignature(compacted, verificationOptions);
591
591
  if (key == null) return false;
592
592
  if (key.ownerId == null) {
593
- logger$3.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
593
+ logger$4.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
594
594
  return false;
595
595
  }
596
596
  attributions.delete(key.ownerId.href);
597
597
  if (attributions.size > 0) {
598
- logger$3.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
598
+ logger$4.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
599
599
  return false;
600
600
  }
601
601
  verified = true;
@@ -624,8 +624,20 @@ async function hashJsonLd(jsonLd, contextLoader) {
624
624
  }
625
625
  //#endregion
626
626
  //#region src/sig/owner.ts
627
+ const logger$3 = (0, _logtape_logtape.getLogger)([
628
+ "fedify",
629
+ "sig",
630
+ "owner"
631
+ ]);
627
632
  /**
628
633
  * Checks if the actor of the given activity owns the specified key.
634
+ *
635
+ * Ownership is never taken from what the activity or the key say about
636
+ * themselves, as both are written by whoever sent the activity. It is
637
+ * established from a document that the owner itself serves: either the key's
638
+ * claimed owner links back to the key, or the actor the activity claims lists
639
+ * the key as its own.
640
+ *
629
641
  * @param activity The activity to check.
630
642
  * @param key The public key to check.
631
643
  * @param options Options for checking the key ownership.
@@ -640,23 +652,30 @@ async function doesActorOwnKey(activity, key, options) {
640
652
  }
641
653
  }, async (span) => {
642
654
  try {
643
- if (key.ownerId != null) {
644
- const owns = key.ownerId.href === activity.actorId?.href;
645
- span.setAttribute("activitypub.key_ownership.verified", owns);
646
- span.setAttribute("activitypub.key_ownership.method", "owner_id");
647
- return owns;
648
- }
649
- const actor = await activity.getActor(options);
650
- if (actor == null || !(0, _fedify_vocab.isActor)(actor)) {
655
+ const actorId = activity.actorId;
656
+ if (actorId == null) {
651
657
  span.setAttribute("activitypub.key_ownership.verified", false);
652
- span.setAttribute("activitypub.key_ownership.method", "actor_fetch");
658
+ span.setAttribute("activitypub.key_ownership.method", "none");
653
659
  return false;
654
660
  }
655
- for (const publicKeyId of actor.publicKeyIds) if (key.id != null && publicKeyId.href === key.id.href) {
661
+ if (key.ownerId != null) {
662
+ const owner = await require_http.verifyKeyOwnership(key, options);
663
+ if (owner?.id != null && owner.id.href === actorId.href) {
664
+ span.setAttribute("activitypub.key_ownership.verified", true);
665
+ span.setAttribute("activitypub.key_ownership.method", "key_owner");
666
+ return true;
667
+ }
668
+ }
669
+ const actor = await require_http.fetchActorDocument(actorId, options);
670
+ if (actor != null && await doesActorListKey(actor, key, options)) {
656
671
  span.setAttribute("activitypub.key_ownership.verified", true);
657
672
  span.setAttribute("activitypub.key_ownership.method", "actor_fetch");
658
673
  return true;
659
674
  }
675
+ logger$3.debug("The actor {actorId} does not own key {keyId}: neither the owner the key claims nor the actor's own document links the two to each other.", {
676
+ actorId: actorId.href,
677
+ keyId: key.id?.href
678
+ });
660
679
  span.setAttribute("activitypub.key_ownership.verified", false);
661
680
  span.setAttribute("activitypub.key_ownership.method", "actor_fetch");
662
681
  return false;
@@ -673,9 +692,46 @@ async function doesActorOwnKey(activity, key, options) {
673
692
  });
674
693
  }
675
694
  /**
695
+ * Determines whether the given actor's own document lists the given key among
696
+ * its public keys.
697
+ *
698
+ * Keys are matched by id, which is what an actor document links to. A key
699
+ * document may leave its id implicit, though, and then there is no id for the
700
+ * actor to link to; the key material itself is the only thing the two can
701
+ * have in common, so that is what gets compared.
702
+ */
703
+ async function doesActorListKey(actor, key, options) {
704
+ if (key.id != null) {
705
+ for (const publicKeyId of actor.publicKeyIds) if (publicKeyId.href === key.id.href) return true;
706
+ return false;
707
+ }
708
+ if (key.publicKey == null) return false;
709
+ const publicKeys = actor.getPublicKeys({
710
+ ...options,
711
+ suppressError: true
712
+ });
713
+ for await (const publicKey of publicKeys) {
714
+ if (publicKey.publicKey == null) continue;
715
+ if (await isSamePublicKey(publicKey.publicKey, key.publicKey)) return true;
716
+ }
717
+ return false;
718
+ }
719
+ /**
720
+ * Compares two public keys by their key material, ignoring the metadata that
721
+ * surrounds it.
722
+ */
723
+ async function isSamePublicKey(a, b) {
724
+ const [x, y] = await Promise.all([require_http.exportJwk(a), require_http.exportJwk(b)]);
725
+ return x.kty === y.kty && x.crv === y.crv && x.n === y.n && x.e === y.e && x.x === y.x && x.y === y.y;
726
+ }
727
+ /**
676
728
  * Gets the actor that owns the specified key. Returns `null` if the key has no
677
729
  * known owner.
678
730
  *
731
+ * The owner is only returned when the key and the owner link to each other:
732
+ * a document that merely claims to own a key is not enough, as the claim and
733
+ * the key can come from the same host.
734
+ *
679
735
  * @param keyId The ID of the key to check, or the key itself.
680
736
  * @param options Options for getting the key owner.
681
737
  * @returns The actor that owns the key, or `null` if the key has no known
@@ -685,52 +741,50 @@ async function doesActorOwnKey(activity, key, options) {
685
741
  async function getKeyOwner(keyId, options) {
686
742
  const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
687
743
  const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
688
- const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
744
+ const fetchOptions = {
745
+ documentLoader,
746
+ contextLoader: options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)(),
747
+ tracerProvider
748
+ };
749
+ if (keyId instanceof _fedify_vocab.CryptographicKey) return await require_http.verifyKeyOwnership(keyId, fetchOptions);
750
+ let keyDoc;
751
+ let documentUrl = keyId;
752
+ try {
753
+ const remoteDocument = await documentLoader(keyId.href);
754
+ keyDoc = remoteDocument.document;
755
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", keyId);
756
+ } catch (_) {
757
+ return null;
758
+ }
689
759
  let object;
690
- if (keyId instanceof _fedify_vocab.CryptographicKey) {
691
- object = keyId;
692
- if (object.id == null) return null;
693
- keyId = object.id;
694
- } else {
695
- let keyDoc;
696
- try {
697
- const { document } = await documentLoader(keyId.href);
698
- keyDoc = document;
699
- } catch (_) {
700
- return null;
701
- }
760
+ try {
761
+ object = await _fedify_vocab.Object.fromJsonLd(keyDoc, {
762
+ ...fetchOptions,
763
+ baseUrl: documentUrl
764
+ });
765
+ } catch (e) {
766
+ if (!(e instanceof TypeError)) throw e;
702
767
  try {
703
- object = await _fedify_vocab.Object.fromJsonLd(keyDoc, {
704
- documentLoader,
705
- contextLoader,
706
- tracerProvider
707
- });
768
+ object = await _fedify_vocab.CryptographicKey.fromJsonLd(keyDoc, fetchOptions);
708
769
  } catch (e) {
709
- if (!(e instanceof TypeError)) throw e;
710
- try {
711
- object = await _fedify_vocab.CryptographicKey.fromJsonLd(keyDoc, {
712
- documentLoader,
713
- contextLoader,
714
- tracerProvider
715
- });
716
- } catch (e) {
717
- if (e instanceof TypeError) return null;
718
- throw e;
719
- }
770
+ if (e instanceof TypeError) return null;
771
+ throw e;
720
772
  }
721
773
  }
722
- let owner = null;
723
774
  if (object instanceof _fedify_vocab.CryptographicKey) {
724
- if (object.ownerId == null) return null;
725
- owner = await object.getOwner({
726
- documentLoader,
727
- contextLoader,
728
- tracerProvider
775
+ if (object.id != null && object.id.href !== keyId.href) return null;
776
+ return await require_http.verifyKeyOwnership(object.id == null ? object.clone({ id: keyId }) : object, fetchOptions);
777
+ }
778
+ if (!(0, _fedify_vocab.isActor)(object)) return null;
779
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
780
+ logger$3.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as the owner of key {keyId}.", {
781
+ documentUrl: documentUrl.href,
782
+ actorId: object.id?.href,
783
+ keyId: keyId.href
729
784
  });
730
- } else if ((0, _fedify_vocab.isActor)(object)) owner = object;
731
- else return null;
732
- if (owner == null) return null;
733
- for (const kid of owner.publicKeyIds) if (kid.href === keyId.href) return owner;
785
+ return null;
786
+ }
787
+ for (const kid of object.publicKeyIds) if (kid.href === keyId.href) return object;
734
788
  return null;
735
789
  }
736
790
  //#endregion
@@ -1265,7 +1319,7 @@ async function signObject(object, privateKey, keyId, options = {}) {
1265
1319
  async function verifyProof(jsonLd, proof, options = {}) {
1266
1320
  return await verifyProofWithMessageDigestCache(jsonLd, proof, options);
1267
1321
  }
1268
- async function verifyProofWithMessageDigestCache(jsonLd, proof, options, messageDigestCache = {}, rawProofCandidate) {
1322
+ async function verifyProofWithMessageDigestCache(jsonLd, proof, options, messageDigestCache = {}, rawProofCandidate, keyIdBoundByCaller = false) {
1269
1323
  return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(require_http.name, require_http.version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
1270
1324
  const start = performance.now();
1271
1325
  let verified = false;
@@ -1277,7 +1331,7 @@ async function verifyProofWithMessageDigestCache(jsonLd, proof, options, message
1277
1331
  if (proof.proofValue != null) span.setAttribute("object_integrity_proofs.signature", (0, byte_encodings_hex.encodeHex)(proof.proofValue));
1278
1332
  }
1279
1333
  try {
1280
- const key = await verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate);
1334
+ const key = await verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
1281
1335
  if (key == null) span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1282
1336
  else verified = true;
1283
1337
  return key;
@@ -1594,12 +1648,15 @@ async function hasValidProofOptions(proofConfig, options, documentLoader) {
1594
1648
  if (previousProof == null || previousProof.present && typeof previousProof.value !== "string" && (!Array.isArray(previousProof.value) || previousProof.value.some((item) => typeof item !== "string"))) return false;
1595
1649
  return true;
1596
1650
  }
1597
- async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate) {
1651
+ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate, keyIdBoundByCaller = false) {
1598
1652
  if (!isJsonLdNode(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
1599
1653
  const proofContextLoader = messageDigestCache.proofContextLoader ?? preloadedOnlyDocumentLoader;
1600
1654
  const proofConfiguration = await createProofConfiguration(jsonLd, proof, options, proofContextLoader, rawProofCandidate);
1601
1655
  if (proofConfiguration == null || !await hasValidProofOptions(proofConfiguration.value, options, proofContextLoader)) return null;
1602
- const publicKeyPromise = require_http.measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => require_http.fetchKey(proof.verificationMethodId, _fedify_vocab.Multikey, options));
1656
+ const publicKeyPromise = require_http.measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => require_http.fetchKey(proof.verificationMethodId, _fedify_vocab.Multikey, {
1657
+ ...options,
1658
+ keyIdBoundByCaller
1659
+ }));
1603
1660
  const proofBytes = new TextEncoder().encode((0, json_canon.default)(proofConfiguration.value));
1604
1661
  const proofDigest = await crypto.subtle.digest("SHA-256", proofBytes);
1605
1662
  let fetchedKey;
@@ -1633,7 +1690,7 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
1633
1690
  get: () => Promise.resolve(void 0),
1634
1691
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
1635
1692
  }
1636
- }, messageDigestCache, rawProofCandidate);
1693
+ }, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
1637
1694
  }
1638
1695
  logger.debug("The fetched key (verificationMethod) for the proof is not a valid Ed25519 key:\n{keyId}", {
1639
1696
  proof,
@@ -1670,7 +1727,7 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
1670
1727
  get: () => Promise.resolve(void 0),
1671
1728
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
1672
1729
  }
1673
- }, messageDigestCache, rawProofCandidate);
1730
+ }, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
1674
1731
  }
1675
1732
  logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
1676
1733
  keyId: proof.verificationMethodId.href,
@@ -1885,7 +1942,7 @@ async function verifyPortableObjectProof(jsonLd, options = {}) {
1885
1942
  proofIndex
1886
1943
  }
1887
1944
  };
1888
- const key = await verifyProofWithMessageDigestCache(jsonLd, proofs[proofIndex], options, messageDigestCache, rawProofCandidate);
1945
+ const key = await verifyProofWithMessageDigestCache(jsonLd, proofs[proofIndex], options, messageDigestCache, rawProofCandidate, true);
1889
1946
  if (key == null) return {
1890
1947
  verified: false,
1891
1948
  reason: {
@@ -1,12 +1,12 @@
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-CfLMX4HF.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-Dkyzq7ml.mjs";
6
- import { n as fetchKey, o as validateCryptoKey } from "./key-BcA-AzPp.mjs";
4
+ import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CrqsHP0l.mjs";
6
+ import { r as fetchKey, s as validateCryptoKey } from "./key-Cwbg5ggV.mjs";
7
7
  import { n as preloadedOnlyDocumentLoader } from "./public-audience-Cc2YJ3ip.mjs";
8
8
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-B5Ne7JoL.mjs";
9
- import { c as getNormalizationContextLoader } from "./ld-QpPkmUjy.mjs";
9
+ import { c as getNormalizationContextLoader } from "./ld-CzhdWyTt.mjs";
10
10
  import { getLogger } from "@logtape/logtape";
11
11
  import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
12
12
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -164,7 +164,7 @@ async function signObject(object, privateKey, keyId, options = {}) {
164
164
  async function verifyProof(jsonLd, proof, options = {}) {
165
165
  return await verifyProofWithMessageDigestCache(jsonLd, proof, options);
166
166
  }
167
- async function verifyProofWithMessageDigestCache(jsonLd, proof, options, messageDigestCache = {}, rawProofCandidate) {
167
+ async function verifyProofWithMessageDigestCache(jsonLd, proof, options, messageDigestCache = {}, rawProofCandidate, keyIdBoundByCaller = false) {
168
168
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
169
169
  const start = performance.now();
170
170
  let verified = false;
@@ -176,7 +176,7 @@ async function verifyProofWithMessageDigestCache(jsonLd, proof, options, message
176
176
  if (proof.proofValue != null) span.setAttribute("object_integrity_proofs.signature", encodeHex(proof.proofValue));
177
177
  }
178
178
  try {
179
- const key = await verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate);
179
+ const key = await verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
180
180
  if (key == null) span.setStatus({ code: SpanStatusCode.ERROR });
181
181
  else verified = true;
182
182
  return key;
@@ -493,12 +493,15 @@ async function hasValidProofOptions(proofConfig, options, documentLoader) {
493
493
  if (previousProof == null || previousProof.present && typeof previousProof.value !== "string" && (!Array.isArray(previousProof.value) || previousProof.value.some((item) => typeof item !== "string"))) return false;
494
494
  return true;
495
495
  }
496
- async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate) {
496
+ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate, keyIdBoundByCaller = false) {
497
497
  if (!isJsonLdNode(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
498
498
  const proofContextLoader = messageDigestCache.proofContextLoader ?? preloadedOnlyDocumentLoader;
499
499
  const proofConfiguration = await createProofConfiguration(jsonLd, proof, options, proofContextLoader, rawProofCandidate);
500
500
  if (proofConfiguration == null || !await hasValidProofOptions(proofConfiguration.value, options, proofContextLoader)) return null;
501
- const publicKeyPromise = measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => fetchKey(proof.verificationMethodId, Multikey, options));
501
+ const publicKeyPromise = measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => fetchKey(proof.verificationMethodId, Multikey, {
502
+ ...options,
503
+ keyIdBoundByCaller
504
+ }));
502
505
  const proofBytes = new TextEncoder().encode(serialize(proofConfiguration.value));
503
506
  const proofDigest = await crypto.subtle.digest("SHA-256", proofBytes);
504
507
  let fetchedKey;
@@ -532,7 +535,7 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
532
535
  get: () => Promise.resolve(void 0),
533
536
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
534
537
  }
535
- }, messageDigestCache, rawProofCandidate);
538
+ }, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
536
539
  }
537
540
  logger.debug("The fetched key (verificationMethod) for the proof is not a valid Ed25519 key:\n{keyId}", {
538
541
  proof,
@@ -569,7 +572,7 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
569
572
  get: () => Promise.resolve(void 0),
570
573
  set: async (keyId, key) => await options.keyCache?.set(keyId, key)
571
574
  }
572
- }, messageDigestCache, rawProofCandidate);
575
+ }, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
573
576
  }
574
577
  logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
575
578
  keyId: proof.verificationMethodId.href,
@@ -784,7 +787,7 @@ async function verifyPortableObjectProof(jsonLd, options = {}) {
784
787
  proofIndex
785
788
  }
786
789
  };
787
- const key = await verifyProofWithMessageDigestCache(jsonLd, proofs[proofIndex], options, messageDigestCache, rawProofCandidate);
790
+ const key = await verifyProofWithMessageDigestCache(jsonLd, proofs[proofIndex], options, messageDigestCache, rawProofCandidate, true);
788
791
  if (key == null) return {
789
792
  verified: false,
790
793
  reason: {
@@ -1,12 +1,12 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CfLMX4HF.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics } from "./metrics-Dkyzq7ml.mjs";
6
- import { n as doubleKnock } from "./http-BR190IDB.mjs";
4
+ import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics } from "./metrics-CrqsHP0l.mjs";
6
+ import { n as doubleKnock } from "./http-CtidZS_t.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
9
- import { FetchError } from "@fedify/vocab-runtime";
9
+ import { FetchError, UrlError, validatePublicUrl } from "@fedify/vocab-runtime";
10
10
  //#region src/federation/send.ts
11
11
  /**
12
12
  * Extracts the inbox URLs from recipients.
@@ -112,7 +112,7 @@ async function readLimitedResponseBody(response, maxBytes) {
112
112
  if (truncated) result += "… (truncated)";
113
113
  return result;
114
114
  }
115
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
115
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
116
116
  const logger = getLogger([
117
117
  "fedify",
118
118
  "federation",
@@ -121,12 +121,25 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
121
121
  const federationMetrics = getFederationMetrics(meterProvider);
122
122
  const started = performance.now();
123
123
  let deliverySuccess = false;
124
+ async function validateUrl(url) {
125
+ if (!allowPrivateAddress) try {
126
+ await validatePublicUrl(url);
127
+ } catch (error) {
128
+ if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
129
+ url,
130
+ error
131
+ });
132
+ throw error;
133
+ }
134
+ }
135
+ await validateUrl(inbox.href);
124
136
  headers = new Headers(headers);
125
137
  headers.set("Content-Type", "application/activity+json");
126
138
  const request = new Request(inbox, {
127
139
  method: "POST",
128
140
  headers,
129
- body: JSON.stringify(activity)
141
+ body: JSON.stringify(activity),
142
+ redirect: "manual"
130
143
  });
131
144
  let rsaKey = null;
132
145
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
@@ -142,19 +155,20 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
142
155
  });
143
156
  let response;
144
157
  try {
145
- response = rsaKey == null ? await fetch(request) : await doubleKnock(request, rsaKey, {
158
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
146
159
  tracerProvider,
147
- specDeterminer
160
+ specDeterminer,
161
+ validateRedirect: validateUrl
148
162
  });
149
163
  } catch (error) {
150
- const transportError = error instanceof FetchError ? error : createFetchError(inbox.href, error);
164
+ const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
151
165
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
152
166
  activityId,
153
167
  inbox: inbox.href,
154
- error: transportError
168
+ error: failure
155
169
  });
156
170
  federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
157
- throw transportError;
171
+ throw failure;
158
172
  }
159
173
  try {
160
174
  if (!response.ok) {
@@ -191,6 +205,49 @@ function createFetchError(url, cause) {
191
205
  error.cause = cause;
192
206
  return error;
193
207
  }
208
+ async function fetchWithValidatedRedirects(request, validateUrl) {
209
+ let body = await request.clone().text();
210
+ for (let redirects = 0;; redirects++) {
211
+ const response = await fetch(request, { redirect: "manual" });
212
+ const location = response.headers.get("Location");
213
+ if (![
214
+ 301,
215
+ 302,
216
+ 303,
217
+ 307,
218
+ 308
219
+ ].includes(response.status) || location == null) return response;
220
+ await response.body?.cancel();
221
+ if (redirects >= 20) throw new TypeError("Too many redirects");
222
+ const url = new URL(location, request.url);
223
+ await validateUrl(url.href);
224
+ const headers = new Headers(request.headers);
225
+ if (url.origin !== new URL(request.url).origin) {
226
+ headers.delete("Authorization");
227
+ headers.delete("Proxy-Authorization");
228
+ headers.delete("Cookie");
229
+ headers.delete("Host");
230
+ }
231
+ let method = request.method;
232
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
233
+ method = "GET";
234
+ body = void 0;
235
+ for (const name of [
236
+ "Content-Encoding",
237
+ "Content-Language",
238
+ "Content-Length",
239
+ "Content-Location",
240
+ "Content-Type"
241
+ ]) headers.delete(name);
242
+ }
243
+ request = new Request(url, {
244
+ method,
245
+ headers,
246
+ body,
247
+ redirect: "manual"
248
+ });
249
+ }
250
+ }
194
251
  /**
195
252
  * An error that is thrown when an activity fails to send to a remote inbox.
196
253
  * It contains structured information about the failure, including the HTTP
@@ -10,8 +10,8 @@ import { t as assertThrows } from "../assert_throws-BJ5wuvR2.mjs";
10
10
  import "../std__assert-CxFT0MNt.mjs";
11
11
  import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
12
12
  import { t as assert } from "../assert-CebeUf0C.mjs";
13
- import { t as exportJwk } from "../key-BcA-AzPp.mjs";
14
- import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-BR190IDB.mjs";
13
+ import { t as exportJwk } from "../key-Cwbg5ggV.mjs";
14
+ import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-CtidZS_t.mjs";
15
15
  import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
16
16
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
17
17
  import { FetchError, exportSpki } from "@fedify/vocab-runtime";