@fedify/fedify 2.4.0-pr.936.41 → 2.4.1
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-C1o3n_ih.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-DoFNbS9M.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-D7MEaAg-.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-uYfrT5sv.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 +246 -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-4rvjH8Md.cjs} +8663 -3725
- package/dist/http-B_wV17Ij.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-CNzz4a_O.mjs} +397 -168
- package/dist/http-riIBL3-C.cjs +10630 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-hMoNKe7p.mjs +951 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-D-MNltOk.mjs +221 -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-DQ29C4XL.cjs → kv-cache-BjK2Y9Wu.cjs} +55 -19
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-CetcQsQa.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-DVJLXeI0.js} +54 -16
- package/dist/{ld-Cj9F_5AP.mjs → ld-CW_8GOEC.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-CBugc7uM.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-CvwTUgnP.mjs → middleware-CEQBUZaH.mjs} +3157 -272
- package/dist/{middleware-Y-kpsp6S.js → middleware-CjdyJcGd.js} +5530 -1120
- package/dist/{middleware-CEctxBN6.mjs → middleware-D0PeYTxA.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--VRABJfK.mjs +267 -0
- 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/portable-collection-BVQYFfEq.mjs +418 -0
- package/dist/portable-collection-CiKz-t9W.cjs +686 -0
- package/dist/portable-collection-CnSD193J.js +670 -0
- package/dist/portable-key-BjUXSULt.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-kTlg91Kb.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-DohrDmGq.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 +759 -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-VKyzJSNt.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-hMoNKe7p.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-CNzz4a_O.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-D-MNltOk.mjs";
|
|
19
|
+
import { FetchError, UrlError, 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();
|
|
@@ -440,6 +472,63 @@ test("verifyRequestDetailed() records verification duration metric", async (t) =
|
|
|
440
472
|
assertFalse("http_signatures.algorithm" in measurements[0].attributes);
|
|
441
473
|
});
|
|
442
474
|
});
|
|
475
|
+
test("verifyRequestDetailed() preserves cached key fetch errors", async (t) => {
|
|
476
|
+
const keyId = new URL("https://unreachable.example/actors/alice#main-key");
|
|
477
|
+
const timeout = new FetchError("https://unreachable.example/resolved-key", "Timed out after 100 ms");
|
|
478
|
+
timeout.cause = new DOMException("The operation timed out", "TimeoutError");
|
|
479
|
+
const failures = [
|
|
480
|
+
timeout,
|
|
481
|
+
new UrlError("DNS lookup failed", {
|
|
482
|
+
reason: "dns",
|
|
483
|
+
cause: /* @__PURE__ */ new TypeError("No usable IP addresses")
|
|
484
|
+
}),
|
|
485
|
+
new UrlError("Private IP address is not allowed")
|
|
486
|
+
];
|
|
487
|
+
for (const failure of failures) await t.step(failure.message, async () => {
|
|
488
|
+
const kv = new MemoryKvStore();
|
|
489
|
+
let fetches = 0;
|
|
490
|
+
const documentLoader = (url) => {
|
|
491
|
+
if (url === keyId.href) {
|
|
492
|
+
fetches++;
|
|
493
|
+
throw failure;
|
|
494
|
+
}
|
|
495
|
+
return mockDocumentLoader(url);
|
|
496
|
+
};
|
|
497
|
+
for (const cached of [false, true]) {
|
|
498
|
+
const result = await verifyRequestDetailed(await signRequest(new Request("https://example.com/inbox", {
|
|
499
|
+
method: "POST",
|
|
500
|
+
body: "Test activity"
|
|
501
|
+
}), rsaPrivateKey2, keyId), {
|
|
502
|
+
documentLoader,
|
|
503
|
+
contextLoader: mockDocumentLoader,
|
|
504
|
+
keyCache: new KvKeyCache(kv, ["pk"])
|
|
505
|
+
});
|
|
506
|
+
assertFalse(result.verified);
|
|
507
|
+
assert(result.reason.type === "keyFetchError");
|
|
508
|
+
assert("error" in result.reason.result);
|
|
509
|
+
const error = result.reason.result.error;
|
|
510
|
+
assertEquals(error.name, failure.name);
|
|
511
|
+
assertEquals(error.message, failure.message);
|
|
512
|
+
if (failure instanceof FetchError) {
|
|
513
|
+
assert(error instanceof FetchError);
|
|
514
|
+
assertEquals(error.url.href, failure.url.href);
|
|
515
|
+
assertEquals(error.response, void 0);
|
|
516
|
+
assert(error.cause instanceof DOMException);
|
|
517
|
+
assertEquals(error.cause.name, "TimeoutError");
|
|
518
|
+
assertEquals(error.cause.message, "The operation timed out");
|
|
519
|
+
} else {
|
|
520
|
+
assert(error instanceof UrlError);
|
|
521
|
+
assertEquals(error.reason, failure.reason);
|
|
522
|
+
if (failure.cause instanceof Error) {
|
|
523
|
+
assert(error.cause instanceof Error);
|
|
524
|
+
assertEquals(error.cause.name, failure.cause.name);
|
|
525
|
+
assertEquals(error.cause.message, failure.cause.message);
|
|
526
|
+
} else assertEquals(error.cause, void 0);
|
|
527
|
+
}
|
|
528
|
+
assertEquals(fetches, 1, cached ? "Cached failure was refetched" : "");
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
});
|
|
443
532
|
test("signRequest() and verifyRequest() [rfc9421] implementation", async () => {
|
|
444
533
|
const currentTimestamp = 1709626184;
|
|
445
534
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
@@ -628,6 +717,430 @@ test("verifyRequest() [rfc9421] successful GET verification", async () => {
|
|
|
628
717
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
|
|
629
718
|
}), rsaPublicKey2, "Valid signature should verify to the correct public key");
|
|
630
719
|
});
|
|
720
|
+
for (const failure of [
|
|
721
|
+
"missing key",
|
|
722
|
+
"invalid signature",
|
|
723
|
+
"digest mismatch",
|
|
724
|
+
"empty body",
|
|
725
|
+
"cached key"
|
|
726
|
+
]) test(`verifyRequest() [rfc9421] multiple POST signatures: ${failure}`, async () => {
|
|
727
|
+
const body = failure === "empty body" ? "" : "Hello, world!";
|
|
728
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
729
|
+
const signed = await signRequest(new Request("https://example.com/inbox", {
|
|
730
|
+
method: "POST",
|
|
731
|
+
body
|
|
732
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
733
|
+
spec: "rfc9421",
|
|
734
|
+
currentTime
|
|
735
|
+
});
|
|
736
|
+
const input = signed.headers.get("Signature-Input");
|
|
737
|
+
const signature = signed.headers.get("Signature");
|
|
738
|
+
const firstInput = failure === "missing key" ? input.replace("https://example.com/key2", "https://example.com/missing-key") : input;
|
|
739
|
+
signed.headers.set("Signature-Input", `${firstInput}, ${input.replace(/^sig1=/, "sig2=")}`);
|
|
740
|
+
signed.headers.set("Signature", `sig1=:AAAAAA==:, ${signature.replace(/^sig1=/, "sig2=")}`);
|
|
741
|
+
const request = failure === "digest mismatch" ? new Request(signed, { body: "Tampered body" }) : signed;
|
|
742
|
+
const loaded = [];
|
|
743
|
+
assertEquals(await verifyRequest(request, {
|
|
744
|
+
keyCache: failure === "cached key" ? {
|
|
745
|
+
get: () => Promise.resolve(rsaPublicKey1),
|
|
746
|
+
set: async () => {}
|
|
747
|
+
} : void 0,
|
|
748
|
+
spec: "rfc9421",
|
|
749
|
+
currentTime,
|
|
750
|
+
contextLoader: mockDocumentLoader,
|
|
751
|
+
documentLoader: (url, options) => {
|
|
752
|
+
loaded.push(url);
|
|
753
|
+
return mockDocumentLoader(url, options);
|
|
754
|
+
}
|
|
755
|
+
}), failure === "digest mismatch" ? null : rsaPublicKey2);
|
|
756
|
+
assertEquals(loaded, failure === "digest mismatch" ? [] : failure === "missing key" ? ["https://example.com/missing-key", "https://example.com/key2"] : ["https://example.com/key2"]);
|
|
757
|
+
assertFalse(request.bodyUsed);
|
|
758
|
+
assertEquals(await request.text(), failure === "digest mismatch" ? "Tampered body" : body);
|
|
759
|
+
});
|
|
760
|
+
async function signRfc9421Many(signatures, currentTime) {
|
|
761
|
+
let request = new Request("https://example.com/inbox", {
|
|
762
|
+
method: "POST",
|
|
763
|
+
body: "Hello, world!"
|
|
764
|
+
});
|
|
765
|
+
let i = 0;
|
|
766
|
+
for (const signature of signatures) {
|
|
767
|
+
const { label, keyId, privateKey } = typeof signature === "string" ? { keyId: signature } : signature;
|
|
768
|
+
i++;
|
|
769
|
+
request = await signRequest(request, privateKey ?? rsaPrivateKey2, new URL(keyId), {
|
|
770
|
+
spec: "rfc9421",
|
|
771
|
+
currentTime,
|
|
772
|
+
rfc9421: { label: label ?? `sig${i}` }
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return request;
|
|
776
|
+
}
|
|
777
|
+
function splitRfc9421Header(header) {
|
|
778
|
+
return Object.fromEntries(header.split(/,\s*/).map((member) => [member.slice(0, member.indexOf("=")), member]));
|
|
779
|
+
}
|
|
780
|
+
function createCountingKeyLoader() {
|
|
781
|
+
const loaded = [];
|
|
782
|
+
const loader = (url, options) => {
|
|
783
|
+
loaded.push(url);
|
|
784
|
+
if (url === "https://example.com/key2") return mockDocumentLoader(url, options);
|
|
785
|
+
return Promise.reject(new FetchError(url, `Not found: ${url}`));
|
|
786
|
+
};
|
|
787
|
+
return Object.assign(loader, { loaded });
|
|
788
|
+
}
|
|
789
|
+
test("verifyRequestDetailed() [rfc9421] caps the number of signatures", async (t) => {
|
|
790
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
791
|
+
const missing = (n) => `https://example.com/missing-${n}`;
|
|
792
|
+
const valid = "https://example.com/key2";
|
|
793
|
+
await t.step("fetches only the keys of the first signatures", async () => {
|
|
794
|
+
const request = await signRfc9421Many([
|
|
795
|
+
1,
|
|
796
|
+
2,
|
|
797
|
+
3,
|
|
798
|
+
4,
|
|
799
|
+
5
|
|
800
|
+
].map(missing), currentTime);
|
|
801
|
+
for (const [maxSignatures, count] of [
|
|
802
|
+
[void 0, 3],
|
|
803
|
+
[1, 1],
|
|
804
|
+
[5, 5],
|
|
805
|
+
[Infinity, 5]
|
|
806
|
+
]) {
|
|
807
|
+
const documentLoader = createCountingKeyLoader();
|
|
808
|
+
const result = await verifyRequestDetailed(request, {
|
|
809
|
+
documentLoader,
|
|
810
|
+
contextLoader: mockDocumentLoader,
|
|
811
|
+
currentTime,
|
|
812
|
+
maxSignatures
|
|
813
|
+
});
|
|
814
|
+
assertFalse(result.verified);
|
|
815
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
816
|
+
assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(count));
|
|
817
|
+
assertEquals(documentLoader.loaded, [
|
|
818
|
+
1,
|
|
819
|
+
2,
|
|
820
|
+
3,
|
|
821
|
+
4,
|
|
822
|
+
5
|
|
823
|
+
].slice(0, count).map(missing));
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
await t.step("accepts a valid signature within the cap", async () => {
|
|
827
|
+
const documentLoader = createCountingKeyLoader();
|
|
828
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
829
|
+
missing(1),
|
|
830
|
+
missing(2),
|
|
831
|
+
valid,
|
|
832
|
+
missing(4)
|
|
833
|
+
], currentTime), {
|
|
834
|
+
documentLoader,
|
|
835
|
+
contextLoader: mockDocumentLoader,
|
|
836
|
+
currentTime
|
|
837
|
+
});
|
|
838
|
+
assert(result.verified);
|
|
839
|
+
assertEquals(result.key, rsaPublicKey2);
|
|
840
|
+
assertEquals(result.signatureLabel, "sig3");
|
|
841
|
+
assertEquals(documentLoader.loaded, [
|
|
842
|
+
missing(1),
|
|
843
|
+
missing(2),
|
|
844
|
+
valid
|
|
845
|
+
]);
|
|
846
|
+
});
|
|
847
|
+
await t.step("ignores a valid signature beyond the cap", async () => {
|
|
848
|
+
const request = await signRfc9421Many([
|
|
849
|
+
missing(1),
|
|
850
|
+
missing(2),
|
|
851
|
+
missing(3),
|
|
852
|
+
valid
|
|
853
|
+
], currentTime);
|
|
854
|
+
const documentLoader = createCountingKeyLoader();
|
|
855
|
+
const result = await verifyRequestDetailed(request, {
|
|
856
|
+
documentLoader,
|
|
857
|
+
contextLoader: mockDocumentLoader,
|
|
858
|
+
currentTime
|
|
859
|
+
});
|
|
860
|
+
assertFalse(result.verified);
|
|
861
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
862
|
+
assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(3));
|
|
863
|
+
assertEquals(documentLoader.loaded, [
|
|
864
|
+
1,
|
|
865
|
+
2,
|
|
866
|
+
3
|
|
867
|
+
].map(missing));
|
|
868
|
+
assertEquals(await verifyRequest(request, {
|
|
869
|
+
documentLoader: createCountingKeyLoader(),
|
|
870
|
+
contextLoader: mockDocumentLoader,
|
|
871
|
+
currentTime,
|
|
872
|
+
maxSignatures: 4
|
|
873
|
+
}), rsaPublicKey2);
|
|
874
|
+
});
|
|
875
|
+
await t.step("tries signatures in the header order", async () => {
|
|
876
|
+
const request = await signRfc9421Many([
|
|
877
|
+
{
|
|
878
|
+
label: "z",
|
|
879
|
+
keyId: missing(1)
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
label: "a",
|
|
883
|
+
keyId: valid
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
label: "m",
|
|
887
|
+
keyId: missing(3)
|
|
888
|
+
}
|
|
889
|
+
], currentTime);
|
|
890
|
+
const documentLoader = createCountingKeyLoader();
|
|
891
|
+
assertEquals(await verifyRequest(request, {
|
|
892
|
+
documentLoader,
|
|
893
|
+
contextLoader: mockDocumentLoader,
|
|
894
|
+
currentTime,
|
|
895
|
+
maxSignatures: 1
|
|
896
|
+
}), null);
|
|
897
|
+
assertEquals(documentLoader.loaded, [missing(1)]);
|
|
898
|
+
});
|
|
899
|
+
await t.step("counts signatures that fail before their keys are fetched", async () => {
|
|
900
|
+
const outdated = await signRfc9421Many([{
|
|
901
|
+
label: "old",
|
|
902
|
+
keyId: missing(1)
|
|
903
|
+
}], currentTime.subtract({ hours: 2 }));
|
|
904
|
+
const signed = await signRfc9421Many([
|
|
905
|
+
{
|
|
906
|
+
label: "nobytes1",
|
|
907
|
+
keyId: missing(2)
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
label: "nobytes2",
|
|
911
|
+
keyId: missing(3)
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
label: "valid",
|
|
915
|
+
keyId: valid
|
|
916
|
+
}
|
|
917
|
+
], currentTime);
|
|
918
|
+
const inputs = {
|
|
919
|
+
...splitRfc9421Header(outdated.headers.get("Signature-Input")),
|
|
920
|
+
...splitRfc9421Header(signed.headers.get("Signature-Input"))
|
|
921
|
+
};
|
|
922
|
+
const signatures = {
|
|
923
|
+
...splitRfc9421Header(outdated.headers.get("Signature")),
|
|
924
|
+
...splitRfc9421Header(signed.headers.get("Signature"))
|
|
925
|
+
};
|
|
926
|
+
const build = (labels) => {
|
|
927
|
+
const headers = new Headers(signed.headers);
|
|
928
|
+
headers.set("Signature-Input", [`dropped=("@method");keyid="${missing(0)}"`, ...labels.map((label) => inputs[label])].join(", "));
|
|
929
|
+
headers.set("Signature", labels.filter((label) => !label.startsWith("nobytes")).map((label) => signatures[label]).join(", "));
|
|
930
|
+
return new Request(signed.url, {
|
|
931
|
+
method: "POST",
|
|
932
|
+
headers,
|
|
933
|
+
body: "Hello, world!"
|
|
934
|
+
});
|
|
935
|
+
};
|
|
936
|
+
let documentLoader = createCountingKeyLoader();
|
|
937
|
+
let result = await verifyRequestDetailed(build([
|
|
938
|
+
"old",
|
|
939
|
+
"nobytes1",
|
|
940
|
+
"valid"
|
|
941
|
+
]), {
|
|
942
|
+
documentLoader,
|
|
943
|
+
contextLoader: mockDocumentLoader,
|
|
944
|
+
currentTime
|
|
945
|
+
});
|
|
946
|
+
assert(result.verified);
|
|
947
|
+
assertEquals(result.signatureLabel, "valid");
|
|
948
|
+
assertEquals(documentLoader.loaded, [valid]);
|
|
949
|
+
documentLoader = createCountingKeyLoader();
|
|
950
|
+
result = await verifyRequestDetailed(build([
|
|
951
|
+
"old",
|
|
952
|
+
"nobytes1",
|
|
953
|
+
"nobytes2",
|
|
954
|
+
"valid"
|
|
955
|
+
]), {
|
|
956
|
+
documentLoader,
|
|
957
|
+
contextLoader: mockDocumentLoader,
|
|
958
|
+
currentTime
|
|
959
|
+
});
|
|
960
|
+
assertFalse(result.verified);
|
|
961
|
+
assertEquals(documentLoader.loaded, []);
|
|
962
|
+
});
|
|
963
|
+
await t.step("rejects an invalid maxSignatures", async () => {
|
|
964
|
+
const unsigned = new Request("https://example.com/inbox");
|
|
965
|
+
for (const maxSignatures of [
|
|
966
|
+
0,
|
|
967
|
+
-1,
|
|
968
|
+
1.5,
|
|
969
|
+
NaN,
|
|
970
|
+
2 ** 53,
|
|
971
|
+
-Infinity
|
|
972
|
+
]) {
|
|
973
|
+
await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures }), RangeError);
|
|
974
|
+
await assertRejects(() => verifyRequest(unsigned, { maxSignatures }), RangeError);
|
|
975
|
+
}
|
|
976
|
+
await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures: null }), RangeError);
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
test("verifyRequestDetailed() [rfc9421] looks up each key once per request", async (t) => {
|
|
980
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
981
|
+
const missing = (n) => `https://example.com/missing-${n}`;
|
|
982
|
+
const valid = "https://example.com/key2";
|
|
983
|
+
const forged = {
|
|
984
|
+
keyId: valid,
|
|
985
|
+
privateKey: rsaPrivateKey3
|
|
986
|
+
};
|
|
987
|
+
await t.step("signatures naming the same key", async () => {
|
|
988
|
+
const request = await signRfc9421Many([
|
|
989
|
+
forged,
|
|
990
|
+
forged,
|
|
991
|
+
valid
|
|
992
|
+
], currentTime);
|
|
993
|
+
for (const keyCache of [void 0, new KvKeyCache(new MemoryKvStore(), ["pk"])]) {
|
|
994
|
+
const documentLoader = createCountingKeyLoader();
|
|
995
|
+
const result = await verifyRequestDetailed(request, {
|
|
996
|
+
documentLoader,
|
|
997
|
+
contextLoader: mockDocumentLoader,
|
|
998
|
+
currentTime,
|
|
999
|
+
keyCache
|
|
1000
|
+
});
|
|
1001
|
+
assert(result.verified);
|
|
1002
|
+
assertEquals(result.signatureLabel, "sig3");
|
|
1003
|
+
assertEquals(documentLoader.loaded, [valid]);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
await t.step("signatures naming the same missing key", async () => {
|
|
1007
|
+
const documentLoader = createCountingKeyLoader();
|
|
1008
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
1009
|
+
missing(1),
|
|
1010
|
+
missing(1),
|
|
1011
|
+
missing(1)
|
|
1012
|
+
], currentTime), {
|
|
1013
|
+
documentLoader,
|
|
1014
|
+
contextLoader: mockDocumentLoader,
|
|
1015
|
+
currentTime
|
|
1016
|
+
});
|
|
1017
|
+
assertFalse(result.verified);
|
|
1018
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
1019
|
+
assertEquals(documentLoader.loaded, [missing(1)]);
|
|
1020
|
+
});
|
|
1021
|
+
await t.step("separate requests", async () => {
|
|
1022
|
+
const request = await signRfc9421Many([valid], currentTime);
|
|
1023
|
+
const documentLoader = createCountingKeyLoader();
|
|
1024
|
+
const options = {
|
|
1025
|
+
documentLoader,
|
|
1026
|
+
contextLoader: mockDocumentLoader,
|
|
1027
|
+
currentTime
|
|
1028
|
+
};
|
|
1029
|
+
assertEquals(await verifyRequest(request, options), rsaPublicKey2);
|
|
1030
|
+
assertEquals(await verifyRequest(request, options), rsaPublicKey2);
|
|
1031
|
+
assertEquals(documentLoader.loaded, [valid, valid]);
|
|
1032
|
+
});
|
|
1033
|
+
const createStaleKeyCache = () => {
|
|
1034
|
+
const gets = [];
|
|
1035
|
+
return {
|
|
1036
|
+
keyCache: {
|
|
1037
|
+
get(keyId) {
|
|
1038
|
+
gets.push(keyId.href);
|
|
1039
|
+
return Promise.resolve(keyId.href === valid ? rsaPublicKey1 : keyId.href === missing(1) ? null : void 0);
|
|
1040
|
+
},
|
|
1041
|
+
set: () => Promise.resolve(),
|
|
1042
|
+
getFetchError: (keyId) => Promise.resolve(keyId.href === missing(1) ? { error: new FetchError(keyId, "Cached failure") } : void 0),
|
|
1043
|
+
setFetchError: () => Promise.resolve()
|
|
1044
|
+
},
|
|
1045
|
+
gets
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
await t.step("refreshes a stale cached key once, only for its signature", async () => {
|
|
1049
|
+
const { keyCache, gets } = createStaleKeyCache();
|
|
1050
|
+
const documentLoader = createCountingKeyLoader();
|
|
1051
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
1052
|
+
missing(1),
|
|
1053
|
+
missing(2),
|
|
1054
|
+
forged,
|
|
1055
|
+
valid
|
|
1056
|
+
], currentTime), {
|
|
1057
|
+
documentLoader,
|
|
1058
|
+
contextLoader: mockDocumentLoader,
|
|
1059
|
+
currentTime,
|
|
1060
|
+
keyCache,
|
|
1061
|
+
maxSignatures: 4
|
|
1062
|
+
});
|
|
1063
|
+
assert(result.verified);
|
|
1064
|
+
assertEquals(result.key, rsaPublicKey2);
|
|
1065
|
+
assertEquals(result.signatureLabel, "sig4");
|
|
1066
|
+
assertEquals(documentLoader.loaded, [missing(2), valid]);
|
|
1067
|
+
assertEquals(gets, [
|
|
1068
|
+
missing(1),
|
|
1069
|
+
missing(2),
|
|
1070
|
+
valid
|
|
1071
|
+
]);
|
|
1072
|
+
});
|
|
1073
|
+
await t.step("refreshes each stale cached key at most once", async () => {
|
|
1074
|
+
const stale = (n) => `https://example.com/stale-${n}`;
|
|
1075
|
+
const gets = [];
|
|
1076
|
+
const keyCache = {
|
|
1077
|
+
get(keyId) {
|
|
1078
|
+
gets.push(keyId.href);
|
|
1079
|
+
return Promise.resolve(rsaPublicKey1);
|
|
1080
|
+
},
|
|
1081
|
+
set: () => Promise.resolve()
|
|
1082
|
+
};
|
|
1083
|
+
const documentLoader = createCountingKeyLoader();
|
|
1084
|
+
const result = await verifyRequestDetailed(await signRfc9421Many([
|
|
1085
|
+
stale(1),
|
|
1086
|
+
stale(2),
|
|
1087
|
+
stale(1),
|
|
1088
|
+
stale(4)
|
|
1089
|
+
], currentTime), {
|
|
1090
|
+
documentLoader,
|
|
1091
|
+
contextLoader: mockDocumentLoader,
|
|
1092
|
+
currentTime,
|
|
1093
|
+
keyCache
|
|
1094
|
+
});
|
|
1095
|
+
assertFalse(result.verified);
|
|
1096
|
+
assertEquals(result.reason.type, "keyFetchError");
|
|
1097
|
+
assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, stale(1));
|
|
1098
|
+
assertEquals(documentLoader.loaded, [stale(1), stale(2)]);
|
|
1099
|
+
assertEquals(gets, [stale(1), stale(2)]);
|
|
1100
|
+
});
|
|
1101
|
+
await t.step("does not refresh a cached key for other failures", async () => {
|
|
1102
|
+
const [input] = Object.values(splitRfc9421Header((await signRfc9421Many([valid], currentTime)).headers.get("Signature-Input")));
|
|
1103
|
+
const withAlg = (alg) => /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, `;alg="${alg}"`) : `${input};alg="${alg}"`;
|
|
1104
|
+
for (const signatureInput of [withAlg("unknown"), input.replace(/^sig1=\([^)]*\)/, "sig1=(\"@unsupported\")")]) {
|
|
1105
|
+
const request = await signRfc9421Many([valid], currentTime);
|
|
1106
|
+
request.headers.set("Signature-Input", signatureInput);
|
|
1107
|
+
const { keyCache } = createStaleKeyCache();
|
|
1108
|
+
const documentLoader = createCountingKeyLoader();
|
|
1109
|
+
assertEquals(await verifyRequest(request, {
|
|
1110
|
+
documentLoader,
|
|
1111
|
+
contextLoader: mockDocumentLoader,
|
|
1112
|
+
currentTime,
|
|
1113
|
+
keyCache
|
|
1114
|
+
}), null, signatureInput);
|
|
1115
|
+
assertEquals(documentLoader.loaded, [], signatureInput);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
await t.step("refreshes a cached key of another algorithm family", async () => {
|
|
1119
|
+
const keyId = ed25519PublicKey.id.href;
|
|
1120
|
+
const request = await signRfc9421Many([keyId], currentTime);
|
|
1121
|
+
const input = request.headers.get("Signature-Input");
|
|
1122
|
+
request.headers.set("Signature-Input", /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, ";alg=\"ed25519\"") : `${input};alg="ed25519"`);
|
|
1123
|
+
const { sig1 } = parseRfc9421SignatureInput(request.headers.get("Signature-Input"));
|
|
1124
|
+
const signature = await crypto.subtle.sign("Ed25519", ed25519PrivateKey, new TextEncoder().encode(createRfc9421SignatureBase(request, sig1.components, sig1.parameters)));
|
|
1125
|
+
request.headers.set("Signature", `sig1=:${encodeBase64(new Uint8Array(signature))}:`);
|
|
1126
|
+
const loaded = [];
|
|
1127
|
+
const result = await verifyRequestDetailed(request, {
|
|
1128
|
+
documentLoader: (url, options) => {
|
|
1129
|
+
loaded.push(url);
|
|
1130
|
+
return mockDocumentLoader(url, options);
|
|
1131
|
+
},
|
|
1132
|
+
contextLoader: mockDocumentLoader,
|
|
1133
|
+
currentTime,
|
|
1134
|
+
keyCache: {
|
|
1135
|
+
get: (id) => Promise.resolve(id.href === keyId ? rsaPublicKey1 : void 0),
|
|
1136
|
+
set: () => Promise.resolve()
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
assert(result.verified);
|
|
1140
|
+
assertEquals(result.key.id?.href, keyId);
|
|
1141
|
+
assertEquals(loaded.length, 1);
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
631
1144
|
test("verifyRequest() [rfc9421] manual POST verification", async () => {
|
|
632
1145
|
const currentTimestamp = 1709626184;
|
|
633
1146
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
@@ -737,7 +1250,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
737
1250
|
contextLoader: mockDocumentLoader,
|
|
738
1251
|
documentLoader: mockDocumentLoader,
|
|
739
1252
|
spec: "rfc9421",
|
|
740
|
-
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(
|
|
1253
|
+
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1712218184 * 1e3)).toISOString()}`),
|
|
741
1254
|
timeWindow: { hours: 1 }
|
|
742
1255
|
}), null, "Should fail verification when signature timestamp is too old");
|
|
743
1256
|
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
@@ -769,7 +1282,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
769
1282
|
contextLoader: mockDocumentLoader,
|
|
770
1283
|
documentLoader: mockDocumentLoader,
|
|
771
1284
|
spec: "rfc9421",
|
|
772
|
-
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(
|
|
1285
|
+
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1741162184 * 1e3)).toISOString()}`),
|
|
773
1286
|
timeWindow: false
|
|
774
1287
|
}), rsaPublicKey2, "Should verify signature when time checking is disabled");
|
|
775
1288
|
const freshSignedPostRequest = await signRequest(new Request("https://example.com/api/resource", {
|
|
@@ -1001,6 +1514,7 @@ test("doubleKnock() function with redirect handling", async () => {
|
|
|
1001
1514
|
esm_default.spyGlobal();
|
|
1002
1515
|
const requestedUrls = [];
|
|
1003
1516
|
const responseCodes = [];
|
|
1517
|
+
const validatedRedirects = [];
|
|
1004
1518
|
esm_default.post("https://example.com/redirect-endpoint", (cl) => {
|
|
1005
1519
|
requestedUrls.push(cl.url);
|
|
1006
1520
|
responseCodes.push(302);
|
|
@@ -1018,11 +1532,41 @@ test("doubleKnock() function with redirect handling", async () => {
|
|
|
1018
1532
|
}), {
|
|
1019
1533
|
keyId: rsaPublicKey2.id,
|
|
1020
1534
|
privateKey: rsaPrivateKey2
|
|
1021
|
-
}
|
|
1535
|
+
}, { validateRedirect(url) {
|
|
1536
|
+
validatedRedirects.push(url);
|
|
1537
|
+
} })).status, 202, "Final response status should be 202 Accepted");
|
|
1022
1538
|
assertEquals(requestedUrls.length, 2, "Two URLs should have been requested");
|
|
1023
1539
|
assertEquals(requestedUrls[0], "https://example.com/redirect-endpoint", "First request should be to redirect-endpoint");
|
|
1024
1540
|
assertEquals(requestedUrls[1], "https://example.com/final-endpoint", "Second request should be to final-endpoint");
|
|
1025
1541
|
assertEquals(responseCodes, [302, 202], "Response status codes should match expected sequence");
|
|
1542
|
+
assertEquals(validatedRedirects, ["https://example.com/final-endpoint"]);
|
|
1543
|
+
esm_default.hardReset();
|
|
1544
|
+
});
|
|
1545
|
+
test("doubleKnock() validates redirects after signature fallback", async () => {
|
|
1546
|
+
esm_default.spyGlobal();
|
|
1547
|
+
let requestCount = 0;
|
|
1548
|
+
let privateRequestCount = 0;
|
|
1549
|
+
esm_default.post("https://example.com/redirect-after-fallback", (cl) => {
|
|
1550
|
+
requestCount++;
|
|
1551
|
+
if (cl.request.headers.has("Signature-Input")) return new Response("Unauthorized", { status: 401 });
|
|
1552
|
+
return Response.redirect("http://localhost/private", 302);
|
|
1553
|
+
});
|
|
1554
|
+
esm_default.post("http://localhost/private", () => {
|
|
1555
|
+
privateRequestCount++;
|
|
1556
|
+
return new Response("", { status: 202 });
|
|
1557
|
+
});
|
|
1558
|
+
const request = new Request("https://example.com/redirect-after-fallback", {
|
|
1559
|
+
method: "POST",
|
|
1560
|
+
body: "Test message"
|
|
1561
|
+
});
|
|
1562
|
+
await assertRejects(() => doubleKnock(request, {
|
|
1563
|
+
keyId: rsaPublicKey2.id,
|
|
1564
|
+
privateKey: rsaPrivateKey2
|
|
1565
|
+
}, { validateRedirect(url) {
|
|
1566
|
+
throw new Error(`Disallowed redirect: ${url}`);
|
|
1567
|
+
} }), Error, "Disallowed redirect: http://localhost/private");
|
|
1568
|
+
assertEquals(requestCount, 2);
|
|
1569
|
+
assertEquals(privateRequestCount, 0);
|
|
1026
1570
|
esm_default.hardReset();
|
|
1027
1571
|
});
|
|
1028
1572
|
test("doubleKnock() function with both specs rejected", async () => {
|
|
@@ -1813,6 +2357,37 @@ test("doubleKnock(): Accept-Signature challenge retry succeeds", async () => {
|
|
|
1813
2357
|
assertEquals(requestCount, 2);
|
|
1814
2358
|
esm_default.hardReset();
|
|
1815
2359
|
});
|
|
2360
|
+
test("doubleKnock(): validates redirects after Accept-Signature challenge retry", async () => {
|
|
2361
|
+
esm_default.spyGlobal();
|
|
2362
|
+
let requestCount = 0;
|
|
2363
|
+
let privateRequestCount = 0;
|
|
2364
|
+
esm_default.post("https://example.com/inbox-challenge-redirect", () => {
|
|
2365
|
+
requestCount++;
|
|
2366
|
+
if (requestCount === 1) return new Response("Not Authorized", {
|
|
2367
|
+
status: 401,
|
|
2368
|
+
headers: { "Accept-Signature": "sig1=(\"@method\" \"@target-uri\" \"@authority\" \"content-digest\");created;nonce=\"challenge-nonce-redirect\"" }
|
|
2369
|
+
});
|
|
2370
|
+
return Response.redirect("http://localhost/private", 302);
|
|
2371
|
+
});
|
|
2372
|
+
esm_default.post("http://localhost/private", () => {
|
|
2373
|
+
privateRequestCount++;
|
|
2374
|
+
return new Response("", { status: 202 });
|
|
2375
|
+
});
|
|
2376
|
+
const request = new Request("https://example.com/inbox-challenge-redirect", {
|
|
2377
|
+
method: "POST",
|
|
2378
|
+
body: "Test message",
|
|
2379
|
+
headers: { "Content-Type": "text/plain" }
|
|
2380
|
+
});
|
|
2381
|
+
await assertRejects(() => doubleKnock(request, {
|
|
2382
|
+
keyId: rsaPublicKey2.id,
|
|
2383
|
+
privateKey: rsaPrivateKey2
|
|
2384
|
+
}, { validateRedirect(url) {
|
|
2385
|
+
throw new Error(`Disallowed redirect: ${url}`);
|
|
2386
|
+
} }), Error, "Disallowed redirect: http://localhost/private");
|
|
2387
|
+
assertEquals(requestCount, 2);
|
|
2388
|
+
assertEquals(privateRequestCount, 0);
|
|
2389
|
+
esm_default.hardReset();
|
|
2390
|
+
});
|
|
1816
2391
|
test("doubleKnock(): unfulfillable Accept-Signature falls to legacy fallback", async () => {
|
|
1817
2392
|
esm_default.spyGlobal();
|
|
1818
2393
|
let requestCount = 0;
|
|
@@ -2009,5 +2584,158 @@ test("doubleKnock(): Accept-Signature with multiple compatible entries fulfills
|
|
|
2009
2584
|
assertEquals(requestCount, 2);
|
|
2010
2585
|
esm_default.hardReset();
|
|
2011
2586
|
});
|
|
2587
|
+
test("listRequestSignatures()", async (t) => {
|
|
2588
|
+
const request = () => new Request("https://example.com/inbox", {
|
|
2589
|
+
method: "POST",
|
|
2590
|
+
body: "Test message"
|
|
2591
|
+
});
|
|
2592
|
+
await t.step("draft-cavage", async () => {
|
|
2593
|
+
const signed = await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id);
|
|
2594
|
+
const signatures = listRequestSignatures(signed);
|
|
2595
|
+
assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, null]]);
|
|
2596
|
+
const components = signatures[0].components;
|
|
2597
|
+
assertEquals(components.map((c) => c.value), signed.headers.get("Signature").match(/headers="([^"]*)"/)[1].split(" "));
|
|
2598
|
+
assert(components.every((c) => Object.keys(c.params).length < 1));
|
|
2599
|
+
});
|
|
2600
|
+
await t.step("RFC 9421", async () => {
|
|
2601
|
+
const signatures = listRequestSignatures(await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id, {
|
|
2602
|
+
spec: "rfc9421",
|
|
2603
|
+
rfc9421: { label: "a" }
|
|
2604
|
+
}));
|
|
2605
|
+
assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, "a"]]);
|
|
2606
|
+
assertEquals(signatures[0].components.map((c) => c.value), [
|
|
2607
|
+
"@method",
|
|
2608
|
+
"@target-uri",
|
|
2609
|
+
"@authority",
|
|
2610
|
+
"host",
|
|
2611
|
+
"date",
|
|
2612
|
+
"content-digest"
|
|
2613
|
+
]);
|
|
2614
|
+
});
|
|
2615
|
+
await t.step("RFC 9421 beyond the limit", async () => {
|
|
2616
|
+
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
2617
|
+
const signed = await signRfc9421Many([
|
|
2618
|
+
1,
|
|
2619
|
+
2,
|
|
2620
|
+
3,
|
|
2621
|
+
4
|
|
2622
|
+
].map((n) => `https://example.com/key-${n}`), currentTime);
|
|
2623
|
+
const headers = new Headers(signed.headers);
|
|
2624
|
+
headers.set("Signature-Input", headers.get("Signature-Input").replace("https://example.com/key-1", "not a URL"));
|
|
2625
|
+
const request = new Request(signed.url, {
|
|
2626
|
+
method: "POST",
|
|
2627
|
+
headers
|
|
2628
|
+
});
|
|
2629
|
+
const keyIds = (maxSignatures) => listRequestSignatures(request, maxSignatures).map((s) => s.keyId.href);
|
|
2630
|
+
assertEquals(keyIds(), ["https://example.com/key-2", "https://example.com/key-3"]);
|
|
2631
|
+
assertEquals(keyIds(1), []);
|
|
2632
|
+
assertEquals(keyIds(Infinity), [
|
|
2633
|
+
"https://example.com/key-2",
|
|
2634
|
+
"https://example.com/key-3",
|
|
2635
|
+
"https://example.com/key-4"
|
|
2636
|
+
]);
|
|
2637
|
+
});
|
|
2638
|
+
await t.step("unsigned or malformed", () => {
|
|
2639
|
+
assertEquals(listRequestSignatures(request()), []);
|
|
2640
|
+
const malformed = request();
|
|
2641
|
+
malformed.headers.set("Signature", "keyId=\"not a URL\",signature=\"AA==\"");
|
|
2642
|
+
assertEquals(listRequestSignatures(malformed), []);
|
|
2643
|
+
const garbage = request();
|
|
2644
|
+
garbage.headers.set("Signature-Input", "((");
|
|
2645
|
+
garbage.headers.set("Signature", "((");
|
|
2646
|
+
assertEquals(listRequestSignatures(garbage), []);
|
|
2647
|
+
});
|
|
2648
|
+
});
|
|
2649
|
+
test("selectRequestSignature()", async () => {
|
|
2650
|
+
const body = "Test message";
|
|
2651
|
+
const sign = (label, keyId) => signRequest(new Request("https://example.com/inbox", {
|
|
2652
|
+
method: "POST",
|
|
2653
|
+
body
|
|
2654
|
+
}), rsaPrivateKey2, keyId, {
|
|
2655
|
+
spec: "rfc9421",
|
|
2656
|
+
rfc9421: { label }
|
|
2657
|
+
});
|
|
2658
|
+
const first = await sign("a", new URL("https://example.com/other#key"));
|
|
2659
|
+
const second = await sign("b", rsaPublicKey2.id);
|
|
2660
|
+
const headers = new Headers(second.headers);
|
|
2661
|
+
for (const name of ["Signature-Input", "Signature"]) headers.set(name, `${first.headers.get(name)}, ${second.headers.get(name)}`);
|
|
2662
|
+
const request = new Request(second.url, {
|
|
2663
|
+
method: "POST",
|
|
2664
|
+
headers,
|
|
2665
|
+
body
|
|
2666
|
+
});
|
|
2667
|
+
const signatures = listRequestSignatures(request);
|
|
2668
|
+
assertEquals(signatures.map((s) => s.label), ["a", "b"]);
|
|
2669
|
+
const selected = selectRequestSignature(request, signatures[1]);
|
|
2670
|
+
assertEquals(listRequestSignatures(selected).map((s) => [s.label, s.keyId.href]), [["b", rsaPublicKey2.id.href]]);
|
|
2671
|
+
const result = await verifyRequestDetailed(selected, {
|
|
2672
|
+
documentLoader: mockDocumentLoader,
|
|
2673
|
+
contextLoader: mockDocumentLoader
|
|
2674
|
+
});
|
|
2675
|
+
assert(result.verified);
|
|
2676
|
+
assertEquals(result.key.id, rsaPublicKey2.id);
|
|
2677
|
+
assertEquals(await selected.text(), body);
|
|
2678
|
+
assertEquals(await request.text(), body);
|
|
2679
|
+
});
|
|
2680
|
+
test("verifyRequestDetailed() reports a timed-out key fetch", async (t) => {
|
|
2681
|
+
for (const stall of ["headers", "body"]) await t.step(`stalled ${stall}`, async () => {
|
|
2682
|
+
esm_default.mockGlobal();
|
|
2683
|
+
const keyId = new URL(`https://slow.example/actors/alice#${stall}`);
|
|
2684
|
+
let requests = 0;
|
|
2685
|
+
esm_default.get(`begin:https://slow.example/actors/alice`, () => {
|
|
2686
|
+
requests++;
|
|
2687
|
+
if (stall === "headers") return new Promise(() => {});
|
|
2688
|
+
return new Response(new ReadableStream({ pull: () => new Promise(() => {}) }), { status: 404 });
|
|
2689
|
+
});
|
|
2690
|
+
const loader = getDocumentLoader({
|
|
2691
|
+
allowPrivateAddress: true,
|
|
2692
|
+
timeout: 100
|
|
2693
|
+
});
|
|
2694
|
+
const documentLoader = (url, options) => url.startsWith("https://slow.example/") ? loader(url, options) : mockDocumentLoader(url);
|
|
2695
|
+
const kv = new MemoryKvStore();
|
|
2696
|
+
try {
|
|
2697
|
+
const sign = () => signRequest(new Request("https://example.com/inbox", {
|
|
2698
|
+
method: "POST",
|
|
2699
|
+
headers: {
|
|
2700
|
+
"Content-Type": "application/activity+json",
|
|
2701
|
+
accept: "application/ld+json"
|
|
2702
|
+
},
|
|
2703
|
+
body: JSON.stringify({
|
|
2704
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2705
|
+
type: "Create",
|
|
2706
|
+
actor: "https://slow.example/actors/alice"
|
|
2707
|
+
})
|
|
2708
|
+
}), rsaPrivateKey2, keyId);
|
|
2709
|
+
const result = await verifyRequestDetailed(await sign(), {
|
|
2710
|
+
contextLoader: mockDocumentLoader,
|
|
2711
|
+
documentLoader,
|
|
2712
|
+
keyCache: new KvKeyCache(kv, ["pk"])
|
|
2713
|
+
});
|
|
2714
|
+
assertFalse(result.verified);
|
|
2715
|
+
assert(result.reason.type === "keyFetchError");
|
|
2716
|
+
assertEquals(result.reason.keyId, keyId);
|
|
2717
|
+
assert("error" in result.reason.result);
|
|
2718
|
+
const error = result.reason.result.error;
|
|
2719
|
+
assert(error instanceof FetchError);
|
|
2720
|
+
assertEquals(error.response, void 0);
|
|
2721
|
+
assert(error.cause instanceof DOMException);
|
|
2722
|
+
assertEquals(error.cause.name, "TimeoutError");
|
|
2723
|
+
assertEquals(requests, 1);
|
|
2724
|
+
const cached = await verifyRequestDetailed(await sign(), {
|
|
2725
|
+
contextLoader: mockDocumentLoader,
|
|
2726
|
+
documentLoader,
|
|
2727
|
+
keyCache: new KvKeyCache(kv, ["pk"])
|
|
2728
|
+
});
|
|
2729
|
+
assertFalse(cached.verified);
|
|
2730
|
+
assert(cached.reason.type === "keyFetchError");
|
|
2731
|
+
assert("error" in cached.reason.result);
|
|
2732
|
+
assertEquals(cached.reason.result.error.name, "FetchError");
|
|
2733
|
+
assertStringIncludes(cached.reason.result.error.message, "Timed out after 100 ms");
|
|
2734
|
+
assertEquals(requests, 1);
|
|
2735
|
+
} finally {
|
|
2736
|
+
esm_default.hardReset();
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
});
|
|
2012
2740
|
//#endregion
|
|
2013
2741
|
export {};
|