@fedify/fedify 2.4.0-dev.2169 → 2.4.0-dev.2193

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 (103) hide show
  1. package/dist/{builder-B2d_Pt0j.mjs → builder-0onlMY4u.mjs} +26 -6
  2. package/dist/{circuit-breaker-C5NqoLBJ.mjs → circuit-breaker-XJ53md4S.mjs} +16 -11
  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-D28gMFqP.mjs → compound-proof-B-Fk6Slo.mjs} +3 -3
  7. package/dist/{context-BykNUf04.d.ts → context-9T--NSo4.d.ts} +55 -1
  8. package/dist/{context-L0Fnib9j.d.cts → context-CDcAgm4l.d.cts} +55 -1
  9. package/dist/{deno-DU8dvnWk.mjs → deno-DB_Sh0LK.mjs} +1 -1
  10. package/dist/{docloader-CghLH7Zi.mjs → docloader-DpWYYY6a.mjs} +25 -17
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/circuit-breaker.test.mjs +50 -1
  13. package/dist/federation/compound-fanout.test.mjs +3 -3
  14. package/dist/federation/handler-compound-proof.test.mjs +52 -3
  15. package/dist/federation/handler.test.mjs +5 -5
  16. package/dist/federation/hashlink-media.test.mjs +2 -2
  17. package/dist/federation/idempotency.test.mjs +2 -2
  18. package/dist/federation/keycache.test.mjs +1 -1
  19. package/dist/federation/metrics.test.mjs +1 -1
  20. package/dist/federation/middleware.test.mjs +182 -13
  21. package/dist/federation/mod.cjs +1 -1
  22. package/dist/federation/mod.d.cts +2 -2
  23. package/dist/federation/mod.d.ts +2 -2
  24. package/dist/federation/mod.js +1 -1
  25. package/dist/federation/outgoing-proof.test.mjs +5 -5
  26. package/dist/federation/portable-actor.test.mjs +3 -3
  27. package/dist/federation/portable-chain.test.mjs +3 -3
  28. package/dist/federation/portable-collection.test.mjs +4 -4
  29. package/dist/federation/portable-gateway-keys.test.mjs +6 -6
  30. package/dist/federation/portable-inbox.test.mjs +35 -5
  31. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  32. package/dist/federation/portable-route-activity.test.d.mts +2 -0
  33. package/dist/federation/portable-route-activity.test.mjs +259 -0
  34. package/dist/federation/portable.test.mjs +49 -5
  35. package/dist/federation/send.test.mjs +3 -3
  36. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  37. package/dist/federation/tasks/tasks.test.mjs +2 -2
  38. package/dist/federation/temporal.test.mjs +1 -1
  39. package/dist/federation/webfinger.test.mjs +1 -1
  40. package/dist/{federation-DrNgkLfJ.cjs → federation-BsGo0EH2.cjs} +76 -35
  41. package/dist/{http-5CJBdU8n.cjs → http-BEivI0TS.cjs} +279 -87
  42. package/dist/{http-BGpexPO7.js → http-BGmxtpo2.js} +263 -89
  43. package/dist/{http-Cok5-vIC.d.ts → http-BrwKINQn.d.cts} +29 -1
  44. package/dist/{http-DC1cqAQ2.d.cts → http-DGx0KOws.d.ts} +29 -1
  45. package/dist/{http-Bd6rZlJX.mjs → http-DQf9Mw4V.mjs} +180 -92
  46. package/dist/{key-gIhVZO3p.mjs → key-DA3Yx8p2.mjs} +4 -4
  47. package/dist/{key-owner-evidence-Bo3yaeL5.mjs → key-owner-evidence-APB6NpSk.mjs} +1 -1
  48. package/dist/{keycache-PiOM9pM0.mjs → keycache-Da8A8mZa.mjs} +1 -1
  49. package/dist/{kv-cache-YRi_g8GB.cjs → kv-cache-1hafSoqH.cjs} +23 -15
  50. package/dist/{kv-cache-iR55fqlk.js → kv-cache-DduYhpee.js} +24 -16
  51. package/dist/{kv-cache-D0qjSeWY.mjs → kv-cache-OFBvrKh3.mjs} +1 -1
  52. package/dist/{ld-BsTkRrwF.mjs → ld-C15yzuY6.mjs} +3 -3
  53. package/dist/{metrics-jUDFfVgi.mjs → metrics-B1H23mlq.mjs} +1 -1
  54. package/dist/{middleware-Dq-OCmb6.mjs → middleware-BZQnf0nI.mjs} +1 -1
  55. package/dist/{middleware-Coqgp-K0.mjs → middleware-BqqycC0P.mjs} +52 -36
  56. package/dist/{middleware-DlxMxVp5.js → middleware-ClaUFaLb.js} +77 -36
  57. package/dist/{mod-DK0y9fd-.d.ts → mod-B3OBLj5g.d.ts} +1 -1
  58. package/dist/{mod-B5z_q9TH.d.ts → mod-C5Mwx5ty.d.ts} +5 -1
  59. package/dist/{mod-CPPqkyV_.d.cts → mod-Cy9VNHVw.d.ts} +14 -3
  60. package/dist/{mod-BLOXGs6m.d.cts → mod-D58KCqUg.d.cts} +1 -1
  61. package/dist/{mod--jv6vyrP.d.ts → mod-DoTWIc7F.d.cts} +14 -3
  62. package/dist/{mod-BA_F2x53.d.cts → mod-Dp3Ij7sw.d.cts} +5 -1
  63. package/dist/mod.cjs +4 -4
  64. package/dist/mod.d.cts +5 -5
  65. package/dist/mod.d.ts +5 -5
  66. package/dist/mod.js +4 -4
  67. package/dist/nodeinfo/handler.test.mjs +1 -1
  68. package/dist/{owner-D3GoetXS.mjs → owner-BVpZBM-V.mjs} +5 -5
  69. package/dist/{portable-collection-CnlCamup.cjs → portable-collection-BFSShzwh.cjs} +1 -1
  70. package/dist/{portable-collection-Bkdh_WpL.js → portable-collection-D535cSz_.js} +1 -1
  71. package/dist/{portable-collection-C60_acmx.mjs → portable-collection-Dn2GdZdI.mjs} +2 -2
  72. package/dist/{portable-key-DyF5AnZo.mjs → portable-key-Bmwmq7Y6.mjs} +2 -2
  73. package/dist/{portable-key-id-Bg259c4Z.mjs → portable-key-id-Ca30-hOa.mjs} +45 -1
  74. package/dist/{proof-CQAYqWdj.mjs → proof-DrJWKeOl.mjs} +49 -7
  75. package/dist/{send-DjkQrxt5.mjs → send-CT3a-0Vd.mjs} +3 -3
  76. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  77. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  78. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  79. package/dist/sig/compound-proof.test.mjs +2 -2
  80. package/dist/sig/http.test.mjs +495 -23
  81. package/dist/sig/key-owner-evidence.test.mjs +2 -2
  82. package/dist/sig/key.test.mjs +1 -1
  83. package/dist/sig/ld.test.mjs +2 -2
  84. package/dist/sig/mod.cjs +2 -2
  85. package/dist/sig/mod.d.cts +2 -2
  86. package/dist/sig/mod.d.ts +2 -2
  87. package/dist/sig/mod.js +2 -2
  88. package/dist/sig/owner.test.mjs +1 -1
  89. package/dist/sig/portable-collection.test.mjs +1 -1
  90. package/dist/sig/portable-key-id.test.d.mts +2 -0
  91. package/dist/sig/portable-key-id.test.mjs +67 -0
  92. package/dist/sig/portable-key.test.mjs +7 -7
  93. package/dist/sig/proof.test.mjs +141 -7
  94. package/dist/{temporal-oRKP803M.mjs → temporal-DNqluyEc.mjs} +1 -1
  95. package/dist/testing/kv.test.mjs +1 -1
  96. package/dist/testing/mod.d.mts +52 -0
  97. package/dist/utils/docloader.test.mjs +93 -2
  98. package/dist/utils/kv-cache.test.mjs +1 -1
  99. package/dist/utils/mod.cjs +1 -1
  100. package/dist/utils/mod.d.cts +1 -1
  101. package/dist/utils/mod.d.ts +1 -1
  102. package/dist/utils/mod.js +1 -1
  103. package/package.json +7 -7
