diff --git a/packages/backend/migration/1708980134301-APMultipleKeys.js b/packages/backend/migration/1708980134301-APMultipleKeys.js new file mode 100644 index 0000000000..d0687fa7fa --- /dev/null +++ b/packages/backend/migration/1708980134301-APMultipleKeys.js @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class APMultipleKeys1708980134301 { + name = 'APMultipleKeys1708980134301' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_171e64971c780ebd23fae140bb"`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKey" character varying(128)`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PrivateKey" character varying(128)`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKeySignature" character varying(720)`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519SignatureAlgorithm" character varying(32)`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_0db6a5fdb992323449edc8ee421"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_171e64971c780ebd23fae140bba" PRIMARY KEY ("keyId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_10c146e4b39b443ede016f6736" ON "user_publickey" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`DROP INDEX "public"."IDX_10c146e4b39b443ede016f6736"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_171e64971c780ebd23fae140bba"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_0db6a5fdb992323449edc8ee421"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_10c146e4b39b443ede016f6736d" PRIMARY KEY ("userId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" TYPE "public"."user_profile_followersVisibility_enum_old" USING "followersVisibility"::"text"::"public"."user_profile_followersVisibility_enum_old"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" SET DEFAULT 'public'`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519SignatureAlgorithm"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKeySignature"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PrivateKey"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKey"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `); + } +} diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 7c1b34da05..331df4315d 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -240,6 +240,7 @@ export interface InternalEventTypes { unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; }; userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; + userKeypairUpdated: { userId: MiUser['id']; }; } // name/messages(spec) pairs dictionary diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 51ac99179a..7946f410cf 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { sign } from 'node:crypto'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; @@ -11,6 +12,8 @@ import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { ED25519_SIGN_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { @@ -19,9 +22,12 @@ export class UserKeypairService implements OnApplicationShutdown { constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, + + private globalEventService: GlobalEventService, ) { this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h @@ -30,6 +36,8 @@ export class UserKeypairService implements OnApplicationShutdown { toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), }); + + this.redisForSub.on('message', this.onMessage); } @bindThis @@ -37,6 +45,41 @@ export class UserKeypairService implements OnApplicationShutdown { return await this.cache.fetch(userId); } + @bindThis + public async refresh(userId: MiUser['id']): Promise { + return await this.cache.refresh(userId); + } + + @bindThis + public async prepareEd25519KeyPair(userId: MiUser['id']): Promise { + await this.refresh(userId); + const keypair = await this.cache.fetch(userId); + if (keypair.ed25519PublicKey != null) return; + const ed25519 = await genEd25519KeyPair(); + const ed25519PublicKeySignature = sign(ED25519_SIGN_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); + await this.userKeypairsRepository.update({ userId }, { + ed25519PublicKey: ed25519.publicKey, + ed25519PrivateKey: ed25519.privateKey, + ed25519PublicKeySignature, + ed25519SignatureAlgorithm: `rsa-${ED25519_SIGN_ALGORITHM}`, + }); + this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'userKeypairUpdated': { + this.refresh(body.userId); + break; + } + } + } + } @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index f6b70ead44..913070fdd4 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -36,7 +36,7 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService implements OnApplicationShutdown { private publicKeyCache: MemoryKVCache; - private publicKeyByUserIdCache: MemoryKVCache; + private publicKeyByUserIdCache: MemoryKVCache; constructor( @Inject(DI.config) @@ -55,7 +55,7 @@ export class ApDbResolverService implements OnApplicationShutdown { private apPersonService: ApPersonService, ) { this.publicKeyCache = new MemoryKVCache(Infinity); - this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); + this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); } @bindThis @@ -159,7 +159,7 @@ export class ApDbResolverService implements OnApplicationShutdown { const key = await this.publicKeyByUserIdCache.fetch( user.id, - () => this.userPublickeysRepository.findOneBy({ userId: user.id }), + () => this.userPublickeysRepository.find({ where: { userId: user.id } }), v => v != null, ); diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f..27a9c11dab 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -9,10 +9,10 @@ import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; +import { UserKeypairService } from '../UserKeypairService.js'; interface IRecipe { type: string; @@ -40,14 +40,14 @@ class DeliverManager { /** * Constructor - * @param userEntityService + * @param userKeypairService * @param followingsRepository * @param queueService * @param actor Actor * @param activity Activity to deliver */ constructor( - private userEntityService: UserEntityService, + private userKeypairService: UserKeypairService, private followingsRepository: FollowingsRepository, private queueService: QueueService, @@ -105,6 +105,13 @@ class DeliverManager { */ @bindThis public async execute(): Promise { + //#region MIGRATION + /** + * ed25519の署名がなければ追加する + */ + await this.userKeypairService.prepareEd25519KeyPair(this.actor.id); + //#endregion + // The value flags whether it is shared or not. // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); @@ -154,7 +161,7 @@ export class ApDeliverManagerService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private userEntityService: UserEntityService, + private userKeypairService: UserKeypairService, private queueService: QueueService, ) { } @@ -167,7 +174,7 @@ export class ApDeliverManagerService { @bindThis public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise { const manager = new DeliverManager( - this.userEntityService, + this.userKeypairService, this.followingsRepository, this.queueService, actor, @@ -186,7 +193,7 @@ export class ApDeliverManagerService { @bindThis public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise { const manager = new DeliverManager( - this.userEntityService, + this.userKeypairService, this.followingsRepository, this.queueService, actor, @@ -199,7 +206,7 @@ export class ApDeliverManagerService { @bindThis public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { return new DeliverManager( - this.userEntityService, + this.userKeypairService, this.followingsRepository, this.queueService, diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index d7fb977a99..66322f6870 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -250,15 +250,16 @@ export class ApRendererService { } @bindThis - public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { + public renderKey(user: MiLocalUser, publicKey: string, postfix?: string, signature?: IKey['signature']): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', owner: this.userEntityService.genLocalUserUri(user.id), - publicKeyPem: createPublicKey(key.publicKey).export({ + publicKeyPem: createPublicKey(publicKey).export({ type: 'spki', format: 'pem', - }), + }) as string, + signature, }; } @@ -498,7 +499,10 @@ export class ApRendererService { tag, manuallyApprovesFollowers: user.isLocked, discoverable: user.isExplorable, - publicKey: this.renderKey(user, keypair, '#main-key'), + publicKey: this.renderKey(user, keypair.publicKey, '#main-key'), + additionalPublicKeys: [ + ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key', { type: keypair.ed25519SignatureAlgorithm!, signatureValue: keypair.ed25519PublicKeySignature! })] : []), + ], isCat: user.isCat, attachment: attachment.length ? attachment : undefined, }; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 744b1ea683..c31aa03261 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -3,9 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { verify } from 'crypto'; import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import { DataSource } from 'typeorm'; +import { DataSource, In, Not } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; @@ -357,10 +358,25 @@ export class ApPersonService implements OnModuleInit { if (person.publicKey) { await transactionalEntityManager.save(new MiUserPublickey({ - userId: user.id, keyId: person.publicKey.id, + userId: user.id, keyPem: person.publicKey.publicKeyPem, })); + + if (person.additionalPublicKeys) { + for (const key of person.additionalPublicKeys) { + if ( + key.signature && key.signature.type && key.signature.signatureValue && + verify(key.signature.type, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) + ) { + await transactionalEntityManager.save(new MiUserPublickey({ + keyId: key.id, + userId: user.id, + keyPem: key.publicKeyPem, + })); + } + } + } } }); } catch (e) { @@ -506,13 +522,35 @@ export class ApPersonService implements OnModuleInit { // Update user await this.usersRepository.update(exist.id, updates); + const availablePublicKeys = new Set(); if (person.publicKey) { - await this.userPublickeysRepository.update({ userId: exist.id }, { - keyId: person.publicKey.id, + await this.userPublickeysRepository.update({ keyId: person.publicKey.id }, { + userId: exist.id, keyPem: person.publicKey.publicKeyPem, }); + availablePublicKeys.add(person.publicKey.id); + + if (person.additionalPublicKeys) { + for (const key of person.additionalPublicKeys) { + if ( + key.signature && key.signature.type && key.signature.signatureValue && + verify(key.signature.type, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) + ) { + await this.userPublickeysRepository.update({ keyId: key.id }, { + userId: exist.id, + keyPem: key.publicKeyPem, + }); + availablePublicKeys.add(key.id); + } + } + } } + this.userPublickeysRepository.delete({ + keyId: Not(In(Array.from(availablePublicKeys))), + userId: exist.id, + }); + let _description: string | null = null; if (person._misskey_summary) { diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index b43dddad61..72d383442e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -168,10 +168,8 @@ export interface IActor extends IObject { discoverable?: boolean; inbox: string; sharedInbox?: string; // 後方互換性のため - publicKey?: { - id: string; - publicKeyPem: string; - }; + publicKey?: IKey; + additionalPublicKeys?: IKey[]; followers?: string | ICollection | IOrderedCollection; following?: string | ICollection | IOrderedCollection; featured?: string | IOrderedCollection; @@ -235,8 +233,17 @@ export const isEmoji = (object: IObject): object is IApEmoji => export interface IKey extends IObject { type: 'Key'; + id: string; owner: string; - publicKeyPem: string | Buffer; + publicKeyPem: string; + + /** + * Signature of publicKeyPem, signed by root privateKey (for additionalPublicKey) + */ + signature?: { + type: string; + signatureValue: string + }; } export interface IApDocument extends IObject { diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 02a303dc0a..7b2e84d991 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -8,7 +8,9 @@ import * as util from 'node:util'; const generateKeyPair = util.promisify(crypto.generateKeyPair); -export async function genRsaKeyPair(modulusLength = 2048) { +export const ED25519_SIGN_ALGORITHM = 'sha256'; + +export async function genRsaKeyPair(modulusLength = 4096) { return await generateKeyPair('rsa', { modulusLength, publicKeyEncoding: { @@ -24,9 +26,8 @@ export async function genRsaKeyPair(modulusLength = 2048) { }); } -export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') { - return await generateKeyPair('ec', { - namedCurve, +export async function genEd25519KeyPair() { + return await generateKeyPair('ed25519', { publicKeyEncoding: { type: 'spki', format: 'pem', @@ -39,3 +40,17 @@ export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'sec }, }); } + +export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { + const rsa = await genRsaKeyPair(rsaModulusLength); + const ed25519 = await genEd25519KeyPair(); + const ed25519PublicKeySignature = crypto.sign(ED25519_SIGN_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); + return { + publicKey: rsa.publicKey, + privateKey: rsa.privateKey, + ed25519PublicKey: ed25519.publicKey, + ed25519PrivateKey: ed25519.privateKey, + ed25519PublicKeySignature, + ed25519SignatureAlgorithm: `rsa-${ED25519_SIGN_ALGORITHM}`, + }; +} diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts index f5252d126c..47d44af849 100644 --- a/packages/backend/src/models/UserKeypair.ts +++ b/packages/backend/src/models/UserKeypair.ts @@ -18,16 +18,56 @@ export class MiUserKeypair { @JoinColumn() public user: MiUser | null; + /** + * RSA public key + */ @Column('varchar', { length: 4096, }) public publicKey: string; + /** + * RSA private key + */ @Column('varchar', { length: 4096, }) public privateKey: string; + @Column('varchar', { + length: 128, + nullable: true, + default: null, + }) + public ed25519PublicKey: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + default: null, + }) + public ed25519PrivateKey: string | null; + + /** + * Signature of ed25519PublicKey, signed by privateKey. (base64) + */ + @Column('varchar', { + length: 720, + nullable: true, + default: null, + }) + public ed25519PublicKeySignature: string | null; + + /** + * Signature algorithm of ed25519PublicKeySignature. + */ + @Column('varchar', { + length: 32, + nullable: true, + default: null, + }) + public ed25519SignatureAlgorithm: string | null; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts index 6bcd785304..0ecff2bcbe 100644 --- a/packages/backend/src/models/UserPublickey.ts +++ b/packages/backend/src/models/UserPublickey.ts @@ -9,7 +9,13 @@ import { MiUser } from './User.js'; @Entity('user_publickey') export class MiUserPublickey { - @PrimaryColumn(id()) + @PrimaryColumn('varchar', { + length: 256, + }) + public keyId: string; + + @Index() + @Column(id()) public userId: MiUser['id']; @OneToOne(type => MiUser, { @@ -18,12 +24,6 @@ export class MiUserPublickey { @JoinColumn() public user: MiUser | null; - @Index({ unique: true }) - @Column('varchar', { - length: 256, - }) - public keyId: string; - @Column('varchar', { length: 4096, }) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 60366dd5c2..5e6cb0a05a 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -640,7 +640,7 @@ export class ActivityPubServerService { if (this.userEntityService.isLocalUser(user)) { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); + return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair.publicKey))); } else { reply.code(400); return; diff --git a/packages/backend/test/unit/misc/gen-key-pair.ts b/packages/backend/test/unit/misc/gen-key-pair.ts new file mode 100644 index 0000000000..91c62f621d --- /dev/null +++ b/packages/backend/test/unit/misc/gen-key-pair.ts @@ -0,0 +1,40 @@ +import * as crypto from 'node:crypto'; +import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; + +describe(genRSAAndEd25519KeyPair, () => { + test('generates key pair', async () => { + const keyPair = await genRSAAndEd25519KeyPair(); + // 毎回違うキーペアが生成されることを確認するために2回生成して比較してみる + const keyPair2 = await genRSAAndEd25519KeyPair(); + console.log(Object.entries(keyPair).map(([k, v]) => `${k}: ${v.length}`).join('\n')); + console.log(Object.entries(keyPair).map(([k, v]) => `${k}\n${v}`).join('\n')); + + expect(keyPair.publicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); + expect(keyPair.publicKey).toMatch(/-----END PUBLIC KEY-----\n$/); + expect(keyPair.publicKey).not.toBe(keyPair2.publicKey); + + const publicKeyObj = crypto.createPublicKey(keyPair.publicKey); + expect(publicKeyObj.asymmetricKeyType).toBe('rsa'); + + expect(keyPair.privateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); + expect(keyPair.privateKey).toMatch(/-----END PRIVATE KEY-----\n$/); + expect(keyPair.privateKey).not.toBe(keyPair2.privateKey); + expect(keyPair.ed25519PublicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); + expect(keyPair.ed25519PublicKey).toMatch(/-----END PUBLIC KEY-----\n$/); + expect(keyPair.ed25519PublicKey).not.toBe(keyPair2.ed25519PublicKey); + + const ed25519PublicKeyObj = crypto.createPublicKey(keyPair.ed25519PublicKey); + expect(ed25519PublicKeyObj.asymmetricKeyType).toBe('ed25519'); + + expect(keyPair.ed25519PrivateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); + expect(keyPair.ed25519PrivateKey).toMatch(/-----END PRIVATE KEY-----\n$/); + expect(keyPair.ed25519PrivateKey).not.toBe(keyPair2.ed25519PrivateKey); + expect(keyPair.ed25519PublicKeySignature).toBe( + crypto.sign(keyPair.ed25519SignatureAlgorithm.split('-').pop(), Buffer.from(keyPair.ed25519PublicKey), keyPair.privateKey).toString('base64'), + ); + expect(crypto.verify(keyPair.ed25519SignatureAlgorithm, Buffer.from(keyPair.ed25519PublicKey), keyPair.publicKey, Buffer.from(keyPair.ed25519PublicKeySignature, 'base64'))).toBe(true); + expect(keyPair.ed25519PublicKeySignature).not.toBe(keyPair2.ed25519PublicKeySignature); + + //const imported = await webCrypto.subtle.importKey('spki', Buffer.from(keyPair.publicKey).buffer, { name: 'rsa-pss', hash: 'sha-256' }, false, ['verify']); + }); +});