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

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-CFF0YNTp.mjs} +3 -3
  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-F2jjkrt0.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-EopndaNb.mjs} +1 -1
  10. package/dist/{docloader-CghLH7Zi.mjs → docloader-u6zzOExF.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 +3 -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 +137 -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 +3 -3
  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-BSS2dUO7.cjs} +38 -21
  41. package/dist/{http-BGpexPO7.js → http-BqN5kyUf.js} +219 -87
  42. package/dist/{http-Cok5-vIC.d.ts → http-BrwKINQn.d.cts} +29 -1
  43. package/dist/{http-5CJBdU8n.cjs → http-CCr2_ipG.cjs} +236 -86
  44. package/dist/{http-DC1cqAQ2.d.cts → http-DGx0KOws.d.ts} +29 -1
  45. package/dist/{http-Bd6rZlJX.mjs → http-Y1ObT1A8.mjs} +180 -92
  46. package/dist/{key-gIhVZO3p.mjs → key-DyXf88bq.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-iR55fqlk.js → kv-cache-C02XitwK.js} +24 -16
  50. package/dist/{kv-cache-D0qjSeWY.mjs → kv-cache-C8YjNAi_.mjs} +1 -1
  51. package/dist/{kv-cache-YRi_g8GB.cjs → kv-cache-DUXluAc0.cjs} +23 -15
  52. package/dist/{ld-BsTkRrwF.mjs → ld-oQj6j_hP.mjs} +3 -3
  53. package/dist/{metrics-jUDFfVgi.mjs → metrics-zMe834ID.mjs} +1 -1
  54. package/dist/{middleware-Coqgp-K0.mjs → middleware-Bdu2-OPD.mjs} +37 -25
  55. package/dist/{middleware-DlxMxVp5.js → middleware-CaR2MRWY.js} +39 -22
  56. package/dist/{middleware-Dq-OCmb6.mjs → middleware-Ck9yghxs.mjs} +1 -1
  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--jv6vyrP.d.ts → mod-Ck9_lSJB.d.ts} +1 -1
  60. package/dist/{mod-BLOXGs6m.d.cts → mod-D58KCqUg.d.cts} +1 -1
  61. package/dist/{mod-CPPqkyV_.d.cts → mod-DHTuu3l8.d.cts} +1 -1
  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-Dem4fbwc.mjs} +5 -5
  69. package/dist/{portable-collection-Bkdh_WpL.js → portable-collection-BEqLrMW3.js} +1 -1
  70. package/dist/{portable-collection-C60_acmx.mjs → portable-collection-BL6aZlQ0.mjs} +2 -2
  71. package/dist/{portable-collection-CnlCamup.cjs → portable-collection-DJe_bk50.cjs} +1 -1
  72. package/dist/{portable-key-DyF5AnZo.mjs → portable-key-Duue9Y2Y.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-C9115DX_.mjs} +5 -5
  75. package/dist/{send-DjkQrxt5.mjs → send-D0hxFFRB.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 +3 -3
  94. package/dist/{temporal-oRKP803M.mjs → temporal-DKchpKyp.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
@@ -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.2190+be3a8be0";
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
@@ -5056,16 +5100,19 @@ function parseDraftSignature(signature) {
5056
5100
  * Lists the HTTP Signatures that a request claims to carry, without verifying
5057
5101
  * any of them. Signatures whose key IDs are not URLs are left out.
5058
5102
  * @param request The request.
5103
+ * @param maxSignatures The maximum number of RFC 9421 signatures to consider,
5104
+ * counted the same way as by {@link verifyRequest}, so
5105
+ * that the signatures it ignores are not listed either.
5059
5106
  * @returns The signatures; RFC 9421 ones if the request has
5060
5107
  * a `Signature-Input` header, or a draft-cavage one otherwise.
5061
5108
  * @internal
5062
5109
  */
5063
- function listRequestSignatures(request) {
5110
+ function listRequestSignatures(request, maxSignatures = 3) {
5064
5111
  const signatureInput = request.headers.get("Signature-Input");
5065
5112
  if (signatureInput != null) {
5066
5113
  const signatures = [];
5067
- const inputs = parseRfc9421SignatureInput(signatureInput);
5068
- for (const [label, input] of Object.entries(inputs)) {
5114
+ const inputs = Object.entries(parseRfc9421SignatureInput(signatureInput));
5115
+ for (const [label, input] of inputs.slice(0, maxSignatures)) {
5069
5116
  const keyId = parseKeyId(input.keyId);
5070
5117
  if (keyId == null) continue;
5071
5118
  signatures.push({
@@ -5193,6 +5240,18 @@ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime,
5193
5240
  body
5194
5241
  });
5195
5242
  }
5243
+ /**
5244
+ * Checks that a maximum number of signatures to verify is a positive safe
5245
+ * integer or `Infinity`.
5246
+ * @param value The value to check.
5247
+ * @param name The name of the option, used in the error message.
5248
+ * @throws {RangeError} Thrown if the value is not valid.
5249
+ * @internal
5250
+ */
5251
+ function validateMaxSignatures(value, name) {
5252
+ if (value === Infinity || Number.isSafeInteger(value) && value >= 1) return;
5253
+ throw new RangeError(`${name} must be a positive integer or Infinity.`);
5254
+ }
5196
5255
  const supportedHashAlgorithms = {
5197
5256
  "sha": "SHA-1",
5198
5257
  "sha-256": "SHA-256",
@@ -5285,6 +5344,8 @@ function recordVerificationResult(span, result) {
5285
5344
  * @param options Options for verifying the request.
5286
5345
  * @returns The public key of the verified signature, or `null` if the signature
5287
5346
  * could not be verified.
5347
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
5348
+ * is not a positive integer or `Infinity`.
5288
5349
  */
5289
5350
  async function verifyRequest(request, options = {}) {
5290
5351
  const result = await verifyRequestDetailed(request, options);
@@ -5297,9 +5358,12 @@ async function verifyRequest(request, options = {}) {
5297
5358
  * @param request The request to verify.
5298
5359
  * @param options Options for verifying the request.
5299
5360
  * @returns The verified public key, or a structured verification failure.
5361
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
5362
+ * is not a positive integer or `Infinity`.
5300
5363
  * @since 2.1.0
5301
5364
  */
5302
5365
  async function verifyRequestDetailed(request, options = {}) {
5366
+ if (options.maxSignatures !== void 0) validateMaxSignatures(options.maxSignatures, "maxSignatures");
5303
5367
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("http_signatures.verify", async (span) => {
5304
5368
  if (span.isRecording()) {
5305
5369
  span.setAttribute(ATTR_HTTP_REQUEST_METHOD, request.method);
@@ -5608,7 +5672,100 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
5608
5672
  }
5609
5673
  return false;
5610
5674
  }
5611
- async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
5675
+ /**
5676
+ * Verifies one RFC 9421 signature of a request with a key.
5677
+ * @param request The request, whose body may be consumed already.
5678
+ * @param sigInput The parsed `Signature-Input` member of the signature.
5679
+ * @param sigBytes The bytes of the signature.
5680
+ * @param key The public key named by the signature.
5681
+ * @param span The span to record the algorithm and the signature on.
5682
+ * @returns Whether the signature is verified, and the algorithm of the
5683
+ * signature if it is supported.
5684
+ */
5685
+ async function verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span) {
5686
+ const logger = getLogger([
5687
+ "fedify",
5688
+ "sig",
5689
+ "http"
5690
+ ]);
5691
+ let alg = sigInput.alg?.toLowerCase();
5692
+ if (alg == null) {
5693
+ 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";
5694
+ else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
5695
+ 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";
5696
+ else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
5697
+ }
5698
+ if (alg) span?.setAttribute("http_signatures.algorithm", alg);
5699
+ const algorithm = alg && rfc9421AlgorithmMap[alg];
5700
+ if (!algorithm) {
5701
+ logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
5702
+ algorithm: sigInput.alg,
5703
+ supported: Object.keys(rfc9421AlgorithmMap)
5704
+ });
5705
+ return {
5706
+ verified: false,
5707
+ mismatched: false
5708
+ };
5709
+ }
5710
+ const metricAlgorithm = alg;
5711
+ let signatureBase;
5712
+ try {
5713
+ signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
5714
+ } catch (error) {
5715
+ logger.debug("Failed to create signature base for verification: {error}", {
5716
+ error,
5717
+ signatureInput: sigInput
5718
+ });
5719
+ return {
5720
+ verified: false,
5721
+ algorithm: metricAlgorithm,
5722
+ mismatched: false
5723
+ };
5724
+ }
5725
+ const signatureBaseBytes = new TextEncoder().encode(signatureBase);
5726
+ span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
5727
+ const algorithmName = typeof algorithm === "string" ? algorithm : algorithm.name;
5728
+ if (key.publicKey.algorithm.name !== algorithmName) {
5729
+ logger.debug("Failed to verify; key {keyId} is not a key for algorithm {algorithm}.", {
5730
+ keyId: sigInput.keyId,
5731
+ algorithm: alg
5732
+ });
5733
+ return {
5734
+ verified: false,
5735
+ algorithm: metricAlgorithm,
5736
+ mismatched: true
5737
+ };
5738
+ }
5739
+ let verified;
5740
+ try {
5741
+ verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
5742
+ } catch (error) {
5743
+ logger.debug("Error during signature verification: {error}", {
5744
+ error,
5745
+ keyId: sigInput.keyId,
5746
+ algorithm: sigInput.alg
5747
+ });
5748
+ return {
5749
+ verified: false,
5750
+ algorithm: metricAlgorithm,
5751
+ mismatched: false
5752
+ };
5753
+ }
5754
+ if (verified) return {
5755
+ verified: true,
5756
+ algorithm: metricAlgorithm
5757
+ };
5758
+ logger.debug("Failed to verify signature with key {keyId}; signature invalid.", {
5759
+ keyId: sigInput.keyId,
5760
+ signatureBase
5761
+ });
5762
+ return {
5763
+ verified: false,
5764
+ algorithm: metricAlgorithm,
5765
+ mismatched: true
5766
+ };
5767
+ }
5768
+ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, maxSignatures = 3, meterProvider, tracerProvider } = {}) {
5612
5769
  const logger = getLogger([
5613
5770
  "fedify",
5614
5771
  "sig",
@@ -5621,7 +5778,6 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5621
5778
  logger.error("Failed to verify; the request body is locked.", { url: request.url });
5622
5779
  return noSignatureResult();
5623
5780
  }
5624
- const originalRequest = request;
5625
5781
  request = request.clone();
5626
5782
  const signatureInputHeader = request.headers.get("Signature-Input");
5627
5783
  if (!signatureInputHeader) {
@@ -5641,6 +5797,29 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5641
5797
  logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
5642
5798
  return invalidSignatureResult(null);
5643
5799
  }
5800
+ if (signatureNames.length > maxSignatures) {
5801
+ logger.debug("Verifying only the first {maxSignatures} of the {count} signatures in the Signature-Input header; the rest are ignored.", {
5802
+ maxSignatures,
5803
+ count: signatureNames.length
5804
+ });
5805
+ signatureNames.splice(maxSignatures);
5806
+ }
5807
+ const keyLookups = /* @__PURE__ */ new Map();
5808
+ const refreshedKeyIds = /* @__PURE__ */ new Set();
5809
+ const lookUpKey = (keyId, keyCache) => measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
5810
+ documentLoader,
5811
+ contextLoader,
5812
+ keyCache,
5813
+ tracerProvider,
5814
+ meterProvider,
5815
+ portableKeyResolvers: createPortableKeyResolvers({
5816
+ documentLoader,
5817
+ contextLoader,
5818
+ keyCache,
5819
+ tracerProvider,
5820
+ meterProvider
5821
+ })
5822
+ }));
5644
5823
  let failure = noSignatureResult();
5645
5824
  let failureAlgorithm;
5646
5825
  const setFailure = (result, algorithm) => {
@@ -5714,94 +5893,40 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5714
5893
  setFailure(invalidSignatureResult(null));
5715
5894
  continue;
5716
5895
  }
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;
5757
- }
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;
5896
+ let lookup = keyLookups.get(keyId.href);
5897
+ if (lookup == null) {
5898
+ lookup = await lookUpKey(keyId, keyCache);
5899
+ keyLookups.set(keyId.href, lookup);
5768
5900
  }
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;
5901
+ while (true) {
5902
+ const { key, cached, fetchError } = lookup;
5903
+ if (fetchError != null) {
5904
+ setFailure(keyFetchErrorResult(keyId, fetchError));
5905
+ break;
5906
+ }
5907
+ if (!key) {
5908
+ logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
5909
+ setFailure(invalidSignatureResult(keyId));
5910
+ break;
5911
+ }
5912
+ const result = await verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span);
5913
+ if (result.verified) {
5914
+ metricsContext.algorithm = result.algorithm;
5774
5915
  return {
5775
5916
  verified: true,
5776
5917
  key,
5777
5918
  signatureLabel: sigName
5778
5919
  };
5779
- } else if (cached) {
5920
+ }
5921
+ if (result.mismatched && cached && !refreshedKeyIds.has(keyId.href)) {
5780
5922
  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);
5923
+ refreshedKeyIds.add(keyId.href);
5924
+ lookup = await lookUpKey(keyId, bypassKeyCacheReads(keyCache));
5925
+ keyLookups.set(keyId.href, lookup);
5926
+ continue;
5797
5927
  }
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);
5928
+ setFailure(invalidSignatureResult(keyId), result.algorithm);
5929
+ break;
5805
5930
  }
5806
5931
  }
5807
5932
  metricsContext.algorithm = failureAlgorithm;
@@ -5901,16 +6026,19 @@ async function doubleKnock(request, identity, options = {}) {
5901
6026
  }
5902
6027
  async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
5903
6028
  const { specDeterminer, log, tracerProvider, signal } = options;
6029
+ signal?.throwIfAborted();
5904
6030
  const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
5905
6031
  visited.add(request.url);
5906
6032
  const origin = new URL(request.url).origin;
5907
6033
  const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
6034
+ signal?.throwIfAborted();
5908
6035
  const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
5909
6036
  let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
5910
6037
  spec: firstTrySpec,
5911
6038
  tracerProvider,
5912
6039
  body
5913
6040
  });
