@fedify/fedify 2.1.23 → 2.1.25

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 (101) hide show
  1. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert-BhQd8QHP.mjs} +19 -1
  2. package/dist/{builder-BlCsjkrX.mjs → builder-DsBAef6b.mjs} +3 -3
  3. package/dist/{client-CZWLyLmp.mjs → client-DM72NZuk.mjs} +85 -7
  4. package/dist/compat/mod.d.cts +1 -1
  5. package/dist/compat/mod.d.ts +1 -1
  6. package/dist/compat/transformers.test.mjs +3 -4
  7. package/dist/{context-_l9t359k.d.ts → context-BgU5UWTx.d.ts} +4 -3
  8. package/dist/{context-o22vjzS2.d.cts → context-D4s0Wk3N.d.cts} +4 -3
  9. package/dist/{deno-B_Gwkjh7.mjs → deno-9AaebKOA.mjs} +1 -1
  10. package/dist/docloader-DX96uPUj.mjs +78 -0
  11. package/dist/federation/builder.test.mjs +3 -3
  12. package/dist/federation/collection.test.mjs +2 -2
  13. package/dist/federation/handler.test.mjs +128 -10
  14. package/dist/federation/idempotency.test.mjs +5 -5
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/keycache.test.mjs +44 -10
  17. package/dist/federation/kv.test.mjs +2 -2
  18. package/dist/federation/middleware.test.mjs +227 -12
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/mq.test.mjs +99 -44
  24. package/dist/federation/negotiation.test.mjs +3 -3
  25. package/dist/federation/retry.test.mjs +1 -1
  26. package/dist/federation/router.test.mjs +2 -2
  27. package/dist/federation/send.test.mjs +202 -7
  28. package/dist/federation/temporal.test.mjs +3 -3
  29. package/dist/federation/webfinger.test.mjs +3 -3
  30. package/dist/{http-DIh9_s_-.mjs → http-B5qAeoJd.mjs} +8 -4
  31. package/dist/{http-Yoq0YGz7.cjs → http-BJEMpnnY.cjs} +171 -7
  32. package/dist/{http-88deUx3Y.js → http-CdJ6n4rA.js} +160 -8
  33. package/dist/{http-BDZeS5om.d.ts → http-Dl00HXr4.d.ts} +1 -1
  34. package/dist/{http-C87EWkO0.d.cts → http-DqZhyEso.d.cts} +1 -1
  35. package/dist/{inbox-BxCHIWPD.mjs → inbox-BSyjaFbo.mjs} +1 -1
  36. package/dist/{key-DywPPmb0.mjs → key-x9f-H7eO.mjs} +154 -6
  37. package/dist/{keycache-CCSwkQcY.mjs → keycache-CaO7wyQA.mjs} +12 -4
  38. package/dist/{kv-cache-HA011rBG.cjs → kv-cache-BDp2c_QE.cjs} +34 -8
  39. package/dist/{kv-cache-rHQOc5rE.js → kv-cache-Dx3QwNBI.js} +35 -9
  40. package/dist/{ld-Ch6FVq7X.mjs → ld-cfUwoi0C.mjs} +3 -3
  41. package/dist/{middleware-itcr4fAv.cjs → middleware-BXJpAXhO.cjs} +1 -1
  42. package/dist/{middleware-CYsQG25-.js → middleware-CP35ElZv.js} +99 -14
  43. package/dist/{middleware-DABFg_Xy.mjs → middleware-DDT9ljTQ.mjs} +1 -1
  44. package/dist/{middleware-D2O8b03q.mjs → middleware-DHHRwSZH.mjs} +31 -20
  45. package/dist/{middleware-CUNIlYzF.cjs → middleware-XXuSt_6b.cjs} +99 -14
  46. package/dist/{mod-CjiK2zrW.d.ts → mod-BLDHhuW0.d.ts} +1 -1
  47. package/dist/{mod-0dF0OeGw.d.cts → mod-Bx80LUTA.d.cts} +1 -1
  48. package/dist/{mod-Bv1elC-b.d.ts → mod-DcAiY74j.d.ts} +1 -1
  49. package/dist/{mod-D5YlzzP3.d.cts → mod-DiuFTYUt.d.cts} +1 -1
  50. package/dist/{mod-dxOnKp2Y.d.cts → mod-DkE98l_7.d.cts} +3 -1
  51. package/dist/{mod-BTWFtZwa.d.ts → mod-DqkY4FDr.d.ts} +3 -1
  52. package/dist/mod.cjs +5 -5
  53. package/dist/mod.d.cts +6 -6
  54. package/dist/mod.d.ts +6 -6
  55. package/dist/mod.js +5 -5
  56. package/dist/nodeinfo/client.test.mjs +77 -2
  57. package/dist/nodeinfo/handler.test.mjs +3 -3
  58. package/dist/nodeinfo/mod.cjs +1 -1
  59. package/dist/nodeinfo/mod.js +1 -1
  60. package/dist/nodeinfo/types.test.mjs +2 -2
  61. package/dist/otel/exporter.test.mjs +2 -2
  62. package/dist/{owner-DEvZuyOE.d.cts → owner-B3hNQCG8.d.cts} +11 -0
  63. package/dist/{owner-CnngXDNJ.d.ts → owner-CiBPcC6F.d.ts} +11 -0
  64. package/dist/owner-DklgvZXn.mjs +175 -0
  65. package/dist/{proof-Dt5GV-xi.mjs → proof-CI4rsVLu.mjs} +2 -2
  66. package/dist/{proof-K3inAqnp.js → proof-Dsa_ZC-M.js} +113 -59
  67. package/dist/{proof-CEn2R6W9.cjs → proof-DzFzyfIc.cjs} +113 -59
  68. package/dist/{send-CqaJDT2z.mjs → send-DJmW6Z-I.mjs} +73 -6
  69. package/dist/sig/accept.test.mjs +1 -1
  70. package/dist/sig/http.test.mjs +45 -5
  71. package/dist/sig/key.test.mjs +171 -4
  72. package/dist/sig/ld.test.mjs +59 -4
  73. package/dist/sig/mod.cjs +2 -2
  74. package/dist/sig/mod.d.cts +3 -3
  75. package/dist/sig/mod.d.ts +3 -3
  76. package/dist/sig/mod.js +2 -2
  77. package/dist/sig/owner.test.mjs +106 -7
  78. package/dist/sig/proof.test.mjs +73 -4
  79. package/dist/{std__assert-Duiq_YC9.mjs → std__assert-BUOJXi-Z.mjs} +1 -26
  80. package/dist/{temporal-G_GDghC-.mjs → temporal-BanFifxu.mjs} +1 -1
  81. package/dist/{types-m8u9L86X.cjs → types-DRPjmHVV.cjs} +101 -5
  82. package/dist/{types-DTONwNqL.js → types-miAYfiGO.js} +85 -7
  83. package/dist/utils/docloader.test.mjs +325 -5
  84. package/dist/utils/kv-cache.test.mjs +1 -1
  85. package/dist/utils/mod.cjs +1 -1
  86. package/dist/utils/mod.d.cts +1 -1
  87. package/dist/utils/mod.d.ts +1 -1
  88. package/dist/utils/mod.js +1 -1
  89. package/dist/vocab/cjs.test.mjs +1 -1
  90. package/package.json +7 -7
  91. package/dist/assert-ddO5KLpe.mjs +0 -23
  92. package/dist/docloader-B-IG1dVk.mjs +0 -52
  93. package/dist/owner-Cygg-fHW.mjs +0 -120
  94. /package/dist/{accept-Dd__NiUL.mjs → accept-BavzedPB.mjs} +0 -0
  95. /package/dist/{collection-BD6-SZ6O.mjs → collection-ChgDTHLz.mjs} +0 -0
  96. /package/dist/{keys-BAK-tUlf.mjs → keys-CtZLJq76.mjs} +0 -0
  97. /package/dist/{kv-tL2TOE9X.mjs → kv-BrZHNugx.mjs} +0 -0
  98. /package/dist/{kv-cache-FsT1ljhs.mjs → kv-cache-DqEKe5E0.mjs} +0 -0
  99. /package/dist/{negotiation-DnsfFF8I.mjs → negotiation-BehA2uul.mjs} +0 -0
  100. /package/dist/{retry-CJL0poaU.mjs → retry-B_E3V_Dx.mjs} +0 -0
  101. /package/dist/{types-DCP0WLdt.mjs → types-C37hquWI.mjs} +0 -0
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature, t as formatAcceptSignature } from "../accept-Dd__NiUL.mjs";
4
+ import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature, t as formatAcceptSignature } from "../accept-BavzedPB.mjs";
5
5
  import { test } from "@fedify/fixture";
