@fedify/fedify 2.4.0-dev.1727 → 2.4.0-dev.1758

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 (57) hide show
  1. package/dist/{builder-f5-YwHfj.mjs → builder-DWu8BAPs.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{deno-Deyoxesx.mjs → deno-D7ld9Esp.mjs} +1 -1
  4. package/dist/{docloader-B3kuDBg9.mjs → docloader-Cxg2qaBo.mjs} +2 -2
  5. package/dist/federation/builder.test.mjs +1 -1
  6. package/dist/federation/handler.test.mjs +3 -3
  7. package/dist/federation/idempotency.test.mjs +2 -2
  8. package/dist/federation/metrics.test.mjs +1 -1
  9. package/dist/federation/middleware.test.mjs +7 -7
  10. package/dist/federation/mod.cjs +1 -1
  11. package/dist/federation/mod.js +1 -1
  12. package/dist/federation/send.test.mjs +3 -3
  13. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  14. package/dist/federation/tasks/tasks.test.mjs +2 -2
  15. package/dist/federation/temporal.test.mjs +1 -1
  16. package/dist/federation/webfinger.test.mjs +1 -1
  17. package/dist/{federation-BzRjeOky.cjs → federation-1xfo16fx.cjs} +3 -3
  18. package/dist/{http-Bw6s_dOB.js → http-5U30379W.js} +1 -1
  19. package/dist/{http-TjhZfiSN.mjs → http-BBDPU-Rw.mjs} +3 -3
  20. package/dist/{http-AyKngJ_h.cjs → http-DUm4owJl.cjs} +1 -1
  21. package/dist/{key-b4kf4yZT.mjs → key-BfV1x8KO.mjs} +2 -2
  22. package/dist/{kv-cache-CQsh-KUw.js → kv-cache-DT6mvUaV.js} +1 -1
  23. package/dist/{kv-cache-BTVaZrf-.cjs → kv-cache-ufxbpVJ1.cjs} +1 -1
  24. package/dist/{kv-cache-CpILvhUV.mjs → kv-cache-z0NLyZT_.mjs} +1 -1
  25. package/dist/{ld-CKujkAfG.mjs → ld-B2FQHinq.mjs} +3 -3
  26. package/dist/{metrics-AlCGTDrr.mjs → metrics-B9zYPaIm.mjs} +1 -1
  27. package/dist/{middleware-LPbZz6-1.mjs → middleware-2cCYidZp.mjs} +1 -1
  28. package/dist/{middleware-CZcuedl-.js → middleware-Bcc-Y7Lj.js} +3 -3
  29. package/dist/{middleware-C9Y5lLZN.mjs → middleware-SfRDkyWy.mjs} +12 -12
  30. package/dist/{mod-CuDDaIIt.d.cts → mod-CnGLNoVw.d.cts} +85 -3
  31. package/dist/{mod-D-IhOEOP.d.ts → mod-DzRXACM5.d.ts} +85 -3
  32. package/dist/mod.cjs +5 -4
  33. package/dist/mod.d.cts +3 -3
  34. package/dist/mod.d.ts +3 -3
  35. package/dist/mod.js +5 -5
  36. package/dist/nodeinfo/handler.test.mjs +1 -1
  37. package/dist/{owner-D1O8v8FB.mjs → owner-C7EZVCtD.mjs} +2 -2
  38. package/dist/{proof-DcSm-2eG.cjs → proof-BcFOlgpM.cjs} +607 -29
  39. package/dist/proof-CFbBDSR8.mjs +890 -0
  40. package/dist/{proof-BTBolSnd.js → proof-DEeUDySM.js} +603 -31
  41. package/dist/{send-DG7JG280.mjs → send-BwlAzLrv.mjs} +3 -3
  42. package/dist/sig/http.test.mjs +2 -2
  43. package/dist/sig/key.test.mjs +1 -1
  44. package/dist/sig/ld.test.mjs +2 -2
  45. package/dist/sig/mod.cjs +3 -2
  46. package/dist/sig/mod.d.cts +2 -2
  47. package/dist/sig/mod.d.ts +2 -2
  48. package/dist/sig/mod.js +3 -3
  49. package/dist/sig/owner.test.mjs +1 -1
  50. package/dist/sig/proof.test.mjs +956 -8
  51. package/dist/{temporal-DA3jx8x1.mjs → temporal-D6rOK4lX.mjs} +1 -1
  52. package/dist/utils/docloader.test.mjs +2 -2
  53. package/dist/utils/kv-cache.test.mjs +1 -1
  54. package/dist/utils/mod.cjs +1 -1
  55. package/dist/utils/mod.js +1 -1
  56. package/package.json +6 -6
  57. package/dist/proof-DeMR7Akd.mjs +0 -316
@@ -10,11 +10,12 @@ import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
10
10
  import { t as assert } from "../assert-CebeUf0C.mjs";
11
11
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
12
12
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-BQSHRLhg.mjs";
13
- import { a as verifyProof, i as verifyObject, n as hasProofLike, r as signObject, t as createProof } from "../proof-DeMR7Akd.mjs";
13
+ import { a as verifyPortableObjectProof, i as verifyObject, n as hasProofLike, o as verifyProof, r as signObject, t as createProof } from "../proof-CFbBDSR8.mjs";
14
14
  import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab";
15
15
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
16
- import { decodeMultibase, exportDidKey, exportMultibaseKey, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
16
+ import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
17
17
  import { decodeHex } from "byte-encodings/hex";
18
+ import serialize from "json-canon";
18
19
  //#region src/sig/proof.test.ts
19
20
  const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
20
21
  "kty": "OKP",
@@ -25,6 +26,52 @@ const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
25
26
  ext: true
26
27
  }, "Ed25519", true, ["sign"]);