@@ -3,7 +3,7 @@ import { URLPattern } from "urlpattern-polyfill";
3
3
  import { getLogger } from "@logtape/logtape";
4
4
  import { Activity, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1, PUBLIC_COLLECTION, getTypeId, isActor } from "@fedify/vocab";
5
5
  import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
6
- import { FetchError, canonicalizePortableUri, encodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDidKeyVerificationMethod, parseIri, preloadedContexts, toCompatibleEf61Id } from "@fedify/vocab-runtime";
6
+ import { FetchError, canonicalizePortableUri, encodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDidKeyVerificationMethod, parseGatewayUrl, parseIri, preloadedContexts, toCompatibleEf61Id } from "@fedify/vocab-runtime";
7
7
  import { getPortableGatewayCandidates, isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
8
8
  import { encodeHex } from "byte-encodings/hex";
9
9
  import jsonld from "@fedify/vocab-runtime/jsonld";
@@ -14,7 +14,7 @@ import serialize from "json-canon";
14
14
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
15
15
  //#region deno.json
16
16
  var name = "@fedify/fedify";
17
- var version = "2.4.0-dev.2169+0a45df07";
17
+ var version = "2.4.0-dev.2193+402149b5";
18
18
  //#endregion
19
19
  //#region src/sig/portable-key-id.ts
20
20
  const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
@@ -115,6 +115,50 @@ function getCanonicalPortableId(id) {
115
115
  }
116
116
  }
