From 5c035c31fe24322228435ddf877ec78ea7275dcd Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Thu, 6 Jun 2024 00:21:23 +0900 Subject: [PATCH] Multiple key pairs can be registered for an actor https://github.com/dahlia/fedify/issues/55 --- CHANGES.md | 16 +++ cli/import_map.g.json | 1 + deno.json | 1 + docs/manual/actor.md | 82 ++++++------ docs/manual/context.md | 8 +- docs/manual/send.md | 14 +- docs/tutorial.md | 73 ++++++----- examples/blog/federation/mod.ts | 25 ++-- examples/blog/import_map.g.json | 2 + examples/blog/models/blog.ts | 36 +++++- federation/callback.ts | 20 +++ federation/context.ts | 29 ++++- federation/handler.ts | 6 +- federation/middleware.test.ts | 44 ++++++- federation/middleware.ts | 183 ++++++++++++++++++++++----- runtime/key.test.ts | 37 +++++- runtime/key.ts | 43 +++++-- sig/key.ts | 2 +- testing/context.ts | 25 +++- testing/fixtures/example.com/key4 | 7 + testing/fixtures/example.com/person2 | 6 + testing/keys.ts | 34 ++--- 22 files changed, 517 insertions(+), 177 deletions(-) create mode 100644 testing/fixtures/example.com/key4 diff --git a/CHANGES.md b/CHANGES.md index 33d567c3..2f6aa1c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/cli/import_map.g.json b/cli/import_map.g.json index 84feef06..c30ecd95 100644 --- a/cli/import_map.g.json +++ b/cli/import_map.g.json @@ -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", diff --git a/deno.json b/deno.json index f254a6ae..dacfcec4 100644 --- a/deno.json +++ b/deno.json @@ -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" }, diff --git a/docs/manual/actor.md b/docs/manual/actor.md index 601bd441..4bc85f8d 100644 --- a/docs/manual/actor.md +++ b/docs/manual/actor.md @@ -130,55 +130,58 @@ 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: @@ -186,42 +189,37 @@ store it in a [Deno KV] database in form of JWK: 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 diff --git a/docs/manual/context.md b/docs/manual/context.md index 43a717cd..4e531bdf 100644 --- a/docs/manual/context.md +++ b/docs/manual/context.md @@ -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 @@ -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] @@ -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 diff --git a/docs/manual/send.md b/docs/manual/send.md index 276fffda..1831f185 100644 --- a/docs/manual/send.md +++ b/docs/manual/send.md @@ -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 diff --git a/docs/tutorial.md b/docs/tutorial.md index 8328e4b0..63019bfa 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -740,11 +740,11 @@ 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 @@ -752,7 +752,7 @@ should be chained after the `Federation.setActorDispatcher()` method: 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), @@ -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"], @@ -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 }]; }); ~~~~ @@ -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"], @@ -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 }]; }); ~~~~ @@ -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"], @@ -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] diff --git a/examples/blog/federation/mod.ts b/examples/blog/federation/mod.ts index 3c4105dc..fd40a991 100644 --- a/examples/blog/federation/mod.ts +++ b/examples/blog/federation/mod.ts @@ -45,7 +45,7 @@ export const federation = new Federation({ // `Actor` object (`Person` in this case) for a given actor URI. // The actor dispatch is not only used for the actor URI, but also for // the WebFinger resource: -federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { +federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; @@ -74,17 +74,24 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { // for the HTTP Signatures. Note that the `key` object is not a // `CryptoKey` instance, but a `CryptographicKey` instance which is // used for ActivityPub: - publicKey: key, + publicKeys: (await ctx.getActorKeyPairs(handle)) + .map((keyPair) => keyPair.cryptographicKey), }); }) - .setKeyPairDispatcher(async (_ctxData, handle) => { + .setKeyPairsDispatcher(async (_ctxData, handle) => { const blog = await getBlog(); - if (blog == null) return null; - else if (blog.handle !== handle) return null; - return { - publicKey: blog.publicKey, - privateKey: blog.privateKey, - }; + if (blog == null) return []; + else if (blog.handle !== handle) return []; + return [ + { + publicKey: blog.publicKey, + privateKey: blog.privateKey, + }, + { + publicKey: blog.ed25519PublicKey, + privateKey: blog.ed25519PrivateKey, + }, + ]; }); // Registers the object dispatcher, which is responsible for creating an diff --git a/examples/blog/import_map.g.json b/examples/blog/import_map.g.json index 9e015ba0..001a8e92 100644 --- a/examples/blog/import_map.g.json +++ b/examples/blog/import_map.g.json @@ -35,6 +35,8 @@ "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", + "pkijs": "npm:pkijs@^3.1.0", "uri-template-router": "npm:uri-template-router@^0.0.16", "url-template": "npm:url-template@^3.1.1", "$fresh/": "https://deno.land/x/fresh@1.6.8/", diff --git a/examples/blog/models/blog.ts b/examples/blog/models/blog.ts index 6c111c82..0bcb9a00 100644 --- a/examples/blog/models/blog.ts +++ b/examples/blog/models/blog.ts @@ -21,12 +21,17 @@ export interface Blog extends BlogBase { passwordHash: string; privateKey: CryptoKey; publicKey: CryptoKey; + ed25519PrivateKey: CryptoKey; + ed25519PublicKey: CryptoKey; published: Temporal.Instant; } export async function setBlog(blog: BlogInput): Promise { const kv = await openKv(); - const { privateKey, publicKey } = await generateCryptoKeyPair(); + const { privateKey, publicKey } = await generateCryptoKeyPair( + "RSASSA-PKCS1-v1_5", + ); + const ed25519KeyPair = await generateCryptoKeyPair("Ed25519"); await kv.set(["blog"], { handle: blog.handle, title: blog.title, @@ -35,6 +40,8 @@ export async function setBlog(blog: BlogInput): Promise { passwordHash: hash(blog.password, undefined, "scrypt"), privateKey: await exportJwk(privateKey), publicKey: await exportJwk(publicKey), + ed25519PrivateKey: await exportJwk(ed25519KeyPair.privateKey), + ed25519PublicKey: await exportJwk(ed25519KeyPair.publicKey), }); } @@ -42,6 +49,8 @@ export interface BlogInternal extends BlogBase { passwordHash: string; privateKey: Record; publicKey: Record; + ed25519PrivateKey?: Record; + ed25519PublicKey?: Record; published: string; } @@ -49,11 +58,28 @@ export async function getBlog(): Promise { const kv = await openKv(); const entry = await kv.get(["blog"]); if (entry == null || entry.value == null) return null; + const { value } = entry; + let ed25519KeyPair: CryptoKeyPair; + if (value.ed25519PrivateKey == null || value.ed25519PublicKey == null) { + ed25519KeyPair = await generateCryptoKeyPair("Ed25519"); + await kv.set(["blog"], { + ...value, + ed25519PrivateKey: await exportJwk(ed25519KeyPair.privateKey), + ed25519PublicKey: await exportJwk(ed25519KeyPair.publicKey), + }); + } else { + ed25519KeyPair = { + privateKey: await importJwk(value.ed25519PrivateKey, "private"), + publicKey: await importJwk(value.ed25519PublicKey, "public"), + }; + } return { - ...entry.value, - privateKey: await importJwk(entry.value.privateKey, "private"), - publicKey: await importJwk(entry.value.publicKey, "public"), - published: Temporal.Instant.from(entry.value.published), + ...value, + privateKey: await importJwk(value.privateKey, "private"), + publicKey: await importJwk(value.publicKey, "public"), + ed25519PrivateKey: ed25519KeyPair.privateKey, + ed25519PublicKey: ed25519KeyPair.publicKey, + published: Temporal.Instant.from(value.published), }; } diff --git a/federation/callback.ts b/federation/callback.ts index 79542d24..38915fb9 100644 --- a/federation/callback.ts +++ b/federation/callback.ts @@ -18,6 +18,11 @@ export type NodeInfoDispatcher = ( * A callback that dispatches an {@link Actor} object. * * @typeParam TContextData The context data to pass to the {@link Context}. + * @param context The request context. + * @param handle The actor's handle. + * @param key The public key of the actor. **Deprecated: Do not rely on this + * parameter. Instead, use the {@link Context.getActorKeyPairs} + * method.** */ export type ActorDispatcher = ( context: RequestContext, @@ -25,10 +30,25 @@ export type ActorDispatcher = ( key: CryptographicKey | null, ) => Actor | null | Promise; +/** + * A callback that dispatches key pairs for an actor. + * + * @typeParam TContextData The context data to pass to the {@link Context}. + * @param contextData The context data. + * @param handle The actor's handle. + * @returns The key pairs. + * @since 0.10.0 + */ +export type ActorKeyPairsDispatcher = ( + contextData: TContextData, + handle: string, +) => CryptoKeyPair[] | Promise; + /** * A callback that dispatches a key pair for an actor. * * @typeParam TContextData The context data to pass to the {@link Context}. + * @deprecated */ export type ActorKeyPairDispatcher = ( contextData: TContextData, diff --git a/federation/context.ts b/federation/context.ts index 8246bb11..571f0a38 100644 --- a/federation/context.ts +++ b/federation/context.ts @@ -102,14 +102,24 @@ export interface Context { * Extracts the actor's handle from an actor URI, if it is a valid actor URI. * @param actorUri The actor's URI. * @returns The actor's handle, or `null` if the URI is not a valid actor URI. - * @deprecated Use {@link parseUri} instead. + * @deprecated Use {@link Context.parseUri} instead. */ getHandleFromActorUri(actorUri: URL): string | null; /** - * Gets a public {@link CryptographicKey} for an actor, if any exists. + * Gets the key pairs for an actor. + * @param handle The actor's handle. + * @returns An async iterable of the actor's key pairs. It can be empty. + * @since 0.10.0 + */ + getActorKeyPairs(handle: string): Promise; + + /** + * Gets a public RSA-PKCS#1-v1.5 {@link CryptographicKey} for an actor, + * if any exists. * @param handle The actor's handle. * @returns The actor's public key, or `null` if the actor has no key. + * @deprecated Use {@link Context.getActorKeyPairs} instead. */ getActorKey(handle: string): Promise; @@ -319,3 +329,18 @@ export interface SendActivityOptions { */ excludeBaseUris?: URL[]; } + +/** + * A pair of a public key and a private key in various formats. + */ +export interface ActorKeyPair extends CryptoKeyPair { + /** + * The URI of the public key, which is used for verifying HTTP Signatures. + */ + keyId: URL; + + /** + * A {@link CryptographicKey} instance of the public key. + */ + cryptographicKey: CryptographicKey; +} diff --git a/federation/handler.ts b/federation/handler.ts index 0f1a41fd..bac2d1d2 100644 --- a/federation/handler.ts +++ b/federation/handler.ts @@ -59,8 +59,7 @@ export async function handleActor( }: ActorHandlerParameters, ): Promise { if (actorDispatcher == null) return await onNotFound(request); - const key = await context.getActorKey(handle); - const actor = await actorDispatcher(context, handle, key); + const actor = await context.getActor(handle); if (actor == null) return await onNotFound(request); if (!acceptsJsonLd(request)) return await onNotAcceptable(request); if (authorizePredicate != null) { @@ -337,8 +336,7 @@ export async function handleInbox( logger.error("Actor dispatcher is not set.", { handle }); return await onNotFound(request); } else if (handle != null) { - const key = await context.getActorKey(handle); - const actor = await actorDispatcher(context, handle, key); + const actor = await context.getActor(handle); if (actor == null) { logger.error("Actor {handle} not found.", { handle }); return await onNotFound(request); diff --git a/federation/middleware.test.ts b/federation/middleware.test.ts index ef460eb6..247d1d85 100644 --- a/federation/middleware.test.ts +++ b/federation/middleware.test.ts @@ -14,6 +14,8 @@ import { } from "../runtime/docloader.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { + ed25519PrivateKey, + ed25519PublicKey, rsaPrivateKey2, rsaPrivateKey3, rsaPublicKey2, @@ -73,16 +75,17 @@ Deno.test("Federation.createContext()", async (t) => { ctx.getHandleFromActorUri(new URL("https://example.com/")), null, ); + assertEquals(await ctx.getActorKeyPairs("handle"), []); assertEquals(await ctx.getActorKey("handle"), null); assertRejects( () => ctx.getDocumentLoader({ handle: "handle" }), Error, - "No actor key pair dispatcher registered", + "No actor key pairs dispatcher registered", ); assertRejects( () => ctx.sendActivity({ handle: "handle" }, [], new Create({})), Error, - "No actor key pair dispatcher registered", + "No actor key pairs dispatcher registered", ); federation.setNodeInfoDispatcher("/nodeinfo/2.1", () => ({ @@ -104,10 +107,16 @@ Deno.test("Federation.createContext()", async (t) => { federation .setActorDispatcher("/users/{handle}", () => new Person({})) - .setKeyPairDispatcher(() => ({ - privateKey: rsaPrivateKey2, - publicKey: rsaPublicKey2.publicKey!, - })); + .setKeyPairsDispatcher(() => [ + { + privateKey: rsaPrivateKey2, + publicKey: rsaPublicKey2.publicKey!, + }, + { + privateKey: ed25519PrivateKey, + publicKey: ed25519PublicKey.publicKey!, + }, + ]); assertEquals( ctx.getActorUri("handle"), new URL("https://example.com/users/handle"), @@ -125,6 +134,29 @@ Deno.test("Federation.createContext()", async (t) => { ctx.getHandleFromActorUri(new URL("https://example.com/users/handle")), "handle", ); + assertEquals( + await ctx.getActorKeyPairs("handle"), + [ + { + keyId: new URL("https://example.com/users/handle#main-key"), + privateKey: rsaPrivateKey2, + publicKey: rsaPublicKey2.publicKey!, + cryptographicKey: rsaPublicKey2.clone({ + id: new URL("https://example.com/users/handle#main-key"), + owner: new URL("https://example.com/users/handle"), + }), + }, + { + keyId: new URL("https://example.com/users/handle#key-2"), + privateKey: ed25519PrivateKey, + publicKey: ed25519PublicKey.publicKey!, + cryptographicKey: ed25519PublicKey.clone({ + id: new URL("https://example.com/users/handle#key-2"), + owner: new URL("https://example.com/users/handle"), + }), + }, + ], + ); assertEquals( await ctx.getActorKey("handle"), rsaPublicKey2.clone({ diff --git a/federation/middleware.ts b/federation/middleware.ts index 2092c87a..2ff78c7e 100644 --- a/federation/middleware.ts +++ b/federation/middleware.ts @@ -16,6 +16,7 @@ import { handleWebFinger } from "../webfinger/handler.ts"; import type { ActorDispatcher, ActorKeyPairDispatcher, + ActorKeyPairsDispatcher, AuthorizePredicate, CollectionCounter, CollectionCursor, @@ -29,6 +30,7 @@ import type { } from "./callback.ts"; import { buildCollectionSynchronizationHeader } from "./collection.ts"; import type { + ActorKeyPair, Context, ParseUriResult, RequestContext, @@ -320,6 +322,43 @@ export class Federation { ); } + async #getKeyPairsFromHandle( + url: URL | string, + contextData: TContextData, + handle: string, + ): Promise<(CryptoKeyPair & { keyId: URL })[]> { + const logger = getLogger(["fedify", "federation", "actor"]); + if (this.#actorCallbacks?.keyPairsDispatcher == null) { + throw new Error("No actor key pairs dispatcher registered."); + } + const path = this.#router.build("actor", { handle }); + if (path == null) { + logger.warn("No actor dispatcher registered."); + return []; + } + const actorUri = new URL(path, url); + const keyPairs = await this.#actorCallbacks?.keyPairsDispatcher( + contextData, + handle, + ); + if (keyPairs.length < 1) { + logger.warn("No key pairs found for actor {handle}.", { handle }); + } + let i = 0; + const result = []; + for (const keyPair of keyPairs) { + result.push({ + ...keyPair, + keyId: new URL( + i == 0 ? `#main-key` : `#key-${i + 1}`, + actorUri, + ), + }); + i++; + } + return result; + } + /** * Create a new context. * @param baseUrl The base URL of the server. The `pathname` remains root, @@ -354,27 +393,31 @@ export class Federation { url.search = ""; } if (this.#treatHttps) url.protocol = "https:"; - const getKeyPairFromHandle = async (handle: string) => { - if (this.#actorCallbacks?.keyPairDispatcher == null) { - throw new Error("No actor key pair dispatcher registered."); - } - let keyPair = this.#actorCallbacks?.keyPairDispatcher( + const getRsaKeyPairFromHandle = async (handle: string) => { + const keyPairs = await this.#getKeyPairsFromHandle( + url, contextData, handle, ); - if (keyPair instanceof Promise) keyPair = await keyPair; - if (keyPair == null) { - throw new Error( - `No key pair found for actor ${JSON.stringify(handle)}`, - ); + for (const keyPair of keyPairs) { + const { privateKey } = keyPair; + if ( + privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && + (privateKey.algorithm as RsaHashedKeyAlgorithm).hash.name === + "SHA-256" + ) { + return keyPair; + } } - return { - keyId: new URL(`${context.getActorUri(handle)}#main-key`), - privateKey: keyPair.privateKey, - }; + getLogger(["fedify", "federation", "actor"]).warn( + "No RSA-PKCS#1-v1.5 SHA-256 key found for actor {handle}.", + { handle }, + ); + return null; }; const getAuthenticatedDocumentLoader = this.#authenticatedDocumentLoaderFactory; + const documentLoader = this.#documentLoader; function getDocumentLoader( identity: { handle: string }, ): Promise; @@ -385,8 +428,11 @@ export class Federation { identity: { keyId: URL; privateKey: CryptoKey } | { handle: string }, ): DocumentLoader | Promise { if ("handle" in identity) { - const keyPair = getKeyPairFromHandle(identity.handle); - return keyPair.then((pair) => getAuthenticatedDocumentLoader(pair)); + const keyPair = getRsaKeyPairFromHandle(identity.handle); + if (keyPair == null) return documentLoader; + return keyPair.then((pair) => + pair == null ? documentLoader : getAuthenticatedDocumentLoader(pair) + ); } return getAuthenticatedDocumentLoader(identity); } @@ -499,15 +545,48 @@ export class Federation { if (result?.type === "actor") return result.handle; return null; }, + getActorKeyPairs: async (handle: string) => { + let keyPairs: (CryptoKeyPair & { keyId: URL })[]; + try { + keyPairs = await this.#getKeyPairsFromHandle( + url, + contextData, + handle, + ); + } catch (_) { + getLogger(["fedify", "federation", "actor"]) + .warn("No actor key pairs dispatcher registered."); + return []; + } + const owner = context.getActorUri(handle); + const result = []; + for (const keyPair of keyPairs) { + const newPair: ActorKeyPair = { + ...keyPair, + cryptographicKey: new CryptographicKey({ + id: keyPair.keyId, + owner, + publicKey: keyPair.publicKey, + }), + }; + result.push(newPair); + } + return result; + }, getActorKey: async (handle: string): Promise => { - let keyPair = this.#actorCallbacks?.keyPairDispatcher?.( - contextData, - handle, + getLogger(["fedify", "federation", "actor"]).warn( + "Context.getActorKey() method is deprecated; " + + "use Context.getActorKeyPairs() method instead.", ); - if (keyPair instanceof Promise) keyPair = await keyPair; + let keyPair: CryptoKeyPair & { keyId: URL } | null; + try { + keyPair = await getRsaKeyPairFromHandle(handle); + } catch (_) { + return null; + } if (keyPair == null) return null; return new CryptographicKey({ - id: new URL(`${context.getActorUri(handle)}#main-key`), + id: keyPair.keyId, owner: context.getActorUri(handle), publicKey: keyPair.publicKey, }); @@ -519,10 +598,16 @@ export class Federation { activity: Activity, options: SendActivityOptions = {}, ): Promise => { - const senderPair: { keyId: URL; privateKey: CryptoKey } = - "handle" in sender - ? await getKeyPairFromHandle(sender.handle) - : sender; + let senderPair: { keyId: URL; privateKey: CryptoKey }; + if ("handle" in sender) { + const keyPair = await getRsaKeyPairFromHandle(sender.handle); + if (keyPair == null) { + throw new Error(`No key pair found for actor ${sender.handle}`); + } + senderPair = keyPair; + } else { + senderPair = sender; + } const opts: SendActivityInternalOptions = { ...options }; let expandedRecipients: Recipient[]; if (Array.isArray(recipients)) { @@ -569,6 +654,12 @@ export class Federation { ) { throw new Error("No actor dispatcher registered."); } + let rsaKey: CryptoKeyPair & { keyId: URL } | null; + try { + rsaKey = await getRsaKeyPairFromHandle(handle); + } catch (_) { + rsaKey = null; + } return await this.#actorCallbacks.dispatcher( { ...reqCtx, @@ -583,7 +674,11 @@ export class Federation { }, }, handle, - await context.getActorKey(handle), + rsaKey == null ? null : new CryptographicKey({ + id: rsaKey.keyId, + owner: context.getActorUri(handle), + publicKey: rsaKey.publicKey, + }), ); }, getObject: async (cls, values) => { @@ -669,7 +764,7 @@ export class Federation { * ``` typescript * federation.setActorDispatcher( * "/users/{handle}", - * async (ctx, handle, key) => { + * async (ctx, handle) => { * return new Person({ * id: ctx.getActorUri(handle), * preferredUsername: handle, @@ -800,8 +895,21 @@ export class Federation { }; this.#actorCallbacks = callbacks; const setters: ActorCallbackSetters = { + setKeyPairsDispatcher(dispatcher: ActorKeyPairsDispatcher) { + callbacks.keyPairsDispatcher = dispatcher; + return setters; + }, setKeyPairDispatcher(dispatcher: ActorKeyPairDispatcher) { - callbacks.keyPairDispatcher = dispatcher; + getLogger(["fedify", "federation", "actor"]).warn( + "The ActorCallbackSetters.setKeyPairDispatcher() method is " + + "deprecated. Use the ActorCallbackSetters.setKeyPairsDispatcher() " + + "instead.", + ); + callbacks.keyPairsDispatcher = async (ctxData, handle) => { + const key = await dispatcher(ctxData, handle); + if (key == null) return []; + return [key]; + }; return setters; }, authorize(predicate: AuthorizePredicate) { @@ -1575,7 +1683,7 @@ export interface FederationFetchOptions { interface ActorCallbacks { dispatcher?: ActorDispatcher; - keyPairDispatcher?: ActorKeyPairDispatcher; + keyPairsDispatcher?: ActorKeyPairsDispatcher; authorizePredicate?: AuthorizePredicate; } @@ -1587,16 +1695,29 @@ interface ActorCallbacks { * federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { * ... * }) - * .setKeyPairDispatcher(async (ctxData, handle) => { + * .setKeyPairsDispatcher(async (ctxData, handle) => { * ... * }); * ``` */ export interface ActorCallbackSetters { /** - * Sets the key pair dispatcher for actors. + * Sets the key pairs dispatcher for actors. + * @param dispatcher A callback that returns the key pairs for an actor. + * @returns The setters object so that settings can be chained. + * @since 0.10.0 + */ + setKeyPairsDispatcher( + dispatcher: ActorKeyPairsDispatcher, + ): ActorCallbackSetters; + + /** + * Sets the RSA-PKCS#1-v1.5 key pair dispatcher for actors. + * + * Use {@link ActorCallbackSetters.setKeyPairsDispatcher} instead. * @param dispatcher A callback that returns the key pair for an actor. * @returns The setters object so that settings can be chained. + * @deprecated */ setKeyPairDispatcher( dispatcher: ActorKeyPairDispatcher, diff --git a/runtime/key.test.ts b/runtime/key.test.ts index 749e7a0a..d68d7280 100644 --- a/runtime/key.test.ts +++ b/runtime/key.test.ts @@ -3,7 +3,7 @@ import { exportJwk, importJwk } from "../sig/key.ts"; import { exportSpki, importSpki } from "./key.ts"; // cSpell: disable -const pem = "-----BEGIN PUBLIC KEY-----\n" + +const rsaPem = "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsRuvCkgJtflBTl4OVsm\n" + "nt/J1mQfZasfJtN33dcZ3d1lJroxmgmMu69zjGEAwkNbMQaWNLqC4eogkJaeJ4RR\n" + "5MHYXkL9nNilVoTkjX5BVit3puzs7XJ7WQnKQgQMI+ezn24GHsZ/v1JIo77lerX5\n" + @@ -14,7 +14,7 @@ const pem = "-----BEGIN PUBLIC KEY-----\n" + "-----END PUBLIC KEY-----\n"; // cSpell: enable -const jwk = { +const rsaJwk = { alg: "RS256", // cSpell: disable e: "AQAB", @@ -31,13 +31,36 @@ const jwk = { // cSpell: enable }; +// cSpell: disable +const ed25519Pem = "-----BEGIN PUBLIC KEY-----\n" + + "MCowBQYDK2VwAyEAvrabdlLgVI5jWl7GpF+fLFJVF4ccI8D7h+v5ulBCYwo=\n" + + "-----END PUBLIC KEY-----\n"; +// cSpell: enable + +const ed25519Jwk = { + kty: "OKP", + crv: "Ed25519", + // cSpell: disable + x: "vrabdlLgVI5jWl7GpF-fLFJVF4ccI8D7h-v5ulBCYwo", + // cSpell: enable + key_ops: ["verify"], + ext: true, +}; + Deno.test("importSpki()", async () => { - const key = await importSpki(pem); - assertEquals(await exportJwk(key), jwk); + const rsaKey = await importSpki(rsaPem); + assertEquals(await exportJwk(rsaKey), rsaJwk); + + const ed25519Key = await importSpki(ed25519Pem); + assertEquals(await exportJwk(ed25519Key), ed25519Jwk); }); Deno.test("exportSpki()", async () => { - const key = await importJwk(jwk, "public"); - const spki = await exportSpki(key); - assertEquals(spki, pem); + const rsaKey = await importJwk(rsaJwk, "public"); + const rsaSpki = await exportSpki(rsaKey); + assertEquals(rsaSpki, rsaPem); + + const ed25519Key = await importJwk(ed25519Jwk, "public"); + const ed25519Spki = await exportSpki(ed25519Key); + assertEquals(ed25519Spki, ed25519Pem); }); diff --git a/runtime/key.ts b/runtime/key.ts index 8198cec2..f135540c 100644 --- a/runtime/key.ts +++ b/runtime/key.ts @@ -1,34 +1,57 @@ import { decodeBase64, encodeBase64 } from "@std/encoding/base64"; +import { PublicKeyInfo } from "pkijs"; +import { validateCryptoKey } from "../sig/key.ts"; + +const algorithms: Record< + string, + | AlgorithmIdentifier + | HmacImportParams + | RsaHashedImportParams + | EcKeyImportParams +> = { + "1.2.840.113549.1.1.1": { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + "1.3.101.112": "Ed25519", +}; /** - * Imports a PEM-SPKI formatted RSA-PKCS#1-v1.5 public key. - * @param pem The PEM-SPKI formatted RSA-PKCS#1-v1.5 public key. + * Imports a PEM-SPKI formatted public key. + * @param pem The PEM-SPKI formatted public key. * @returns The imported public key. + * @throws {TypeError} If the key is invalid or unsupported. * @since 0.5.0 */ export async function importSpki(pem: string): Promise { pem = pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, ""); - const spki = decodeBase64(pem); + let spki: Uint8Array; + try { + spki = decodeBase64(pem); + } catch (_) { + throw new TypeError("Invalid PEM-SPKI format."); + } + const pki = PublicKeyInfo.fromBER(spki); + const oid = pki.algorithm.algorithmId; + const algorithm = algorithms[oid]; + if (algorithm == null) { + throw new TypeError("Unsupported algorithm: " + oid); + } return await crypto.subtle.importKey( "spki", spki, - { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, + algorithm, true, ["verify"], ); } /** - * Exports an RSA-PKCS#1-v1.5 public key in PEM-SPKI format. + * Exports a public key in PEM-SPKI format. * @param key The public key to export. - * @returns The exported RSA-PKCS#1-v1.5 public key in PEM-SPKI format. - * @since 0.5.0 + * @returns The exported public key in PEM-SPKI format. * @throws {TypeError} If the key is invalid or unsupported. + * @since 0.5.0 */ export async function exportSpki(key: CryptoKey): Promise { - if (key.algorithm.name !== "RSASSA-PKCS1-v1_5") { - throw new TypeError("Unsupported algorithm: " + key.algorithm.name); - } + validateCryptoKey(key); const spki = await crypto.subtle.exportKey("spki", key); let pem = encodeBase64(spki); pem = (pem.match(/.{1,64}/g) || []).join("\n"); diff --git a/sig/key.ts b/sig/key.ts index 13f7749c..ea14cee8 100644 --- a/sig/key.ts +++ b/sig/key.ts @@ -30,7 +30,7 @@ export function validateCryptoKey( if (key.algorithm.name === "RSASSA-PKCS1-v1_5") { // @ts-ignore TS2304 const algorithm = key.algorithm as unknown as RsaHashedKeyAlgorithm; - if (algorithm.hash.name != "SHA-256") { + if (algorithm.hash.name !== "SHA-256") { throw new TypeError( "For compatibility with the existing Fediverse software " + "(e.g., Mastodon), hash algorithm for RSASSA-PKCS1-v1_5 keys " + diff --git a/testing/context.ts b/testing/context.ts index 1bdc8c12..da0fd75e 100644 --- a/testing/context.ts +++ b/testing/context.ts @@ -2,6 +2,7 @@ import { getLogger } from "@logtape/logtape"; import type { Context, RequestContext } from "../federation/context.ts"; import { RouterError } from "../federation/router.ts"; import { mockDocumentLoader } from "./docloader.ts"; +import { CryptographicKey } from "../vocab/vocab.ts"; export function createContext( { @@ -16,6 +17,7 @@ export function createContext( getFollowingUri, getFollowersUri, parseUri, + getActorKeyPairs, getActorKey, getDocumentLoader, sendActivity, @@ -50,8 +52,27 @@ export function createContext( getDocumentLoader: getDocumentLoader ?? ((_params) => { throw new Error("Not implemented"); }), - getActorKey: getActorKey ?? ((_handle) => { - return Promise.resolve(null); + getActorKeyPairs: getActorKeyPairs ?? ((_handle) => Promise.resolve([])), + getActorKey: getActorKey ?? (async (handle) => { + getLogger(["fedify", "federation"]).warn( + "Context.getActorKeys() method is deprecated; " + + "use Context.getActorKeyPairs() method instead.", + ); + if (getActorKeyPairs == null) return null; + for (const keyPair of await getActorKeyPairs(handle)) { + const { privateKey } = keyPair; + if ( + privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5" || + (privateKey.algorithm as RsaHashedKeyAlgorithm).hash.name !== + "SHA-256" + ) continue; + return new CryptographicKey({ + id: keyPair.keyId, + owner: getActorUri?.(handle) ?? throwRouteError(), + publicKey: keyPair.publicKey, + }); + } + return null; }), sendActivity: sendActivity ?? ((_params) => { throw new Error("Not implemented"); diff --git a/testing/fixtures/example.com/key4 b/testing/fixtures/example.com/key4 new file mode 100644 index 00000000..91ca8b3e --- /dev/null +++ b/testing/fixtures/example.com/key4 @@ -0,0 +1,7 @@ +{ + "@context": "https://w3id.org/security/v1", + "id": "https://example.com/key4", + "type": "CryptographicKey", + "owner": "https://example.com/person2", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALR8epAGDe+cVq5p2Tx49CCfphpk1rNhkNoY9i+XEUfg=\n-----END PUBLIC KEY-----\n" +} diff --git a/testing/fixtures/example.com/person2 b/testing/fixtures/example.com/person2 index fa35e3c9..a6540f33 100644 --- a/testing/fixtures/example.com/person2 +++ b/testing/fixtures/example.com/person2 @@ -12,6 +12,12 @@ "type": "CryptographicKey", "owner": "https://example.com/person2", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4GUqWgdiYlN3Su5Gr4l6\ni+xRS8gDDVKZ718vpGk6eIpvqs33q430nRbHIzbHRXRaAhc/1++rUBcK0V4/kjZl\nCSzVtRgGU6HMkmjcD+uE56a8XbTczfltbEDj7afoEuB2F3UhQEWrSz+QJ29DPXaL\nMIa1Yv61NR2vxGqNbdtoMjDORMBYtg77CYbcFkiJHw65PDa7+f/yjLxuCRPye5L7\nhncN0UZuuFoRJmHNRLSg5omBad9WTvQXmSyXEhEdk9fHwlI022AqAzlWbT79hldc\nDSKGGLLbQIs1c3JZIG8G5i6Uh5Vy0Z7tSNBcxbhqoI9i9je4f/x/OPIVc19f04BE\n1LgWuHsftZzRgW9Sdqz53W83XxVdxlyHeywXOnstSWT11f8dkLyQUcHKTH+E6urb\nH+aiPLiRpYK8W7D9KTQA9kZ5JXaEuveBd5vJX7wakhbzAn8pWJU7GYIHNY38Ycok\nmivkU5pY8S2cKFMwY0b7ade3MComlir5P3ZYSjF+n6gRVsT96P+9mNfCu9gXt/f8\nXCyjKlH89kGwuJ7HhR8CuVdm0l+jYozVt6GsDy0hHYyn79NCCAEzP7ZbhBMR0T5V\nrkl+TIGXoJH9WFiz4VxO+NnglF6dNQjDS5IzYLoFRXIK1f3cmQiEB4FZmL70l9HL\nrgwR+Xys83xia79OqFDRezMCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + { + "id": "https://example.com/key4", + "type": "CryptographicKey", + "owner": "https://example.com/person2", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALR8epAGDe+cVq5p2Tx49CCfphpk1rNhkNoY9i+XEUfg=\n-----END PUBLIC KEY-----\n" } ] } diff --git a/testing/keys.ts b/testing/keys.ts index 8101c2ff..4771434c 100644 --- a/testing/keys.ts +++ b/testing/keys.ts @@ -208,18 +208,22 @@ export const ed25519PrivateKey = await crypto.subtle.importKey( ["sign"], ); -export const ed25519PublicKey = await crypto.subtle.importKey( - "jwk", - { - crv: "Ed25519", - ext: true, - key_ops: ["verify"], - kty: "OKP", - // cSpell: disable - x: "LR8epAGDe-cVq5p2Tx49CCfphpk1rNhkNoY9i-XEUfg", - // cSpell: enable - }, - "Ed25519", - true, - ["verify"], -); +export const ed25519PublicKey = new CryptographicKey({ + id: new URL("https://example.com/key4"), + owner: new URL("https://example.com/person2"), + publicKey: await crypto.subtle.importKey( + "jwk", + { + crv: "Ed25519", + ext: true, + key_ops: ["verify"], + kty: "OKP", + // cSpell: disable + x: "LR8epAGDe-cVq5p2Tx49CCfphpk1rNhkNoY9i-XEUfg", + // cSpell: enable + }, + "Ed25519", + true, + ["verify"], + ), +});