@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
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.
- package/README.md +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
package/dist/sig/http.test.mjs
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as esm_default } from "../esm-
|
|
5
|
-
import { t as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { t as
|
|
9
|
-
import { t as
|
|
10
|
-
import { t as
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
4
|
+
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
6
|
+
import { t as AssertionError } from "../assertion_error-B-cx0fFA.mjs";
|
|
7
|
+
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
8
|
+
import { t as assertExists } from "../assert_exists-cffSpJ7X.mjs";
|
|
9
|
+
import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
10
|
+
import { t as assertGreaterOrEqual } from "../assert_greater_or_equal-BlafOF4-.mjs";
|
|
11
|
+
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
12
|
+
import "../std__assert-DvMDRPes.mjs";
|
|
13
|
+
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
14
|
+
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
15
|
+
import { n as exportJwk } from "../key-BspPiXqn.mjs";
|
|
16
|
+
import { a as listRequestSignatures, c as selectRequestSignature, f as verifyRequest, i as formatRfc9421SignatureParameters, l as signRequest, n as doubleKnock, o as parseRfc9421Signature, p as verifyRequestDetailed, r as formatRfc9421Signature, s as parseRfc9421SignatureInput, t as createRfc9421SignatureBase, u as timingSafeEqual } from "../http-siciWkmx.mjs";
|
|
17
|
+
import { a as rsaPrivateKey3, i as rsaPrivateKey2, l as rsaPublicKey5, n as ed25519PrivateKey, o as rsaPublicKey1, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
18
|
+
import { t as KvKeyCache } from "../keycache-Da8A8mZa.mjs";
|
|
19
|
+
import { FetchError, exportSpki, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
13
20
|
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
14
|
-
import { FetchError, exportSpki } from "@fedify/vocab-runtime";
|
|
15
21
|
import { encodeBase64 } from "byte-encodings/base64";
|
|
22
|
+
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_string_includes.js
|
|
23
|
+
/**
|
|
24
|
+
* Make an assertion that actual includes expected. If not
|
|
25
|
+
* then throw.
|
|
26
|
+
*
|
|
27
|
+
* @example Usage
|
|
28
|
+
* ```ts no-eval
|
|
29
|
+
* import { assertStringIncludes } from "@std/assert/assert-string-includes";
|
|
30
|
+
*
|
|
31
|
+
* assertStringIncludes("Hello", "ello"); // Doesn't throw
|
|
32
|
+
* assertStringIncludes("Hello", "world"); // Throws
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @param actual The actual string to check for inclusion.
|
|
36
|
+
* @param expected The expected string to check for inclusion.
|
|
37
|
+
* @param msg The optional message to display if the assertion fails.
|
|
38
|
+
*/ function assertStringIncludes(actual, expected, msg) {
|
|
39
|
+
if (!actual.includes(expected)) {
|
|
40
|
+
msg = `Expected actual: "${actual}" to contain: "${expected}"${msg ? `: ${msg}` : "."}`;
|
|
41
|
+
throw new AssertionError(msg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
16
45
|
//#region src/sig/http.test.ts
|
|
17
46
|
test("signRequest() [draft-cavage]", async () => {
|
|
18
47
|
assertEquals(await verifyRequest(await signRequest(new Request("https://example.com/", {
|
|
@@ -231,7 +260,7 @@ test("verifyRequestDetailed() records failure details on span", async () => {
|
|
|
231
260
|
assertEquals(span.attributes["http_signatures.key_fetch_status"], 410);
|
|
232
261
|
});
|
|
233
262
|
test("verifyRequestDetailed() records verification duration metric", async (t) => {
|
|
234
|
-
const buildSignedRequest = () => signRequest(new Request("https://example.com/inbox", {
|
|
263
|
+
const buildSignedRequest = (spec) => signRequest(new Request("https://example.com/inbox", {
|
|
235
264
|
method: "POST",
|
|
236
265
|
headers: {
|
|
237
266
|
"Content-Type": "application/activity+json",
|
|
@@ -242,7 +271,7 @@ test("verifyRequestDetailed() records verification duration metric", async (t) =
|
|
|
242
271
|
type: "Create",
|
|
243
272
|
actor: "https://example.com/key2"
|
|
244
273
|
})
|
|
245
|
-
}), rsaPrivateKey2, new URL("https://example.com/key2"));
|
|
274
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), { spec });
|
|
246
275
|
await t.step("verified path emits one measurement", async () => {
|
|
247
276
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
248
277
|
assert((await verifyRequestDetailed(await buildSignedRequest(), {
|
|
@@ -345,23 +374,26 @@ test("verifyRequestDetailed() records verification duration metric", async (t) =
|
|
|
345
374
|
assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1);
|
|
346
375
|
});
|
|
347
376
|
await t.step("cached-key retry emits one measurement, not two", async () => {
|
|
348
|
-
const [
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
377
|
+
for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) {
|
|
378
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
379
|
+
const cache = { "https://example.com/key2": rsaPublicKey1 };
|
|
380
|
+
assertExists(await verifyRequest(await buildSignedRequest(spec), {
|
|
381
|
+
contextLoader: mockDocumentLoader,
|
|
382
|
+
documentLoader: mockDocumentLoader,
|
|
383
|
+
meterProvider,
|
|
384
|
+
keyCache: {
|
|
385
|
+
get(keyId) {
|
|
386
|
+
return Promise.resolve(cache[keyId.href]);
|
|
387
|
+
},
|
|
388
|
+
set(keyId, k) {
|
|
389
|
+
cache[keyId.href] = k;
|
|
390
|
+
return Promise.resolve();
|
|
391
|
+
}
|
|
361
392
|
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
393
|
+
}), spec);
|
|
394
|
+
assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1, spec);
|
|
395
|
+
assertEquals(recorder.getMeasurements("activitypub.signature.key_fetch.duration").length, 2, spec);
|
|
396
|
+
}
|
|
365
397
|
});
|
|
366
398
|
await t.step("key fetch records result=fetched on a cold cache", async () => {
|
|
367
399
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
@@ -628,6 +660,430 @@ test("verifyRequest() [rfc9421] successful GET verification", async () => {
|
|
|
628
660
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
|
|
629
661
|
}), rsaPublicKey2, "Valid signature should verify to the correct public key");
|
|
630
662
|
});
|
|
663
|
+
for (const failure of [
|
|
664
|
+
"missing key",
|
|
665
|
+
"invalid signature",
|
|
666
|
+
"digest mismatch",
|
|
667
|
+
"empty body",
|
|
668
|
+
"cached key"
|
|
669
|
+
]) test(`verifyRequest() [rfc9421] multiple POST signatures: ${failure}`, async () => {
|
|
670
|
+
const body = failure === "empty body" ? "" : "Hello, world!";
|
|
671
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
672
|
+
const signed = await signRequest(new Request("https://example.com/inbox", {
|
|
673
|
+
method: "POST",
|
|
674
|
+
body
|
|
675
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
676
|
+
spec: "rfc9421",
|
|
677
|
+
currentTime
|
|
678
|
+
});
|
|
679
|
+
const input = signed.headers.get("Signature-Input");
|
|
680
|
+
const signature = signed.headers.get("Signature");
|
|
681
|
+
const firstInput = failure === "missing key" ? input.replace("https://example.com/key2", "https://example.com/missing-key") : input;
|
|
682
|
+
signed.headers.set("Signature-Input", `${firstInput}, ${input.replace(/^sig1=/, "sig2=")}`);
|
|
683
|
+
signed.headers.set("Signature", `sig1=:AAAAAA==:, ${signature.replace(/^sig1=/, "sig2=")}`);
|
|
684
|
+
const request = failure === "digest mismatch" ? new Request(signed, { body: "Tampered body" }) : signed;
|
|
685
|
+
const loaded = [];
|
|
686
|
+
assertEquals(await verifyRequest(request, {
|
|
687
|
+
keyCache: failure === "cached key" ? {
|
|
688
|
+
get: () => Promise.resolve(rsaPublicKey1),
|
|
689
|
+
set: async () => {}
|
|
690
|
+
} : void 0,
|
|
691
|
+
spec: "rfc9421",
|
|
692
|
+
currentTime,
|
|
693
|
+
contextLoader: mockDocumentLoader,
|
|
694
|
+
documentLoader: (url, options) => {
|
|
695
|
+
loaded.push(url);
|
|
696
|
+
return mockDocumentLoader(url, options);
|
|
697
|
+
}
|
|
698
|
+
}), failure === "digest mismatch" ? null : rsaPublicKey2);
|
|
699
|
+
assertEquals(loaded, failure === "digest mismatch" ? [] : failure === "missing key" ? ["https://example.com/missing-key", "https://example.com/key2"] : ["https://example.com/key2"]);
|
|
700
|
+
assertFalse(request.bodyUsed);
|
|
701
|
+
assertEquals(await request.text(), failure === "digest mismatch" ? "Tampered body" : body);
|
|
702
|
+
});
|
|
703
|
+
async function signRfc9421Many(signatures, currentTime) {
|
|
704
|
+
let request = new Request("https://example.com/inbox", {
|
|
705
|
+
method: "POST",
|
|
706
|
+
body: "Hello, world!"
|
|
707
|
+
});
|
|
708
|
+
let i = 0;
|
|
709
|
+
for (const signature of signatures) {
|
|
710
|
+
const { label, keyId, privateKey } = typeof signature === "string" ? { keyId: signature } : signature;
|
|
711
|
+
i++;
|
|
712
|
+
request = await signRequest(request, privateKey ?? rsaPrivateKey2, new URL(keyId), {
|
|
713
|
+
spec: "rfc9421",
|
|
714
|
+
currentTime,
|
|
715
|
+
rfc9421: { label: label ?? `sig${i}` }
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return request;
|
|
719
|
+
}
|
|
720
|
+
function splitRfc9421Header(header) {
|
|
721
|
+
return Object.fromEntries(header.split(/,\s*/).map((member) => [member.slice(0, member.indexOf("=")), member]));
|
|
722
|
+
}
|
|
723
|
+
function createCountingKeyLoader() {
|
|
724
|
+
const loaded = [];
|
|
725
|
+
const loader = (url, options) => {
|
|
726
|
+
loaded.push(url);
|
|
727
|
+
if (url === "https://example.com/key2") return mockDocumentLoader(url, options);
|
|
728
|
+
return Promise.reject(new FetchError(url, `Not found: ${url}`));
|
|
729
|
+
};
|
|
730
|
+
return Object.assign(loader, { loaded });
|
|
731
|
+
}
|
|
732
|
+
test("verifyRequestDetailed() [rfc9421] caps the number of signatures", async (t) => {
|
|
733
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
734
|
+
const missing = (n) => `https://example.com/missing-${n}`;
|
|
735
|
+
const valid = "https://example.com/key2";
|
|
736
|
+
await t.step("fetches only the keys of the first signatures", async () => {
|
|
737
|
+
const request = await signRfc9421Many([
|
|
738
|
+
1,
|
|
739
|
+
2,
|
|
740
|
+
3,
|
|
741
|
+
4,
|
|
742
|
+
5
|
|
743
|
+
].map(missing), currentTime);
|
|
744
|
+
for (const [maxSignatures, count] of [
|
|
745
|
+
[void 0, 3],
|
|
746
|
+
[1, 1],
|
|
747
|
+
[5, 5],
|
|
748
|
+
[Infinity, 5]
|
|
749
|
+
]) {
|
|
750
|
+
const documentLoader = createCountingKeyLoader();
|
|
751
|
+
const result = await verifyRequestDetailed(request, {
|
|
752
|
+
documentLoader,
|
|
753
|
+
contextLoader: mockDocumentLoader,
|
|
754
|
+
currentTime,
|
|
755
|
+
maxSignatures
|
|
756
|
+
});
|
|
757
|
+
assertFalse(result.verified);
|
|
758
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
759
|
+
assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(count));
|
|
760
|
+
assertEquals(documentLoader.loaded, [
|
|
761
|
+
1,
|
|
762
|
+
2,
|
|
763
|
+
3,
|
|
764
|
+
4,
|
|
765
|
+
5
|
|
766
|
+
].slice(0, count).map(missing));
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
await t.step("accepts a valid signature within the cap", async () => {
|
|
770
|
+
const documentLoader = createCountingKeyLoader();
|
|
771
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
772
|
+
missing(1),
|
|
773
|
+
missing(2),
|
|
774
|
+
valid,
|
|
775
|
+
missing(4)
|
|
776
|
+
], currentTime), {
|
|
777
|
+
documentLoader,
|
|
778
|
+
contextLoader: mockDocumentLoader,
|
|
779
|
+
currentTime
|
|
780
|
+
});
|
|
781
|
+
assert(result.verified);
|
|
782
|
+
assertEquals(result.key, rsaPublicKey2);
|
|
783
|
+
assertEquals(result.signatureLabel, "sig3");
|
|
784
|
+
assertEquals(documentLoader.loaded, [
|
|
785
|
+
missing(1),
|
|
786
|
+
missing(2),
|
|
787
|
+
valid
|
|
788
|
+
]);
|
|
789
|
+
});
|
|
790
|
+
await t.step("ignores a valid signature beyond the cap", async () => {
|
|
791
|
+
const request = await signRfc9421Many([
|
|
792
|
+
missing(1),
|
|
793
|
+
missing(2),
|
|
794
|
+
missing(3),
|
|
795
|
+
valid
|
|
796
|
+
], currentTime);
|
|
797
|
+
const documentLoader = createCountingKeyLoader();
|
|
798
|
+
const result = await verifyRequestDetailed(request, {
|
|
799
|
+
documentLoader,
|
|
800
|
+
contextLoader: mockDocumentLoader,
|
|
801
|
+
currentTime
|
|
802
|
+
});
|
|
803
|
+
assertFalse(result.verified);
|
|
804
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
805
|
+
assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(3));
|
|
806
|
+
assertEquals(documentLoader.loaded, [
|
|
807
|
+
1,
|
|
808
|
+
2,
|
|
809
|
+
3
|
|
810
|
+
].map(missing));
|
|
811
|
+
assertEquals(await verifyRequest(request, {
|
|
812
|
+
documentLoader: createCountingKeyLoader(),
|
|
813
|
+
contextLoader: mockDocumentLoader,
|
|
814
|
+
currentTime,
|
|
815
|
+
maxSignatures: 4
|
|
816
|
+
}), rsaPublicKey2);
|
|
817
|
+
});
|
|
818
|
+
await t.step("tries signatures in the header order", async () => {
|
|
819
|
+
const request = await signRfc9421Many([
|
|
820
|
+
{
|
|
821
|
+
label: "z",
|
|
822
|
+
keyId: missing(1)
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
label: "a",
|
|
826
|
+
keyId: valid
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
label: "m",
|
|
830
|
+
keyId: missing(3)
|
|
831
|
+
}
|
|
832
|
+
], currentTime);
|
|
833
|
+
const documentLoader = createCountingKeyLoader();
|
|
834
|
+
assertEquals(await verifyRequest(request, {
|
|
835
|
+
documentLoader,
|
|
836
|
+
contextLoader: mockDocumentLoader,
|
|
837
|
+
currentTime,
|
|
838
|
+
maxSignatures: 1
|
|
839
|
+
}), null);
|
|
840
|
+
assertEquals(documentLoader.loaded, [missing(1)]);
|
|
841
|
+
});
|
|
842
|
+
await t.step("counts signatures that fail before their keys are fetched", async () => {
|
|
843
|
+
const outdated = await signRfc9421Many([{
|
|
844
|
+
label: "old",
|
|
845
|
+
keyId: missing(1)
|
|
846
|
+
}], currentTime.subtract({ hours: 2 }));
|
|
847
|
+
const signed = await signRfc9421Many([
|
|
848
|
+
{
|
|
849
|
+
label: "nobytes1",
|
|
850
|
+
keyId: missing(2)
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
label: "nobytes2",
|
|
854
|
+
keyId: missing(3)
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
label: "valid",
|
|
858
|
+
keyId: valid
|
|
859
|
+
}
|
|
860
|
+
], currentTime);
|
|
861
|
+
const inputs = {
|
|
862
|
+
...splitRfc9421Header(outdated.headers.get("Signature-Input")),
|
|
863
|
+
...splitRfc9421Header(signed.headers.get("Signature-Input"))
|
|
864
|
+
};
|
|
865
|
+
const signatures = {
|
|
866
|
+
...splitRfc9421Header(outdated.headers.get("Signature")),
|
|
867
|
+
...splitRfc9421Header(signed.headers.get("Signature"))
|
|
868
|
+
};
|
|
869
|
+
const build = (labels) => {
|
|
870
|
+
const headers = new Headers(signed.headers);
|
|
871
|
+
headers.set("Signature-Input", [`dropped=("@method");keyid="${missing(0)}"`, ...labels.map((label) => inputs[label])].join(", "));
|
|
872
|
+
headers.set("Signature", labels.filter((label) => !label.startsWith("nobytes")).map((label) => signatures[label]).join(", "));
|
|
873
|
+
return new Request(signed.url, {
|
|
874
|
+
method: "POST",
|
|
875
|
+
headers,
|
|
876
|
+
body: "Hello, world!"
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
let documentLoader = createCountingKeyLoader();
|
|
880
|
+
let result = await verifyRequestDetailed(build([
|
|
881
|
+
"old",
|
|
882
|
+
"nobytes1",
|
|
883
|
+
"valid"
|
|
884
|
+
]), {
|
|
885
|
+
documentLoader,
|
|
886
|
+
contextLoader: mockDocumentLoader,
|
|
887
|
+
currentTime
|
|
888
|
+
});
|
|
889
|
+
assert(result.verified);
|
|
890
|
+
assertEquals(result.signatureLabel, "valid");
|
|
891
|
+
assertEquals(documentLoader.loaded, [valid]);
|
|
892
|
+
documentLoader = createCountingKeyLoader();
|
|
893
|
+
result = await verifyRequestDetailed(build([
|
|
894
|
+
"old",
|
|
895
|
+
"nobytes1",
|
|
896
|
+
"nobytes2",
|
|
897
|
+
"valid"
|
|
898
|
+
]), {
|
|
899
|
+
documentLoader,
|
|
900
|
+
contextLoader: mockDocumentLoader,
|
|
901
|
+
currentTime
|
|
902
|
+
});
|
|
903
|
+
assertFalse(result.verified);
|
|
904
|
+
assertEquals(documentLoader.loaded, []);
|
|
905
|
+
});
|
|
906
|
+
await t.step("rejects an invalid maxSignatures", async () => {
|
|
907
|
+
const unsigned = new Request("https://example.com/inbox");
|
|
908
|
+
for (const maxSignatures of [
|
|
909
|
+
0,
|
|
910
|
+
-1,
|
|
911
|
+
1.5,
|
|
912
|
+
NaN,
|
|
913
|
+
2 ** 53,
|
|
914
|
+
-Infinity
|
|
915
|
+
]) {
|
|
916
|
+
await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures }), RangeError);
|
|
917
|
+
await assertRejects(() => verifyRequest(unsigned, { maxSignatures }), RangeError);
|
|
918
|
+
}
|
|
919
|
+
await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures: null }), RangeError);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
test("verifyRequestDetailed() [rfc9421] looks up each key once per request", async (t) => {
|
|
923
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
924
|
+
const missing = (n) => `https://example.com/missing-${n}`;
|
|
925
|
+
const valid = "https://example.com/key2";
|
|
926
|
+
const forged = {
|
|
927
|
+
keyId: valid,
|
|
928
|
+
privateKey: rsaPrivateKey3
|
|
929
|
+
};
|
|
930
|
+
await t.step("signatures naming the same key", async () => {
|
|
931
|
+
const request = await signRfc9421Many([
|
|
932
|
+
forged,
|
|
933
|
+
forged,
|
|
934
|
+
valid
|
|
935
|
+
], currentTime);
|
|
936
|
+
for (const keyCache of [void 0, new KvKeyCache(new MemoryKvStore(), ["pk"])]) {
|
|
937
|
+
const documentLoader = createCountingKeyLoader();
|
|
938
|
+
const result = await verifyRequestDetailed(request, {
|
|
939
|
+
documentLoader,
|
|
940
|
+
contextLoader: mockDocumentLoader,
|
|
941
|
+
currentTime,
|
|
942
|
+
keyCache
|
|
943
|
+
});
|
|
944
|
+
assert(result.verified);
|
|
945
|
+
assertEquals(result.signatureLabel, "sig3");
|
|
946
|
+
assertEquals(documentLoader.loaded, [valid]);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
await t.step("signatures naming the same missing key", async () => {
|
|
950
|
+
const documentLoader = createCountingKeyLoader();
|
|
951
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
952
|
+
missing(1),
|
|
953
|
+
missing(1),
|
|
954
|
+
missing(1)
|
|
955
|
+
], currentTime), {
|
|
956
|
+
documentLoader,
|
|
957
|
+
contextLoader: mockDocumentLoader,
|
|
958
|
+
currentTime
|
|
959
|
+
});
|
|
960
|
+
assertFalse(result.verified);
|
|
961
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
962
|
+
assertEquals(documentLoader.loaded, [missing(1)]);
|
|
963
|
+
});
|
|
964
|
+
await t.step("separate requests", async () => {
|
|
965
|
+
const request = await signRfc9421Many([valid], currentTime);
|
|
966
|
+
const documentLoader = createCountingKeyLoader();
|
|
967
|
+
const options = {
|
|
968
|
+
documentLoader,
|
|
969
|
+
contextLoader: mockDocumentLoader,
|
|
970
|
+
currentTime
|
|
971
|
+
};
|
|
972
|
+
assertEquals(await verifyRequest(request, options), rsaPublicKey2);
|
|
973
|
+
assertEquals(await verifyRequest(request, options), rsaPublicKey2);
|
|
974
|
+
assertEquals(documentLoader.loaded, [valid, valid]);
|
|
975
|
+
});
|
|
976
|
+
const createStaleKeyCache = () => {
|
|
977
|
+
const gets = [];
|
|
978
|
+
return {
|
|
979
|
+
keyCache: {
|
|
980
|
+
get(keyId) {
|
|
981
|
+
gets.push(keyId.href);
|
|
982
|
+
return Promise.resolve(keyId.href === valid ? rsaPublicKey1 : keyId.href === missing(1) ? null : void 0);
|
|
983
|
+
},
|
|
984
|
+
set: () => Promise.resolve(),
|
|
985
|
+
getFetchError: (keyId) => Promise.resolve(keyId.href === missing(1) ? { error: new FetchError(keyId, "Cached failure") } : void 0),
|
|
986
|
+
setFetchError: () => Promise.resolve()
|
|
987
|
+
},
|
|
988
|
+
gets
|
|
989
|
+
};
|
|
990
|
+
};
|
|
991
|
+
await t.step("refreshes a stale cached key once, only for its signature", async () => {
|
|
992
|
+
const { keyCache, gets } = createStaleKeyCache();
|
|
993
|
+
const documentLoader = createCountingKeyLoader();
|
|
994
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
995
|
+
missing(1),
|
|
996
|
+
missing(2),
|
|
997
|
+
forged,
|
|
998
|
+
valid
|
|
999
|
+
], currentTime), {
|
|
1000
|
+
documentLoader,
|
|
1001
|
+
contextLoader: mockDocumentLoader,
|
|
1002
|
+
currentTime,
|
|
1003
|
+
keyCache,
|
|
1004
|
+
maxSignatures: 4
|
|
1005
|
+
});
|
|
1006
|
+
assert(result.verified);
|
|
1007
|
+
assertEquals(result.key, rsaPublicKey2);
|
|
1008
|
+
assertEquals(result.signatureLabel, "sig4");
|
|
1009
|
+
assertEquals(documentLoader.loaded, [missing(2), valid]);
|
|
1010
|
+
assertEquals(gets, [
|
|
1011
|
+
missing(1),
|
|
1012
|
+
missing(2),
|
|
1013
|
+
valid
|
|
1014
|
+
]);
|
|
1015
|
+
});
|
|
1016
|
+
await t.step("refreshes each stale cached key at most once", async () => {
|
|
1017
|
+
const stale = (n) => `https://example.com/stale-${n}`;
|
|
1018
|
+
const gets = [];
|
|
1019
|
+
const keyCache = {
|
|
1020
|
+
get(keyId) {
|
|
1021
|
+
gets.push(keyId.href);
|
|
1022
|
+
return Promise.resolve(rsaPublicKey1);
|
|
1023
|
+
},
|
|
1024
|
+
set: () => Promise.resolve()
|
|
1025
|
+
};
|
|
1026
|
+
const documentLoader = createCountingKeyLoader();
|
|
1027
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
1028
|
+
stale(1),
|
|
1029
|
+
stale(2),
|
|
1030
|
+
stale(1),
|
|
1031
|
+
stale(4)
|
|
1032
|
+
], currentTime), {
|
|
1033
|
+
documentLoader,
|
|
1034
|
+
contextLoader: mockDocumentLoader,
|
|
1035
|
+
currentTime,
|
|
1036
|
+
keyCache
|
|
1037
|
+
});
|
|
1038
|
+
assertFalse(result.verified);
|
|
1039
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
1040
|
+
assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, stale(1));
|
|
1041
|
+
assertEquals(documentLoader.loaded, [stale(1), stale(2)]);
|
|
1042
|
+
assertEquals(gets, [stale(1), stale(2)]);
|
|
1043
|
+
});
|
|
1044
|
+
await t.step("does not refresh a cached key for other failures", async () => {
|
|
1045
|
+
const [input] = Object.values(splitRfc9421Header((await signRfc9421Many([valid], currentTime)).headers.get("Signature-Input")));
|
|
1046
|
+
const withAlg = (alg) => /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, `;alg="${alg}"`) : `${input};alg="${alg}"`;
|
|
1047
|
+
for (const signatureInput of [withAlg("unknown"), input.replace(/^sig1=\([^)]*\)/, "sig1=(\"@unsupported\")")]) {
|
|
1048
|
+
const request = await signRfc9421Many([valid], currentTime);
|
|
1049
|
+
request.headers.set("Signature-Input", signatureInput);
|
|
1050
|
+
const { keyCache } = createStaleKeyCache();
|
|
1051
|
+
const documentLoader = createCountingKeyLoader();
|
|
1052
|
+
assertEquals(await verifyRequest(request, {
|
|
1053
|
+
documentLoader,
|
|
1054
|
+
contextLoader: mockDocumentLoader,
|
|
1055
|
+
currentTime,
|
|
1056
|
+
keyCache
|
|
1057
|
+
}), null, signatureInput);
|
|
1058
|
+
assertEquals(documentLoader.loaded, [], signatureInput);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
await t.step("refreshes a cached key of another algorithm family", async () => {
|
|
1062
|
+
const keyId = ed25519PublicKey.id.href;
|
|
1063
|
+
const request = await signRfc9421Many([keyId], currentTime);
|
|
1064
|
+
const input = request.headers.get("Signature-Input");
|
|
1065
|
+
request.headers.set("Signature-Input", /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, ";alg=\"ed25519\"") : `${input};alg="ed25519"`);
|
|
1066
|
+
const { sig1 } = parseRfc9421SignatureInput(request.headers.get("Signature-Input"));
|
|
1067
|
+
const signature = await crypto.subtle.sign("Ed25519", ed25519PrivateKey, new TextEncoder().encode(createRfc9421SignatureBase(request, sig1.components, sig1.parameters)));
|
|
1068
|
+
request.headers.set("Signature", `sig1=:${encodeBase64(new Uint8Array(signature))}:`);
|
|
1069
|
+
const loaded = [];
|
|
1070
|
+
const result = await verifyRequestDetailed(request, {
|
|
1071
|
+
documentLoader: (url, options) => {
|
|
1072
|
+
loaded.push(url);
|
|
1073
|
+
return mockDocumentLoader(url, options);
|
|
1074
|
+
},
|
|
1075
|
+
contextLoader: mockDocumentLoader,
|
|
1076
|
+
currentTime,
|
|
1077
|
+
keyCache: {
|
|
1078
|
+
get: (id) => Promise.resolve(id.href === keyId ? rsaPublicKey1 : void 0),
|
|
1079
|
+
set: () => Promise.resolve()
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
assert(result.verified);
|
|
1083
|
+
assertEquals(result.key.id?.href, keyId);
|
|
1084
|
+
assertEquals(loaded.length, 1);
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
631
1087
|
test("verifyRequest() [rfc9421] manual POST verification", async () => {
|
|
632
1088
|
const currentTimestamp = 1709626184;
|
|
633
1089
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
@@ -737,7 +1193,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
737
1193
|
contextLoader: mockDocumentLoader,
|
|
738
1194
|
documentLoader: mockDocumentLoader,
|
|
739
1195
|
spec: "rfc9421",
|
|
740
|
-
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(
|
|
1196
|
+
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1712218184 * 1e3)).toISOString()}`),
|
|
741
1197
|
timeWindow: { hours: 1 }
|
|
742
1198
|
}), null, "Should fail verification when signature timestamp is too old");
|
|
743
1199
|
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
@@ -769,7 +1225,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
769
1225
|
contextLoader: mockDocumentLoader,
|
|
770
1226
|
documentLoader: mockDocumentLoader,
|
|
771
1227
|
spec: "rfc9421",
|
|
772
|
-
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(
|
|
1228
|
+
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1741162184 * 1e3)).toISOString()}`),
|
|
773
1229
|
timeWindow: false
|
|
774
1230
|
}), rsaPublicKey2, "Should verify signature when time checking is disabled");
|
|
775
1231
|
const freshSignedPostRequest = await signRequest(new Request("https://example.com/api/resource", {
|
|
@@ -1001,6 +1457,7 @@ test("doubleKnock() function with redirect handling", async () => {
|
|
|
1001
1457
|
esm_default.spyGlobal();
|
|
1002
1458
|
const requestedUrls = [];
|
|
1003
1459
|
const responseCodes = [];
|
|
1460
|
+
const validatedRedirects = [];
|
|
1004
1461
|
esm_default.post("https://example.com/redirect-endpoint", (cl) => {
|
|
1005
1462
|
requestedUrls.push(cl.url);
|
|
1006
1463
|
responseCodes.push(302);
|
|
@@ -1018,11 +1475,41 @@ test("doubleKnock() function with redirect handling", async () => {
|
|
|
1018
1475
|
}), {
|
|
1019
1476
|
keyId: rsaPublicKey2.id,
|
|
1020
1477
|
privateKey: rsaPrivateKey2
|
|
1021
|
-
}
|
|
1478
|
+
}, { validateRedirect(url) {
|
|
1479
|
+
validatedRedirects.push(url);
|
|
1480
|
+
} })).status, 202, "Final response status should be 202 Accepted");
|
|
1022
1481
|
assertEquals(requestedUrls.length, 2, "Two URLs should have been requested");
|
|
1023
1482
|
assertEquals(requestedUrls[0], "https://example.com/redirect-endpoint", "First request should be to redirect-endpoint");
|
|
1024
1483
|
assertEquals(requestedUrls[1], "https://example.com/final-endpoint", "Second request should be to final-endpoint");
|
|
1025
1484
|
assertEquals(responseCodes, [302, 202], "Response status codes should match expected sequence");
|
|
1485
|
+
assertEquals(validatedRedirects, ["https://example.com/final-endpoint"]);
|
|
1486
|
+
esm_default.hardReset();
|
|
1487
|
+
});
|
|
1488
|
+
test("doubleKnock() validates redirects after signature fallback", async () => {
|
|
1489
|
+
esm_default.spyGlobal();
|
|
1490
|
+
let requestCount = 0;
|
|
1491
|
+
let privateRequestCount = 0;
|
|
1492
|
+
esm_default.post("https://example.com/redirect-after-fallback", (cl) => {
|
|
1493
|
+
requestCount++;
|
|
1494
|
+
if (cl.request.headers.has("Signature-Input")) return new Response("Unauthorized", { status: 401 });
|
|
1495
|
+
return Response.redirect("http://localhost/private", 302);
|
|
1496
|
+
});
|
|
1497
|
+
esm_default.post("http://localhost/private", () => {
|
|
1498
|
+
privateRequestCount++;
|
|
1499
|
+
return new Response("", { status: 202 });
|
|
1500
|
+
});
|
|
1501
|
+
const request = new Request("https://example.com/redirect-after-fallback", {
|
|
1502
|
+
method: "POST",
|
|
1503
|
+
body: "Test message"
|
|
1504
|
+
});
|
|
1505
|
+
await assertRejects(() => doubleKnock(request, {
|
|
1506
|
+
keyId: rsaPublicKey2.id,
|
|
1507
|
+
privateKey: rsaPrivateKey2
|
|
1508
|
+
}, { validateRedirect(url) {
|
|
1509
|
+
throw new Error(`Disallowed redirect: ${url}`);
|
|
1510
|
+
} }), Error, "Disallowed redirect: http://localhost/private");
|
|
1511
|
+
assertEquals(requestCount, 2);
|
|
1512
|
+
assertEquals(privateRequestCount, 0);
|
|
1026
1513
|
esm_default.hardReset();
|
|
1027
1514
|
});
|
|
1028
1515
|
test("doubleKnock() function with both specs rejected", async () => {
|
|
@@ -1813,6 +2300,37 @@ test("doubleKnock(): Accept-Signature challenge retry succeeds", async () => {
|
|
|
1813
2300
|
assertEquals(requestCount, 2);
|
|
1814
2301
|
esm_default.hardReset();
|
|
1815
2302
|
});
|
|
2303
|
+
test("doubleKnock(): validates redirects after Accept-Signature challenge retry", async () => {
|
|
2304
|
+
esm_default.spyGlobal();
|
|
2305
|
+
let requestCount = 0;
|
|
2306
|
+
let privateRequestCount = 0;
|
|
2307
|
+
esm_default.post("https://example.com/inbox-challenge-redirect", () => {
|
|
2308
|
+
requestCount++;
|
|
2309
|
+
if (requestCount === 1) return new Response("Not Authorized", {
|
|
2310
|
+
status: 401,
|
|
2311
|
+
headers: { "Accept-Signature": "sig1=(\"@method\" \"@target-uri\" \"@authority\" \"content-digest\");created;nonce=\"challenge-nonce-redirect\"" }
|
|
2312
|
+
});
|
|
2313
|
+
return Response.redirect("http://localhost/private", 302);
|
|
2314
|
+
});
|
|
2315
|
+
esm_default.post("http://localhost/private", () => {
|
|
2316
|
+
privateRequestCount++;
|
|
2317
|
+
return new Response("", { status: 202 });
|
|
2318
|
+
});
|
|
2319
|
+
const request = new Request("https://example.com/inbox-challenge-redirect", {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
body: "Test message",
|
|
2322
|
+
headers: { "Content-Type": "text/plain" }
|
|
2323
|
+
});
|
|
2324
|
+
await assertRejects(() => doubleKnock(request, {
|
|
2325
|
+
keyId: rsaPublicKey2.id,
|
|
2326
|
+
privateKey: rsaPrivateKey2
|
|
2327
|
+
}, { validateRedirect(url) {
|
|
2328
|
+
throw new Error(`Disallowed redirect: ${url}`);
|
|
2329
|
+
} }), Error, "Disallowed redirect: http://localhost/private");
|
|
2330
|
+
assertEquals(requestCount, 2);
|
|
2331
|
+
assertEquals(privateRequestCount, 0);
|
|
2332
|
+
esm_default.hardReset();
|
|
2333
|
+
});
|
|
1816
2334
|
test("doubleKnock(): unfulfillable Accept-Signature falls to legacy fallback", async () => {
|
|
1817
2335
|
esm_default.spyGlobal();
|
|
1818
2336
|
let requestCount = 0;
|
|
@@ -2009,5 +2527,158 @@ test("doubleKnock(): Accept-Signature with multiple compatible entries fulfills
|
|
|
2009
2527
|
assertEquals(requestCount, 2);
|
|
2010
2528
|
esm_default.hardReset();
|
|
2011
2529
|
});
|
|
2530
|
+
test("listRequestSignatures()", async (t) => {
|
|
2531
|
+
const request = () => new Request("https://example.com/inbox", {
|
|
2532
|
+
method: "POST",
|
|
2533
|
+
body: "Test message"
|
|
2534
|
+
});
|
|
2535
|
+
await t.step("draft-cavage", async () => {
|
|
2536
|
+
const signed = await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id);
|
|
2537
|
+
const signatures = listRequestSignatures(signed);
|
|
2538
|
+
assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, null]]);
|
|
2539
|
+
const components = signatures[0].components;
|
|
2540
|
+
assertEquals(components.map((c) => c.value), signed.headers.get("Signature").match(/headers="([^"]*)"/)[1].split(" "));
|
|
2541
|
+
assert(components.every((c) => Object.keys(c.params).length < 1));
|
|
2542
|
+
});
|
|
2543
|
+
await t.step("RFC 9421", async () => {
|
|
2544
|
+
const signatures = listRequestSignatures(await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id, {
|
|
2545
|
+
spec: "rfc9421",
|
|
2546
|
+
rfc9421: { label: "a" }
|
|
2547
|
+
}));
|
|
2548
|
+
assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, "a"]]);
|
|
2549
|
+
assertEquals(signatures[0].components.map((c) => c.value), [
|
|
2550
|
+
"@method",
|
|
2551
|
+
"@target-uri",
|
|
2552
|
+
"@authority",
|
|
2553
|
+
"host",
|
|
2554
|
+
"date",
|
|
2555
|
+
"content-digest"
|
|
2556
|
+
]);
|
|
2557
|
+
});
|
|
2558
|
+
await t.step("RFC 9421 beyond the limit", async () => {
|
|
2559
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
2560
|
+
const signed = await signRfc9421Many([
|
|
2561
|
+
1,
|
|
2562
|
+
2,
|
|
2563
|
+
3,
|
|
2564
|
+
4
|
|
2565
|
+
].map((n) => `https://example.com/key-${n}`), currentTime);
|
|
2566
|
+
const headers = new Headers(signed.headers);
|
|
2567
|
+
headers.set("Signature-Input", headers.get("Signature-Input").replace("https://example.com/key-1", "not a URL"));
|
|
2568
|
+
const request = new Request(signed.url, {
|
|
2569
|
+
method: "POST",
|
|
2570
|
+
headers
|
|
2571
|
+
});
|
|
2572
|
+
const keyIds = (maxSignatures) => listRequestSignatures(request, maxSignatures).map((s) => s.keyId.href);
|
|
2573
|
+
assertEquals(keyIds(), ["https://example.com/key-2", "https://example.com/key-3"]);
|
|
2574
|
+
assertEquals(keyIds(1), []);
|
|
2575
|
+
assertEquals(keyIds(Infinity), [
|
|
2576
|
+
"https://example.com/key-2",
|
|
2577
|
+
"https://example.com/key-3",
|
|
2578
|
+
"https://example.com/key-4"
|
|
2579
|
+
]);
|
|
2580
|
+
});
|
|
2581
|
+
await t.step("unsigned or malformed", () => {
|
|
2582
|
+
assertEquals(listRequestSignatures(request()), []);
|
|
2583
|
+
const malformed = request();
|
|
2584
|
+
malformed.headers.set("Signature", "keyId=\"not a URL\",signature=\"AA==\"");
|
|
2585
|
+
assertEquals(listRequestSignatures(malformed), []);
|
|
2586
|
+
const garbage = request();
|
|
2587
|
+
garbage.headers.set("Signature-Input", "((");
|
|
2588
|
+
garbage.headers.set("Signature", "((");
|
|
2589
|
+
assertEquals(listRequestSignatures(garbage), []);
|
|
2590
|
+
});
|
|
2591
|
+
});
|
|
2592
|
+
test("selectRequestSignature()", async () => {
|
|
2593
|
+
const body = "Test message";
|
|
2594
|
+
const sign = (label, keyId) => signRequest(new Request("https://example.com/inbox", {
|
|
2595
|
+
method: "POST",
|
|
2596
|
+
body
|
|
2597
|
+
}), rsaPrivateKey2, keyId, {
|
|
2598
|
+
spec: "rfc9421",
|
|
2599
|
+
rfc9421: { label }
|
|
2600
|
+
});
|
|
2601
|
+
const first = await sign("a", new URL("https://example.com/other#key"));
|
|
2602
|
+
const second = await sign("b", rsaPublicKey2.id);
|
|
2603
|
+
const headers = new Headers(second.headers);
|
|
2604
|
+
for (const name of ["Signature-Input", "Signature"]) headers.set(name, `${first.headers.get(name)}, ${second.headers.get(name)}`);
|
|
2605
|
+
const request = new Request(second.url, {
|
|
2606
|
+
method: "POST",
|
|
2607
|
+
headers,
|
|
2608
|
+
body
|
|
2609
|
+
});
|
|
2610
|
+
const signatures = listRequestSignatures(request);
|
|
2611
|
+
assertEquals(signatures.map((s) => s.label), ["a", "b"]);
|
|
2612
|
+
const selected = selectRequestSignature(request, signatures[1]);
|
|
2613
|
+
assertEquals(listRequestSignatures(selected).map((s) => [s.label, s.keyId.href]), [["b", rsaPublicKey2.id.href]]);
|
|
2614
|
+
const result = await verifyRequestDetailed(selected, {
|
|
2615
|
+
documentLoader: mockDocumentLoader,
|
|
2616
|
+
contextLoader: mockDocumentLoader
|
|
2617
|
+
});
|
|
2618
|
+
assert(result.verified);
|
|
2619
|
+
assertEquals(result.key.id, rsaPublicKey2.id);
|
|
2620
|
+
assertEquals(await selected.text(), body);
|
|
2621
|
+
assertEquals(await request.text(), body);
|
|
2622
|
+
});
|
|
2623
|
+
test("verifyRequestDetailed() reports a timed-out key fetch", async (t) => {
|
|
2624
|
+
for (const stall of ["headers", "body"]) await t.step(`stalled ${stall}`, async () => {
|
|
2625
|
+
esm_default.mockGlobal();
|
|
2626
|
+
const keyId = new URL(`https://slow.example/actors/alice#${stall}`);
|
|
2627
|
+
let requests = 0;
|
|
2628
|
+
esm_default.get(`begin:https://slow.example/actors/alice`, () => {
|
|
2629
|
+
requests++;
|
|
2630
|
+
if (stall === "headers") return new Promise(() => {});
|
|
2631
|
+
return new Response(new ReadableStream({ pull: () => new Promise(() => {}) }), { status: 404 });
|
|
2632
|
+
});
|
|
2633
|
+
const loader = getDocumentLoader({
|
|
2634
|
+
allowPrivateAddress: true,
|
|
2635
|
+
timeout: 100
|
|
2636
|
+
});
|
|
2637
|
+
const documentLoader = (url, options) => url.startsWith("https://slow.example/") ? loader(url, options) : mockDocumentLoader(url);
|
|
2638
|
+
const kv = new MemoryKvStore();
|
|
2639
|
+
try {
|
|
2640
|
+
const sign = () => signRequest(new Request("https://example.com/inbox", {
|
|
2641
|
+
method: "POST",
|
|
2642
|
+
headers: {
|
|
2643
|
+
"Content-Type": "application/activity+json",
|
|
2644
|
+
accept: "application/ld+json"
|
|
2645
|
+
},
|
|
2646
|
+
body: JSON.stringify({
|
|
2647
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2648
|
+
type: "Create",
|
|
2649
|
+
actor: "https://slow.example/actors/alice"
|
|
2650
|
+
})
|
|
2651
|
+
}), rsaPrivateKey2, keyId);
|
|
2652
|
+
const result = await verifyRequestDetailed(await sign(), {
|
|
2653
|
+
contextLoader: mockDocumentLoader,
|
|
2654
|
+
documentLoader,
|
|
2655
|
+
keyCache: new KvKeyCache(kv, ["pk"])
|
|
2656
|
+
});
|
|
2657
|
+
assertFalse(result.verified);
|
|
2658
|
+
assert(result.reason.type === "keyFetchError");
|
|
2659
|
+
assertEquals(result.reason.keyId, keyId);
|
|
2660
|
+
assert("error" in result.reason.result);
|
|
2661
|
+
const error = result.reason.result.error;
|
|
2662
|
+
assert(error instanceof FetchError);
|
|
2663
|
+
assertEquals(error.response, void 0);
|
|
2664
|
+
assert(error.cause instanceof DOMException);
|
|
2665
|
+
assertEquals(error.cause.name, "TimeoutError");
|
|
2666
|
+
assertEquals(requests, 1);
|
|
2667
|
+
const cached = await verifyRequestDetailed(await sign(), {
|
|
2668
|
+
contextLoader: mockDocumentLoader,
|
|
2669
|
+
documentLoader,
|
|
2670
|
+
keyCache: new KvKeyCache(kv, ["pk"])
|
|
2671
|
+
});
|
|
2672
|
+
assertFalse(cached.verified);
|
|
2673
|
+
assert(cached.reason.type === "keyFetchError");
|
|
2674
|
+
assert("error" in cached.reason.result);
|
|
2675
|
+
assertEquals(cached.reason.result.error.name, "FetchError");
|
|
2676
|
+
assertStringIncludes(cached.reason.result.error.message, "Timed out after 100 ms");
|
|
2677
|
+
assertEquals(requests, 1);
|
|
2678
|
+
} finally {
|
|
2679
|
+
esm_default.hardReset();
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
});
|
|
2012
2683
|
//#endregion
|
|
2013
2684
|
export {};
|