117
117
  /**
118
+ * Gets the [FEP-fe34] origin of an ID for same-origin checks. The origin of
119
+ * an `ap:` or `ap+ef61:` URI, or of a compatible identifier on any gateway,
120
+ * is the DID of its canonical portable ID, as FEP-ef61 treats a compatible
121
+ * identifier as the portable ID it stands for; unlike `getFe34Origin()`,
122
+ * which gives a compatible identifier the web origin of its gateway. The
123
+ * origin of any other HTTP(S) URL is its web origin, and that of a DID URL
124
+ * is its DID.
125
+ *
126
+ * Only the parsed `URL` is validated, so dot segments that URL parsing has
127
+ * already resolved cannot be detected here.
128
+ *
129
+ * [FEP-fe34]: https://w3id.org/fep/fe34
130
+ * @returns The origin, or `null` if the ID has none that is supported, or is
131
+ * a malformed portable ID or compatible identifier.
132
+ * @internal
133
+ */
134
+ function getAuthenticationOrigin(id) {
135
+ try {
136
+ if (isPortableId(id)) {
137
+ const canonical = getCanonicalPortableId(id);
138
+ return canonical == null ? null : getFe34Origin(canonical);
139
+ }
140
+ return getFe34Origin(id);
141
+ } catch (error) {
142
+ if (error instanceof TypeError) return null;
143
+ throw error;
144
+ }
145
+ }
146
+ /**
147
+ * Checks whether two IDs identify the same object. Two portable IDs, i.e.,
148
+ * `ap:` or `ap+ef61:` URIs or compatible identifiers, are compared by their
149
+ * canonical portable IDs, so that the same object on different gateways and
150
+ * with different location hints matches. Other IDs have to be equal as
151
+ * they are.
152
+ * @internal
153
+ */
154
+ function isSameObjectId(a, b) {
155
+ const aPortable = isPortableId(a);
156
+ if (aPortable !== isPortableId(b)) return false;
157
+ if (!aPortable) return a.href === b.href;
158
+ const canonical = getCanonicalPortableId(a);
159
+ return canonical != null && canonical === getCanonicalPortableId(b);
160
+ }
161
+ /**
118
162
  * Gets the canonical portable ID of an `ap:` or `ap+ef61:` URI or
119
163
  * a compatible identifier as it is written, before any URL parsing.
120
164
  * Parsing an ID into a `URL` resolves dot segments in its path, which would
@@ -3860,6 +3904,7 @@ const FEP_2277_COLLECTION_PROPERTIES = [
3860
3904
  "prev",
3861
3905
  "current"
3862
3906
  ].map((property) => AS_NAMESPACE + property);
3907
+ const FEP_EF61_GATEWAYS = "https://w3id.org/fep/ef61/gateways";
3863
3908
  const PORTABLE_OBJECT_ID_PATTERN = /^ap(?:\+ef61)?:\/\//i;
3864
3909
  const FUNCTIONAL_PROOF_PROPERTIES = [
3865
3910
  `${SECURITY_NAMESPACE}cryptosuite`,
@@ -3886,6 +3931,35 @@ function hasValidPortableProofShape(proofValue) {
3886
3931
  });
3887
3932
  }
3888
3933
  /**
3934
+ * Checks whether an expanded portable actor node has the `gateways` that
3935
+ * FEP-ef61 requires: a non-empty list whose items are all HTTP(S) URIs with
3936
+ * an empty path, query, and fragment. Like the generated vocabulary decoder,
3937
+ * this also tolerates a plain set of values instead of a `@list`, and gateway
3938
+ * strings given as `@value`s instead of `@id`s.
3939
+ */
3940
+ function hasValidPortableActorGateways(node) {
3941
+ const values = node[FEP_EF61_GATEWAYS];
3942
+ if (!Array.isArray(values)) return false;
3943
+ let items = values;
3944
+ if (values.some((value) => isJsonLdNode(value) && "@list" in value)) {
3945
+ if (values.length !== 1) return false;
3946
+ items = values[0]["@list"];
3947
+ }
3948
+ if (!Array.isArray(items) || items.length < 1) return false;
3949
+ return items.every((item) => {
3950
+ if (!isJsonLdNode(item)) return false;
3951
+ const gateway = typeof item["@id"] === "string" ? item["@id"] : item["@value"];
3952
+ if (typeof gateway !== "string") return false;
3953
+ try {
3954
+ parseGatewayUrl(gateway);
3955
+ } catch (error) {
3956
+ if (error instanceof TypeError) return false;
3957
+ throw error;
3958
+ }
3959
+ return true;
3960
+ });
3961
+ }
3962
+ /**
3889
3963
  * Classifies an expanded JSON-LD node into an FEP-2277 core type by the
3890
3964
  * properties it has.
3891
3965
  * @internal
@@ -4089,6 +4163,15 @@ async function preparePortableObjectProof(jsonLd, options) {
4089
4163
  }
4090
4164
  };
4091
4165
  }
4166
+ if (objectType === "actor" && !hasValidPortableActorGateways(root)) return {
4167
+ prepared: false,
4168
+ root,
4169
+ objectType,
4170
+ result: {
4171
+ verified: false,
4172
+ reason: { type: "invalidGateways" }
4173
+ }
4174
+ };
4092
4175
  return {
4093
4176
  prepared: true,
4094
4177
  root,
@@ -4142,7 +4225,10 @@ async function verifyPortableObjectProofPolicy(jsonLd, key, options = {}) {
4142
4225
  *
4143
4226
  * Every proof must use a DID URL whose DID matches the portable object's
4144
4227
  * authority, i.e., the DID of its canonical portable ID, and every proof must
4145
- * pass {@link verifyProof}. The proofs are verified over the document as
4228
+ * pass {@link verifyProof}. A portable actor must also have a non-empty
4229
+ * `gateways` list whose items are all HTTP(S) URIs with an empty path, query,
4230
+ * and fragment; otherwise, it is rejected with the `invalidGateways` reason
4231
+ * before its proofs are verified. The proofs are verified over the document as
4146
4232
  * given; a compatible identifier is never rewritten into a portable ID.
4147
4233
  *
4148
4234
  * [FEP-ef61]: https://w3id.org/fep/ef61
@@ -5056,16 +5142,19 @@ function parseDraftSignature(signature) {
5056
5142
  * Lists the HTTP Signatures that a request claims to carry, without verifying
5057
5143
  * any of them. Signatures whose key IDs are not URLs are left out.
5058
5144
  * @param request The request.
5145
+ * @param maxSignatures The maximum number of RFC 9421 signatures to consider,
5146
+ * counted the same way as by {@link verifyRequest}, so
5147
+ * that the signatures it ignores are not listed either.
5059
5148
  * @returns The signatures; RFC 9421 ones if the request has
5060
5149
  * a `Signature-Input` header, or a draft-cavage one otherwise.
5061
5150
  * @internal
5062
5151
  */
