@fedify/fedify 0.10.0-dev.200 → 0.10.0-dev.203
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.
- package/CHANGES.md +14 -0
- package/esm/federation/middleware.js +45 -21
- package/esm/federation/send.js +54 -4
- package/esm/sig/proof.js +182 -0
- package/esm/vocab/vocab.js +7 -7
- package/esm/webfinger/handler.js +6 -12
- package/package.json +3 -3
- package/types/federation/context.d.ts +5 -10
- package/types/federation/context.d.ts.map +1 -1
- package/types/federation/middleware.d.ts +3 -5
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/federation/mod.d.ts +1 -0
- package/types/federation/mod.d.ts.map +1 -1
- package/types/federation/queue.d.ts +5 -2
- package/types/federation/queue.d.ts.map +1 -1
- package/types/federation/send.d.ts +23 -7
- package/types/federation/send.d.ts.map +1 -1
- package/types/sig/proof.d.ts +101 -0
- package/types/sig/proof.d.ts.map +1 -1
- package/types/vocab/vocab.d.ts.map +1 -1
- package/types/webfinger/handler.d.ts.map +1 -1
package/CHANGES.md
CHANGED
@@ -95,6 +95,11 @@ To be released.
|
|
95
95
|
- Added `VerifyProofOptions` interface.
|
96
96
|
- Added `fetchKey()` function.
|
97
97
|
- Added `FetchKeyOptions` interface.
|
98
|
+
- Added `SenderKeyPair` interface.
|
99
|
+
- The type of `Federation.sendActivity()` method's first parameter became
|
100
|
+
`SenderKeyPair[]` (was `{ keyId: URL; privateKey: CryptoKey }`).
|
101
|
+
- The `Context.sendActivity()` method's first parameter now accepts
|
102
|
+
`SenderKeyPair[]` as well.
|
98
103
|
|
99
104
|
- Added `context` option to `Object.toJsonLd()` method. This applies to
|
100
105
|
any subclasses of the `Object` class too.
|
@@ -123,6 +128,15 @@ To be released.
|
|
123
128
|
[x-forwarded-fetch]: https://github.com/dahlia/x-forwarded-fetch
|
124
129
|
|
125
130
|
|
131
|
+
Version 0.9.1
|
132
|
+
-------------
|
133
|
+
|
134
|
+
Released on June 13, 2024.
|
135
|
+
|
136
|
+
- Fixed a bug of Activity Vocabulary API that `clone()` method of Vocabulary
|
137
|
+
classes had not cloned the `id` property from the source object.
|
138
|
+
|
139
|
+
|
126
140
|
Version 0.9.0
|
127
141
|
-------------
|
128
142
|
|
@@ -98,7 +98,7 @@ export class Federation {
|
|
98
98
|
async #listenQueue(message) {
|
99
99
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
100
100
|
const logData = {
|
101
|
-
|
101
|
+
keyIds: message.keys.map((pair) => pair.keyId),
|
102
102
|
inbox: message.inbox,
|
103
103
|
activity: message.activity,
|
104
104
|
trial: message.trial,
|
@@ -106,16 +106,28 @@ export class Federation {
|
|
106
106
|
};
|
107
107
|
let activity = null;
|
108
108
|
try {
|
109
|
-
const
|
110
|
-
|
111
|
-
const
|
109
|
+
const keys = [];
|
110
|
+
let rsaKeyPair = null;
|
111
|
+
for (const { keyId, privateKey } of message.keys) {
|
112
|
+
const pair = {
|
113
|
+
keyId: new URL(keyId),
|
114
|
+
privateKey: await importJwk(privateKey, "private"),
|
115
|
+
};
|
116
|
+
if (rsaKeyPair == null &&
|
117
|
+
pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
118
|
+
rsaKeyPair = pair;
|
119
|
+
}
|
120
|
+
keys.push(pair);
|
121
|
+
}
|
122
|
+
const documentLoader = rsaKeyPair == null
|
123
|
+
? this.#documentLoader
|
124
|
+
: this.#authenticatedDocumentLoaderFactory(rsaKeyPair);
|
112
125
|
activity = await Activity.fromJsonLd(message.activity, {
|
113
126
|
documentLoader,
|
114
127
|
contextLoader: this.#contextLoader,
|
115
128
|
});
|
116
129
|
await sendActivity({
|
117
|
-
|
118
|
-
privateKey,
|
130
|
+
keys,
|
119
131
|
activity,
|
120
132
|
inbox: new URL(message.inbox),
|
121
133
|
contextLoader: this.#contextLoader,
|
@@ -578,14 +590,20 @@ export class Federation {
|
|
578
590
|
* Sends an activity to recipients' inboxes. You would typically use
|
579
591
|
* {@link Context.sendActivity} instead of this method.
|
580
592
|
*
|
581
|
-
* @param
|
593
|
+
* @param keys The sender's key pairs.
|
582
594
|
* @param recipients The recipients of the activity.
|
583
595
|
* @param activity The activity to send.
|
584
596
|
* @param options Options for sending the activity.
|
585
597
|
* @throws {TypeError} If the activity to send does not have an actor.
|
586
598
|
*/
|
587
|
-
async sendActivity(
|
599
|
+
async sendActivity(keys, recipients, activity, { preferSharedInbox, immediate, excludeBaseUris, collectionSync } = {}) {
|
588
600
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
601
|
+
if (keys.length < 1) {
|
602
|
+
throw new TypeError("The sender's keys must not be empty.");
|
603
|
+
}
|
604
|
+
for (const { privateKey } of keys) {
|
605
|
+
validateCryptoKey(privateKey, "private");
|
606
|
+
}
|
589
607
|
if (activity.actorId == null) {
|
590
608
|
logger.error("Activity {activityId} to send does not have an actor.", { activity, activityId: activity?.id?.href });
|
591
609
|
throw new TypeError("The activity to send must have at least one actor property.");
|
@@ -596,7 +614,6 @@ export class Federation {
|
|
596
614
|
id: new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`),
|
597
615
|
});
|
598
616
|
}
|
599
|
-
validateCryptoKey(privateKey, "private");
|
600
617
|
const inboxes = extractInboxes({
|
601
618
|
recipients: Array.isArray(recipients) ? recipients : [recipients],
|
602
619
|
preferSharedInbox,
|
@@ -618,8 +635,7 @@ export class Federation {
|
|
618
635
|
const promises = [];
|
619
636
|
for (const inbox in inboxes) {
|
620
637
|
promises.push(sendActivity({
|
621
|
-
|
622
|
-
privateKey,
|
638
|
+
keys,
|
623
639
|
activity,
|
624
640
|
inbox: new URL(inbox),
|
625
641
|
contextLoader: this.#contextLoader,
|
@@ -632,15 +648,18 @@ export class Federation {
|
|
632
648
|
return;
|
633
649
|
}
|
634
650
|
logger.debug("Enqueuing activity {activityId} to send later.", { activityId: activity.id?.href, activity });
|
635
|
-
const
|
651
|
+
const keyJwkPairs = [];
|
652
|
+
for (const { keyId, privateKey } of keys) {
|
653
|
+
const privateKeyJwk = await exportJwk(privateKey);
|
654
|
+
keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk });
|
655
|
+
}
|
636
656
|
const activityJson = await activity.toJsonLd({
|
637
657
|
contextLoader: this.#contextLoader,
|
638
658
|
});
|
639
659
|
for (const inbox in inboxes) {
|
640
660
|
const message = {
|
641
661
|
type: "outbox",
|
642
|
-
|
643
|
-
privateKey: privateKeyJwk,
|
662
|
+
keys: keyJwkPairs,
|
644
663
|
activity: activityJson,
|
645
664
|
inbox,
|
646
665
|
trial: 0,
|
@@ -1053,16 +1072,21 @@ class ContextImpl {
|
|
1053
1072
|
return this.#authenticatedDocumentLoaderFactory(identity);
|
1054
1073
|
}
|
1055
1074
|
async sendActivity(sender, recipients, activity, options = {}) {
|
1056
|
-
let
|
1075
|
+
let keys;
|
1057
1076
|
if ("handle" in sender) {
|
1058
|
-
|
1059
|
-
if (
|
1060
|
-
throw new Error(`No key pair found for actor ${sender.handle}
|
1077
|
+
keys = await this.getKeyPairsFromHandle(this.#url, this.data, sender.handle);
|
1078
|
+
if (keys.length < 1) {
|
1079
|
+
throw new Error(`No key pair found for actor ${JSON.stringify(sender.handle)}.`);
|
1080
|
+
}
|
1081
|
+
}
|
1082
|
+
else if (Array.isArray(sender)) {
|
1083
|
+
if (sender.length < 1) {
|
1084
|
+
throw new Error("The sender's key pairs are empty.");
|
1061
1085
|
}
|
1062
|
-
|
1086
|
+
keys = sender;
|
1063
1087
|
}
|
1064
1088
|
else {
|
1065
|
-
|
1089
|
+
keys = [sender];
|
1066
1090
|
}
|
1067
1091
|
const opts = { ...options };
|
1068
1092
|
let expandedRecipients;
|
@@ -1085,7 +1109,7 @@ class ContextImpl {
|
|
1085
1109
|
else {
|
1086
1110
|
expandedRecipients = [recipients];
|
1087
1111
|
}
|
1088
|
-
return await this.#federation.sendActivity(
|
1112
|
+
return await this.#federation.sendActivity(keys, expandedRecipients, activity, opts);
|
1089
1113
|
}
|
1090
1114
|
getFollowers(_handle) {
|
1091
1115
|
throw new Error('"followers" recipients are not supported in Context. ' +
|
package/esm/federation/send.js
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
2
2
|
import { signRequest } from "../sig/http.js";
|
3
|
+
import { validateCryptoKey } from "../sig/key.js";
|
4
|
+
import { signObject } from "../sig/proof.js";
|
3
5
|
/**
|
4
6
|
* Extracts the inbox URLs from recipients.
|
5
7
|
* @param parameters The parameters to extract the inboxes.
|
@@ -30,11 +32,40 @@ export function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris
|
|
30
32
|
* See also {@link SendActivityParameters}.
|
31
33
|
* @throws {Error} If the activity fails to send.
|
32
34
|
*/
|
33
|
-
export async function sendActivity({ activity,
|
35
|
+
export async function sendActivity({ activity, keys, inbox, contextLoader, documentLoader, headers, }) {
|
34
36
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
37
|
+
if (activity.id == null) {
|
38
|
+
throw new TypeError("The activity to send must have an id.");
|
39
|
+
}
|
35
40
|
if (activity.actorId == null) {
|
36
41
|
throw new TypeError("The activity to send must have at least one actor property.");
|
37
42
|
}
|
43
|
+
else if (keys.length < 1) {
|
44
|
+
throw new TypeError("The keys must not be empty.");
|
45
|
+
}
|
46
|
+
const activityId = activity.id.href;
|
47
|
+
let proofCreated = false;
|
48
|
+
for (const { keyId, privateKey } of keys) {
|
49
|
+
validateCryptoKey(privateKey, "private");
|
50
|
+
if (privateKey.algorithm.name === "Ed25519") {
|
51
|
+
activity = await signObject(activity, privateKey, keyId, {
|
52
|
+
documentLoader,
|
53
|
+
contextLoader,
|
54
|
+
});
|
55
|
+
proofCreated = true;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
if (!proofCreated) {
|
59
|
+
logger.warn("No supported key found to create a proof for the activity {activityId}. " +
|
60
|
+
"The activity will be sent without a proof. " +
|
61
|
+
"In order to create a proof, at least one Ed25519 key must be provided.", {
|
62
|
+
activityId,
|
63
|
+
keys: keys.map((pair) => ({
|
64
|
+
keyId: pair.keyId.href,
|
65
|
+
privateKey: pair.privateKey,
|
66
|
+
})),
|
67
|
+
});
|
68
|
+
}
|
38
69
|
const jsonLd = await activity.toJsonLd({ contextLoader });
|
39
70
|
headers = new Headers(headers);
|
40
71
|
headers.set("Content-Type", "application/activity+json");
|
@@ -43,7 +74,26 @@ export async function sendActivity({ activity, privateKey, keyId, inbox, context
|
|
43
74
|
headers,
|
44
75
|
body: JSON.stringify(jsonLd),
|
45
76
|
});
|
46
|
-
|
77
|
+
let requestSigned = false;
|
78
|
+
for (const { privateKey, keyId } of keys) {
|
79
|
+
if (privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
80
|
+
request = await signRequest(request, privateKey, keyId);
|
81
|
+
requestSigned = true;
|
82
|
+
break;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
if (!requestSigned) {
|
86
|
+
logger.warn("No supported key found to sign the request to {inbox}. " +
|
87
|
+
"The request will be sent without a signature. " +
|
88
|
+
"In order to sign the request, at least one RSASSA-PKCS1-v1_5 key " +
|
89
|
+
"must be provided.", {
|
90
|
+
inbox: inbox.href,
|
91
|
+
keys: keys.map((pair) => ({
|
92
|
+
keyId: pair.keyId.href,
|
93
|
+
privateKey: pair.privateKey,
|
94
|
+
})),
|
95
|
+
});
|
96
|
+
}
|
47
97
|
const response = await fetch(request);
|
48
98
|
if (!response.ok) {
|
49
99
|
let error;
|
@@ -55,13 +105,13 @@ export async function sendActivity({ activity, privateKey, keyId, inbox, context
|
|
55
105
|
}
|
56
106
|
logger.error("Failed to send activity {activityId} to {inbox} ({status} " +
|
57
107
|
"{statusText}):\n{error}", {
|
58
|
-
activityId
|
108
|
+
activityId,
|
59
109
|
inbox: inbox.href,
|
60
110
|
status: response.status,
|
61
111
|
statusText: response.statusText,
|
62
112
|
error,
|
63
113
|
});
|
64
|
-
throw new Error(`Failed to send activity ${
|
114
|
+
throw new Error(`Failed to send activity ${activityId} to ${inbox.href} ` +
|
65
115
|
`(${response.status} ${response.statusText}):\n${error}`);
|
66
116
|
}
|
67
117
|
}
|
package/esm/sig/proof.js
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
import * as dntShim from "../_dnt.shims.js";
|
2
|
+
// @ts-ignore: json-canon is not typed
|
3
|
+
import serialize from "json-canon";
|
4
|
+
import { DataIntegrityProof, Object } from "../vocab/vocab.js";
|
5
|
+
import { fetchKey, validateCryptoKey } from "./key.js";
|
6
|
+
import { Activity, Multikey } from "../vocab/mod.js";
|
7
|
+
import { getLogger } from "@logtape/logtape";
|
8
|
+
const logger = getLogger(["fedify", "sig", "proof"]);
|
9
|
+
/**
|
10
|
+
* Creates a proof for the given object.
|
11
|
+
* @param object The object to create a proof for.
|
12
|
+
* @param privateKey The private key to sign the proof with.
|
13
|
+
* @param keyId The key ID to use in the proof. It will be used by the verifier.
|
14
|
+
* @param options Additional options. See also {@link CreateProofOptions}.
|
15
|
+
* @returns The created proof.
|
16
|
+
* @throws {TypeError} If the private key is invalid or unsupported.
|
17
|
+
* @since 0.10.0
|
18
|
+
*/
|
19
|
+
export async function createProof(object, privateKey, keyId, { contextLoader, context, created } = {}) {
|
20
|
+
validateCryptoKey(privateKey, "private");
|
21
|
+
if (privateKey.algorithm.name !== "Ed25519") {
|
22
|
+
throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
|
23
|
+
}
|
24
|
+
const objectWithoutProofs = object.clone({ proofs: [] });
|
25
|
+
const compactMsg = await objectWithoutProofs.toJsonLd({
|
26
|
+
contextLoader,
|
27
|
+
context,
|
28
|
+
});
|
29
|
+
const msgCanon = serialize(compactMsg);
|
30
|
+
const encoder = new TextEncoder();
|
31
|
+
const msgBytes = encoder.encode(msgCanon);
|
32
|
+
const msgDigest = await dntShim.crypto.subtle.digest("SHA-256", msgBytes);
|
33
|
+
created ??= dntShim.Temporal.Now.instant();
|
34
|
+
const proofConfig = {
|
35
|
+
// The below commented out line is needed according to section 3.3.1 of
|
36
|
+
// the Data Integrity EdDSA Cryptosuites v1.0 spec, the FEP-8b32 spec does
|
37
|
+
// not reflect this step; however, the FEP-8b32 spec will be updated to
|
38
|
+
// be consistent with the Data Integrity EdDSA Cryptosuites v1.0 spec
|
39
|
+
// some time soon. Before that happens, the below line is commented out.
|
40
|
+
// See also: https://socialhub.activitypub.rocks/t/fep-8b32-object-integrity-proofs/2725/91?u=hongminhee
|
41
|
+
// "@context": (compactMsg as any)["@context"],
|
42
|
+
type: "DataIntegrityProof",
|
43
|
+
cryptosuite: "eddsa-jcs-2022",
|
44
|
+
verificationMethod: keyId.href,
|
45
|
+
proofPurpose: "assertionMethod",
|
46
|
+
created: created.toString(),
|
47
|
+
};
|
48
|
+
const proofCanon = serialize(proofConfig);
|
49
|
+
const proofBytes = encoder.encode(proofCanon);
|
50
|
+
const proofDigest = await dntShim.crypto.subtle.digest("SHA-256", proofBytes);
|
51
|
+
const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
|
52
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
53
|
+
digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
|
54
|
+
const sig = await dntShim.crypto.subtle.sign("Ed25519", privateKey, digest);
|
55
|
+
return new DataIntegrityProof({
|
56
|
+
cryptosuite: "eddsa-jcs-2022",
|
57
|
+
verificationMethod: keyId,
|
58
|
+
proofPurpose: "assertionMethod",
|
59
|
+
created: created ?? dntShim.Temporal.Now.instant(),
|
60
|
+
proofValue: new Uint8Array(sig),
|
61
|
+
});
|
62
|
+
}
|
63
|
+
/**
|
64
|
+
* Signs the given object with the private key and returns the signed object.
|
65
|
+
* @param object The object to create a proof for.
|
66
|
+
* @param privateKey The private key to sign the proof with.
|
67
|
+
* @param keyId The key ID to use in the proof. It will be used by the verifier.
|
68
|
+
* @param options Additional options. See also {@link SignObjectOptions}.
|
69
|
+
* @returns The signed object.
|
70
|
+
* @throws {TypeError} If the private key is invalid or unsupported.
|
71
|
+
* @since 0.10.0
|
72
|
+
*/
|
73
|
+
export async function signObject(object, privateKey, keyId, options = {}) {
|
74
|
+
const existingProofs = [];
|
75
|
+
for await (const proof of object.getProofs(options)) {
|
76
|
+
existingProofs.push(proof);
|
77
|
+
}
|
78
|
+
const proof = await createProof(object, privateKey, keyId, options);
|
79
|
+
return object.clone({ proofs: [...existingProofs, proof] });
|
80
|
+
}
|
81
|
+
/**
|
82
|
+
* Verifies the given proof for the object.
|
83
|
+
* @param jsonLd The JSON-LD object to verify the proof for. If it contains
|
84
|
+
* any proofs, they will be ignored.
|
85
|
+
* @param proof The proof to verify.
|
86
|
+
* @param options Additional options. See also {@link VerifyProofOptions}.
|
87
|
+
* @returns The public key that was used to sign the proof, or `null` if the
|
88
|
+
* proof is invalid.
|
89
|
+
* @since 0.10.0
|
90
|
+
*/
|
91
|
+
export async function verifyProof(jsonLd, proof, options = {}) {
|
92
|
+
if (typeof jsonLd !== "object" ||
|
93
|
+
proof.cryptosuite !== "eddsa-jcs-2022" ||
|
94
|
+
proof.verificationMethodId == null ||
|
95
|
+
proof.proofPurpose !== "assertionMethod" ||
|
96
|
+
proof.proofValue == null ||
|
97
|
+
proof.created == null)
|
98
|
+
return null;
|
99
|
+
const publicKeyPromise = fetchKey(proof.verificationMethodId, Multikey, options);
|
100
|
+
const proofConfig = {
|
101
|
+
// The below commented out line is needed according to section 3.3.1 of
|
102
|
+
// the Data Integrity EdDSA Cryptosuites v1.0 spec, the FEP-8b32 spec does
|
103
|
+
// not reflect this step; however, the FEP-8b32 spec will be updated to
|
104
|
+
// be consistent with the Data Integrity EdDSA Cryptosuites v1.0 spec
|
105
|
+
// some time soon. Before that happens, the below line is commented out.
|
106
|
+
// See also: https://socialhub.activitypub.rocks/t/fep-8b32-object-integrity-proofs/2725/91?u=hongminhee
|
107
|
+
// "@context": (jsonLd as any)["@context"],
|
108
|
+
type: "DataIntegrityProof",
|
109
|
+
cryptosuite: proof.cryptosuite,
|
110
|
+
verificationMethod: proof.verificationMethodId.href,
|
111
|
+
proofPurpose: proof.proofPurpose,
|
112
|
+
created: proof.created.toString(),
|
113
|
+
};
|
114
|
+
const proofCanon = serialize(proofConfig);
|
115
|
+
const encoder = new TextEncoder();
|
116
|
+
const proofBytes = encoder.encode(proofCanon);
|
117
|
+
const proofDigest = await dntShim.crypto.subtle.digest("SHA-256", proofBytes);
|
118
|
+
const msg = { ...jsonLd };
|
119
|
+
if ("proof" in msg)
|
120
|
+
delete msg.proof;
|
121
|
+
const msgCanon = serialize(msg);
|
122
|
+
const msgBytes = encoder.encode(msgCanon);
|
123
|
+
const msgDigest = await dntShim.crypto.subtle.digest("SHA-256", msgBytes);
|
124
|
+
const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
|
125
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
126
|
+
digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
|
127
|
+
let publicKey;
|
128
|
+
try {
|
129
|
+
publicKey = await publicKeyPromise;
|
130
|
+
}
|
131
|
+
catch (error) {
|
132
|
+
logger.debug("Failed to get the key (verificationMethod) for the proof:\n{proof}", { proof, error });
|
133
|
+
return null;
|
134
|
+
}
|
135
|
+
if (publicKey == null || publicKey.publicKey.algorithm.name !== "Ed25519") {
|
136
|
+
logger.debug("The key (verificationMethod) for the proof is not a valid Ed25519 " +
|
137
|
+
"key:\n{keyId}", { proof, keyId: proof.verificationMethodId.href });
|
138
|
+
return null;
|
139
|
+
}
|
140
|
+
const verified = await dntShim.crypto.subtle.verify("Ed25519", publicKey.publicKey, proof.proofValue, digest);
|
141
|
+
if (!verified) {
|
142
|
+
logger.debug("The proof's signature is invalid.", { proof });
|
143
|
+
return null;
|
144
|
+
}
|
145
|
+
return publicKey;
|
146
|
+
}
|
147
|
+
/**
|
148
|
+
* Verifies the given object. It will verify all the proofs in the object,
|
149
|
+
* and succeed only if all the proofs are valid and all attributions and
|
150
|
+
* actors are authenticated by the proofs.
|
151
|
+
* @param jsonLd The JSON-LD object to verify. It's assumed that the object
|
152
|
+
* is a compacted JSON-LD representation of an {@link Object}
|
153
|
+
* with `@context`.
|
154
|
+
* @param options Additional options. See also {@link VerifyObjectOptions}.
|
155
|
+
* @returns The object if it's verified, or `null` if it's not.
|
156
|
+
* @throws {TypeError} If the object is invalid or unsupported.
|
157
|
+
* @since 0.10.0
|
158
|
+
*/
|
159
|
+
export async function verifyObject(jsonLd, options = {}) {
|
160
|
+
const logger = getLogger(["fedify", "sig", "proof"]);
|
161
|
+
const object = await Object.fromJsonLd(jsonLd, options);
|
162
|
+
const attributions = new Set(object.attributionIds.map((uri) => uri.href));
|
163
|
+
if (object instanceof Activity) {
|
164
|
+
for (const uri of object.actorIds)
|
165
|
+
attributions.add(uri.href);
|
166
|
+
}
|
167
|
+
for await (const proof of object.getProofs(options)) {
|
168
|
+
const key = await verifyProof(jsonLd, proof, options);
|
169
|
+
if (key === null)
|
170
|
+
return null;
|
171
|
+
if (key.controllerId == null) {
|
172
|
+
logger.debug("Key {keyId} does not have a controller.", { keyId: key.id?.href });
|
173
|
+
continue;
|
174
|
+
}
|
175
|
+
attributions.delete(key.controllerId.href);
|
176
|
+
}
|
177
|
+
if (attributions.size > 0) {
|
178
|
+
logger.debug("Some attributions are not authenticated by the proofs: {attributions}.", { attributions: [...attributions] });
|
179
|
+
return null;
|
180
|
+
}
|
181
|
+
return object;
|
182
|
+
}
|
package/esm/vocab/vocab.js
CHANGED
@@ -288,7 +288,7 @@ export class Object {
|
|
288
288
|
*/
|
289
289
|
clone(values = {}, options = {}) {
|
290
290
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
291
|
-
const clone = new this.constructor({ id: values.id }, options);
|
291
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
292
292
|
clone.#_49BipA5dq9eoH8LX8xdsVumveTca = this.#_49BipA5dq9eoH8LX8xdsVumveTca;
|
293
293
|
if ("attachments" in values && values.attachments != null) {
|
294
294
|
clone.#_49BipA5dq9eoH8LX8xdsVumveTca = values.attachments;
|
@@ -3625,7 +3625,7 @@ export class PropertyValue {
|
|
3625
3625
|
*/
|
3626
3626
|
clone(values = {}, options = {}) {
|
3627
3627
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
3628
|
-
const clone = new this.constructor({ id: values.id }, options);
|
3628
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
3629
3629
|
clone.#_4ZHbBuK7PrsvGgrjM8wgc6KMWjav = this.#_4ZHbBuK7PrsvGgrjM8wgc6KMWjav;
|
3630
3630
|
if ("name" in values && values.name != null) {
|
3631
3631
|
clone.#_4ZHbBuK7PrsvGgrjM8wgc6KMWjav = [values.name];
|
@@ -3867,7 +3867,7 @@ export class DataIntegrityProof {
|
|
3867
3867
|
*/
|
3868
3868
|
clone(values = {}, options = {}) {
|
3869
3869
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
3870
|
-
const clone = new this.constructor({ id: values.id }, options);
|
3870
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
3871
3871
|
clone.#_3RurJsa7tnptyqMFR5hDGcP9pMs5 = this.#_3RurJsa7tnptyqMFR5hDGcP9pMs5;
|
3872
3872
|
if ("cryptosuite" in values && values.cryptosuite != null) {
|
3873
3873
|
clone.#_3RurJsa7tnptyqMFR5hDGcP9pMs5 = [values.cryptosuite];
|
@@ -4235,7 +4235,7 @@ export class CryptographicKey {
|
|
4235
4235
|
*/
|
4236
4236
|
clone(values = {}, options = {}) {
|
4237
4237
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
4238
|
-
const clone = new this.constructor({ id: values.id }, options);
|
4238
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
4239
4239
|
clone.#_5UJq9NDh3ZHgswFwwdVxQvJxdx2 = this.#_5UJq9NDh3ZHgswFwwdVxQvJxdx2;
|
4240
4240
|
if ("owner" in values && values.owner != null) {
|
4241
4241
|
clone.#_5UJq9NDh3ZHgswFwwdVxQvJxdx2 = [values.owner];
|
@@ -4543,7 +4543,7 @@ export class Multikey {
|
|
4543
4543
|
*/
|
4544
4544
|
clone(values = {}, options = {}) {
|
4545
4545
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
4546
|
-
const clone = new this.constructor({ id: values.id }, options);
|
4546
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
4547
4547
|
clone.#_2yr3eUBTP6cNcyaxKzAXWjFsnGzN = this.#_2yr3eUBTP6cNcyaxKzAXWjFsnGzN;
|
4548
4548
|
if ("controller" in values && values.controller != null) {
|
4549
4549
|
clone.#_2yr3eUBTP6cNcyaxKzAXWjFsnGzN = [values.controller];
|
@@ -9103,7 +9103,7 @@ export class Endpoints {
|
|
9103
9103
|
*/
|
9104
9104
|
clone(values = {}, options = {}) {
|
9105
9105
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
9106
|
-
const clone = new this.constructor({ id: values.id }, options);
|
9106
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
9107
9107
|
clone.#_2JCYDbSxEHCCLdBYed33cCETfGyR = this.#_2JCYDbSxEHCCLdBYed33cCETfGyR;
|
9108
9108
|
if ("proxyUrl" in values && values.proxyUrl != null) {
|
9109
9109
|
clone.#_2JCYDbSxEHCCLdBYed33cCETfGyR = [values.proxyUrl];
|
@@ -11120,7 +11120,7 @@ export class Link {
|
|
11120
11120
|
*/
|
11121
11121
|
clone(values = {}, options = {}) {
|
11122
11122
|
// @ts-ignore: this.constructor is not recognized as a constructor, but it is.
|
11123
|
-
const clone = new this.constructor({ id: values.id }, options);
|
11123
|
+
const clone = new this.constructor({ id: values.id ?? this.id }, options);
|
11124
11124
|
clone.#_pVjLsybKQdmkjuU7MHjiVmNnuj7 = this.#_pVjLsybKQdmkjuU7MHjiVmNnuj7;
|
11125
11125
|
if ("href" in values && values.href != null) {
|
11126
11126
|
clone.#_pVjLsybKQdmkjuU7MHjiVmNnuj7 = [values.href];
|
package/esm/webfinger/handler.js
CHANGED
@@ -7,10 +7,8 @@ import { Link as LinkObject } from "../vocab/mod.js";
|
|
7
7
|
* @returns The response to the request.
|
8
8
|
*/
|
9
9
|
export async function handleWebFinger(request, { context, actorDispatcher, onNotFound, }) {
|
10
|
-
if (actorDispatcher == null)
|
11
|
-
|
12
|
-
return response instanceof Promise ? await response : response;
|
13
|
-
}
|
10
|
+
if (actorDispatcher == null)
|
11
|
+
return await onNotFound(request);
|
14
12
|
const resource = context.url.searchParams.get("resource");
|
15
13
|
if (resource == null) {
|
16
14
|
return new Response("Missing resource parameter.", { status: 400 });
|
@@ -30,20 +28,16 @@ export async function handleWebFinger(request, { context, actorDispatcher, onNot
|
|
30
28
|
if (uriParsed?.type != "actor") {
|
31
29
|
const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
|
32
30
|
if (match == null || match[2] != context.url.host) {
|
33
|
-
|
34
|
-
return response instanceof Promise ? await response : response;
|
31
|
+
return await onNotFound(request);
|
35
32
|
}
|
36
33
|
handle = match[1];
|
37
34
|
}
|
38
35
|
else {
|
39
36
|
handle = uriParsed.handle;
|
40
37
|
}
|
41
|
-
const
|
42
|
-
|
43
|
-
|
44
|
-
const response = onNotFound(request);
|
45
|
-
return response instanceof Promise ? await response : response;
|
46
|
-
}
|
38
|
+
const actor = await context.getActor(handle);
|
39
|
+
if (actor == null)
|
40
|
+
return await onNotFound(request);
|
47
41
|
const links = [
|
48
42
|
{
|
49
43
|
rel: "self",
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fedify/fedify",
|
3
|
-
"version": "0.10.0-dev.
|
3
|
+
"version": "0.10.0-dev.203+43162fc2",
|
4
4
|
"description": "An ActivityPub server framework",
|
5
5
|
"keywords": [
|
6
6
|
"ActivityPub",
|
@@ -85,6 +85,7 @@
|
|
85
85
|
"@logtape/logtape": "^0.4.0",
|
86
86
|
"@phensley/language-tag": "^1.8.0",
|
87
87
|
"asn1js": "^3.0.5",
|
88
|
+
"json-canon": "^1.0.1",
|
88
89
|
"jsonld": "^8.3.2",
|
89
90
|
"multibase": "^4.0.6",
|
90
91
|
"multicodec": "^3.2.1",
|
@@ -100,8 +101,7 @@
|
|
100
101
|
"@types/node": "^20.9.0",
|
101
102
|
"picocolors": "^1.0.0",
|
102
103
|
"@cfworker/json-schema": "^1.12.8",
|
103
|
-
"fast-check": "^3.18.0"
|
104
|
-
"json-canon": "^1.0.1"
|
104
|
+
"fast-check": "^3.18.0"
|
105
105
|
},
|
106
106
|
"_generatedBy": "dnt@dev"
|
107
107
|
}
|
@@ -4,6 +4,7 @@ import * as dntShim from "../_dnt.shims.js";
|
|
4
4
|
import type { DocumentLoader } from "../runtime/docloader.js";
|
5
5
|
import type { Actor, Recipient } from "../vocab/actor.js";
|
6
6
|
import type { Activity, CryptographicKey, Multikey, Object } from "../vocab/mod.js";
|
7
|
+
import type { SenderKeyPair } from "./send.js";
|
7
8
|
/**
|
8
9
|
* A context.
|
9
10
|
*/
|
@@ -138,15 +139,12 @@ export interface Context<TContextData> {
|
|
138
139
|
}): DocumentLoader;
|
139
140
|
/**
|
140
141
|
* Sends an activity to recipients' inboxes.
|
141
|
-
* @param sender The sender's handle or the sender's key pair.
|
142
|
+
* @param sender The sender's handle or the sender's key pair(s).
|
142
143
|
* @param recipients The recipients of the activity.
|
143
144
|
* @param activity The activity to send.
|
144
145
|
* @param options Options for sending the activity.
|
145
146
|
*/
|
146
|
-
sendActivity(sender: {
|
147
|
-
keyId: URL;
|
148
|
-
privateKey: dntShim.CryptoKey;
|
149
|
-
} | {
|
147
|
+
sendActivity(sender: SenderKeyPair | SenderKeyPair[] | {
|
150
148
|
handle: string;
|
151
149
|
}, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions): Promise<void>;
|
152
150
|
}
|
@@ -212,15 +210,12 @@ export interface RequestContext<TContextData> extends Context<TContextData> {
|
|
212
210
|
getSignedKeyOwner(): Promise<Actor | null>;
|
213
211
|
/**
|
214
212
|
* Sends an activity to recipients' inboxes.
|
215
|
-
* @param sender The sender's handle or the sender's key pair.
|
213
|
+
* @param sender The sender's handle or the sender's key pair(s).
|
216
214
|
* @param recipients The recipients of the activity.
|
217
215
|
* @param activity The activity to send.
|
218
216
|
* @param options Options for sending the activity.
|
219
217
|
*/
|
220
|
-
sendActivity(sender: {
|
221
|
-
keyId: URL;
|
222
|
-
privateKey: dntShim.CryptoKey;
|
223
|
-
} | {
|
218
|
+
sendActivity(sender: SenderKeyPair | SenderKeyPair[] | {
|
224
219
|
handle: string;
|
225
220
|
}, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions): Promise<void>;
|
226
221
|
/**
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/federation/context.ts"],"names":[],"mappings":";;AAAA,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,MAAM,EACP,MAAM,iBAAiB,CAAC;
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/federation/context.ts"],"names":[],"mappings":"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"}
|
@@ -8,6 +8,7 @@ import type { ActorDispatcher, ActorKeyPairDispatcher, ActorKeyPairsDispatcher,
|
|
8
8
|
import type { Context, RequestContext, SendActivityOptions } from "./context.js";
|
9
9
|
import type { KvKey, KvStore } from "./kv.js";
|
10
10
|
import type { MessageQueue } from "./mq.js";
|
11
|
+
import { type SenderKeyPair } from "./send.js";
|
11
12
|
/**
|
12
13
|
* Parameters for initializing a {@link Federation} instance.
|
13
14
|
*/
|
@@ -346,16 +347,13 @@ export declare class Federation<TContextData> {
|
|
346
347
|
* Sends an activity to recipients' inboxes. You would typically use
|
347
348
|
* {@link Context.sendActivity} instead of this method.
|
348
349
|
*
|
349
|
-
* @param
|
350
|
+
* @param keys The sender's key pairs.
|
350
351
|
* @param recipients The recipients of the activity.
|
351
352
|
* @param activity The activity to send.
|
352
353
|
* @param options Options for sending the activity.
|
353
354
|
* @throws {TypeError} If the activity to send does not have an actor.
|
354
355
|
*/
|
355
|
-
sendActivity({
|
356
|
-
keyId: URL;
|
357
|
-
privateKey: dntShim.CryptoKey;
|
358
|
-
}, recipients: Recipient | Recipient[], activity: Activity, { preferSharedInbox, immediate, excludeBaseUris, collectionSync }?: SendActivityInternalOptions): Promise<void>;
|
356
|
+
sendActivity(keys: SenderKeyPair[], recipients: Recipient | Recipient[], activity: Activity, { preferSharedInbox, immediate, excludeBaseUris, collectionSync }?: SendActivityInternalOptions): Promise<void>;
|
359
357
|
/**
|
360
358
|
* Handles a request related to federation. If a request is not related to
|
361
359
|
* federation, the `onNotFound` or `onNotAcceptable` callback is called.
|