@fedify/fedify 2.4.0-dev.2077 → 2.4.0-dev.2093
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/{builder-i3jxVe9m.mjs → builder-Ld4Y9ymR.mjs} +34 -3
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-Cayy-ECj.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
- package/dist/{deno-Djkzd-G_.mjs → deno-8t8k_6Yv.mjs} +1 -1
- package/dist/{docloader-dA0RswUV.mjs → docloader-DJKcsAiF.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +254 -5
- package/dist/federation/portable-gateway-keys.test.mjs +72 -11
- package/dist/federation/portable-inbox.test.mjs +513 -6
- package/dist/federation/portable.test.mjs +182 -4
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +89 -1
- package/dist/{federation-CFd0Jhhv.cjs → federation-aSHO04ob.cjs} +472 -74
- package/dist/{http-CCJ97Jql.mjs → http-B2ftY0Ha.mjs} +4 -4
- package/dist/{http-Dvy_IwRa.cjs → http-DSaSAN7X.cjs} +1609 -1457
- package/dist/{http-aqh3yFdM.js → http-DyT7DP1Y.js} +1518 -1390
- package/dist/{key-IxTTC8Yf.mjs → key-Dwm4VswT.mjs} +39 -99
- package/dist/{kv-cache-BP69NV0P.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
- package/dist/{kv-cache-Bwwbh-xR.mjs → kv-cache-D0yYukqk.mjs} +1 -1
- package/dist/{kv-cache-Y6SDRSX6.js → kv-cache-DmroAz3M.js} +1 -1
- package/dist/{ld-l0B9nREb.mjs → ld-BYZOoDzZ.mjs} +3 -3
- package/dist/{metrics-D1p6hv4z.mjs → metrics-B7YwKTFr.mjs} +1 -1
- package/dist/{middleware-BDjCUHMS.mjs → middleware-BNeqKSsL.mjs} +1 -1
- package/dist/{middleware-SSfryVzY.js → middleware-Blq0882d.js} +480 -82
- package/dist/{middleware-BMgzqBtr.mjs → middleware-E_0IH72m.mjs} +278 -80
- package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
- package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-DGbYmXqU.mjs → owner-Cgx2Dc7I.mjs} +5 -4
- package/dist/{owner-jH1z_YfD.js → owner-DAGSC5gX.js} +2 -2
- package/dist/{owner-BpOxL5PG.cjs → owner-szIZl4qI.cjs} +2 -2
- package/dist/{portable-key-CCwJGSsU.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-CVL7JnZd.mjs → proof-DVXC6nYz.mjs} +59 -18
- package/dist/{send-Ejy1jOz9.mjs → send-D-hOvYKy.mjs} +3 -3
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +160 -3
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +69 -5
- package/dist/sig/portable-key.test.mjs +176 -10
- package/dist/sig/proof.test.mjs +107 -4
- package/dist/{sig-Dpy1B1Ad.js → sig-B5Ell7pO.js} +19 -3
- package/dist/{sig-xoqy3ATq.cjs → sig-CXOKN4Br.cjs} +19 -3
- package/dist/{temporal-DNJ1cNpr.mjs → temporal-CqpKGEPk.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
|
@@ -8,11 +8,11 @@ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
|
8
8
|
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
9
9
|
import "../std__assert-DvMDRPes.mjs";
|
|
10
10
|
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
11
|
-
import { p as signJsonLd } from "../ld-
|
|
12
|
-
import {
|
|
13
|
-
import { s as signRequest } from "../http-
|
|
11
|
+
import { p as signJsonLd } from "../ld-BYZOoDzZ.mjs";
|
|
12
|
+
import { i as signObject } from "../proof-DVXC6nYz.mjs";
|
|
13
|
+
import { s as signRequest, u as verifyRequestDetailed } from "../http-B2ftY0Ha.mjs";
|
|
14
14
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import { l as forwardPortableInboxActivity, o as createFederation, u as resolvePortableInboxForwardingOptions } from "../middleware-E_0IH72m.mjs";
|
|
16
16
|
import { Create, Follow, Note, Person, Tombstone } from "@fedify/vocab";
|
|
17
17
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
18
18
|
import { exportDidKey, formatIri, parseIri } from "@fedify/vocab-runtime";
|
|
@@ -56,7 +56,7 @@ function setup({ gateways = [
|
|
|
56
56
|
LOCAL,
|
|
57
57
|
GATEWAY2,
|
|
58
58
|
GATEWAY3
|
|
59
|
-
], kv = new MemoryKvStore(), queue, options = {}, actor, listenerError = false, keyPairs = true, onSharedInboxKey } = {}) {
|
|
59
|
+
], kv = new MemoryKvStore(), queue, options = {}, actor, listenerError = false, keyPairs = true, aliceKeyPairs = [], mapPortableActorId, onSharedInboxKey } = {}) {
|
|
60
60
|
const federation = createFederation({
|
|
61
61
|
kv,
|
|
62
62
|
...options.allowPrivateAddress ? {} : {
|
|
@@ -83,7 +83,8 @@ function setup({ gateways = [
|
|
|
83
83
|
if (keyPairs) actorCallbacks.setKeyPairsDispatcher((_ctx, identifier) => identifier === "ordinary" ? [{
|
|
84
84
|
privateKey: rsaPrivateKey2,
|
|
85
85
|
publicKey: rsaPublicKey2.publicKey
|
|
86
|
-
}] : []);
|
|
86
|
+
}] : identifier === "alice" ? aliceKeyPairs : []);
|
|
87
|
+
if (mapPortableActorId != null) actorCallbacks.mapPortableActorId((_ctx, identifier) => mapPortableActorId(identifier));
|
|
87
88
|
const inboxListeners = federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
|
|
88
89
|
if (onSharedInboxKey != null) inboxListeners.setSharedKeyDispatcher(() => {
|
|
89
90
|
onSharedInboxKey();
|
|
@@ -494,6 +495,7 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
|
|
|
494
495
|
const activityId = parseIri(`ap+ef61://${did}/follows/slow`);
|
|
495
496
|
const parameters = {
|
|
496
497
|
recipient: {
|
|
498
|
+
actorId: parseIri(`ap+ef61://${did}/users/alice`),
|
|
497
499
|
inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
|
|
498
500
|
canonicalInboxId: `ap://${did}/users/alice/inbox`,
|
|
499
501
|
gateways: [new URL(LOCAL), new URL(GATEWAY2)]
|
|
@@ -520,6 +522,511 @@ test("forwardPortableInboxActivity() does not wait for slow gateways", async ()
|
|
|
520
522
|
esm_default.hardReset();
|
|
521
523
|
}
|
|
522
524
|
});
|
|
525
|
+
function compatibleAlice(path = "/users/alice", { authority = did, origin = LOCAL, gateways = [
|
|
526
|
+
LOCAL,
|
|
527
|
+
GATEWAY2,
|
|
528
|
+
GATEWAY3
|
|
529
|
+
] } = {}) {
|
|
530
|
+
return {
|
|
531
|
+
id: new URL(`${origin}/.well-known/apgateway/${authority}${path}`),
|
|
532
|
+
inbox: new URL(inboxUrl(authority, `${path}/inbox`, origin)),
|
|
533
|
+
gateways: gateways.map((g) => new URL(g))
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
test("Federation.fetch() delivers to portable inboxes of compatible-ID actors", async (t) => {
|
|
537
|
+
const apAlice = parseIri(`ap+ef61://${did}/users/alice`);
|
|
538
|
+
const apInbox = parseIri(`ap+ef61://${did}/users/alice/inbox`);
|
|
539
|
+
const cases = [
|
|
540
|
+
[
|
|
541
|
+
"compatible actor and inbox",
|
|
542
|
+
compatibleAlice(),
|
|
543
|
+
""
|
|
544
|
+
],
|
|
545
|
+
[
|
|
546
|
+
"on another gateway",
|
|
547
|
+
compatibleAlice(void 0, { origin: GATEWAY2 }),
|
|
548
|
+
""
|
|
549
|
+
],
|
|
550
|
+
[
|
|
551
|
+
"compatible actor and ap: inbox",
|
|
552
|
+
{
|
|
553
|
+
...compatibleAlice(),
|
|
554
|
+
inbox: apInbox
|
|
555
|
+
},
|
|
556
|
+
""
|
|
557
|
+
],
|
|
558
|
+
[
|
|
559
|
+
"ap: actor and compatible inbox",
|
|
560
|
+
{
|
|
561
|
+
...compatibleAlice(),
|
|
562
|
+
id: apAlice
|
|
563
|
+
},
|
|
564
|
+
""
|
|
565
|
+
],
|
|
566
|
+
[
|
|
567
|
+
"compatible inbox with a query",
|
|
568
|
+
{
|
|
569
|
+
...compatibleAlice(),
|
|
570
|
+
inbox: new URL(inboxUrl() + "?page=1")
|
|
571
|
+
},
|
|
572
|
+
"?page=1"
|
|
573
|
+
]
|
|
574
|
+
];
|
|
575
|
+
for (const [name, alice, query] of cases) await t.step(name, async () => {
|
|
576
|
+
const queue = new RecordingQueue();
|
|
577
|
+
const { federation, received } = setup({
|
|
578
|
+
queue,
|
|
579
|
+
actor: (_ctx, identifier) => identifier === "alice" ? new Person(alice) : null
|
|
580
|
+
});
|
|
581
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
582
|
+
assertEquals(received.length, 1);
|
|
583
|
+
assertEquals(received[0].recipient, "alice");
|
|
584
|
+
assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3) + query, inboxUrl(did, "/users/alice/inbox", GATEWAY2) + query]);
|
|
585
|
+
});
|
|
586
|
+
const json = await signedFollow();
|
|
587
|
+
const rejections = [
|
|
588
|
+
["DID mismatch", new Person(compatibleAlice(void 0, { authority: otherDid }))],
|
|
589
|
+
["inbox mismatch", new Person({
|
|
590
|
+
...compatibleAlice(),
|
|
591
|
+
inbox: new URL(inboxUrl(did, "/users/alice/other-inbox"))
|
|
592
|
+
})],
|
|
593
|
+
["this server is not a gateway of the actor", new Person(compatibleAlice(void 0, { gateways: [GATEWAY2] }))],
|
|
594
|
+
["malformed actor ID", new Person({
|
|
595
|
+
...compatibleAlice(),
|
|
596
|
+
id: new URL(compatibleAlice().id.href + "?@gateway=https%3A%2F%2Fexample.com")
|
|
597
|
+
})],
|
|
598
|
+
["malformed inbox ID", new Person({
|
|
599
|
+
...compatibleAlice(),
|
|
600
|
+
inbox: new URL(inboxUrl() + "?@gateway=https%3A%2F%2Fexample.com")
|
|
601
|
+
})]
|
|
602
|
+
];
|
|
603
|
+
for (const [name, actor] of rejections) await t.step(name, async () => {
|
|
604
|
+
const { federation, received } = setup({ actor: (_ctx, identifier) => identifier === "alice" ? actor : null });
|
|
605
|
+
assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 404);
|
|
606
|
+
assertEquals(received.length, 0);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
test("forwardPortableInboxActivity() deduplicates compatible activity IDs", async () => {
|
|
610
|
+
const kv = new MemoryKvStore();
|
|
611
|
+
const queue = new RecordingQueue();
|
|
612
|
+
const forward = (activityId) => forwardPortableInboxActivity({
|
|
613
|
+
recipient: {
|
|
614
|
+
actorId: compatibleAlice().id,
|
|
615
|
+
inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
|
|
616
|
+
canonicalInboxId: `ap+ef61://${did}/users/alice/inbox`,
|
|
617
|
+
gateways: [new URL(LOCAL), new URL(GATEWAY2)]
|
|
618
|
+
},
|
|
619
|
+
activity: { id: activityId.href },
|
|
620
|
+
activityId,
|
|
621
|
+
activityType: "https://www.w3.org/ns/activitystreams#Follow",
|
|
622
|
+
excludedOrigins: [LOCAL],
|
|
623
|
+
baseUrl: LOCAL,
|
|
624
|
+
kv,
|
|
625
|
+
kvPrefix: ["_fedify", "portableInboxForwarding"],
|
|
626
|
+
outboxQueue: queue
|
|
627
|
+
});
|
|
628
|
+
const path = `${did}/follows/compatible`;
|
|
629
|
+
assertEquals((await forward(new URL(`${LOCAL}/.well-known/apgateway/${path}`))).length, 1);
|
|
630
|
+
for (const id of [
|
|
631
|
+
new URL(`${GATEWAY2}/.well-known/apgateway/${path}`),
|
|
632
|
+
new URL(`${LOCAL}/.well-known/apgateway/${path}?x=1`),
|
|
633
|
+
parseIri(`ap://${path}`)
|
|
634
|
+
]) assertEquals(await forward(id), [], id.href);
|
|
635
|
+
assertEquals(queue.outbox.length, 1);
|
|
636
|
+
});
|
|
637
|
+
const aliceId = parseIri(`ap+ef61://${did}/users/alice`);
|
|
638
|
+
const rsaGatewayKeyPair = await crypto.subtle.generateKey({
|
|
639
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
640
|
+
modulusLength: 2048,
|
|
641
|
+
publicExponent: new Uint8Array([
|
|
642
|
+
1,
|
|
643
|
+
0,
|
|
644
|
+
1
|
|
645
|
+
]),
|
|
646
|
+
hash: "SHA-256"
|
|
647
|
+
}, true, ["sign", "verify"]);
|
|
648
|
+
function compatibleAliceId(origin = LOCAL) {
|
|
649
|
+
return `${origin}/.well-known/apgateway/${did}/users/alice`;
|
|
650
|
+
}
|
|
651
|
+
function mapAlice(identifier) {
|
|
652
|
+
return identifier === "alice" ? aliceId : null;
|
|
653
|
+
}
|
|
654
|
+
function signatureKeyId(headers) {
|
|
655
|
+
return headers.get("Signature-Input")?.match(/keyid="([^"]+)"/)?.[1] ?? headers.get("Signature")?.match(/keyId="([^"]+)"/)?.[1];
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Makes a document loader that serves Alice's actor document as the gateway
|
|
659
|
+
* at {@link LOCAL} does: signed by her DID, and embedding the gateway keys in
|
|
660
|
+
* its `assertionMethod`.
|
|
661
|
+
*/
|
|
662
|
+
async function createGatewayLoader(keyPairs) {
|
|
663
|
+
const { federation } = setup({
|
|
664
|
+
aliceKeyPairs: keyPairs,
|
|
665
|
+
mapPortableActorId: mapAlice
|
|
666
|
+
});
|
|
667
|
+
const keys = await federation.createContext(new URL(LOCAL)).getActorKeyPairs("alice");
|
|
668
|
+
const document = await (await signObject(new Person({
|
|
669
|
+
id: aliceId,
|
|
670
|
+
inbox: parseIri(`${aliceId.href}/inbox`),
|
|
671
|
+
gateways: [
|
|
672
|
+
new URL(LOCAL),
|
|
673
|
+
new URL(GATEWAY2),
|
|
674
|
+
new URL(GATEWAY3)
|
|
675
|
+
],
|
|
676
|
+
assertionMethods: keys.map((k) => k.multikey)
|
|
677
|
+
}), ed25519PrivateKey, keyId, { contextLoader: mockDocumentLoader })).toJsonLd({ contextLoader: mockDocumentLoader });
|
|
678
|
+
return (url) => {
|
|
679
|
+
if (url.replace(/#.*$/, "") !== compatibleAliceId()) return mockDocumentLoader(url);
|
|
680
|
+
return Promise.resolve({
|
|
681
|
+
contextUrl: null,
|
|
682
|
+
documentUrl: url,
|
|
683
|
+
document: structuredClone(document)
|
|
684
|
+
});
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function toRequest({ url, headers, body }) {
|
|
688
|
+
return new Request(url, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers,
|
|
691
|
+
body
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async function assertSignedBy(request, expectedKeyId, documentLoader) {
|
|
695
|
+
assertEquals(signatureKeyId(request.headers), expectedKeyId);
|
|
696
|
+
const result = await verifyRequestDetailed(toRequest(request), {
|
|
697
|
+
documentLoader,
|
|
698
|
+
contextLoader: mockDocumentLoader
|
|
699
|
+
});
|
|
700
|
+
assert(result.verified, JSON.stringify(result));
|
|
701
|
+
assertEquals(result.key.id?.href, expectedKeyId);
|
|
702
|
+
}
|
|
703
|
+
/** Records the requests made to the gateways other than {@link LOCAL}. */
|
|
704
|
+
function captureGatewayRequests(respond = () => new Response(null, { status: 202 })) {
|
|
705
|
+
const requests = [];
|
|
706
|
+
esm_default.spyGlobal();
|
|
707
|
+
for (const gateway of [GATEWAY2, GATEWAY3]) esm_default.post(`begin:${gateway}/`, async (callLog) => {
|
|
708
|
+
const request = callLog.request;
|
|
709
|
+
const captured = {
|
|
710
|
+
url: request.url,
|
|
711
|
+
headers: new Headers(request.headers),
|
|
712
|
+
body: await request.text()
|
|
713
|
+
};
|
|
714
|
+
requests.push(captured);
|
|
715
|
+
return respond(captured);
|
|
716
|
+
});
|
|
717
|
+
return requests;
|
|
718
|
+
}
|
|
719
|
+
test("Federation.fetch() signs forwarded portable inbox deliveries with gateway keys", async (t) => {
|
|
720
|
+
const keyOrders = [[
|
|
721
|
+
"RSA key first",
|
|
722
|
+
[rsaGatewayKeyPair, otherKeyPair],
|
|
723
|
+
"#main-key"
|
|
724
|
+
], [
|
|
725
|
+
"Ed25519 key first",
|
|
726
|
+
[otherKeyPair, rsaGatewayKeyPair],
|
|
727
|
+
"#key-2"
|
|
728
|
+
]];
|
|
729
|
+
for (const [name, aliceKeyPairs, fragment] of keyOrders) await t.step(name, async () => {
|
|
730
|
+
const requests = captureGatewayRequests();
|
|
731
|
+
try {
|
|
732
|
+
const kv = new MemoryKvStore();
|
|
733
|
+
const { federation, received } = setup({
|
|
734
|
+
kv,
|
|
735
|
+
aliceKeyPairs,
|
|
736
|
+
mapPortableActorId: mapAlice,
|
|
737
|
+
options: { allowPrivateAddress: true }
|
|
738
|
+
});
|
|
739
|
+
const json = await signedFollow();
|
|
740
|
+
assertEquals((await federation.fetch(post(inboxUrl(), json), { contextData: void 0 })).status, 202);
|
|
741
|
+
assertEquals(received.length, 1);
|
|
742
|
+
assertEquals(requests.map((r) => r.url).sort(), [inboxUrl(did, "/users/alice/inbox", GATEWAY3), inboxUrl(did, "/users/alice/inbox", GATEWAY2)]);
|
|
743
|
+
const loader = await createGatewayLoader(aliceKeyPairs);
|
|
744
|
+
for (const request of requests) {
|
|
745
|
+
await assertSignedBy(request, compatibleAliceId() + fragment, loader);
|
|
746
|
+
assertEquals(JSON.parse(request.body), json);
|
|
747
|
+
}
|
|
748
|
+
assertEquals(await kv.get([
|
|
749
|
+
"_fedify",
|
|
750
|
+
"httpMessageSignaturesSpec",
|
|
751
|
+
GATEWAY2
|
|
752
|
+
]), "rfc9421");
|
|
753
|
+
} finally {
|
|
754
|
+
esm_default.hardReset();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
test("Federation.fetch() signs queued forwarded portable inbox deliveries with gateway keys", async () => {
|
|
759
|
+
const queue = new RecordingQueue();
|
|
760
|
+
const aliceKeyPairs = [otherKeyPair, rsaGatewayKeyPair];
|
|
761
|
+
const { federation } = setup({
|
|
762
|
+
queue,
|
|
763
|
+
aliceKeyPairs,
|
|
764
|
+
mapPortableActorId: mapAlice,
|
|
765
|
+
options: { allowPrivateAddress: true }
|
|
766
|
+
});
|
|
767
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
768
|
+
assertEquals(queue.outbox.length, 2);
|
|
769
|
+
const expectedKeyId = `${compatibleAliceId()}#key-2`;
|
|
770
|
+
for (const message of queue.outbox) {
|
|
771
|
+
assertEquals(message.keys.map((k) => k.keyId), [expectedKeyId]);
|
|
772
|
+
assertEquals(message.keys[0].privateKey.kty, "RSA");
|
|
773
|
+
}
|
|
774
|
+
const requests = captureGatewayRequests();
|
|
775
|
+
try {
|
|
776
|
+
for (const message of queue.outbox) await federation.processQueuedTask(void 0, message);
|
|
777
|
+
} finally {
|
|
778
|
+
esm_default.hardReset();
|
|
779
|
+
}
|
|
780
|
+
assertEquals(requests.length, 2);
|
|
781
|
+
const loader = await createGatewayLoader(aliceKeyPairs);
|
|
782
|
+
for (const request of requests) await assertSignedBy(request, expectedKeyId, loader);
|
|
783
|
+
});
|
|
784
|
+
test("Federation.fetch() forwards portable inbox deliveries unsigned without gateway keys", async (t) => {
|
|
785
|
+
const otherActorId = parseIri(`ap+ef61://${otherDid}/users/alice`);
|
|
786
|
+
const cases = [
|
|
787
|
+
["without a mapper", { aliceKeyPairs: [rsaGatewayKeyPair] }],
|
|
788
|
+
["the mapper returns null", {
|
|
789
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
790
|
+
mapPortableActorId: () => null
|
|
791
|
+
}],
|
|
792
|
+
["the mapper returns another actor", {
|
|
793
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
794
|
+
mapPortableActorId: () => otherActorId
|
|
795
|
+
}],
|
|
796
|
+
["the mapper returns another actor of the same DID", {
|
|
797
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
798
|
+
mapPortableActorId: () => parseIri(`ap+ef61://${did}/users/bob`)
|
|
799
|
+
}],
|
|
800
|
+
["without a key pairs dispatcher", {
|
|
801
|
+
keyPairs: false,
|
|
802
|
+
mapPortableActorId: mapAlice
|
|
803
|
+
}],
|
|
804
|
+
["without key pairs", { mapPortableActorId: mapAlice }],
|
|
805
|
+
["without an RSA key", {
|
|
806
|
+
aliceKeyPairs: [otherKeyPair],
|
|
807
|
+
mapPortableActorId: mapAlice
|
|
808
|
+
}]
|
|
809
|
+
];
|
|
810
|
+
for (const [name, options] of cases) await t.step(name, async () => {
|
|
811
|
+
const queue = new RecordingQueue();
|
|
812
|
+
const { federation, received } = setup({
|
|
813
|
+
...options,
|
|
814
|
+
queue
|
|
815
|
+
});
|
|
816
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
817
|
+
assertEquals(received.length, 1);
|
|
818
|
+
assertEquals(queue.outbox.length, 2);
|
|
819
|
+
for (const message of queue.outbox) assertEquals(message.keys, []);
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
test("Federation.fetch() signs forwarded portable inbox deliveries if the mapper returns an equivalent ID", async (t) => {
|
|
823
|
+
const ids = [
|
|
824
|
+
["ap: URI", parseIri(`ap://${did}/users/alice`)],
|
|
825
|
+
["percent-encoded DID", parseIri(`ap+ef61://${did.replaceAll(":", "%3A")}/users/alice`)],
|
|
826
|
+
["compatible identifier", new URL(compatibleAliceId(GATEWAY2))]
|
|
827
|
+
];
|
|
828
|
+
for (const [name, id] of ids) await t.step(name, async () => {
|
|
829
|
+
const queue = new RecordingQueue();
|
|
830
|
+
const { federation } = setup({
|
|
831
|
+
queue,
|
|
832
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
833
|
+
mapPortableActorId: (identifier) => identifier === "alice" ? id : null
|
|
834
|
+
});
|
|
835
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
836
|
+
assertEquals(queue.outbox.length, 2);
|
|
837
|
+
for (const message of queue.outbox) assertEquals(message.keys.map((k) => k.keyId), [`${compatibleAliceId()}#main-key`]);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
test("Federation.fetch() signs forwarded portable inbox deliveries with the gateway keys of the canonical origin", async () => {
|
|
841
|
+
const queue = new RecordingQueue();
|
|
842
|
+
const { federation } = setup({
|
|
843
|
+
queue,
|
|
844
|
+
gateways: [
|
|
845
|
+
GATEWAY2,
|
|
846
|
+
LOCAL,
|
|
847
|
+
GATEWAY3
|
|
848
|
+
],
|
|
849
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
850
|
+
mapPortableActorId: mapAlice,
|
|
851
|
+
options: { origin: GATEWAY2 }
|
|
852
|
+
});
|
|
853
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
854
|
+
assertEquals(queue.outbox.length, 1);
|
|
855
|
+
assertEquals(queue.outbox[0].keys.map((k) => k.keyId), [`${compatibleAliceId(GATEWAY2)}#main-key`]);
|
|
856
|
+
});
|
|
857
|
+
test("Federation.fetch() double-knocks forwarded portable inbox deliveries", async (t) => {
|
|
858
|
+
await t.step("firstKnock", async () => {
|
|
859
|
+
const requests = captureGatewayRequests();
|
|
860
|
+
try {
|
|
861
|
+
const { federation } = setup({
|
|
862
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
863
|
+
mapPortableActorId: mapAlice,
|
|
864
|
+
options: {
|
|
865
|
+
allowPrivateAddress: true,
|
|
866
|
+
firstKnock: "draft-cavage-http-signatures-12"
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
870
|
+
assertEquals(requests.length, 2);
|
|
871
|
+
for (const request of requests) {
|
|
872
|
+
assert(request.headers.has("Signature"));
|
|
873
|
+
assertFalse(request.headers.has("Signature-Input"));
|
|
874
|
+
}
|
|
875
|
+
} finally {
|
|
876
|
+
esm_default.hardReset();
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
await t.step("falls back to the other spec", async () => {
|
|
880
|
+
const requests = captureGatewayRequests((request) => new Response(null, { status: request.headers.has("Signature-Input") ? 401 : 202 }));
|
|
881
|
+
try {
|
|
882
|
+
const kv = new MemoryKvStore();
|
|
883
|
+
const { federation } = setup({
|
|
884
|
+
kv,
|
|
885
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
886
|
+
mapPortableActorId: mapAlice,
|
|
887
|
+
options: { allowPrivateAddress: true }
|
|
888
|
+
});
|
|
889
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
890
|
+
assertEquals(requests.length, 4);
|
|
891
|
+
const loader = await createGatewayLoader([rsaGatewayKeyPair]);
|
|
892
|
+
for (const request of requests) {
|
|
893
|
+
if (request.headers.has("Signature-Input")) continue;
|
|
894
|
+
await assertSignedBy(request, `${compatibleAliceId()}#main-key`, loader);
|
|
895
|
+
}
|
|
896
|
+
assertEquals(await kv.get([
|
|
897
|
+
"_fedify",
|
|
898
|
+
"httpMessageSignaturesSpec",
|
|
899
|
+
GATEWAY2
|
|
900
|
+
]), "draft-cavage-http-signatures-12");
|
|
901
|
+
} finally {
|
|
902
|
+
esm_default.hardReset();
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
test("A Fedify gateway accepts signed forwarded portable inbox deliveries", async () => {
|
|
907
|
+
const requests = captureGatewayRequests();
|
|
908
|
+
let forwarded;
|
|
909
|
+
try {
|
|
910
|
+
const { federation } = setup({
|
|
911
|
+
aliceKeyPairs: [rsaGatewayKeyPair],
|
|
912
|
+
mapPortableActorId: mapAlice,
|
|
913
|
+
options: { allowPrivateAddress: true }
|
|
914
|
+
});
|
|
915
|
+
assertEquals((await federation.fetch(post(inboxUrl(), await signedFollow()), { contextData: void 0 })).status, 202);
|
|
916
|
+
forwarded = requests.find((r) => r.url.startsWith(`${GATEWAY2}/`));
|
|
917
|
+
} finally {
|
|
918
|
+
esm_default.hardReset();
|
|
919
|
+
}
|
|
920
|
+
await assertSignedBy(forwarded, `${compatibleAliceId()}#main-key`, await createGatewayLoader([rsaGatewayKeyPair]));
|
|
921
|
+
const queue = new RecordingQueue();
|
|
922
|
+
const { federation, received } = setup({
|
|
923
|
+
queue,
|
|
924
|
+
options: { origin: GATEWAY2 }
|
|
925
|
+
});
|
|
926
|
+
assertEquals((await federation.fetch(toRequest(forwarded), { contextData: void 0 })).status, 202);
|
|
927
|
+
assertEquals(received.length, 1);
|
|
928
|
+
assertEquals(received[0].recipient, "alice");
|
|
929
|
+
assertEquals(queue.outbox.map((m) => m.inbox).sort(), [inboxUrl(did, "/users/alice/inbox", LOCAL), inboxUrl(did, "/users/alice/inbox", GATEWAY3)]);
|
|
930
|
+
});
|
|
931
|
+
function forwardingParameters(overrides = {}) {
|
|
932
|
+
const activityId = parseIri(`ap+ef61://${did}/follows/${++activityCounter}`);
|
|
933
|
+
return {
|
|
934
|
+
recipient: {
|
|
935
|
+
actorId: aliceId,
|
|
936
|
+
inboxId: parseIri(`ap+ef61://${did}/users/alice/inbox`),
|
|
937
|
+
canonicalInboxId: `ap://${did}/users/alice/inbox`,
|
|
938
|
+
gateways: [new URL(LOCAL), new URL(GATEWAY2)]
|
|
939
|
+
},
|
|
940
|
+
activity: { id: formatIri(activityId) },
|
|
941
|
+
activityId,
|
|
942
|
+
activityType: "https://www.w3.org/ns/activitystreams#Follow",
|
|
943
|
+
excludedOrigins: [LOCAL],
|
|
944
|
+
baseUrl: LOCAL,
|
|
945
|
+
kv: new MemoryKvStore(),
|
|
946
|
+
kvPrefix: ["_fedify", "portableInboxForwarding"],
|
|
947
|
+
allowPrivateAddress: true,
|
|
948
|
+
...overrides
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
const gatewayKey = {
|
|
952
|
+
keyId: new URL(`${compatibleAliceId()}#main-key`),
|
|
953
|
+
privateKey: rsaGatewayKeyPair.privateKey
|
|
954
|
+
};
|
|
955
|
+
test("forwardPortableInboxActivity() gets keys only when it forwards", async () => {
|
|
956
|
+
let calls = 0;
|
|
957
|
+
const parameters = forwardingParameters({
|
|
958
|
+
outboxQueue: new RecordingQueue(),
|
|
959
|
+
getKeys: () => {
|
|
960
|
+
calls++;
|
|
961
|
+
return Promise.resolve([gatewayKey]);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
assertEquals((await forwardPortableInboxActivity(parameters)).length, 1);
|
|
965
|
+
assertEquals(calls, 1);
|
|
966
|
+
assertEquals(await forwardPortableInboxActivity(parameters), []);
|
|
967
|
+
assertEquals(calls, 1);
|
|
968
|
+
});
|
|
969
|
+
test("forwardPortableInboxActivity() forwards unsigned if it cannot get keys", async (t) => {
|
|
970
|
+
const nonExtractable = await crypto.subtle.generateKey({
|
|
971
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
972
|
+
modulusLength: 2048,
|
|
973
|
+
publicExponent: new Uint8Array([
|
|
974
|
+
1,
|
|
975
|
+
0,
|
|
976
|
+
1
|
|
977
|
+
]),
|
|
978
|
+
hash: "SHA-256"
|
|
979
|
+
}, false, ["sign", "verify"]);
|
|
980
|
+
const getKeysCases = [["getKeys() rejects", () => Promise.reject(/* @__PURE__ */ new Error("Unavailable."))], ["the key is not extractable", () => Promise.resolve([{
|
|
981
|
+
keyId: gatewayKey.keyId,
|
|
982
|
+
privateKey: nonExtractable.privateKey
|
|
983
|
+
}])]];
|
|
984
|
+
for (const [name, getKeys] of getKeysCases) {
|
|
985
|
+
await t.step(`${name} (queued)`, async () => {
|
|
986
|
+
const queue = new RecordingQueue();
|
|
987
|
+
assertEquals((await forwardPortableInboxActivity(forwardingParameters({
|
|
988
|
+
outboxQueue: queue,
|
|
989
|
+
getKeys
|
|
990
|
+
}))).length, 1);
|
|
991
|
+
assertEquals(queue.outbox.length, 1);
|
|
992
|
+
assertEquals(queue.outbox[0].keys, []);
|
|
993
|
+
});
|
|
994
|
+
await t.step(`${name} (immediate)`, async () => {
|
|
995
|
+
const requests = captureGatewayRequests();
|
|
996
|
+
try {
|
|
997
|
+
assertEquals((await forwardPortableInboxActivity(forwardingParameters({ getKeys }))).length, 1);
|
|
998
|
+
} finally {
|
|
999
|
+
esm_default.hardReset();
|
|
1000
|
+
}
|
|
1001
|
+
assertEquals(requests.length, 1);
|
|
1002
|
+
assertEquals(signatureKeyId(requests[0].headers), void 0);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
test("forwardPortableInboxActivity() does not wait for slow keys", async () => {
|
|
1007
|
+
let release = () => {};
|
|
1008
|
+
const pending = new Promise((resolve) => release = resolve);
|
|
1009
|
+
const requests = captureGatewayRequests();
|
|
1010
|
+
try {
|
|
1011
|
+
const started = performance.now();
|
|
1012
|
+
const forwarded = await forwardPortableInboxActivity(forwardingParameters({
|
|
1013
|
+
options: resolvePortableInboxForwardingOptions({ deadline: { milliseconds: 10 } }),
|
|
1014
|
+
getKeys: async () => {
|
|
1015
|
+
await pending;
|
|
1016
|
+
throw new Error("Unavailable.");
|
|
1017
|
+
}
|
|
1018
|
+
}));
|
|
1019
|
+
assert(performance.now() - started < 5e3);
|
|
1020
|
+
assertEquals(forwarded.length, 1);
|
|
1021
|
+
assertEquals(requests.length, 0);
|
|
1022
|
+
release();
|
|
1023
|
+
for (let i = 0; i < 100 && requests.length < 1; i++) await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1024
|
+
assertEquals(requests.length, 1);
|
|
1025
|
+
assertEquals(signatureKeyId(requests[0].headers), void 0);
|
|
1026
|
+
} finally {
|
|
1027
|
+
esm_default.hardReset();
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
523
1030
|
test("Context.getPortableInboxUri()", async (t) => {
|
|
524
1031
|
const { federation } = setup();
|
|
525
1032
|
await t.step("Context", () => {
|