Skip to content

Commit

Permalink
Multiple key pairs can be registered for an actor
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Jun 5, 2024
1 parent 8e23b46 commit 0f19c44
Show file tree
Hide file tree
Showing 23 changed files with 523 additions and 177 deletions.
16 changes: 16 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ To be released.
- Added an optional parameter to `generateCryptoKeyPair()` function,
`algorithm`, which can be either `"RSASSA-PKCS1-v1_5"` or `"Ed25519"`.
- The `importJwk()` function now accepts Ed25519 keys.
- The `exportJwk()` function now exports Ed25519 keys.
- The `importSpki()` function now accepts Ed25519 keys.
- The `exportJwk()` function now exports Ed25519 keys.

- Now multiple key pairs can be registered for an actor.

- Added `Context.getActorKeyPairs()` method.
- Deprecated `Context.getActorKey()` method.
Use `Context.getActorKeyPairs()` method instead.
- Added `ActorKeyPair` interface.
- Added `ActorCallbackSetters.setKeyPairsDispatcher()` method.
- Added `ActorKeyPairsDispatcher` type.
- Deprecated `ActorCallbackSetters.setKeyPairDispatcher()` method.
- Deprecated `ActorKeyPairDispatcher` type.
- Deprecated the third parameter of the `ActorDispatcher` callback type.
Use `Context.getActorKeyPairs()` method instead.

- Deprecated `treatHttps` option in `FederationParameters` interface.
Instead, use the [x-forwarded-fetch] library to recognize the
Expand Down
1 change: 1 addition & 0 deletions cli/import_map.g.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"fast-check": "npm:fast-check@^3.18.0",
"jsonld": "npm:jsonld@^8.3.2",
"mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts",
"multibase": "npm:multibase@^4.0.6",
"uri-template-router": "npm:uri-template-router@^0.0.16",
"url-template": "npm:url-template@^3.1.1",
"@fedify/fedify/sig": ".././sig/mod.ts",
Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"jsonld": "npm:jsonld@^8.3.2",
"mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts",
"multibase": "npm:multibase@^4.0.6",
"pkijs": "npm:pkijs@^3.1.0",
"uri-template-router": "npm:uri-template-router@^0.0.16",
"url-template": "npm:url-template@^3.1.1"
},
Expand Down
82 changes: 40 additions & 42 deletions docs/manual/actor.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,98 +130,96 @@ The `following` property is the URI of the actor's following collection.
You can use the `Context.getFollowingUri()` method to generate the URI of
the actor's following collection.

### `publicKey`
### `publicKeys`

The `publicKey` property is the public key of the actor. It is an instance
of `CryptographicKey` class.
The `publicKeys` property contains the public keys of the actor. It is
an array of `CryptographicKey` instances.