5063
- function listRequestSignatures(request) {
5152
+ function listRequestSignatures(request, maxSignatures = 3) {
5064
5153
  const signatureInput = request.headers.get("Signature-Input");
5065
5154
  if (signatureInput != null) {
5066
5155
  const signatures = [];
5067
- const inputs = parseRfc9421SignatureInput(signatureInput);
5068
- for (const [label, input] of Object.entries(inputs)) {
5156
+ const inputs = Object.entries(parseRfc9421SignatureInput(signatureInput));
5157
+ for (const [label, input] of inputs.slice(0, maxSignatures)) {
5069
5158
  const keyId = parseKeyId(input.keyId);
5070
5159
  if (keyId == null) continue;
5071
5160
  signatures.push({
@@ -5193,6 +5282,18 @@ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime,
5193
5282
  body
5194
5283
  });
5195
5284
  }
5285
+ /**
5286
+ * Checks that a maximum number of signatures to verify is a positive safe
5287
+ * integer or `Infinity`.
5288
+ * @param value The value to check.
5289
+ * @param name The name of the option, used in the error message.
5290
+ * @throws {RangeError} Thrown if the value is not valid.
5291
+ * @internal
5292
+ */
5293
+ function validateMaxSignatures(value, name) {
5294
+ if (value === Infinity || Number.isSafeInteger(value) && value >= 1) return;
5295
+ throw new RangeError(`${name} must be a positive integer or Infinity.`);
5296
+ }
5196
5297
  const supportedHashAlgorithms = {
5197
5298
  "sha": "SHA-1",
5198
5299
  "sha-256": "SHA-256",
@@ -5285,6 +5386,8 @@ function recordVerificationResult(span, result) {
5285
5386
  * @param options Options for verifying the request.
5286
5387
  * @returns The public key of the verified signature, or `null` if the signature
5287
5388
  * could not be verified.
5389
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
5390
+ * is not a positive integer or `Infinity`.
5288
5391
  */
5289
5392
  async function verifyRequest(request, options = {}) {
5290
5393
  const result = await verifyRequestDetailed(request, options);
@@ -5297,9 +5400,12 @@ async function verifyRequest(request, options = {}) {
5297
5400
  * @param request The request to verify.
5298
5401
  * @param options Options for verifying the request.
5299
5402
  * @returns The verified public key, or a structured verification failure.
5403
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
5404
+ * is not a positive integer or `Infinity`.
5300
5405
  * @since 2.1.0
5301
5406
  */
5302
5407
  async function verifyRequestDetailed(request, options = {}) {
5408
+ if (options.maxSignatures !== void 0) validateMaxSignatures(options.maxSignatures, "maxSignatures");
5303
5409
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("http_signatures.verify", async (span) => {
5304
5410
  if (span.isRecording()) {
5305
5411
  span.setAttribute(ATTR_HTTP_REQUEST_METHOD, request.method);
@@ -5608,7 +5714,100 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
5608
5714
  }
5609
5715
  return false;
5610
5716
  }
5611
- async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
5717
+ /**
5718
+ * Verifies one RFC 9421 signature of a request with a key.
5719
+ * @param request The request, whose body may be consumed already.
5720
+ * @param sigInput The parsed `Signature-Input` member of the signature.
5721
+ * @param sigBytes The bytes of the signature.
5722
+ * @param key The public key named by the signature.
5723
+ * @param span The span to record the algorithm and the signature on.
5724
+ * @returns Whether the signature is verified, and the algorithm of the
5725
+ * signature if it is supported.
5726
+ */
5727
+ async function verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span) {
5728
+ const logger = getLogger([
5729
+ "fedify",
5730
+ "sig",
5731
+ "http"
5732
+ ]);
5733
+ let alg = sigInput.alg?.toLowerCase();
5734
+ if (alg == null) {
5735
+ if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
5736
+ else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
5737
+ else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
5738
+ else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
5739
+ }
5740
+ if (alg) span?.setAttribute("http_signatures.algorithm", alg);
5741
+ const algorithm = alg && rfc9421AlgorithmMap[alg];
5742
+ if (!algorithm) {
5743
+ logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
5744
+ algorithm: sigInput.alg,
5745
+ supported: Object.keys(rfc9421AlgorithmMap)
5746
+ });
5747
+ return {
5748
+ verified: false,
5749
+ mismatched: false
5750
+ };
5751
+ }
5752
+ const metricAlgorithm = alg;
5753
+ let signatureBase;
5754
+ try {
5755
+ signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
5756
+ } catch (error) {
5757
+ logger.debug("Failed to create signature base for verification: {error}", {
5758
+ error,
5759
+ signatureInput: sigInput
5760
+ });
5761
+ return {
5762
+ verified: false,
5763
+ algorithm: metricAlgorithm,
5764
+ mismatched: false
5765
+ };
5766
+ }
5767
+ const signatureBaseBytes = new TextEncoder().encode(signatureBase);
5768
+ span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
5769
+ const algorithmName = typeof algorithm === "string" ? algorithm : algorithm.name;
5770
+ if (key.publicKey.algorithm.name !== algorithmName) {
5771
+ logger.debug("Failed to verify; key {keyId} is not a key for algorithm {algorithm}.", {
5772
+ keyId: sigInput.keyId,
5773
+ algorithm: alg
5774
+ });
5775
+ return {
5776
+ verified: false,
5777
+ algorithm: metricAlgorithm,
5778
+ mismatched: true
5779
+ };
5780
+ }
5781
+ let verified;
5782
+ try {
5783
+ verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
5784
+ } catch (error) {
5785
+ logger.debug("Error during signature verification: {error}", {
5786
+ error,
5787
+ keyId: sigInput.keyId,
5788
+ algorithm: sigInput.alg
5789
+ });
5790
+ return {
5791
+ verified: false,
5792
+ algorithm: metricAlgorithm,
5793
+ mismatched: false
5794
+ };
5795
+ }
5796
+ if (verified) return {
5797
+ verified: true,
5798
+ algorithm: metricAlgorithm
5799
+ };
5800
+ logger.debug("Failed to verify signature with key {keyId}; signature invalid.", {
5801
+ keyId: sigInput.keyId,
5802
+ signatureBase
5803
+ });
5804
+ return {
5805
+ verified: false,
5806
+ algorithm: metricAlgorithm,
5807
+ mismatched: true
5808
+ };
5809
+ }
5810
+ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, maxSignatures = 3, meterProvider, tracerProvider } = {}) {
5612
5811
  const logger = getLogger([
5613
5812
  "fedify",
5614
5813
  "sig",
@@ -5621,7 +5820,6 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5621
5820
  logger.error("Failed to verify; the request body is locked.", { url: request.url });
5622
5821
  return noSignatureResult();
5623
5822
  }
5624
- const originalRequest = request;
5625
5823
  request = request.clone();
5626
5824
  const signatureInputHeader = request.headers.get("Signature-Input");
5627
5825
  if (!signatureInputHeader) {
@@ -5641,6 +5839,29 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5641
5839
  logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
5642
5840
  return invalidSignatureResult(null);
5643
5841
  }
5842
+ if (signatureNames.length > maxSignatures) {
5843
+ logger.debug("Verifying only the first {maxSignatures} of the {count} signatures in the Signature-Input header; the rest are ignored.", {
5844
+ maxSignatures,
5845
+ count: signatureNames.length
5846
+ });
5847
+ signatureNames.splice(maxSignatures);
5848
+ }
5849
+ const keyLookups = /* @__PURE__ */ new Map();
5850
+ const refreshedKeyIds = /* @__PURE__ */ new Set();
5851
+ const lookUpKey = (keyId, keyCache) => measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
5852
+ documentLoader,
5853
+ contextLoader,
5854
+ keyCache,
5855
+ tracerProvider,
5856
+ meterProvider,
5857
+ portableKeyResolvers: createPortableKeyResolvers({
5858
+ documentLoader,
5859
+ contextLoader,
5860
+ keyCache,
5861
+ tracerProvider,
5862
+ meterProvider
5863
+ })
5864
+ }));
5644
5865
  let failure = noSignatureResult();
5645
5866
  let failureAlgorithm;
5646
5867
  const setFailure = (result, algorithm) => {
@@ -5714,94 +5935,40 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5714
5935
  setFailure(invalidSignatureResult(null));
5715
5936
  continue;
5716
5937
  }
5717
- const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
5718
- documentLoader,
5719
- contextLoader,
5720
- keyCache,
5721
- tracerProvider,
5722
- meterProvider,
5723
- portableKeyResolvers: createPortableKeyResolvers({
5724
- documentLoader,
5725
- contextLoader,
5726
- keyCache,
5727
- tracerProvider,
5728
- meterProvider
5729
- })
5730
- }));
5731
- if (fetchError != null) {
5732
- setFailure(keyFetchErrorResult(keyId, fetchError));
5733
- continue;
5734
- }
5735
- if (!key) {
5736
- logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
5737
- setFailure(invalidSignatureResult(keyId));
5738
- continue;
5739
- }
5740
- let alg = sigInput.alg?.toLowerCase();
5741
- if (alg == null) {
5742
- if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
5743
- else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
5744
- else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
5745
- else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
5746
- }
5747
- if (alg) span?.setAttribute("http_signatures.algorithm", alg);
5748
- const algorithm = alg && rfc9421AlgorithmMap[alg];
5749
- const candidateAlgorithm = algorithm ? alg : void 0;
5750
- if (!algorithm) {
5751
- logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
5752
- algorithm: sigInput.alg,
5753
- supported: Object.keys(rfc9421AlgorithmMap)
5754
- });
5755
- setFailure(invalidSignatureResult(keyId));
5756
- continue;
5938
+ let lookup = keyLookups.get(keyId.href);
5939
+ if (lookup == null) {
5940
+ lookup = await lookUpKey(keyId, keyCache);
5941
+ keyLookups.set(keyId.href, lookup);
5757
5942
  }
