@fedify/fedify 2.1.0-dev.543 → 2.1.0-dev.592
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/dist/accept-D7sAxyNa.js +143 -0
- package/dist/{assert_rejects-Ce45JcFg.js → assert_rejects-0h7I2Esa.js} +1 -1
- package/dist/{builder-BBucr-Bp.js → builder-B24i8eYp.js} +4 -4
- package/dist/{client-Dg7OfUDA.js → client-CoCIaTNO.js} +1 -1
- package/dist/compat/mod.d.cts +3 -3
- package/dist/compat/mod.d.ts +3 -3
- package/dist/compat/transformers.test.js +19 -18
- package/dist/{context-CZ5llAss.js → context-Aqenou7c.js} +1 -1
- package/dist/{context-DL0cPpPV.d.cts → context-BcqA-0BL.d.cts} +52 -2
- package/dist/{context--RwChtri.d.ts → context-DyJjQQ_H.d.ts} +52 -2
- package/dist/{deno-9yc0TPBI.js → deno-OR506Yti.js} +1 -2
- package/dist/{docloader-6Wrqp6SE.js → docloader-BG_pP2fW.js} +3 -3
- package/dist/{esm-DGl7uK1r.js → esm-nLm00z9V.js} +27 -1
- package/dist/federation/builder.test.js +8 -8
- package/dist/federation/collection.test.js +6 -6
- package/dist/federation/handler.test.js +808 -28
- package/dist/federation/idempotency.test.js +24 -23
- package/dist/federation/inbox.test.js +4 -4
- package/dist/federation/keycache.test.js +2 -2
- package/dist/federation/kv.test.js +5 -5
- package/dist/federation/middleware.test.js +25 -24
- package/dist/federation/mod.cjs +4 -4
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +4 -4
- package/dist/federation/mq.test.js +5 -5
- package/dist/federation/negotiation.test.js +6 -6
- package/dist/federation/retry.test.js +3 -3
- package/dist/federation/router.test.js +5 -5
- package/dist/federation/send.test.js +13 -12
- package/dist/federation/webfinger.test.js +24 -23
- package/dist/{http-CpvoK0Y7.js → http-BUCxbGks.js} +145 -50
- package/dist/{http-DsqqmkXi.d.cts → http-BudnHZE2.d.cts} +229 -1
- package/dist/{http-C_9L2wFv.cjs → http-CaXARmaJ.cjs} +307 -50
- package/dist/{http-BbfOqHGG.d.ts → http-Dax_FIBo.d.ts} +229 -1
- package/dist/{http-DxwzIU0F.js → http-DePHjWKP.js} +278 -51
- package/dist/{inbox-DMq3a5bc.js → inbox-D_LU1opv.js} +2 -2
- package/dist/{key-DFG6tJgw.js → key-Cx3Tx_In.js} +2 -2
- package/dist/{kv-cache-B__dHl7g.js → kv-cache-Bw2F2ABq.js} +1 -1
- package/dist/{kv-cache-BHoLc85Z.cjs → kv-cache-CYTDBChd.cjs} +1 -1
- package/dist/{kv-cache-CRRUsyJ9.js → kv-cache-DizRqYX4.js} +1 -1
- package/dist/{ld-DVnRS9IK.js → ld-CLMJw_iX.js} +4 -4
- package/dist/{middleware-D6peKsn1.js → middleware--uATyG9i.js} +95 -18
- package/dist/{middleware-CAk-LkSS.js → middleware-4fo4pEtA.js} +4 -4
- package/dist/{middleware-pUJBhWSu.cjs → middleware-9YDezkYJ.cjs} +94 -17
- package/dist/middleware-C2PqSUaA.js +27 -0
- package/dist/middleware-DNY45l5T.cjs +12 -0
- package/dist/{middleware-FZ0T8vIp.js → middleware-DzICTgdC.js} +115 -36
- package/dist/{mod-DE8MYisy.d.cts → mod-B7QkWzrL.d.cts} +1 -1
- package/dist/{mod-DKG0ovjR.d.cts → mod-Bx9jcLB8.d.cts} +1 -1
- package/dist/{mod-CFBU2OT3.d.cts → mod-Coe7KEgX.d.cts} +1 -1
- package/dist/{mod-BugwI0JN.d.ts → mod-Cs2dYEwI.d.ts} +1 -1
- package/dist/{mod-DcfFNgYf.d.ts → mod-D6MdymW7.d.ts} +1 -1
- package/dist/{mod-CvxylbuV.d.ts → mod-D6dOd--H.d.ts} +1 -1
- package/dist/{mod-Z7lIaCfo.d.ts → mod-SMHOMNpZ.d.ts} +1 -1
- package/dist/{mod-Dp0kK0hO.d.cts → mod-em2Il1eD.d.cts} +1 -1
- package/dist/mod.cjs +12 -4
- package/dist/mod.d.cts +8 -8
- package/dist/mod.d.ts +8 -8
- package/dist/mod.js +9 -5
- package/dist/nodeinfo/client.test.js +7 -7
- package/dist/nodeinfo/handler.test.js +24 -23
- package/dist/nodeinfo/types.test.js +5 -5
- package/dist/otel/exporter.test.js +6 -6
- package/dist/{owner-MCqkZ1KE.js → owner-D5J299vd.js} +1 -1
- package/dist/{proof-D2B3jvnF.js → proof-BBLHhWMC.js} +3 -3
- package/dist/{proof-BF_LZjDb.cjs → proof-BVl5IgbN.cjs} +3 -3
- package/dist/{proof-ooYMfVCa.js → proof-CiCp_mCG.js} +2 -2
- package/dist/{send-DtP5YkuY.js → send-2b0Fn9cn.js} +2 -2
- package/dist/sig/accept.test.d.ts +3 -0
- package/dist/sig/accept.test.js +451 -0
- package/dist/sig/http.test.js +454 -29
- package/dist/sig/key.test.js +8 -8
- package/dist/sig/ld.test.js +7 -7
- package/dist/sig/mod.cjs +6 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.js +9 -9
- package/dist/sig/proof.test.js +9 -9
- package/dist/testing/mod.d.ts +1 -1
- package/dist/testing/mod.js +2 -2
- package/dist/utils/docloader.test.js +12 -11
- package/dist/utils/kv-cache.test.js +2 -2
- package/dist/utils/mod.cjs +2 -2
- package/dist/utils/mod.d.cts +2 -2
- package/dist/utils/mod.d.ts +2 -2
- package/dist/utils/mod.js +2 -2
- package/package.json +6 -7
- package/dist/dist-B5f6a8Tt.js +0 -281
- package/dist/middleware-D7yrgd0I.cjs +0 -12
- package/dist/middleware-GmHZnwkU.js +0 -26
- /package/dist/{assert_not_equals-C80BG-_5.js → assert_not_equals-f3m3epl3.js} +0 -0
- /package/dist/{assert_throws-BNXdRGWP.js → assert_throws-rjdMBf31.js} +0 -0
- /package/dist/{collection-CcnIw1qY.js → collection-CSzG2j1P.js} +0 -0
- /package/dist/{keycache-C7k8s1Bk.js → keycache-CpGWAUbj.js} +0 -0
- /package/dist/{keys-ZbcByPg9.js → keys-BFve7QQv.js} +0 -0
- /package/dist/{negotiation-5NPJL6zp.js → negotiation-BlAuS_nr.js} +0 -0
- /package/dist/{retry-D4GJ670a.js → retry-mqLf4b-R.js} +0 -0
- /package/dist/{std__assert-DWivtrGR.js → std__assert-X-_kMxKM.js} +0 -0
|
@@ -3,36 +3,37 @@
|
|
|
3
3
|
import { URLPattern } from "urlpattern-polyfill";
|
|
4
4
|
globalThis.addEventListener = () => {};
|
|
5
5
|
|
|
6
|
-
import { createTestTracerProvider, mockDocumentLoader, test } from "../dist-B5f6a8Tt.js";
|
|
7
6
|
import { assertEquals } from "../assert_equals-DSbWqCm3.js";
|
|
8
7
|
import { assert } from "../assert-MZs1qjMx.js";
|
|
9
8
|
import "../assert_instance_of-DHz7EHNU.js";
|
|
10
9
|
import { MemoryKvStore } from "../kv-QzKcOQgP.js";
|
|
11
|
-
import "../deno-
|
|
12
|
-
import { createFederation, handleActor, handleCollection, handleCustomCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable } from "../middleware-
|
|
13
|
-
import "../client-
|
|
10
|
+
import "../deno-OR506Yti.js";
|
|
11
|
+
import { createFederation, handleActor, handleCollection, handleCustomCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable } from "../middleware-DzICTgdC.js";
|
|
12
|
+
import "../client-CoCIaTNO.js";
|
|
14
13
|
import "../router-D9eI0s4b.js";
|
|
15
14
|
import "../types-CPz01LGH.js";
|
|
16
|
-
import "../
|
|
17
|
-
import
|
|
18
|
-
import "../
|
|
19
|
-
import "../
|
|
20
|
-
import "../
|
|
21
|
-
import "../
|
|
22
|
-
import "../
|
|
23
|
-
import
|
|
24
|
-
import "../
|
|
25
|
-
import "../
|
|
26
|
-
import "../
|
|
27
|
-
import "../
|
|
28
|
-
import "../
|
|
29
|
-
import "../
|
|
30
|
-
import "../
|
|
31
|
-
import "../
|
|
32
|
-
import "../
|
|
33
|
-
import "../
|
|
34
|
-
import
|
|
35
|
-
import {
|
|
15
|
+
import { parseAcceptSignature } from "../accept-D7sAxyNa.js";
|
|
16
|
+
import "../key-Cx3Tx_In.js";
|
|
17
|
+
import { signRequest } from "../http-BUCxbGks.js";
|
|
18
|
+
import "../ld-CLMJw_iX.js";
|
|
19
|
+
import "../owner-D5J299vd.js";
|
|
20
|
+
import "../proof-BBLHhWMC.js";
|
|
21
|
+
import "../docloader-BG_pP2fW.js";
|
|
22
|
+
import "../kv-cache-Bw2F2ABq.js";
|
|
23
|
+
import { InboxListenerSet } from "../inbox-D_LU1opv.js";
|
|
24
|
+
import "../builder-B24i8eYp.js";
|
|
25
|
+
import "../collection-CSzG2j1P.js";
|
|
26
|
+
import "../keycache-CpGWAUbj.js";
|
|
27
|
+
import "../negotiation-BlAuS_nr.js";
|
|
28
|
+
import "../retry-mqLf4b-R.js";
|
|
29
|
+
import "../send-2b0Fn9cn.js";
|
|
30
|
+
import "../std__assert-X-_kMxKM.js";
|
|
31
|
+
import "../assert_rejects-0h7I2Esa.js";
|
|
32
|
+
import "../assert_throws-rjdMBf31.js";
|
|
33
|
+
import "../assert_not_equals-f3m3epl3.js";
|
|
34
|
+
import { createInboxContext, createRequestContext } from "../context-Aqenou7c.js";
|
|
35
|
+
import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3 } from "../keys-BFve7QQv.js";
|
|
36
|
+
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
36
37
|
import { Create, Note, Person } from "@fedify/vocab";
|
|
37
38
|
import { FetchError } from "@fedify/vocab-runtime";
|
|
38
39
|
|
|
@@ -914,7 +915,8 @@ test("handleInbox()", async () => {
|
|
|
914
915
|
kv: new MemoryKvStore(),
|
|
915
916
|
kvPrefixes: {
|
|
916
917
|
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
917
|
-
publicKey: ["_fedify", "publicKey"]
|
|
918
|
+
publicKey: ["_fedify", "publicKey"],
|
|
919
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
918
920
|
},
|
|
919
921
|
actorDispatcher,
|
|
920
922
|
onNotFound,
|
|
@@ -1160,7 +1162,8 @@ test("handleInbox() - authentication bypass vulnerability", async () => {
|
|
|
1160
1162
|
kv: new MemoryKvStore(),
|
|
1161
1163
|
kvPrefixes: {
|
|
1162
1164
|
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1163
|
-
publicKey: ["_fedify", "publicKey"]
|
|
1165
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1166
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1164
1167
|
},
|
|
1165
1168
|
actorDispatcher,
|
|
1166
1169
|
inboxListeners,
|
|
@@ -1556,7 +1559,8 @@ test("handleInbox() records OpenTelemetry span events", async () => {
|
|
|
1556
1559
|
kv,
|
|
1557
1560
|
kvPrefixes: {
|
|
1558
1561
|
activityIdempotence: ["activityIdempotence"],
|
|
1559
|
-
publicKey: ["publicKey"]
|
|
1562
|
+
publicKey: ["publicKey"],
|
|
1563
|
+
acceptSignatureNonce: ["acceptSignatureNonce"]
|
|
1560
1564
|
},
|
|
1561
1565
|
actorDispatcher,
|
|
1562
1566
|
inboxListeners: listeners,
|
|
@@ -1642,7 +1646,8 @@ test("handleInbox() records unverified HTTP signature details", async () => {
|
|
|
1642
1646
|
kv,
|
|
1643
1647
|
kvPrefixes: {
|
|
1644
1648
|
activityIdempotence: ["activityIdempotence"],
|
|
1645
|
-
publicKey: ["publicKey"]
|
|
1649
|
+
publicKey: ["publicKey"],
|
|
1650
|
+
acceptSignatureNonce: ["acceptSignatureNonce"]
|
|
1646
1651
|
},
|
|
1647
1652
|
actorDispatcher,
|
|
1648
1653
|
inboxListeners: new InboxListenerSet(),
|
|
@@ -1667,5 +1672,780 @@ test("handleInbox() records unverified HTTP signature details", async () => {
|
|
|
1667
1672
|
assertEquals(event.attributes["http_signatures.failure_reason"], "keyFetchError");
|
|
1668
1673
|
assertEquals(event.attributes["http_signatures.key_fetch_status"], 410);
|
|
1669
1674
|
});
|
|
1675
|
+
test("handleInbox() challenge policy enabled + unsigned request", async () => {
|
|
1676
|
+
const activity = new Create({
|
|
1677
|
+
id: new URL("https://example.com/activities/challenge-1"),
|
|
1678
|
+
actor: new URL("https://example.com/person2"),
|
|
1679
|
+
object: new Note({
|
|
1680
|
+
id: new URL("https://example.com/notes/challenge-1"),
|
|
1681
|
+
attribution: new URL("https://example.com/person2"),
|
|
1682
|
+
content: "Hello!"
|
|
1683
|
+
})
|
|
1684
|
+
});
|
|
1685
|
+
const unsignedRequest = new Request("https://example.com/", {
|
|
1686
|
+
method: "POST",
|
|
1687
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
1688
|
+
});
|
|
1689
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
1690
|
+
const context = createRequestContext({
|
|
1691
|
+
federation,
|
|
1692
|
+
request: unsignedRequest,
|
|
1693
|
+
url: new URL(unsignedRequest.url),
|
|
1694
|
+
data: void 0
|
|
1695
|
+
});
|
|
1696
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
1697
|
+
if (identifier !== "someone") return null;
|
|
1698
|
+
return new Person({ name: "Someone" });
|
|
1699
|
+
};
|
|
1700
|
+
const kv = new MemoryKvStore();
|
|
1701
|
+
const response = await handleInbox(unsignedRequest, {
|
|
1702
|
+
recipient: "someone",
|
|
1703
|
+
context,
|
|
1704
|
+
inboxContextFactory(_activity) {
|
|
1705
|
+
return createInboxContext({
|
|
1706
|
+
...context,
|
|
1707
|
+
clone: void 0,
|
|
1708
|
+
recipient: "someone"
|
|
1709
|
+
});
|
|
1710
|
+
},
|
|
1711
|
+
kv,
|
|
1712
|
+
kvPrefixes: {
|
|
1713
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1714
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1715
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1716
|
+
},
|
|
1717
|
+
actorDispatcher,
|
|
1718
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1719
|
+
signatureTimeWindow: { minutes: 5 },
|
|
1720
|
+
skipSignatureVerification: false,
|
|
1721
|
+
inboxChallengePolicy: { enabled: true }
|
|
1722
|
+
});
|
|
1723
|
+
assertEquals(response.status, 401);
|
|
1724
|
+
const acceptSig = response.headers.get("Accept-Signature");
|
|
1725
|
+
assert(acceptSig != null, "Accept-Signature header must be present");
|
|
1726
|
+
const parsed = parseAcceptSignature(acceptSig);
|
|
1727
|
+
assert(parsed.length > 0, "Accept-Signature must have at least one entry");
|
|
1728
|
+
assertEquals(parsed[0].label, "sig1");
|
|
1729
|
+
assert(parsed[0].components.some((c) => c.value === "@method"), "Must include @method component");
|
|
1730
|
+
assertEquals(response.headers.get("Cache-Control"), "no-store");
|
|
1731
|
+
assertEquals(response.headers.get("Vary"), "Accept, Signature");
|
|
1732
|
+
});
|
|
1733
|
+
test("handleInbox() challenge policy enabled + invalid signature", async () => {
|
|
1734
|
+
const activity = new Create({
|
|
1735
|
+
id: new URL("https://example.com/activities/challenge-2"),
|
|
1736
|
+
actor: new URL("https://example.com/person2"),
|
|
1737
|
+
object: new Note({
|
|
1738
|
+
id: new URL("https://example.com/notes/challenge-2"),
|
|
1739
|
+
attribution: new URL("https://example.com/person2"),
|
|
1740
|
+
content: "Hello!"
|
|
1741
|
+
})
|
|
1742
|
+
});
|
|
1743
|
+
const originalRequest = new Request("https://example.com/", {
|
|
1744
|
+
method: "POST",
|
|
1745
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
1746
|
+
});
|
|
1747
|
+
const signedRequest = await signRequest(originalRequest, rsaPrivateKey3, rsaPublicKey3.id);
|
|
1748
|
+
const jsonLd = await activity.toJsonLd();
|
|
1749
|
+
const tamperedBody = JSON.stringify({
|
|
1750
|
+
...jsonLd,
|
|
1751
|
+
"https://example.com/tampered": true
|
|
1752
|
+
});
|
|
1753
|
+
const tamperedRequest = new Request(signedRequest.url, {
|
|
1754
|
+
method: signedRequest.method,
|
|
1755
|
+
headers: signedRequest.headers,
|
|
1756
|
+
body: tamperedBody
|
|
1757
|
+
});
|
|
1758
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
1759
|
+
const context = createRequestContext({
|
|
1760
|
+
federation,
|
|
1761
|
+
request: tamperedRequest,
|
|
1762
|
+
url: new URL(tamperedRequest.url),
|
|
1763
|
+
data: void 0,
|
|
1764
|
+
documentLoader: mockDocumentLoader
|
|
1765
|
+
});
|
|
1766
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
1767
|
+
if (identifier !== "someone") return null;
|
|
1768
|
+
return new Person({ name: "Someone" });
|
|
1769
|
+
};
|
|
1770
|
+
const kv = new MemoryKvStore();
|
|
1771
|
+
const response = await handleInbox(tamperedRequest, {
|
|
1772
|
+
recipient: "someone",
|
|
1773
|
+
context,
|
|
1774
|
+
inboxContextFactory(_activity) {
|
|
1775
|
+
return createInboxContext({
|
|
1776
|
+
...context,
|
|
1777
|
+
clone: void 0,
|
|
1778
|
+
recipient: "someone"
|
|
1779
|
+
});
|
|
1780
|
+
},
|
|
1781
|
+
kv,
|
|
1782
|
+
kvPrefixes: {
|
|
1783
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1784
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1785
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1786
|
+
},
|
|
1787
|
+
actorDispatcher,
|
|
1788
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1789
|
+
signatureTimeWindow: { minutes: 5 },
|
|
1790
|
+
skipSignatureVerification: false,
|
|
1791
|
+
inboxChallengePolicy: { enabled: true }
|
|
1792
|
+
});
|
|
1793
|
+
assertEquals(response.status, 401);
|
|
1794
|
+
const acceptSig = response.headers.get("Accept-Signature");
|
|
1795
|
+
assert(acceptSig != null, "Accept-Signature header must be present");
|
|
1796
|
+
assertEquals(response.headers.get("Cache-Control"), "no-store");
|
|
1797
|
+
});
|
|
1798
|
+
test("handleInbox() challenge policy enabled + valid signature", async () => {
|
|
1799
|
+
const activity = new Create({
|
|
1800
|
+
id: new URL("https://example.com/activities/challenge-3"),
|
|
1801
|
+
actor: new URL("https://example.com/person2"),
|
|
1802
|
+
object: new Note({
|
|
1803
|
+
id: new URL("https://example.com/notes/challenge-3"),
|
|
1804
|
+
attribution: new URL("https://example.com/person2"),
|
|
1805
|
+
content: "Hello!"
|
|
1806
|
+
})
|
|
1807
|
+
});
|
|
1808
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
1809
|
+
const signedRequest = await signRequest(new Request("https://example.com/", {
|
|
1810
|
+
method: "POST",
|
|
1811
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
1812
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1813
|
+
const context = createRequestContext({
|
|
1814
|
+
federation,
|
|
1815
|
+
request: signedRequest,
|
|
1816
|
+
url: new URL(signedRequest.url),
|
|
1817
|
+
data: void 0,
|
|
1818
|
+
documentLoader: mockDocumentLoader
|
|
1819
|
+
});
|
|
1820
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
1821
|
+
if (identifier !== "someone") return null;
|
|
1822
|
+
return new Person({ name: "Someone" });
|
|
1823
|
+
};
|
|
1824
|
+
const kv = new MemoryKvStore();
|
|
1825
|
+
const response = await handleInbox(signedRequest, {
|
|
1826
|
+
recipient: "someone",
|
|
1827
|
+
context,
|
|
1828
|
+
inboxContextFactory(_activity) {
|
|
1829
|
+
return createInboxContext({
|
|
1830
|
+
...context,
|
|
1831
|
+
clone: void 0,
|
|
1832
|
+
recipient: "someone"
|
|
1833
|
+
});
|
|
1834
|
+
},
|
|
1835
|
+
kv,
|
|
1836
|
+
kvPrefixes: {
|
|
1837
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1838
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1839
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1840
|
+
},
|
|
1841
|
+
actorDispatcher,
|
|
1842
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1843
|
+
signatureTimeWindow: { minutes: 5 },
|
|
1844
|
+
skipSignatureVerification: false,
|
|
1845
|
+
inboxChallengePolicy: { enabled: true }
|
|
1846
|
+
});
|
|
1847
|
+
assertEquals(response.status, 202);
|
|
1848
|
+
assertEquals(response.headers.get("Accept-Signature"), null, "No Accept-Signature header on successful request");
|
|
1849
|
+
});
|
|
1850
|
+
test("handleInbox() challenge policy disabled + unsigned request", async () => {
|
|
1851
|
+
const activity = new Create({
|
|
1852
|
+
id: new URL("https://example.com/activities/challenge-4"),
|
|
1853
|
+
actor: new URL("https://example.com/person2"),
|
|
1854
|
+
object: new Note({
|
|
1855
|
+
id: new URL("https://example.com/notes/challenge-4"),
|
|
1856
|
+
attribution: new URL("https://example.com/person2"),
|
|
1857
|
+
content: "Hello!"
|
|
1858
|
+
})
|
|
1859
|
+
});
|
|
1860
|
+
const unsignedRequest = new Request("https://example.com/", {
|
|
1861
|
+
method: "POST",
|
|
1862
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
1863
|
+
});
|
|
1864
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
1865
|
+
const context = createRequestContext({
|
|
1866
|
+
federation,
|
|
1867
|
+
request: unsignedRequest,
|
|
1868
|
+
url: new URL(unsignedRequest.url),
|
|
1869
|
+
data: void 0
|
|
1870
|
+
});
|
|
1871
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
1872
|
+
if (identifier !== "someone") return null;
|
|
1873
|
+
return new Person({ name: "Someone" });
|
|
1874
|
+
};
|
|
1875
|
+
const kv = new MemoryKvStore();
|
|
1876
|
+
const response = await handleInbox(unsignedRequest, {
|
|
1877
|
+
recipient: "someone",
|
|
1878
|
+
context,
|
|
1879
|
+
inboxContextFactory(_activity) {
|
|
1880
|
+
return createInboxContext({
|
|
1881
|
+
...context,
|
|
1882
|
+
clone: void 0,
|
|
1883
|
+
recipient: "someone"
|
|
1884
|
+
});
|
|
1885
|
+
},
|
|
1886
|
+
kv,
|
|
1887
|
+
kvPrefixes: {
|
|
1888
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1889
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1890
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1891
|
+
},
|
|
1892
|
+
actorDispatcher,
|
|
1893
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1894
|
+
signatureTimeWindow: { minutes: 5 },
|
|
1895
|
+
skipSignatureVerification: false
|
|
1896
|
+
});
|
|
1897
|
+
assertEquals(response.status, 401);
|
|
1898
|
+
assertEquals(response.headers.get("Accept-Signature"), null, "No Accept-Signature header when challenge policy is disabled");
|
|
1899
|
+
});
|
|
1900
|
+
test("handleInbox() actor/key mismatch → plain 401 (no challenge)", async () => {
|
|
1901
|
+
const maliciousActivity = new Create({
|
|
1902
|
+
id: new URL("https://attacker.example.com/activities/challenge-5"),
|
|
1903
|
+
actor: new URL("https://victim.example.com/users/alice"),
|
|
1904
|
+
object: new Note({
|
|
1905
|
+
id: new URL("https://attacker.example.com/notes/challenge-5"),
|
|
1906
|
+
attribution: new URL("https://victim.example.com/users/alice"),
|
|
1907
|
+
content: "Forged message!"
|
|
1908
|
+
})
|
|
1909
|
+
});
|
|
1910
|
+
const maliciousRequest = await signRequest(new Request("https://example.com/", {
|
|
1911
|
+
method: "POST",
|
|
1912
|
+
body: JSON.stringify(await maliciousActivity.toJsonLd())
|
|
1913
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1914
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
1915
|
+
const context = createRequestContext({
|
|
1916
|
+
federation,
|
|
1917
|
+
request: maliciousRequest,
|
|
1918
|
+
url: new URL(maliciousRequest.url),
|
|
1919
|
+
data: void 0,
|
|
1920
|
+
documentLoader: mockDocumentLoader
|
|
1921
|
+
});
|
|
1922
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
1923
|
+
if (identifier !== "someone") return null;
|
|
1924
|
+
return new Person({ name: "Someone" });
|
|
1925
|
+
};
|
|
1926
|
+
const kv = new MemoryKvStore();
|
|
1927
|
+
const response = await handleInbox(maliciousRequest, {
|
|
1928
|
+
recipient: "someone",
|
|
1929
|
+
context,
|
|
1930
|
+
inboxContextFactory(_activity) {
|
|
1931
|
+
return createInboxContext({
|
|
1932
|
+
...context,
|
|
1933
|
+
clone: void 0,
|
|
1934
|
+
recipient: "someone"
|
|
1935
|
+
});
|
|
1936
|
+
},
|
|
1937
|
+
kv,
|
|
1938
|
+
kvPrefixes: {
|
|
1939
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1940
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1941
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1942
|
+
},
|
|
1943
|
+
actorDispatcher,
|
|
1944
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1945
|
+
signatureTimeWindow: { minutes: 5 },
|
|
1946
|
+
skipSignatureVerification: false,
|
|
1947
|
+
inboxChallengePolicy: { enabled: true }
|
|
1948
|
+
});
|
|
1949
|
+
assertEquals(response.status, 401);
|
|
1950
|
+
assertEquals(response.headers.get("Accept-Signature"), null, "Actor/key mismatch should not emit Accept-Signature challenge");
|
|
1951
|
+
assertEquals(await response.text(), "The signer and the actor do not match.");
|
|
1952
|
+
});
|
|
1953
|
+
test("handleInbox() nonce issuance in challenge", async () => {
|
|
1954
|
+
const activity = new Create({
|
|
1955
|
+
id: new URL("https://example.com/activities/nonce-1"),
|
|
1956
|
+
actor: new URL("https://example.com/person2"),
|
|
1957
|
+
object: new Note({
|
|
1958
|
+
id: new URL("https://example.com/notes/nonce-1"),
|
|
1959
|
+
attribution: new URL("https://example.com/person2"),
|
|
1960
|
+
content: "Hello!"
|
|
1961
|
+
})
|
|
1962
|
+
});
|
|
1963
|
+
const unsignedRequest = new Request("https://example.com/", {
|
|
1964
|
+
method: "POST",
|
|
1965
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
1966
|
+
});
|
|
1967
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
1968
|
+
const context = createRequestContext({
|
|
1969
|
+
federation,
|
|
1970
|
+
request: unsignedRequest,
|
|
1971
|
+
url: new URL(unsignedRequest.url),
|
|
1972
|
+
data: void 0
|
|
1973
|
+
});
|
|
1974
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
1975
|
+
if (identifier !== "someone") return null;
|
|
1976
|
+
return new Person({ name: "Someone" });
|
|
1977
|
+
};
|
|
1978
|
+
const kv = new MemoryKvStore();
|
|
1979
|
+
const response = await handleInbox(unsignedRequest, {
|
|
1980
|
+
recipient: "someone",
|
|
1981
|
+
context,
|
|
1982
|
+
inboxContextFactory(_activity) {
|
|
1983
|
+
return createInboxContext({
|
|
1984
|
+
...context,
|
|
1985
|
+
clone: void 0,
|
|
1986
|
+
recipient: "someone"
|
|
1987
|
+
});
|
|
1988
|
+
},
|
|
1989
|
+
kv,
|
|
1990
|
+
kvPrefixes: {
|
|
1991
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
1992
|
+
publicKey: ["_fedify", "publicKey"],
|
|
1993
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
1994
|
+
},
|
|
1995
|
+
actorDispatcher,
|
|
1996
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1997
|
+
signatureTimeWindow: { minutes: 5 },
|
|
1998
|
+
skipSignatureVerification: false,
|
|
1999
|
+
inboxChallengePolicy: {
|
|
2000
|
+
enabled: true,
|
|
2001
|
+
requestNonce: true,
|
|
2002
|
+
nonceTtlSeconds: 300
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
assertEquals(response.status, 401);
|
|
2006
|
+
const acceptSig = response.headers.get("Accept-Signature");
|
|
2007
|
+
assert(acceptSig != null, "Accept-Signature header must be present");
|
|
2008
|
+
const parsed = parseAcceptSignature(acceptSig);
|
|
2009
|
+
assert(parsed.length > 0);
|
|
2010
|
+
assert(parsed[0].parameters.nonce != null, "Nonce must be present in Accept-Signature parameters");
|
|
2011
|
+
assertEquals(response.headers.get("Cache-Control"), "no-store");
|
|
2012
|
+
const nonceKey = [
|
|
2013
|
+
"_fedify",
|
|
2014
|
+
"acceptSignatureNonce",
|
|
2015
|
+
parsed[0].parameters.nonce
|
|
2016
|
+
];
|
|
2017
|
+
const stored = await kv.get(nonceKey);
|
|
2018
|
+
assertEquals(stored, true, "Nonce must be stored in KV store");
|
|
2019
|
+
});
|
|
2020
|
+
test("handleInbox() nonce consumption on valid signed request", async () => {
|
|
2021
|
+
const activity = new Create({
|
|
2022
|
+
id: new URL("https://example.com/activities/nonce-2"),
|
|
2023
|
+
actor: new URL("https://example.com/person2"),
|
|
2024
|
+
object: new Note({
|
|
2025
|
+
id: new URL("https://example.com/notes/nonce-2"),
|
|
2026
|
+
attribution: new URL("https://example.com/person2"),
|
|
2027
|
+
content: "Hello!"
|
|
2028
|
+
})
|
|
2029
|
+
});
|
|
2030
|
+
const kv = new MemoryKvStore();
|
|
2031
|
+
const noncePrefix = ["_fedify", "acceptSignatureNonce"];
|
|
2032
|
+
const nonce = "test-nonce-abc123";
|
|
2033
|
+
await kv.set([
|
|
2034
|
+
"_fedify",
|
|
2035
|
+
"acceptSignatureNonce",
|
|
2036
|
+
nonce
|
|
2037
|
+
], true, { ttl: Temporal.Duration.from({ seconds: 300 }) });
|
|
2038
|
+
const signedRequest = await signRequest(new Request("https://example.com/", {
|
|
2039
|
+
method: "POST",
|
|
2040
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
2041
|
+
}), rsaPrivateKey3, rsaPublicKey3.id, {
|
|
2042
|
+
spec: "rfc9421",
|
|
2043
|
+
rfc9421: { nonce }
|
|
2044
|
+
});
|
|
2045
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2046
|
+
const context = createRequestContext({
|
|
2047
|
+
federation,
|
|
2048
|
+
request: signedRequest,
|
|
2049
|
+
url: new URL(signedRequest.url),
|
|
2050
|
+
data: void 0,
|
|
2051
|
+
documentLoader: mockDocumentLoader
|
|
2052
|
+
});
|
|
2053
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
2054
|
+
if (identifier !== "someone") return null;
|
|
2055
|
+
return new Person({ name: "Someone" });
|
|
2056
|
+
};
|
|
2057
|
+
const response = await handleInbox(signedRequest, {
|
|
2058
|
+
recipient: "someone",
|
|
2059
|
+
context,
|
|
2060
|
+
inboxContextFactory(_activity) {
|
|
2061
|
+
return createInboxContext({
|
|
2062
|
+
...context,
|
|
2063
|
+
clone: void 0,
|
|
2064
|
+
recipient: "someone"
|
|
2065
|
+
});
|
|
2066
|
+
},
|
|
2067
|
+
kv,
|
|
2068
|
+
kvPrefixes: {
|
|
2069
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2070
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2071
|
+
acceptSignatureNonce: noncePrefix
|
|
2072
|
+
},
|
|
2073
|
+
actorDispatcher,
|
|
2074
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2075
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2076
|
+
skipSignatureVerification: false,
|
|
2077
|
+
inboxChallengePolicy: {
|
|
2078
|
+
enabled: true,
|
|
2079
|
+
requestNonce: true,
|
|
2080
|
+
nonceTtlSeconds: 300
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
assertEquals(response.status, 202);
|
|
2084
|
+
const stored = await kv.get([
|
|
2085
|
+
"_fedify",
|
|
2086
|
+
"acceptSignatureNonce",
|
|
2087
|
+
nonce
|
|
2088
|
+
]);
|
|
2089
|
+
assertEquals(stored, void 0, "Nonce must be consumed after use");
|
|
2090
|
+
});
|
|
2091
|
+
test("handleInbox() nonce replay prevention", async () => {
|
|
2092
|
+
const activity = new Create({
|
|
2093
|
+
id: new URL("https://example.com/activities/nonce-3"),
|
|
2094
|
+
actor: new URL("https://example.com/person2"),
|
|
2095
|
+
object: new Note({
|
|
2096
|
+
id: new URL("https://example.com/notes/nonce-3"),
|
|
2097
|
+
attribution: new URL("https://example.com/person2"),
|
|
2098
|
+
content: "Hello!"
|
|
2099
|
+
})
|
|
2100
|
+
});
|
|
2101
|
+
const kv = new MemoryKvStore();
|
|
2102
|
+
const noncePrefix = ["_fedify", "acceptSignatureNonce"];
|
|
2103
|
+
const nonce = "replay-nonce-xyz";
|
|
2104
|
+
const signedRequest = await signRequest(new Request("https://example.com/", {
|
|
2105
|
+
method: "POST",
|
|
2106
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
2107
|
+
}), rsaPrivateKey3, rsaPublicKey3.id, {
|
|
2108
|
+
spec: "rfc9421",
|
|
2109
|
+
rfc9421: { nonce }
|
|
2110
|
+
});
|
|
2111
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2112
|
+
const context = createRequestContext({
|
|
2113
|
+
federation,
|
|
2114
|
+
request: signedRequest,
|
|
2115
|
+
url: new URL(signedRequest.url),
|
|
2116
|
+
data: void 0,
|
|
2117
|
+
documentLoader: mockDocumentLoader
|
|
2118
|
+
});
|
|
2119
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
2120
|
+
if (identifier !== "someone") return null;
|
|
2121
|
+
return new Person({ name: "Someone" });
|
|
2122
|
+
};
|
|
2123
|
+
const response = await handleInbox(signedRequest, {
|
|
2124
|
+
recipient: "someone",
|
|
2125
|
+
context,
|
|
2126
|
+
inboxContextFactory(_activity) {
|
|
2127
|
+
return createInboxContext({
|
|
2128
|
+
...context,
|
|
2129
|
+
clone: void 0,
|
|
2130
|
+
recipient: "someone"
|
|
2131
|
+
});
|
|
2132
|
+
},
|
|
2133
|
+
kv,
|
|
2134
|
+
kvPrefixes: {
|
|
2135
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2136
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2137
|
+
acceptSignatureNonce: noncePrefix
|
|
2138
|
+
},
|
|
2139
|
+
actorDispatcher,
|
|
2140
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2141
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2142
|
+
skipSignatureVerification: false,
|
|
2143
|
+
inboxChallengePolicy: {
|
|
2144
|
+
enabled: true,
|
|
2145
|
+
requestNonce: true,
|
|
2146
|
+
nonceTtlSeconds: 300
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
assertEquals(response.status, 401);
|
|
2150
|
+
const acceptSig = response.headers.get("Accept-Signature");
|
|
2151
|
+
assert(acceptSig != null, "Must emit fresh Accept-Signature challenge");
|
|
2152
|
+
const parsed = parseAcceptSignature(acceptSig);
|
|
2153
|
+
assert(parsed.length > 0);
|
|
2154
|
+
assert(parsed[0].parameters.nonce != null, "Fresh challenge must include a new nonce");
|
|
2155
|
+
assert(parsed[0].parameters.nonce !== nonce, "Fresh nonce must differ from the replayed one");
|
|
2156
|
+
assertEquals(response.headers.get("Cache-Control"), "no-store", "Challenge response must have Cache-Control: no-store");
|
|
2157
|
+
});
|
|
2158
|
+
test("handleInbox() nonce bypass: valid sig without nonce + invalid sig with nonce", async () => {
|
|
2159
|
+
const activity = new Create({
|
|
2160
|
+
id: new URL("https://example.com/activities/nonce-bypass-1"),
|
|
2161
|
+
actor: new URL("https://example.com/person2"),
|
|
2162
|
+
object: new Note({
|
|
2163
|
+
id: new URL("https://example.com/notes/nonce-bypass-1"),
|
|
2164
|
+
attribution: new URL("https://example.com/person2"),
|
|
2165
|
+
content: "Hello!"
|
|
2166
|
+
})
|
|
2167
|
+
});
|
|
2168
|
+
const kv = new MemoryKvStore();
|
|
2169
|
+
const noncePrefix = ["_fedify", "acceptSignatureNonce"];
|
|
2170
|
+
const storedNonce = "bypass-nonce-abc123";
|
|
2171
|
+
await kv.set([
|
|
2172
|
+
"_fedify",
|
|
2173
|
+
"acceptSignatureNonce",
|
|
2174
|
+
storedNonce
|
|
2175
|
+
], true, { ttl: Temporal.Duration.from({ seconds: 300 }) });
|
|
2176
|
+
const signedRequest = await signRequest(new Request("https://example.com/", {
|
|
2177
|
+
method: "POST",
|
|
2178
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
2179
|
+
}), rsaPrivateKey3, rsaPublicKey3.id, { spec: "rfc9421" });
|
|
2180
|
+
const existingSignatureInput = signedRequest.headers.get("Signature-Input");
|
|
2181
|
+
const existingSignature = signedRequest.headers.get("Signature");
|
|
2182
|
+
const bogusSigInput = `sig2=("@method" "@target-uri");alg="rsa-v1_5-sha256";keyid="${rsaPublicKey3.id.href}";created=${Math.floor(Date.now() / 1e3)};nonce="${storedNonce}"`;
|
|
2183
|
+
const bogusSigValue = `sig2=:AAAA:`;
|
|
2184
|
+
const tamperedHeaders = new Headers(signedRequest.headers);
|
|
2185
|
+
tamperedHeaders.set("Signature-Input", `${existingSignatureInput}, ${bogusSigInput}`);
|
|
2186
|
+
tamperedHeaders.set("Signature", `${existingSignature}, ${bogusSigValue}`);
|
|
2187
|
+
const tamperedRequest = new Request(signedRequest.url, {
|
|
2188
|
+
method: signedRequest.method,
|
|
2189
|
+
headers: tamperedHeaders,
|
|
2190
|
+
body: await signedRequest.clone().arrayBuffer()
|
|
2191
|
+
});
|
|
2192
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2193
|
+
const context = createRequestContext({
|
|
2194
|
+
federation,
|
|
2195
|
+
request: tamperedRequest,
|
|
2196
|
+
url: new URL(tamperedRequest.url),
|
|
2197
|
+
data: void 0,
|
|
2198
|
+
documentLoader: mockDocumentLoader
|
|
2199
|
+
});
|
|
2200
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
2201
|
+
if (identifier !== "someone") return null;
|
|
2202
|
+
return new Person({ name: "Someone" });
|
|
2203
|
+
};
|
|
2204
|
+
const response = await handleInbox(tamperedRequest, {
|
|
2205
|
+
recipient: "someone",
|
|
2206
|
+
context,
|
|
2207
|
+
inboxContextFactory(_activity) {
|
|
2208
|
+
return createInboxContext({
|
|
2209
|
+
...context,
|
|
2210
|
+
clone: void 0,
|
|
2211
|
+
recipient: "someone"
|
|
2212
|
+
});
|
|
2213
|
+
},
|
|
2214
|
+
kv,
|
|
2215
|
+
kvPrefixes: {
|
|
2216
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2217
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2218
|
+
acceptSignatureNonce: noncePrefix
|
|
2219
|
+
},
|
|
2220
|
+
actorDispatcher,
|
|
2221
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2222
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2223
|
+
skipSignatureVerification: false,
|
|
2224
|
+
inboxChallengePolicy: {
|
|
2225
|
+
enabled: true,
|
|
2226
|
+
requestNonce: true,
|
|
2227
|
+
nonceTtlSeconds: 300
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
assertEquals(response.status, 401, "Request with nonce only in a non-verified signature must be rejected (nonce verification must be bound to the verified signature label)");
|
|
2231
|
+
const stored = await kv.get([
|
|
2232
|
+
"_fedify",
|
|
2233
|
+
"acceptSignatureNonce",
|
|
2234
|
+
storedNonce
|
|
2235
|
+
]);
|
|
2236
|
+
assertEquals(stored, true, "Nonce must not be consumed when it comes from a non-verified signature");
|
|
2237
|
+
});
|
|
2238
|
+
test("handleInbox() actor/key mismatch does not consume nonce", async () => {
|
|
2239
|
+
const maliciousActivity = new Create({
|
|
2240
|
+
id: new URL("https://attacker.example.com/activities/mismatch-nonce-1"),
|
|
2241
|
+
actor: new URL("https://victim.example.com/users/alice"),
|
|
2242
|
+
object: new Note({
|
|
2243
|
+
id: new URL("https://attacker.example.com/notes/mismatch-nonce-1"),
|
|
2244
|
+
attribution: new URL("https://victim.example.com/users/alice"),
|
|
2245
|
+
content: "Forged message with nonce!"
|
|
2246
|
+
})
|
|
2247
|
+
});
|
|
2248
|
+
const kv = new MemoryKvStore();
|
|
2249
|
+
const noncePrefix = ["_fedify", "acceptSignatureNonce"];
|
|
2250
|
+
const nonce = "mismatch-nonce-xyz";
|
|
2251
|
+
await kv.set([
|
|
2252
|
+
"_fedify",
|
|
2253
|
+
"acceptSignatureNonce",
|
|
2254
|
+
nonce
|
|
2255
|
+
], true, { ttl: Temporal.Duration.from({ seconds: 300 }) });
|
|
2256
|
+
const maliciousRequest = await signRequest(new Request("https://example.com/", {
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
body: JSON.stringify(await maliciousActivity.toJsonLd())
|
|
2259
|
+
}), rsaPrivateKey3, rsaPublicKey3.id, {
|
|
2260
|
+
spec: "rfc9421",
|
|
2261
|
+
rfc9421: { nonce }
|
|
2262
|
+
});
|
|
2263
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2264
|
+
const context = createRequestContext({
|
|
2265
|
+
federation,
|
|
2266
|
+
request: maliciousRequest,
|
|
2267
|
+
url: new URL(maliciousRequest.url),
|
|
2268
|
+
data: void 0,
|
|
2269
|
+
documentLoader: mockDocumentLoader
|
|
2270
|
+
});
|
|
2271
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
2272
|
+
if (identifier !== "someone") return null;
|
|
2273
|
+
return new Person({ name: "Someone" });
|
|
2274
|
+
};
|
|
2275
|
+
const response = await handleInbox(maliciousRequest, {
|
|
2276
|
+
recipient: "someone",
|
|
2277
|
+
context,
|
|
2278
|
+
inboxContextFactory(_activity) {
|
|
2279
|
+
return createInboxContext({
|
|
2280
|
+
...context,
|
|
2281
|
+
clone: void 0,
|
|
2282
|
+
recipient: "someone"
|
|
2283
|
+
});
|
|
2284
|
+
},
|
|
2285
|
+
kv,
|
|
2286
|
+
kvPrefixes: {
|
|
2287
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2288
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2289
|
+
acceptSignatureNonce: noncePrefix
|
|
2290
|
+
},
|
|
2291
|
+
actorDispatcher,
|
|
2292
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2293
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2294
|
+
skipSignatureVerification: false,
|
|
2295
|
+
inboxChallengePolicy: {
|
|
2296
|
+
enabled: true,
|
|
2297
|
+
requestNonce: true,
|
|
2298
|
+
nonceTtlSeconds: 300
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
assertEquals(response.status, 401);
|
|
2302
|
+
assertEquals(await response.text(), "The signer and the actor do not match.");
|
|
2303
|
+
const stored = await kv.get([
|
|
2304
|
+
"_fedify",
|
|
2305
|
+
"acceptSignatureNonce",
|
|
2306
|
+
nonce
|
|
2307
|
+
]);
|
|
2308
|
+
assertEquals(stored, true, "Nonce must not be consumed when actor/key ownership check fails");
|
|
2309
|
+
});
|
|
2310
|
+
test("handleInbox() challenge policy enabled + unverifiedActivityHandler returns undefined", async () => {
|
|
2311
|
+
const activity = new Create({
|
|
2312
|
+
id: new URL("https://example.com/activities/challenge-unverified"),
|
|
2313
|
+
actor: new URL("https://example.com/person2"),
|
|
2314
|
+
object: new Note({
|
|
2315
|
+
id: new URL("https://example.com/notes/challenge-unverified"),
|
|
2316
|
+
attribution: new URL("https://example.com/person2"),
|
|
2317
|
+
content: "Hello!"
|
|
2318
|
+
})
|
|
2319
|
+
});
|
|
2320
|
+
const originalRequest = new Request("https://example.com/", {
|
|
2321
|
+
method: "POST",
|
|
2322
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
2323
|
+
});
|
|
2324
|
+
const signedRequest = await signRequest(originalRequest, rsaPrivateKey3, rsaPublicKey3.id);
|
|
2325
|
+
const jsonLd = await activity.toJsonLd();
|
|
2326
|
+
const tamperedBody = JSON.stringify({
|
|
2327
|
+
...jsonLd,
|
|
2328
|
+
"https://example.com/tampered": true
|
|
2329
|
+
});
|
|
2330
|
+
const tamperedRequest = new Request(signedRequest.url, {
|
|
2331
|
+
method: signedRequest.method,
|
|
2332
|
+
headers: signedRequest.headers,
|
|
2333
|
+
body: tamperedBody
|
|
2334
|
+
});
|
|
2335
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2336
|
+
const context = createRequestContext({
|
|
2337
|
+
federation,
|
|
2338
|
+
request: tamperedRequest,
|
|
2339
|
+
url: new URL(tamperedRequest.url),
|
|
2340
|
+
data: void 0,
|
|
2341
|
+
documentLoader: mockDocumentLoader
|
|
2342
|
+
});
|
|
2343
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
2344
|
+
if (identifier !== "someone") return null;
|
|
2345
|
+
return new Person({ name: "Someone" });
|
|
2346
|
+
};
|
|
2347
|
+
const kv = new MemoryKvStore();
|
|
2348
|
+
const response = await handleInbox(tamperedRequest, {
|
|
2349
|
+
recipient: "someone",
|
|
2350
|
+
context,
|
|
2351
|
+
inboxContextFactory(_activity) {
|
|
2352
|
+
return createInboxContext({
|
|
2353
|
+
...context,
|
|
2354
|
+
clone: void 0,
|
|
2355
|
+
recipient: "someone"
|
|
2356
|
+
});
|
|
2357
|
+
},
|
|
2358
|
+
kv,
|
|
2359
|
+
kvPrefixes: {
|
|
2360
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2361
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2362
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
2363
|
+
},
|
|
2364
|
+
actorDispatcher,
|
|
2365
|
+
unverifiedActivityHandler() {},
|
|
2366
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2367
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2368
|
+
skipSignatureVerification: false,
|
|
2369
|
+
inboxChallengePolicy: { enabled: true }
|
|
2370
|
+
});
|
|
2371
|
+
assertEquals(response.status, 401);
|
|
2372
|
+
const acceptSig = response.headers.get("Accept-Signature");
|
|
2373
|
+
assert(acceptSig != null, "Accept-Signature header must be present when unverifiedActivityHandler returns undefined and challenge policy is enabled");
|
|
2374
|
+
const parsed = parseAcceptSignature(acceptSig);
|
|
2375
|
+
assert(parsed.length > 0, "Accept-Signature must have at least one entry");
|
|
2376
|
+
assertEquals(response.headers.get("Cache-Control"), "no-store", "Cache-Control: no-store must be set for challenge-response");
|
|
2377
|
+
assertEquals(response.headers.get("Vary"), "Accept, Signature", "Vary header must include Accept and Signature");
|
|
2378
|
+
});
|
|
2379
|
+
test("handleInbox() challenge policy enabled + unverifiedActivityHandler throws error", async () => {
|
|
2380
|
+
const activity = new Create({
|
|
2381
|
+
id: new URL("https://example.com/activities/challenge-throw"),
|
|
2382
|
+
actor: new URL("https://example.com/person2"),
|
|
2383
|
+
object: new Note({
|
|
2384
|
+
id: new URL("https://example.com/notes/challenge-throw"),
|
|
2385
|
+
attribution: new URL("https://example.com/person2"),
|
|
2386
|
+
content: "Hello!"
|
|
2387
|
+
})
|
|
2388
|
+
});
|
|
2389
|
+
const originalRequest = new Request("https://example.com/", {
|
|
2390
|
+
method: "POST",
|
|
2391
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
2392
|
+
});
|
|
2393
|
+
const signedRequest = await signRequest(originalRequest, rsaPrivateKey3, rsaPublicKey3.id);
|
|
2394
|
+
const jsonLd = await activity.toJsonLd();
|
|
2395
|
+
const tamperedBody = JSON.stringify({
|
|
2396
|
+
...jsonLd,
|
|
2397
|
+
"https://example.com/tampered": true
|
|
2398
|
+
});
|
|
2399
|
+
const tamperedRequest = new Request(signedRequest.url, {
|
|
2400
|
+
method: signedRequest.method,
|
|
2401
|
+
headers: signedRequest.headers,
|
|
2402
|
+
body: tamperedBody
|
|
2403
|
+
});
|
|
2404
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2405
|
+
const context = createRequestContext({
|
|
2406
|
+
federation,
|
|
2407
|
+
request: tamperedRequest,
|
|
2408
|
+
url: new URL(tamperedRequest.url),
|
|
2409
|
+
data: void 0,
|
|
2410
|
+
documentLoader: mockDocumentLoader
|
|
2411
|
+
});
|
|
2412
|
+
const actorDispatcher = (_ctx, identifier) => {
|
|
2413
|
+
if (identifier !== "someone") return null;
|
|
2414
|
+
return new Person({ name: "Someone" });
|
|
2415
|
+
};
|
|
2416
|
+
const kv = new MemoryKvStore();
|
|
2417
|
+
const response = await handleInbox(tamperedRequest, {
|
|
2418
|
+
recipient: "someone",
|
|
2419
|
+
context,
|
|
2420
|
+
inboxContextFactory(_activity) {
|
|
2421
|
+
return createInboxContext({
|
|
2422
|
+
...context,
|
|
2423
|
+
clone: void 0,
|
|
2424
|
+
recipient: "someone"
|
|
2425
|
+
});
|
|
2426
|
+
},
|
|
2427
|
+
kv,
|
|
2428
|
+
kvPrefixes: {
|
|
2429
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2430
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2431
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
2432
|
+
},
|
|
2433
|
+
actorDispatcher,
|
|
2434
|
+
unverifiedActivityHandler() {
|
|
2435
|
+
throw new Error("handler error");
|
|
2436
|
+
},
|
|
2437
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2438
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2439
|
+
skipSignatureVerification: false,
|
|
2440
|
+
inboxChallengePolicy: { enabled: true }
|
|
2441
|
+
});
|
|
2442
|
+
assertEquals(response.status, 401);
|
|
2443
|
+
const acceptSig = response.headers.get("Accept-Signature");
|
|
2444
|
+
assert(acceptSig != null, "Accept-Signature header must be present when unverifiedActivityHandler throws and challenge policy is enabled");
|
|
2445
|
+
const parsed = parseAcceptSignature(acceptSig);
|
|
2446
|
+
assert(parsed.length > 0, "Accept-Signature must have at least one entry");
|
|
2447
|
+
assertEquals(response.headers.get("Cache-Control"), "no-store", "Cache-Control: no-store must be set for challenge-response");
|
|
2448
|
+
assertEquals(response.headers.get("Vary"), "Accept, Signature", "Vary header must include Accept and Signature");
|
|
2449
|
+
});
|
|
1670
2450
|
|
|
1671
2451
|
//#endregion
|