@fedify/fedify 2.0.28-dev.1979 → 2.0.28-dev.1982

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 (96) hide show
  1. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert-BhQd8QHP.mjs} +19 -1
  2. package/dist/{builder-CAAXTNeK.mjs → builder-DG5rS_Zv.mjs} +3 -3
  3. package/dist/{client-HLLGHiTU.mjs → client-glFHmpn4.mjs} +78 -4
  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-DJ5oMnte.d.ts → context-B21klrfz.d.ts} +2 -2
  8. package/dist/{context-zLLwVY_G.d.cts → context-M_MxoQgC.d.cts} +2 -2
  9. package/dist/{deno-D3gZ1DsB.mjs → deno-eqaJj6JL.mjs} +1 -1
  10. package/dist/{docloader-DIk3fEXZ.mjs → docloader-BXUqhXNd.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +2 -2
  12. package/dist/federation/collection.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +125 -8
  14. package/dist/federation/idempotency.test.mjs +4 -4
  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 +1 -1
  18. package/dist/federation/middleware.test.mjs +66 -10
  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 +1 -1
  24. package/dist/federation/negotiation.test.mjs +2 -2
  25. package/dist/federation/retry.test.mjs +1 -1
  26. package/dist/federation/router.test.mjs +1 -1
  27. package/dist/federation/send.test.mjs +5 -6
  28. package/dist/federation/temporal.test.mjs +2 -2
  29. package/dist/federation/webfinger.test.mjs +2 -2
  30. package/dist/{http-BLqv_3ue.cjs → http-BgOlx4n1.cjs} +163 -3
  31. package/dist/{http-C4D0ajx0.d.ts → http-Bs6thuCC.d.cts} +2 -2
  32. package/dist/{http-Bn3-am6C.mjs → http-Bu3NxfGb.mjs} +2 -2
  33. package/dist/{http-RYvZHWMc.d.cts → http-CDupyfOc.d.ts} +2 -2
  34. package/dist/{http-Cx5dD1D_.js → http-CwcG2wDs.js} +152 -4
  35. package/dist/{inbox-DC8NbYIj.mjs → inbox-Hou2ZAjv.mjs} +1 -1
  36. package/dist/{key-BQUnVMwc.mjs → key-bSphUqhB.mjs} +152 -4
  37. package/dist/{keycache-DaQ3ndaJ.mjs → keycache-D239uGT9.mjs} +12 -4
  38. package/dist/{kv-cache-CCEitzFw.cjs → kv-cache-B8GaLaNy.cjs} +1 -1
  39. package/dist/{kv-cache-DI4m-4xy.js → kv-cache-lbswb9pg.js} +1 -1
  40. package/dist/{ld-CVbwQ8Fz.mjs → ld-D0uFMLmq.mjs} +2 -2
  41. package/dist/{middleware-C7Afa66S.cjs → middleware-BKpP3cnZ.cjs} +1 -1
  42. package/dist/{middleware-D3a1JsO1.mjs → middleware-BuVM1K-J.mjs} +27 -19
  43. package/dist/{middleware-BqkNe0c6.mjs → middleware-DXPBIbm6.mjs} +1 -1
  44. package/dist/{middleware-Fcpx7tz6.cjs → middleware-D_7wZH27.cjs} +26 -10
  45. package/dist/{middleware-2Qp34eTb.js → middleware-DomxFoLd.js} +25 -9
  46. package/dist/{mod-B2zB0rDg.d.cts → mod-Bt0eg_Lv.d.cts} +1 -1
  47. package/dist/{mod-Du-UVCX_.d.ts → mod-D24V_w-K.d.ts} +1 -1
  48. package/dist/{mod-BtvLkCgb.d.cts → mod-Dp5dfH-i.d.cts} +1 -1
  49. package/dist/{mod-CiQNNAQL.d.ts → mod-E7YM72JZ.d.ts} +1 -1
  50. package/dist/{mod-CB6pc2pw.d.cts → mod-IOWUF5Yr.d.cts} +1 -1
  51. package/dist/{mod-CD1youdZ.d.ts → mod-XucBiDNd.d.ts} +1 -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 +28 -1
  57. package/dist/nodeinfo/handler.test.mjs +2 -2
  58. package/dist/nodeinfo/mod.cjs +1 -1
  59. package/dist/nodeinfo/mod.js +1 -1
  60. package/dist/nodeinfo/types.test.mjs +1 -1
  61. package/dist/otel/exporter.test.mjs +1 -1
  62. package/dist/{owner-CnngXDNJ.d.ts → owner-B3hNQCG8.d.cts} +12 -1
  63. package/dist/owner-Bzg6w7eA.mjs +175 -0
  64. package/dist/{owner-DEvZuyOE.d.cts → owner-CiBPcC6F.d.ts} +12 -1
  65. package/dist/{proof-DZWmoF6Z.mjs → proof-CgLA51pO.mjs} +2 -2
  66. package/dist/{proof-EY-uItWr.js → proof-DsJHFTkx.js} +112 -58
  67. package/dist/{proof-liMyPvsb.cjs → proof-xCEe8t7U.cjs} +112 -58
  68. package/dist/{send-IMhmEkun.mjs → send-D89-FDX-.mjs} +2 -2
  69. package/dist/sig/http.test.mjs +4 -4
  70. package/dist/sig/key.test.mjs +169 -2
  71. package/dist/sig/ld.test.mjs +56 -4
  72. package/dist/sig/mod.cjs +2 -2
  73. package/dist/sig/mod.d.cts +3 -3
  74. package/dist/sig/mod.d.ts +3 -3
  75. package/dist/sig/mod.js +2 -2
  76. package/dist/sig/owner.test.mjs +105 -6
  77. package/dist/sig/proof.test.mjs +72 -3
  78. package/dist/{temporal-BovRWwqL.mjs → temporal-DPJlkHh3.mjs} +1 -1
  79. package/dist/{types-m8u9L86X.cjs → types-DWs-qZaY.cjs} +94 -2
  80. package/dist/{types-DTONwNqL.js → types-sXITXcWa.js} +78 -4
  81. package/dist/utils/docloader.test.mjs +28 -4
  82. package/dist/utils/kv-cache.test.mjs +1 -1
  83. package/dist/utils/mod.cjs +1 -1
  84. package/dist/utils/mod.d.cts +1 -1
  85. package/dist/utils/mod.d.ts +1 -1
  86. package/dist/utils/mod.js +1 -1
  87. package/package.json +5 -5
  88. package/dist/assert-ddO5KLpe.mjs +0 -23
  89. package/dist/owner-D9CjMxWb.mjs +0 -120
  90. /package/dist/{collection-ChgDTHLz.mjs → collection-DI6YzYpd.mjs} +0 -0
  91. /package/dist/{keys-CtZLJq76.mjs → keys-CCxRJhWa.mjs} +0 -0
  92. /package/dist/{kv-cache-DqEKe5E0.mjs → kv-cache-BrjuJue8.mjs} +0 -0
  93. /package/dist/{kv-BrZHNugx.mjs → kv-mI4Ix2EU.mjs} +0 -0
  94. /package/dist/{negotiation-BehA2uul.mjs → negotiation-TWEs6Cl8.mjs} +0 -0
  95. /package/dist/{retry-B_E3V_Dx.mjs → retry-Ddbq3AcK.mjs} +0 -0
  96. /package/dist/{types-C37hquWI.mjs → types-MNlFfxjv.mjs} +0 -0
@@ -4,9 +4,10 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import "../std__assert-Duiq_YC9.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 validateCryptoKey, i as importJwk, n as fetchKey, r as generateCryptoKeyPair, t as exportJwk } from "../key-BQUnVMwc.mjs";
9
- import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
9
+ import { a as importJwk, i as generateCryptoKeyPair, o as validateCryptoKey, r as fetchKey, t as exportJwk } from "../key-bSphUqhB.mjs";
10
+ import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CCxRJhWa.mjs";
10
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
11
12
  import { CryptographicKey, Multikey } from "@fedify/vocab";
12
13
  //#region src/sig/key.test.ts
@@ -331,5 +332,171 @@ test("fetchKey() returns null for a malformed actor publicKey", async () => {
331
332
  cached: true
332
333
  });
333
334
  });