5758
- let signatureBase;
5759
- try {
5760
- signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
5761
- } catch (error) {
5762
- logger.debug("Failed to create signature base for verification: {error}", {
5763
- error,
5764
- signatureInput: sigInput
5765
- });
5766
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
5767
- continue;
5768
- }
5769
- const signatureBaseBytes = new TextEncoder().encode(signatureBase);
5770
- span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
5771
- try {
5772
- if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
5773
- metricsContext.algorithm = candidateAlgorithm;
5943
+ while (true) {
5944
+ const { key, cached, fetchError } = lookup;
5945
+ if (fetchError != null) {
5946
+ setFailure(keyFetchErrorResult(keyId, fetchError));
5947
+ break;
5948
+ }
5949
+ if (!key) {
5950
+ logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
5951
+ setFailure(invalidSignatureResult(keyId));
5952
+ break;
5953
+ }
5954
+ const result = await verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span);
5955
+ if (result.verified) {
5956
+ metricsContext.algorithm = result.algorithm;
5774
5957
  return {
5775
5958
  verified: true,
5776
5959
  key,
5777
5960
  signatureLabel: sigName
5778
5961
  };
5779
- } else if (cached) {
5962
+ }
5963
+ if (result.mismatched && cached && !refreshedKeyIds.has(keyId.href)) {
5780
5964
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
5781
- return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
5782
- documentLoader,
5783
- contextLoader,
5784
- timeWindow,
5785
- currentTime,
5786
- keyCache: bypassKeyCacheReads(keyCache),
5787
- spec: "rfc9421",
5788
- meterProvider,
5789
- tracerProvider
5790
- });
5791
- } else {
5792
- logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
5793
- keyId: sigInput.keyId,
5794
- signatureBase
5795
- });
5796
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
5965
+ refreshedKeyIds.add(keyId.href);
5966
+ lookup = await lookUpKey(keyId, bypassKeyCacheReads(keyCache));
5967
+ keyLookups.set(keyId.href, lookup);
5968
+ continue;
5797
5969
  }