6
6
  import { deepStrictEqual, strictEqual } from "node:assert/strict";
7
7
  //#region src/sig/accept.test.ts
@@ -3,13 +3,13 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as esm_default } from "../esm-DVILvP5e.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
- import { a as assertExists, t as assertStringIncludes } from "../std__assert-Duiq_YC9.mjs";
6
+ import { i as assertExists, t as assertStringIncludes } from "../std__assert-BUOJXi-Z.mjs";
7
7
  import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
8
+ import { t as assert } from "../assert-BhQd8QHP.mjs";
8
9
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
9
- import { t as assert } from "../assert-ddO5KLpe.mjs";
10
- import { t as exportJwk } from "../key-DywPPmb0.mjs";
11
- import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-DIh9_s_-.mjs";
12
- import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
10
+ import { t as exportJwk } from "../key-x9f-H7eO.mjs";
11
+ import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-B5qAeoJd.mjs";
12
+ import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-CtZLJq76.mjs";
13
13
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
14
14
  import { FetchError, exportSpki } from "@fedify/vocab-runtime";
15
15
  import { encodeBase64 } from "byte-encodings/base64";
@@ -421,6 +421,46 @@ test("verifyRequest() [rfc9421] successful GET verification", async () => {
421
421
  currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
422
422
  }), rsaPublicKey2, "Valid signature should verify to the correct public key");
423
423
  });