27
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
+ }
28
75
  const fep8b32TestVectorActivity = new Create({
29
76
  id: new URL("https://server.example/activities/1"),
30
77
  actor: new URL("https://server.example/users/alice"),
@@ -328,6 +375,202 @@ test("verifyProof()", async () => {
328
375
  }), null);
329
376
  assertFalse(contextLoaderCalls.includes("https://attacker.example/ctx"));
330
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
+ });
331
574
  test("verifyProof() resolves did:key verification methods locally", async () => {
332
575
  const multibaseKey = (await exportDidKey(ed25519PublicKey.publicKey)).slice(8);
333
576
  const did = `did:key:${multibaseKey}`;
@@ -510,12 +753,13 @@ test("verifyProof() records verification duration metric", async (t) => {
510
753
  });
511
754
  await t.step("verifyObject() wrapper emits one measurement per inner verifyProof()", async () => {
512
755
  const [meterProvider, recorder] = createTestMeterProvider();
756
+ const { "@context": _proofContext, ...embeddedProof } = await proof.toJsonLd({
757
+ format: "compact",
758
+ contextLoader: mockDocumentLoader
759
+ });
513
760
  assert(await verifyObject(Create, {
514
761
  ...jsonLd,
515
- proof: await proof.toJsonLd({
516
- format: "compact",
517
- contextLoader: mockDocumentLoader
518
- })
762
+ proof: embeddedProof
519
763
  }, {
520
764
  documentLoader: mockDocumentLoader,
521
765
  contextLoader: mockDocumentLoader,
@@ -549,6 +793,632 @@ test("verifyProof() records verification duration metric", async (t) => {
549
793
  assertFalse("object_integrity_proofs.cryptosuite" in measurements[0].attributes);
550
794
  });
551
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
+ });
552
1422
  test("verifyObject()", async () => {
553
1423
  const options = {
554
1424
  documentLoader: mockDocumentLoader,
@@ -595,7 +1465,7 @@ test("verifyObject()", async () => {
595
1465
  test("verifyObject() accepts did:key proofs for matching portable attribution origin", async () => {
596
1466
  const did = await exportDidKey(ed25519PublicKey.publicKey);
597
1467
  const keyId = new URL(`${did}#${did.substring(8)}`);
598
- const verified = await verifyObject(Note, await (await signObject(new Note({
1468
+ const jsonLd = await (await signObject(new Note({
599
1469
  id: parseIri(`ap://did:key:${did.substring(8)}/objects/1`),
600
1470
  attribution: parseIri(`ap://did:key:${did.substring(8)}/actor`),
601
1471
  content: "Portable note"
@@ -606,7 +1476,8 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
606
1476
  format: "compact",
607
1477
  contextLoader: mockDocumentLoader,
608
1478
  context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
609
- }), {
1479
+ });
1480
+ const verified = await verifyObject(Note, jsonLd, {
610
1481
  documentLoader() {
611
1482
  throw new TypeError("did:key must not use the document loader");
612
1483
  },
@@ -614,6 +1485,83 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
614
1485
  });
615
1486
  assertInstanceOf(verified, Note);
616
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);
617
1565
  });
618
1566
  test("verifyObject() accepts multiple portable attributions from the same did:key origin", async () => {
619
1567
  const did = await exportDidKey(ed25519PublicKey.publicKey);