335
+ test("fetchKey() rejects a key whose owner does not link back", async () => {
336
+ const keyId = "https://attacker.example/key";
337
+ const keyDocument = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
338
+ const serveKeyOwnedBy = (owner) => ({
339
+ documentLoader(resource) {
340
+ if (resource === keyId) return Promise.resolve({
341
+ contextUrl: null,
342
+ documentUrl: resource,
343
+ document: {
344
+ ...keyDocument,
345
+ id: keyId,
346
+ owner
347
+ }
348
+ });
349
+ return mockDocumentLoader(resource);
350
+ },
351
+ contextLoader: mockDocumentLoader
352
+ });
353
+ assertEquals(await fetchKey(keyId, CryptographicKey, serveKeyOwnedBy("https://example.com/person")), {
354
+ key: null,
355
+ cached: false
356
+ });
357
+ assertEquals(await fetchKey(keyId, CryptographicKey, serveKeyOwnedBy("https://impersonated.invalid/users/victim")), {
358
+ key: null,
359
+ cached: false
360
+ });
361
+ });
362
+ test("fetchKey() rejects a Multikey whose controller does not link back", async () => {
363
+ const keyId = "https://attacker.example/multikey";
364
+ const keyDocument = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
365
+ assertEquals(await fetchKey(keyId, Multikey, {
366
+ documentLoader(resource) {
367
+ if (resource === keyId) return Promise.resolve({
368
+ contextUrl: null,
369
+ documentUrl: resource,
370
+ document: {
371
+ ...keyDocument,
372
+ id: keyId,
373
+ controller: "https://example.com/person2"
374
+ }
375
+ });
376
+ return mockDocumentLoader(resource);
377
+ },
378
+ contextLoader: mockDocumentLoader
379
+ }), {
380
+ key: null,
381
+ cached: false
382
+ });
383
+ });
384
+ test("fetchKey() rejects an actor document from another origin", async () => {
385
+ const keyId = "https://attacker.example/key";
386
+ const impersonated = "https://example.com/person";
387
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
388
+ assertEquals(await fetchKey(keyId, CryptographicKey, {
389
+ documentLoader(resource) {
390
+ if (resource === keyId) return Promise.resolve({
391
+ contextUrl: null,
392
+ documentUrl: resource,
393
+ document: {
394
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
395
+ id: impersonated,
396
+ type: "Person",
397
+ publicKey: [{
398
+ id: `${impersonated}#main-key`,
399
+ type: "CryptographicKey",
400
+ owner: impersonated,
401
+ publicKeyPem
402
+ }]
403
+ }
404
+ });
405
+ return mockDocumentLoader(resource);
406
+ },
407
+ contextLoader: mockDocumentLoader
408
+ }), {
409
+ key: null,
410
+ cached: false
411
+ });
412
+ });
413
+ test("fetchKey() rejects a key that disowns the actor document holding it", async () => {
414
+ const actorId = "https://attacker.example/actor";
415
+ const keyId = `${actorId}#main-key`;
416
+ const { publicKeyPem } = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
417
+ assertEquals(await fetchKey(keyId, CryptographicKey, {
418
+ documentLoader(resource) {
419
+ if (resource === keyId || resource === actorId) return Promise.resolve({
420
+ contextUrl: null,
421
+ documentUrl: resource,
422
+ document: {
423
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
424
+ id: actorId,
425
+ type: "Person",
426
+ publicKey: [{
427
+ id: keyId,
428
+ type: "CryptographicKey",
429
+ owner: "https://example.com/person",
430
+ publicKeyPem
431
+ }]
432
+ }
433
+ });
434
+ return mockDocumentLoader(resource);
435
+ },
436
+ contextLoader: mockDocumentLoader
437
+ }), {
438
+ key: null,
439
+ cached: false
440
+ });
441
+ });
442
+ test("fetchKey() records the owner its actor document establishes", async () => {
443
+ const { key } = await fetchKey("https://example.com/users/handle#main-key", CryptographicKey, {
444
+ documentLoader: mockDocumentLoader,
445
+ contextLoader: mockDocumentLoader
446
+ });
447
+ assertEquals(key?.ownerId, new URL("https://example.com/users/handle"));
448
+ });
449
+ test("fetchKey() accepts a key document that leaves its id implicit", async () => {
450
+ const keyId = "https://example.com/key";
451
+ const keyDocument = await rsaPublicKey1.toJsonLd({ contextLoader: mockDocumentLoader });
452
+ delete keyDocument.id;
453
+ const { key } = await fetchKey(keyId, CryptographicKey, {
454
+ documentLoader(resource) {
455
+ if (resource === keyId) return Promise.resolve({
456
+ contextUrl: null,
457
+ documentUrl: resource,
458
+ document: keyDocument
459
+ });
460
+ return mockDocumentLoader(resource);
461
+ },
462
+ contextLoader: mockDocumentLoader
463
+ });
464
+ assertEquals(key?.id, new URL(keyId));
465
+ assertEquals(key?.ownerId, new URL("https://example.com/person"));
466
+ assert(key?.publicKey != null);
467
+ });
468
+ test("fetchKey() records the controller an actor document establishes", async () => {
469
+ const actorId = "https://example.com/multikey-actor";
470
+ const keyId = `${actorId}#assertion`;
471
+ const { publicKeyMultibase } = await ed25519Multikey.toJsonLd({ contextLoader: mockDocumentLoader });
472
+ const { key } = await fetchKey(keyId, Multikey, {
473
+ documentLoader(resource) {
474
+ if (resource === actorId || resource === keyId) return Promise.resolve({
475
+ contextUrl: null,
476
+ documentUrl: actorId,
477
+ document: {
478
+ "@context": [
479
+ "https://www.w3.org/ns/activitystreams",
480
+ "https://w3id.org/security/v1",
481
+ "https://w3id.org/security/multikey/v1",
482
+ "https://w3id.org/security/data-integrity/v1",
483
+ "https://www.w3.org/ns/did/v1"
484
+ ],
485
+ id: actorId,
486
+ type: "Person",
487
+ assertionMethod: [{
488
+ id: keyId,
489
+ type: "Multikey",
490
+ publicKeyMultibase
491
+ }]
492
+ }
493
+ });
494
+ return mockDocumentLoader(resource);
495
+ },
496
+ contextLoader: mockDocumentLoader
497
+ });
498
+ assertEquals(key?.controllerId, new URL(actorId));
499
+ assert(key?.publicKey != null);
500
+ });
334
501
  //#endregion