424
+ for (const failure of [
425
+ "missing key",
426
+ "invalid signature",
427
+ "digest mismatch",
428
+ "empty body",
429
+ "cached key"
430
+ ]) test(`verifyRequest() [rfc9421] multiple POST signatures: ${failure}`, async () => {
431
+ const body = failure === "empty body" ? "" : "Hello, world!";
432
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
433
+ const signed = await signRequest(new Request("https://example.com/inbox", {
434
+ method: "POST",
435
+ body
436
+ }), rsaPrivateKey2, new URL("https://example.com/key2"), {
437
+ spec: "rfc9421",
438
+ currentTime
439
+ });
440
+ const input = signed.headers.get("Signature-Input");
441
+ const signature = signed.headers.get("Signature");
442
+ const firstInput = failure === "missing key" ? input.replace("https://example.com/key2", "https://example.com/missing-key") : input;
443
+ signed.headers.set("Signature-Input", `${firstInput}, ${input.replace(/^sig1=/, "sig2=")}`);
444
+ signed.headers.set("Signature", `sig1=:AAAAAA==:, ${signature.replace(/^sig1=/, "sig2=")}`);
445
+ const request = failure === "digest mismatch" ? new Request(signed, { body: "Tampered body" }) : signed;
446
+ const loaded = [];
447
+ assertEquals(await verifyRequest(request, {
448
+ keyCache: failure === "cached key" ? {
449
+ get: () => Promise.resolve(rsaPublicKey1),
450
+ set: async () => {}
451
+ } : void 0,
452
+ spec: "rfc9421",
453
+ currentTime,
454
+ contextLoader: mockDocumentLoader,
455
+ documentLoader: (url, options) => {
456
+ loaded.push(url);
457
+ return mockDocumentLoader(url, options);
458
+ }
459
+ }), failure === "digest mismatch" ? null : rsaPublicKey2);
460
+ assertEquals(loaded, failure === "digest mismatch" ? [] : failure === "missing key" ? ["https://example.com/missing-key", "https://example.com/key2"] : ["https://example.com/key2", "https://example.com/key2"]);
461
+ assertFalse(request.bodyUsed);
462
+ assertEquals(await request.text(), failure === "digest mismatch" ? "Tampered body" : body);
463
+ });
424
464
  test("verifyRequest() [rfc9421] manual POST verification", async () => {
425
465
  const currentTimestamp = 1709626184;
426
466
  const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
@@ -2,11 +2,12 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
5
+ import "../std__assert-BUOJXi-Z.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
7
+ import { t as assert } from "../assert-BhQd8QHP.mjs";
7
8
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
8
- import { a as importJwk, i as generateCryptoKeyPair, n as fetchKey, o as validateCryptoKey, r as fetchKeyDetailed, t as exportJwk } from "../key-DywPPmb0.mjs";
9
- import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
9
+ import { a as generateCryptoKeyPair, i as fetchKeyDetailed, o as importJwk, r as fetchKey, s as validateCryptoKey, t as exportJwk } from "../key-x9f-H7eO.mjs";
10
+ import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
10
11
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
11
12
  import { CryptographicKey, Multikey } from "@fedify/vocab";
12
13
  import { FetchError } from "@fedify/vocab-runtime";
@@ -267,7 +268,7 @@ test("fetchKeyDetailed()", async () => {
267
268
  key: rsaPublicKey1,
268
269
  cached: false
269
270
  });
270
- assertEquals(documentLoaderCalls, 1);
271
+ assertEquals(documentLoaderCalls, 2);
271
272
  const spans = exporter.getSpans("activitypub.fetch_key");
272
273
  assertEquals(spans.length, 2);
273
274
  assertEquals(spans[0].attributes["activitypub.actor.key.cached"], true);
@@ -398,5 +399,171 @@ test("fetchKey() returns null for a malformed actor publicKey", async () => {
398
399
  cached: true
399
400
  });
400
401
  });
