@fedify/fedify 0.10.0-dev.200 → 0.10.0-dev.203
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|