335
502
  export {};
@@ -3,11 +3,11 @@ 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 { r as generateCryptoKeyPair } from "../key-BQUnVMwc.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-CVbwQ8Fz.mjs";
8
+ import { i as generateCryptoKeyPair } from "../key-bSphUqhB.mjs";
9
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CCxRJhWa.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-D0uFMLmq.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { CryptographicKey } from "@fedify/vocab";
13
13
  import { encodeBase64 } from "byte-encodings/base64";
@@ -763,5 +763,57 @@ test("compactJsonLd() rejects inputs that compact into @graph wrappers", async (
763
763
  content: "two"
764
764
  }], mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
765
765
  });
766
+ test("verifyJsonLd() rejects a key that claims a forged owner", async () => {
767
+ const keyId = new URL("https://attacker.example/key");
768
+ const impersonated = "https://example.com/person";
769
+ const { publicKeyPem } = await rsaPublicKey2.toJsonLd({ contextLoader: mockDocumentLoader });
770
+ const signed = await signJsonLd({
771
+ "@context": "https://www.w3.org/ns/activitystreams",
772
+ id: "https://attacker.example/activities/1",
773
+ type: "Create",
774
+ actor: impersonated
775
+ }, rsaPrivateKey2, keyId, { contextLoader: mockDocumentLoader });
776
+ const serveKeyOwnedBy = (owner) => (resource) => {
777
+ if (resource === keyId.href) return Promise.resolve({
778
+ contextUrl: null,
779
+ documentUrl: resource,
780
+ document: {
781
+ "@context": "https://w3id.org/security/v1",
782
+ id: keyId.href,
783
+ type: "CryptographicKey",
784
+ owner,
785
+ publicKeyPem
786
+ }
787
+ });
788
+ if (resource === "https://attacker.example/actor") return Promise.resolve({
789
+ contextUrl: null,
790
+ documentUrl: resource,
791
+ document: {
792
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
793
+ id: resource,
794
+ type: "Person",
795
+ publicKey: [keyId.href]
796
+ }
797
+ });
798
+ return mockDocumentLoader(resource);
799
+ };
800
+ assertEquals(await verifySignature(signed, {
801
+ documentLoader: serveKeyOwnedBy(impersonated),
802
+ contextLoader: mockDocumentLoader
803
+ }), null);
804
+ assertFalse(await verifyJsonLd(signed, {
805
+ documentLoader: serveKeyOwnedBy(impersonated),
806
+ contextLoader: mockDocumentLoader
807
+ }));
808
+ const honestLoader = serveKeyOwnedBy("https://attacker.example/actor");
809
+ assertInstanceOf(await verifySignature(signed, {
810
+ documentLoader: honestLoader,
811
+ contextLoader: mockDocumentLoader
812
+ }), CryptographicKey);
813
+ assertFalse(await verifyJsonLd(signed, {
814
+ documentLoader: honestLoader,
815
+ contextLoader: mockDocumentLoader
816
+ }));
817
+ });
766
818
  //#endregion