402
+ test("fetchKey() rejects a key whose owner does not link back", async () => {
403
+ const keyId = "https://attacker.example/key";
404
+ const keyDocument = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
405
+ const serveKeyOwnedBy = (owner) => ({
406
+ documentLoader(resource) {
407
+ if (resource === keyId) return Promise.resolve({
408
+ contextUrl: null,
409
+ documentUrl: resource,
410
+ document: {
411
+ ...keyDocument,
412
+ id: keyId,
413
+ owner
414
+ }
415
+ });
416
+ return mockDocumentLoader(resource);
417
+ },
418
+ contextLoader: mockDocumentLoader
419
+ });
420
+ assertEquals(await fetchKey(keyId, CryptographicKey, serveKeyOwnedBy("https://example.com/person")), {
421
+ key: null,
422
+ cached: false
423
+ });
424
+ assertEquals(await fetchKey(keyId, CryptographicKey, serveKeyOwnedBy("https://impersonated.invalid/users/victim")), {
425
+ key: null,
426
+ cached: false
427
+ });
428
+ });
429
+ test("fetchKey() rejects a Multikey whose controller does not link back", async () => {
430
+ const keyId = "https://attacker.example/multikey";
431
+ const keyDocument = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
432
+ assertEquals(await fetchKey(keyId, Multikey, {
433
+ documentLoader(resource) {
434
+ if (resource === keyId) return Promise.resolve({
435
+ contextUrl: null,
436
+ documentUrl: resource,
437
+ document: {
438
+ ...keyDocument,
439
+ id: keyId,
440
+ controller: "https://example.com/person2"
441
+ }
442
+ });
443
+ return mockDocumentLoader(resource);
444
+ },
445
+ contextLoader: mockDocumentLoader
446
+ }), {
447
+ key: null,
448
+ cached: false
449
+ });
450
+ });
451
+ test("fetchKey() rejects an actor document from another origin", async () => {
452
+ const keyId = "https://attacker.example/key";
453
+ const impersonated = "https://example.com/person";
454
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
455
+ assertEquals(await fetchKey(keyId, CryptographicKey, {
456
+ documentLoader(resource) {
457
+ if (resource === keyId) return Promise.resolve({
458
+ contextUrl: null,
459
+ documentUrl: resource,
460
+ document: {
461
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
462
+ id: impersonated,
463
+ type: "Person",
464
+ publicKey: [{
465
+ id: `${impersonated}#main-key`,
466
+ type: "CryptographicKey",
467
+ owner: impersonated,
468
+ publicKeyPem
469
+ }]
470
+ }
471
+ });
472
+ return mockDocumentLoader(resource);
473
+ },
474
+ contextLoader: mockDocumentLoader
475
+ }), {
476
+ key: null,
477
+ cached: false
478
+ });
479
+ });
480
+ test("fetchKey() rejects a key that disowns the actor document holding it", async () => {
481
+ const actorId = "https://attacker.example/actor";
482
+ const keyId = `${actorId}#main-key`;
483
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
484
+ assertEquals(await fetchKey(keyId, CryptographicKey, {
485
+ documentLoader(resource) {
486
+ if (resource === keyId || resource === actorId) return Promise.resolve({
487
+ contextUrl: null,
488
+ documentUrl: resource,
489
+ document: {
490
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
491
+ id: actorId,
492
+ type: "Person",
493
+ publicKey: [{
494
+ id: keyId,
495
+ type: "CryptographicKey",
496
+ owner: "https://example.com/person",
497
+ publicKeyPem
498
+ }]
499
+ }
500
+ });
501
+ return mockDocumentLoader(resource);
502
+ },
503
+ contextLoader: mockDocumentLoader
504
+ }), {
505
+ key: null,
506
+ cached: false
507
+ });
508
+ });
509
+ test("fetchKey() records the owner its actor document establishes", async () => {
510
+ const { key } = await fetchKey("https://example.com/users/handle#main-key", CryptographicKey, {
511
+ documentLoader: mockDocumentLoader,
512
+ contextLoader: mockDocumentLoader
513
+ });
514
+ assertEquals(key?.ownerId, new URL("https://example.com/users/handle"));
515
+ });
516
+ test("fetchKey() accepts a key document that leaves its id implicit", async () => {
517
+ const keyId = "https://example.com/key";
518
+ const keyDocument = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
519
+ delete keyDocument.id;
520
+ const { key } = await fetchKey(keyId, CryptographicKey, {
521
+ documentLoader(resource) {
522
+ if (resource === keyId) return Promise.resolve({
523
+ contextUrl: null,
524
+ documentUrl: resource,
525
+ document: keyDocument
526
+ });
527
+ return mockDocumentLoader(resource);
528
+ },
529
+ contextLoader: mockDocumentLoader
530
+ });
531
+ assertEquals(key?.id, new URL(keyId));
532
+ assertEquals(key?.ownerId, new URL("https://example.com/person"));
533
+ assert(key?.publicKey != null);
534
+ });
535
+ test("fetchKey() records the controller an actor document establishes", async () => {
536
+ const actorId = "https://example.com/multikey-actor";
537
+ const keyId = `${actorId}#assertion`;
538
+ const { publicKeyMultibase } = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
539
+ const { key } = await fetchKey(keyId, Multikey, {
540
+ documentLoader(resource) {
541
+ if (resource === actorId || resource === keyId) return Promise.resolve({
542
+ contextUrl: null,
543
+ documentUrl: actorId,
544
+ document: {
545
+ "@context": [
546
+ "https://www.w3.org/ns/activitystreams",
547
+ "https://w3id.org/security/v1",
548
+ "https://w3id.org/security/multikey/v1",
549
+ "https://w3id.org/security/data-integrity/v1",
550
+ "https://www.w3.org/ns/did/v1"
551
+ ],
552
+ id: actorId,
553
+ type: "Person",
554
+ assertionMethod: [{
555
+ id: keyId,
556
+ type: "Multikey",
557
+ publicKeyMultibase
558
+ }]
559
+ }
560
+ });
561
+ return mockDocumentLoader(resource);
562
+ },
563
+ contextLoader: mockDocumentLoader
564
+ });
565
+ assertEquals(key?.controllerId, new URL(actorId));
566
+ assert(key?.publicKey != null);
567
+ });
401
568
  //#endregion
402
569
  export {};
@@ -3,16 +3,19 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
6
+ import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
6
7
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
7
- import { t as assert } from "../assert-ddO5KLpe.mjs";
8
- import { i as generateCryptoKeyPair } from "../key-DywPPmb0.mjs";
9
- import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
10
- import { a as compactJsonLd, d as isInvalidUrlTypeError, f as signJsonLd, h as verifySignature, i as attachSignature, m as verifyJsonLd, n as UnsafeJsonLdError, o as createSignature, s as detachSignature } from "../ld-Ch6FVq7X.mjs";
8
+ import { a as generateCryptoKeyPair } from "../key-x9f-H7eO.mjs";
9
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
10
+ import { a as compactJsonLd, d as isInvalidUrlTypeError, f as signJsonLd, h as verifySignature, i as attachSignature, m as verifyJsonLd, n as UnsafeJsonLdError, o as createSignature, s as detachSignature } from "../ld-cfUwoi0C.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { CryptographicKey } from "@fedify/vocab";
13
13
  import { encodeBase64 } from "byte-encodings/base64";
14
14
  //#region src/sig/ld.test.ts
15
15
  test("isInvalidUrlTypeError()", () => {
16
+ assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Invalid URL string.")));
17
+ assertFalse(isInvalidUrlTypeError(/* @__PURE__ */ new Error("Invalid URL string.")));
18
+ assertFalse(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Invalid URL string. Other error")));
16
19
  assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Invalid URL: http://[")));
17
20
  assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("\"http://[\" cannot be parsed as a URL.")));
18
21
  const error = /* @__PURE__ */ new TypeError("Failed to parse URL");
@@ -763,5 +766,57 @@ test("compactJsonLd() rejects inputs that compact into @graph wrappers", async (
763
766
  content: "two"
764
767
  }], mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
765
768
  });