5798
- } catch (error) {
5799
- logger.debug("Error during signature verification: {error}", {
5800
- error,
5801
- keyId: sigInput.keyId,
5802
- algorithm: sigInput.alg
5803
- });
5804
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
5970
+ setFailure(invalidSignatureResult(keyId), result.algorithm);
5971
+ break;
5805
5972
  }
5806
5973
  }
5807
5974
  metricsContext.algorithm = failureAlgorithm;
@@ -5901,16 +6068,19 @@ async function doubleKnock(request, identity, options = {}) {
5901
6068
  }
5902
6069
  async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
5903
6070
  const { specDeterminer, log, tracerProvider, signal } = options;
6071
+ signal?.throwIfAborted();
5904
6072
  const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
5905
6073
  visited.add(request.url);
5906
6074
  const origin = new URL(request.url).origin;
5907
6075
  const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
6076
+ signal?.throwIfAborted();
5908
6077
  const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
5909
6078
  let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
5910
6079
  spec: firstTrySpec,
5911
6080
  tracerProvider,
5912
6081
  body
5913
6082
  });
6083
+ signal?.throwIfAborted();
5914
6084
  log?.(signedRequest);
5915
6085
  let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
5916
6086
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -5935,6 +6105,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
5935
6105
  let fulfilled = false;
5936
6106
  let challengeRequest;
5937
6107
  for (const entry of entries) {
6108
+ signal?.throwIfAborted();
5938
6109
  const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
5939
6110
  if (rfc9421 == null) continue;
5940
6111
  logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
@@ -5957,6 +6128,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
5957
6128
  }
5958
6129
  }
5959
6130
  if (fulfilled && challengeRequest != null) {
6131
+ signal?.throwIfAborted();
5960
6132
  signedRequest = challengeRequest;
5961
6133
  log?.(signedRequest);
5962
6134
  response = await fetch(signedRequest, {
@@ -5981,11 +6153,13 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
5981
6153
  status: response.status,
5982
6154
  statusText: response.statusText
5983
6155
  });
6156
+ signal?.throwIfAborted();
5984
6157
  signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
5985
6158
  spec,
5986
6159
  tracerProvider,
5987
6160
  body
5988
6161
  });
6162
+ signal?.throwIfAborted();
5989
6163
  log?.(signedRequest);
5990
6164
  response = await fetchDoubleKnockRequest(request, signedRequest, signal);
5991
6165
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -6028,4 +6202,4 @@ function timingSafeEqual(a, b) {
6028
6202
  return result === 0;
6029
6203
  }
