@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
5
|
+
import { a as fetchKeyDetailed, i as fetchKey } from "../key-BspPiXqn.mjs";
|
|
6
|
+
import { h as verifyJsonLd, p as signJsonLd } from "../ld-CzaP-hJl.mjs";
|
|
7
|
+
import { d as verifyProof, r as createProof } from "../proof-Dxy6ByqI.mjs";
|
|
8
|
+
import { n as resolvePortableActorKey } from "../portable-key-BIMLrAP4.mjs";
|
|
9
|
+
import { f as verifyRequest, l as signRequest, p as verifyRequestDetailed } from "../http-siciWkmx.mjs";
|
|
10
|
+
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
|
|
11
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-DYgfTFHy.mjs";
|
|
12
|
+
import { t as KvKeyCache } from "../keycache-Da8A8mZa.mjs";
|
|
13
|
+
import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
|
|
14
|
+
import { FetchError, encodeMultibase, exportDidKey, exportSpki, parseIri, preloadedContexts } from "@fedify/vocab-runtime";
|
|
15
|
+
import { test } from "@fedify/fixture";
|
|
16
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
17
|
+
import serialize from "json-canon";
|
|
18
|
+
//#region src/sig/portable-key.test.ts
|
|
19
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
20
|
+
const didKeyId = `${did}#${did.slice(8)}`;
|
|
21
|
+
const otherKeyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
|
|
22
|
+
const otherDid = await exportDidKey(otherKeyPair.publicKey);
|
|
23
|
+
const actorId = `ap+ef61://${did}/actor`;
|
|
24
|
+
const gw1 = "https://gw1.example";
|
|
25
|
+
const gw2 = "https://gw2.example";
|
|
26
|
+
function compatibleId(gateway, id = actorId) {
|
|
27
|
+
return `${gateway}/.well-known/apgateway/${id.replace(/^ap\+ef61:\/\//, "")}`;
|
|
28
|
+
}
|
|
29
|
+
const keyId = new URL(`${compatibleId(gw1)}#main-key`);
|
|
30
|
+
const gatewayPublicKey = rsaPublicKey2.publicKey;
|
|
31
|
+
const contextLoader = (url) => {
|
|
32
|
+
const document = preloadedContexts[url];
|
|
33
|
+
if (document == null) return Promise.reject(/* @__PURE__ */ new Error(`No context: ${url}`));
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
contextUrl: null,
|
|
36
|
+
documentUrl: url,
|
|
37
|
+
document
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
function createLoader(responses) {
|
|
41
|
+
const fetched = [];
|
|
42
|
+
const loader = (url) => {
|
|
43
|
+
fetched.push(url);
|
|
44
|
+
const document = responses[url.replace(/#.*$/, "")];
|
|
45
|
+
if (document == null) return Promise.reject(new FetchError(url, "HTTP 404", new Response(null, { status: 404 })));
|
|
46
|
+
return Promise.resolve({
|
|
47
|
+
contextUrl: null,
|
|
48
|
+
documentUrl: url,
|
|
49
|
+
document: structuredClone(document)
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
return Object.assign(loader, { fetched });
|
|
53
|
+
}
|
|
54
|
+
async function sign(document, privateKey = ed25519PrivateKey, verificationMethod = didKeyId, proofOptions = {}) {
|
|
55
|
+
const proofConfig = {
|
|
56
|
+
"@context": document["@context"],
|
|
57
|
+
type: "DataIntegrityProof",
|
|
58
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
59
|
+
verificationMethod,
|
|
60
|
+
proofPurpose: "assertionMethod",
|
|
61
|
+
created: "2023-02-24T23:36:38Z",
|
|
62
|
+
...proofOptions
|
|
63
|
+
};
|
|
64
|
+
const encoder = new TextEncoder();
|
|
65
|
+
const proofDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)));
|
|
66
|
+
const messageDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(document)));
|
|
67
|
+
const digest = /* @__PURE__ */ new Uint8Array(64);
|
|
68
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
|
69
|
+
digest.set(new Uint8Array(messageDigest), 32);
|
|
70
|
+
const signature = await crypto.subtle.sign("Ed25519", privateKey, digest);
|
|
71
|
+
return {
|
|
72
|
+
...document,
|
|
73
|
+
proof: {
|
|
74
|
+
...proofConfig,
|
|
75
|
+
proofValue: new TextDecoder().decode(encodeMultibase("base58btc", new Uint8Array(signature)))
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function actorJson(options = {}) {
|
|
80
|
+
const id = parseIri(options.id ?? actorId);
|
|
81
|
+
const key = options.key ?? keyId;
|
|
82
|
+
const json = await new Person({
|
|
83
|
+
id,
|
|
84
|
+
preferredUsername: "alice",
|
|
85
|
+
inbox: parseIri(`${options.id ?? actorId}/inbox`),
|
|
86
|
+
gateways: (options.gateways ?? [gw1, gw2]).map((g) => new URL(g)),
|
|
87
|
+
assertionMethods: options.assertionMethod === false ? [] : [new Multikey({
|
|
88
|
+
id: key,
|
|
89
|
+
controller: id,
|
|
90
|
+
publicKey: gatewayPublicKey
|
|
91
|
+
})],
|
|
92
|
+
publicKeys: options.publicKey === null ? [] : [new CryptographicKey({
|
|
93
|
+
id: key,
|
|
94
|
+
owner: id,
|
|
95
|
+
publicKey: options.publicKey ?? gatewayPublicKey
|
|
96
|
+
})]
|
|
97
|
+
}).toJsonLd({
|
|
98
|
+
format: "compact",
|
|
99
|
+
contextLoader
|
|
100
|
+
});
|
|
101
|
+
const context = json["@context"];
|
|
102
|
+
if (!context.includes("https://w3id.org/security/data-integrity/v1")) json["@context"] = [...context, "https://w3id.org/security/data-integrity/v1"];
|
|
103
|
+
return json;
|
|
104
|
+
}
|
|
105
|
+
async function signedRequest(spec, key = keyId) {
|
|
106
|
+
return await signRequest(new Request("https://recipient.example/inbox", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: "{}",
|
|
109
|
+
headers: { "Content-Type": "application/activity+json" }
|
|
110
|
+
}), rsaPrivateKey2, key, { spec });
|
|
111
|
+
}
|
|
112
|
+
test("verifyRequest() accepts gateway keys vouched for by portable actors", async () => {
|
|
113
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
|
|
114
|
+
for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) {
|
|
115
|
+
const key = await verifyRequest(await signedRequest(spec), {
|
|
116
|
+
documentLoader,
|
|
117
|
+
contextLoader,
|
|
118
|
+
spec
|
|
119
|
+
});
|
|
120
|
+
ok(key != null, spec);
|
|
121
|
+
strictEqual(key.id?.href, keyId.href);
|
|
122
|
+
strictEqual(key.ownerId?.href, parseIri(actorId).href);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
test("verifyRequest() accepts gateway keys only in assertionMethod", async () => {
|
|
126
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson({ publicKey: null })) });
|
|
127
|
+
strictEqual((await verifyRequest(await signedRequest(), {
|
|
128
|
+
documentLoader,
|
|
129
|
+
contextLoader
|
|
130
|
+
}))?.id?.href, keyId.href);
|
|
131
|
+
});
|
|
132
|
+
test("verifyRequest() rejects gateway keys that portable actors do not vouch for", async () => {
|
|
133
|
+
const unsigned = await actorJson();
|
|
134
|
+
const cases = {
|
|
135
|
+
"an unsigned actor document": unsigned,
|
|
136
|
+
"an actor document signed by another DID": await sign(unsigned, otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`),
|
|
137
|
+
"an unlisted gateway": await sign(await actorJson({ gateways: [gw2] })),
|
|
138
|
+
"a key missing from both assertionMethod and publicKey": await sign(await actorJson({
|
|
139
|
+
assertionMethod: false,
|
|
140
|
+
publicKey: null
|
|
141
|
+
})),
|
|
142
|
+
"a key referred to by URL in assertionMethod, embedded in publicKey": await sign({
|
|
143
|
+
...await actorJson({ assertionMethod: false }),
|
|
144
|
+
assertionMethod: [keyId.href]
|
|
145
|
+
}),
|
|
146
|
+
"a key only in publicKey without an owner": await sign(withoutPublicKeyOwner(await actorJson({ assertionMethod: false }))),
|
|
147
|
+
"two keys with the same ID in publicKey": await sign(await duplicatePublicKey(await actorJson({ assertionMethod: false }))),
|
|
148
|
+
"a key referred to by URL": await sign({
|
|
149
|
+
...await actorJson({
|
|
150
|
+
publicKey: null,
|
|
151
|
+
assertionMethod: false
|
|
152
|
+
}),
|
|
153
|
+
assertionMethod: [keyId.href]
|
|
154
|
+
}),
|
|
155
|
+
"a publicKey entry with other key material": await sign(await actorJson({ publicKey: rsaPublicKey3.publicKey })),
|
|
156
|
+
"a key ID for another actor": await sign(await actorJson({ key: new URL(`${compatibleId(gw1, `${actorId}/other`)}#main-key`) }))
|
|
157
|
+
};
|
|
158
|
+
for (const [name, document] of Object.entries(cases)) {
|
|
159
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: document });
|
|
160
|
+
strictEqual(await verifyRequest(await signedRequest(), {
|
|
161
|
+
documentLoader,
|
|
162
|
+
contextLoader
|
|
163
|
+
}), null, name);
|
|
164
|
+
}
|
|
165
|
+
const otherActorKey = new URL(`${compatibleId(gw1, `${actorId}/other`)}#main-key`);
|
|
166
|
+
const documentLoader = createLoader({ [compatibleId(gw1, `${actorId}/other`)]: await sign(await actorJson()) });
|
|
167
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, otherActorKey), {
|
|
168
|
+
documentLoader,
|
|
169
|
+
contextLoader
|
|
170
|
+
}), null);
|
|
171
|
+
});
|
|
172
|
+
function withoutPublicKeyOwner(json) {
|
|
173
|
+
const { owner: _, ...publicKey } = json.publicKey;
|
|
174
|
+
return {
|
|
175
|
+
...json,
|
|
176
|
+
publicKey
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function duplicatePublicKey(json) {
|
|
180
|
+
const publicKey = json.publicKey;
|
|
181
|
+
return {
|
|
182
|
+
...json,
|
|
183
|
+
publicKey: [publicKey, {
|
|
184
|
+
...publicKey,
|
|
185
|
+
publicKeyPem: await exportSpki(rsaPublicKey3.publicKey)
|
|
186
|
+
}]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
test("verifyRequest() accepts gateway keys referred to by URL in publicKey", async () => {
|
|
190
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign({
|
|
191
|
+
...await actorJson({ publicKey: null }),
|
|
192
|
+
publicKey: keyId.href
|
|
193
|
+
}) });
|
|
194
|
+
strictEqual((await verifyRequest(await signedRequest(), {
|
|
195
|
+
documentLoader,
|
|
196
|
+
contextLoader
|
|
197
|
+
}))?.id?.href, keyId.href);
|
|
198
|
+
});
|
|
199
|
+
test("verifyRequest() accepts gateway keys only in publicKey", async () => {
|
|
200
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson({ assertionMethod: false })) });
|
|
201
|
+
const key = await verifyRequest(await signedRequest(), {
|
|
202
|
+
documentLoader,
|
|
203
|
+
contextLoader
|
|
204
|
+
});
|
|
205
|
+
strictEqual(key?.id?.href, keyId.href);
|
|
206
|
+
strictEqual(key?.ownerId?.href, parseIri(actorId).href);
|
|
207
|
+
});
|
|
208
|
+
test("verifyRequest() resolves gateway keys of compatible-ID actors", async () => {
|
|
209
|
+
const id = compatibleId(gw1);
|
|
210
|
+
const signed = await sign(await actorJson({
|
|
211
|
+
id,
|
|
212
|
+
gateways: [gw1]
|
|
213
|
+
}));
|
|
214
|
+
let key = await verifyRequest(await signedRequest(), {
|
|
215
|
+
documentLoader: createLoader({ [id]: signed }),
|
|
216
|
+
contextLoader
|
|
217
|
+
});
|
|
218
|
+
strictEqual(key?.id?.href, keyId.href);
|
|
219
|
+
strictEqual(key?.ownerId?.href, id);
|
|
220
|
+
key = await verifyRequest(await signedRequest(), {
|
|
221
|
+
documentLoader: createLoader({ [id]: await sign(await actorJson({
|
|
222
|
+
id,
|
|
223
|
+
gateways: [gw1],
|
|
224
|
+
assertionMethod: false
|
|
225
|
+
})) }),
|
|
226
|
+
contextLoader
|
|
227
|
+
});
|
|
228
|
+
strictEqual(key?.id?.href, keyId.href);
|
|
229
|
+
const evil = "https://evil.example";
|
|
230
|
+
const evilId = compatibleId(evil);
|
|
231
|
+
const evilKeyId = new URL(`${evilId}#main-key`);
|
|
232
|
+
const unsigned = await actorJson({
|
|
233
|
+
id: evilId,
|
|
234
|
+
gateways: [evil],
|
|
235
|
+
key: evilKeyId
|
|
236
|
+
});
|
|
237
|
+
const documentLoader = createLoader({ [evilId]: unsigned });
|
|
238
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, evilKeyId), {
|
|
239
|
+
documentLoader,
|
|
240
|
+
contextLoader
|
|
241
|
+
}), null);
|
|
242
|
+
strictEqual(await getKeyOwner(evilKeyId, {
|
|
243
|
+
documentLoader,
|
|
244
|
+
contextLoader
|
|
245
|
+
}), null);
|
|
246
|
+
strictEqual(await verifyRequest(await signedRequest(), {
|
|
247
|
+
documentLoader: createLoader({ [id]: await sign(await actorJson({
|
|
248
|
+
id,
|
|
249
|
+
gateways: [gw1]
|
|
250
|
+
}), otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`) }),
|
|
251
|
+
contextLoader
|
|
252
|
+
}), null);
|
|
253
|
+
});
|
|
254
|
+
test("keys at ordinary URLs cannot belong to portable actors", async () => {
|
|
255
|
+
const evil = "https://evil.example";
|
|
256
|
+
const evilActorId = compatibleId(evil);
|
|
257
|
+
const evilKeyId = new URL(`${evil}/keys/1`);
|
|
258
|
+
const options = {
|
|
259
|
+
documentLoader: createLoader({
|
|
260
|
+
[evilKeyId.href]: {
|
|
261
|
+
"@context": "https://w3id.org/security/v1",
|
|
262
|
+
id: evilKeyId.href,
|
|
263
|
+
type: "Key",
|
|
264
|
+
owner: evilActorId,
|
|
265
|
+
publicKeyPem: await exportSpki(gatewayPublicKey)
|
|
266
|
+
},
|
|
267
|
+
[evilActorId]: await actorJson({
|
|
268
|
+
id: evilActorId,
|
|
269
|
+
gateways: [evil],
|
|
270
|
+
key: evilKeyId,
|
|
271
|
+
assertionMethod: false
|
|
272
|
+
})
|
|
273
|
+
}),
|
|
274
|
+
contextLoader
|
|
275
|
+
};
|
|
276
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, evilKeyId), options), null);
|
|
277
|
+
strictEqual((await fetchKey(evilKeyId, CryptographicKey, options)).key, null);
|
|
278
|
+
strictEqual(await getKeyOwner(evilKeyId, options), null);
|
|
279
|
+
const key = new CryptographicKey({
|
|
280
|
+
id: evilKeyId,
|
|
281
|
+
owner: new URL(evilActorId),
|
|
282
|
+
publicKey: gatewayPublicKey
|
|
283
|
+
});
|
|
284
|
+
strictEqual(await getKeyOwner(key, options), null);
|
|
285
|
+
ok(!await doesActorOwnKey(new Create({
|
|
286
|
+
id: new URL(`${evil}/activities/1`),
|
|
287
|
+
actor: new URL(evilActorId)
|
|
288
|
+
}), key, options));
|
|
289
|
+
const actorKeyId = new URL(`${evil}/users/alice#main-key`);
|
|
290
|
+
for (const document of [await actorJson({
|
|
291
|
+
id: evilActorId,
|
|
292
|
+
gateways: [evil],
|
|
293
|
+
key: actorKeyId
|
|
294
|
+
}), withoutPublicKeyOwner(await actorJson({
|
|
295
|
+
id: evilActorId,
|
|
296
|
+
gateways: [evil],
|
|
297
|
+
key: actorKeyId,
|
|
298
|
+
assertionMethod: false
|
|
299
|
+
}))]) {
|
|
300
|
+
const actorOptions = {
|
|
301
|
+
documentLoader: createLoader({ [`${evil}/users/alice`]: document }),
|
|
302
|
+
contextLoader
|
|
303
|
+
};
|
|
304
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, actorKeyId), actorOptions), null);
|
|
305
|
+
strictEqual((await fetchKey(actorKeyId, CryptographicKey, actorOptions)).key, null);
|
|
306
|
+
strictEqual(await getKeyOwner(actorKeyId, actorOptions), null);
|
|
307
|
+
}
|
|
308
|
+
const ownerKeyId = new URL(`${evil}/keys/2`);
|
|
309
|
+
const ownerOptions = {
|
|
310
|
+
documentLoader: createLoader({
|
|
311
|
+
[ownerKeyId.href]: {
|
|
312
|
+
"@context": "https://w3id.org/security/v1",
|
|
313
|
+
id: ownerKeyId.href,
|
|
314
|
+
type: "Key",
|
|
315
|
+
owner: `${evil}/users/bob`,
|
|
316
|
+
publicKeyPem: await exportSpki(gatewayPublicKey)
|
|
317
|
+
},
|
|
318
|
+
[`${evil}/users/bob`]: await actorJson({
|
|
319
|
+
id: evilActorId,
|
|
320
|
+
gateways: [evil],
|
|
321
|
+
key: ownerKeyId,
|
|
322
|
+
assertionMethod: false
|
|
323
|
+
})
|
|
324
|
+
}),
|
|
325
|
+
contextLoader
|
|
326
|
+
};
|
|
327
|
+
strictEqual(await getKeyOwner(ownerKeyId, ownerOptions), null);
|
|
328
|
+
strictEqual(await getKeyOwner(new CryptographicKey({
|
|
329
|
+
id: ownerKeyId,
|
|
330
|
+
owner: new URL(`${evil}/users/bob`),
|
|
331
|
+
publicKey: gatewayPublicKey
|
|
332
|
+
}), ownerOptions), null);
|
|
333
|
+
const keyCache = {
|
|
334
|
+
get: () => Promise.resolve(key),
|
|
335
|
+
set: () => Promise.resolve()
|
|
336
|
+
};
|
|
337
|
+
strictEqual((await fetchKey(evilKeyId, CryptographicKey, {
|
|
338
|
+
...options,
|
|
339
|
+
keyCache
|
|
340
|
+
})).key, null);
|
|
341
|
+
});
|
|
342
|
+
const specs = ["draft-cavage-http-signatures-12", "rfc9421"];
|
|
343
|
+
function fetchKeyOf(cls, options) {
|
|
344
|
+
return cls === CryptographicKey ? fetchKey(keyId, CryptographicKey, options) : fetchKey(keyId, Multikey, options);
|
|
345
|
+
}
|
|
346
|
+
function fetchCount(loader, url = keyId) {
|
|
347
|
+
const document = url.href.replace(/#.*$/, "");
|
|
348
|
+
return loader.fetched.filter((u) => u.replace(/#.*$/, "") === document).length;
|
|
349
|
+
}
|
|
350
|
+
test("verifyRequest() caches failures to fetch compatible key IDs", async () => {
|
|
351
|
+
const unreachable = {
|
|
352
|
+
"404 Not Found": createLoader({}),
|
|
353
|
+
"a network error": (url) => Promise.reject(/* @__PURE__ */ new TypeError(`${url}`))
|
|
354
|
+
};
|
|
355
|
+
for (const spec of specs) for (const [name, inner] of Object.entries(unreachable)) {
|
|
356
|
+
const fetched = [];
|
|
357
|
+
const documentLoader = Object.assign((url) => {
|
|
358
|
+
fetched.push(url);
|
|
359
|
+
return inner(url);
|
|
360
|
+
}, { fetched });
|
|
361
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
362
|
+
const options = {
|
|
363
|
+
documentLoader,
|
|
364
|
+
contextLoader,
|
|
365
|
+
keyCache,
|
|
366
|
+
spec
|
|
367
|
+
};
|
|
368
|
+
for (let i = 0; i < 3; i++) {
|
|
369
|
+
const result = await verifyRequestDetailed(await signedRequest(spec), options);
|
|
370
|
+
ok(!result.verified, `${spec}, ${name}`);
|
|
371
|
+
strictEqual(result.reason.type, "keyFetchError", `${spec}, ${name}`);
|
|
372
|
+
}
|
|
373
|
+
strictEqual(fetchCount(documentLoader), 1, `${spec}, ${name}`);
|
|
374
|
+
for (const cls of [CryptographicKey, Multikey]) {
|
|
375
|
+
const result = await fetchKeyOf(cls, options);
|
|
376
|
+
strictEqual(result.key, null);
|
|
377
|
+
ok(result.cached, `${spec}, ${name}, ${cls.name}`);
|
|
378
|
+
}
|
|
379
|
+
strictEqual(fetchCount(documentLoader), 1, `${spec}, ${name}`);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
test("verifyRequest() caches gateway keys only for HTTP Signatures", async () => {
|
|
383
|
+
for (const spec of specs) {
|
|
384
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
|
|
385
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
386
|
+
const options = {
|
|
387
|
+
documentLoader,
|
|
388
|
+
contextLoader,
|
|
389
|
+
keyCache,
|
|
390
|
+
spec
|
|
391
|
+
};
|
|
392
|
+
for (let i = 0; i < 3; i++) strictEqual((await verifyRequest(await signedRequest(spec), options))?.ownerId?.href, parseIri(actorId).href, spec);
|
|
393
|
+
strictEqual(fetchCount(documentLoader), 1, spec);
|
|
394
|
+
const document = {
|
|
395
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
396
|
+
type: "Create",
|
|
397
|
+
id: "https://gw1.example/activities/1",
|
|
398
|
+
actor: compatibleId(gw1)
|
|
399
|
+
};
|
|
400
|
+
const { signature } = await signJsonLd(document, rsaPrivateKey2, keyId, { contextLoader });
|
|
401
|
+
ok(!await verifyJsonLd({
|
|
402
|
+
...document,
|
|
403
|
+
signature
|
|
404
|
+
}, options), spec);
|
|
405
|
+
const note = new Note({
|
|
406
|
+
id: new URL("https://gw1.example/notes/1"),
|
|
407
|
+
attribution: new URL(compatibleId(gw1)),
|
|
408
|
+
content: "Hello"
|
|
409
|
+
});
|
|
410
|
+
const proof = await createProof(note, ed25519PrivateKey, keyId, { contextLoader });
|
|
411
|
+
strictEqual(await verifyProof(await note.toJsonLd({
|
|
412
|
+
format: "compact",
|
|
413
|
+
contextLoader
|
|
414
|
+
}), proof, options), null, spec);
|
|
415
|
+
for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null, spec);
|
|
416
|
+
const fetched = fetchCount(documentLoader);
|
|
417
|
+
ok(await verifyRequest(await signedRequest(spec), options) != null, spec);
|
|
418
|
+
strictEqual(fetchCount(documentLoader), fetched, spec);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
test("verifyRequest() is not rejected by other purposes' negative entries", async () => {
|
|
422
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
|
|
423
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
424
|
+
const options = {
|
|
425
|
+
documentLoader,
|
|
426
|
+
contextLoader,
|
|
427
|
+
keyCache
|
|
428
|
+
};
|
|
429
|
+
for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null);
|
|
430
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
431
|
+
const fetched = fetchCount(documentLoader);
|
|
432
|
+
for (const cls of [CryptographicKey, Multikey]) {
|
|
433
|
+
const result = await fetchKeyOf(cls, options);
|
|
434
|
+
strictEqual(result.key, null);
|
|
435
|
+
ok(result.cached);
|
|
436
|
+
}
|
|
437
|
+
strictEqual(fetchCount(documentLoader), fetched);
|
|
438
|
+
});
|
|
439
|
+
test("fetchKey() does not share negative entries of compatible key IDs across key classes", async () => {
|
|
440
|
+
const ownerId = new URL("https://gw1.example/users/bob");
|
|
441
|
+
const multikey = new Multikey({
|
|
442
|
+
id: keyId,
|
|
443
|
+
controller: ownerId,
|
|
444
|
+
publicKey: ed25519PublicKey.publicKey
|
|
445
|
+
});
|
|
446
|
+
const documentLoader = createLoader({
|
|
447
|
+
[compatibleId(gw1)]: await multikey.toJsonLd({ contextLoader }),
|
|
448
|
+
[ownerId.href]: await new Person({
|
|
449
|
+
id: ownerId,
|
|
450
|
+
assertionMethods: [multikey]
|
|
451
|
+
}).toJsonLd({ contextLoader })
|
|
452
|
+
});
|
|
453
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
454
|
+
const options = {
|
|
455
|
+
documentLoader,
|
|
456
|
+
contextLoader,
|
|
457
|
+
keyCache
|
|
458
|
+
};
|
|
459
|
+
strictEqual((await fetchKey(keyId, CryptographicKey, options)).key, null);
|
|
460
|
+
strictEqual(await verifyRequest(await signedRequest(), options), null);
|
|
461
|
+
const result = await fetchKey(keyId, Multikey, options);
|
|
462
|
+
strictEqual(result.key?.id?.href, keyId.href);
|
|
463
|
+
ok(!result.cached);
|
|
464
|
+
const cached = await fetchKey(keyId, Multikey, options);
|
|
465
|
+
strictEqual(cached.key?.id?.href, keyId.href);
|
|
466
|
+
ok(cached.cached);
|
|
467
|
+
const other = await fetchKey(keyId, CryptographicKey, options);
|
|
468
|
+
strictEqual(other.key, null);
|
|
469
|
+
ok(other.cached);
|
|
470
|
+
});
|
|
471
|
+
test("verifyRequest() caches rejected portable actor documents", async () => {
|
|
472
|
+
for (const spec of specs) {
|
|
473
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await actorJson() });
|
|
474
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
475
|
+
const options = {
|
|
476
|
+
documentLoader,
|
|
477
|
+
contextLoader,
|
|
478
|
+
keyCache,
|
|
479
|
+
spec
|
|
480
|
+
};
|
|
481
|
+
for (let i = 0; i < 3; i++) {
|
|
482
|
+
const result = await verifyRequestDetailed(await signedRequest(spec), options);
|
|
483
|
+
ok(!result.verified, spec);
|
|
484
|
+
strictEqual(result.reason.type, "invalidSignature", spec);
|
|
485
|
+
}
|
|
486
|
+
strictEqual(fetchCount(documentLoader), 1, spec);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
test("verifyRequest() refetches a cached gateway key that fails to verify", async () => {
|
|
490
|
+
for (const spec of specs) {
|
|
491
|
+
const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
|
|
492
|
+
const documentLoader = createLoader(responses);
|
|
493
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
494
|
+
const options = {
|
|
495
|
+
documentLoader,
|
|
496
|
+
contextLoader,
|
|
497
|
+
keyCache,
|
|
498
|
+
spec
|
|
499
|
+
};
|
|
500
|
+
ok(await verifyRequest(await signedRequest(spec), options) != null);
|
|
501
|
+
responses[compatibleId(gw1)] = await sign(await actorJson({
|
|
502
|
+
publicKey: rsaPublicKey3.publicKey,
|
|
503
|
+
assertionMethod: false
|
|
504
|
+
}));
|
|
505
|
+
const rotated = async () => await signRequest(new Request("https://recipient.example/inbox", {
|
|
506
|
+
method: "POST",
|
|
507
|
+
body: "{}",
|
|
508
|
+
headers: { "Content-Type": "application/activity+json" }
|
|
509
|
+
}), rsaPrivateKey3, keyId, { spec });
|
|
510
|
+
ok(await verifyRequest(await rotated(), options) != null, spec);
|
|
511
|
+
strictEqual(fetchCount(documentLoader), 2, spec);
|
|
512
|
+
ok(await verifyRequest(await rotated(), options) != null, spec);
|
|
513
|
+
strictEqual(fetchCount(documentLoader), 2, spec);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
test("verifyRequest() drops a cached gateway key when its refetch fails", async () => {
|
|
517
|
+
for (const spec of specs) {
|
|
518
|
+
const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
|
|
519
|
+
const documentLoader = createLoader(responses);
|
|
520
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
521
|
+
const options = {
|
|
522
|
+
documentLoader,
|
|
523
|
+
contextLoader,
|
|
524
|
+
keyCache,
|
|
525
|
+
spec
|
|
526
|
+
};
|
|
527
|
+
ok(await verifyRequest(await signedRequest(spec), options) != null);
|
|
528
|
+
delete responses[compatibleId(gw1)];
|
|
529
|
+
const result = await verifyRequestDetailed(await signRequest(new Request("https://recipient.example/inbox", {
|
|
530
|
+
method: "POST",
|
|
531
|
+
body: "{}",
|
|
532
|
+
headers: { "Content-Type": "application/activity+json" }
|
|
533
|
+
}), rsaPrivateKey3, keyId, { spec }), options);
|
|
534
|
+
ok(!result.verified, spec);
|
|
535
|
+
strictEqual(result.reason.type, "keyFetchError", spec);
|
|
536
|
+
strictEqual(fetchCount(documentLoader), 2, spec);
|
|
537
|
+
const next = await verifyRequestDetailed(await signedRequest(spec), options);
|
|
538
|
+
ok(!next.verified, spec);
|
|
539
|
+
strictEqual(next.reason.type, "keyFetchError", spec);
|
|
540
|
+
strictEqual(fetchCount(documentLoader), 2, spec);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
test("verifyRequest() does not trust a cached gateway key for longer than its proof", async () => {
|
|
544
|
+
let now = Temporal.Now.instant();
|
|
545
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
|
|
546
|
+
const expires = now.add({ minutes: 30 });
|
|
547
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson(), ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
|
|
548
|
+
const options = {
|
|
549
|
+
documentLoader,
|
|
550
|
+
contextLoader,
|
|
551
|
+
keyCache
|
|
552
|
+
};
|
|
553
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
554
|
+
now = expires.subtract({ seconds: 1 });
|
|
555
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
556
|
+
strictEqual(fetchCount(documentLoader), 1);
|
|
557
|
+
now = expires;
|
|
558
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
559
|
+
strictEqual(fetchCount(documentLoader), 2);
|
|
560
|
+
});
|
|
561
|
+
test("verifyRequest() looks up a cached gateway key again after an hour", async () => {
|
|
562
|
+
let now = Temporal.Now.instant();
|
|
563
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
|
|
564
|
+
const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
|
|
565
|
+
const documentLoader = createLoader(responses);
|
|
566
|
+
const options = {
|
|
567
|
+
documentLoader,
|
|
568
|
+
contextLoader,
|
|
569
|
+
keyCache
|
|
570
|
+
};
|
|
571
|
+
const start = now;
|
|
572
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
573
|
+
now = start.add({ minutes: 59 });
|
|
574
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
575
|
+
strictEqual(fetchCount(documentLoader), 1);
|
|
576
|
+
responses[compatibleId(gw1)] = await sign(await actorJson({ gateways: [gw2] }));
|
|
577
|
+
now = start.add({ hours: 1 });
|
|
578
|
+
strictEqual(await verifyRequest(await signedRequest(), options), null);
|
|
579
|
+
strictEqual(fetchCount(documentLoader), 2);
|
|
580
|
+
});
|
|
581
|
+
test("verifyRequest() rejects and caches gateway keys of expired documents", async () => {
|
|
582
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson(), ed25519PrivateKey, didKeyId, { expires: "2000-01-01T00:00:00Z" }) });
|
|
583
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
584
|
+
const options = {
|
|
585
|
+
documentLoader,
|
|
586
|
+
contextLoader,
|
|
587
|
+
keyCache
|
|
588
|
+
};
|
|
589
|
+
strictEqual(await verifyRequest(await signedRequest(), options), null);
|
|
590
|
+
strictEqual(await verifyRequest(await signedRequest(), options), null);
|
|
591
|
+
strictEqual(fetchCount(documentLoader), 1);
|
|
592
|
+
});
|
|
593
|
+
test("verifyRequest() takes the expiration of a proof whatever its context", async () => {
|
|
594
|
+
let now = Temporal.Now.instant();
|
|
595
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
|
|
596
|
+
const expires = now.add({ minutes: 30 });
|
|
597
|
+
const actor = await actorJson();
|
|
598
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign({
|
|
599
|
+
...actor,
|
|
600
|
+
"@context": [...actor["@context"], { expires: "https://example.com/ns#notExpiration" }]
|
|
601
|
+
}, ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
|
|
602
|
+
const options = {
|
|
603
|
+
documentLoader,
|
|
604
|
+
contextLoader,
|
|
605
|
+
keyCache
|
|
606
|
+
};
|
|
607
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
608
|
+
now = expires.subtract({ seconds: 1 });
|
|
609
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
610
|
+
strictEqual(fetchCount(documentLoader), 1);
|
|
611
|
+
now = expires;
|
|
612
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
613
|
+
strictEqual(fetchCount(documentLoader), 2);
|
|
614
|
+
});
|
|
615
|
+
test("a key cache without namespaces caches only fetch failures of compatible key IDs", async () => {
|
|
616
|
+
const calls = [];
|
|
617
|
+
const entries = /* @__PURE__ */ new Map();
|
|
618
|
+
const keyCache = {
|
|
619
|
+
get(id) {
|
|
620
|
+
calls.push(`get ${id.href}`);
|
|
621
|
+
return Promise.resolve(entries.get(id.href));
|
|
622
|
+
},
|
|
623
|
+
set(id, key) {
|
|
624
|
+
calls.push(`set ${id.href} ${key == null ? "null" : "key"}`);
|
|
625
|
+
entries.set(id.href, key);
|
|
626
|
+
return Promise.resolve();
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const options = {
|
|
630
|
+
documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) }),
|
|
631
|
+
contextLoader,
|
|
632
|
+
keyCache
|
|
633
|
+
};
|
|
634
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
635
|
+
for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null);
|
|
636
|
+
ok(await verifyRequest(await signedRequest(), options) != null);
|
|
637
|
+
deepStrictEqual(calls.filter((c) => c.startsWith("set ") && c.includes(keyId.origin)), []);
|
|
638
|
+
const unreachable = new URL(`${compatibleId(gw2)}#main-key`);
|
|
639
|
+
strictEqual((await fetchKey(unreachable, CryptographicKey, options)).key, null);
|
|
640
|
+
deepStrictEqual(calls.filter((c) => c.startsWith("set ") && c.includes(gw2)), [`set ${unreachable.href} null`]);
|
|
641
|
+
});
|
|
642
|
+
test("doesActorOwnKey() and getKeyOwner() resolve gateway keys to portable actors", async () => {
|
|
643
|
+
const options = {
|
|
644
|
+
documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) }),
|
|
645
|
+
contextLoader
|
|
646
|
+
};
|
|
647
|
+
const key = await verifyRequest(await signedRequest(), options);
|
|
648
|
+
ok(key != null);
|
|
649
|
+
const activity = (actor) => new Create({
|
|
650
|
+
id: parseIri(`ap+ef61://${did}/activities/1`),
|
|
651
|
+
actor: parseIri(actor)
|
|
652
|
+
});
|
|
653
|
+
ok(await doesActorOwnKey(activity(actorId), key, options));
|
|
654
|
+
ok(await doesActorOwnKey(activity(compatibleId(gw2)), key, options));
|
|
655
|
+
ok(!await doesActorOwnKey(activity(`${actorId}/other`), key, options));
|
|
656
|
+
strictEqual((await getKeyOwner(keyId, options))?.id?.href, parseIri(actorId).href);
|
|
657
|
+
strictEqual((await getKeyOwner(key, options))?.id?.href, key.ownerId?.href);
|
|
658
|
+
const forged = key.clone({ publicKey: rsaPublicKey3.publicKey });
|
|
659
|
+
strictEqual(await getKeyOwner(forged, options), null);
|
|
660
|
+
ok(!await doesActorOwnKey(activity(actorId), forged, options));
|
|
661
|
+
});
|
|
662
|
+
test("doesActorOwnKey() and getKeyOwner() do not fall back for rejected gateway keys", async () => {
|
|
663
|
+
const options = {
|
|
664
|
+
documentLoader: createLoader({ [compatibleId(gw1)]: await actorJson() }),
|
|
665
|
+
contextLoader
|
|
666
|
+
};
|
|
667
|
+
const key = new CryptographicKey({
|
|
668
|
+
id: keyId,
|
|
669
|
+
owner: parseIri(actorId),
|
|
670
|
+
publicKey: gatewayPublicKey
|
|
671
|
+
});
|
|
672
|
+
ok(!await doesActorOwnKey(new Create({
|
|
673
|
+
id: parseIri(`ap+ef61://${did}/activities/1`),
|
|
674
|
+
actor: parseIri(actorId)
|
|
675
|
+
}), key, options));
|
|
676
|
+
strictEqual(await getKeyOwner(keyId, options), null);
|
|
677
|
+
strictEqual(await getKeyOwner(key, options), null);
|
|
678
|
+
});
|
|
679
|
+
test("verifyRequest() binds gateway keys to the actor whose document is signed", async () => {
|
|
680
|
+
const attackerDid = otherDid;
|
|
681
|
+
const attackerActorId = `ap+ef61://${attackerDid}/actor`;
|
|
682
|
+
const shiftingContext = "https://attacker.example/context";
|
|
683
|
+
let loads = 0;
|
|
684
|
+
const shiftingContextLoader = (url) => {
|
|
685
|
+
if (url !== shiftingContext) return contextLoader(url);
|
|
686
|
+
loads++;
|
|
687
|
+
const document = { "@context": loads <= 2 ? {
|
|
688
|
+
victimId: "@id",
|
|
689
|
+
attackerId: "https://attacker.example/ns#a"
|
|
690
|
+
} : {
|
|
691
|
+
victimId: "https://attacker.example/ns#v",
|
|
692
|
+
attackerId: "@id"
|
|
693
|
+
} };
|
|
694
|
+
return Promise.resolve({
|
|
695
|
+
contextUrl: null,
|
|
696
|
+
documentUrl: url,
|
|
697
|
+
document
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
const { id: _, ...victim } = await actorJson({ gateways: [gw1] });
|
|
701
|
+
const document = await sign({
|
|
702
|
+
...victim,
|
|
703
|
+
"@context": [...victim["@context"], shiftingContext],
|
|
704
|
+
victimId: actorId,
|
|
705
|
+
attackerId: attackerActorId
|
|
706
|
+
}, otherKeyPair.privateKey, `${attackerDid}#${attackerDid.slice(8)}`);
|
|
707
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: document });
|
|
708
|
+
strictEqual(await verifyRequest(await signedRequest(), {
|
|
709
|
+
documentLoader,
|
|
710
|
+
contextLoader: shiftingContextLoader
|
|
711
|
+
}), null);
|
|
712
|
+
ok(loads > 2, "the shifting context was loaded for the proof");
|
|
713
|
+
loads = 0;
|
|
714
|
+
strictEqual(await getKeyOwner(keyId, {
|
|
715
|
+
documentLoader,
|
|
716
|
+
contextLoader: shiftingContextLoader
|
|
717
|
+
}), null);
|
|
718
|
+
});
|
|
719
|
+
test("verifyRequest() matches gateway keys listed under ap: URIs", async () => {
|
|
720
|
+
const portableKeyId = parseIri(`${actorId}#main-key`);
|
|
721
|
+
const cases = {
|
|
722
|
+
"both properties": await actorJson({ key: portableKeyId }),
|
|
723
|
+
"assertionMethod only": await actorJson({
|
|
724
|
+
key: portableKeyId,
|
|
725
|
+
publicKey: null
|
|
726
|
+
}),
|
|
727
|
+
"publicKey only": await actorJson({
|
|
728
|
+
key: portableKeyId,
|
|
729
|
+
assertionMethod: false
|
|
730
|
+
})
|
|
731
|
+
};
|
|
732
|
+
for (const [name, document] of Object.entries(cases)) {
|
|
733
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(document) });
|
|
734
|
+
for (const spec of specs) {
|
|
735
|
+
const key = await verifyRequest(await signedRequest(spec), {
|
|
736
|
+
documentLoader,
|
|
737
|
+
contextLoader,
|
|
738
|
+
spec
|
|
739
|
+
});
|
|
740
|
+
strictEqual(key?.id?.href, keyId.href, `${name}, ${spec}`);
|
|
741
|
+
strictEqual(key?.ownerId?.href, parseIri(actorId).href, name);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
test("verifyRequest() rejects ambiguous or foreign aliases of gateway keys", async () => {
|
|
746
|
+
const portableKeyId = `${actorId}#main-key`;
|
|
747
|
+
const base = await actorJson();
|
|
748
|
+
const assertionMethod = base.assertionMethod;
|
|
749
|
+
const publicKey = base.publicKey;
|
|
750
|
+
const cases = {
|
|
751
|
+
"an exact entry and an ap: alias in assertionMethod": {
|
|
752
|
+
...base,
|
|
753
|
+
assertionMethod: [assertionMethod, {
|
|
754
|
+
...assertionMethod,
|
|
755
|
+
id: portableKeyId
|
|
756
|
+
}]
|
|
757
|
+
},
|
|
758
|
+
"a reference and an embedded ap: alias in assertionMethod": {
|
|
759
|
+
...base,
|
|
760
|
+
assertionMethod: [keyId.href, {
|
|
761
|
+
...assertionMethod,
|
|
762
|
+
id: portableKeyId
|
|
763
|
+
}]
|
|
764
|
+
},
|
|
765
|
+
"an exact entry and an ap: alias in publicKey": {
|
|
766
|
+
...base,
|
|
767
|
+
publicKey: [publicKey, {
|
|
768
|
+
...publicKey,
|
|
769
|
+
id: portableKeyId
|
|
770
|
+
}]
|
|
771
|
+
},
|
|
772
|
+
"a key of another gateway": await actorJson({ key: new URL(`${compatibleId(gw2)}#main-key`) }),
|
|
773
|
+
"an ap: key ID with dot segments": {
|
|
774
|
+
...base,
|
|
775
|
+
assertionMethod: {
|
|
776
|
+
...assertionMethod,
|
|
777
|
+
id: portableKeyId.replace("/actor#", "/x/../actor#")
|
|
778
|
+
},
|
|
779
|
+
publicKey: {
|
|
780
|
+
...publicKey,
|
|
781
|
+
id: portableKeyId.replace("/actor#", "/x/../actor#")
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
"a compatible key ID with dot segments": {
|
|
785
|
+
...base,
|
|
786
|
+
assertionMethod: {
|
|
787
|
+
...assertionMethod,
|
|
788
|
+
id: keyId.href.replace("/actor#", "/x/../actor#")
|
|
789
|
+
},
|
|
790
|
+
publicKey: {
|
|
791
|
+
...publicKey,
|
|
792
|
+
id: keyId.href.replace("/actor#", "/x/../actor#")
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
for (const [name, document] of Object.entries(cases)) {
|
|
797
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(document) });
|
|
798
|
+
strictEqual(await verifyRequest(await signedRequest(), {
|
|
799
|
+
documentLoader,
|
|
800
|
+
contextLoader
|
|
801
|
+
}), null, name);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
test("verifyRequest() refuses key IDs whose identity URL parsing changes", async () => {
|
|
805
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
|
|
806
|
+
const request = await signedRequest("draft-cavage-http-signatures-12");
|
|
807
|
+
const dotted = keyId.href.replace("/actor#", "/x/../actor#");
|
|
808
|
+
const headers = new Headers(request.headers);
|
|
809
|
+
headers.set("Signature", headers.get("Signature").replace(keyId.href, dotted));
|
|
810
|
+
strictEqual(await verifyRequest(new Request(request, { headers }), {
|
|
811
|
+
documentLoader,
|
|
812
|
+
contextLoader
|
|
813
|
+
}), null);
|
|
814
|
+
strictEqual(documentLoader.fetched.length, 0);
|
|
815
|
+
const dottedPortable = `${actorId.replace("/actor", "/x/../actor")}#main-key`;
|
|
816
|
+
for (const id of [
|
|
817
|
+
dotted,
|
|
818
|
+
dottedPortable,
|
|
819
|
+
` ${dotted}`,
|
|
820
|
+
` ${dottedPortable}`,
|
|
821
|
+
` ${parseIri(actorId).href}#main-key`,
|
|
822
|
+
dottedPortable.replace("ap+ef61:", "ap+ ef61:"),
|
|
823
|
+
dotted.replace("did:key:", "did:ke y:"),
|
|
824
|
+
`${gw1}/.well-known/apgateway/${did}/../../../users/alice#main-key`,
|
|
825
|
+
` ap://not-a-did/actor#main-key`
|
|
826
|
+
]) {
|
|
827
|
+
let error;
|
|
828
|
+
try {
|
|
829
|
+
await fetchKey(id, CryptographicKey, {
|
|
830
|
+
documentLoader,
|
|
831
|
+
contextLoader
|
|
832
|
+
});
|
|
833
|
+
} catch (e) {
|
|
834
|
+
error = e;
|
|
835
|
+
}
|
|
836
|
+
ok(error instanceof TypeError, id);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
const apKeyId = parseIri(`${actorId}#main-key`);
|
|
840
|
+
function hinted(...gateways) {
|
|
841
|
+
const query = gateways.map((g) => `@gateway=${encodeURIComponent(g)}`).join("&");
|
|
842
|
+
return parseIri(`${actorId}${query === "" ? "" : `?${query}`}#main-key`);
|
|
843
|
+
}
|
|
844
|
+
async function apActorJson(options = {}) {
|
|
845
|
+
return await actorJson({
|
|
846
|
+
key: apKeyId,
|
|
847
|
+
...options
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
test("verifyRequest() accepts keys of portable actors at ap: key IDs", async () => {
|
|
851
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
|
|
852
|
+
const hintedKeyId = hinted(gw1);
|
|
853
|
+
for (const spec of specs) {
|
|
854
|
+
const key = await verifyRequest(await signedRequest(spec, hintedKeyId), {
|
|
855
|
+
documentLoader,
|
|
856
|
+
contextLoader,
|
|
857
|
+
spec
|
|
858
|
+
});
|
|
859
|
+
ok(key != null, spec);
|
|
860
|
+
strictEqual(key.id?.href, hintedKeyId.href, spec);
|
|
861
|
+
strictEqual(key.ownerId?.href, parseIri(actorId).href, spec);
|
|
862
|
+
}
|
|
863
|
+
const raw = `ap://${did}/actor?@gateway=${encodeURIComponent(gw1)}#main-key`;
|
|
864
|
+
for (const written of [raw, raw.replace(/^ap:/, "ap+ef61:")]) {
|
|
865
|
+
const request = await signedRequest("draft-cavage-http-signatures-12", hintedKeyId);
|
|
866
|
+
const headers = new Headers(request.headers);
|
|
867
|
+
headers.set("Signature", headers.get("Signature").replace(hintedKeyId.href, written));
|
|
868
|
+
strictEqual((await verifyRequest(new Request(request, { headers }), {
|
|
869
|
+
documentLoader,
|
|
870
|
+
contextLoader
|
|
871
|
+
}))?.ownerId?.href, parseIri(actorId).href, written);
|
|
872
|
+
}
|
|
873
|
+
deepStrictEqual([...new Set(documentLoader.fetched)], [compatibleId(gw1)]);
|
|
874
|
+
});
|
|
875
|
+
test("verifyRequest() asks the document loader for ap: key IDs without hints", async () => {
|
|
876
|
+
const documentLoader = createLoader({ [actorId]: await sign(await apActorJson()) });
|
|
877
|
+
strictEqual((await verifyRequest(await signedRequest(void 0, apKeyId), {
|
|
878
|
+
documentLoader,
|
|
879
|
+
contextLoader
|
|
880
|
+
}))?.ownerId?.href, parseIri(actorId).href);
|
|
881
|
+
deepStrictEqual(documentLoader.fetched, [actorId]);
|
|
882
|
+
const result = await verifyRequestDetailed(await signedRequest(void 0, apKeyId), {
|
|
883
|
+
documentLoader: createLoader({}),
|
|
884
|
+
contextLoader
|
|
885
|
+
});
|
|
886
|
+
ok(!result.verified);
|
|
887
|
+
strictEqual(result.reason.type, "keyFetchError");
|
|
888
|
+
});
|
|
889
|
+
test("verifyRequest() rejects keys at ap: key IDs that portable actors do not vouch for", async () => {
|
|
890
|
+
const unsigned = await apActorJson();
|
|
891
|
+
const ordinaryActorId = "https://gw1.example/users/alice";
|
|
892
|
+
const cases = {
|
|
893
|
+
"an unsigned actor document": unsigned,
|
|
894
|
+
"an actor document signed by another DID": await sign(unsigned, otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`),
|
|
895
|
+
"a key missing from both assertionMethod and publicKey": await sign(await apActorJson({
|
|
896
|
+
assertionMethod: false,
|
|
897
|
+
publicKey: null
|
|
898
|
+
})),
|
|
899
|
+
"a key referred to by URL": await sign({
|
|
900
|
+
...await apActorJson({
|
|
901
|
+
publicKey: null,
|
|
902
|
+
assertionMethod: false
|
|
903
|
+
}),
|
|
904
|
+
assertionMethod: [apKeyId.href]
|
|
905
|
+
}),
|
|
906
|
+
"a key only in publicKey without an owner": await sign(withoutPublicKeyOwner(await apActorJson({ assertionMethod: false }))),
|
|
907
|
+
"a publicKey entry with other key material": await sign(await apActorJson({ publicKey: rsaPublicKey3.publicKey })),
|
|
908
|
+
"a key only at a compatible identifier": await sign(await actorJson()),
|
|
909
|
+
"an actor without gateways": await sign(await apActorJson({ gateways: [] })),
|
|
910
|
+
"the document of another actor": await sign(await apActorJson({ id: `${actorId}/other` })),
|
|
911
|
+
"an ordinary actor": await actorJson({
|
|
912
|
+
id: ordinaryActorId,
|
|
913
|
+
key: apKeyId
|
|
914
|
+
}),
|
|
915
|
+
"a standalone key": {
|
|
916
|
+
"@context": "https://w3id.org/security/v1",
|
|
917
|
+
id: apKeyId.href,
|
|
918
|
+
type: "Key",
|
|
919
|
+
owner: actorId,
|
|
920
|
+
publicKeyPem: await exportSpki(gatewayPublicKey)
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
for (const [name, document] of Object.entries(cases)) {
|
|
924
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: document });
|
|
925
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, hinted(gw1)), {
|
|
926
|
+
documentLoader,
|
|
927
|
+
contextLoader
|
|
928
|
+
}), null, name);
|
|
929
|
+
}
|
|
930
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
|
|
931
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, parseIri(`${actorId}?@gateway=${encodeURIComponent(gw1)}`)), {
|
|
932
|
+
documentLoader,
|
|
933
|
+
contextLoader
|
|
934
|
+
}), null);
|
|
935
|
+
strictEqual(documentLoader.fetched.length, 0);
|
|
936
|
+
});
|
|
937
|
+
test("verifyRequest() tries the gateways of ap: key IDs in order", async () => {
|
|
938
|
+
const signed = await sign(await apActorJson());
|
|
939
|
+
const cases = {
|
|
940
|
+
"an invalid document at the first gateway": {
|
|
941
|
+
[compatibleId(gw1)]: await apActorJson(),
|
|
942
|
+
[compatibleId(gw2)]: signed
|
|
943
|
+
},
|
|
944
|
+
"an unreachable first gateway": { [compatibleId(gw2)]: signed }
|
|
945
|
+
};
|
|
946
|
+
for (const [name, responses] of Object.entries(cases)) {
|
|
947
|
+
const documentLoader = createLoader(responses);
|
|
948
|
+
strictEqual((await verifyRequest(await signedRequest(void 0, hinted(gw1, gw2)), {
|
|
949
|
+
documentLoader,
|
|
950
|
+
contextLoader
|
|
951
|
+
}))?.ownerId?.href, parseIri(actorId).href, name);
|
|
952
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId(gw1), compatibleId(gw2)], name);
|
|
953
|
+
}
|
|
954
|
+
const documentLoader = createLoader({
|
|
955
|
+
[compatibleId(gw1)]: signed,
|
|
956
|
+
[compatibleId(gw2)]: signed
|
|
957
|
+
});
|
|
958
|
+
ok(await verifyRequest(await signedRequest(void 0, hinted(gw1, gw2)), {
|
|
959
|
+
documentLoader,
|
|
960
|
+
contextLoader
|
|
961
|
+
}) != null);
|
|
962
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
|
|
963
|
+
const gateways = Array.from({ length: 7 }, (_, i) => `https://gw${i + 10}.example`);
|
|
964
|
+
const many = createLoader({});
|
|
965
|
+
ok(!(await verifyRequestDetailed(await signedRequest(void 0, hinted(...gateways)), {
|
|
966
|
+
documentLoader: many,
|
|
967
|
+
contextLoader
|
|
968
|
+
})).verified);
|
|
969
|
+
deepStrictEqual(many.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)));
|
|
970
|
+
for (const [index, expected] of [[2, true], [3, false]]) {
|
|
971
|
+
const documentLoader = createLoader({ [compatibleId(gateways[index])]: signed });
|
|
972
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, hinted(...gateways)), {
|
|
973
|
+
documentLoader,
|
|
974
|
+
contextLoader
|
|
975
|
+
}) != null, expected, `gateway ${index + 1}`);
|
|
976
|
+
deepStrictEqual(documentLoader.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)), `gateway ${index + 1}`);
|
|
977
|
+
}
|
|
978
|
+
const sparse = createLoader({ [compatibleId(gateways[2])]: signed });
|
|
979
|
+
ok(await verifyRequest(await signedRequest(void 0, hinted("not a gateway", gateways[0], gateways[0].toUpperCase(), "https://gw.example/path", gateways[1], gateways[2])), {
|
|
980
|
+
documentLoader: sparse,
|
|
981
|
+
contextLoader
|
|
982
|
+
}) != null);
|
|
983
|
+
deepStrictEqual(sparse.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)));
|
|
984
|
+
});
|
|
985
|
+
function failingLoader(responses) {
|
|
986
|
+
const fetched = [];
|
|
987
|
+
const loader = (url) => {
|
|
988
|
+
fetched.push(url);
|
|
989
|
+
const response = responses[url.replace(/#.*$/, "")] ?? 404;
|
|
990
|
+
if (typeof response === "number") return Promise.reject(new FetchError(url, `HTTP ${response}`, new Response(null, { status: response })));
|
|
991
|
+
return Promise.resolve({
|
|
992
|
+
contextUrl: null,
|
|
993
|
+
documentUrl: url,
|
|
994
|
+
document: structuredClone(response)
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
return Object.assign(loader, { fetched });
|
|
998
|
+
}
|
|
999
|
+
test("verifyRequest() reports why keys at ap: key IDs could not be looked up", async () => {
|
|
1000
|
+
const hintedKeyId = hinted(gw1, gw2);
|
|
1001
|
+
const invalid = await apActorJson();
|
|
1002
|
+
const cases = [
|
|
1003
|
+
{
|
|
1004
|
+
name: "every gateway serves an invalid document",
|
|
1005
|
+
responses: {
|
|
1006
|
+
[compatibleId(gw1)]: invalid,
|
|
1007
|
+
[compatibleId(gw2)]: invalid
|
|
1008
|
+
},
|
|
1009
|
+
reason: "invalidSignature",
|
|
1010
|
+
cached: true
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "every gateway responds with 410 Gone",
|
|
1014
|
+
responses: {
|
|
1015
|
+
[compatibleId(gw1)]: 410,
|
|
1016
|
+
[compatibleId(gw2)]: 410
|
|
1017
|
+
},
|
|
1018
|
+
reason: "keyFetchError",
|
|
1019
|
+
status: 410,
|
|
1020
|
+
cached: true
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
name: "the gateways respond with different statuses",
|
|
1024
|
+
responses: {
|
|
1025
|
+
[compatibleId(gw1)]: 404,
|
|
1026
|
+
[compatibleId(gw2)]: 503
|
|
1027
|
+
},
|
|
1028
|
+
reason: "keyFetchError",
|
|
1029
|
+
cached: true
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: "one gateway serves an invalid document, the other is gone",
|
|
1033
|
+
responses: {
|
|
1034
|
+
[compatibleId(gw1)]: invalid,
|
|
1035
|
+
[compatibleId(gw2)]: 410
|
|
1036
|
+
},
|
|
1037
|
+
reason: "keyFetchError",
|
|
1038
|
+
cached: false
|
|
1039
|
+
}
|
|
1040
|
+
];
|
|
1041
|
+
for (const { name, responses, reason, status, cached } of cases) for (const spec of specs) {
|
|
1042
|
+
const documentLoader = failingLoader(responses);
|
|
1043
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
1044
|
+
const options = {
|
|
1045
|
+
documentLoader,
|
|
1046
|
+
contextLoader,
|
|
1047
|
+
keyCache,
|
|
1048
|
+
spec
|
|
1049
|
+
};
|
|
1050
|
+
for (let i = 0; i < 2; i++) {
|
|
1051
|
+
const result = await verifyRequestDetailed(await signedRequest(spec, hintedKeyId), options);
|
|
1052
|
+
ok(!result.verified, `${name}, ${spec}`);
|
|
1053
|
+
strictEqual(result.reason.type, reason, `${name}, ${spec}`);
|
|
1054
|
+
if (result.reason.type === "keyFetchError") strictEqual("status" in result.reason.result ? result.reason.result.status : void 0, status, `${name}, ${spec}`);
|
|
1055
|
+
}
|
|
1056
|
+
strictEqual(documentLoader.fetched.length, cached ? 2 : 4, `${name}, ${spec}`);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
test("verifyRequest() accepts keys at ap: key IDs only for HTTP Signatures", async () => {
|
|
1060
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
|
|
1061
|
+
const hintedKeyId = hinted(gw1);
|
|
1062
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
1063
|
+
const options = {
|
|
1064
|
+
documentLoader,
|
|
1065
|
+
contextLoader,
|
|
1066
|
+
keyCache
|
|
1067
|
+
};
|
|
1068
|
+
for (const cls of [CryptographicKey, Multikey]) strictEqual((cls === CryptographicKey ? await fetchKey(hintedKeyId, CryptographicKey, options) : await fetchKey(hintedKeyId, Multikey, options)).key, null);
|
|
1069
|
+
strictEqual(documentLoader.fetched.length, 0);
|
|
1070
|
+
for (let i = 0; i < 3; i++) {
|
|
1071
|
+
const key = await verifyRequest(await signedRequest(void 0, hintedKeyId), options);
|
|
1072
|
+
strictEqual(key?.id?.href, hintedKeyId.href);
|
|
1073
|
+
strictEqual(key?.ownerId?.href, parseIri(actorId).href);
|
|
1074
|
+
}
|
|
1075
|
+
strictEqual(documentLoader.fetched.length, 1);
|
|
1076
|
+
for (const cls of [CryptographicKey, Multikey]) strictEqual((cls === CryptographicKey ? await fetchKey(hintedKeyId, CryptographicKey, options) : await fetchKey(hintedKeyId, Multikey, options)).key, null);
|
|
1077
|
+
const document = {
|
|
1078
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1079
|
+
type: "Create",
|
|
1080
|
+
id: "https://gw1.example/activities/1",
|
|
1081
|
+
actor: compatibleId(gw1)
|
|
1082
|
+
};
|
|
1083
|
+
const { signature } = await signJsonLd(document, rsaPrivateKey2, hintedKeyId, { contextLoader });
|
|
1084
|
+
ok(!await verifyJsonLd({
|
|
1085
|
+
...document,
|
|
1086
|
+
signature
|
|
1087
|
+
}, options));
|
|
1088
|
+
strictEqual(documentLoader.fetched.length, 1);
|
|
1089
|
+
const legacyCache = {
|
|
1090
|
+
get: () => Promise.resolve(new CryptographicKey({
|
|
1091
|
+
id: hintedKeyId,
|
|
1092
|
+
owner: parseIri(actorId),
|
|
1093
|
+
publicKey: gatewayPublicKey
|
|
1094
|
+
})),
|
|
1095
|
+
set: () => Promise.resolve()
|
|
1096
|
+
};
|
|
1097
|
+
const empty = createLoader({});
|
|
1098
|
+
strictEqual(await verifyRequest(await signedRequest(void 0, hintedKeyId), {
|
|
1099
|
+
documentLoader: empty,
|
|
1100
|
+
contextLoader,
|
|
1101
|
+
keyCache: legacyCache
|
|
1102
|
+
}), null);
|
|
1103
|
+
strictEqual(empty.fetched.length, 1);
|
|
1104
|
+
});
|
|
1105
|
+
test("verifyRequest() refreshes a cached key at an ap: key ID that fails to verify", async () => {
|
|
1106
|
+
for (const spec of specs) {
|
|
1107
|
+
const responses = { [compatibleId(gw1)]: await sign(await apActorJson()) };
|
|
1108
|
+
const documentLoader = failingLoader(responses);
|
|
1109
|
+
const hintedKeyId = hinted(gw1, gw2);
|
|
1110
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
1111
|
+
const options = {
|
|
1112
|
+
documentLoader,
|
|
1113
|
+
contextLoader,
|
|
1114
|
+
keyCache,
|
|
1115
|
+
spec
|
|
1116
|
+
};
|
|
1117
|
+
ok(await verifyRequest(await signedRequest(spec, hintedKeyId), options) != null);
|
|
1118
|
+
strictEqual(documentLoader.fetched.length, 1, spec);
|
|
1119
|
+
responses[compatibleId(gw1)] = await apActorJson();
|
|
1120
|
+
responses[compatibleId(gw2)] = 503;
|
|
1121
|
+
ok(!(await verifyRequestDetailed(await signRequest(new Request("https://recipient.example/inbox", {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
body: "{}",
|
|
1124
|
+
headers: { "Content-Type": "application/activity+json" }
|
|
1125
|
+
}), rsaPrivateKey3, hintedKeyId, { spec }), options)).verified, spec);
|
|
1126
|
+
strictEqual(documentLoader.fetched.length, 3, spec);
|
|
1127
|
+
const next = await verifyRequestDetailed(await signedRequest(spec, hintedKeyId), options);
|
|
1128
|
+
ok(!next.verified, spec);
|
|
1129
|
+
strictEqual(next.reason.type, "keyFetchError", spec);
|
|
1130
|
+
strictEqual(documentLoader.fetched.length, 5, spec);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
test("doesActorOwnKey() and getKeyOwner() resolve keys at ap: key IDs to portable actors", async () => {
|
|
1134
|
+
const options = {
|
|
1135
|
+
documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }),
|
|
1136
|
+
contextLoader
|
|
1137
|
+
};
|
|
1138
|
+
const hintedKeyId = hinted(gw1);
|
|
1139
|
+
const key = new CryptographicKey({
|
|
1140
|
+
id: hintedKeyId,
|
|
1141
|
+
owner: parseIri(actorId),
|
|
1142
|
+
publicKey: gatewayPublicKey
|
|
1143
|
+
});
|
|
1144
|
+
const activity = new Create({
|
|
1145
|
+
id: parseIri(`${actorId.replace("/actor", "")}/activities/1`),
|
|
1146
|
+
actor: parseIri(`ap://${did}/actor`)
|
|
1147
|
+
});
|
|
1148
|
+
ok(await doesActorOwnKey(activity, key, options));
|
|
1149
|
+
strictEqual((await getKeyOwner(hintedKeyId, options))?.id?.href, parseIri(actorId).href);
|
|
1150
|
+
strictEqual((await getKeyOwner(key, options))?.id?.href, parseIri(actorId).href);
|
|
1151
|
+
ok(!await doesActorOwnKey(new Create({
|
|
1152
|
+
id: parseIri(`ap+ef61://${otherDid}/activities/1`),
|
|
1153
|
+
actor: parseIri(`ap+ef61://${otherDid}/actor`)
|
|
1154
|
+
}), key, options));
|
|
1155
|
+
const other = key.clone({ publicKey: rsaPublicKey3.publicKey });
|
|
1156
|
+
ok(!await doesActorOwnKey(activity, other, options));
|
|
1157
|
+
strictEqual(await getKeyOwner(other, options), null);
|
|
1158
|
+
const unsigned = {
|
|
1159
|
+
documentLoader: createLoader({ [compatibleId(gw1)]: await apActorJson() }),
|
|
1160
|
+
contextLoader
|
|
1161
|
+
};
|
|
1162
|
+
ok(!await doesActorOwnKey(activity, key, unsigned));
|
|
1163
|
+
strictEqual(await getKeyOwner(hintedKeyId, unsigned), null);
|
|
1164
|
+
});
|
|
1165
|
+
test("verifyRequest() does not trust a cached key at an ap: key ID for longer than its proof", async () => {
|
|
1166
|
+
let now = Temporal.Now.instant();
|
|
1167
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
|
|
1168
|
+
const expires = now.add({ minutes: 30 });
|
|
1169
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson(), ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
|
|
1170
|
+
const options = {
|
|
1171
|
+
documentLoader,
|
|
1172
|
+
contextLoader,
|
|
1173
|
+
keyCache
|
|
1174
|
+
};
|
|
1175
|
+
const hintedKeyId = hinted(gw1);
|
|
1176
|
+
ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
|
|
1177
|
+
now = expires.subtract({ seconds: 1 });
|
|
1178
|
+
ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
|
|
1179
|
+
strictEqual(documentLoader.fetched.length, 1);
|
|
1180
|
+
now = expires;
|
|
1181
|
+
ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
|
|
1182
|
+
strictEqual(documentLoader.fetched.length, 2);
|
|
1183
|
+
});
|
|
1184
|
+
test("verifyRequest() does not cache keys at ap: key IDs it could not process", async () => {
|
|
1185
|
+
let failures = 1;
|
|
1186
|
+
const flakyContextLoader = (url, options) => {
|
|
1187
|
+
if (failures > 0) {
|
|
1188
|
+
failures--;
|
|
1189
|
+
return Promise.reject(/* @__PURE__ */ new Error(`Temporarily failed to load ${url}`));
|
|
1190
|
+
}
|
|
1191
|
+
return contextLoader(url, options);
|
|
1192
|
+
};
|
|
1193
|
+
const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
|
|
1194
|
+
const options = {
|
|
1195
|
+
documentLoader,
|
|
1196
|
+
contextLoader: flakyContextLoader,
|
|
1197
|
+
keyCache: new KvKeyCache(new MemoryKvStore(), ["pk"])
|
|
1198
|
+
};
|
|
1199
|
+
const hintedKeyId = hinted(gw1);
|
|
1200
|
+
const result = await verifyRequestDetailed(await signedRequest(void 0, hintedKeyId), options);
|
|
1201
|
+
ok(!result.verified);
|
|
1202
|
+
strictEqual(result.reason.type, "keyFetchError");
|
|
1203
|
+
if (result.reason.type === "keyFetchError") ok(!("status" in result.reason.result));
|
|
1204
|
+
strictEqual((await verifyRequest(await signedRequest(void 0, hintedKeyId), options))?.ownerId?.href, parseIri(actorId).href);
|
|
1205
|
+
strictEqual(documentLoader.fetched.length, 2);
|
|
1206
|
+
});
|
|
1207
|
+
/**
|
|
1208
|
+
* A loader that serves the given responses, and never responds for the URLs
|
|
1209
|
+
* in `hanging`. A hanging response is given up when its signal aborts,
|
|
1210
|
+
* unless `ignoreSignal` is set.
|
|
1211
|
+
*/
|
|
1212
|
+
function controlledLoader(responses, hanging, { ignoreSignal = false, delays = {} } = {}) {
|
|
1213
|
+
const fetched = [];
|
|
1214
|
+
const signals = [];
|
|
1215
|
+
const respond = failingLoader(responses);
|
|
1216
|
+
const loader = async (url, options) => {
|
|
1217
|
+
fetched.push(url);
|
|
1218
|
+
signals.push(options?.signal);
|
|
1219
|
+
const signal = ignoreSignal ? void 0 : options?.signal;
|
|
1220
|
+
const delay = delays[url];
|
|
1221
|
+
if (delay != null) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1222
|
+
if (hanging.includes(url)) return await new Promise((_, reject) => {
|
|
1223
|
+
signal?.addEventListener("abort", () => reject(signal.reason));
|
|
1224
|
+
});
|
|
1225
|
+
return await respond(url);
|
|
1226
|
+
};
|
|
1227
|
+
return Object.assign(loader, {
|
|
1228
|
+
fetched,
|
|
1229
|
+
signals
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
function isTimeoutError(error) {
|
|
1233
|
+
return error instanceof DOMException && error.name === "TimeoutError";
|
|
1234
|
+
}
|
|
1235
|
+
test("resolvePortableActorKey() gives up gateways that do not respond in time", async () => {
|
|
1236
|
+
const signed = await sign(await apActorJson());
|
|
1237
|
+
for (const ignoreSignal of [false, true]) {
|
|
1238
|
+
const documentLoader = controlledLoader({ [compatibleId(gw2)]: signed }, [compatibleId(gw1)], { ignoreSignal });
|
|
1239
|
+
const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
|
|
1240
|
+
documentLoader,
|
|
1241
|
+
contextLoader,
|
|
1242
|
+
lookupTimeout: 50
|
|
1243
|
+
});
|
|
1244
|
+
strictEqual(resolution.type, "unavailable", `ignoreSignal: ${ignoreSignal}`);
|
|
1245
|
+
if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
|
|
1246
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
|
|
1247
|
+
const [signal] = documentLoader.signals;
|
|
1248
|
+
ok(signal?.aborted);
|
|
1249
|
+
ok(isTimeoutError(signal.reason));
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
test("resolvePortableActorKey() caches timeouts only as failures to fetch", async () => {
|
|
1253
|
+
const invalid = await apActorJson();
|
|
1254
|
+
const cases = [
|
|
1255
|
+
{
|
|
1256
|
+
name: "the only gateway times out",
|
|
1257
|
+
responses: {},
|
|
1258
|
+
hanging: [compatibleId(gw1)],
|
|
1259
|
+
hints: [gw1],
|
|
1260
|
+
cacheable: true
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: "a gateway responds with 404, and the last one times out",
|
|
1264
|
+
responses: { [compatibleId(gw1)]: 404 },
|
|
1265
|
+
hanging: [compatibleId(gw2)],
|
|
1266
|
+
hints: [gw1, gw2],
|
|
1267
|
+
cacheable: true
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: "a gateway serves an invalid document, and the last one times out",
|
|
1271
|
+
responses: { [compatibleId(gw1)]: invalid },
|
|
1272
|
+
hanging: [compatibleId(gw2)],
|
|
1273
|
+
hints: [gw1, gw2],
|
|
1274
|
+
cacheable: false
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
name: "the document loader cannot resolve the ap: URI in time",
|
|
1278
|
+
responses: {},
|
|
1279
|
+
hanging: [actorId],
|
|
1280
|
+
hints: [],
|
|
1281
|
+
cacheable: true
|
|
1282
|
+
}
|
|
1283
|
+
];
|
|
1284
|
+
for (const { name, responses, hanging, hints, cacheable } of cases) {
|
|
1285
|
+
const documentLoader = controlledLoader(responses, hanging);
|
|
1286
|
+
const resolution = await resolvePortableActorKey(hinted(...hints), {
|
|
1287
|
+
documentLoader,
|
|
1288
|
+
contextLoader,
|
|
1289
|
+
lookupTimeout: 50
|
|
1290
|
+
});
|
|
1291
|
+
strictEqual(resolution.type, "unavailable", name);
|
|
1292
|
+
if (resolution.type !== "unavailable") continue;
|
|
1293
|
+
strictEqual(resolution.cacheable, cacheable, name);
|
|
1294
|
+
ok(!(resolution.error instanceof FetchError), name);
|
|
1295
|
+
if (hanging.length === 1 && hints.length < 2) ok(isTimeoutError(resolution.error), name);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
test("resolvePortableActorKey() shares one timeout among all gateways", async () => {
|
|
1299
|
+
const documentLoader = controlledLoader({ [compatibleId(gw1)]: 404 }, [compatibleId(gw2)], { delays: { [compatibleId(gw1)]: 250 } });
|
|
1300
|
+
const started = Date.now();
|
|
1301
|
+
const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
|
|
1302
|
+
documentLoader,
|
|
1303
|
+
contextLoader,
|
|
1304
|
+
lookupTimeout: 300
|
|
1305
|
+
});
|
|
1306
|
+
const elapsed = Date.now() - started;
|
|
1307
|
+
ok(elapsed < 500, `took ${elapsed} ms`);
|
|
1308
|
+
strictEqual(resolution.type, "unavailable");
|
|
1309
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId(gw1), compatibleId(gw2)]);
|
|
1310
|
+
ok(documentLoader.signals[1]?.aborted);
|
|
1311
|
+
});
|
|
1312
|
+
test("resolvePortableActorKey() bounds loading the contexts of actor documents", async () => {
|
|
1313
|
+
const documentLoader = controlledLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }, []);
|
|
1314
|
+
const hangingContextLoader = (_url, options) => new Promise((_, reject) => {
|
|
1315
|
+
options?.signal?.addEventListener("abort", () => reject(options.signal.reason));
|
|
1316
|
+
});
|
|
1317
|
+
const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
|
|
1318
|
+
documentLoader,
|
|
1319
|
+
contextLoader: hangingContextLoader,
|
|
1320
|
+
lookupTimeout: 50
|
|
1321
|
+
});
|
|
1322
|
+
strictEqual(resolution.type, "unavailable");
|
|
1323
|
+
if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
|
|
1324
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
|
|
1325
|
+
});
|
|
1326
|
+
test("resolvePortableActorKey() ignores what is resolved after the timeout", async () => {
|
|
1327
|
+
let verifiedLate = () => {};
|
|
1328
|
+
const late = new Promise((resolve) => verifiedLate = resolve);
|
|
1329
|
+
let lookupDone = () => {};
|
|
1330
|
+
const done = new Promise((resolve) => lookupDone = resolve);
|
|
1331
|
+
const slowKeyCache = {
|
|
1332
|
+
async get() {
|
|
1333
|
+
await done;
|
|
1334
|
+
},
|
|
1335
|
+
set() {
|
|
1336
|
+
verifiedLate();
|
|
1337
|
+
return Promise.resolve();
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
const documentLoader = controlledLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }, []);
|
|
1341
|
+
const resolution = await resolvePortableActorKey(hinted(gw1), {
|
|
1342
|
+
documentLoader,
|
|
1343
|
+
contextLoader,
|
|
1344
|
+
keyCache: slowKeyCache,
|
|
1345
|
+
lookupTimeout: 300
|
|
1346
|
+
});
|
|
1347
|
+
lookupDone();
|
|
1348
|
+
strictEqual(resolution.type, "unavailable");
|
|
1349
|
+
if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
|
|
1350
|
+
await late;
|
|
1351
|
+
const lateRejection = controlledLoader({}, [], { delays: { [compatibleId(gw1)]: 60 } });
|
|
1352
|
+
strictEqual((await resolvePortableActorKey(hinted(gw1), {
|
|
1353
|
+
documentLoader: lateRejection,
|
|
1354
|
+
contextLoader,
|
|
1355
|
+
lookupTimeout: 20
|
|
1356
|
+
})).type, "unavailable");
|
|
1357
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1358
|
+
});
|
|
1359
|
+
test("resolvePortableActorKey() validates the lookup timeout", async () => {
|
|
1360
|
+
const documentLoader = controlledLoader({}, []);
|
|
1361
|
+
for (const lookupTimeout of [
|
|
1362
|
+
-1,
|
|
1363
|
+
NaN,
|
|
1364
|
+
Infinity,
|
|
1365
|
+
2 ** 31
|
|
1366
|
+
]) {
|
|
1367
|
+
let error;
|
|
1368
|
+
try {
|
|
1369
|
+
await resolvePortableActorKey(hinted(gw1), {
|
|
1370
|
+
documentLoader,
|
|
1371
|
+
contextLoader,
|
|
1372
|
+
lookupTimeout
|
|
1373
|
+
});
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
error = e;
|
|
1376
|
+
}
|
|
1377
|
+
ok(error instanceof RangeError, String(lookupTimeout));
|
|
1378
|
+
}
|
|
1379
|
+
const resolution = await resolvePortableActorKey(hinted(gw1), {
|
|
1380
|
+
documentLoader,
|
|
1381
|
+
contextLoader,
|
|
1382
|
+
lookupTimeout: 0
|
|
1383
|
+
});
|
|
1384
|
+
strictEqual(resolution.type, "unavailable");
|
|
1385
|
+
if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
|
|
1386
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
1387
|
+
});
|
|
1388
|
+
test("fetchKeyDetailed() caches keys at ap: key IDs that time out as fetch failures", async () => {
|
|
1389
|
+
const cases = [{
|
|
1390
|
+
responses: { [compatibleId(gw1)]: 404 },
|
|
1391
|
+
hanging: [compatibleId(gw2)],
|
|
1392
|
+
cached: true
|
|
1393
|
+
}, {
|
|
1394
|
+
responses: {},
|
|
1395
|
+
hanging: [compatibleId(gw1)],
|
|
1396
|
+
cached: false
|
|
1397
|
+
}];
|
|
1398
|
+
for (const { responses, hanging, cached } of cases) {
|
|
1399
|
+
const documentLoader = controlledLoader(responses, hanging);
|
|
1400
|
+
const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
|
|
1401
|
+
const options = {
|
|
1402
|
+
documentLoader,
|
|
1403
|
+
contextLoader,
|
|
1404
|
+
keyCache,
|
|
1405
|
+
portableKeyResolvers: {
|
|
1406
|
+
gatewayKey: () => Promise.resolve({ type: "legacy" }),
|
|
1407
|
+
actorKey: (keyId) => resolvePortableActorKey(keyId, {
|
|
1408
|
+
documentLoader,
|
|
1409
|
+
contextLoader,
|
|
1410
|
+
lookupTimeout: 50
|
|
1411
|
+
})
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
const hintedKeyId = hinted(gw1, gw2);
|
|
1415
|
+
for (let i = 0; i < 2; i++) {
|
|
1416
|
+
const result = await fetchKeyDetailed(hintedKeyId, CryptographicKey, options);
|
|
1417
|
+
strictEqual(result.key, null);
|
|
1418
|
+
strictEqual(result.cached, cached && i > 0, `${cached}, ${i}`);
|
|
1419
|
+
ok(result.fetchError == null || !("status" in result.fetchError), "a timeout has no HTTP status");
|
|
1420
|
+
}
|
|
1421
|
+
strictEqual(documentLoader.fetched.filter((url) => url === compatibleId(gw1)).length, cached ? 1 : 2);
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
test("verifyRequestDetailed() reports keys at ap: key IDs that time out as fetch errors", async () => {
|
|
1425
|
+
const timeout = new DOMException("Timed out", "TimeoutError");
|
|
1426
|
+
const documentLoader = () => Promise.reject(timeout);
|
|
1427
|
+
for (const spec of specs) {
|
|
1428
|
+
const result = await verifyRequestDetailed(await signedRequest(spec, hinted(gw1)), {
|
|
1429
|
+
documentLoader,
|
|
1430
|
+
contextLoader,
|
|
1431
|
+
spec
|
|
1432
|
+
});
|
|
1433
|
+
ok(!result.verified, spec);
|
|
1434
|
+
strictEqual(result.reason.type, "keyFetchError", spec);
|
|
1435
|
+
if (result.reason.type === "keyFetchError") ok(!("status" in result.reason.result), spec);
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
//#endregion
|
|
1439
|
+
export {};
|