769
+ test("verifyJsonLd() rejects a key that claims a forged owner", async () => {
770
+ const keyId = new URL("https://attacker.example/key");
771
+ const impersonated = "https://example.com/person";
772
+ const { publicKeyPem } = await rsaPublicKey2.toJsonLd({ contextLoader: mockDocumentLoader });
773
+ const signed = await signJsonLd({
774
+ "@context": "https://www.w3.org/ns/activitystreams",
775
+ id: "https://attacker.example/activities/1",
776
+ type: "Create",
777
+ actor: impersonated
778
+ }, rsaPrivateKey2, keyId, { contextLoader: mockDocumentLoader });
779
+ const serveKeyOwnedBy = (owner) => (resource) => {
780
+ if (resource === keyId.href) return Promise.resolve({
781
+ contextUrl: null,
782
+ documentUrl: resource,
783
+ document: {
784
+ "@context": "https://w3id.org/security/v1",
785
+ id: keyId.href,
786
+ type: "CryptographicKey",
787
+ owner,
788
+ publicKeyPem
789
+ }
790
+ });
791
+ if (resource === "https://attacker.example/actor") return Promise.resolve({
792
+ contextUrl: null,
793
+ documentUrl: resource,
794
+ document: {
795
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
796
+ id: resource,
797
+ type: "Person",
798
+ publicKey: [keyId.href]
799
+ }
800
+ });
801
+ return mockDocumentLoader(resource);
802
+ };
803
+ assertEquals(await verifySignature(signed, {
804
+ documentLoader: serveKeyOwnedBy(impersonated),
805
+ contextLoader: mockDocumentLoader
806
+ }), null);
807
+ assertFalse(await verifyJsonLd(signed, {
808
+ documentLoader: serveKeyOwnedBy(impersonated),
809
+ contextLoader: mockDocumentLoader
810
+ }));
811
+ const honestLoader = serveKeyOwnedBy("https://attacker.example/actor");
812
+ assertInstanceOf(await verifySignature(signed, {
813
+ documentLoader: honestLoader,
814
+ contextLoader: mockDocumentLoader
815
+ }), CryptographicKey);
816
+ assertFalse(await verifyJsonLd(signed, {
817
+ documentLoader: honestLoader,
818
+ contextLoader: mockDocumentLoader
819
+ }));
820
+ });
766
821
  //#endregion
767
822
  export {};
package/dist/sig/mod.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_http = require("../http-Yoq0YGz7.cjs");
5
- const require_proof = require("../proof-CEn2R6W9.cjs");
4
+ const require_http = require("../http-BJEMpnnY.cjs");
5
+ const require_proof = require("../proof-DzFzyfIc.cjs");
6
6
  exports.attachSignature = require_proof.attachSignature;
7
7
  exports.createProof = require_proof.createProof;
8
8
  exports.createSignature = require_proof.createSignature;
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { C as exportJwk, D as importJwk, E as generateCryptoKeyPair, S as KeyCache, T as fetchKeyDetailed, _ as validateAcceptSignature, a as VerifyRequestDetailedResult, b as FetchKeyOptions, c as signRequest, d as AcceptSignatureMember, f as AcceptSignatureParameters, g as parseAcceptSignature, h as fulfillAcceptSignature, i as SignRequestOptions, l as verifyRequest, m as formatAcceptSignature, n as HttpMessageSignaturesSpecDeterminer, o as VerifyRequestFailureReason, p as FulfillAcceptSignatureResult, r as Rfc9421SignRequestOptions, s as VerifyRequestOptions, t as HttpMessageSignaturesSpec, u as verifyRequestDetailed, v as FetchKeyDetailedResult, w as fetchKey, x as FetchKeyResult, y as FetchKeyErrorResult } from "../http-C87EWkO0.cjs";
3
- import { i as getKeyOwner, n as GetKeyOwnerOptions, r as doesActorOwnKey, t as DoesActorOwnKeyOptions } from "../owner-DEvZuyOE.cjs";
4
- import { _ as verifyJsonLd, a as createProof, c as verifyProof, d as VerifyJsonLdOptions, f as VerifySignatureOptions, g as signJsonLd, h as detachSignature, i as VerifyProofOptions, l as CreateSignatureOptions, m as createSignature, n as SignObjectOptions, o as signObject, p as attachSignature, r as VerifyObjectOptions, s as verifyObject, t as CreateProofOptions, u as SignJsonLdOptions, v as verifySignature } from "../mod-D5YlzzP3.cjs";
2
+ import { C as exportJwk, D as importJwk, E as generateCryptoKeyPair, S as KeyCache, T as fetchKeyDetailed, _ as validateAcceptSignature, a as VerifyRequestDetailedResult, b as FetchKeyOptions, c as signRequest, d as AcceptSignatureMember, f as AcceptSignatureParameters, g as parseAcceptSignature, h as fulfillAcceptSignature, i as SignRequestOptions, l as verifyRequest, m as formatAcceptSignature, n as HttpMessageSignaturesSpecDeterminer, o as VerifyRequestFailureReason, p as FulfillAcceptSignatureResult, r as Rfc9421SignRequestOptions, s as VerifyRequestOptions, t as HttpMessageSignaturesSpec, u as verifyRequestDetailed, v as FetchKeyDetailedResult, w as fetchKey, x as FetchKeyResult, y as FetchKeyErrorResult } from "../http-DqZhyEso.cjs";
3
+ import { i as getKeyOwner, n as GetKeyOwnerOptions, r as doesActorOwnKey, t as DoesActorOwnKeyOptions } from "../owner-B3hNQCG8.cjs";
4
+ import { _ as verifyJsonLd, a as createProof, c as verifyProof, d as VerifyJsonLdOptions, f as VerifySignatureOptions, g as signJsonLd, h as detachSignature, i as VerifyProofOptions, l as CreateSignatureOptions, m as createSignature, n as SignObjectOptions, o as signObject, p as attachSignature, r as VerifyObjectOptions, s as verifyObject, t as CreateProofOptions, u as SignJsonLdOptions, v as verifySignature } from "../mod-DiuFTYUt.cjs";
5
5
  export { AcceptSignatureMember, AcceptSignatureParameters, CreateProofOptions, CreateSignatureOptions, DoesActorOwnKeyOptions, FetchKeyDetailedResult, FetchKeyErrorResult, FetchKeyOptions, FetchKeyResult, FulfillAcceptSignatureResult, GetKeyOwnerOptions, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, Rfc9421SignRequestOptions, SignJsonLdOptions, SignObjectOptions, SignRequestOptions, VerifyJsonLdOptions, VerifyObjectOptions, VerifyProofOptions, VerifyRequestDetailedResult, VerifyRequestFailureReason, VerifyRequestOptions, VerifySignatureOptions, attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, fetchKeyDetailed, formatAcceptSignature, fulfillAcceptSignature, generateCryptoKeyPair, getKeyOwner, importJwk, parseAcceptSignature, signJsonLd, signObject, signRequest, validateAcceptSignature, verifyJsonLd, verifyObject, verifyProof, verifyRequest, verifyRequestDetailed, verifySignature };
