@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.
Files changed (244) hide show
  1. package/README.md +50 -39
  2. package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
  3. package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
  4. package/dist/assert_exists-cffSpJ7X.mjs +28 -0
  5. package/dist/assert_false-CPdRnc3E.mjs +23 -0
  6. package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
  7. package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
  8. package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
  9. package/dist/assert_rejects-CTptdYck.mjs +39 -0
  10. package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
  11. package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
  12. package/dist/assertion_error-B-cx0fFA.mjs +31 -0
  13. package/dist/audience-CHQ-FlLD.mjs +95 -0
  14. package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
  15. package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
  16. package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
  17. package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
  18. package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
  19. package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
  20. package/dist/compat/mod.cjs +1 -1
  21. package/dist/compat/mod.d.cts +1 -1
  22. package/dist/compat/mod.d.ts +1 -1
  23. package/dist/compat/mod.js +1 -1
  24. package/dist/compat/outgoing-jsonld.test.mjs +71 -3
  25. package/dist/compat/public-audience.test.mjs +14 -5
  26. package/dist/compat/secured-document.test.d.mts +2 -0
  27. package/dist/compat/secured-document.test.mjs +58 -0
  28. package/dist/compat/transformers.test.mjs +5 -5
  29. package/dist/compound-proof-B-9mXxFW.mjs +989 -0
  30. package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
  31. package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
  32. package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
  33. package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
  34. package/dist/dist-DJQM4u7i.mjs +189 -0
  35. package/dist/docloader-DBoYbvYW.mjs +87 -0
  36. package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
  37. package/dist/federation/audience.test.d.mts +2 -0
  38. package/dist/federation/audience.test.mjs +130 -0
  39. package/dist/federation/builder.test.mjs +54 -5
  40. package/dist/federation/circuit-breaker.test.mjs +838 -7
  41. package/dist/federation/collection.test.mjs +3 -3
  42. package/dist/federation/compound-fanout.test.d.mts +2 -0
  43. package/dist/federation/compound-fanout.test.mjs +228 -0
  44. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  45. package/dist/federation/handler-compound-proof.test.mjs +384 -0
  46. package/dist/federation/handler.test.mjs +385 -15
  47. package/dist/federation/hashlink-media.test.d.mts +2 -0
  48. package/dist/federation/hashlink-media.test.mjs +422 -0
  49. package/dist/federation/idempotency.test.mjs +6 -6
  50. package/dist/federation/inbox-report.test.d.mts +2 -0
  51. package/dist/federation/inbox-report.test.mjs +475 -0
  52. package/dist/federation/inbox.test.mjs +3 -3
  53. package/dist/federation/keycache.test.mjs +215 -14
  54. package/dist/federation/kv.test.mjs +3 -3
  55. package/dist/federation/metrics.test.mjs +5 -5
  56. package/dist/federation/middleware.test.mjs +967 -35
  57. package/dist/federation/mod.cjs +18 -261
  58. package/dist/federation/mod.d.cts +5 -5
  59. package/dist/federation/mod.d.ts +5 -5
  60. package/dist/federation/mod.js +279 -1
  61. package/dist/federation/mq.test.mjs +123 -47
  62. package/dist/federation/negotiation.test.mjs +4 -4
  63. package/dist/federation/outgoing-proof.test.d.mts +2 -0
  64. package/dist/federation/outgoing-proof.test.mjs +931 -0
  65. package/dist/federation/portable-actor.test.d.mts +2 -0
  66. package/dist/federation/portable-actor.test.mjs +496 -0
  67. package/dist/federation/portable-chain.test.d.mts +2 -0
  68. package/dist/federation/portable-chain.test.mjs +370 -0
  69. package/dist/federation/portable-collection.test.d.mts +2 -0
  70. package/dist/federation/portable-collection.test.mjs +832 -0
  71. package/dist/federation/portable-delivery.test.d.mts +2 -0
  72. package/dist/federation/portable-delivery.test.mjs +746 -0
  73. package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
  74. package/dist/federation/portable-gateway-keys.test.mjs +686 -0
  75. package/dist/federation/portable-inbox.test.d.mts +2 -0
  76. package/dist/federation/portable-inbox.test.mjs +1535 -0
  77. package/dist/federation/portable-parse-uri.test.d.mts +2 -0
  78. package/dist/federation/portable-parse-uri.test.mjs +348 -0
  79. package/dist/federation/portable-route-activity.test.d.mts +2 -0
  80. package/dist/federation/portable-route-activity.test.mjs +259 -0
  81. package/dist/federation/portable.test.d.mts +2 -0
  82. package/dist/federation/portable.test.mjs +1072 -0
  83. package/dist/federation/retry.test.mjs +4 -3
  84. package/dist/federation/router.test.mjs +9 -159
  85. package/dist/federation/send.test.mjs +245 -27
  86. package/dist/federation/tasks/codec.test.mjs +4 -4
  87. package/dist/federation/tasks/enqueue.test.mjs +42 -4
  88. package/dist/federation/tasks/tasks.test.mjs +7 -6
  89. package/dist/federation/temporal.test.mjs +4 -4
  90. package/dist/federation/webfinger.test.mjs +275 -12
  91. package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
  92. package/dist/http-B5JT115n.cjs +10630 -0
  93. package/dist/http-DydWCB7R.js +6527 -0
  94. package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
  95. package/dist/key-BspPiXqn.mjs +951 -0
  96. package/dist/key-C1nv7_jD.d.ts +1125 -0
  97. package/dist/key-DYxuyd-s.d.cts +1125 -0
  98. package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
  99. package/dist/keycache-Da8A8mZa.mjs +200 -0
  100. package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
  101. package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
  102. package/dist/kv-DBRWVx3Q.mjs +104 -0
  103. package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
  104. package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
  105. package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
  106. package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
  107. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  108. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  109. package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
  110. package/dist/metrics-DVKYY6K1.d.cts +36 -0
  111. package/dist/metrics-DoRtI0qU.d.ts +36 -0
  112. package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
  113. package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
  114. package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
  115. package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
  116. package/dist/mod-CavhAgLw.d.cts +320 -0
  117. package/dist/mod-CtpF2BU5.d.ts +320 -0
  118. package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
  119. package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
  120. package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
  121. package/dist/mod.cjs +40 -39
  122. package/dist/mod.d.cts +10 -11
  123. package/dist/mod.d.ts +10 -11
  124. package/dist/mod.js +9 -9
  125. package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
  126. package/dist/nodeinfo/client.test.mjs +139 -4
  127. package/dist/nodeinfo/handler.test.mjs +5 -5
  128. package/dist/nodeinfo/mod.cjs +1 -1
  129. package/dist/nodeinfo/mod.d.cts +1 -1
  130. package/dist/nodeinfo/mod.d.ts +1 -1
  131. package/dist/nodeinfo/mod.js +1 -1
  132. package/dist/nodeinfo/types.test.mjs +4 -4
  133. package/dist/otel/exporter.test.mjs +3 -3
  134. package/dist/otel/mod.cjs +0 -1
  135. package/dist/otel/mod.d.cts +1 -1
  136. package/dist/otel/mod.d.ts +1 -1
  137. package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
  138. package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
  139. package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
  140. package/dist/owner-DYgfTFHy.mjs +267 -0
  141. package/dist/portable-collection-BpsVsRov.js +670 -0
  142. package/dist/portable-collection-C74P1vtC.mjs +418 -0
  143. package/dist/portable-collection-DZd2bm7M.cjs +686 -0
  144. package/dist/portable-key-BIMLrAP4.mjs +549 -0
  145. package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
  146. package/dist/proof-Dxy6ByqI.mjs +1299 -0
  147. package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
  148. package/dist/router-u5O5KCQr.mjs +156 -0
  149. package/dist/runtime/mod.cjs +1 -1
  150. package/dist/runtime/mod.js +1 -1
  151. package/dist/secured-document-CxzZZ9nL.mjs +35 -0
  152. package/dist/send-C4dDpAxm.mjs +495 -0
  153. package/dist/sig/accept.test.mjs +1 -1
  154. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  155. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  156. package/dist/sig/compound-proof-producer.test.d.mts +2 -0
  157. package/dist/sig/compound-proof-producer.test.mjs +107 -0
  158. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  159. package/dist/sig/compound-proof-verification.test.mjs +833 -0
  160. package/dist/sig/compound-proof.test.d.mts +2 -0
  161. package/dist/sig/compound-proof.test.mjs +411 -0
  162. package/dist/sig/http.test.mjs +702 -31
  163. package/dist/sig/key-owner-evidence.test.d.mts +2 -0
  164. package/dist/sig/key-owner-evidence.test.mjs +100 -0
  165. package/dist/sig/key.test.mjs +220 -8
  166. package/dist/sig/ld.test.mjs +65 -7
  167. package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
  168. package/dist/sig/mitra-receive-interop.test.mjs +260 -0
  169. package/dist/sig/mitra-send-interop.test.d.mts +2 -0
  170. package/dist/sig/mitra-send-interop.test.mjs +461 -0
  171. package/dist/sig/mod.cjs +18 -16
  172. package/dist/sig/mod.d.cts +4 -4
  173. package/dist/sig/mod.d.ts +4 -4
  174. package/dist/sig/mod.js +3 -3
  175. package/dist/sig/owner.test.mjs +108 -9
  176. package/dist/sig/portable-collection.test.d.mts +2 -0
  177. package/dist/sig/portable-collection.test.mjs +697 -0
  178. package/dist/sig/portable-interop.test.d.mts +2 -0
  179. package/dist/sig/portable-interop.test.mjs +156 -0
  180. package/dist/sig/portable-key-id.test.d.mts +2 -0
  181. package/dist/sig/portable-key-id.test.mjs +67 -0
  182. package/dist/sig/portable-key.test.d.mts +2 -0
  183. package/dist/sig/portable-key.test.mjs +1439 -0
  184. package/dist/sig/proof.test.mjs +1753 -16
  185. package/dist/sig/tootik-request-interop.test.d.mts +2 -0
  186. package/dist/sig/tootik-request-interop.test.mjs +190 -0
  187. package/dist/sig/tootik-send-interop.test.d.mts +2 -0
  188. package/dist/sig/tootik-send-interop.test.mjs +331 -0
  189. package/dist/sig/verification.test.d.mts +2 -0
  190. package/dist/sig/verification.test.mjs +310 -0
  191. package/dist/std__assert-DvMDRPes.mjs +8 -0
  192. package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
  193. package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
  194. package/dist/testing/context.test.d.mts +2 -0
  195. package/dist/testing/context.test.mjs +19 -0
  196. package/dist/testing/kv.test.d.mts +2 -0
  197. package/dist/testing/kv.test.mjs +101 -0
  198. package/dist/testing/mod.d.mts +1617 -48
  199. package/dist/testing/mod.mjs +6 -3
  200. package/dist/testing-t8xDeJZ5.mjs +8 -0
  201. package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
  202. package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
  203. package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
  204. package/dist/utils/docloader.test.mjs +530 -8
  205. package/dist/utils/kv-cache.test.mjs +2 -2
  206. package/dist/utils/mod.cjs +1 -1
  207. package/dist/utils/mod.d.cts +1 -1
  208. package/dist/utils/mod.d.ts +1 -1
  209. package/dist/utils/mod.js +1 -1
  210. package/dist/verification-C3954hQh.mjs +5 -0
  211. package/dist/verification-Dpfcw8ml.mjs +105 -0
  212. package/dist/vocab/cjs.test.mjs +1 -1
  213. package/package.json +8 -8
  214. package/dist/assert_rejects-EgstcQy5.mjs +0 -82
  215. package/dist/docloader-e1khNUKy.mjs +0 -49
  216. package/dist/http-CsmQ-BQ4.js +0 -2483
  217. package/dist/http-VyDTd4G3.d.cts +0 -553
  218. package/dist/http-hlIjmewS.cjs +0 -2694
  219. package/dist/http-lf8Hsd91.d.ts +0 -553
  220. package/dist/key-DfPsX8hv.mjs +0 -453
  221. package/dist/keycache-_DHqz5z_.mjs +0 -97
  222. package/dist/metrics-CoUJLQhZ.d.cts +0 -276
  223. package/dist/metrics-Dc4jR-IC.d.ts +0 -276
  224. package/dist/mod-B0hW12_O.d.cts +0 -307
  225. package/dist/mod-COIAjwRS.d.ts +0 -307
  226. package/dist/owner-Br9YYzn_.mjs +0 -120
  227. package/dist/proof-B4ngoaNQ.mjs +0 -316
  228. package/dist/proof-B6E0XUXJ.js +0 -1415
  229. package/dist/proof-NN4xCQGL.cjs +0 -1562
  230. package/dist/send-CxHKUNzv.mjs +0 -239
  231. package/dist/std__assert-B2NnY9bq.mjs +0 -79
  232. package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
  233. /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
  234. /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
  235. /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
  236. /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
  237. /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
  238. /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
  239. /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
  240. /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
  241. /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
  242. /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
  243. /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
  244. /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
@@ -1,18 +1,26 @@
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-B4ngoaNQ.mjs";
12
- import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab";
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: await proof.toJsonLd({
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 verified = await verifyObject(Note, await (await signObject(new Note({
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 {};