6030
6204
  //#endregion
6031
- export { isAbortError$1 as $, hasSignatureLike as A, fetchKey as B, assertSafeJsonLd as C, isPortableId as Ct, detachSignature as D, name as Dt, createSignature as E, isSamePublicKey as Et, verifyJsonLd as F, verifyKeyOwnership as G, generateCryptoKeyPair as H, verifySignature as I, parseKeyOwnerEvidence as J, MAX_PORTABLE_KEY_TTL as K, wrapContextLoaderForJsonLd as L, isInvalidUrlTypeError as M, signJsonLd as N, getNormalizationContextLoader as O, version as Ot, verifyCompactJsonLd as P, instrumentDocumentLoader as Q, exportJwk as R, InvalidContextReferenceError as S, isCompatibleKeyId as St, compactJsonLd as T, isPortableUri as Tt, importJwk as U, fetchKeyDetailed as V, validateCryptoKey as W, getFederationMetrics as X, getDurationMs as Y, getRemoteHost as Z, verifyPortableObjectProof as _, getCanonicalPortableId as _t, signRequest as a, recordDocumentCache as at, verifyProof as b, getRawCanonicalPortableId as bt, fetchPortableGatewayKey as c, recordOutboxActivity as ct, classifyFep2277CoreType as d, registerQueueDepthGauge as dt, recordCircuitBreakerStateChange as et, createProof as f, preloadedOnlyDocumentLoader as ft, verifyObject as g, validateAcceptSignature as gt, verifyMapLocalProof as h, parseAcceptSignature as ht, selectRequestSignature as i, recordCollectionTotalItems as it, isClearlyMalformedContextReference as j, hasSignature as k, resolvePortableActorKey as l, recordOutboxEnqueue as lt, signObject as m, fulfillAcceptSignature as mt, listRequestSignatures as n, recordCollectionPageItems as nt, verifyRequest as o, recordFanoutRecipients as ot, hasProofLike as p, formatAcceptSignature as pt, getVerifiedKeyOwnerEvidence as q, parseRfc9421SignatureInput as r, recordCollectionRequest as rt, verifyRequestDetailed as s, recordInboxActivity as st, doubleKnock as t, recordCollectionDispatchDuration as tt, InvalidPortableObjectIdError as u, recordWebFingerHandle as ut, verifyPortableObjectProofPolicy as v, getGatewayKeyBase as vt, attachSignature as w, isPortableKeyId as wt, normalizeOutgoingActivityJsonLd as x, hasPortableActor as xt, verifyPortableObjectProofWithRoot as y, getPortableDid as yt, fetchActorDocument as z };
6205
+ export { instrumentDocumentLoader as $, hasSignature as A, name as At, fetchActorDocument as B, InvalidContextReferenceError as C, hasPortableActor as Ct, createSignature as D, isPortableUri as Dt, compactJsonLd as E, isPortableKeyId as Et, verifyCompactJsonLd as F, validateCryptoKey as G, fetchKeyDetailed as H, verifyJsonLd as I, getVerifiedKeyOwnerEvidence as J, verifyKeyOwnership as K, verifySignature as L, isClearlyMalformedContextReference as M, isInvalidUrlTypeError as N, detachSignature as O, isSameObjectId as Ot, signJsonLd as P, getRemoteHost as Q, wrapContextLoaderForJsonLd as R, normalizeOutgoingActivityJsonLd as S, getRawCanonicalPortableId as St, attachSignature as T, isPortableId as Tt, generateCryptoKeyPair as U, fetchKey as V, importJwk as W, getDurationMs as X, parseKeyOwnerEvidence as Y, getFederationMetrics as Z, verifyObject as _, validateAcceptSignature as _t, signRequest as a, recordCollectionTotalItems as at, verifyPortableObjectProofWithRoot as b, getGatewayKeyBase as bt, verifyRequestDetailed as c, recordInboxActivity as ct, InvalidPortableObjectIdError as d, recordWebFingerHandle as dt, isAbortError$1 as et, classifyFep2277CoreType as f, registerQueueDepthGauge as ft, verifyMapLocalProof as g, parseAcceptSignature as gt, signObject as h, fulfillAcceptSignature as ht, selectRequestSignature as i, recordCollectionRequest as it, hasSignatureLike as j, version as jt, getNormalizationContextLoader as k, isSamePublicKey as kt, fetchPortableGatewayKey as l, recordOutboxActivity as lt, hasProofLike as m, formatAcceptSignature as mt, listRequestSignatures as n, recordCollectionDispatchDuration as nt, validateMaxSignatures as o, recordDocumentCache as ot, createProof as p, preloadedOnlyDocumentLoader as pt, MAX_PORTABLE_KEY_TTL as q, parseRfc9421SignatureInput as r, recordCollectionPageItems as rt, verifyRequest as s, recordFanoutRecipients as st, doubleKnock as t, recordCircuitBreakerStateChange as tt, resolvePortableActorKey as u, recordOutboxEnqueue as ut, verifyPortableObjectProof as v, getAuthenticationOrigin as vt, assertSafeJsonLd as w, isCompatibleKeyId as wt, verifyProof as x, getPortableDid as xt, verifyPortableObjectProofPolicy as y, getCanonicalPortableId as yt, exportJwk as z };
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/sig/portable-key.d.ts
7
7
  /**
@@ -601,6 +601,30 @@ interface VerifyRequestOptions {
601
601
  */