6041
+ signal?.throwIfAborted();
5914
6042
  log?.(signedRequest);
5915
6043
  let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
5916
6044
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -5935,6 +6063,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
5935
6063
  let fulfilled = false;
5936
6064
  let challengeRequest;
5937
6065
  for (const entry of entries) {
6066
+ signal?.throwIfAborted();
5938
6067
  const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
5939
6068
  if (rfc9421 == null) continue;
5940
6069
  logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
@@ -5957,6 +6086,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
5957
6086
  }
5958
6087
  }
5959
6088
  if (fulfilled && challengeRequest != null) {
6089
+ signal?.throwIfAborted();
5960
6090
  signedRequest = challengeRequest;
5961
6091
  log?.(signedRequest);
5962
6092
  response = await fetch(signedRequest, {
@@ -5981,11 +6111,13 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
5981
6111
  status: response.status,
5982
6112
  statusText: response.statusText
5983
6113
  });
6114
+ signal?.throwIfAborted();
5984
6115
  signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
5985
6116
  spec,
5986
6117
  tracerProvider,
5987
6118
  body
5988
6119
  });
6120
+ signal?.throwIfAborted();
5989
6121
  log?.(signedRequest);
5990
6122
  response = await fetchDoubleKnockRequest(request, signedRequest, signal);
5991
6123
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -6028,4 +6160,4 @@ function timingSafeEqual(a, b) {
6028
6160
  return result === 0;
6029
6161
  }
6030
6162
  //#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 };
6163
+ 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>;