@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
package/dist/sig/proof.test.mjs
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as
|
|
5
|
-
import "../
|
|
6
|
-
import {
|
|
7
|
-
import { t as
|
|
8
|
-
import { t as
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
4
|
+
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
+
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
6
|
+
import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
7
|
+
import { t as assertGreaterOrEqual } from "../assert_greater_or_equal-BlafOF4-.mjs";
|
|
8
|
+
import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
|
|
9
|
+
import "../std__assert-DvMDRPes.mjs";
|
|
10
|
+
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
11
|
+
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
12
|
+
import { a as verificationObservation } from "../verification-Dpfcw8ml.mjs";
|
|
13
|
+
import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-4IJqPYos.mjs";
|
|
14
|
+
import { a as signObject, c as verifyPortableObjectProof, d as verifyProof, i as hasProofLike, o as verifyMapLocalProof, r as createProof, s as verifyObject } from "../proof-Dxy6ByqI.mjs";
|
|
15
|
+
import { f as verifyRequest } from "../http-siciWkmx.mjs";
|
|
16
|
+
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
|
|
17
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-DBoYbvYW.mjs";
|
|
18
|
+
import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place, Translation } from "@fedify/vocab";
|
|
19
|
+
import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
|
|
13
20
|
import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
14
|
-
import { decodeMultibase, exportDidKey, exportMultibaseKey, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
|
|
15
21
|
import { decodeHex } from "byte-encodings/hex";
|
|
22
|
+
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
23
|
+
import serialize from "json-canon";
|
|
16
24
|
//#region src/sig/proof.test.ts
|
|
17
25
|
const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
|
|
18
26
|
"kty": "OKP",
|
|
@@ -23,6 +31,53 @@ const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
|
|
|
23
31
|
ext: true
|
|
24
32
|
}, "Ed25519", true, ["sign"]);
|
|
25
33
|
const fep8b32TestVectorKeyId = new URL("https://server.example/users/alice#ed25519-key");
|
|
34
|
+
const portableDid = await exportDidKey(ed25519PublicKey.publicKey);
|
|
35
|
+
const portableDidMethod = portableDid.substring(8);
|
|
36
|
+
const portableKeyId = new URL(`${portableDid}#${portableDidMethod}`);
|
|
37
|
+
const portableContext = ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"];
|
|
38
|
+
const portableActorContext = [...portableContext, "https://w3id.org/fep/ef61"];
|
|
39
|
+
const portableProofCreated = "2023-02-24T23:36:38Z";
|
|
40
|
+
async function signPortableJsonLd(document, { privateKey = ed25519PrivateKey, verificationMethod = portableKeyId, proofOptions = {} } = {}) {
|
|
41
|
+
const proofConfig = {
|
|
42
|
+
"@context": document["@context"],
|
|
43
|
+
type: "DataIntegrityProof",
|
|
44
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
45
|
+
verificationMethod: verificationMethod.href,
|
|
46
|
+
proofPurpose: "assertionMethod",
|
|
47
|
+
created: portableProofCreated,
|
|
48
|
+
...proofOptions
|
|
49
|
+
};
|
|
50
|
+
const encoder = new TextEncoder();
|
|
51
|
+
const proofDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)));
|
|
52
|
+
const messageDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(document)));
|
|
53
|
+
const digest = new Uint8Array(proofDigest.byteLength + messageDigest.byteLength);
|
|
54
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
|
55
|
+
digest.set(new Uint8Array(messageDigest), proofDigest.byteLength);
|
|
56
|
+
const signature = await crypto.subtle.sign("Ed25519", privateKey, digest);
|
|
57
|
+
return {
|
|
58
|
+
...document,
|
|
59
|
+
proof: {
|
|
60
|
+
...proofConfig,
|
|
61
|
+
proofValue: new TextDecoder().decode(encodeMultibase("base58btc", new Uint8Array(signature)))
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function parsePortableProof(document) {
|
|
66
|
+
const proof = document.proof;
|
|
67
|
+
assert(typeof proof === "object" && proof != null && !Array.isArray(proof));
|
|
68
|
+
return await DataIntegrityProof.fromJsonLd({
|
|
69
|
+
"@context": document["@context"],
|
|
70
|
+
...proof
|
|
71
|
+
}, {
|
|
72
|
+
documentLoader: mockDocumentLoader,
|
|
73
|
+
contextLoader: mockDocumentLoader
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function assertPortableProofVerified(document, options) {
|
|
77
|
+
const key = await verifyProof(document, await parsePortableProof(document), options);
|
|
78
|
+
assert(key != null);
|
|
79
|
+
assertEquals(key.id, portableKeyId);
|
|
80
|
+
}
|
|
26
81
|
const fep8b32TestVectorActivity = new Create({
|
|
27
82
|
id: new URL("https://server.example/activities/1"),
|
|
28
83
|
actor: new URL("https://server.example/users/alice"),
|
|
@@ -326,6 +381,202 @@ test("verifyProof()", async () => {
|
|
|
326
381
|
}), null);
|
|
327
382
|
assertFalse(contextLoaderCalls.includes("https://attacker.example/ctx"));
|
|
328
383
|
});
|
|
384
|
+
test("verifyProof() authenticates the complete proof configuration", async (t) => {
|
|
385
|
+
const jsonLd = {
|
|
386
|
+
"@context": portableContext,
|
|
387
|
+
id: "https://example.com/activities/complete-proof-options",
|
|
388
|
+
type: "Create",
|
|
389
|
+
actor: "https://example.com/users/alice",
|
|
390
|
+
object: {
|
|
391
|
+
type: "Note",
|
|
392
|
+
content: "Every proof option is authenticated."
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const options = {
|
|
396
|
+
documentLoader: mockDocumentLoader,
|
|
397
|
+
contextLoader: mockDocumentLoader
|
|
398
|
+
};
|
|
399
|
+
await t.step("rejects an expires option added after signing", async () => {
|
|
400
|
+
const signed = await signPortableJsonLd(jsonLd);
|
|
401
|
+
const tampered = {
|
|
402
|
+
...signed,
|
|
403
|
+
proof: {
|
|
404
|
+
...signed.proof,
|
|
405
|
+
expires: "3000-01-01T00:00:00Z"
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
|
|
409
|
+
});
|
|
410
|
+
await t.step("accepts an authenticated future expiration", async () => {
|
|
411
|
+
await assertPortableProofVerified(await signPortableJsonLd(jsonLd, { proofOptions: { expires: "3000-01-01T00:00:00Z" } }), options);
|
|
412
|
+
});
|
|
413
|
+
await t.step("rejects an authenticated expired proof", async () => {
|
|
414
|
+
const signed = await signPortableJsonLd(jsonLd, { proofOptions: { expires: "2000-01-01T00:00:00Z" } });
|
|
415
|
+
assertEquals(await verifyProof(signed, await parsePortableProof(signed), options), null);
|
|
416
|
+
});
|
|
417
|
+
await t.step("authenticates domain, challenge, and nonce", async () => {
|
|
418
|
+
const signed = await signPortableJsonLd(jsonLd, { proofOptions: {
|
|
419
|
+
domain: ["social.example", "https://social.example"],
|
|
420
|
+
challenge: "challenge-123",
|
|
421
|
+
nonce: "nonce-456"
|
|
422
|
+
} });
|
|
423
|
+
const proof = await parsePortableProof(signed);
|
|
424
|
+
const matchingKey = await verifyProof(signed, proof, {
|
|
425
|
+
...options,
|
|
426
|
+
domain: ["https://social.example", "social.example"],
|
|
427
|
+
challenge: "challenge-123"
|
|
428
|
+
});
|
|
429
|
+
assert(matchingKey != null);
|
|
430
|
+
assertEquals(matchingKey.id, portableKeyId);
|
|
431
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
432
|
+
...options,
|
|
433
|
+
domain: ["social.example"],
|
|
434
|
+
challenge: "challenge-123"
|
|
435
|
+
}), null);
|
|
436
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
437
|
+
...options,
|
|
438
|
+
domain: ["https://social.example", "social.example"],
|
|
439
|
+
challenge: "wrong-challenge"
|
|
440
|
+
}), null);
|
|
441
|
+
for (const [property, value] of [
|
|
442
|
+
["domain", ["other.example"]],
|
|
443
|
+
["challenge", "tampered-challenge"],
|
|
444
|
+
["nonce", "tampered-nonce"]
|
|
445
|
+
]) {
|
|
446
|
+
const tampered = {
|
|
447
|
+
...signed,
|
|
448
|
+
proof: {
|
|
449
|
+
...signed.proof,
|
|
450
|
+
[property]: value
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
await t.step("requires requested domain and challenge options", async () => {
|
|
457
|
+
const signed = await signPortableJsonLd(jsonLd);
|
|
458
|
+
const proof = await parsePortableProof(signed);
|
|
459
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
460
|
+
...options,
|
|
461
|
+
domain: "social.example"
|
|
462
|
+
}), null);
|
|
463
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
464
|
+
...options,
|
|
465
|
+
challenge: "challenge-123"
|
|
466
|
+
}), null);
|
|
467
|
+
});
|
|
468
|
+
await t.step("rejects malformed proof options", async () => {
|
|
469
|
+
for (const proofOptions of [
|
|
470
|
+
{ expires: "not-a-date" },
|
|
471
|
+
{ domain: ["social.example", 42] },
|
|
472
|
+
{ challenge: 42 },
|
|
473
|
+
{ nonce: { value: "not-a-string" } },
|
|
474
|
+
{ nonce: [
|
|
475
|
+
"not",
|
|
476
|
+
"a",
|
|
477
|
+
"string"
|
|
478
|
+
] },
|
|
479
|
+
{ previousProof: ["urn:uuid:proof-1", 42] }
|
|
480
|
+
]) {
|
|
481
|
+
const signed = await signPortableJsonLd(jsonLd, { proofOptions });
|
|
482
|
+
assertEquals(await verifyProof(signed, await parsePortableProof(signed), options), null);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
await t.step("authenticates aliased and extension options", async () => {
|
|
486
|
+
const signed = await signPortableJsonLd({
|
|
487
|
+
...jsonLd,
|
|
488
|
+
"@context": [...portableContext, {
|
|
489
|
+
validUntil: {
|
|
490
|
+
"@id": "https://w3id.org/security#expiration",
|
|
491
|
+
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
|
|
492
|
+
},
|
|
493
|
+
extensionOption: "https://example.com/security#extensionOption"
|
|
494
|
+
}]
|
|
495
|
+
}, { proofOptions: {
|
|
496
|
+
validUntil: "3000-01-01T00:00:00Z",
|
|
497
|
+
extensionOption: { nested: ["one", "two"] }
|
|
498
|
+
} });
|
|
499
|
+
await assertPortableProofVerified(signed, options);
|
|
500
|
+
for (const [property, value] of [["validUntil", "2999-01-01T00:00:00Z"], ["extensionOption", { nested: ["one", "changed"] }]]) {
|
|
501
|
+
const tampered = {
|
|
502
|
+
...signed,
|
|
503
|
+
proof: {
|
|
504
|
+
...signed.proof,
|
|
505
|
+
[property]: value
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
await t.step("distinguishes equivalent and ambiguous raw proof configurations", async () => {
|
|
512
|
+
const signed = await signPortableJsonLd(jsonLd);
|
|
513
|
+
const proof = signed.proof;
|
|
514
|
+
const parsed = await parsePortableProof(signed);
|
|
515
|
+
assert(await verifyProof({
|
|
516
|
+
...signed,
|
|
517
|
+
proof: [proof, structuredClone(proof)]
|
|
518
|
+
}, parsed, options) != null);
|
|
519
|
+
const inheritedContextProof = structuredClone(proof);
|
|
520
|
+
delete inheritedContextProof["@context"];
|
|
521
|
+
assert(await verifyProof({
|
|
522
|
+
...signed,
|
|
523
|
+
proof: [proof, inheritedContextProof]
|
|
524
|
+
}, parsed, options) != null);
|
|
525
|
+
assertEquals(await verifyProof({
|
|
526
|
+
...signed,
|
|
527
|
+
proof: [proof, {
|
|
528
|
+
...structuredClone(proof),
|
|
529
|
+
expires: "3000-01-01T00:00:00Z"
|
|
530
|
+
}]
|
|
531
|
+
}, parsed, options), null);
|
|
532
|
+
});
|
|
533
|
+
await t.step("accepts literal proof fields with an unresolved extra context", async () => {
|
|
534
|
+
const signed = await signPortableJsonLd({
|
|
535
|
+
...jsonLd,
|
|
536
|
+
"@context": [...portableContext, "https://context.example/unrelated"]
|
|
537
|
+
});
|
|
538
|
+
const rawProof = signed.proof;
|
|
539
|
+
const key = await verifyProof(signed, new DataIntegrityProof({
|
|
540
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
541
|
+
verificationMethod: portableKeyId,
|
|
542
|
+
proofPurpose: "assertionMethod",
|
|
543
|
+
proofValue: decodeMultibase(rawProof.proofValue),
|
|
544
|
+
created: Temporal.Instant.from(portableProofCreated)
|
|
545
|
+
}), options);
|
|
546
|
+
assert(key != null);
|
|
547
|
+
assertEquals(key.id, portableKeyId);
|
|
548
|
+
});
|
|
549
|
+
await t.step("rejects aliases whose proof context cannot be resolved safely", async () => {
|
|
550
|
+
const contextUrl = "https://context.example/proof-options";
|
|
551
|
+
const signed = await signPortableJsonLd({
|
|
552
|
+
...jsonLd,
|
|
553
|
+
"@context": [...portableContext, contextUrl]
|
|
554
|
+
}, { proofOptions: { validUntil: "2000-01-01T00:00:00Z" } });
|
|
555
|
+
const rawProof = signed.proof;
|
|
556
|
+
const proof = new DataIntegrityProof({
|
|
557
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
558
|
+
verificationMethod: portableKeyId,
|
|
559
|
+
proofPurpose: "assertionMethod",
|
|
560
|
+
proofValue: decodeMultibase(rawProof.proofValue),
|
|
561
|
+
created: Temporal.Instant.from(portableProofCreated)
|
|
562
|
+
});
|
|
563
|
+
const contextLoader = async (url) => {
|
|
564
|
+
if (url !== contextUrl) return await mockDocumentLoader(url);
|
|
565
|
+
return {
|
|
566
|
+
contextUrl: null,
|
|
567
|
+
documentUrl: url,
|
|
568
|
+
document: { "@context": { validUntil: {
|
|
569
|
+
"@id": "https://w3id.org/security#expiration",
|
|
570
|
+
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
|
|
571
|
+
} } }
|
|
572
|
+
};
|
|
573
|
+
};
|
|
574
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
575
|
+
...options,
|
|
576
|
+
contextLoader
|
|
577
|
+
}), null);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
329
580
|
test("verifyProof() resolves did:key verification methods locally", async () => {
|
|
330
581
|
const multibaseKey = (await exportDidKey(ed25519PublicKey.publicKey)).slice(8);
|
|
331
582
|
const did = `did:key:${multibaseKey}`;
|
|
@@ -508,12 +759,13 @@ test("verifyProof() records verification duration metric", async (t) => {
|
|
|
508
759
|
});
|
|
509
760
|
await t.step("verifyObject() wrapper emits one measurement per inner verifyProof()", async () => {
|
|
510
761
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
762
|
+
const { "@context": _proofContext, ...embeddedProof } = await proof.toJsonLd({
|
|
763
|
+
format: "compact",
|
|
764
|
+
contextLoader: mockDocumentLoader
|
|
765
|
+
});
|
|
511
766
|
assert(await verifyObject(Create, {
|
|
512
767
|
...jsonLd,
|
|
513
|
-
proof:
|
|
514
|
-
format: "compact",
|
|
515
|
-
contextLoader: mockDocumentLoader
|
|
516
|
-
})
|
|
768
|
+
proof: embeddedProof
|
|
517
769
|
}, {
|
|
518
770
|
documentLoader: mockDocumentLoader,
|
|
519
771
|
contextLoader: mockDocumentLoader,
|
|
@@ -547,6 +799,1012 @@ test("verifyProof() records verification duration metric", async (t) => {
|
|
|
547
799
|
assertFalse("object_integrity_proofs.cryptosuite" in measurements[0].attributes);
|
|
548
800
|
});
|
|
549
801
|
});
|
|
802
|
+
test("proof verification processes raw declaration contexts only for observers", async (t) => {
|
|
803
|
+
const options = {
|
|
804
|
+
documentLoader: mockDocumentLoader,
|
|
805
|
+
contextLoader: mockDocumentLoader
|
|
806
|
+
};
|
|
807
|
+
const proofUrl = `ap://did:key:${portableDidMethod}/proofs/diagnostics`;
|
|
808
|
+
const signed = await signPortableJsonLd({
|
|
809
|
+
"@context": portableContext,
|
|
810
|
+
id: `ap://did:key:${portableDidMethod}/objects/diagnostics`,
|
|
811
|
+
type: "Note",
|
|
812
|
+
attributedTo: `ap://did:key:${portableDidMethod}/actor`,
|
|
813
|
+
content: "Observed proof"
|
|
814
|
+
}, { proofOptions: { id: proofUrl } });
|
|
815
|
+
const rawProof = signed.proof;
|
|
816
|
+
const proof = await DataIntegrityProof.fromJsonLd(rawProof, options);
|
|
817
|
+
const referenced = { ...signed };
|
|
818
|
+
delete referenced.proof;
|
|
819
|
+
referenced["https://w3id.org/security#proof"] = { "@graph": [{ "@id": proofUrl }] };
|
|
820
|
+
const cases = [
|
|
821
|
+
{
|
|
822
|
+
name: "object",
|
|
823
|
+
verify: async (options) => await verifyObject(Note, structuredClone(signed), options) != null
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: "portable",
|
|
827
|
+
verify: async (options) => (await verifyPortableObjectProof(structuredClone(signed), options)).verified
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "map-local",
|
|
831
|
+
verify: async (options) => await verifyMapLocalProof(structuredClone(signed), options) != null
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
name: "proof",
|
|
835
|
+
verify: async (options) => await verifyProof(structuredClone(signed), proof, options) != null
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
name: "remote proof",
|
|
839
|
+
verify: async (options) => await verifyObject(Note, structuredClone(referenced), {
|
|
840
|
+
...options,
|
|
841
|
+
documentLoader: (url) => Promise.resolve({
|
|
842
|
+
contextUrl: null,
|
|
843
|
+
documentUrl: url,
|
|
844
|
+
document: structuredClone(rawProof)
|
|
845
|
+
})
|
|
846
|
+
}) != null
|
|
847
|
+
}
|
|
848
|
+
];
|
|
849
|
+
for (const { name, verify } of cases) await t.step(name, async () => {
|
|
850
|
+
const original = jsonld.processContext;
|
|
851
|
+
let calls = 0;
|
|
852
|
+
jsonld.processContext = (...args) => {
|
|
853
|
+
calls++;
|
|
854
|
+
return original(...args);
|
|
855
|
+
};
|
|
856
|
+
try {
|
|
857
|
+
assert(await verify(options));
|
|
858
|
+
const unobservedCalls = calls;
|
|
859
|
+
calls = 0;
|
|
860
|
+
assert(await verify({
|
|
861
|
+
...options,
|
|
862
|
+
[verificationObservation]: {
|
|
863
|
+
attempts: [],
|
|
864
|
+
attempt: { checks: [] }
|
|
865
|
+
}
|
|
866
|
+
}));
|
|
867
|
+
assert(calls > unobservedCalls, "Raw declaration processing should be absent from unobserved verification");
|
|
868
|
+
} finally {
|
|
869
|
+
jsonld.processContext = original;
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
test("verifyPortableObjectProof()", async (t) => {
|
|
874
|
+
const options = {
|
|
875
|
+
documentLoader() {
|
|
876
|
+
throw new TypeError("did:key must not use the document loader");
|
|
877
|
+
},
|
|
878
|
+
contextLoader: mockDocumentLoader
|
|
879
|
+
};
|
|
880
|
+
const objectId = `ap://did:key:${portableDidMethod}/objects/1`;
|
|
881
|
+
const unsignedObject = {
|
|
882
|
+
"@context": portableContext,
|
|
883
|
+
id: objectId,
|
|
884
|
+
type: "Note",
|
|
885
|
+
attributedTo: `ap://did:key:${portableDidMethod}/actor`,
|
|
886
|
+
content: "Portable note"
|
|
887
|
+
};
|
|
888
|
+
await t.step("verifies a portable object with a matching DID proof", async () => {
|
|
889
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject), options);
|
|
890
|
+
assert(result.verified);
|
|
891
|
+
assertEquals(result.keys.length, 1);
|
|
892
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
893
|
+
});
|
|
894
|
+
await t.step("rejects unauthenticated and expired proof options", async () => {
|
|
895
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
896
|
+
const tamperedResult = await verifyPortableObjectProof({
|
|
897
|
+
...signed,
|
|
898
|
+
proof: {
|
|
899
|
+
...signed.proof,
|
|
900
|
+
expires: "3000-01-01T00:00:00Z"
|
|
901
|
+
}
|
|
902
|
+
}, options);
|
|
903
|
+
assertFalse(tamperedResult.verified);
|
|
904
|
+
assertEquals(tamperedResult.reason, {
|
|
905
|
+
type: "invalidProof",
|
|
906
|
+
proofIndex: 0
|
|
907
|
+
});
|
|
908
|
+
const expiredResult = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { proofOptions: { expires: "2000-01-01T00:00:00Z" } }), options);
|
|
909
|
+
assertFalse(expiredResult.verified);
|
|
910
|
+
assertEquals(expiredResult.reason, {
|
|
911
|
+
type: "invalidProof",
|
|
912
|
+
proofIndex: 0
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
await t.step("verifies aliased proof properties", async () => {
|
|
916
|
+
const proofAlias = "integrityProof";
|
|
917
|
+
const proofIri = "https://w3id.org/security#proof";
|
|
918
|
+
for (const { context, proofProperty, typeProperty, type } of [
|
|
919
|
+
{
|
|
920
|
+
context: { [proofAlias]: proofIri },
|
|
921
|
+
proofProperty: proofAlias,
|
|
922
|
+
typeProperty: "type",
|
|
923
|
+
type: "Note"
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
context: { PortableNote: {
|
|
927
|
+
"@id": "https://www.w3.org/ns/activitystreams#Note",
|
|
928
|
+
"@context": { [proofAlias]: proofIri }
|
|
929
|
+
} },
|
|
930
|
+
proofProperty: proofAlias,
|
|
931
|
+
typeProperty: "type",
|
|
932
|
+
type: "PortableNote"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
context: {
|
|
936
|
+
kind: "@type",
|
|
937
|
+
PortableNote: {
|
|
938
|
+
"@id": "https://www.w3.org/ns/activitystreams#Note",
|
|
939
|
+
"@context": { [proofAlias]: proofIri }
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
proofProperty: proofAlias,
|
|
943
|
+
typeProperty: "kind",
|
|
944
|
+
type: "PortableNote"
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
context: { sec: "https://w3id.org/security#" },
|
|
948
|
+
proofProperty: "sec:proof",
|
|
949
|
+
typeProperty: "type",
|
|
950
|
+
type: "Note"
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
context: { sec: {
|
|
954
|
+
"@id": "https://w3id.org/security#",
|
|
955
|
+
"@prefix": true
|
|
956
|
+
} },
|
|
957
|
+
proofProperty: "sec:proof",
|
|
958
|
+
typeProperty: "type",
|
|
959
|
+
type: "Note"
|
|
960
|
+
}
|
|
961
|
+
]) {
|
|
962
|
+
const document = {
|
|
963
|
+
...unsignedObject,
|
|
964
|
+
"@context": [...portableContext, context]
|
|
965
|
+
};
|
|
966
|
+
delete document.type;
|
|
967
|
+
document[typeProperty] = type;
|
|
968
|
+
const { proof, ...signed } = await signPortableJsonLd(document);
|
|
969
|
+
const result = await verifyPortableObjectProof({
|
|
970
|
+
...signed,
|
|
971
|
+
[proofProperty]: proof
|
|
972
|
+
}, options);
|
|
973
|
+
assert(result.verified);
|
|
974
|
+
assertEquals(result.keys.length, 1);
|
|
975
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
await t.step("verifies proof aliases from caller-loaded contexts", async () => {
|
|
979
|
+
const contextUrl = "https://context.example/security";
|
|
980
|
+
const proofAlias = "integrityProof";
|
|
981
|
+
const proofIri = "https://w3id.org/security#proof";
|
|
982
|
+
let contextLoads = 0;
|
|
983
|
+
const contextLoader = async (url) => {
|
|
984
|
+
if (url !== contextUrl) return await mockDocumentLoader(url);
|
|
985
|
+
contextLoads++;
|
|
986
|
+
return {
|
|
987
|
+
contextUrl: null,
|
|
988
|
+
documentUrl: url,
|
|
989
|
+
document: { "@context": { [proofAlias]: proofIri } }
|
|
990
|
+
};
|
|
991
|
+
};
|
|
992
|
+
const { proof, ...signed } = await signPortableJsonLd({
|
|
993
|
+
...unsignedObject,
|
|
994
|
+
"@context": [...portableContext, contextUrl]
|
|
995
|
+
});
|
|
996
|
+
const result = await verifyPortableObjectProof({
|
|
997
|
+
...signed,
|
|
998
|
+
[proofAlias]: proof
|
|
999
|
+
}, {
|
|
1000
|
+
...options,
|
|
1001
|
+
contextLoader
|
|
1002
|
+
});
|
|
1003
|
+
assert(result.verified);
|
|
1004
|
+
assertEquals(result.keys.length, 1);
|
|
1005
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
1006
|
+
assertEquals(contextLoads, 1);
|
|
1007
|
+
});
|
|
1008
|
+
await t.step("verifies portable actors and activities by shape", async () => {
|
|
1009
|
+
for (const document of [{
|
|
1010
|
+
"@context": portableActorContext,
|
|
1011
|
+
id: `ap+ef61://did:key:${portableDidMethod}/actor`,
|
|
1012
|
+
type: "UnknownActorType",
|
|
1013
|
+
inbox: "https://gateway.example/users/alice/inbox",
|
|
1014
|
+
outbox: "https://gateway.example/users/alice/outbox",
|
|
1015
|
+
gateways: ["https://gateway.example"]
|
|
1016
|
+
}, {
|
|
1017
|
+
"@context": portableContext,
|
|
1018
|
+
id: `ap://did:key:${portableDidMethod}/activities/1`,
|
|
1019
|
+
type: "UnknownActivityType",
|
|
1020
|
+
actor: `ap://did:key:${portableDidMethod}/actor`,
|
|
1021
|
+
object: objectId
|
|
1022
|
+
}]) {
|
|
1023
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(document), options);
|
|
1024
|
+
assert(result.verified);
|
|
1025
|
+
assertEquals(result.keys.length, 1);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
await t.step("requires portable actors to have valid gateways", async () => {
|
|
1029
|
+
const actorIds = [
|
|
1030
|
+
`ap://did:key:${portableDidMethod}/actor`,
|
|
1031
|
+
`ap+ef61://did%3Akey%3A${portableDidMethod}/actor`,
|
|
1032
|
+
`https://gateway.example/.well-known/apgateway/did:key:${portableDidMethod}/actor`
|
|
1033
|
+
];
|
|
1034
|
+
const actor = (id, extra) => ({
|
|
1035
|
+
"@context": portableActorContext,
|
|
1036
|
+
id,
|
|
1037
|
+
type: "Person",
|
|
1038
|
+
inbox: `${id}/inbox`,
|
|
1039
|
+
outbox: `${id}/outbox`,
|
|
1040
|
+
...extra
|
|
1041
|
+
});
|
|
1042
|
+
const validGateways = [
|
|
1043
|
+
["https://gateway.example"],
|
|
1044
|
+
["https://gateway.example/"],
|
|
1045
|
+
["https://gateway.example", "http://other.example:8080"]
|
|
1046
|
+
];
|
|
1047
|
+
const invalidGateways = [
|
|
1048
|
+
{},
|
|
1049
|
+
{ gateways: [] },
|
|
1050
|
+
{ gateways: ["https://gateway.example/path"] },
|
|
1051
|
+
{ gateways: ["https://gateway.example/?query"] },
|
|
1052
|
+
{ gateways: ["https://gateway.example/?"] },
|
|
1053
|
+
{ gateways: ["https://gateway.example/#fragment"] },
|
|
1054
|
+
{ gateways: ["https://gateway.example/#"] },
|
|
1055
|
+
{ gateways: ["ftp://gateway.example"] },
|
|
1056
|
+
{ gateways: ["https://user:password@gateway.example"] },
|
|
1057
|
+
{ gateways: ["gateway.example"] },
|
|
1058
|
+
{ gateways: ["https://gateway.example", "https://other.example/path"] }
|
|
1059
|
+
];
|
|
1060
|
+
for (const id of actorIds) {
|
|
1061
|
+
for (const gateways of validGateways) assert((await verifyPortableObjectProof(await signPortableJsonLd(actor(id, { gateways })), options)).verified, `${id} with ${gateways}`);
|
|
1062
|
+
for (const extra of invalidGateways) assertEquals(await verifyPortableObjectProof(await signPortableJsonLd(actor(id, extra)), options), {
|
|
1063
|
+
verified: false,
|
|
1064
|
+
reason: { type: "invalidGateways" }
|
|
1065
|
+
}, `${id} with ${JSON.stringify(extra)}`);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
await t.step("reads gateways in any JSON-LD form", async () => {
|
|
1069
|
+
const id = `ap://did:key:${portableDidMethod}/actor`;
|
|
1070
|
+
const property = "https://w3id.org/fep/ef61/gateways";
|
|
1071
|
+
const documents = [
|
|
1072
|
+
{
|
|
1073
|
+
"@context": portableContext,
|
|
1074
|
+
[property]: { "@list": [{ "@id": "https://gateway.example" }] }
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
"@context": portableContext,
|
|
1078
|
+
[property]: "https://gateway.example"
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
"@context": [...portableContext, { relays: {
|
|
1082
|
+
"@id": property,
|
|
1083
|
+
"@type": "@id",
|
|
1084
|
+
"@container": "@list"
|
|
1085
|
+
} }],
|
|
1086
|
+
relays: ["https://gateway.example"]
|
|
1087
|
+
}
|
|
1088
|
+
];
|
|
1089
|
+
for (const document of documents) assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1090
|
+
...document,
|
|
1091
|
+
id,
|
|
1092
|
+
type: "Person",
|
|
1093
|
+
inbox: `${id}/inbox`,
|
|
1094
|
+
outbox: `${id}/outbox`
|
|
1095
|
+
}), options)).verified, JSON.stringify(document));
|
|
1096
|
+
assertEquals(await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1097
|
+
"@context": portableContext,
|
|
1098
|
+
id,
|
|
1099
|
+
type: "Person",
|
|
1100
|
+
inbox: `${id}/inbox`,
|
|
1101
|
+
outbox: `${id}/outbox`,
|
|
1102
|
+
[property]: [{ "@list": [{ "@id": "https://gateway.example" }] }, { "@list": [{ "@id": "https://other.example" }] }]
|
|
1103
|
+
}), options), {
|
|
1104
|
+
verified: false,
|
|
1105
|
+
reason: { type: "invalidGateways" }
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
await t.step("verifies JCS-signed actors with unmapped gateways", async () => {
|
|
1109
|
+
for (const id of [`ap+ef61://${portableDid}/actor`, `https://gateway.example/.well-known/apgateway/${portableDid}/actor`]) {
|
|
1110
|
+
const signed = await signPortableJsonLd({
|
|
1111
|
+
"@context": portableContext,
|
|
1112
|
+
id,
|
|
1113
|
+
type: "Person",
|
|
1114
|
+
inbox: `${id}/inbox`,
|
|
1115
|
+
outbox: `${id}/outbox`,
|
|
1116
|
+
gateways: ["https://gateway.example", "https://other.example"]
|
|
1117
|
+
});
|
|
1118
|
+
const original = structuredClone(signed);
|
|
1119
|
+
assert((await verifyPortableObjectProof(signed, options)).verified);
|
|
1120
|
+
assertEquals(signed, original);
|
|
1121
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1122
|
+
...signed,
|
|
1123
|
+
gateways: ["https://attacker.example"]
|
|
1124
|
+
}, options), {
|
|
1125
|
+
verified: false,
|
|
1126
|
+
reason: {
|
|
1127
|
+
type: "invalidProof",
|
|
1128
|
+
proofIndex: 0
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
await t.step("rejects malformed unmapped actor gateways", async () => {
|
|
1134
|
+
const id = `ap+ef61://${portableDid}/actor`;
|
|
1135
|
+
for (const gateways of [
|
|
1136
|
+
[],
|
|
1137
|
+
["https://gateway.example", 42],
|
|
1138
|
+
[{ bad: "https://gateway.example" }],
|
|
1139
|
+
["ftp://gateway.example"],
|
|
1140
|
+
["https://gateway.example/path"],
|
|
1141
|
+
["https://gateway.example/?query"],
|
|
1142
|
+
["https://gateway.example/#fragment"],
|
|
1143
|
+
["https://user:password@gateway.example"]
|
|
1144
|
+
]) assertEquals(await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1145
|
+
"@context": portableContext,
|
|
1146
|
+
id,
|
|
1147
|
+
type: "Person",
|
|
1148
|
+
inbox: `${id}/inbox`,
|
|
1149
|
+
outbox: `${id}/outbox`,
|
|
1150
|
+
gateways
|
|
1151
|
+
}), options), {
|
|
1152
|
+
verified: false,
|
|
1153
|
+
reason: { type: "invalidGateways" }
|
|
1154
|
+
}, JSON.stringify(gateways));
|
|
1155
|
+
});
|
|
1156
|
+
await t.step("does not replace context-defined gateway semantics", async () => {
|
|
1157
|
+
const id = `ap+ef61://${portableDid}/actor`;
|
|
1158
|
+
const actor = {
|
|
1159
|
+
"@context": portableContext,
|
|
1160
|
+
id,
|
|
1161
|
+
type: "Person",
|
|
1162
|
+
inbox: `${id}/inbox`,
|
|
1163
|
+
outbox: `${id}/outbox`,
|
|
1164
|
+
gateways: ["https://gateway.example"]
|
|
1165
|
+
};
|
|
1166
|
+
const property = "https://w3id.org/fep/ef61/gateways";
|
|
1167
|
+
for (const extra of [
|
|
1168
|
+
{ [property]: { "@list": [] } },
|
|
1169
|
+
{ [property]: { "@list": [{ "@id": "https://bad.example/path" }] } },
|
|
1170
|
+
{ "@context": [...portableContext, { gateways: null }] },
|
|
1171
|
+
{ "@context": [...portableContext, { gateways: "https://other.example/gateways" }] },
|
|
1172
|
+
{ "@context": [...portableContext, { Person: {
|
|
1173
|
+
"@id": "https://www.w3.org/ns/activitystreams#Person",
|
|
1174
|
+
"@context": { gateways: null }
|
|
1175
|
+
} }] }
|
|
1176
|
+
]) assertEquals(await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1177
|
+
...actor,
|
|
1178
|
+
...extra
|
|
1179
|
+
}), options), {
|
|
1180
|
+
verified: false,
|
|
1181
|
+
reason: { type: "invalidGateways" }
|
|
1182
|
+
}, JSON.stringify(extra));
|
|
1183
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1184
|
+
...actor,
|
|
1185
|
+
gateways: ["https://bad.example/path"],
|
|
1186
|
+
[property]: { "@list": [{ "@id": "https://canonical.example" }] }
|
|
1187
|
+
}), options)).verified);
|
|
1188
|
+
assertEquals(await verifyPortableObjectProof(await signPortableJsonLd(actor, { proofOptions: { cryptosuite: "eddsa-rdfc-2022" } }), options), {
|
|
1189
|
+
verified: false,
|
|
1190
|
+
reason: { type: "invalidGateways" }
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
await t.step("checks gateways only for FEP-2277 actors", async () => {
|
|
1194
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1195
|
+
"@context": portableContext,
|
|
1196
|
+
id: `ap://did:key:${portableDidMethod}/actor`,
|
|
1197
|
+
type: "Person",
|
|
1198
|
+
inbox: `ap://did:key:${portableDidMethod}/actor/inbox`
|
|
1199
|
+
}), options)).verified);
|
|
1200
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject), options)).verified);
|
|
1201
|
+
});
|
|
1202
|
+
await t.step("checks gateways after the proof shape and before the keys", async () => {
|
|
1203
|
+
const id = `ap://did:key:${portableDidMethod}/actor`;
|
|
1204
|
+
const document = {
|
|
1205
|
+
"@context": portableActorContext,
|
|
1206
|
+
id,
|
|
1207
|
+
type: "Person",
|
|
1208
|
+
inbox: `${id}/inbox`,
|
|
1209
|
+
outbox: `${id}/outbox`
|
|
1210
|
+
};
|
|
1211
|
+
assertEquals(await verifyPortableObjectProof(document, options), {
|
|
1212
|
+
verified: false,
|
|
1213
|
+
reason: { type: "missingProof" }
|
|
1214
|
+
});
|
|
1215
|
+
const keyIds = [];
|
|
1216
|
+
const keyCache = {
|
|
1217
|
+
get(keyId) {
|
|
1218
|
+
keyIds.push(keyId.href);
|
|
1219
|
+
return Promise.resolve(void 0);
|
|
1220
|
+
},
|
|
1221
|
+
set(keyId) {
|
|
1222
|
+
keyIds.push(keyId.href);
|
|
1223
|
+
return Promise.resolve();
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1227
|
+
...await signPortableJsonLd(document),
|
|
1228
|
+
name: "Tampered"
|
|
1229
|
+
}, {
|
|
1230
|
+
...options,
|
|
1231
|
+
keyCache
|
|
1232
|
+
}), {
|
|
1233
|
+
verified: false,
|
|
1234
|
+
reason: { type: "invalidGateways" }
|
|
1235
|
+
});
|
|
1236
|
+
assertEquals(keyIds, []);
|
|
1237
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1238
|
+
...document,
|
|
1239
|
+
gateways: ["https://gateway.example"]
|
|
1240
|
+
}), {
|
|
1241
|
+
...options,
|
|
1242
|
+
keyCache
|
|
1243
|
+
})).verified);
|
|
1244
|
+
assert(keyIds.length > 0);
|
|
1245
|
+
});
|
|
1246
|
+
await t.step("uses the normative FEP-2277 precedence order", async () => {
|
|
1247
|
+
for (const document of [{
|
|
1248
|
+
"@context": portableContext,
|
|
1249
|
+
id: `ap://did:key:${portableDidMethod}/actor`,
|
|
1250
|
+
type: "Collection",
|
|
1251
|
+
inbox: "https://gateway.example/users/alice/inbox",
|
|
1252
|
+
outbox: "https://gateway.example/users/alice/outbox",
|
|
1253
|
+
totalItems: 0
|
|
1254
|
+
}, {
|
|
1255
|
+
"@context": portableContext,
|
|
1256
|
+
id: `ap://did:key:${portableDidMethod}/activities/1`,
|
|
1257
|
+
type: "Collection",
|
|
1258
|
+
actor: `ap://did:key:${portableDidMethod}/actor`,
|
|
1259
|
+
totalItems: 0
|
|
1260
|
+
}]) assertEquals(await verifyPortableObjectProof(document, options), {
|
|
1261
|
+
verified: false,
|
|
1262
|
+
reason: { type: "missingProof" }
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
await t.step("accepts portable URI spelling and location-hint variants", async () => {
|
|
1266
|
+
const variants = [
|
|
1267
|
+
`ap+ef61://did%3Akey%3A${portableDidMethod}/objects/1`,
|
|
1268
|
+
`AP://did:key:${portableDidMethod}/objects/1`,
|
|
1269
|
+
`ap://did:key:${portableDidMethod}/objects/1?%40gateway=${encodeURIComponent("https://gateway.example")}`,
|
|
1270
|
+
`ap://did:key:${portableDidMethod}/objects/1#content`
|
|
1271
|
+
];
|
|
1272
|
+
for (const id of variants) assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1273
|
+
...unsignedObject,
|
|
1274
|
+
id
|
|
1275
|
+
}), options)).verified, id);
|
|
1276
|
+
});
|
|
1277
|
+
await t.step("supports matching DID methods resolved by a caller", async () => {
|
|
1278
|
+
const verificationMethod = new URL("did:web:example.com#key");
|
|
1279
|
+
const document = {
|
|
1280
|
+
...unsignedObject,
|
|
1281
|
+
id: "ap://did:web:example.com/objects/1"
|
|
1282
|
+
};
|
|
1283
|
+
let fetches = 0;
|
|
1284
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd(document, { verificationMethod }), {
|
|
1285
|
+
contextLoader: mockDocumentLoader,
|
|
1286
|
+
documentLoader: async (url) => {
|
|
1287
|
+
fetches++;
|
|
1288
|
+
assertEquals(url, verificationMethod.href);
|
|
1289
|
+
return {
|
|
1290
|
+
contextUrl: null,
|
|
1291
|
+
documentUrl: url,
|
|
1292
|
+
document: {
|
|
1293
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
1294
|
+
id: verificationMethod.href,
|
|
1295
|
+
type: "Multikey",
|
|
1296
|
+
controller: "did:web:example.com",
|
|
1297
|
+
publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
})).verified);
|
|
1302
|
+
assertEquals(fetches, 1);
|
|
1303
|
+
});
|
|
1304
|
+
await t.step("caches keys owned by their own DID", async () => {
|
|
1305
|
+
const verificationMethod = new URL("did:web:example.com#key");
|
|
1306
|
+
const document = {
|
|
1307
|
+
...unsignedObject,
|
|
1308
|
+
id: "ap://did:web:example.com/objects/1"
|
|
1309
|
+
};
|
|
1310
|
+
const cached = [];
|
|
1311
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd(document, { verificationMethod }), {
|
|
1312
|
+
contextLoader: mockDocumentLoader,
|
|
1313
|
+
keyCache: {
|
|
1314
|
+
get: () => Promise.resolve(void 0),
|
|
1315
|
+
set: (keyId) => {
|
|
1316
|
+
cached.push(keyId.href);
|
|
1317
|
+
return Promise.resolve();
|
|
1318
|
+
}
|
|
1319
|
+
},
|
|
1320
|
+
documentLoader: async (url) => ({
|
|
1321
|
+
contextUrl: null,
|
|
1322
|
+
documentUrl: url,
|
|
1323
|
+
document: {
|
|
1324
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
1325
|
+
id: verificationMethod.href,
|
|
1326
|
+
type: "Multikey",
|
|
1327
|
+
controller: "did:web:example.com",
|
|
1328
|
+
publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
|
|
1329
|
+
}
|
|
1330
|
+
})
|
|
1331
|
+
})).verified);
|
|
1332
|
+
assertEquals(cached, [verificationMethod.href]);
|
|
1333
|
+
});
|
|
1334
|
+
await t.step("rejects a portable key that names another controller", async () => {
|
|
1335
|
+
const verificationMethod = new URL("did:web:example.com#key");
|
|
1336
|
+
assertEquals((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1337
|
+
...unsignedObject,
|
|
1338
|
+
id: "ap://did:web:example.com/objects/1"
|
|
1339
|
+
}, { verificationMethod }), {
|
|
1340
|
+
contextLoader: mockDocumentLoader,
|
|
1341
|
+
documentLoader: async (url) => ({
|
|
1342
|
+
contextUrl: null,
|
|
1343
|
+
documentUrl: url,
|
|
1344
|
+
document: {
|
|
1345
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
1346
|
+
id: verificationMethod.href,
|
|
1347
|
+
type: "Multikey",
|
|
1348
|
+
controller: "did:web:attacker.example",
|
|
1349
|
+
publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
|
|
1350
|
+
}
|
|
1351
|
+
})
|
|
1352
|
+
})).verified, false);
|
|
1353
|
+
});
|
|
1354
|
+
await t.step("reports a missing proof on portable objects", async () => {
|
|
1355
|
+
assertEquals(await verifyPortableObjectProof(unsignedObject, options), {
|
|
1356
|
+
verified: false,
|
|
1357
|
+
reason: { type: "missingProof" }
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
await t.step("treats compatible identifiers as portable IDs", async () => {
|
|
1361
|
+
const compatibleActorId = `https://gateway.example/.well-known/apgateway/did:key:${portableDidMethod}/actor`;
|
|
1362
|
+
const actor = {
|
|
1363
|
+
"@context": portableActorContext,
|
|
1364
|
+
id: compatibleActorId,
|
|
1365
|
+
type: "Person",
|
|
1366
|
+
inbox: `${compatibleActorId}/inbox`,
|
|
1367
|
+
outbox: `${compatibleActorId}/outbox`,
|
|
1368
|
+
gateways: ["https://gateway.example"]
|
|
1369
|
+
};
|
|
1370
|
+
const signed = await signPortableJsonLd(actor);
|
|
1371
|
+
const result = await verifyPortableObjectProof(signed, options);
|
|
1372
|
+
assert(result.verified);
|
|
1373
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
1374
|
+
assertEquals(signed.id, compatibleActorId);
|
|
1375
|
+
assertEquals(await verifyPortableObjectProof(actor, options), {
|
|
1376
|
+
verified: false,
|
|
1377
|
+
reason: { type: "missingProof" }
|
|
1378
|
+
});
|
|
1379
|
+
const otherId = `https://gateway.example/.well-known/apgateway/did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/activities/1`;
|
|
1380
|
+
assertEquals(await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1381
|
+
"@context": portableContext,
|
|
1382
|
+
id: otherId,
|
|
1383
|
+
type: "Create",
|
|
1384
|
+
actor: compatibleActorId,
|
|
1385
|
+
object: "https://social.example/objects/1"
|
|
1386
|
+
}), options), {
|
|
1387
|
+
verified: false,
|
|
1388
|
+
reason: {
|
|
1389
|
+
type: "verificationMethodMismatch",
|
|
1390
|
+
proofIndex: 0,
|
|
1391
|
+
objectId: new URL(otherId),
|
|
1392
|
+
verificationMethod: portableKeyId
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
await assertRejects(() => verifyPortableObjectProof({
|
|
1396
|
+
...actor,
|
|
1397
|
+
id: `https://user@gateway.example/.well-known/apgateway/did:key:${portableDidMethod}/actor`
|
|
1398
|
+
}, options), TypeError);
|
|
1399
|
+
});
|
|
1400
|
+
await t.step("keeps non-portable documents outside the policy", async () => {
|
|
1401
|
+
for (const document of [
|
|
1402
|
+
{
|
|
1403
|
+
...unsignedObject,
|
|
1404
|
+
id: "https://social.example/objects/1"
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
...unsignedObject,
|
|
1408
|
+
id: "https://gateway.example/.well-known/apgateway/hl:zQmdfTbBqBPQ"
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
"@context": portableContext,
|
|
1412
|
+
type: "Note",
|
|
1413
|
+
content: "Object without an ID"
|
|
1414
|
+
}
|
|
1415
|
+
]) assertEquals(await verifyPortableObjectProof(document, options), {
|
|
1416
|
+
verified: false,
|
|
1417
|
+
reason: { type: "notPortableObject" }
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
await t.step("short-circuits clearly non-portable raw IDs", async () => {
|
|
1421
|
+
let contextLoads = 0;
|
|
1422
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1423
|
+
"@context": "https://attacker.example/context",
|
|
1424
|
+
"@id": "https://social.example/objects/1"
|
|
1425
|
+
}, {
|
|
1426
|
+
...options,
|
|
1427
|
+
contextLoader() {
|
|
1428
|
+
contextLoads++;
|
|
1429
|
+
throw new TypeError("unexpected context load");
|
|
1430
|
+
}
|
|
1431
|
+
}), {
|
|
1432
|
+
verified: false,
|
|
1433
|
+
reason: { type: "notPortableObject" }
|
|
1434
|
+
});
|
|
1435
|
+
assertEquals(contextLoads, 0);
|
|
1436
|
+
});
|
|
1437
|
+
await t.step("distinguishes unsecured and signed collections", async () => {
|
|
1438
|
+
const collection = {
|
|
1439
|
+
"@context": portableContext,
|
|
1440
|
+
id: `ap://did:key:${portableDidMethod}/collections/1`,
|
|
1441
|
+
type: "Note",
|
|
1442
|
+
totalItems: 0
|
|
1443
|
+
};
|
|
1444
|
+
assertEquals(await verifyPortableObjectProof(collection, options), {
|
|
1445
|
+
verified: false,
|
|
1446
|
+
reason: { type: "unsecuredCollection" }
|
|
1447
|
+
});
|
|
1448
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd(collection), options)).verified);
|
|
1449
|
+
});
|
|
1450
|
+
await t.step("classifies unsupported core types without using type", async () => {
|
|
1451
|
+
const cases = [
|
|
1452
|
+
{
|
|
1453
|
+
document: {
|
|
1454
|
+
...unsignedObject,
|
|
1455
|
+
href: "https://example.com/"
|
|
1456
|
+
},
|
|
1457
|
+
objectType: "link"
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
document: {
|
|
1461
|
+
...unsignedObject,
|
|
1462
|
+
"https://w3id.org/security#publicKeyMultibase": "z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2"
|
|
1463
|
+
},
|
|
1464
|
+
objectType: "verificationMethod"
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
document: {
|
|
1468
|
+
...unsignedObject,
|
|
1469
|
+
"https://w3id.org/security#publicKeyPem": "not-a-real-key"
|
|
1470
|
+
},
|
|
1471
|
+
objectType: "publicKey"
|
|
1472
|
+
}
|
|
1473
|
+
];
|
|
1474
|
+
for (const { document, objectType } of cases) assertEquals(await verifyPortableObjectProof(document, options), {
|
|
1475
|
+
verified: false,
|
|
1476
|
+
reason: {
|
|
1477
|
+
type: "unsupportedObjectType",
|
|
1478
|
+
objectType
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1482
|
+
await t.step("rejects non-DID verification methods before fetching", async () => {
|
|
1483
|
+
let fetched = false;
|
|
1484
|
+
const verificationMethod = new URL("https://attacker.example/keys/ed25519");
|
|
1485
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), {
|
|
1486
|
+
contextLoader: mockDocumentLoader,
|
|
1487
|
+
documentLoader() {
|
|
1488
|
+
fetched = true;
|
|
1489
|
+
throw new TypeError("unexpected fetch");
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
assertFalse(result.verified);
|
|
1493
|
+
assertEquals(result.reason, {
|
|
1494
|
+
type: "unsupportedVerificationMethod",
|
|
1495
|
+
proofIndex: 0,
|
|
1496
|
+
verificationMethod
|
|
1497
|
+
});
|
|
1498
|
+
assertFalse(fetched);
|
|
1499
|
+
});
|
|
1500
|
+
await t.step("rejects malformed DID verification methods", async () => {
|
|
1501
|
+
const verificationMethod = new URL("did:invalid");
|
|
1502
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), options);
|
|
1503
|
+
assertFalse(result.verified);
|
|
1504
|
+
assertEquals(result.reason, {
|
|
1505
|
+
type: "unsupportedVerificationMethod",
|
|
1506
|
+
proofIndex: 0,
|
|
1507
|
+
verificationMethod
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
await t.step("rejects a proof from another DID before fetching", async () => {
|
|
1511
|
+
let fetched = false;
|
|
1512
|
+
const verificationMethod = new URL("did:key:z6MkMallory#z6MkMallory");
|
|
1513
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), {
|
|
1514
|
+
contextLoader: mockDocumentLoader,
|
|
1515
|
+
documentLoader() {
|
|
1516
|
+
fetched = true;
|
|
1517
|
+
throw new TypeError("unexpected fetch");
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
assertFalse(result.verified);
|
|
1521
|
+
assertEquals(result.reason, {
|
|
1522
|
+
type: "verificationMethodMismatch",
|
|
1523
|
+
proofIndex: 0,
|
|
1524
|
+
objectId: parseIri(objectId),
|
|
1525
|
+
verificationMethod
|
|
1526
|
+
});
|
|
1527
|
+
assertFalse(fetched);
|
|
1528
|
+
});
|
|
1529
|
+
await t.step("rejects proofs with multiple verification methods", async () => {
|
|
1530
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1531
|
+
const proof = signed.proof;
|
|
1532
|
+
for (const additionalVerificationMethod of ["did:key:z6MkMallory#z6MkMallory", "https://attacker.example/key"]) assertEquals(await verifyPortableObjectProof({
|
|
1533
|
+
...signed,
|
|
1534
|
+
proof: {
|
|
1535
|
+
...proof,
|
|
1536
|
+
verificationMethod: [portableKeyId.href, additionalVerificationMethod]
|
|
1537
|
+
}
|
|
1538
|
+
}, options), {
|
|
1539
|
+
verified: false,
|
|
1540
|
+
reason: {
|
|
1541
|
+
type: "invalidProof",
|
|
1542
|
+
proofIndex: 0
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
});
|
|
1546
|
+
await t.step("rejects multiple values in functional proof fields", async () => {
|
|
1547
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1548
|
+
const proof = signed.proof;
|
|
1549
|
+
const cases = [
|
|
1550
|
+
["cryptosuite", "eddsa-jcs-2022"],
|
|
1551
|
+
["proofPurpose", "authentication"],
|
|
1552
|
+
["proofValue", proof.proofValue],
|
|
1553
|
+
["created", "2024-01-01T00:00:00Z"]
|
|
1554
|
+
];
|
|
1555
|
+
for (const [field, additionalValue] of cases) {
|
|
1556
|
+
const originalValue = proof[field];
|
|
1557
|
+
assert(originalValue != null);
|
|
1558
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1559
|
+
...signed,
|
|
1560
|
+
proof: {
|
|
1561
|
+
...proof,
|
|
1562
|
+
[field]: [originalValue, additionalValue]
|
|
1563
|
+
}
|
|
1564
|
+
}, options), {
|
|
1565
|
+
verified: false,
|
|
1566
|
+
reason: {
|
|
1567
|
+
type: "invalidProof",
|
|
1568
|
+
proofIndex: 0
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
await t.step("requires exactly the DataIntegrityProof type", async () => {
|
|
1574
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1575
|
+
const proof = signed.proof;
|
|
1576
|
+
const missingType = { ...proof };
|
|
1577
|
+
delete missingType.type;
|
|
1578
|
+
for (const invalidProof of [
|
|
1579
|
+
missingType,
|
|
1580
|
+
{
|
|
1581
|
+
...proof,
|
|
1582
|
+
type: "Object"
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
...proof,
|
|
1586
|
+
type: ["DataIntegrityProof", "Object"]
|
|
1587
|
+
}
|
|
1588
|
+
]) assertEquals(await verifyPortableObjectProof({
|
|
1589
|
+
...signed,
|
|
1590
|
+
proof: invalidProof
|
|
1591
|
+
}, options), {
|
|
1592
|
+
verified: false,
|
|
1593
|
+
reason: {
|
|
1594
|
+
type: "invalidProof",
|
|
1595
|
+
proofIndex: 0
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
});
|
|
1599
|
+
await t.step("reports cryptographic failures separately", async () => {
|
|
1600
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1601
|
+
...await signPortableJsonLd(unsignedObject),
|
|
1602
|
+
content: "Tampered"
|
|
1603
|
+
}, options), {
|
|
1604
|
+
verified: false,
|
|
1605
|
+
reason: {
|
|
1606
|
+
type: "invalidProof",
|
|
1607
|
+
proofIndex: 0
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
});
|
|
1611
|
+
await t.step("reports malformed proof structures as invalid", async () => {
|
|
1612
|
+
for (const proof of [{ type: "DataIntegrityProof" }, {
|
|
1613
|
+
type: "DataIntegrityProof",
|
|
1614
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
1615
|
+
verificationMethod: portableKeyId.href,
|
|
1616
|
+
proofPurpose: "assertionMethod",
|
|
1617
|
+
proofValue: "zInvalid",
|
|
1618
|
+
created: "not-an-instant"
|
|
1619
|
+
}]) assertEquals(await verifyPortableObjectProof({
|
|
1620
|
+
...unsignedObject,
|
|
1621
|
+
proof
|
|
1622
|
+
}, options), {
|
|
1623
|
+
verified: false,
|
|
1624
|
+
reason: {
|
|
1625
|
+
type: "invalidProof",
|
|
1626
|
+
proofIndex: 0
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1630
|
+
await t.step("accepts multiple valid proofs in input order", async () => {
|
|
1631
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1632
|
+
const proof = signed.proof;
|
|
1633
|
+
const result = await verifyPortableObjectProof({
|
|
1634
|
+
...signed,
|
|
1635
|
+
proof: [proof, structuredClone(proof)]
|
|
1636
|
+
}, options);
|
|
1637
|
+
assert(result.verified);
|
|
1638
|
+
assertEquals(result.keys.length, 2);
|
|
1639
|
+
assertEquals(result.keys.map((key) => key.id), [portableKeyId, portableKeyId]);
|
|
1640
|
+
});
|
|
1641
|
+
await t.step("rejects a tampered duplicate proof", async () => {
|
|
1642
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1643
|
+
const proof = signed.proof;
|
|
1644
|
+
assertFalse((await verifyPortableObjectProof({
|
|
1645
|
+
...signed,
|
|
1646
|
+
proof: [proof, {
|
|
1647
|
+
...structuredClone(proof),
|
|
1648
|
+
expires: "3000-01-01T00:00:00Z"
|
|
1649
|
+
}]
|
|
1650
|
+
}, options)).verified);
|
|
1651
|
+
});
|
|
1652
|
+
await t.step("matches proofs across aliased properties", async () => {
|
|
1653
|
+
const document = {
|
|
1654
|
+
...unsignedObject,
|
|
1655
|
+
"@context": [...portableContext, {
|
|
1656
|
+
firstProof: "https://w3id.org/security#proof",
|
|
1657
|
+
secondProof: "https://w3id.org/security#proof"
|
|
1658
|
+
}]
|
|
1659
|
+
};
|
|
1660
|
+
const firstSigned = await signPortableJsonLd(document, { proofOptions: { domain: "first.example" } });
|
|
1661
|
+
const secondSigned = await signPortableJsonLd(document, { proofOptions: { domain: "second.example" } });
|
|
1662
|
+
const result = await verifyPortableObjectProof({
|
|
1663
|
+
...document,
|
|
1664
|
+
secondProof: secondSigned.proof,
|
|
1665
|
+
firstProof: firstSigned.proof
|
|
1666
|
+
}, options);
|
|
1667
|
+
assert(result.verified);
|
|
1668
|
+
assertEquals(result.keys.length, 2);
|
|
1669
|
+
});
|
|
1670
|
+
await t.step("canonicalizes the document only once for multiple proofs", async () => {
|
|
1671
|
+
async function countContentReads(proofCount) {
|
|
1672
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1673
|
+
const proof = signed.proof;
|
|
1674
|
+
const document = {
|
|
1675
|
+
...signed,
|
|
1676
|
+
proof: Array.from({ length: proofCount }, () => structuredClone(proof))
|
|
1677
|
+
};
|
|
1678
|
+
let contentReads = 0;
|
|
1679
|
+
Object.defineProperty(document, "content", {
|
|
1680
|
+
configurable: true,
|
|
1681
|
+
enumerable: true,
|
|
1682
|
+
get() {
|
|
1683
|
+
contentReads++;
|
|
1684
|
+
return unsignedObject.content;
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
const result = await verifyPortableObjectProof(document, options);
|
|
1688
|
+
assert(result.verified);
|
|
1689
|
+
assertEquals(result.keys.length, proofCount);
|
|
1690
|
+
return contentReads;
|
|
1691
|
+
}
|
|
1692
|
+
assertEquals(await countContentReads(3), await countContentReads(1));
|
|
1693
|
+
});
|
|
1694
|
+
await t.step("requires every proof to verify", async () => {
|
|
1695
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1696
|
+
const proof = signed.proof;
|
|
1697
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1698
|
+
...signed,
|
|
1699
|
+
proof: [proof, {
|
|
1700
|
+
...proof,
|
|
1701
|
+
proofValue: "zInvalid"
|
|
1702
|
+
}]
|
|
1703
|
+
}, options), {
|
|
1704
|
+
verified: false,
|
|
1705
|
+
reason: {
|
|
1706
|
+
type: "invalidProof",
|
|
1707
|
+
proofIndex: 1
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
await t.step("checks every proof policy before resolving keys", async () => {
|
|
1712
|
+
const didWebId = "ap://did:web:example.com/objects/1";
|
|
1713
|
+
const didWebMethod = new URL("did:web:example.com#key");
|
|
1714
|
+
const didWebSigned = await signPortableJsonLd({
|
|
1715
|
+
...unsignedObject,
|
|
1716
|
+
id: didWebId
|
|
1717
|
+
}, { verificationMethod: didWebMethod });
|
|
1718
|
+
const httpMethod = new URL("https://attacker.example/key");
|
|
1719
|
+
const httpSigned = await signPortableJsonLd({
|
|
1720
|
+
...unsignedObject,
|
|
1721
|
+
id: didWebId
|
|
1722
|
+
}, { verificationMethod: httpMethod });
|
|
1723
|
+
let fetched = false;
|
|
1724
|
+
const result = await verifyPortableObjectProof({
|
|
1725
|
+
...didWebSigned,
|
|
1726
|
+
proof: [didWebSigned.proof, httpSigned.proof]
|
|
1727
|
+
}, {
|
|
1728
|
+
contextLoader: mockDocumentLoader,
|
|
1729
|
+
documentLoader() {
|
|
1730
|
+
fetched = true;
|
|
1731
|
+
throw new TypeError("unexpected fetch");
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
assertFalse(result.verified);
|
|
1735
|
+
assertEquals(result.reason, {
|
|
1736
|
+
type: "unsupportedVerificationMethod",
|
|
1737
|
+
proofIndex: 1,
|
|
1738
|
+
verificationMethod: httpMethod
|
|
1739
|
+
});
|
|
1740
|
+
assertFalse(fetched);
|
|
1741
|
+
});
|
|
1742
|
+
await t.step("accepts the expanded proof property", async () => {
|
|
1743
|
+
const { proof, ...document } = await signPortableJsonLd(unsignedObject);
|
|
1744
|
+
const expandedProof = await (await DataIntegrityProof.fromJsonLd(proof, {
|
|
1745
|
+
contextLoader: mockDocumentLoader,
|
|
1746
|
+
documentLoader: mockDocumentLoader
|
|
1747
|
+
})).toJsonLd({
|
|
1748
|
+
format: "expand",
|
|
1749
|
+
contextLoader: mockDocumentLoader
|
|
1750
|
+
});
|
|
1751
|
+
assert((await verifyPortableObjectProof({
|
|
1752
|
+
...document,
|
|
1753
|
+
"https://w3id.org/security#proof": expandedProof
|
|
1754
|
+
}, options)).verified);
|
|
1755
|
+
});
|
|
1756
|
+
await t.step("classifies JSON-LD aliases by their expanded properties", async () => {
|
|
1757
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1758
|
+
"@context": [...portableContext, { performedBy: "https://www.w3.org/ns/activitystreams#actor" }],
|
|
1759
|
+
id: `ap://did:key:${portableDidMethod}/activities/aliased`,
|
|
1760
|
+
type: "UnknownActivity",
|
|
1761
|
+
performedBy: `ap://did:key:${portableDidMethod}/actor`
|
|
1762
|
+
}), options)).verified);
|
|
1763
|
+
});
|
|
1764
|
+
await t.step("does not claim to verify embedded portable objects", async () => {
|
|
1765
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1766
|
+
"@context": portableContext,
|
|
1767
|
+
id: `ap://did:key:${portableDidMethod}/activities/outer`,
|
|
1768
|
+
type: "Create",
|
|
1769
|
+
actor: `ap://did:key:${portableDidMethod}/actor`,
|
|
1770
|
+
object: {
|
|
1771
|
+
id: `ap://did:key:${portableDidMethod}/objects/embedded`,
|
|
1772
|
+
type: "Note",
|
|
1773
|
+
content: "Unsigned embedded object"
|
|
1774
|
+
}
|
|
1775
|
+
}), options)).verified);
|
|
1776
|
+
});
|
|
1777
|
+
await t.step("rejects malformed roots and portable IDs", async () => {
|
|
1778
|
+
await assertRejects(() => verifyPortableObjectProof([], options), TypeError, "single JSON-LD object");
|
|
1779
|
+
await assertRejects(() => verifyPortableObjectProof({
|
|
1780
|
+
...unsignedObject,
|
|
1781
|
+
id: `ap://did:key:${portableDidMethod}`
|
|
1782
|
+
}, options), TypeError);
|
|
1783
|
+
await assertRejects(() => verifyPortableObjectProof({
|
|
1784
|
+
...unsignedObject,
|
|
1785
|
+
id: "ap://did%ZZkey/objects/1"
|
|
1786
|
+
}, options), TypeError);
|
|
1787
|
+
});
|
|
1788
|
+
await t.step("emits metrics only for attempted proof verification", async () => {
|
|
1789
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1790
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1791
|
+
const proof = signed.proof;
|
|
1792
|
+
assert((await verifyPortableObjectProof({
|
|
1793
|
+
...signed,
|
|
1794
|
+
proof: [proof, structuredClone(proof)]
|
|
1795
|
+
}, {
|
|
1796
|
+
...options,
|
|
1797
|
+
meterProvider
|
|
1798
|
+
})).verified);
|
|
1799
|
+
assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 2);
|
|
1800
|
+
const [rejectedMeterProvider, rejectedRecorder] = createTestMeterProvider();
|
|
1801
|
+
await verifyPortableObjectProof(unsignedObject, {
|
|
1802
|
+
...options,
|
|
1803
|
+
meterProvider: rejectedMeterProvider
|
|
1804
|
+
});
|
|
1805
|
+
assertEquals(rejectedRecorder.getMeasurements("activitypub.signature.verification.duration").length, 0);
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
550
1808
|
test("verifyObject()", async () => {
|
|
551
1809
|
const options = {
|
|
552
1810
|
documentLoader: mockDocumentLoader,
|
|
@@ -593,7 +1851,7 @@ test("verifyObject()", async () => {
|
|
|
593
1851
|
test("verifyObject() accepts did:key proofs for matching portable attribution origin", async () => {
|
|
594
1852
|
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
595
1853
|
const keyId = new URL(`${did}#${did.substring(8)}`);
|
|
596
|
-
const
|
|
1854
|
+
const jsonLd = await (await signObject(new Note({
|
|
597
1855
|
id: parseIri(`ap://did:key:${did.substring(8)}/objects/1`),
|
|
598
1856
|
attribution: parseIri(`ap://did:key:${did.substring(8)}/actor`),
|
|
599
1857
|
content: "Portable note"
|
|
@@ -604,7 +1862,8 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
|
|
|
604
1862
|
format: "compact",
|
|
605
1863
|
contextLoader: mockDocumentLoader,
|
|
606
1864
|
context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
|
|
607
|
-
})
|
|
1865
|
+
});
|
|
1866
|
+
const verified = await verifyObject(Note, jsonLd, {
|
|
608
1867
|
documentLoader() {
|
|
609
1868
|
throw new TypeError("did:key must not use the document loader");
|
|
610
1869
|
},
|
|
@@ -612,6 +1871,199 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
|
|
|
612
1871
|
});
|
|
613
1872
|
assertInstanceOf(verified, Note);
|
|
614
1873
|
assertEquals(verified.content, "Portable note");
|
|
1874
|
+
assert(typeof jsonLd === "object" && jsonLd != null && !Array.isArray(jsonLd));
|
|
1875
|
+
const rawProof = jsonLd.proof;
|
|
1876
|
+
assert(typeof rawProof === "object" && rawProof != null && !Array.isArray(rawProof));
|
|
1877
|
+
assertEquals(await verifyObject(Note, {
|
|
1878
|
+
...jsonLd,
|
|
1879
|
+
proof: [rawProof, {
|
|
1880
|
+
...structuredClone(rawProof),
|
|
1881
|
+
expires: "3000-01-01T00:00:00Z"
|
|
1882
|
+
}]
|
|
1883
|
+
}, {
|
|
1884
|
+
documentLoader() {
|
|
1885
|
+
throw new TypeError("did:key must not use the document loader");
|
|
1886
|
+
},
|
|
1887
|
+
contextLoader: mockDocumentLoader
|
|
1888
|
+
}), null);
|
|
1889
|
+
async function verifyRemotelyReferencedProof(proofOptions = {}, proofUrl = "https://proof.example/proofs/1") {
|
|
1890
|
+
const remotelyReferencedJsonLd = { ...jsonLd };
|
|
1891
|
+
delete remotelyReferencedJsonLd.proof;
|
|
1892
|
+
remotelyReferencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
|
|
1893
|
+
let proofFetches = 0;
|
|
1894
|
+
const result = await verifyObject(Note, remotelyReferencedJsonLd, {
|
|
1895
|
+
documentLoader(url) {
|
|
1896
|
+
if (url !== formatIri(parseIri(proofUrl))) throw new TypeError(`unexpected document URL: ${url}`);
|
|
1897
|
+
proofFetches++;
|
|
1898
|
+
return Promise.resolve({
|
|
1899
|
+
contextUrl: null,
|
|
1900
|
+
document: {
|
|
1901
|
+
"@context": jsonLd["@context"],
|
|
1902
|
+
...structuredClone(rawProof),
|
|
1903
|
+
...proofOptions
|
|
1904
|
+
},
|
|
1905
|
+
documentUrl: url
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
contextLoader: mockDocumentLoader
|
|
1909
|
+
});
|
|
1910
|
+
assertEquals(proofFetches, 1);
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
assertInstanceOf(await verifyRemotelyReferencedProof(), Note);
|
|
1914
|
+
assertInstanceOf(await verifyRemotelyReferencedProof({}, `ap://did:key:${did.substring(8)}/proofs/1`), Note);
|
|
1915
|
+
assertEquals(await verifyRemotelyReferencedProof({ expires: "3000-01-01T00:00:00Z" }), null);
|
|
1916
|
+
});
|
|
1917
|
+
test("verifyObject() hydrates pending proof references", async () => {
|
|
1918
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
1919
|
+
const method = did.substring(8);
|
|
1920
|
+
const keyId = new URL(`${did}#${method}`);
|
|
1921
|
+
const proofUrl = `ap://did:key:${method}/proofs/1`;
|
|
1922
|
+
const signed = await signPortableJsonLd({
|
|
1923
|
+
"@context": portableContext,
|
|
1924
|
+
id: `ap://did:key:${method}/objects/1`,
|
|
1925
|
+
type: "Note",
|
|
1926
|
+
attributedTo: `ap://did:key:${method}/actor`,
|
|
1927
|
+
content: "Portable note with a referenced proof"
|
|
1928
|
+
}, {
|
|
1929
|
+
verificationMethod: keyId,
|
|
1930
|
+
proofOptions: { id: proofUrl }
|
|
1931
|
+
});
|
|
1932
|
+
const rawProof = signed.proof;
|
|
1933
|
+
const remotelyReferencedJsonLd = { ...signed };
|
|
1934
|
+
delete remotelyReferencedJsonLd.proof;
|
|
1935
|
+
remotelyReferencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
|
|
1936
|
+
let proofFetches = 0;
|
|
1937
|
+
const verified = await verifyObject(Note, remotelyReferencedJsonLd, {
|
|
1938
|
+
documentLoader(url) {
|
|
1939
|
+
assertEquals(url, formatIri(parseIri(proofUrl)));
|
|
1940
|
+
proofFetches++;
|
|
1941
|
+
return Promise.resolve({
|
|
1942
|
+
contextUrl: null,
|
|
1943
|
+
document: structuredClone(rawProof),
|
|
1944
|
+
documentUrl: url
|
|
1945
|
+
});
|
|
1946
|
+
},
|
|
1947
|
+
contextLoader: mockDocumentLoader
|
|
1948
|
+
});
|
|
1949
|
+
assertEquals(proofFetches, 1);
|
|
1950
|
+
assertInstanceOf(verified, Note);
|
|
1951
|
+
});
|
|
1952
|
+
test("verifyObject() does not pass its proof verifier on to cached proofs", async () => {
|
|
1953
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
1954
|
+
const method = did.substring(8);
|
|
1955
|
+
const keyId = new URL(`${did}#${method}`);
|
|
1956
|
+
const proofUrl = `ap://did:key:${method}/proofs/1`;
|
|
1957
|
+
const signed = await signPortableJsonLd({
|
|
1958
|
+
"@context": portableContext,
|
|
1959
|
+
id: `ap://did:key:${method}/objects/1`,
|
|
1960
|
+
type: "Note",
|
|
1961
|
+
attributedTo: `ap://did:key:${method}/actor`,
|
|
1962
|
+
content: "Portable note with a referenced proof"
|
|
1963
|
+
}, {
|
|
1964
|
+
verificationMethod: keyId,
|
|
1965
|
+
proofOptions: { id: proofUrl }
|
|
1966
|
+
});
|
|
1967
|
+
const rawProof = signed.proof;
|
|
1968
|
+
const referencedJsonLd = { ...signed };
|
|
1969
|
+
delete referencedJsonLd.proof;
|
|
1970
|
+
referencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
|
|
1971
|
+
const rootVerifier = async () => ({ verified: false });
|
|
1972
|
+
for (const verifyPortableObject of [void 0, rootVerifier]) {
|
|
1973
|
+
let proofFetches = 0;
|
|
1974
|
+
const verified = await verifyObject(Note, referencedJsonLd, {
|
|
1975
|
+
documentLoader(url) {
|
|
1976
|
+
proofFetches++;
|
|
1977
|
+
return Promise.resolve({
|
|
1978
|
+
contextUrl: null,
|
|
1979
|
+
document: structuredClone(rawProof),
|
|
1980
|
+
documentUrl: url
|
|
1981
|
+
});
|
|
1982
|
+
},
|
|
1983
|
+
contextLoader: mockDocumentLoader,
|
|
1984
|
+
...verifyPortableObject == null ? {} : { verifyPortableObject }
|
|
1985
|
+
});
|
|
1986
|
+
assertInstanceOf(verified, Note);
|
|
1987
|
+
assertEquals(proofFetches, 1);
|
|
1988
|
+
const proofs = await Array.fromAsync(verified.getProofs());
|
|
1989
|
+
assertEquals(proofs.length, 2);
|
|
1990
|
+
assertEquals(proofFetches, 1);
|
|
1991
|
+
for (const proof of proofs) assert(proof._verifyPortableObject === verifyPortableObject);
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
test("verifyObject() hydrates portable proof references with gateway hints", async () => {
|
|
1995
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
1996
|
+
const method = did.substring(8);
|
|
1997
|
+
const keyId = new URL(`${did}#${method}`);
|
|
1998
|
+
const proofId = `ap://did:key:${method}/proofs/1`;
|
|
1999
|
+
const proofReference = `${proofId}?@gateway=https%3A%2F%2Fgateway.example`;
|
|
2000
|
+
const signed = await signPortableJsonLd({
|
|
2001
|
+
"@context": portableContext,
|
|
2002
|
+
id: `ap://did:key:${method}/objects/1`,
|
|
2003
|
+
type: "Note",
|
|
2004
|
+
attributedTo: `ap://did:key:${method}/actor`,
|
|
2005
|
+
content: "Portable note with a hinted proof reference"
|
|
2006
|
+
}, {
|
|
2007
|
+
verificationMethod: keyId,
|
|
2008
|
+
proofOptions: { id: proofId }
|
|
2009
|
+
});
|
|
2010
|
+
const rawProof = signed.proof;
|
|
2011
|
+
const referencedJsonLd = { ...signed };
|
|
2012
|
+
delete referencedJsonLd.proof;
|
|
2013
|
+
referencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofReference }] }];
|
|
2014
|
+
const fetched = [];
|
|
2015
|
+
const verified = await verifyObject(Note, referencedJsonLd, {
|
|
2016
|
+
documentLoader(url) {
|
|
2017
|
+
fetched.push(url);
|
|
2018
|
+
return Promise.resolve({
|
|
2019
|
+
contextUrl: null,
|
|
2020
|
+
document: structuredClone(rawProof),
|
|
2021
|
+
documentUrl: url
|
|
2022
|
+
});
|
|
2023
|
+
},
|
|
2024
|
+
contextLoader: mockDocumentLoader
|
|
2025
|
+
});
|
|
2026
|
+
assertEquals(fetched, [formatIri(parseIri(proofReference))]);
|
|
2027
|
+
assertInstanceOf(verified, Note);
|
|
2028
|
+
});
|
|
2029
|
+
test("verifyObject() rejects portable proof references it cannot dereference", async () => {
|
|
2030
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
2031
|
+
const method = did.substring(8);
|
|
2032
|
+
const keyId = new URL(`${did}#${method}`);
|
|
2033
|
+
const proofUrl = `ap://did:key:${method}/proofs/2`;
|
|
2034
|
+
const signed = await signPortableJsonLd({
|
|
2035
|
+
"@context": portableContext,
|
|
2036
|
+
id: `ap://did:key:${method}/objects/1`,
|
|
2037
|
+
type: "Note",
|
|
2038
|
+
attributedTo: `ap://did:key:${method}/actor`,
|
|
2039
|
+
content: "Portable note with an extra referenced proof"
|
|
2040
|
+
}, { verificationMethod: keyId });
|
|
2041
|
+
const rawProof = signed.proof;
|
|
2042
|
+
const withProofs = (proofs) => {
|
|
2043
|
+
const referencedJsonLd = { ...signed };
|
|
2044
|
+
delete referencedJsonLd.proof;
|
|
2045
|
+
referencedJsonLd["https://w3id.org/security#proof"] = proofs;
|
|
2046
|
+
return referencedJsonLd;
|
|
2047
|
+
};
|
|
2048
|
+
const embedded = { "@graph": [rawProof] };
|
|
2049
|
+
const referenced = { "@graph": [{ "@id": proofUrl }] };
|
|
2050
|
+
for (const [id, referencedJsonLd] of [
|
|
2051
|
+
[`ap://did:key:${method}/proofs/3`, withProofs([embedded, referenced])],
|
|
2052
|
+
[`ap://did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK/proofs/2`, withProofs([embedded, referenced])],
|
|
2053
|
+
[`ap://did:key:${method}/proofs/3`, withProofs([referenced, embedded])]
|
|
2054
|
+
]) assertEquals(await verifyObject(Note, referencedJsonLd, {
|
|
2055
|
+
documentLoader(url) {
|
|
2056
|
+
return Promise.resolve({
|
|
2057
|
+
contextUrl: null,
|
|
2058
|
+
document: {
|
|
2059
|
+
...rawProof,
|
|
2060
|
+
id
|
|
2061
|
+
},
|
|
2062
|
+
documentUrl: url
|
|
2063
|
+
});
|
|
2064
|
+
},
|
|
2065
|
+
contextLoader: mockDocumentLoader
|
|
2066
|
+
}), null);
|
|
615
2067
|
});
|
|
616
2068
|
test("verifyObject() accepts multiple portable attributions from the same did:key origin", async () => {
|
|
617
2069
|
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
@@ -727,5 +2179,290 @@ test("verifyObject() rejects did:key proofs from another portable attribution or
|
|
|
727
2179
|
contextLoader: mockDocumentLoader
|
|
728
2180
|
}), null);
|
|
729
2181
|
});
|
|
2182
|
+
async function signCompatibleNote(attribution, keyId) {
|
|
2183
|
+
const context = ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"];
|
|
2184
|
+
return await (await signObject(new Note({
|
|
2185
|
+
id: new URL(`${attribution}/notes/1`),
|
|
2186
|
+
attribution: new URL(attribution),
|
|
2187
|
+
content: "Note of a compatible-ID actor"
|
|
2188
|
+
}), ed25519PrivateKey, keyId, {
|
|
2189
|
+
contextLoader: mockDocumentLoader,
|
|
2190
|
+
context
|
|
2191
|
+
})).toJsonLd({
|
|
2192
|
+
format: "compact",
|
|
2193
|
+
contextLoader: mockDocumentLoader,
|
|
2194
|
+
context
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
test("verifyObject() authenticates compatible-ID attributions by DID proofs", async () => {
|
|
2198
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
2199
|
+
const keyId = new URL(`${did}#${did.substring(8)}`);
|
|
2200
|
+
const options = {
|
|
2201
|
+
documentLoader() {
|
|
2202
|
+
throw new TypeError("did:key must not use the document loader");
|
|
2203
|
+
},
|
|
2204
|
+
contextLoader: mockDocumentLoader
|
|
2205
|
+
};
|
|
2206
|
+
for (const gateway of ["https://gw1.example", "https://gw2.example"]) assertInstanceOf(await verifyObject(Note, await signCompatibleNote(`${gateway}/.well-known/apgateway/${did}/actor`, keyId), options), Note);
|
|
2207
|
+
assertEquals(await verifyObject(Note, await signCompatibleNote("https://gw1.example/.well-known/apgateway/did:key:z6MkOther/actor", keyId), options), null);
|
|
2208
|
+
});
|
|
2209
|
+
test("verifyObject() does not trust compatible-ID actors by web origin", async () => {
|
|
2210
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
2211
|
+
const attribution = `https://evil.example/.well-known/apgateway/${did}/actor`;
|
|
2212
|
+
const keyId = new URL("https://evil.example/keys/1");
|
|
2213
|
+
const documents = {
|
|
2214
|
+
[keyId.href]: {
|
|
2215
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
2216
|
+
id: keyId.href,
|
|
2217
|
+
type: "Multikey",
|
|
2218
|
+
controller: attribution,
|
|
2219
|
+
publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
|
|
2220
|
+
},
|
|
2221
|
+
[attribution]: {
|
|
2222
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/multikey/v1"],
|
|
2223
|
+
id: attribution,
|
|
2224
|
+
type: "Person",
|
|
2225
|
+
inbox: `${attribution}/inbox`,
|
|
2226
|
+
assertionMethod: keyId.href
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
const fetched = [];
|
|
2230
|
+
assertEquals(await verifyObject(Note, await signCompatibleNote(attribution, keyId), {
|
|
2231
|
+
documentLoader: (url) => {
|
|
2232
|
+
fetched.push(url);
|
|
2233
|
+
const document = documents[url];
|
|
2234
|
+
if (document == null) throw new TypeError(`Unexpected: ${url}`);
|
|
2235
|
+
return Promise.resolve({
|
|
2236
|
+
contextUrl: null,
|
|
2237
|
+
documentUrl: url,
|
|
2238
|
+
document
|
|
2239
|
+
});
|
|
2240
|
+
},
|
|
2241
|
+
contextLoader: mockDocumentLoader
|
|
2242
|
+
}), null);
|
|
2243
|
+
assertEquals(fetched, [keyId.href]);
|
|
2244
|
+
assertEquals(await verifyObject(Note, await signCompatibleNote(`https://user@gw.example/.well-known/apgateway/${did}/actor`, new URL(`${did}#${did.substring(8)}`)), { contextLoader: mockDocumentLoader }), null);
|
|
2245
|
+
});
|
|
2246
|
+
test("signObject() preserves FEP-22cd contexts through proof verification", async () => {
|
|
2247
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
2248
|
+
const keyId = new URL(`${did}#${did.substring(8)}`);
|
|
2249
|
+
const id = parseIri(`ap://${did}/objects/translation`);
|
|
2250
|
+
const json = await (await signObject(new Note({
|
|
2251
|
+
id,
|
|
2252
|
+
attribution: parseIri(`ap://${did}/actor`),
|
|
2253
|
+
content: "A translated note",
|
|
2254
|
+
translations: [new Translation({
|
|
2255
|
+
language: new Intl.Locale("ko"),
|
|
2256
|
+
translator: new URL("https://example.com/translator"),
|
|
2257
|
+
original: id
|
|
2258
|
+
})]
|
|
2259
|
+
}), ed25519PrivateKey, keyId)).toJsonLd({ format: "compact" });
|
|
2260
|
+
assert(typeof json === "object" && json != null && "@context" in json && "proof" in json);
|
|
2261
|
+
assertEquals(json.proof["@context"], json["@context"]);
|
|
2262
|
+
const verified = await verifyObject(Note, json, { documentLoader() {
|
|
2263
|
+
throw new Error("No document fetch expected");
|
|
2264
|
+
} });
|
|
2265
|
+
assertInstanceOf(verified, Note);
|
|
2266
|
+
assertEquals(verified.translations[0].language?.baseName, "ko");
|
|
2267
|
+
});
|
|
2268
|
+
test("verifyObject() rejects a key that claims a forged controller", async () => {
|
|
2269
|
+
const keyId = new URL("https://attacker.example/multikey");
|
|
2270
|
+
const impersonated = "https://example.com/person2";
|
|
2271
|
+
const attackerActor = "https://attacker.example/actor";
|
|
2272
|
+
const multikeyDocument = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
|
|
2273
|
+
const options = {
|
|
2274
|
+
format: "compact",
|
|
2275
|
+
contextLoader: mockDocumentLoader,
|
|
2276
|
+
documentLoader: mockDocumentLoader,
|
|
2277
|
+
context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
|
|
2278
|
+
};
|
|
2279
|
+
const signed = await signObject(new Create({
|
|
2280
|
+
id: new URL("https://attacker.example/activities/1"),
|
|
2281
|
+
actor: new URL(impersonated),
|
|
2282
|
+
object: new Note({
|
|
2283
|
+
id: new URL("https://attacker.example/notes/1"),
|
|
2284
|
+
attribution: new URL(impersonated),
|
|
2285
|
+
content: "Hello world"
|
|
2286
|
+
})
|
|
2287
|
+
}), ed25519PrivateKey, keyId, options);
|
|
2288
|
+
const jsonLd = await signed.toJsonLd(options);
|
|
2289
|
+
const serveKeyControlledBy = (controller) => (resource) => {
|
|
2290
|
+
if (resource === keyId.href) return Promise.resolve({
|
|
2291
|
+
contextUrl: null,
|
|
2292
|
+
documentUrl: resource,
|
|
2293
|
+
document: {
|
|
2294
|
+
...multikeyDocument,
|
|
2295
|
+
id: keyId.href,
|
|
2296
|
+
controller
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
if (resource === attackerActor) return Promise.resolve({
|
|
2300
|
+
contextUrl: null,
|
|
2301
|
+
documentUrl: resource,
|
|
2302
|
+
document: {
|
|
2303
|
+
"@context": [
|
|
2304
|
+
"https://www.w3.org/ns/activitystreams",
|
|
2305
|
+
"https://w3id.org/security/v1",
|
|
2306
|
+
"https://w3id.org/security/multikey/v1",
|
|
2307
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
2308
|
+
"https://www.w3.org/ns/did/v1"
|
|
2309
|
+
],
|
|
2310
|
+
id: resource,
|
|
2311
|
+
type: "Person",
|
|
2312
|
+
assertionMethod: [keyId.href]
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
return mockDocumentLoader(resource);
|
|
2316
|
+
};
|
|
2317
|
+
assertEquals(await verifyObject(Create, jsonLd, {
|
|
2318
|
+
documentLoader: serveKeyControlledBy(impersonated),
|
|
2319
|
+
contextLoader: mockDocumentLoader
|
|
2320
|
+
}), null);
|
|
2321
|
+
const honestLoader = serveKeyControlledBy(attackerActor);
|
|
2322
|
+
let proof = null;
|
|
2323
|
+
for await (const p of signed.getProofs(options)) {
|
|
2324
|
+
proof = p;
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2327
|
+
assertInstanceOf(proof, DataIntegrityProof);
|
|
2328
|
+
assertInstanceOf(await verifyProof(jsonLd, proof, {
|
|
2329
|
+
documentLoader: honestLoader,
|
|
2330
|
+
contextLoader: mockDocumentLoader
|
|
2331
|
+
}), Multikey);
|
|
2332
|
+
assertEquals(await verifyObject(Create, jsonLd, {
|
|
2333
|
+
documentLoader: honestLoader,
|
|
2334
|
+
contextLoader: mockDocumentLoader
|
|
2335
|
+
}), null);
|
|
2336
|
+
});
|
|
2337
|
+
test("verifyPortableObjectProof() verifies objects fetched through gateways", async (t) => {
|
|
2338
|
+
const objectId = `ap://${portableDid}/objects/gateway`;
|
|
2339
|
+
const gatewayUrl = `https://gateway.example/.well-known/apgateway/${portableDid}/objects/gateway`;
|
|
2340
|
+
const signedNote = await signPortableJsonLd({
|
|
2341
|
+
"@context": portableContext,
|
|
2342
|
+
id: objectId,
|
|
2343
|
+
type: "Note",
|
|
2344
|
+
attributedTo: `ap://${portableDid}/actor`,
|
|
2345
|
+
content: "Hello from a gateway"
|
|
2346
|
+
});
|
|
2347
|
+
const getObject = async (document, object = objectId, url = gatewayUrl) => {
|
|
2348
|
+
return await (await Create.fromJsonLd({
|
|
2349
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2350
|
+
type: "Create",
|
|
2351
|
+
id: `ap://${portableDid}/activities/1`,
|
|
2352
|
+
object
|
|
2353
|
+
}, { contextLoader: mockDocumentLoader })).getObject({
|
|
2354
|
+
documentLoader: async (requestUrl) => {
|
|
2355
|
+
if (requestUrl !== url) throw new Error(`Unexpected URL: ${requestUrl}`);
|
|
2356
|
+
return {
|
|
2357
|
+
contextUrl: null,
|
|
2358
|
+
documentUrl: requestUrl,
|
|
2359
|
+
document
|
|
2360
|
+
};
|
|
2361
|
+
},
|
|
2362
|
+
contextLoader: mockDocumentLoader,
|
|
2363
|
+
gateways: ["https://gateway.example"],
|
|
2364
|
+
verifyPortableObject: verifyPortableObjectProof
|
|
2365
|
+
});
|
|
2366
|
+
};
|
|
2367
|
+
await t.step("a properly signed object", async () => {
|
|
2368
|
+
const object = await getObject(signedNote);
|
|
2369
|
+
assertInstanceOf(object, Note);
|
|
2370
|
+
assertEquals(object.content, "Hello from a gateway");
|
|
2371
|
+
});
|
|
2372
|
+
await t.step("a tampered object", async () => {
|
|
2373
|
+
assertEquals(await getObject({
|
|
2374
|
+
...signedNote,
|
|
2375
|
+
content: "Tampered"
|
|
2376
|
+
}), null);
|
|
2377
|
+
});
|
|
2378
|
+
await t.step("an object without a proof", async () => {
|
|
2379
|
+
const { proof: _, ...unsigned } = signedNote;
|
|
2380
|
+
assertEquals(await getObject(unsigned), null);
|
|
2381
|
+
});
|
|
2382
|
+
await t.step("an object without an ID", async () => {
|
|
2383
|
+
const withoutId = await signPortableJsonLd({
|
|
2384
|
+
"@context": portableContext,
|
|
2385
|
+
type: "Note",
|
|
2386
|
+
content: "Hello from a gateway"
|
|
2387
|
+
});
|
|
2388
|
+
assertEquals(await getObject(withoutId), null);
|
|
2389
|
+
});
|
|
2390
|
+
await t.step("another object of the same DID", async () => {
|
|
2391
|
+
assertEquals(await getObject(signedNote, `ap://${portableDid}/objects/other`, `https://gateway.example/.well-known/apgateway/${portableDid}/objects/other`), null);
|
|
2392
|
+
});
|
|
2393
|
+
await t.step("an object signed by another DID", async () => {
|
|
2394
|
+
const otherDid = await exportDidKey(await crypto.subtle.importKey("jwk", {
|
|
2395
|
+
kty: "OKP",
|
|
2396
|
+
crv: "Ed25519",
|
|
2397
|
+
x: "sA2Nk45_dz1RVlqtNqYj9TRPf10ZYPnPPo4SYg6igQ8",
|
|
2398
|
+
key_ops: ["verify"],
|
|
2399
|
+
ext: true
|
|
2400
|
+
}, "Ed25519", true, ["verify"]));
|
|
2401
|
+
const otherObjectId = `ap://${otherDid}/objects/gateway`;
|
|
2402
|
+
const otherGatewayUrl = `https://gateway.example/.well-known/apgateway/${otherDid}/objects/gateway`;
|
|
2403
|
+
const spoofed = await signPortableJsonLd({
|
|
2404
|
+
"@context": portableContext,
|
|
2405
|
+
id: otherObjectId,
|
|
2406
|
+
type: "Note",
|
|
2407
|
+
content: "Spoofed"
|
|
2408
|
+
});
|
|
2409
|
+
assertEquals(await getObject(spoofed, otherObjectId, otherGatewayUrl), null);
|
|
2410
|
+
const forged = await signPortableJsonLd({
|
|
2411
|
+
"@context": portableContext,
|
|
2412
|
+
id: objectId,
|
|
2413
|
+
type: "Note",
|
|
2414
|
+
content: "Forged"
|
|
2415
|
+
}, { privateKey: fep8b32TestVectorPrivateKey });
|
|
2416
|
+
assertEquals(await getObject(forged), null);
|
|
2417
|
+
});
|
|
2418
|
+
});
|
|
2419
|
+
test("getAuthenticatedDocumentLoader() signs portable object requests to gateways", {
|
|
2420
|
+
sanitizeResources: false,
|
|
2421
|
+
sanitizeOps: false
|
|
2422
|
+
}, async () => {
|
|
2423
|
+
const objectId = `ap://${portableDid}/objects/private`;
|
|
2424
|
+
const signedNote = await signPortableJsonLd({
|
|
2425
|
+
"@context": portableContext,
|
|
2426
|
+
id: objectId,
|
|
2427
|
+
type: "Note",
|
|
2428
|
+
attributedTo: `ap://${portableDid}/actor`,
|
|
2429
|
+
content: "Followers only"
|
|
2430
|
+
});
|
|
2431
|
+
esm_default.spyGlobal();
|
|
2432
|
+
let signed = false;
|
|
2433
|
+
let accept = null;
|
|
2434
|
+
esm_default.get(`https://example.com/.well-known/apgateway/${portableDid}/objects/private`, async (cl) => {
|
|
2435
|
+
accept = cl.request.headers.get("Accept");
|
|
2436
|
+
signed = await verifyRequest(cl.request, {
|
|
2437
|
+
documentLoader: mockDocumentLoader,
|
|
2438
|
+
contextLoader: mockDocumentLoader,
|
|
2439
|
+
currentTime: Temporal.Now.instant()
|
|
2440
|
+
}) != null;
|
|
2441
|
+
if (!signed) return new Response(null, { status: 401 });
|
|
2442
|
+
return new Response(JSON.stringify(signedNote), { headers: { "Content-Type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" } });
|
|
2443
|
+
});
|
|
2444
|
+
try {
|
|
2445
|
+
const object = await (await Create.fromJsonLd({
|
|
2446
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
2447
|
+
type: "Create",
|
|
2448
|
+
id: `ap://${portableDid}/activities/1`,
|
|
2449
|
+
object: objectId
|
|
2450
|
+
}, { contextLoader: mockDocumentLoader })).getObject({
|
|
2451
|
+
documentLoader: getAuthenticatedDocumentLoader({
|
|
2452
|
+
keyId: new URL("https://example.com/key2"),
|
|
2453
|
+
privateKey: rsaPrivateKey2
|
|
2454
|
+
}),
|
|
2455
|
+
contextLoader: mockDocumentLoader,
|
|
2456
|
+
gateways: ["https://example.com"],
|
|
2457
|
+
verifyPortableObject: verifyPortableObjectProof
|
|
2458
|
+
});
|
|
2459
|
+
assertInstanceOf(object, Note);
|
|
2460
|
+
assertEquals(object.content, "Followers only");
|
|
2461
|
+
assert(signed);
|
|
2462
|
+
assertEquals(accept, "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"");
|
|
2463
|
+
} finally {
|
|
2464
|
+
esm_default.hardReset();
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
730
2467
|
//#endregion
|
|
731
2468
|
export {};
|