602
602
  spec?: HttpMessageSignaturesSpec;
603
603
  /**
604
+ * The maximum number of [RFC 9421] signatures of a request to verify.
605
+ * A request can carry several signatures, each of which may make Fedify
606
+ * fetch the key that it names, so only the first ones in the order of
607
+ * the `Signature-Input` header are verified, and the rest are ignored as
608
+ * if they were absent. Every signature among the first ones counts, even
609
+ * if it fails before its key is fetched, e.g., because it is too old, or
610
+ * names the same key as another one. The key of a key ID is looked up
611
+ * only once for all the signatures that name it.
612
+ *
613
+ * It has to be a positive integer, or `Infinity` to verify every
614
+ * signature, which lets a single request make Fedify fetch any number of
615
+ * keys. Draft-cavage HTTP Signatures carry a single signature, so this
616
+ * option does not affect them.
617
+ *
618
+ * Three by default.
619
+ *
620
+ * [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
621
+ * @throws {RangeError} Thrown by {@link verifyRequest} and
622
+ * {@link verifyRequestDetailed} if the value is not a positive
623
+ * integer or `Infinity`.
624
+ * @since 2.4.0
625
+ */
626
+ maxSignatures?: number;
627
+ /**
604
628
  * The OpenTelemetry tracer provider. If omitted, the global tracer provider
605
629
  * is used.
606
630
  * @since 1.3.0
@@ -651,6 +675,8 @@ type VerifyRequestDetailedResult = {
651
675
  * @param options Options for verifying the request.
652
676
  * @returns The public key of the verified signature, or `null` if the signature
653
677
  * could not be verified.
678
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
679
+ * is not a positive integer or `Infinity`.
654
680
  */
655
681
  declare function verifyRequest(request: Request, options?: VerifyRequestOptions): Promise<CryptographicKey | null>;
656
682
  /**
@@ -660,6 +686,8 @@ declare function verifyRequest(request: Request, options?: VerifyRequestOptions)
660
686
  * @param request The request to verify.
661
687
  * @param options Options for verifying the request.
662
688
  * @returns The verified public key, or a structured verification failure.
689
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
690
+ * is not a positive integer or `Infinity`.
663
691
  * @since 2.1.0
664
692
  */
665
693
  declare function verifyRequestDetailed(request: Request, options?: VerifyRequestOptions): Promise<VerifyRequestDetailedResult>;
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { DocumentLoader } from "@fedify/vocab-runtime";
4
3
  import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
+ import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
 
6
6
  //#region src/sig/portable-key.d.ts
7
7
  /**
@@ -601,6 +601,30 @@ interface VerifyRequestOptions {
601
601
  */
602
602
  spec?: HttpMessageSignaturesSpec;
603
603
  /**
604
+ * The maximum number of [RFC 9421] signatures of a request to verify.
605
+ * A request can carry several signatures, each of which may make Fedify
606
+ * fetch the key that it names, so only the first ones in the order of
607
+ * the `Signature-Input` header are verified, and the rest are ignored as
608
+ * if they were absent. Every signature among the first ones counts, even
609
+ * if it fails before its key is fetched, e.g., because it is too old, or
610
+ * names the same key as another one. The key of a key ID is looked up
611
+ * only once for all the signatures that name it.
612
+ *
613
+ * It has to be a positive integer, or `Infinity` to verify every
614
+ * signature, which lets a single request make Fedify fetch any number of
615
+ * keys. Draft-cavage HTTP Signatures carry a single signature, so this
616
+ * option does not affect them.
617
+ *
618
+ * Three by default.
619
+ *
620
+ * [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
621
+ * @throws {RangeError} Thrown by {@link verifyRequest} and
622
+ * {@link verifyRequestDetailed} if the value is not a positive
623
+ * integer or `Infinity`.
624
+ * @since 2.4.0
625
+ */
626
+ maxSignatures?: number;
627
+ /**
604
628
  * The OpenTelemetry tracer provider. If omitted, the global tracer provider
605
629
  * is used.
606
630
  * @since 1.3.0
@@ -651,6 +675,8 @@ type VerifyRequestDetailedResult = {
651
675
  * @param options Options for verifying the request.
652
676
  * @returns The public key of the verified signature, or `null` if the signature
653
677
  * could not be verified.
678
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
679
+ * is not a positive integer or `Infinity`.
654
680
  */
655
681
  declare function verifyRequest(request: Request, options?: VerifyRequestOptions): Promise<CryptographicKey | null>;
656
682
  /**
@@ -660,6 +686,8 @@ declare function verifyRequest(request: Request, options?: VerifyRequestOptions)
660
686
  * @param request The request to verify.
661
687
  * @param options Options for verifying the request.
662
688
  * @returns The verified public key, or a structured verification failure.
689
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
690
+ * is not a positive integer or `Infinity`.
663
691
  * @since 2.1.0
664
692
  */
665
693
  declare function verifyRequestDetailed(request: Request, options?: VerifyRequestOptions): Promise<VerifyRequestDetailedResult>;