See the [next section](#public-key-of-an-actor) for details.
See the [next section](#public-keys-of-an-actor) for details.


Public key of an `Actor`
------------------------
Public keys of an `Actor`
-------------------------

In order to sign and verify the activities, you need to set the `publicKey`
property of the actor. The `publicKey` property is an instance of the
`CryptographicKey` class, and usually you don't have to create it manually.
Instead, you can register a key pair dispatcher through
the `~ActorCallbackSetters.setKeyPairDispatcher()` method so that Fedify can
dispatch an appropriate key pair by the actor's bare handle:
In order to sign and verify the activities, you need to set the `publicKeys`
property of the actor. The `publicKeys` property contains an array of
`CryptographicKey` instances, and usually you don't have to create it manually.
Instead, you can register a key pairs dispatcher through
the `~ActorCallbackSetters.setKeyPairsDispatcher()` method so that Fedify can
dispatch appropriate key pairs by the actor's bare handle:

~~~~ typescript{7-9,12-17}
federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => {
federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => {
// Work with the database to find the actor by the handle.
if (user == null) return null; // Return null if the actor is not found.
return new Person({
id: ctx.getActorUri(handle),
preferredUsername: handle,
// The third parameter of the actor dispatcher is the public key, if any.
publicKey: key,
// Context.getActorKeyPairs() method dispatches the key pairs of an actor
// by the handle, and returns an array of key pairs in various formats.
// In this example, we only use the CryptographicKey instances.
publicKey: (await ctx.getActorKeyPairs(handle))
.map(keyPair => keyPair.cryptographicKey),
// Many more properties; see the previous section for details.
});
})
.setKeyPairDispatcher(async (ctxData, handle) => {
.setKeyPairsDispatcher(async (ctxData, handle) => {
// Work with the database to find the key pair by the handle.
if (user == null) return null; // Return null if the key pair is not found.
if (user == null) return []; // Return null if the key pair is not found.
// Return the loaded key pair. See the below example for details.
return { publicKey, privateKey };
return [{ publicKey, privateKey }];
});
~~~~

In the above example, the `~ActorCallbackSetters.setKeyPairDispatcher()` method
registers a key pair dispatcher. The key pair dispatcher is a callback function
that takes context data and a bare handle, and returns a [`CryptoKeyPair`]
object which is defined in the Web Cryptography API.
In the above example, the `~ActorCallbackSetters.setKeyPairsDispatcher()` method
registers a key pairs dispatcher. The key pairs dispatcher is a callback
function that takes context data and a bare handle, and returns an array of
[`CryptoKeyPair`] object which is defined in the Web Cryptography API.

Usually, you need to generate a key pair for each actor when the actor is
Usually, you need to generate key pairs for each actor when the actor is
created (i.e., when a new user is signed up), and securely store an actor's key
pair in the database. The key pair dispatcher should load the key pair from
the database and return it.
pairs in the database. The key pairs dispatcher should load the key pairs from
the database and return them.

How to generate a key pair and store it in the database is out of the scope of
How to generate key pairs and store them in the database is out of the scope of
this document, but here's a simple example of how to generate a key pair and
store it in a [Deno KV] database in form of JWK:

~~~~ typescript
import { generateCryptoKeyPair, exportJwk } from "@fedify/fedify";

const kv = await Deno.openKv();
const { privateKey, publicKey } = await generateCryptoKeyPair();
const { privateKey, publicKey } =
await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
await kv.set(["keypair", handle], {
privateKey: await exportJwk(privateKey),
publicKey: await exportJwk(publicKey),
});
~~~~

> [!NOTE]
> As of March 2024, Fedify only supports RSA-PKCS#1-v1.5 algorithm with SHA-256
> hash function for signing and verifying the activities. This limitation
> is due to the fact that Mastodon, the most popular ActivityPub implementation,
> [only supports it][1]. In the future, Fedify will support more algorithms
> and hash functions.
Here's an example of how to load a key pair from the database too:

~~~~ typescript{8-16}
import { importJwk } from "@fedify/fedify";
federation
.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => {
.setActorDispatcher("/users/{handle}", async (ctx, handle) => {
// Omitted for brevity; see the previous example for details.
})
.setKeyPairDispatcher(async (ctxData, handle) => {
.setKeyPairsDispatcher(async (ctxData, handle) => {
const kv = await Deno.openKv();
const entry = await kv.get<{ privateKey: JsonWebKey; publicKey: JsonWebKey }>(
["keypair", handle],
);
if (entry == null || entry.value == null) return null;
return {
privateKey: await importJwk(entry.value.privateKey, "private"),
publicKey: await importJwk(entry.value.publicKey, "public"),
};
if (entry == null || entry.value == null) return [];
return [
{
privateKey: await importJwk(entry.value.privateKey, "private"),
publicKey: await importJwk(entry.value.publicKey, "public"),
}
];
});
~~~~

[`CryptoKeyPair`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
[Deno KV]: https://deno.com/kv
[1]: https://github.com/mastodon/mastodon/issues/21429
8 changes: 4 additions & 4 deletions docs/manual/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => {

On the other way around, you can use the `~Context.parseUri()` method to
determine the type of the URI and extract the handle or other values from
the URI.
the URI.


Enqueuing an outgoing activity
Expand Down Expand Up @@ -123,8 +123,8 @@ For more information about this topic, see the [*Sending activities*
section](./send.md).

> [!NOTE]
> The `~Context.sendActivity()` method works only if the [key pair dispatcher]
> is registered to the `Federation` object. If the key pair dispatcher is not
> The `~Context.sendActivity()` method works only if the [key pairs dispatcher]
> is registered to the `Federation` object. If the key pairs dispatcher is not
> registered, the `~Context.sendActivity()` method throws an error.
> [!TIP]
Expand All @@ -138,7 +138,7 @@ section](./send.md).
> Fedify handles the delivery failure by enqueuing the outgoing
> activity to the actor's outbox and retrying the delivery on failure.
[key pair dispatcher]: ./actor.md#public-key-of-an-actor
[key pairs dispatcher]: ./actor.md#public-keys-of-an-actor


Dispatching objects
Expand Down
14 changes: 7 additions & 7 deletions docs/manual/send.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ an abstracted way to send activities to other actors' inboxes.
[1]: https://www.w3.org/TR/activitypub/#delivery


Prerequisite: actor key pair
----------------------------
Prerequisite: actor key pairs
-----------------------------

Before sending an activity to another actor, you need to have the sender's
key pair. The key pair is used to sign the activity so that the recipient can
verify the sender's identity. The key pair can be registered by calling
`~ActorCallbackSetters.setKeyPairDispatcher()` method.
key pairs. The key pairs are used to sign the activity so that the recipient
can verify the sender's identity. The key pairs can be registered by calling
`~ActorCallbackSetters.setKeyPairsDispatcher()` method.

For more information about this topic, see [*Public key of an `Actor`*
section](./actor.md#public-key-of-an-actor) in the *Actor dispatcher* section.
For more information about this topic, see [*Public keys of an `Actor`*
section](./actor.md#public-keys-of-an-actor) in the *Actor dispatcher* section.


Sending an activity
Expand Down
73 changes: 41 additions & 32 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,19 +740,19 @@ a key pair when the actor is created. In our case, we generate a key pair when
the actor *me* is dispatched for the first time. Then, we store the key pair
in the key-value store so that the server can use the key pair later.
The `~ActorCallbackSetters.setKeyPairDispatcher()` method is used to set a key
pair dispatcher for the actor. The key pair dispatcher is a function that is
called when the key pair of an actor is needed. Let's set a key pair dispatcher
for the actor *me*. `~ActorCallbackSetters.setKeyPairDispatcher()` method
should be chained after the `Federation.setActorDispatcher()` method:
The `~ActorCallbackSetters.setKeyPairsDispatcher()` method is used to set a key
pairs dispatcher for the actor. The key pairs dispatcher is a function that is
called when the key pairs of an actor is needed. Let's set a key pairs
dispatcher for the actor *me*. `~ActorCallbackSetters.setKeyPairsDispatcher()`
method should be chained after the `Federation.setActorDispatcher()` method:
::: code-group
~~~~ typescript{13-14,17-37} [Deno]
const kv = await Deno.openKv(); // Open the key-value store
federation
.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => {
.setActorDispatcher("/users/{handle}", async (ctx, handle) => {
if (handle !== "me") return null;
return new Person({
id: ctx.getActorUri(handle),
Expand All @@ -761,16 +761,19 @@ federation
preferredUsername: handle,
url: new URL("/", ctx.url),
inbox: ctx.getInboxUri(handle),
publicKey: key, // The public key of the actor; it's provided by the key
// pair dispatcher we define below
// The public keys of the actor; they are provided by the key pairs
// dispatcher we define below:
publicKeys: (await ctx.getActorKeyPairs(handle))
.map(keyPair => keyPair.cryptographicKey),
});
})
.setKeyPairDispatcher(async (ctx, handle) => {
if (handle != "me") return null; // Other than "me" is not found.
.setKeyPairsDispatcher(async (ctx, handle) => {
if (handle != "me") return []; // Other than "me" is not found.
const entry = await kv.get<{ privateKey: unknown, publicKey: unknown }>(["key"]);
if (entry == null || entry.value == null) {
// Generate a new key pair at the first time:
const { privateKey, publicKey } = await generateCryptoKeyPair();
const { privateKey, publicKey } =
await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
// Store the generated key pair to the Deno KV database in JWK format:
await kv.set(
["key"],
Expand All @@ -779,12 +782,12 @@ federation
publicKey: await exportJwk(publicKey),
}
);
return { privateKey, publicKey };
return [{ privateKey, publicKey }];
}
// Load the key pair from the Deno KV database:
const privateKey = await importJwk(entry.value.privateKey, "private");
const publicKey = await importJwk(entry.value.publicKey, "public");
return { privateKey, publicKey };
return [{ privateKey, publicKey }];
});
~~~~
Expand All @@ -803,16 +806,19 @@ federation
preferredUsername: handle,
url: new URL("/", ctx.url),
inbox: ctx.getInboxUri(handle),
publicKey: key, // The public key of the actor; it's provided by the key
// pair dispatcher we define below
// The public keys of the actor; they are provided by the key pairs
// dispatcher we define below:
publicKeys: (await ctx.getActorKeyPairs(handle))
.map(keyPair => keyPair.cryptographicKey),
});
})
.setKeyPairDispatcher(async (ctx, handle) => {
if (handle != "me") return null; // Other than "me" is not found.
.setKeyPairsDispatcher(async (ctx, handle) => {
if (handle != "me") return []; // Other than "me" is not found.
const entry = await kv.get<{ privateKey: unknown, publicKey: unknown }>(["key"]);
if (entry == null || entry.value == null) {
// Generate a new key pair at the first time:
const { privateKey, publicKey } = await generateCryptoKeyPair();
const { privateKey, publicKey } =
await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
// Store the generated key pair to the Deno KV database in JWK format:
await kv.set(
["key"],
Expand All @@ -821,12 +827,12 @@ federation
publicKey: await exportJwk(publicKey),
}
);
return { privateKey, publicKey };
return [{ privateKey, publicKey }];
}
// Load the key pair from the Deno KV database:
const privateKey = await importJwk(entry.value.privateKey, "private");
const publicKey = await importJwk(entry.value.publicKey, "public");
return { privateKey, publicKey };
return [{ privateKey, publicKey }];
});
~~~~
Expand All @@ -847,16 +853,19 @@ federation
preferredUsername: handle,
url: new URL("/", ctx.url),
inbox: ctx.getInboxUri(handle),
publicKey: key, // The public key of the actor; it's provided by the key
// pair dispatcher we define below
// The public keys of the actor; they are provided by the key pairs
// dispatcher we define below:
publicKeys: (await ctx.getActorKeyPairs(handle))
.map(keyPair => keyPair.cryptographicKey),
});
})
.setKeyPairDispatcher(async (ctx, handle) => {
if (handle != "me") return null; // Other than "me" is not found.
.setKeyPairsDispatcher(async (ctx, handle) => {
if (handle != "me") return []; // Other than "me" is not found.
const entry = await kv.get<{ privateKey: unknown, publicKey: unknown }>(["key"]);
if (entry == null || entry.value == null) {
// Generate a new key pair at the first time:
const { privateKey, publicKey } = await generateCryptoKeyPair();
const { privateKey, publicKey } =
await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
// Store the generated key pair to the Deno KV database in JWK format:
await kv.set(
["key"],
Expand All @@ -865,24 +874,24 @@ federation
publicKey: await exportJwk(publicKey),
}
);
return { privateKey, publicKey };
return [{ privateKey, publicKey }];
}
// Load the key pair from the Deno KV database:
const privateKey = await importJwk(entry.value.privateKey, "private");
const publicKey = await importJwk(entry.value.publicKey, "public");
return { privateKey, publicKey };
return [{ privateKey, publicKey }];
});
~~~~
:::
In the above code, we use the `~ActorCallbackSetters.setKeyPairDispatcher()`
method to set a key pair dispatcher for the actor *me*. The key pair dispatcher
is a function that is called when the key pair of an actor is needed.
The key pair dispatcher should return an object that contains the private key
In the above code, we use the `~ActorCallbackSetters.setKeyPairsDispatcher()`
method to set a key pairs dispatcher for the actor *me*. The key pairs
dispatcher is called when the key pairs of an actor is needed. The key pairs
dispatcher should return an array of objects that contain the private key
and the public key of the actor. In this case, we generate a new key pair
at the first time and store it in the key-value store. When the actor *me* is
dispatched again, the key pair dispatcher loads the key pair from the key-value
dispatched again, the key pairs dispatcher loads the key pair from the key-value
store.
> [!NOTE]
Expand Down
Loading

0 comments on commit 0f19c44

Please sign in to comment.