1. ed25519キーペアを発行・Personとして公開鍵を送受信
This commit is contained in:
		
							parent
							
								
									0fb7b98f96
								
							
						
					
					
						commit
						02dfe0a3d5
					
				|  | @ -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") `); | ||||
|     } | ||||
| } | ||||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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<MiUserKeypair>(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<void> { | ||||
| 		return await this.cache.refresh(userId); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async prepareEd25519KeyPair(userId: MiUser['id']): Promise<void> { | ||||
| 		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<void> { | ||||
| 		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(); | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ export type UriParseResult = { | |||
| @Injectable() | ||||
| export class ApDbResolverService implements OnApplicationShutdown { | ||||
| 	private publicKeyCache: MemoryKVCache<MiUserPublickey | null>; | ||||
| 	private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey | null>; | ||||
| 	private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey[] | null>; | ||||
| 
 | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
|  | @ -55,7 +55,7 @@ export class ApDbResolverService implements OnApplicationShutdown { | |||
| 		private apPersonService: ApPersonService, | ||||
| 	) { | ||||
| 		this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); | ||||
| 		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); | ||||
| 		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey[] | null>(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, | ||||
| 		); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<void> { | ||||
| 		//#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<string, boolean>(); | ||||
|  | @ -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<void> { | ||||
| 		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<void> { | ||||
| 		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, | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
| 		}; | ||||
|  |  | |||
|  | @ -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<string>(); | ||||
| 		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) { | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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}`, | ||||
| 	}; | ||||
| } | ||||
|  |  | |||
|  | @ -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<MiUserKeypair>) { | ||||
| 		if (data == null) return; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
| 	}) | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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']);
 | ||||
| 	}); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue