@fedify/fedify 2.4.0-dev.1668 → 2.4.0-dev.1740

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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/{assert-CJ1KXcvv.mjs → assert-CebeUf0C.mjs} +1 -1
  3. package/dist/assert_exists-DtzkXC47.mjs +28 -0
  4. package/dist/assert_false-3pLLLsO4.mjs +23 -0
  5. package/dist/assert_greater_or_equal-BApOTh1h.mjs +30 -0
  6. package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-g3WcyYNR.mjs} +1 -1
  7. package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-BN7yDMWe.mjs} +1 -1
  8. package/dist/assert_rejects-wFbSPn1O.mjs +39 -0
  9. package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-Bpjl4Psw.mjs} +1 -1
  10. package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BJ5wuvR2.mjs} +1 -1
  11. package/dist/{builder-iXZ5jNVg.mjs → builder-Bw6LVKGw.mjs} +5 -3
  12. package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-BT7CiPbz.mjs} +191 -9
  13. package/dist/circuit-breaker-test-utils-BYIHoliq.mjs +14 -0
  14. package/dist/{client-CjF5IYQS.mjs → client-CtxvmazV.mjs} +63 -5
  15. package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
  16. package/dist/compat/mod.cjs +1 -1
  17. package/dist/compat/mod.d.cts +1 -1
  18. package/dist/compat/mod.d.ts +1 -1
  19. package/dist/compat/mod.js +1 -1
  20. package/dist/compat/outgoing-jsonld.test.mjs +3 -3
  21. package/dist/compat/public-audience.test.mjs +3 -3
  22. package/dist/compat/transformers.test.mjs +5 -5
  23. package/dist/{context-C05Rgxmu.d.cts → context-B-dsBjZ9.d.cts} +281 -7
  24. package/dist/{context-Cjnr0ig1.d.ts → context-DrLXudbA.d.ts} +281 -7
  25. package/dist/{deno-DtHcfB0f.mjs → deno-tRdp8MCl.mjs} +1 -1
  26. package/dist/{docloader-0vAfF6XW.mjs → docloader-Dg_7gMC7.mjs} +2 -2
  27. package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
  28. package/dist/federation/builder.test.mjs +18 -5
  29. package/dist/federation/circuit-breaker.test.mjs +626 -7
  30. package/dist/federation/collection.test.mjs +3 -3
  31. package/dist/federation/handler.test.mjs +14 -13
  32. package/dist/federation/idempotency.test.mjs +6 -6
  33. package/dist/federation/inbox.test.mjs +3 -3
  34. package/dist/federation/keycache.test.mjs +5 -5
  35. package/dist/federation/kv.test.mjs +3 -3
  36. package/dist/federation/metrics.test.mjs +4 -4
  37. package/dist/federation/middleware.test.mjs +37 -28
  38. package/dist/federation/mod.cjs +18 -540
  39. package/dist/federation/mod.d.cts +3 -4
  40. package/dist/federation/mod.d.ts +3 -4
  41. package/dist/federation/mod.js +1 -1
  42. package/dist/federation/mq.test.mjs +32 -5
  43. package/dist/federation/negotiation.test.mjs +4 -4
  44. package/dist/federation/retry.test.mjs +3 -3
  45. package/dist/federation/router.test.mjs +7 -7
  46. package/dist/federation/send.test.mjs +28 -26
  47. package/dist/federation/tasks/codec.test.mjs +4 -4
  48. package/dist/federation/tasks/enqueue.test.mjs +4 -4
  49. package/dist/federation/tasks/tasks.test.mjs +6 -6
  50. package/dist/federation/temporal.test.mjs +4 -4
  51. package/dist/federation/webfinger.test.mjs +74 -12
  52. package/dist/{middleware-DzIUFRO2.cjs → federation-BJBpt_Am.cjs} +855 -114
  53. package/dist/{http-BbnOMQoy.js → http-BIxGa6AG.js} +3 -3
  54. package/dist/{http-DUmWq5Pw.mjs → http-Day1kcpT.mjs} +5 -5
  55. package/dist/{temporal-polyfill-BCdTHexd.cjs → http-DggG1scN.cjs} +2801 -104
  56. package/dist/{key-CM7wDnZG.mjs → key-igh0AMsL.mjs} +2 -2
  57. package/dist/{kv-cache-ZQ22hm44.js → kv-cache-BIAB0HKx.js} +1 -1
  58. package/dist/{kv-cache-DEXKemJ9.mjs → kv-cache-Bz6aQOOX.mjs} +1 -1
  59. package/dist/{kv-cache-zmgG1HUp.cjs → kv-cache-C4U_uBqD.cjs} +2 -4
  60. package/dist/{ld-BttH_-8K.mjs → ld-DnT9sCWa.mjs} +5 -5
  61. package/dist/metrics-DVKYY6K1.d.cts +36 -0
  62. package/dist/{metrics-DT-CnD1g.mjs → metrics-DbdNnWIt.mjs} +2 -2
  63. package/dist/metrics-DoRtI0qU.d.ts +36 -0
  64. package/dist/{middleware-ChlYS0Af.mjs → middleware-B6GweVcg.mjs} +1 -1
  65. package/dist/{middleware-D3IAVMcT.js → middleware-CnsPXG0O.js} +241 -40
  66. package/dist/{middleware-DrroX3aD.mjs → middleware-DPIS4fsB.mjs} +60 -43
  67. package/dist/{mod-BrpVsSfY.d.cts → mod-CLHkCwLm.d.cts} +3 -3
  68. package/dist/{mod-B0hW12_O.d.cts → mod-CnGLNoVw.d.cts} +86 -4
  69. package/dist/{mod-DdONo-ep.d.cts → mod-D-abvu2C.d.cts} +1 -1
  70. package/dist/{mod-BsCE5Ihq.d.ts → mod-D6O8C7eN.d.ts} +1 -1
  71. package/dist/{mod-CNef4in0.d.ts → mod-DypxPS9H.d.ts} +3 -3
  72. package/dist/{mod-COIAjwRS.d.ts → mod-DzRXACM5.d.ts} +86 -4
  73. package/dist/mod.cjs +25 -25
  74. package/dist/mod.d.cts +10 -11
  75. package/dist/mod.d.ts +10 -11
  76. package/dist/mod.js +8 -8
  77. package/dist/nodeinfo/client.test.mjs +64 -4
  78. package/dist/nodeinfo/handler.test.mjs +5 -5
  79. package/dist/nodeinfo/mod.cjs +1 -1
  80. package/dist/nodeinfo/mod.d.cts +1 -1
  81. package/dist/nodeinfo/mod.d.ts +1 -1
  82. package/dist/nodeinfo/mod.js +1 -1
  83. package/dist/nodeinfo/types.test.mjs +4 -4
  84. package/dist/otel/exporter.test.mjs +3 -3
  85. package/dist/otel/mod.cjs +0 -1
  86. package/dist/otel/mod.d.cts +1 -1
  87. package/dist/otel/mod.d.ts +1 -1
  88. package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-BQSHRLhg.mjs} +2 -2
  89. package/dist/{owner-DZALmhdl.mjs → owner-B3ef8GfJ.mjs} +2 -2
  90. package/dist/{proof-CqA-2nfW.js → proof-BBLQXRWw.js} +608 -36
  91. package/dist/{proof-a60oVJ_4.cjs → proof-Cqk2TcLX.cjs} +616 -39
  92. package/dist/proof-DVoDZ8d7.mjs +890 -0
  93. package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--9hx7IGx.mjs} +1 -1
  94. package/dist/runtime/mod.cjs +1 -1
  95. package/dist/runtime/mod.js +1 -1
  96. package/dist/{send-DcwZnQWG.mjs → send-PbBSZ7Qk.mjs} +3 -3
  97. package/dist/sig/accept.test.mjs +1 -1
  98. package/dist/sig/http.test.mjs +37 -11
  99. package/dist/sig/key.test.mjs +6 -6
  100. package/dist/sig/ld.test.mjs +9 -7
  101. package/dist/sig/mod.cjs +3 -2
  102. package/dist/sig/mod.d.cts +4 -4
  103. package/dist/sig/mod.d.ts +4 -4
  104. package/dist/sig/mod.js +3 -3
  105. package/dist/sig/owner.test.mjs +6 -6
  106. package/dist/sig/proof.test.mjs +965 -15
  107. package/dist/std__assert-CxFT0MNt.mjs +8 -0
  108. package/dist/{temporal-Bpxpsl2k.mjs → temporal-DLQIjuTn.mjs} +3 -3
  109. package/dist/testing/mod.d.mts +10 -0
  110. package/dist/testing/mod.mjs +2 -2
  111. package/dist/{testing-CO7ftO8A.mjs → testing-BX0Nicyq.mjs} +2 -2
  112. package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
  113. package/dist/{types-DC1pNgJA.js → types-Dtyf8lVj.js} +63 -5
  114. package/dist/{types-CPym83zc.cjs → types-RXm5YxpC.cjs} +62 -5
  115. package/dist/utils/docloader.test.mjs +7 -7
  116. package/dist/utils/kv-cache.test.mjs +1 -1
  117. package/dist/utils/mod.cjs +1 -1
  118. package/dist/utils/mod.d.cts +1 -1
  119. package/dist/utils/mod.d.ts +1 -1
  120. package/dist/utils/mod.js +1 -1
  121. package/package.json +7 -7
  122. package/skills/fedify/SKILL.md +462 -0
  123. package/dist/assert_rejects-EgstcQy5.mjs +0 -82
  124. package/dist/http-BpT5cE6r.cjs +0 -2694
  125. package/dist/metrics-DN13mCAw.d.cts +0 -294
  126. package/dist/metrics-v_e0n5GQ.d.ts +0 -294
  127. package/dist/proof-DiQ0uBal.mjs +0 -316
  128. package/dist/std__assert-B2NnY9bq.mjs +0 -79
  129. /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
  130. /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
  131. /package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-CBlRfA-_.mjs} +0 -0
  132. /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
  133. /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
  134. /package/dist/{codec-BBga-FWI.mjs → codec-peqOdBaE.mjs} +0 -0
  135. /package/dist/{context-BJXxPElm.mjs → context-Dt36KFEJ.mjs} +0 -0
  136. /package/dist/{http-VyDTd4G3.d.cts → http-6TI0tI03.d.cts} +0 -0
  137. /package/dist/{http-lf8Hsd91.d.ts → http-CdrSn2ip.d.ts} +0 -0
  138. /package/dist/{keycache-_DHqz5z_.mjs → keycache-0Jk78XmB.mjs} +0 -0
  139. /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
  140. /package/dist/{kv-D6hNiMTK.d.ts → kv-BhNbN8Ed.d.cts} +0 -0
  141. /package/dist/{kv-gJ8LYbxX.d.cts → kv-BhNbN8Ed.d.ts} +0 -0
  142. /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
  143. /package/dist/{mq-Bo7Etgh6.mjs → mq-Dm1Wmtm-.mjs} +0 -0
  144. /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
  145. /package/dist/{owner-CnngXDNJ.d.ts → owner-DGSfntI-.d.ts} +0 -0
  146. /package/dist/{owner-DEvZuyOE.d.cts → owner-r_waTiQj.d.cts} +0 -0
  147. /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
  148. /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
  149. /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
  150. /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
  151. /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
@@ -1,18 +1,21 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as assertEquals } from "../assert_equals-BV7oLMxm.mjs";
5
- import "../std__assert-B2NnY9bq.mjs";
6
- import { n as assertGreaterOrEqual, r as assertFalse, t as assertRejects } from "../assert_rejects-EgstcQy5.mjs";
7
- import { t as assertInstanceOf } from "../assert_instance_of-Q9KTBnrf.mjs";
8
- import { t as assert } from "../assert-CJ1KXcvv.mjs";
9
- import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-Dbyd7bhl.mjs";
10
- import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-URuqsICz.mjs";
11
- import { a as verifyProof, i as verifyObject, n as hasProofLike, r as signObject, t as createProof } from "../proof-DiQ0uBal.mjs";
4
+ import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
5
+ import { t as assertFalse } from "../assert_false-3pLLLsO4.mjs";
6
+ import { t as assertGreaterOrEqual } from "../assert_greater_or_equal-BApOTh1h.mjs";
7
+ import { t as assertInstanceOf } from "../assert_instance_of-g3WcyYNR.mjs";
8
+ import "../std__assert-CxFT0MNt.mjs";
9
+ import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
10
+ import { t as assert } from "../assert-CebeUf0C.mjs";
11
+ import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
12
+ import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-BQSHRLhg.mjs";
13
+ import { a as verifyPortableObjectProof, i as verifyObject, n as hasProofLike, o as verifyProof, r as signObject, t as createProof } from "../proof-DVoDZ8d7.mjs";
12
14
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab";
13
15
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
14
- import { decodeMultibase, exportDidKey, exportMultibaseKey, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
16
+ import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
15
17
  import { decodeHex } from "byte-encodings/hex";
18
+ import serialize from "json-canon";
16
19
  //#region src/sig/proof.test.ts
17
20
  const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
18
21
  "kty": "OKP",
@@ -23,6 +26,52 @@ const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
23
26
  ext: true
24
27
  }, "Ed25519", true, ["sign"]);
25
28
  const fep8b32TestVectorKeyId = new URL("https://server.example/users/alice#ed25519-key");
29
+ const portableDid = await exportDidKey(ed25519PublicKey.publicKey);
30
+ const portableDidMethod = portableDid.substring(8);
31
+ const portableKeyId = new URL(`${portableDid}#${portableDidMethod}`);
32
+ const portableContext = ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"];
33
+ const portableProofCreated = "2023-02-24T23:36:38Z";
34
+ async function signPortableJsonLd(document, { privateKey = ed25519PrivateKey, verificationMethod = portableKeyId, proofOptions = {} } = {}) {
35
+ const proofConfig = {
36
+ "@context": document["@context"],
37
+ type: "DataIntegrityProof",
38
+ cryptosuite: "eddsa-jcs-2022",
39
+ verificationMethod: verificationMethod.href,
40
+ proofPurpose: "assertionMethod",
41
+ created: portableProofCreated,
42
+ ...proofOptions
43
+ };
44
+ const encoder = new TextEncoder();
45
+ const proofDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)));
46
+ const messageDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(document)));
47
+ const digest = new Uint8Array(proofDigest.byteLength + messageDigest.byteLength);
48
+ digest.set(new Uint8Array(proofDigest), 0);
49
+ digest.set(new Uint8Array(messageDigest), proofDigest.byteLength);
50
+ const signature = await crypto.subtle.sign("Ed25519", privateKey, digest);
51
+ return {
52
+ ...document,
53
+ proof: {
54
+ ...proofConfig,
55
+ proofValue: new TextDecoder().decode(encodeMultibase("base58btc", new Uint8Array(signature)))
56
+ }
57
+ };
58
+ }
59
+ async function parsePortableProof(document) {
60
+ const proof = document.proof;
61
+ assert(typeof proof === "object" && proof != null && !Array.isArray(proof));
62
+ return await DataIntegrityProof.fromJsonLd({
63
+ "@context": document["@context"],
64
+ ...proof
65
+ }, {
66
+ documentLoader: mockDocumentLoader,
67
+ contextLoader: mockDocumentLoader
68
+ });
69
+ }
70
+ async function assertPortableProofVerified(document, options) {
71
+ const key = await verifyProof(document, await parsePortableProof(document), options);
72
+ assert(key != null);
73
+ assertEquals(key.id, portableKeyId);
74
+ }
26
75
  const fep8b32TestVectorActivity = new Create({
27
76
  id: new URL("https://server.example/activities/1"),
28
77
  actor: new URL("https://server.example/users/alice"),
@@ -326,6 +375,202 @@ test("verifyProof()", async () => {
326
375
  }), null);
327
376
  assertFalse(contextLoaderCalls.includes("https://attacker.example/ctx"));
328
377
  });
378
+ test("verifyProof() authenticates the complete proof configuration", async (t) => {
379
+ const jsonLd = {
380
+ "@context": portableContext,
381
+ id: "https://example.com/activities/complete-proof-options",
382
+ type: "Create",
383
+ actor: "https://example.com/users/alice",
384
+ object: {
385
+ type: "Note",
386
+ content: "Every proof option is authenticated."
387
+ }
388
+ };
389
+ const options = {
390
+ documentLoader: mockDocumentLoader,
391
+ contextLoader: mockDocumentLoader
392
+ };
393
+ await t.step("rejects an expires option added after signing", async () => {
394
+ const signed = await signPortableJsonLd(jsonLd);
395
+ const tampered = {
396
+ ...signed,
397
+ proof: {
398
+ ...signed.proof,
399
+ expires: "3000-01-01T00:00:00Z"
400
+ }
401
+ };
402
+ assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
403
+ });
404
+ await t.step("accepts an authenticated future expiration", async () => {
405
+ await assertPortableProofVerified(await signPortableJsonLd(jsonLd, { proofOptions: { expires: "3000-01-01T00:00:00Z" } }), options);
406
+ });
407
+ await t.step("rejects an authenticated expired proof", async () => {
408
+ const signed = await signPortableJsonLd(jsonLd, { proofOptions: { expires: "2000-01-01T00:00:00Z" } });
409
+ assertEquals(await verifyProof(signed, await parsePortableProof(signed), options), null);
410
+ });
411
+ await t.step("authenticates domain, challenge, and nonce", async () => {
412
+ const signed = await signPortableJsonLd(jsonLd, { proofOptions: {
413
+ domain: ["social.example", "https://social.example"],
414
+ challenge: "challenge-123",
415
+ nonce: "nonce-456"
416
+ } });
417
+ const proof = await parsePortableProof(signed);
418
+ const matchingKey = await verifyProof(signed, proof, {
419
+ ...options,
420
+ domain: ["https://social.example", "social.example"],
421
+ challenge: "challenge-123"
422
+ });
423
+ assert(matchingKey != null);
424
+ assertEquals(matchingKey.id, portableKeyId);
425
+ assertEquals(await verifyProof(signed, proof, {
426
+ ...options,
427
+ domain: ["social.example"],
428
+ challenge: "challenge-123"
429
+ }), null);
430
+ assertEquals(await verifyProof(signed, proof, {
431
+ ...options,
432
+ domain: ["https://social.example", "social.example"],
433
+ challenge: "wrong-challenge"
434
+ }), null);
435
+ for (const [property, value] of [
436
+ ["domain", ["other.example"]],
437
+ ["challenge", "tampered-challenge"],
438
+ ["nonce", "tampered-nonce"]
439
+ ]) {
440
+ const tampered = {
441
+ ...signed,
442
+ proof: {
443
+ ...signed.proof,
444
+ [property]: value
445
+ }
446
+ };
447
+ assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
448
+ }
449
+ });
450
+ await t.step("requires requested domain and challenge options", async () => {
451
+ const signed = await signPortableJsonLd(jsonLd);
452
+ const proof = await parsePortableProof(signed);
453
+ assertEquals(await verifyProof(signed, proof, {
454
+ ...options,
455
+ domain: "social.example"
456
+ }), null);
457
+ assertEquals(await verifyProof(signed, proof, {
458
+ ...options,
459
+ challenge: "challenge-123"
460
+ }), null);
461
+ });
462
+ await t.step("rejects malformed proof options", async () => {
463
+ for (const proofOptions of [
464
+ { expires: "not-a-date" },
465
+ { domain: ["social.example", 42] },
466
+ { challenge: 42 },
467
+ { nonce: { value: "not-a-string" } },
468
+ { nonce: [
469
+ "not",
470
+ "a",
471
+ "string"
472
+ ] },
473
+ { previousProof: ["urn:uuid:proof-1", 42] }
474
+ ]) {
475
+ const signed = await signPortableJsonLd(jsonLd, { proofOptions });
476
+ assertEquals(await verifyProof(signed, await parsePortableProof(signed), options), null);
477
+ }
478
+ });
479
+ await t.step("authenticates aliased and extension options", async () => {
480
+ const signed = await signPortableJsonLd({
481
+ ...jsonLd,
482
+ "@context": [...portableContext, {
483
+ validUntil: {
484
+ "@id": "https://w3id.org/security#expiration",
485
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
486
+ },
487
+ extensionOption: "https://example.com/security#extensionOption"
488
+ }]
489
+ }, { proofOptions: {
490
+ validUntil: "3000-01-01T00:00:00Z",
491
+ extensionOption: { nested: ["one", "two"] }
492
+ } });
493
+ await assertPortableProofVerified(signed, options);
494
+ for (const [property, value] of [["validUntil", "2999-01-01T00:00:00Z"], ["extensionOption", { nested: ["one", "changed"] }]]) {
495
+ const tampered = {
496
+ ...signed,
497
+ proof: {
498
+ ...signed.proof,
499
+ [property]: value
500
+ }
501
+ };
502
+ assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
503
+ }
504
+ });
505
+ await t.step("distinguishes equivalent and ambiguous raw proof configurations", async () => {
506
+ const signed = await signPortableJsonLd(jsonLd);
507
+ const proof = signed.proof;
508
+ const parsed = await parsePortableProof(signed);
509
+ assert(await verifyProof({
510
+ ...signed,
511
+ proof: [proof, structuredClone(proof)]
512
+ }, parsed, options) != null);
513
+ const inheritedContextProof = structuredClone(proof);
514
+ delete inheritedContextProof["@context"];
515
+ assert(await verifyProof({
516
+ ...signed,
517
+ proof: [proof, inheritedContextProof]
518
+ }, parsed, options) != null);
519
+ assertEquals(await verifyProof({
520
+ ...signed,
521
+ proof: [proof, {
522
+ ...structuredClone(proof),
523
+ expires: "3000-01-01T00:00:00Z"
524
+ }]
525
+ }, parsed, options), null);
526
+ });
527
+ await t.step("accepts literal proof fields with an unresolved extra context", async () => {
528
+ const signed = await signPortableJsonLd({
529
+ ...jsonLd,
530
+ "@context": [...portableContext, "https://context.example/unrelated"]
531
+ });
532
+ const rawProof = signed.proof;
533
+ const key = await verifyProof(signed, new DataIntegrityProof({
534
+ cryptosuite: "eddsa-jcs-2022",
535
+ verificationMethod: portableKeyId,
536
+ proofPurpose: "assertionMethod",
537
+ proofValue: decodeMultibase(rawProof.proofValue),
538
+ created: Temporal.Instant.from(portableProofCreated)
539
+ }), options);
540
+ assert(key != null);
541
+ assertEquals(key.id, portableKeyId);
542
+ });
543
+ await t.step("rejects aliases whose proof context cannot be resolved safely", async () => {
544
+ const contextUrl = "https://context.example/proof-options";
545
+ const signed = await signPortableJsonLd({
546
+ ...jsonLd,
547
+ "@context": [...portableContext, contextUrl]
548
+ }, { proofOptions: { validUntil: "2000-01-01T00:00:00Z" } });
549
+ const rawProof = signed.proof;
550
+ const proof = new DataIntegrityProof({
551
+ cryptosuite: "eddsa-jcs-2022",
552
+ verificationMethod: portableKeyId,
553
+ proofPurpose: "assertionMethod",
554
+ proofValue: decodeMultibase(rawProof.proofValue),
555
+ created: Temporal.Instant.from(portableProofCreated)
556
+ });
557
+ const contextLoader = async (url) => {
558
+ if (url !== contextUrl) return await mockDocumentLoader(url);
559
+ return {
560
+ contextUrl: null,
561
+ documentUrl: url,
562
+ document: { "@context": { validUntil: {
563
+ "@id": "https://w3id.org/security#expiration",
564
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
565
+ } } }
566
+ };
567
+ };
568
+ assertEquals(await verifyProof(signed, proof, {
569
+ ...options,
570
+ contextLoader
571
+ }), null);
572
+ });
573
+ });
329
574
  test("verifyProof() resolves did:key verification methods locally", async () => {
330
575
  const multibaseKey = (await exportDidKey(ed25519PublicKey.publicKey)).slice(8);
331
576
  const did = `did:key:${multibaseKey}`;
@@ -508,12 +753,13 @@ test("verifyProof() records verification duration metric", async (t) => {
508
753
  });
509
754
  await t.step("verifyObject() wrapper emits one measurement per inner verifyProof()", async () => {
510
755
  const [meterProvider, recorder] = createTestMeterProvider();
756
+ const { "@context": _proofContext, ...embeddedProof } = await proof.toJsonLd({
757
+ format: "compact",
758
+ contextLoader: mockDocumentLoader
759
+ });
511
760
  assert(await verifyObject(Create, {
512
761
  ...jsonLd,
513
- proof: await proof.toJsonLd({
514
- format: "compact",
515
- contextLoader: mockDocumentLoader
516
- })
762
+ proof: embeddedProof
517
763
  }, {
518
764
  documentLoader: mockDocumentLoader,
519
765
  contextLoader: mockDocumentLoader,
@@ -547,6 +793,632 @@ test("verifyProof() records verification duration metric", async (t) => {
547
793
  assertFalse("object_integrity_proofs.cryptosuite" in measurements[0].attributes);
548
794
  });
549
795
  });
796
+ test("verifyPortableObjectProof()", async (t) => {
797
+ const options = {
798
+ documentLoader() {
799
+ throw new TypeError("did:key must not use the document loader");
800
+ },
801
+ contextLoader: mockDocumentLoader
802
+ };
803
+ const objectId = `ap://did:key:${portableDidMethod}/objects/1`;
804
+ const unsignedObject = {
805
+ "@context": portableContext,
806
+ id: objectId,
807
+ type: "Note",
808
+ attributedTo: `ap://did:key:${portableDidMethod}/actor`,
809
+ content: "Portable note"
810
+ };
811
+ await t.step("verifies a portable object with a matching DID proof", async () => {
812
+ const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject), options);
813
+ assert(result.verified);
814
+ assertEquals(result.keys.length, 1);
815
+ assertEquals(result.keys[0].id, portableKeyId);
816
+ });
817
+ await t.step("rejects unauthenticated and expired proof options", async () => {
818
+ const signed = await signPortableJsonLd(unsignedObject);
819
+ const tamperedResult = await verifyPortableObjectProof({
820
+ ...signed,
821
+ proof: {
822
+ ...signed.proof,
823
+ expires: "3000-01-01T00:00:00Z"
824
+ }
825
+ }, options);
826
+ assertFalse(tamperedResult.verified);
827
+ assertEquals(tamperedResult.reason, {
828
+ type: "invalidProof",
829
+ proofIndex: 0
830
+ });
831
+ const expiredResult = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { proofOptions: { expires: "2000-01-01T00:00:00Z" } }), options);
832
+ assertFalse(expiredResult.verified);
833
+ assertEquals(expiredResult.reason, {
834
+ type: "invalidProof",
835
+ proofIndex: 0
836
+ });
837
+ });
838
+ await t.step("verifies aliased proof properties", async () => {
839
+ const proofAlias = "integrityProof";
840
+ const proofIri = "https://w3id.org/security#proof";
841
+ for (const { context, proofProperty, typeProperty, type } of [
842
+ {
843
+ context: { [proofAlias]: proofIri },
844
+ proofProperty: proofAlias,
845
+ typeProperty: "type",
846
+ type: "Note"
847
+ },
848
+ {
849
+ context: { PortableNote: {
850
+ "@id": "https://www.w3.org/ns/activitystreams#Note",
851
+ "@context": { [proofAlias]: proofIri }
852
+ } },
853
+ proofProperty: proofAlias,
854
+ typeProperty: "type",
855
+ type: "PortableNote"
856
+ },
857
+ {
858
+ context: {
859
+ kind: "@type",
860
+ PortableNote: {
861
+ "@id": "https://www.w3.org/ns/activitystreams#Note",
862
+ "@context": { [proofAlias]: proofIri }
863
+ }
864
+ },
865
+ proofProperty: proofAlias,
866
+ typeProperty: "kind",
867
+ type: "PortableNote"
868
+ },
869
+ {
870
+ context: { sec: "https://w3id.org/security#" },
871
+ proofProperty: "sec:proof",
872
+ typeProperty: "type",
873
+ type: "Note"
874
+ },
875
+ {
876
+ context: { sec: {
877
+ "@id": "https://w3id.org/security#",
878
+ "@prefix": true
879
+ } },
880
+ proofProperty: "sec:proof",
881
+ typeProperty: "type",
882
+ type: "Note"
883
+ }
884
+ ]) {
885
+ const document = {
886
+ ...unsignedObject,
887
+ "@context": [...portableContext, context]
888
+ };
889
+ delete document.type;
890
+ document[typeProperty] = type;
891
+ const { proof, ...signed } = await signPortableJsonLd(document);
892
+ const result = await verifyPortableObjectProof({
893
+ ...signed,
894
+ [proofProperty]: proof
895
+ }, options);
896
+ assert(result.verified);
897
+ assertEquals(result.keys.length, 1);
898
+ assertEquals(result.keys[0].id, portableKeyId);
899
+ }
900
+ });
901
+ await t.step("verifies proof aliases from caller-loaded contexts", async () => {
902
+ const contextUrl = "https://context.example/security";
903
+ const proofAlias = "integrityProof";
904
+ const proofIri = "https://w3id.org/security#proof";
905
+ let contextLoads = 0;
906
+ const contextLoader = async (url) => {
907
+ if (url !== contextUrl) return await mockDocumentLoader(url);
908
+ contextLoads++;
909
+ return {
910
+ contextUrl: null,
911
+ documentUrl: url,
912
+ document: { "@context": { [proofAlias]: proofIri } }
913
+ };
914
+ };
915
+ const { proof, ...signed } = await signPortableJsonLd({
916
+ ...unsignedObject,
917
+ "@context": [...portableContext, contextUrl]
918
+ });
919
+ const result = await verifyPortableObjectProof({
920
+ ...signed,
921
+ [proofAlias]: proof
922
+ }, {
923
+ ...options,
924
+ contextLoader
925
+ });
926
+ assert(result.verified);
927
+ assertEquals(result.keys.length, 1);
928
+ assertEquals(result.keys[0].id, portableKeyId);
929
+ assertEquals(contextLoads, 1);
930
+ });
931
+ await t.step("verifies portable actors and activities by shape", async () => {
932
+ for (const document of [{
933
+ "@context": portableContext,
934
+ id: `ap+ef61://did:key:${portableDidMethod}/actor`,
935
+ type: "UnknownActorType",
936
+ inbox: "https://gateway.example/users/alice/inbox",
937
+ outbox: "https://gateway.example/users/alice/outbox"
938
+ }, {
939
+ "@context": portableContext,
940
+ id: `ap://did:key:${portableDidMethod}/activities/1`,
941
+ type: "UnknownActivityType",
942
+ actor: `ap://did:key:${portableDidMethod}/actor`,
943
+ object: objectId
944
+ }]) {
945
+ const result = await verifyPortableObjectProof(await signPortableJsonLd(document), options);
946
+ assert(result.verified);
947
+ assertEquals(result.keys.length, 1);
948
+ }
949
+ });
950
+ await t.step("uses the normative FEP-2277 precedence order", async () => {
951
+ for (const document of [{
952
+ "@context": portableContext,
953
+ id: `ap://did:key:${portableDidMethod}/actor`,
954
+ type: "Collection",
955
+ inbox: "https://gateway.example/users/alice/inbox",
956
+ outbox: "https://gateway.example/users/alice/outbox",
957
+ totalItems: 0
958
+ }, {
959
+ "@context": portableContext,
960
+ id: `ap://did:key:${portableDidMethod}/activities/1`,
961
+ type: "Collection",
962
+ actor: `ap://did:key:${portableDidMethod}/actor`,
963
+ totalItems: 0
964
+ }]) assertEquals(await verifyPortableObjectProof(document, options), {
965
+ verified: false,
966
+ reason: { type: "missingProof" }
967
+ });
968
+ });
969
+ await t.step("accepts portable URI spelling and location-hint variants", async () => {
970
+ const variants = [
971
+ `ap+ef61://did%3Akey%3A${portableDidMethod}/objects/1`,
972
+ `AP://did:key:${portableDidMethod}/objects/1`,
973
+ `ap://did:key:${portableDidMethod}/objects/1?%40gateway=${encodeURIComponent("https://gateway.example")}`,
974
+ `ap://did:key:${portableDidMethod}/objects/1#content`
975
+ ];
976
+ for (const id of variants) assert((await verifyPortableObjectProof(await signPortableJsonLd({
977
+ ...unsignedObject,
978
+ id
979
+ }), options)).verified, id);
980
+ });
981
+ await t.step("supports matching DID methods resolved by a caller", async () => {
982
+ const verificationMethod = new URL("did:web:example.com#key");
983
+ const document = {
984
+ ...unsignedObject,
985
+ id: "ap://did:web:example.com/objects/1"
986
+ };
987
+ let fetches = 0;
988
+ assert((await verifyPortableObjectProof(await signPortableJsonLd(document, { verificationMethod }), {
989
+ contextLoader: mockDocumentLoader,
990
+ documentLoader: async (url) => {
991
+ fetches++;
992
+ assertEquals(url, verificationMethod.href);
993
+ return {
994
+ contextUrl: null,
995
+ documentUrl: url,
996
+ document: {
997
+ "@context": "https://w3id.org/security/multikey/v1",
998
+ id: verificationMethod.href,
999
+ type: "Multikey",
1000
+ controller: "did:web:example.com",
1001
+ publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
1002
+ }
1003
+ };
1004
+ }
1005
+ })).verified);
1006
+ assertEquals(fetches, 1);
1007
+ });
1008
+ await t.step("reports a missing proof on portable objects", async () => {
1009
+ assertEquals(await verifyPortableObjectProof(unsignedObject, options), {
1010
+ verified: false,
1011
+ reason: { type: "missingProof" }
1012
+ });
1013
+ });
1014
+ await t.step("keeps non-portable documents outside the policy", async () => {
1015
+ for (const document of [
1016
+ {
1017
+ ...unsignedObject,
1018
+ id: "https://social.example/objects/1"
1019
+ },
1020
+ {
1021
+ ...unsignedObject,
1022
+ id: "https://gateway.example/.well-known/apgateway/did:key:z6MkAlice/objects/1"
1023
+ },
1024
+ {
1025
+ "@context": portableContext,
1026
+ type: "Note",
1027
+ content: "Object without an ID"
1028
+ }
1029
+ ]) assertEquals(await verifyPortableObjectProof(document, options), {
1030
+ verified: false,
1031
+ reason: { type: "notPortableObject" }
1032
+ });
1033
+ });
1034
+ await t.step("short-circuits clearly non-portable raw IDs", async () => {
1035
+ let contextLoads = 0;
1036
+ assertEquals(await verifyPortableObjectProof({
1037
+ "@context": "https://attacker.example/context",
1038
+ "@id": "https://social.example/objects/1"
1039
+ }, {
1040
+ ...options,
1041
+ contextLoader() {
1042
+ contextLoads++;
1043
+ throw new TypeError("unexpected context load");
1044
+ }
1045
+ }), {
1046
+ verified: false,
1047
+ reason: { type: "notPortableObject" }
1048
+ });
1049
+ assertEquals(contextLoads, 0);
1050
+ });
1051
+ await t.step("distinguishes unsecured and signed collections", async () => {
1052
+ const collection = {
1053
+ "@context": portableContext,
1054
+ id: `ap://did:key:${portableDidMethod}/collections/1`,
1055
+ type: "Note",
1056
+ totalItems: 0
1057
+ };
1058
+ assertEquals(await verifyPortableObjectProof(collection, options), {
1059
+ verified: false,
1060
+ reason: { type: "unsecuredCollection" }
1061
+ });
1062
+ assert((await verifyPortableObjectProof(await signPortableJsonLd(collection), options)).verified);
1063
+ });
1064
+ await t.step("classifies unsupported core types without using type", async () => {
1065
+ const cases = [
1066
+ {
1067
+ document: {
1068
+ ...unsignedObject,
1069
+ href: "https://example.com/"
1070
+ },
1071
+ objectType: "link"
1072
+ },
1073
+ {
1074
+ document: {
1075
+ ...unsignedObject,
1076
+ "https://w3id.org/security#publicKeyMultibase": "z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2"
1077
+ },
1078
+ objectType: "verificationMethod"
1079
+ },
1080
+ {
1081
+ document: {
1082
+ ...unsignedObject,
1083
+ "https://w3id.org/security#publicKeyPem": "not-a-real-key"
1084
+ },
1085
+ objectType: "publicKey"
1086
+ }
1087
+ ];
1088
+ for (const { document, objectType } of cases) assertEquals(await verifyPortableObjectProof(document, options), {
1089
+ verified: false,
1090
+ reason: {
1091
+ type: "unsupportedObjectType",
1092
+ objectType
1093
+ }
1094
+ });
1095
+ });
1096
+ await t.step("rejects non-DID verification methods before fetching", async () => {
1097
+ let fetched = false;
1098
+ const verificationMethod = new URL("https://attacker.example/keys/ed25519");
1099
+ const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), {
1100
+ contextLoader: mockDocumentLoader,
1101
+ documentLoader() {
1102
+ fetched = true;
1103
+ throw new TypeError("unexpected fetch");
1104
+ }
1105
+ });
1106
+ assertFalse(result.verified);
1107
+ assertEquals(result.reason, {
1108
+ type: "unsupportedVerificationMethod",
1109
+ proofIndex: 0,
1110
+ verificationMethod
1111
+ });
1112
+ assertFalse(fetched);
1113
+ });
1114
+ await t.step("rejects malformed DID verification methods", async () => {
1115
+ const verificationMethod = new URL("did:invalid");
1116
+ const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), options);
1117
+ assertFalse(result.verified);
1118
+ assertEquals(result.reason, {
1119
+ type: "unsupportedVerificationMethod",
1120
+ proofIndex: 0,
1121
+ verificationMethod
1122
+ });
1123
+ });
1124
+ await t.step("rejects a proof from another DID before fetching", async () => {
1125
+ let fetched = false;
1126
+ const verificationMethod = new URL("did:key:z6MkMallory#z6MkMallory");
1127
+ const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), {
1128
+ contextLoader: mockDocumentLoader,
1129
+ documentLoader() {
1130
+ fetched = true;
1131
+ throw new TypeError("unexpected fetch");
1132
+ }
1133
+ });
1134
+ assertFalse(result.verified);
1135
+ assertEquals(result.reason, {
1136
+ type: "verificationMethodMismatch",
1137
+ proofIndex: 0,
1138
+ objectId: parseIri(objectId),
1139
+ verificationMethod
1140
+ });
1141
+ assertFalse(fetched);
1142
+ });
1143
+ await t.step("rejects proofs with multiple verification methods", async () => {
1144
+ const signed = await signPortableJsonLd(unsignedObject);
1145
+ const proof = signed.proof;
1146
+ for (const additionalVerificationMethod of ["did:key:z6MkMallory#z6MkMallory", "https://attacker.example/key"]) assertEquals(await verifyPortableObjectProof({
1147
+ ...signed,
1148
+ proof: {
1149
+ ...proof,
1150
+ verificationMethod: [portableKeyId.href, additionalVerificationMethod]
1151
+ }
1152
+ }, options), {
1153
+ verified: false,
1154
+ reason: {
1155
+ type: "invalidProof",
1156
+ proofIndex: 0
1157
+ }
1158
+ });
1159
+ });
1160
+ await t.step("rejects multiple values in functional proof fields", async () => {
1161
+ const signed = await signPortableJsonLd(unsignedObject);
1162
+ const proof = signed.proof;
1163
+ const cases = [
1164
+ ["cryptosuite", "eddsa-jcs-2022"],
1165
+ ["proofPurpose", "authentication"],
1166
+ ["proofValue", proof.proofValue],
1167
+ ["created", "2024-01-01T00:00:00Z"]
1168
+ ];
1169
+ for (const [field, additionalValue] of cases) {
1170
+ const originalValue = proof[field];
1171
+ assert(originalValue != null);
1172
+ assertEquals(await verifyPortableObjectProof({
1173
+ ...signed,
1174
+ proof: {
1175
+ ...proof,
1176
+ [field]: [originalValue, additionalValue]
1177
+ }
1178
+ }, options), {
1179
+ verified: false,
1180
+ reason: {
1181
+ type: "invalidProof",
1182
+ proofIndex: 0
1183
+ }
1184
+ });
1185
+ }
1186
+ });
1187
+ await t.step("requires exactly the DataIntegrityProof type", async () => {
1188
+ const signed = await signPortableJsonLd(unsignedObject);
1189
+ const proof = signed.proof;
1190
+ const missingType = { ...proof };
1191
+ delete missingType.type;
1192
+ for (const invalidProof of [
1193
+ missingType,
1194
+ {
1195
+ ...proof,
1196
+ type: "Object"
1197
+ },
1198
+ {
1199
+ ...proof,
1200
+ type: ["DataIntegrityProof", "Object"]
1201
+ }
1202
+ ]) assertEquals(await verifyPortableObjectProof({
1203
+ ...signed,
1204
+ proof: invalidProof
1205
+ }, options), {
1206
+ verified: false,
1207
+ reason: {
1208
+ type: "invalidProof",
1209
+ proofIndex: 0
1210
+ }
1211
+ });
1212
+ });
1213
+ await t.step("reports cryptographic failures separately", async () => {
1214
+ assertEquals(await verifyPortableObjectProof({
1215
+ ...await signPortableJsonLd(unsignedObject),
1216
+ content: "Tampered"
1217
+ }, options), {
1218
+ verified: false,
1219
+ reason: {
1220
+ type: "invalidProof",
1221
+ proofIndex: 0
1222
+ }
1223
+ });
1224
+ });
1225
+ await t.step("reports malformed proof structures as invalid", async () => {
1226
+ for (const proof of [{ type: "DataIntegrityProof" }, {
1227
+ type: "DataIntegrityProof",
1228
+ cryptosuite: "eddsa-jcs-2022",
1229
+ verificationMethod: portableKeyId.href,
1230
+ proofPurpose: "assertionMethod",
1231
+ proofValue: "zInvalid",
1232
+ created: "not-an-instant"
1233
+ }]) assertEquals(await verifyPortableObjectProof({
1234
+ ...unsignedObject,
1235
+ proof
1236
+ }, options), {
1237
+ verified: false,
1238
+ reason: {
1239
+ type: "invalidProof",
1240
+ proofIndex: 0
1241
+ }
1242
+ });
1243
+ });
1244
+ await t.step("accepts multiple valid proofs in input order", async () => {
1245
+ const signed = await signPortableJsonLd(unsignedObject);
1246
+ const proof = signed.proof;
1247
+ const result = await verifyPortableObjectProof({
1248
+ ...signed,
1249
+ proof: [proof, structuredClone(proof)]
1250
+ }, options);
1251
+ assert(result.verified);
1252
+ assertEquals(result.keys.length, 2);
1253
+ assertEquals(result.keys.map((key) => key.id), [portableKeyId, portableKeyId]);
1254
+ });
1255
+ await t.step("rejects a tampered duplicate proof", async () => {
1256
+ const signed = await signPortableJsonLd(unsignedObject);
1257
+ const proof = signed.proof;
1258
+ assertFalse((await verifyPortableObjectProof({
1259
+ ...signed,
1260
+ proof: [proof, {
1261
+ ...structuredClone(proof),
1262
+ expires: "3000-01-01T00:00:00Z"
1263
+ }]
1264
+ }, options)).verified);
1265
+ });
1266
+ await t.step("matches proofs across aliased properties", async () => {
1267
+ const document = {
1268
+ ...unsignedObject,
1269
+ "@context": [...portableContext, {
1270
+ firstProof: "https://w3id.org/security#proof",
1271
+ secondProof: "https://w3id.org/security#proof"
1272
+ }]
1273
+ };
1274
+ const firstSigned = await signPortableJsonLd(document, { proofOptions: { domain: "first.example" } });
1275
+ const secondSigned = await signPortableJsonLd(document, { proofOptions: { domain: "second.example" } });
1276
+ const result = await verifyPortableObjectProof({
1277
+ ...document,
1278
+ secondProof: secondSigned.proof,
1279
+ firstProof: firstSigned.proof
1280
+ }, options);
1281
+ assert(result.verified);
1282
+ assertEquals(result.keys.length, 2);
1283
+ });
1284
+ await t.step("canonicalizes the document only once for multiple proofs", async () => {
1285
+ async function countContentReads(proofCount) {
1286
+ const signed = await signPortableJsonLd(unsignedObject);
1287
+ const proof = signed.proof;
1288
+ const document = {
1289
+ ...signed,
1290
+ proof: Array.from({ length: proofCount }, () => structuredClone(proof))
1291
+ };
1292
+ let contentReads = 0;
1293
+ Object.defineProperty(document, "content", {
1294
+ configurable: true,
1295
+ enumerable: true,
1296
+ get() {
1297
+ contentReads++;
1298
+ return unsignedObject.content;
1299
+ }
1300
+ });
1301
+ const result = await verifyPortableObjectProof(document, options);
1302
+ assert(result.verified);
1303
+ assertEquals(result.keys.length, proofCount);
1304
+ return contentReads;
1305
+ }
1306
+ assertEquals(await countContentReads(3), await countContentReads(1));
1307
+ });
1308
+ await t.step("requires every proof to verify", async () => {
1309
+ const signed = await signPortableJsonLd(unsignedObject);
1310
+ const proof = signed.proof;
1311
+ assertEquals(await verifyPortableObjectProof({
1312
+ ...signed,
1313
+ proof: [proof, {
1314
+ ...proof,
1315
+ proofValue: "zInvalid"
1316
+ }]
1317
+ }, options), {
1318
+ verified: false,
1319
+ reason: {
1320
+ type: "invalidProof",
1321
+ proofIndex: 1
1322
+ }
1323
+ });
1324
+ });
1325
+ await t.step("checks every proof policy before resolving keys", async () => {
1326
+ const didWebId = "ap://did:web:example.com/objects/1";
1327
+ const didWebMethod = new URL("did:web:example.com#key");
1328
+ const didWebSigned = await signPortableJsonLd({
1329
+ ...unsignedObject,
1330
+ id: didWebId
1331
+ }, { verificationMethod: didWebMethod });
1332
+ const httpMethod = new URL("https://attacker.example/key");
1333
+ const httpSigned = await signPortableJsonLd({
1334
+ ...unsignedObject,
1335
+ id: didWebId
1336
+ }, { verificationMethod: httpMethod });
1337
+ let fetched = false;
1338
+ const result = await verifyPortableObjectProof({
1339
+ ...didWebSigned,
1340
+ proof: [didWebSigned.proof, httpSigned.proof]
1341
+ }, {
1342
+ contextLoader: mockDocumentLoader,
1343
+ documentLoader() {
1344
+ fetched = true;
1345
+ throw new TypeError("unexpected fetch");
1346
+ }
1347
+ });
1348
+ assertFalse(result.verified);
1349
+ assertEquals(result.reason, {
1350
+ type: "unsupportedVerificationMethod",
1351
+ proofIndex: 1,
1352
+ verificationMethod: httpMethod
1353
+ });
1354
+ assertFalse(fetched);
1355
+ });
1356
+ await t.step("accepts the expanded proof property", async () => {
1357
+ const { proof, ...document } = await signPortableJsonLd(unsignedObject);
1358
+ const expandedProof = await (await DataIntegrityProof.fromJsonLd(proof, {
1359
+ contextLoader: mockDocumentLoader,
1360
+ documentLoader: mockDocumentLoader
1361
+ })).toJsonLd({
1362
+ format: "expand",
1363
+ contextLoader: mockDocumentLoader
1364
+ });
1365
+ assert((await verifyPortableObjectProof({
1366
+ ...document,
1367
+ "https://w3id.org/security#proof": expandedProof
1368
+ }, options)).verified);
1369
+ });
1370
+ await t.step("classifies JSON-LD aliases by their expanded properties", async () => {
1371
+ assert((await verifyPortableObjectProof(await signPortableJsonLd({
1372
+ "@context": [...portableContext, { performedBy: "https://www.w3.org/ns/activitystreams#actor" }],
1373
+ id: `ap://did:key:${portableDidMethod}/activities/aliased`,
1374
+ type: "UnknownActivity",
1375
+ performedBy: `ap://did:key:${portableDidMethod}/actor`
1376
+ }), options)).verified);
1377
+ });
1378
+ await t.step("does not claim to verify embedded portable objects", async () => {
1379
+ assert((await verifyPortableObjectProof(await signPortableJsonLd({
1380
+ "@context": portableContext,
1381
+ id: `ap://did:key:${portableDidMethod}/activities/outer`,
1382
+ type: "Create",
1383
+ actor: `ap://did:key:${portableDidMethod}/actor`,
1384
+ object: {
1385
+ id: `ap://did:key:${portableDidMethod}/objects/embedded`,
1386
+ type: "Note",
1387
+ content: "Unsigned embedded object"
1388
+ }
1389
+ }), options)).verified);
1390
+ });
1391
+ await t.step("rejects malformed roots and portable IDs", async () => {
1392
+ await assertRejects(() => verifyPortableObjectProof([], options), TypeError, "single JSON-LD object");
1393
+ await assertRejects(() => verifyPortableObjectProof({
1394
+ ...unsignedObject,
1395
+ id: `ap://did:key:${portableDidMethod}`
1396
+ }, options), TypeError);
1397
+ await assertRejects(() => verifyPortableObjectProof({
1398
+ ...unsignedObject,
1399
+ id: "ap://did%ZZkey/objects/1"
1400
+ }, options), TypeError);
1401
+ });
1402
+ await t.step("emits metrics only for attempted proof verification", async () => {
1403
+ const [meterProvider, recorder] = createTestMeterProvider();
1404
+ const signed = await signPortableJsonLd(unsignedObject);
1405
+ const proof = signed.proof;
1406
+ assert((await verifyPortableObjectProof({
1407
+ ...signed,
1408
+ proof: [proof, structuredClone(proof)]
1409
+ }, {
1410
+ ...options,
1411
+ meterProvider
1412
+ })).verified);
1413
+ assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 2);
1414
+ const [rejectedMeterProvider, rejectedRecorder] = createTestMeterProvider();
1415
+ await verifyPortableObjectProof(unsignedObject, {
1416
+ ...options,
1417
+ meterProvider: rejectedMeterProvider
1418
+ });
1419
+ assertEquals(rejectedRecorder.getMeasurements("activitypub.signature.verification.duration").length, 0);
1420
+ });
1421
+ });
550
1422
  test("verifyObject()", async () => {
551
1423
  const options = {
552
1424
  documentLoader: mockDocumentLoader,
@@ -593,7 +1465,7 @@ test("verifyObject()", async () => {
593
1465
  test("verifyObject() accepts did:key proofs for matching portable attribution origin", async () => {
594
1466
  const did = await exportDidKey(ed25519PublicKey.publicKey);
595
1467
  const keyId = new URL(`${did}#${did.substring(8)}`);
596
- const verified = await verifyObject(Note, await (await signObject(new Note({
1468
+ const jsonLd = await (await signObject(new Note({
597
1469
  id: parseIri(`ap://did:key:${did.substring(8)}/objects/1`),
598
1470
  attribution: parseIri(`ap://did:key:${did.substring(8)}/actor`),
599
1471
  content: "Portable note"
@@ -604,7 +1476,8 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
604
1476
  format: "compact",
605
1477
  contextLoader: mockDocumentLoader,
606
1478
  context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
607
- }), {
1479
+ });
1480
+ const verified = await verifyObject(Note, jsonLd, {
608
1481
  documentLoader() {
609
1482
  throw new TypeError("did:key must not use the document loader");
610
1483
  },
@@ -612,6 +1485,83 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
612
1485
  });
613
1486
  assertInstanceOf(verified, Note);
614
1487
  assertEquals(verified.content, "Portable note");
1488
+ assert(typeof jsonLd === "object" && jsonLd != null && !Array.isArray(jsonLd));
1489
+ const rawProof = jsonLd.proof;
1490
+ assert(typeof rawProof === "object" && rawProof != null && !Array.isArray(rawProof));
1491
+ assertEquals(await verifyObject(Note, {
1492
+ ...jsonLd,
1493
+ proof: [rawProof, {
1494
+ ...structuredClone(rawProof),
1495
+ expires: "3000-01-01T00:00:00Z"
1496
+ }]
1497
+ }, {
1498
+ documentLoader() {
1499
+ throw new TypeError("did:key must not use the document loader");
1500
+ },
1501
+ contextLoader: mockDocumentLoader
1502
+ }), null);
1503
+ async function verifyRemotelyReferencedProof(proofOptions = {}, proofUrl = "https://proof.example/proofs/1") {
1504
+ const remotelyReferencedJsonLd = { ...jsonLd };
1505
+ delete remotelyReferencedJsonLd.proof;
1506
+ remotelyReferencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
1507
+ let proofFetches = 0;
1508
+ const result = await verifyObject(Note, remotelyReferencedJsonLd, {
1509
+ documentLoader(url) {
1510
+ if (url !== formatIri(parseIri(proofUrl))) throw new TypeError(`unexpected document URL: ${url}`);
1511
+ proofFetches++;
1512
+ return Promise.resolve({
1513
+ contextUrl: null,
1514
+ document: {
1515
+ "@context": jsonLd["@context"],
1516
+ ...structuredClone(rawProof),
1517
+ ...proofOptions
1518
+ },
1519
+ documentUrl: url
1520
+ });
1521
+ },
1522
+ contextLoader: mockDocumentLoader
1523
+ });
1524
+ assertEquals(proofFetches, 1);
1525
+ return result;
1526
+ }
1527
+ assertInstanceOf(await verifyRemotelyReferencedProof(), Note);
1528
+ assertInstanceOf(await verifyRemotelyReferencedProof({}, `ap://did:key:${did.substring(8)}/proofs/1`), Note);
1529
+ assertEquals(await verifyRemotelyReferencedProof({ expires: "3000-01-01T00:00:00Z" }), null);
1530
+ });
1531
+ test("verifyObject() hydrates pending proof references", async () => {
1532
+ const did = await exportDidKey(ed25519PublicKey.publicKey);
1533
+ const method = did.substring(8);
1534
+ const keyId = new URL(`${did}#${method}`);
1535
+ const proofUrl = `ap://did:key:${method}/proofs/1`;
1536
+ const signed = await signPortableJsonLd({
1537
+ "@context": portableContext,
1538
+ id: `ap://did:key:${method}/objects/1`,
1539
+ type: "Note",
1540
+ attributedTo: `ap://did:key:${method}/actor`,
1541
+ content: "Portable note with a referenced proof"
1542
+ }, {
1543
+ verificationMethod: keyId,
1544
+ proofOptions: { id: proofUrl }
1545
+ });
1546
+ const rawProof = signed.proof;
1547
+ const remotelyReferencedJsonLd = { ...signed };
1548
+ delete remotelyReferencedJsonLd.proof;
1549
+ remotelyReferencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
1550
+ let proofFetches = 0;
1551
+ const verified = await verifyObject(Note, remotelyReferencedJsonLd, {
1552
+ documentLoader(url) {
1553
+ assertEquals(url, formatIri(parseIri(proofUrl)));
1554
+ proofFetches++;
1555
+ return Promise.resolve({
1556
+ contextUrl: null,
1557
+ document: structuredClone(rawProof),
1558
+ documentUrl: url
1559
+ });
1560
+ },
1561
+ contextLoader: mockDocumentLoader
1562
+ });
1563
+ assertEquals(proofFetches, 1);
1564
+ assertInstanceOf(verified, Note);
615
1565
  });
616
1566
  test("verifyObject() accepts multiple portable attributions from the same did:key origin", async () => {
617
1567
  const did = await exportDidKey(ed25519PublicKey.publicKey);