package/dist/sig/mod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { C as exportJwk, D as importJwk, E as generateCryptoKeyPair, S as KeyCache, T as fetchKeyDetailed, _ as validateAcceptSignature, a as VerifyRequestDetailedResult, b as FetchKeyOptions, c as signRequest, d as AcceptSignatureMember, f as AcceptSignatureParameters, g as parseAcceptSignature, h as fulfillAcceptSignature, i as SignRequestOptions, l as verifyRequest, m as formatAcceptSignature, n as HttpMessageSignaturesSpecDeterminer, o as VerifyRequestFailureReason, p as FulfillAcceptSignatureResult, r as Rfc9421SignRequestOptions, s as VerifyRequestOptions, t as HttpMessageSignaturesSpec, u as verifyRequestDetailed, v as FetchKeyDetailedResult, w as fetchKey, x as FetchKeyResult, y as FetchKeyErrorResult } from "../http-BDZeS5om.js";
3
- import { i as getKeyOwner, n as GetKeyOwnerOptions, r as doesActorOwnKey, t as DoesActorOwnKeyOptions } from "../owner-CnngXDNJ.js";
4
- import { _ as verifyJsonLd, a as createProof, c as verifyProof, d as VerifyJsonLdOptions, f as VerifySignatureOptions, g as signJsonLd, h as detachSignature, i as VerifyProofOptions, l as CreateSignatureOptions, m as createSignature, n as SignObjectOptions, o as signObject, p as attachSignature, r as VerifyObjectOptions, s as verifyObject, t as CreateProofOptions, u as SignJsonLdOptions, v as verifySignature } from "../mod-CjiK2zrW.js";
2
+ import { C as exportJwk, D as importJwk, E as generateCryptoKeyPair, S as KeyCache, T as fetchKeyDetailed, _ as validateAcceptSignature, a as VerifyRequestDetailedResult, b as FetchKeyOptions, c as signRequest, d as AcceptSignatureMember, f as AcceptSignatureParameters, g as parseAcceptSignature, h as fulfillAcceptSignature, i as SignRequestOptions, l as verifyRequest, m as formatAcceptSignature, n as HttpMessageSignaturesSpecDeterminer, o as VerifyRequestFailureReason, p as FulfillAcceptSignatureResult, r as Rfc9421SignRequestOptions, s as VerifyRequestOptions, t as HttpMessageSignaturesSpec, u as verifyRequestDetailed, v as FetchKeyDetailedResult, w as fetchKey, x as FetchKeyResult, y as FetchKeyErrorResult } from "../http-Dl00HXr4.js";
3
+ import { i as getKeyOwner, n as GetKeyOwnerOptions, r as doesActorOwnKey, t as DoesActorOwnKeyOptions } from "../owner-CiBPcC6F.js";
4
+ import { _ as verifyJsonLd, a as createProof, c as verifyProof, d as VerifyJsonLdOptions, f as VerifySignatureOptions, g as signJsonLd, h as detachSignature, i as VerifyProofOptions, l as CreateSignatureOptions, m as createSignature, n as SignObjectOptions, o as signObject, p as attachSignature, r as VerifyObjectOptions, s as verifyObject, t as CreateProofOptions, u as SignJsonLdOptions, v as verifySignature } from "../mod-BLDHhuW0.js";
5
5
  export { AcceptSignatureMember, AcceptSignatureParameters, CreateProofOptions, CreateSignatureOptions, DoesActorOwnKeyOptions, FetchKeyDetailedResult, FetchKeyErrorResult, FetchKeyOptions, FetchKeyResult, FulfillAcceptSignatureResult, GetKeyOwnerOptions, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, Rfc9421SignRequestOptions, SignJsonLdOptions, SignObjectOptions, SignRequestOptions, VerifyJsonLdOptions, VerifyObjectOptions, VerifyProofOptions, VerifyRequestDetailedResult, VerifyRequestFailureReason, VerifyRequestOptions, VerifySignatureOptions, attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, fetchKeyDetailed, formatAcceptSignature, fulfillAcceptSignature, generateCryptoKeyPair, getKeyOwner, importJwk, parseAcceptSignature, signJsonLd, signObject, signRequest, validateAcceptSignature, verifyJsonLd, verifyObject, verifyProof, verifyRequest, verifyRequestDetailed, verifySignature };