767
819
  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-BLqv_3ue.cjs");
5
- const require_proof = require("../proof-liMyPvsb.cjs");
4
+ const require_http = require("../http-BgOlx4n1.cjs");
5
+ const require_proof = require("../proof-xCEe8t7U.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 { a as signRequest, c as FetchKeyResult, d as fetchKey, f as generateCryptoKeyPair, i as VerifyRequestOptions, l as KeyCache, n as HttpMessageSignaturesSpecDeterminer, o as verifyRequest, p as importJwk, r as SignRequestOptions, s as FetchKeyOptions, t as HttpMessageSignaturesSpec, u as exportJwk } from "../http-RYvZHWMc.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-CB6pc2pw.cjs";
2
+ import { a as signRequest, c as FetchKeyResult, d as fetchKey, f as generateCryptoKeyPair, i as VerifyRequestOptions, l as KeyCache, n as HttpMessageSignaturesSpecDeterminer, o as verifyRequest, p as importJwk, r as SignRequestOptions, s as FetchKeyOptions, t as HttpMessageSignaturesSpec, u as exportJwk } from "../http-Bs6thuCC.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-IOWUF5Yr.cjs";
5
5
  export { CreateProofOptions, CreateSignatureOptions, DoesActorOwnKeyOptions, FetchKeyOptions, FetchKeyResult, GetKeyOwnerOptions, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, SignJsonLdOptions, SignObjectOptions, SignRequestOptions, VerifyJsonLdOptions, VerifyObjectOptions, VerifyProofOptions, VerifyRequestOptions, VerifySignatureOptions, attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, generateCryptoKeyPair, getKeyOwner, importJwk, signJsonLd, signObject, signRequest, verifyJsonLd, verifyObject, verifyProof, verifyRequest, verifySignature };
package/dist/sig/mod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { a as signRequest, c as FetchKeyResult, d as fetchKey, f as generateCryptoKeyPair, i as VerifyRequestOptions, l as KeyCache, n as HttpMessageSignaturesSpecDeterminer, o as verifyRequest, p as importJwk, r as SignRequestOptions, s as FetchKeyOptions, t as HttpMessageSignaturesSpec, u as exportJwk } from "../http-C4D0ajx0.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-CiQNNAQL.js";
2
+ import { a as signRequest, c as FetchKeyResult, d as fetchKey, f as generateCryptoKeyPair, i as VerifyRequestOptions, l as KeyCache, n as HttpMessageSignaturesSpecDeterminer, o as verifyRequest, p as importJwk, r as SignRequestOptions, s as FetchKeyOptions, t as HttpMessageSignaturesSpec, u as exportJwk } from "../http-CDupyfOc.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-E7YM72JZ.js";
5
5
  export { CreateProofOptions, CreateSignatureOptions, DoesActorOwnKeyOptions, FetchKeyOptions, FetchKeyResult, GetKeyOwnerOptions, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, SignJsonLdOptions, SignObjectOptions, SignRequestOptions, VerifyJsonLdOptions, VerifyObjectOptions, VerifyProofOptions, VerifyRequestOptions, VerifySignatureOptions, attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, generateCryptoKeyPair, getKeyOwner, importJwk, signJsonLd, signObject, signRequest, verifyJsonLd, verifyObject, verifyProof, verifyRequest, 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 fetchKey, i as exportJwk, n as signRequest, o as generateCryptoKeyPair, r as verifyRequest, s as importJwk } from "../http-Cx5dD1D_.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-EY-uItWr.js";
3
+ import { c as importJwk, i as exportJwk, n as signRequest, o as fetchKey, r as verifyRequest, s as generateCryptoKeyPair } from "../http-CwcG2wDs.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-DsJHFTkx.js";
5
5
  export { attachSignature, createProof, createSignature, detachSignature, doesActorOwnKey, exportJwk, fetchKey, generateCryptoKeyPair, getKeyOwner, importJwk, signJsonLd, signObject, signRequest, verifyJsonLd, verifyObject, verifyProof, verifyRequest, verifySignature };
@@ -4,9 +4,9 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import "../std__assert-Duiq_YC9.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-CtZLJq76.mjs";
9
- import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-D9CjMxWb.mjs";
7
+ import { t as assert } from "../assert-BhQd8QHP.mjs";
8
+ import { o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-CCxRJhWa.mjs";
9
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-Bzg6w7eA.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 {};
@@ -4,9 +4,9 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import "../std__assert-Duiq_YC9.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-CtZLJq76.mjs";
9
- import { i as verifyProof, n as signObject, r as verifyObject, t as createProof } from "../proof-DZWmoF6Z.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-CCxRJhWa.mjs";
9
+ import { i as verifyProof, n as signObject, r as verifyObject, t as createProof } from "../proof-CgLA51pO.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 {};
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as getNormalizationContextLoader } from "./ld-CVbwQ8Fz.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-D0uFMLmq.mjs";
5
5
  import jsonld from "@fedify/vocab-runtime/jsonld";
6
6
  //#region src/federation/temporal.ts
7
7
  function isPlainObject(value) {
@@ -3,6 +3,80 @@ const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
4
  let _logtape_logtape = require("@logtape/logtape");
5
5
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
6
+ //#region src/utils/body.ts
7
+ /** Decoded JSON body limit: 16 MiB. @internal */
8
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
9
+ /** A body exceeded the byte limit. @internal */
10
+ var BodyTooLargeError = class extends _fedify_vocab_runtime.FetchError {
11
+ /** Creates an error for a body exceeding the given limit. */
12
+ constructor(url, maxBytes) {
13
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
14
+ this.name = "BodyTooLargeError";
15
+ }
16
+ };
17
+ /** Validates a finite, positive byte limit. @internal */
18
+ function validateBodySizeLimit(maxBytes) {
19
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
20
+ }
21
+ /**
22
+ * Reads UTF-8 text while limiting bytes received from the body stream.
23
+ * Fetch responses are already decompressed, so the stream count is authoritative.
24
+ * @param message The response or request to read.
25
+ * @param maxBytes The maximum number of decoded bytes to read.
26
+ * @param url The URL to include in errors and logs.
27
+ * @returns The decoded body text.
28
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
29
+ * @internal
30
+ */
31
+ async function readBoundedText(message, maxBytes, url) {
32
+ validateBodySizeLimit(maxBytes);
33
+ const reader = message.body?.getReader();
34
+ const tooLarge = () => {
35
+ (0, _logtape_logtape.getLogger)([
36
+ "fedify",
37
+ "runtime",
38
+ "body"
39
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
40
+ url: url.toString(),
41
+ maxBytes
42
+ });
43
+ throw new BodyTooLargeError(url, maxBytes);
44
+ };
45
+ try {
46
+ const length = message.headers.get("Content-Length");
47
+ const encoding = message.headers.get("Content-Encoding");
48
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
49
+ if (reader == null) return "";
50
+ let size = 0;
51
+ const textReader = new ReadableStream({ async pull(controller) {
52
+ const { done, value } = await reader.read();
53
+ if (done) {
54
+ controller.close();
55
+ return;
56
+ }
57
+ size += value.byteLength;
58
+ if (size > maxBytes) tooLarge();
59
+ controller.enqueue(value);
60
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
61
+ try {
62
+ const chunks = [];
63
+ while (true) {
64
+ const { done, value } = await textReader.read();
65
+ if (done) break;
66
+ chunks.push(value);
67
+ }
68
+ return chunks.join("");
69
+ } finally {
70
+ textReader.releaseLock();
71
+ }
72
+ } catch (error) {
73
+ if (reader != null) reader.cancel(error).catch(() => {});
74
+ throw error;
75
+ } finally {
76
+ reader?.releaseLock();
77
+ }
78
+ }
79
+ //#endregion
6
80
  //#region src/nodeinfo/client.ts
7
81
  const logger = (0, _logtape_logtape.getLogger)([
8
82
  "fedify",
@@ -83,7 +157,7 @@ async function getNodeInfo(url, options = {}) {
83
157
  });
84
158
  return;
85
159
  }
86
- const wellKnownRd = await wellKnownResponse.json();
160
+ const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
87
161
  const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
88
162
  if (link == null || link.href == null) {
89
163
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -107,7 +181,7 @@ async function getNodeInfo(url, options = {}) {
107
181
  });
108
182
  return;
109
183
  }
110
- const data = await response.json();
184
+ const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
111
185
  if (options.parse === "none") return data;
112
186
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
113
187
  } catch (error) {
@@ -353,6 +427,18 @@ function nodeInfoToJson(nodeInfo) {
353
427
  };
354
428
  }
355
429
  //#endregion
430
+ Object.defineProperty(exports, "BodyTooLargeError", {
431
+ enumerable: true,
432
+ get: function() {
433
+ return BodyTooLargeError;
434
+ }
435
+ });
436
+ Object.defineProperty(exports, "MAX_BODY_SIZE", {
437
+ enumerable: true,
438
+ get: function() {
439
+ return MAX_BODY_SIZE;
440
+ }
441
+ });
356
442
  Object.defineProperty(exports, "getNodeInfo", {
357
443
  enumerable: true,
358
444
  get: function() {
@@ -371,3 +457,9 @@ Object.defineProperty(exports, "parseNodeInfo", {
371
457
  return parseNodeInfo;
372
458
  }
373
459
  });
460
+ Object.defineProperty(exports, "readBoundedText", {
461
+ enumerable: true,
462
+ get: function() {
463
+ return readBoundedText;
464
+ }
465
+ });