package/dist/sig/mod.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { a as verifyRequestDetailed, c as fetchKeyDetailed, f as formatAcceptSignature, h as validateAcceptSignature, i as verifyRequest, l as generateCryptoKeyPair, m as parseAcceptSignature, o as exportJwk, p as fulfillAcceptSignature, r as signRequest, s as fetchKey, u as importJwk } from "../http-88deUx3Y.js";
4
- import { _ as signJsonLd, a as doesActorOwnKey, b as verifySignature, d as createSignature, f as detachSignature, i as verifyProof, l as attachSignature, n as signObject, o as getKeyOwner, r as verifyObject, t as createProof, y as verifyJsonLd } from "../proof-K3inAqnp.js";
3
+ import { _ as validateAcceptSignature, a as verifyRequestDetailed, c as fetchKey, d as importJwk, g as parseAcceptSignature, h as fulfillAcceptSignature, i as verifyRequest, l as fetchKeyDetailed, m as formatAcceptSignature, o as exportJwk, r as signRequest, u as generateCryptoKeyPair } from "../http-CdJ6n4rA.js";
4
+ import { _ as signJsonLd, a as doesActorOwnKey, b as verifySignature, d as createSignature, f as detachSignature, i as verifyProof, l as attachSignature, n as signObject, o as getKeyOwner, r as verifyObject, t as createProof, y as verifyJsonLd } from "../proof-Dsa_ZC-M.js";
5
5
  export { attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, fetchKeyDetailed, formatAcceptSignature, fulfillAcceptSignature, generateCryptoKeyPair, getKeyOwner, importJwk, parseAcceptSignature, signJsonLd, signObject, signRequest, validateAcceptSignature, verifyJsonLd, verifyObject, verifyProof, verifyRequest, verifyRequestDetailed, verifySignature };
@@ -2,11 +2,11 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
5
+ import "../std__assert-BUOJXi-Z.mjs";
6
6
  import { n as assertFalse } from "../assert_rejects-B-qJtC9Z.mjs";
7
- import { t as assert } from "../assert-ddO5KLpe.mjs";
8
- import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
9
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-Cygg-fHW.mjs";
7
+ import { t as assert } from "../assert-BhQd8QHP.mjs";
8
+ import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-CtZLJq76.mjs";
9
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-DklgvZXn.mjs";
10
10
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
11
11
  import { Create, CryptographicKey, lookupObject } from "@fedify/vocab";
12
12
  //#region src/sig/owner.test.ts
@@ -40,7 +40,7 @@ test("doesActorOwnKey() records OpenTelemetry span", async () => {
40
40
  id: new URL("https://example.com/activity"),
41
41
  actor: new URL("https://example.com/person")
42
42
  }), new CryptographicKey({
43
- id: new URL("https://example.com/person#key"),
43
+ id: new URL("https://example.com/key"),
44
44
  owner: new URL("https://example.com/person")
45
45
  }), {
46
46
  documentLoader: mockDocumentLoader,
@@ -50,9 +50,108 @@ test("doesActorOwnKey() records OpenTelemetry span", async () => {
50
50
  assertEquals(spans.length, 1);
51
51
  const span = spans[0];
52
52
  assertEquals(span.attributes["activitypub.actor.id"], "https://example.com/person");
53
- assertEquals(span.attributes["activitypub.key.id"], "https://example.com/person#key");
53
+ assertEquals(span.attributes["activitypub.key.id"], "https://example.com/key");
54
54
  assertEquals(span.attributes["activitypub.key_ownership.verified"], true);
55
- assertEquals(span.attributes["activitypub.key_ownership.method"], "owner_id");
55
+ assertEquals(span.attributes["activitypub.key_ownership.method"], "key_owner");
56
+ });
57
+ test("doesActorOwnKey() rejects a key that only claims to be owned", async () => {
58
+ const options = {
59
+ documentLoader: mockDocumentLoader,
60
+ contextLoader: mockDocumentLoader
61
+ };
62
+ assertFalse(await doesActorOwnKey(new Create({ actor: new URL("https://example.com/person") }), new CryptographicKey({
63
+ id: new URL("https://attacker.example/key"),
64
+ owner: new URL("https://example.com/person")
65
+ }), options));
66
+ const unresolvable = new URL("https://impersonated.invalid/users/victim");
67
+ assertFalse(await doesActorOwnKey(new Create({ actor: unresolvable }), new CryptographicKey({
68
+ id: new URL("https://attacker.example/key"),
69
+ owner: unresolvable
70
+ }), options));
71
+ });
72
+ test("doesActorOwnKey() ignores an actor embedded in the activity", async () => {
73
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
74
+ const activity = await Create.fromJsonLd({
75
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
76
+ id: "https://example.com/activity",
77
+ type: "Create",
78
+ actor: {
79
+ id: "https://example.com/person",
80
+ type: "Person",
81
+ publicKey: {
82
+ id: "https://attacker.example/key",
83
+ type: "CryptographicKey",
84
+ owner: "https://example.com/person",
85
+ publicKeyPem
86
+ }
87
+ }
88
+ }, {
89
+ documentLoader: mockDocumentLoader,
90
+ contextLoader: mockDocumentLoader
91
+ });
92
+ assertEquals(activity.actorId, new URL("https://example.com/person"));
93
+ assertFalse(await doesActorOwnKey(activity, new CryptographicKey({ id: new URL("https://attacker.example/key") }), {
94
+ documentLoader: mockDocumentLoader,
95
+ contextLoader: mockDocumentLoader
96
+ }));
97
+ });
98
+ test("getKeyOwner() rejects an actor document from another origin", async () => {
99
+ const keyId = "https://attacker.example/key";
100
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
101
+ const options = {
102
+ documentLoader(resource) {
103
+ if (resource === keyId) return Promise.resolve({
104
+ contextUrl: null,
105
+ documentUrl: resource,
106
+ document: {
107
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
108
+ id: "https://example.com/person",
109
+ type: "Person",
110
+ publicKey: [{
111
+ id: keyId,
112
+ type: "CryptographicKey",
113
+ publicKeyPem
114
+ }]
115
+ }
116
+ });
117
+ return mockDocumentLoader(resource);
118
+ },
119
+ contextLoader: mockDocumentLoader
120
+ };
121
+ assertEquals(await getKeyOwner(new URL(keyId), options), null);
122
+ });
123
+ test("doesActorOwnKey() matches a key that leaves its id implicit", async () => {
124
+ const actorId = "https://example.com/implicit-key-actor";
125
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
126
+ const options = {
127
+ documentLoader(resource) {
128
+ if (resource === actorId) return Promise.resolve({
129
+ contextUrl: null,
130
+ documentUrl: resource,
131
+ document: {
132
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
133
+ id: actorId,
134
+ type: "Person",
135
+ publicKey: [{
136
+ type: "CryptographicKey",
137
+ owner: actorId,
138
+ publicKeyPem
139
+ }]
140
+ }
141
+ });
142
+ return mockDocumentLoader(resource);
143
+ },
144
+ contextLoader: mockDocumentLoader
145
+ };
146
+ const activity = new Create({ actor: new URL(actorId) });
147
+ assert(await doesActorOwnKey(activity, new CryptographicKey({
148
+ owner: new URL(actorId),
149
+ publicKey: rsaPublicKey1.publicKey
150
+ }), options));
151
+ assertFalse(await doesActorOwnKey(activity, new CryptographicKey({
152
+ owner: new URL(actorId),
153
+ publicKey: rsaPublicKey2.publicKey
154
+ }), options));
56
155
  });
57
156
  //#endregion
58
157
  export {};
@@ -2,11 +2,11 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
5
+ import "../std__assert-BUOJXi-Z.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
7
- import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs";
8
- import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
9
- import { i as verifyProof, n as signObject, r as verifyObject, t as createProof } from "../proof-Dt5GV-xi.mjs";
7
+ import { n as assertInstanceOf } from "../assert-BhQd8QHP.mjs";
8
+ import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
9
+ import { i as verifyProof, n as signObject, r as verifyObject, t as createProof } from "../proof-CI4rsVLu.mjs";
10
10
  import { mockDocumentLoader, test } from "@fedify/fixture";
11
11
  import { Create, DataIntegrityProof, Multikey, Note, Place } from "@fedify/vocab";
12
12
  import { decodeMultibase, importMultibaseKey } from "@fedify/vocab-runtime";
@@ -253,5 +253,74 @@ test("verifyObject()", async () => {
253
253
  assertInstanceOf(note, Note);
254
254
  assertEquals(note.content, "Hello world");
255
255
  });
256
+ test("verifyObject() rejects a key that claims a forged controller", async () => {
257
+ const keyId = new URL("https://attacker.example/multikey");
258
+ const impersonated = "https://example.com/person2";
259
+ const attackerActor = "https://attacker.example/actor";
260
+ const multikeyDocument = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
261
+ const options = {
262
+ format: "compact",
263
+ contextLoader: mockDocumentLoader,
264
+ documentLoader: mockDocumentLoader,
265
+ context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
266
+ };
267
+ const signed = await signObject(new Create({
268
+ id: new URL("https://attacker.example/activities/1"),
269
+ actor: new URL(impersonated),
270
+ object: new Note({
271
+ id: new URL("https://attacker.example/notes/1"),
272
+ attribution: new URL(impersonated),
273
+ content: "Hello world"
274
+ })
275
+ }), ed25519PrivateKey, keyId, options);
276
+ const jsonLd = await signed.toJsonLd(options);
277
+ const serveKeyControlledBy = (controller) => (resource) => {
278
+ if (resource === keyId.href) return Promise.resolve({
279
+ contextUrl: null,
280
+ documentUrl: resource,
281
+ document: {
282
+ ...multikeyDocument,
283
+ id: keyId.href,
284
+ controller
285
+ }
286
+ });
287
+ if (resource === attackerActor) return Promise.resolve({
288
+ contextUrl: null,
289
+ documentUrl: resource,
290
+ document: {
291
+ "@context": [
292
+ "https://www.w3.org/ns/activitystreams",
293
+ "https://w3id.org/security/v1",
294
+ "https://w3id.org/security/multikey/v1",
295
+ "https://w3id.org/security/data-integrity/v1",
296
+ "https://www.w3.org/ns/did/v1"
297
+ ],
298
+ id: resource,
299
+ type: "Person",
300
+ assertionMethod: [keyId.href]
301
+ }
302
+ });
303
+ return mockDocumentLoader(resource);
304
+ };
305
+ assertEquals(await verifyObject(Create, jsonLd, {
306
+ documentLoader: serveKeyControlledBy(impersonated),
307
+ contextLoader: mockDocumentLoader
308
+ }), null);
309
+ const honestLoader = serveKeyControlledBy(attackerActor);
310
+ let proof = null;
311
+ for await (const p of signed.getProofs(options)) {
312
+ proof = p;
313
+ break;
314
+ }
315
+ assertInstanceOf(proof, DataIntegrityProof);
316
+ assertInstanceOf(await verifyProof(jsonLd, proof, {
317
+ documentLoader: honestLoader,
318
+ contextLoader: mockDocumentLoader
319
+ }), Multikey);
320
+ assertEquals(await verifyObject(Create, jsonLd, {
321
+ documentLoader: honestLoader,
322
+ contextLoader: mockDocumentLoader
323
+ }), null);
324
+ });
256
325
  //#endregion
257
326
  export {};