From 10ce7bf3c45c3e09dc86f1b9c3a0d7e79c23f5ee Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Thu, 18 Jul 2024 20:04:23 +0900
Subject: [PATCH 1/9] kill any from streaming API Implementation (#14251)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* chore: add JsonValue type

* refactor: kill any from Connection.ts

* refactor: fix StreamEventEmitter contains undefined instead of null

* refactor: kill any from channels

* docs(changelog): Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題

* fix license header

* fix lints
---
 CHANGELOG.md                                  |  2 +
 .../backend/src/core/GlobalEventService.ts    | 28 +++++----
 packages/backend/src/misc/json-value.ts       |  8 +++
 .../src/server/api/stream/Connection.ts       | 57 ++++++++++++-------
 .../backend/src/server/api/stream/channel.ts  | 13 +++--
 .../src/server/api/stream/channels/admin.ts   |  3 +-
 .../src/server/api/stream/channels/antenna.ts |  6 +-
 .../src/server/api/stream/channels/channel.ts |  6 +-
 .../src/server/api/stream/channels/drive.ts   |  3 +-
 .../api/stream/channels/global-timeline.ts    |  7 ++-
 .../src/server/api/stream/channels/hashtag.ts |  7 ++-
 .../api/stream/channels/home-timeline.ts      |  7 ++-
 .../api/stream/channels/hybrid-timeline.ts    |  9 +--
 .../api/stream/channels/local-timeline.ts     |  9 +--
 .../src/server/api/stream/channels/main.ts    |  3 +-
 .../server/api/stream/channels/queue-stats.ts | 10 +++-
 .../api/stream/channels/reversi-game.ts       | 33 ++++++++---
 .../src/server/api/stream/channels/reversi.ts |  3 +-
 .../api/stream/channels/role-timeline.ts      |  6 +-
 .../api/stream/channels/server-stats.ts       |  8 ++-
 .../server/api/stream/channels/user-list.ts   | 10 ++--
 21 files changed, 155 insertions(+), 83 deletions(-)
 create mode 100644 packages/backend/src/misc/json-value.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ed34ab050..14d638ebe1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ### Note
 - デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
+- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
 
 ### General
 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
@@ -76,6 +77,7 @@
 - Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
 - Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正  
   (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
+- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
 
 ### Misskey.js
 - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 2a7d8d4bbe..312bcfb3b5 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -209,6 +209,10 @@ type SerializedAll<T> = {
 	[K in keyof T]: Serialized<T[K]>;
 };
 
+type UndefinedAsNullAll<T> = {
+	[K in keyof T]: T[K] extends undefined ? null : T[K];
+}
+
 export interface InternalEventTypes {
 	userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
 	userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
@@ -248,43 +252,45 @@ export interface InternalEventTypes {
 	userKeypairUpdated: { userId: MiUser['id']; };
 }
 
+type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
+
 // name/messages(spec) pairs dictionary
 export type GlobalEvents = {
 	internal: {
 		name: 'internal';
-		payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
+		payload: EventTypesToEventPayload<InternalEventTypes>;
 	};
 	broadcast: {
 		name: 'broadcast';
-		payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
+		payload: EventTypesToEventPayload<BroadcastTypes>;
 	};
 	main: {
 		name: `mainStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
+		payload: EventTypesToEventPayload<MainEventTypes>;
 	};
 	drive: {
 		name: `driveStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
+		payload: EventTypesToEventPayload<DriveEventTypes>;
 	};
 	note: {
 		name: `noteStream:${MiNote['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
+		payload: EventTypesToEventPayload<NoteStreamEventTypes>;
 	};
 	userList: {
 		name: `userListStream:${MiUserList['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
+		payload: EventTypesToEventPayload<UserListEventTypes>;
 	};
 	roleTimeline: {
 		name: `roleTimelineStream:${MiRole['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
+		payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
 	};
 	antenna: {
 		name: `antennaStream:${MiAntenna['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
+		payload: EventTypesToEventPayload<AntennaEventTypes>;
 	};
 	admin: {
 		name: `adminStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
+		payload: EventTypesToEventPayload<AdminEventTypes>;
 	};
 	notes: {
 		name: 'notesStream';
@@ -292,11 +298,11 @@ export type GlobalEvents = {
 	};
 	reversi: {
 		name: `reversiStream:${MiUser['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
+		payload: EventTypesToEventPayload<ReversiEventTypes>;
 	};
 	reversiGame: {
 		name: `reversiGameStream:${MiReversiGame['id']}`;
-		payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
+		payload: EventTypesToEventPayload<ReversiGameEventTypes>;
 	};
 };
 
diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts
new file mode 100644
index 0000000000..7994441791
--- /dev/null
+++ b/packages/backend/src/misc/json-value.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
+export type JsonObject = {[K in string]?: JsonValue};
+export type JsonArray = JsonValue[];
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index 41c0feccc7..96082827f8 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
 import { MiFollowing, MiUserProfile } from '@/models/_.js';
 import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
 import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import type { ChannelsService } from './ChannelsService.js';
 import type { EventEmitter } from 'events';
 import type Channel from './channel.js';
@@ -28,7 +29,7 @@ export default class Connection {
 	private wsConnection: WebSocket.WebSocket;
 	public subscriber: StreamEventEmitter;
 	private channels: Channel[] = [];
-	private subscribingNotes: any = {};
+	private subscribingNotes: Partial<Record<string, number>> = {};
 	private cachedNotes: Packed<'Note'>[] = [];
 	public userProfile: MiUserProfile | null = null;
 	public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@@ -101,7 +102,7 @@ export default class Connection {
 	 */
 	@bindThis
 	private async onWsConnectionMessage(data: WebSocket.RawData) {
-		let obj: Record<string, any>;
+		let obj: JsonObject;
 
 		try {
 			obj = JSON.parse(data.toString());
@@ -111,6 +112,8 @@ export default class Connection {
 
 		const { type, body } = obj;
 
+		if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+
 		switch (type) {
 			case 'readNotification': this.onReadNotification(body); break;
 			case 'subNote': this.onSubscribeNote(body); break;
@@ -151,7 +154,7 @@ export default class Connection {
 	}
 
 	@bindThis
-	private readNote(body: any) {
+	private readNote(body: JsonObject) {
 		const id = body.id;
 
 		const note = this.cachedNotes.find(n => n.id === id);
@@ -163,7 +166,7 @@ export default class Connection {
 	}
 
 	@bindThis
-	private onReadNotification(payload: any) {
+	private onReadNotification(payload: JsonObject) {
 		this.notificationService.readAllNotification(this.user!.id);
 	}
 
@@ -171,16 +174,14 @@ export default class Connection {
 	 * 投稿購読要求時
 	 */
 	@bindThis
-	private onSubscribeNote(payload: any) {
-		if (!payload.id) return;
+	private onSubscribeNote(payload: JsonObject) {
+		if (!payload.id || typeof payload.id !== 'string') return;
 
-		if (this.subscribingNotes[payload.id] == null) {
-			this.subscribingNotes[payload.id] = 0;
-		}
+		const current = this.subscribingNotes[payload.id] ?? 0;
+		const updated = current + 1;
+		this.subscribingNotes[payload.id] = updated;
 
-		this.subscribingNotes[payload.id]++;
-
-		if (this.subscribingNotes[payload.id] === 1) {
+		if (updated === 1) {
 			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
 	}
@@ -189,11 +190,14 @@ export default class Connection {
 	 * 投稿購読解除要求時
 	 */
 	@bindThis
-	private onUnsubscribeNote(payload: any) {
-		if (!payload.id) return;
+	private onUnsubscribeNote(payload: JsonObject) {
+		if (!payload.id || typeof payload.id !== 'string') return;
 
-		this.subscribingNotes[payload.id]--;
-		if (this.subscribingNotes[payload.id] <= 0) {
+		const current = this.subscribingNotes[payload.id];
+		if (current == null) return;
+		const updated = current - 1;
+		this.subscribingNotes[payload.id] = updated;
+		if (updated <= 0) {
 			delete this.subscribingNotes[payload.id];
 			this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
@@ -212,17 +216,22 @@ export default class Connection {
 	 * チャンネル接続要求時
 	 */
 	@bindThis
-	private onChannelConnectRequested(payload: any) {
+	private onChannelConnectRequested(payload: JsonObject) {
 		const { channel, id, params, pong } = payload;
-		this.connectChannel(id, params, channel, pong);
+		if (typeof id !== 'string') return;
+		if (typeof channel !== 'string') return;
+		if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
+		if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
+		this.connectChannel(id, params, channel, pong ?? undefined);
 	}
 
 	/**
 	 * チャンネル切断要求時
 	 */
 	@bindThis
-	private onChannelDisconnectRequested(payload: any) {
+	private onChannelDisconnectRequested(payload: JsonObject) {
 		const { id } = payload;
+		if (typeof id !== 'string') return;
 		this.disconnectChannel(id);
 	}
 
@@ -230,7 +239,7 @@ export default class Connection {
 	 * クライアントにメッセージ送信
 	 */
 	@bindThis
-	public sendMessageToWs(type: string, payload: any) {
+	public sendMessageToWs(type: string, payload: JsonObject) {
 		this.wsConnection.send(JSON.stringify({
 			type: type,
 			body: payload,
@@ -241,7 +250,7 @@ export default class Connection {
 	 * チャンネルに接続
 	 */
 	@bindThis
-	public connectChannel(id: string, params: any, channel: string, pong = false) {
+	public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
 		const channelService = this.channelsService.getChannelService(channel);
 
 		if (channelService.requireCredential && this.user == null) {
@@ -288,7 +297,11 @@ export default class Connection {
 	 * @param data メッセージ
 	 */
 	@bindThis
-	private onChannelMessageRequested(data: any) {
+	private onChannelMessageRequested(data: JsonObject) {
+		if (typeof data.id !== 'string') return;
+		if (typeof data.type !== 'string') return;
+		if (typeof data.body === 'undefined') return;
+
 		const channel = this.channels.find(c => c.id === data.id);
 		if (channel != null && channel.onMessage != null) {
 			channel.onMessage(data.type, data.body);
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index a267d27fba..84cb552369 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
 import { isUserRelated } from '@/misc/is-user-related.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
 import type { Packed } from '@/misc/json-schema.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import type Connection from './Connection.js';
 
 /**
@@ -81,10 +82,12 @@ export default abstract class Channel {
 		this.connection = connection;
 	}
 
+	public send(payload: { type: string, body: JsonValue }): void
+	public send(type: string, payload: JsonValue): void
 	@bindThis
-	public send(typeOrPayload: any, payload?: any) {
-		const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
-		const body = payload === undefined ? typeOrPayload.body : payload;
+	public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
+		const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
+		const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
 
 		this.connection.sendMessageToWs('channel', {
 			id: this.id,
@@ -93,11 +96,11 @@ export default abstract class Channel {
 		});
 	}
 
-	public abstract init(params: any): void;
+	public abstract init(params: JsonObject): void;
 
 	public dispose?(): void;
 
-	public onMessage?(type: string, body: any): void;
+	public onMessage?(type: string, body: JsonValue): void;
 }
 
 export type MiChannelService<T extends boolean> = {
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
index 92b6d2ac04..355d5dba21 100644
--- a/packages/backend/src/server/api/stream/channels/admin.ts
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -5,6 +5,7 @@
 
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class AdminChannel extends Channel {
@@ -14,7 +15,7 @@ class AdminChannel extends Channel {
 	public static kind = 'read:admin:stream';
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		// Subscribe admin stream
 		this.subscriber.on(`adminStream:${this.user!.id}`, data => {
 			this.send(data);
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 4a1d2dd109..53dc7f18b6 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class AntennaChannel extends Channel {
@@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.antennaId = params.antennaId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.antennaId !== 'string') return;
+		this.antennaId = params.antennaId;
 
 		// Subscribe stream
 		this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 140dd3dd9b..7108e0cd6e 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class ChannelChannel extends Channel {
@@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.channelId = params.channelId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.channelId !== 'string') return;
+		this.channelId = params.channelId;
 
 		// Subscribe stream
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
index 0d9b486305..03768f3d23 100644
--- a/packages/backend/src/server/api/stream/channels/drive.ts
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -5,6 +5,7 @@
 
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class DriveChannel extends Channel {
@@ -14,7 +15,7 @@ class DriveChannel extends Channel {
 	public static kind = 'read:account';
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		// Subscribe drive stream
 		this.subscriber.on(`driveStream:${this.user!.id}`, data => {
 			this.send(data);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 17116258d8..ed56fe0d40 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class GlobalTimelineChannel extends Channel {
@@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.gtlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 57bada5d9c..8105f15cb1 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class HashtagChannel extends Channel {
@@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
+		if (!Array.isArray(params.q)) return;
+		if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
 		this.q = params.q;
 
-		if (this.q == null) return;
-
 		// Subscribe stream
 		this.subscriber.on('notesStream', this.onNote);
 	}
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 878a3180cb..1f440732a6 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class HomeTimelineChannel extends Channel {
@@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.withRenotes = params.withRenotes ?? true;
-		this.withFiles = params.withFiles ?? false;
+	public async init(params: JsonObject) {
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		this.subscriber.on('notesStream', this.onNote);
 	}
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 575d23d53c..6938b6e3ea 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class HybridTimelineChannel extends Channel {
@@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any): Promise<void> {
+	public async init(params: JsonObject): Promise<void> {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.ltlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withReplies = params.withReplies ?? false;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withReplies = !!(params.withReplies ?? false);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 442d08ae51..491029f5de 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class LocalTimelineChannel extends Channel {
@@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
 		if (!policies.ltlAvailable) return;
 
-		this.withRenotes = params.withRenotes ?? true;
-		this.withReplies = params.withReplies ?? false;
-		this.withFiles = params.withFiles ?? false;
+		this.withRenotes = !!(params.withRenotes ?? true);
+		this.withReplies = !!(params.withReplies ?? false);
+		this.withFiles = !!(params.withFiles ?? false);
 
 		// Subscribe events
 		this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
index a12976d69d..863d7f4c4e 100644
--- a/packages/backend/src/server/api/stream/channels/main.ts
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
 import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class MainChannel extends Channel {
@@ -25,7 +26,7 @@ class MainChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		// Subscribe main stream channel
 		this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
 			switch (data.type) {
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
index 061aa76904..ff7e740226 100644
--- a/packages/backend/src/server/api/stream/channels/queue-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -6,6 +6,7 @@
 import Xev from 'xev';
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 const ev = new Xev();
@@ -22,19 +23,22 @@ class QueueStatsChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		ev.addListener('queueStats', this.onStats);
 	}
 
 	@bindThis
-	private onStats(stats: any) {
+	private onStats(stats: JsonObject) {
 		this.send('stats', stats);
 	}
 
 	@bindThis
-	public onMessage(type: string, body: any) {
+	public onMessage(type: string, body: JsonValue) {
 		switch (type) {
 			case 'requestLog':
+				if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+				if (typeof body.id !== 'string') return;
+				if (typeof body.length !== 'number') return;
 				ev.once(`queueStatsLog:${body.id}`, statsLog => {
 					this.send('statsLog', statsLog);
 				});
diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts
index f4a3a09367..17823a164a 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { ReversiService } from '@/core/ReversiService.js';
 import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class ReversiGameChannel extends Channel {
@@ -28,25 +29,41 @@ class ReversiGameChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.gameId = params.gameId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.gameId !== 'string') return;
+		this.gameId = params.gameId;
 
 		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
 	}
 
 	@bindThis
-	public onMessage(type: string, body: any) {
+	public onMessage(type: string, body: JsonValue) {
 		switch (type) {
-			case 'ready': this.ready(body); break;
-			case 'updateSettings': this.updateSettings(body.key, body.value); break;
-			case 'cancel': this.cancelGame(); break;
-			case 'putStone': this.putStone(body.pos, body.id); break;
+			case 'ready':
+				if (typeof body !== 'boolean') return;
+				this.ready(body);
+				break;
+			case 'updateSettings':
+				if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+				if (typeof body.key !== 'string') return;
+				if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
+				this.updateSettings(body.key, body.value);
+				break;
+			case 'cancel':
+				this.cancelGame();
+				break;
+			case 'putStone':
+				if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+				if (typeof body.pos !== 'number') return;
+				if (typeof body.id !== 'string') return;
+				this.putStone(body.pos, body.id);
+				break;
 			case 'claimTimeIsUp': this.claimTimeIsUp(); break;
 		}
 	}
 
 	@bindThis
-	private async updateSettings(key: string, value: any) {
+	private async updateSettings(key: string, value: JsonObject) {
 		if (this.user == null) return;
 
 		this.reversiService.updateSettings(this.gameId!, this.user, key, value);
diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts
index 3998a0fd36..6e88939724 100644
--- a/packages/backend/src/server/api/stream/channels/reversi.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi.ts
@@ -5,6 +5,7 @@
 
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class ReversiChannel extends Channel {
@@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
 	}
 
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 6a4ad22460..fcfa26c38b 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class RoleTimelineChannel extends Channel {
@@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.roleId = params.roleId as string;
+	public async init(params: JsonObject) {
+		if (typeof params.roleId !== 'string') return;
+		this.roleId = params.roleId;
 
 		this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
 	}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
index eb4d8c9992..6258afba35 100644
--- a/packages/backend/src/server/api/stream/channels/server-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -6,6 +6,7 @@
 import Xev from 'xev';
 import { Injectable } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 const ev = new Xev();
@@ -22,19 +23,20 @@ class ServerStatsChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
+	public async init(params: JsonObject) {
 		ev.addListener('serverStats', this.onStats);
 	}
 
 	@bindThis
-	private onStats(stats: any) {
+	private onStats(stats: JsonObject) {
 		this.send('stats', stats);
 	}
 
 	@bindThis
-	public onMessage(type: string, body: any) {
+	public onMessage(type: string, body: JsonValue) {
 		switch (type) {
 			case 'requestLog':
+				if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
 				ev.once(`serverStatsLog:${body.id}`, statsLog => {
 					this.send('statsLog', statsLog);
 				});
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 14b30a157c..4f38351e94 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
 class UserListChannel extends Channel {
@@ -36,10 +37,11 @@ class UserListChannel extends Channel {
 	}
 
 	@bindThis
-	public async init(params: any) {
-		this.listId = params.listId as string;
-		this.withFiles = params.withFiles ?? false;
-		this.withRenotes = params.withRenotes ?? true;
+	public async init(params: JsonObject) {
+		if (typeof params.listId !== 'string') return;
+		this.listId = params.listId;
+		this.withFiles = !!(params.withFiles ?? false);
+		this.withRenotes = !!(params.withRenotes ?? true);
 
 		// Check existence and owner
 		const listExist = await this.userListsRepository.exists({

From 615e60f25cde9115f0e044200e8adcab5eb5004d Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Fri, 19 Jul 2024 09:52:39 +0900
Subject: [PATCH 2/9] chore: modernize issue template (#14263)

---
 .github/ISSUE_TEMPLATE/01_bug-report.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml
index ac2b39cc12..315e712c30 100644
--- a/.github/ISSUE_TEMPLATE/01_bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml
@@ -53,8 +53,8 @@ body:
         Examples:
           * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4
           * Browser: Chrome 113.0.5672.126
-          * Server URL: misskey.io
-          * Misskey: 13.x.x
+          * Server URL: misskey.example.com
+          * Misskey: 2024.x.x
       value: |
         * Model and OS of the device(s):
         * Browser:
@@ -74,11 +74,11 @@ body:
 
         Examples:
           * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment
-          * Misskey: 13.x.x
+          * Misskey: 2024.x.x
           * Node: 20.x.x
           * PostgreSQL: 15.x.x
           * Redis: 7.x.x
-          * OS and Architecture: Ubuntu 22.04.2 LTS aarch64
+          * OS and Architecture: Ubuntu 24.04.2 LTS aarch64
       value: |
         * Installation Method or Hosting Service:
         * Misskey:

From 54d0a4637847aa13700b40ecb1d5edabd048cc2c Mon Sep 17 00:00:00 2001
From: taichan <40626578+tai-cha@users.noreply.github.com>
Date: Fri, 19 Jul 2024 09:53:49 +0900
Subject: [PATCH 3/9] =?UTF-8?q?fix(frontend):=20=E5=80=8B=E4=BA=BA?=
 =?UTF-8?q?=E5=AE=9B=E3=81=A6=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0?=
 =?UTF-8?q?=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=81=8C=E5=8D=B3=E6=99=82?=
 =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=20(#14260)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): 個人向けお知らせが即時ダイアログで出ない問題

* Update CHANGELOG
---
 CHANGELOG.md                            |  1 +
 packages/frontend/src/boot/main-boot.ts | 10 ++++++++--
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14d638ebe1..1e06f16bdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@
   (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574)
 - Fix: Twitchの埋め込みが開けない問題を修正
 - Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
+- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
 
 ### Server
 - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index d327016317..2a549d1e8b 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -5,6 +5,7 @@
 
 import { createApp, defineAsyncComponent, markRaw } from 'vue';
 import { common } from './common.js';
+import type * as Misskey from 'misskey-js';
 import { ui } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
@@ -113,7 +114,7 @@ export async function mainBoot() {
 			});
 		}
 
-		stream.on('announcementCreated', (ev) => {
+		function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
 			const announcement = ev.announcement;
 			if (announcement.display === 'dialog') {
 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
@@ -122,7 +123,9 @@ export async function mainBoot() {
 					closed: () => dispose(),
 				});
 			}
-		});
+		}
+
+		stream.on('announcementCreated', onAnnouncementCreated);
 
 		if ($i.isDeleted) {
 			alert({
@@ -315,6 +318,9 @@ export async function mainBoot() {
 			updateAccount({ hasUnreadAnnouncement: false });
 		});
 
+		// 個人宛てお知らせが発行されたとき
+		main.on('announcementCreated', onAnnouncementCreated);
+
 		// トークンが再生成されたとき
 		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		main.on('myTokenRegenerated', () => {

From 1f24a8cb5a307c3ff621577189a2a618b9dcfdc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 19 Jul 2024 09:57:01 +0900
Subject: [PATCH 4/9] =?UTF-8?q?enhance(frontend):=20=E3=82=BB=E3=83=B3?=
 =?UTF-8?q?=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96=E3=81=AA=E3=83=A1=E3=83=87?=
 =?UTF-8?q?=E3=82=A3=E3=82=A2=E3=82=92=E9=96=8B=E3=81=8F=E9=9A=9B=E3=81=AB?=
 =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0?=
 =?UTF-8?q?=E3=82=92=E5=87=BA=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
 =?UTF-8?q?=20(#14115)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): センシティブなメディアを開く際に確認ダイアログを出せるように

* Update Changelog
---
 CHANGELOG.md                                  |  1 +
 locales/index.d.ts                            |  8 ++++++
 locales/ja-JP.yml                             |  2 ++
 .../frontend/src/components/MkMediaAudio.vue  | 14 +++++++++-
 .../frontend/src/components/MkMediaBanner.vue | 26 ++++++++++++-------
 .../frontend/src/components/MkMediaImage.vue  | 12 ++++++++-
 .../frontend/src/components/MkMediaList.vue   |  8 +++---
 .../frontend/src/components/MkMediaVideo.vue  | 14 +++++++++-
 .../frontend/src/pages/settings/general.vue   |  3 +++
 packages/frontend/src/store.ts                |  4 +++
 10 files changed, 75 insertions(+), 17 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e06f16bdf..7cf77d6083 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@
   (Cherry-picked from https://github.com/taiyme/misskey/pull/238)
 - Enhance: AiScriptを0.19.0にアップデート
 - Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
+- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
 - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
 - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
 - Fix: リバーシの対局を正しく共有できないことがある問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 694ee53a1f..55c65f2aed 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5008,6 +5008,14 @@ export interface Locale extends ILocale {
      * もう一度お試しください。
      */
     "tryAgain": string;
+    /**
+     * センシティブなメディアを表示するとき確認する
+     */
+    "confirmWhenRevealingSensitiveMedia": string;
+    /**
+     * センシティブなメディアです。表示しますか?
+     */
+    "sensitiveMediaRevealConfirm": string;
     "_delivery": {
         /**
          * 配信状態
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index bb3999f0e3..3ca4b46682 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1248,6 +1248,8 @@ noDescription: "説明文はありません"
 alwaysConfirmFollow: "フォローの際常に確認する"
 inquiry: "お問い合わせ"
 tryAgain: "もう一度お試しください。"
+confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
+sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
 
 _delivery:
   status: "配信状態"
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 582cf238c0..a080550ddf 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@contextmenu.stop
 	@keydown.stop
 >
-	<button v-if="hide" :class="$style.hidden" @click="hide = false">
+	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
 			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
 			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@@ -156,6 +156,18 @@ const audioEl = shallowRef<HTMLAudioElement>();
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
 
+async function show() {
+	if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.ts.sensitiveMediaRevealConfirm,
+		});
+		if (canceled) return;
+	}
+
+	hide.value = false;
+}
+
 // Menu
 const menuShowing = ref(false);
 
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index a219848b7f..11995e1f3b 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
-	<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
+	<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show">
 		<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
 		<b>{{ i18n.ts.sensitive }}</b>
 		<span>{{ i18n.ts.clickToShow }}</span>
@@ -24,24 +24,30 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, watch, ref } from 'vue';
+import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
+import { defaultStore } from '@/store.js';
+import * as os from '@/os.js';
 import MkMediaAudio from '@/components/MkMediaAudio.vue';
 
-const props = withDefaults(defineProps<{
+const props = defineProps<{
 	media: Misskey.entities.DriveFile;
-}>(), {
-});
+}>();
 
-const audioEl = shallowRef<HTMLAudioElement>();
 const hide = ref(true);
 
-watch(audioEl, () => {
-	if (audioEl.value) {
-		audioEl.value.volume = 0.3;
+async function show() {
+	if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.ts.sensitiveMediaRevealConfirm,
+		});
+		if (canceled) return;
 	}
-});
+
+	hide.value = false;
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 82f36fe5c4..0d1409e2c8 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -83,11 +83,21 @@ const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
 		: props.image.thumbnailUrl,
 );
 
-function onclick() {
+async function onclick(ev: MouseEvent) {
 	if (!props.controls) {
 		return;
 	}
+
 	if (hide.value) {
+		ev.stopPropagation();
+		if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+			const { canceled } = await os.confirm({
+				type: 'question',
+				text: i18n.ts.sensitiveMediaRevealConfirm,
+			});
+			if (canceled) return;
+		}
+
 		hide.value = false;
 	}
 }
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 24b177d255..2300802dcf 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -138,15 +138,13 @@ onMounted(() => {
 		pswpModule: PhotoSwipe,
 	});
 
-	lightbox.on('itemData', (ev) => {
-		const { itemData } = ev;
-
+	lightbox.addFilter('itemData', (itemData) => {
 		// element is children
 		const { element } = itemData;
 
 		const id = element?.dataset.id;
 		const file = props.mediaList.find(media => media.id === id);
-		if (!file) return;
+		if (!file) return itemData;
 
 		itemData.src = file.url;
 		itemData.w = Number(file.properties.width);
@@ -158,6 +156,8 @@ onMounted(() => {
 		itemData.alt = file.comment ?? file.name;
 		itemData.comment = file.comment ?? file.name;
 		itemData.thumbCropped = true;
+
+		return itemData;
 	});
 
 	lightbox.on('uiRegister', () => {
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 0ec0039df4..7c5a365148 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@contextmenu.stop
 	@keydown.stop
 >
-	<button v-if="hide" :class="$style.hidden" @click="hide = false">
+	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
 			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
 			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@@ -176,6 +176,18 @@ function hasFocus() {
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
 
+async function show() {
+	if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		const { canceled } = await os.confirm({
+			type: 'question',
+			text: i18n.ts.sensitiveMediaRevealConfirm,
+		});
+		if (canceled) return;
+	}
+
+	hide.value = false;
+}
+
 // Menu
 const menuShowing = ref(false);
 
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index cfc63f2a08..9e429f8dbd 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -169,6 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
 				<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
 				<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
+				<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch>
 			</div>
 			<MkSelect v-model="serverDisconnectedBehavior">
 				<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@@ -315,6 +316,7 @@ const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enabl
 const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
 const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
 const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
+const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -357,6 +359,7 @@ watch([
 	disableStreamingTimeline,
 	enableSeasonalScreenEffect,
 	alwaysConfirmFollow,
+	confirmWhenRevealingSensitiveMedia,
 ], async () => {
 	await reloadAsk();
 });
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 9cb2742069..dbf6b8716f 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -454,6 +454,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	confirmWhenRevealingSensitiveMedia: {
+		where: 'device',
+		default: false,
+	},
 
 	sound_masterVolume: {
 		where: 'device',

From 6920f0fa7e5b0919572ee4ebaeb92a617ec344d1 Mon Sep 17 00:00:00 2001
From: woxtu <woxtup@gmail.com>
Date: Fri, 19 Jul 2024 10:05:34 +0900
Subject: [PATCH 5/9] Disable ESLint for migration files (#14262)

---
 packages/backend/eslint.config.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js
index 318b7fd340..4fd9f0cd51 100644
--- a/packages/backend/eslint.config.js
+++ b/packages/backend/eslint.config.js
@@ -4,7 +4,7 @@ import sharedConfig from '../shared/eslint.config.js';
 export default [
 	...sharedConfig,
 	{
-		ignores: ['**/node_modules', 'built', '@types/**/*'],
+		ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
 	},
 	{
 		files: ['**/*.ts', '**/*.tsx'],

From 56a43dc01d15f1d41bbda9a973da18294389d7c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 19 Jul 2024 10:11:44 +0900
Subject: [PATCH 6/9] =?UTF-8?q?fix(frontend):=20blurhash=E3=81=8C=E7=84=A1?=
 =?UTF-8?q?=E3=81=84=E5=A0=B4=E5=90=88=E3=81=AB=E4=BD=95=E3=82=82=E5=87=BA?=
 =?UTF-8?q?=E5=8A=9B=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#14250)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): blurhashが無い場合に何も出力されないのを修正

* Update Changelog

* Update packages/frontend/src/components/MkImgWithBlurhash.vue

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* attempt to fix test

* Update packages/frontend/src/components/MkImgWithBlurhash.vue

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* attempt to ignore test

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
---
 CHANGELOG.md                                           |  1 +
 packages/frontend/src/components/MkImgWithBlurhash.vue | 10 +++++++---
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cf77d6083..b020c214bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,7 @@
 - Fix: Twitchの埋め込みが開けない問題を修正
 - Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
 - Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
+- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
 
 ### Server
 - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 617404f5c4..8d301f16bd 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -151,22 +151,26 @@ function drawImage(bitmap: CanvasImageSource) {
 }
 
 function drawAvg() {
-	if (!canvas.value || !props.hash) return;
+	if (!canvas.value) return;
+
+	const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888';
 
 	const ctx = canvas.value.getContext('2d');
 	if (!ctx) return;
 
 	// avgColorでお茶をにごす
 	ctx.beginPath();
-	ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
+	ctx.fillStyle = color;
 	ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
 }
 
 async function draw() {
-	if (props.hash == null) return;
+	if (import.meta.env.MODE === 'test' && props.hash == null) return;
 
 	drawAvg();
 
+	if (props.hash == null) return;
+
 	if (props.onlyAvgColor) return;
 
 	const work = await canvasPromise;

From efb04293bb01fba6d7d31278d6a8546eae2d9503 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Fri, 19 Jul 2024 16:47:12 +0900
Subject: [PATCH 7/9] docs(misskey-js): fix broken i-want-you image link in
 README.md (#14265)

---
 packages/misskey-js/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/misskey-js/README.md b/packages/misskey-js/README.md
index 63d4b36c56..4753e2434b 100644
--- a/packages/misskey-js/README.md
+++ b/packages/misskey-js/README.md
@@ -154,5 +154,5 @@ stream.on('_disconnected_', () => {
 ---
 
 <div align="center">
-	<a href="https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md"><img src="https://raw.githubusercontent.com/misskey-dev/assets/main/i-want-you.png" width="300"></a>
+	<a href="https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md"><img src="https://assets.misskey-hub.net/public/i-want-you.png" width="300"></a>
 </div>

From 337b42bcb179bdfb993888ed94342a0158e8f3cb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 20 Jul 2024 21:33:20 +0900
Subject: [PATCH 8/9] revert 5f88d56d96
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

バグがある(かつすぐに修正できそうにない) & まだレビュー途中で意図せずマージされたため
---
 .config/docker_example.yml                    |   6 +-
 .config/example.yml                           |   8 +-
 .devcontainer/devcontainer.yml                |   8 +-
 CHANGELOG.md                                  |   8 -
 CONTRIBUTING.md                               |   2 +-
 chart/files/default.yml                       |   8 +-
 .../migration/1708980134301-APMultipleKeys.js |  39 ----
 .../migration/1709242519122-HttpSignImplLv.js |  16 --
 .../1709269211718-APMultipleKeysFix1.js       |  16 --
 packages/backend/package.json                 |   2 +-
 .../backend/src/@types/http-signature.d.ts    |  82 +++++++
 packages/backend/src/const.ts                 |   5 -
 .../backend/src/core/AccountUpdateService.ts  |  27 +--
 .../src/core/CreateSystemUserService.ts       |   7 +-
 .../src/core/FetchInstanceMetadataService.ts  |  61 ++---
 .../backend/src/core/GlobalEventService.ts    |   1 -
 .../backend/src/core/HttpRequestService.ts    |   2 +-
 packages/backend/src/core/QueueService.ts     |  17 +-
 packages/backend/src/core/RelayService.ts     |  13 +-
 packages/backend/src/core/SignupService.ts    |  22 +-
 .../backend/src/core/UserKeypairService.ts    | 155 +------------
 .../backend/src/core/UserSuspendService.ts    |  66 ++++--
 packages/backend/src/core/WebfingerService.ts |   2 +-
 .../core/activitypub/ApDbResolverService.ts   | 178 ++++-----------
 .../activitypub/ApDeliverManagerService.ts    |  95 +-------
 .../src/core/activitypub/ApInboxService.ts    |  11 +-
 .../src/core/activitypub/ApRendererService.ts |  21 +-
 .../src/core/activitypub/ApRequestService.ts  | 212 +++++++++++-------
 .../src/core/activitypub/ApResolverService.ts |   8 +-
 .../src/core/activitypub/misc/contexts.ts     |   1 -
 .../activitypub/models/ApPersonService.ts     | 114 ++--------
 packages/backend/src/core/activitypub/type.ts |  11 +-
 .../core/entities/InstanceEntityService.ts    |   1 -
 packages/backend/src/misc/cache.ts            |   3 -
 packages/backend/src/misc/gen-key-pair.ts     |  43 +++-
 packages/backend/src/models/Instance.ts       |   5 -
 packages/backend/src/models/UserKeypair.ts    |  24 +-
 packages/backend/src/models/UserPublickey.ts  |  14 +-
 .../models/json-schema/federation-instance.ts |   4 -
 .../src/queue/QueueProcessorService.ts        |   8 +-
 .../processors/DeliverProcessorService.ts     |  38 ++--
 .../queue/processors/InboxProcessorService.ts | 132 +++++------
 packages/backend/src/queue/types.ts           |  23 +-
 .../src/server/ActivityPubServerService.ts    |  91 +++++---
 .../src/server/NodeinfoServerService.ts       |   7 -
 .../endpoints/admin/queue/inbox-delayed.ts    |   3 +-
 packages/backend/test/e2e/timelines.ts        |  10 +-
 packages/backend/test/misc/mock-resolver.ts   |   2 -
 .../test/unit/FetchInstanceMetadataService.ts |  23 +-
 packages/backend/test/unit/ap-request.ts      |  90 +++-----
 packages/misskey-js/src/autogen/types.ts      |   1 -
 pnpm-lock.yaml                                |  44 ++--
 52 files changed, 691 insertions(+), 1099 deletions(-)
 delete mode 100644 packages/backend/migration/1708980134301-APMultipleKeys.js
 delete mode 100644 packages/backend/migration/1709242519122-HttpSignImplLv.js
 delete mode 100644 packages/backend/migration/1709269211718-APMultipleKeysFix1.js
 create mode 100644 packages/backend/src/@types/http-signature.d.ts

diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index bd0ad2872a..d347882d1a 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -164,12 +164,12 @@ id: 'aidx'
 #clusterLimit: 1
 
 # Job concurrency per worker
-# deliverJobConcurrency: 16
-# inboxJobConcurrency: 4
+# deliverJobConcurrency: 128
+# inboxJobConcurrency: 16
 
 # Job rate limiter
 # deliverJobPerSec: 128
-# inboxJobPerSec: 64
+# inboxJobPerSec: 32
 
 # Job attempts
 # deliverJobMaxAttempts: 12
diff --git a/.config/example.yml b/.config/example.yml
index 0d525f61c4..b11cbd1373 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -230,15 +230,15 @@ id: 'aidx'
 #clusterLimit: 1
 
 # Job concurrency per worker
-#deliverJobConcurrency: 16
-#inboxJobConcurrency: 4
+#deliverJobConcurrency: 128
+#inboxJobConcurrency: 16
 #relationshipJobConcurrency: 16
 # What's relationshipJob?:
 #  Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
 
 # Job rate limiter
-#deliverJobPerSec: 1024
-#inboxJobPerSec: 64
+#deliverJobPerSec: 128
+#inboxJobPerSec: 32
 #relationshipJobPerSec: 64
 
 # Job attempts
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index d74d741e02..beefcfd0a2 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -157,12 +157,12 @@ id: 'aidx'
 #clusterLimit: 1
 
 # Job concurrency per worker
-# deliverJobConcurrency: 16
-# inboxJobConcurrency: 4
+# deliverJobConcurrency: 128
+# inboxJobConcurrency: 16
 
 # Job rate limiter
-# deliverJobPerSec: 1024
-# inboxJobPerSec: 64
+# deliverJobPerSec: 128
+# inboxJobPerSec: 32
 
 # Job attempts
 # deliverJobMaxAttempts: 12
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b020c214bc..f429033aa0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,14 +8,6 @@
 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
 - Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
   - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
-- Feat: 連合に使うHTTP SignaturesがEd25519鍵に対応するように #13464
-  - Ed25519署名に対応するサーバーが増えると、deliverで要求されるサーバーリソースが削減されます
-	- ジョブキューのconfig設定のデフォルト値を変更しました。  
-	  default.ymlでジョブキューの並列度を設定している場合は、従前よりもconcurrencyの値をより下げるとパフォーマンスが改善する可能性があります。
-		* deliverJobConcurrency: 16 (←128)
-		* deliverJobPerSec: 1024 (←128)
-		* inboxJobConcurrency: 4 (←16)
-		* inboxJobPerSec: 64 (←32)
 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9a56345e6e..afc21ea594 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -197,7 +197,7 @@ TODO
 ## Environment Variable
 
 - `MISSKEY_CONFIG_YML`: Specify the file path of config.yml instead of default.yml (e.g. `2nd.yml`).
-- `MISSKEY_USE_HTTP`: If it's set true, federation requests (like nodeinfo and webfinger) will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION. (was `MISSKEY_WEBFINGER_USE_HTTP`)
+- `MISSKEY_WEBFINGER_USE_HTTP`: If it's set true, WebFinger requests will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION.
 
 ## Continuous integration
 Misskey uses GitHub Actions for executing automated tests.
diff --git a/chart/files/default.yml b/chart/files/default.yml
index 4017588fa0..f98b8ebfee 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -178,12 +178,12 @@ id: "aidx"
 #clusterLimit: 1
 
 # Job concurrency per worker
-# deliverJobConcurrency: 16
-# inboxJobConcurrency: 4
+# deliverJobConcurrency: 128
+# inboxJobConcurrency: 16
 
 # Job rate limiter
-# deliverJobPerSec: 1024
-# inboxJobPerSec: 64
+# deliverJobPerSec: 128
+# inboxJobPerSec: 32
 
 # Job attempts
 # deliverJobMaxAttempts: 12
diff --git a/packages/backend/migration/1708980134301-APMultipleKeys.js b/packages/backend/migration/1708980134301-APMultipleKeys.js
deleted file mode 100644
index ca55526c6e..0000000000
--- a/packages/backend/migration/1708980134301-APMultipleKeys.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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_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 "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/migration/1709242519122-HttpSignImplLv.js b/packages/backend/migration/1709242519122-HttpSignImplLv.js
deleted file mode 100644
index 7748bae006..0000000000
--- a/packages/backend/migration/1709242519122-HttpSignImplLv.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export class HttpSignImplLv1709242519122 {
-    name = 'HttpSignImplLv1709242519122'
-
-    async up(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "instance" ADD "httpMessageSignaturesImplementationLevel" character varying(16) NOT NULL DEFAULT '00'`);
-    }
-
-    async down(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "httpMessageSignaturesImplementationLevel"`);
-    }
-}
diff --git a/packages/backend/migration/1709269211718-APMultipleKeysFix1.js b/packages/backend/migration/1709269211718-APMultipleKeysFix1.js
deleted file mode 100644
index d2011802f2..0000000000
--- a/packages/backend/migration/1709269211718-APMultipleKeysFix1.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export class APMultipleKeys1709269211718 {
-    name = 'APMultipleKeys1709269211718'
-
-    async up(queryRunner) {
-        await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`);
-    }
-
-    async down(queryRunner) {
-			await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`);
-    }
-}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 893171ebd6..22fdc5cf16 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -79,13 +79,13 @@
 		"@fastify/multipart": "8.3.0",
 		"@fastify/static": "7.0.4",
 		"@fastify/view": "9.1.0",
-		"@misskey-dev/node-http-message-signatures": "0.0.10",
 		"@misskey-dev/sharp-read-bmp": "1.2.0",
 		"@misskey-dev/summaly": "5.1.0",
 		"@napi-rs/canvas": "^0.1.53",
 		"@nestjs/common": "10.3.10",
 		"@nestjs/core": "10.3.10",
 		"@nestjs/testing": "10.3.10",
+		"@peertube/http-signature": "1.7.0",
 		"@sentry/node": "8.13.0",
 		"@sentry/profiling-node": "8.13.0",
 		"@simplewebauthn/server": "10.0.0",
diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts
new file mode 100644
index 0000000000..75b62e55f0
--- /dev/null
+++ b/packages/backend/src/@types/http-signature.d.ts
@@ -0,0 +1,82 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+declare module '@peertube/http-signature' {
+	import type { IncomingMessage, ClientRequest } from 'node:http';
+
+	interface ISignature {
+		keyId: string;
+		algorithm: string;
+		headers: string[];
+		signature: string;
+	}
+
+	interface IOptions {
+		headers?: string[];
+		algorithm?: string;
+		strict?: boolean;
+		authorizationHeaderName?: string;
+	}
+
+	interface IParseRequestOptions extends IOptions {
+		clockSkew?: number;
+	}
+
+	interface IParsedSignature {
+		scheme: string;
+		params: ISignature;
+		signingString: string;
+		algorithm: string;
+		keyId: string;
+	}
+
+	type RequestSignerConstructorOptions =
+		IRequestSignerConstructorOptionsFromProperties |
+		IRequestSignerConstructorOptionsFromFunction;
+
+	interface IRequestSignerConstructorOptionsFromProperties {
+		keyId: string;
+		key: string | Buffer;
+		algorithm?: string;
+	}
+
+	interface IRequestSignerConstructorOptionsFromFunction {
+		sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void;
+	}
+
+	class RequestSigner {
+		constructor(options: RequestSignerConstructorOptions);
+
+		public writeHeader(header: string, value: string): string;
+
+		public writeDateHeader(): string;
+
+		public writeTarget(method: string, path: string): void;
+
+		public sign(cb: (err: any, authz: string) => void): void;
+	}
+
+	interface ISignRequestOptions extends IOptions {
+		keyId: string;
+		key: string;
+		httpVersion?: string;
+	}
+
+	export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
+	export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
+
+	export function sign(request: ClientRequest, options: ISignRequestOptions): boolean;
+	export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean;
+	export function createSigner(): RequestSigner;
+	export function isSigner(obj: any): obj is RequestSigner;
+
+	export function sshKeyToPEM(key: string): string;
+	export function sshKeyFingerprint(key: string): string;
+	export function pemToRsaSSHKey(pem: string, comment: string): string;
+
+	export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
+	export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
+	export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean;
+}
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index c132cc7e7b..4dc689238b 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -9,11 +9,6 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
 export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
 export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
 
-export const REMOTE_USER_CACHE_TTL = 1000 * 60 * 60 * 3; // 3hours
-export const REMOTE_USER_MOVE_COOLDOWN = 1000 * 60 * 60 * 24 * 14; // 14days
-
-export const REMOTE_SERVER_CACHE_TTL = 1000 * 60 * 60 * 3; // 3hours
-
 //#region hard limits
 // If you change DB_* values, you must also change the DB schema.
 
diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts
index ca0864f679..69a57b4854 100644
--- a/packages/backend/src/core/AccountUpdateService.ts
+++ b/packages/backend/src/core/AccountUpdateService.ts
@@ -3,8 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
-import { ModuleRef } from '@nestjs/core';
+import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
 import type { UsersRepository } from '@/models/_.js';
 import type { MiUser } from '@/models/User.js';
@@ -13,44 +12,30 @@ import { RelayService } from '@/core/RelayService.js';
 import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
-import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
 
 @Injectable()
-export class AccountUpdateService implements OnModuleInit {
-	private apDeliverManagerService: ApDeliverManagerService;
+export class AccountUpdateService {
 	constructor(
-		private moduleRef: ModuleRef,
-
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
 		private userEntityService: UserEntityService,
 		private apRendererService: ApRendererService,
+		private apDeliverManagerService: ApDeliverManagerService,
 		private relayService: RelayService,
 	) {
 	}
 
-	async onModuleInit() {
-		this.apDeliverManagerService = this.moduleRef.get(ApDeliverManagerService.name);
-	}
-
 	@bindThis
-	/**
-	 * Deliver account update to followers
-	 * @param userId user id
-	 * @param deliverKey optional. Private key to sign the deliver.
-	 */
-	public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKeyWithPem) {
+	public async publishToFollowers(userId: MiUser['id']) {
 		const user = await this.usersRepository.findOneBy({ id: userId });
 		if (user == null) throw new Error('user not found');
 
 		// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
 		if (this.userEntityService.isLocalUser(user)) {
 			const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
-			await Promise.allSettled([
-				this.apDeliverManagerService.deliverToFollowers(user, content, deliverKey),
-				this.relayService.deliverToRelays(user, content, deliverKey),
-			]);
+			this.apDeliverManagerService.deliverToFollowers(user, content);
+			this.relayService.deliverToRelays(user, content);
 		}
 	}
 }
diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts
index 60ddc9cde2..6c5b0f6a36 100644
--- a/packages/backend/src/core/CreateSystemUserService.ts
+++ b/packages/backend/src/core/CreateSystemUserService.ts
@@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
 import { Inject, Injectable } from '@nestjs/common';
 import bcrypt from 'bcryptjs';
 import { IsNull, DataSource } from 'typeorm';
-import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js';
+import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
 import { MiUser } from '@/models/User.js';
 import { MiUserProfile } from '@/models/UserProfile.js';
 import { IdService } from '@/core/IdService.js';
@@ -38,7 +38,7 @@ export class CreateSystemUserService {
 		// Generate secret
 		const secret = generateNativeUserToken();
 
-		const keyPair = await genRSAAndEd25519KeyPair();
+		const keyPair = await genRsaKeyPair();
 
 		let account!: MiUser;
 
@@ -64,8 +64,9 @@ export class CreateSystemUserService {
 			}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
 
 			await transactionalEntityManager.insert(MiUserKeypair, {
+				publicKey: keyPair.publicKey,
+				privateKey: keyPair.privateKey,
 				userId: account.id,
-				...keyPair,
 			});
 
 			await transactionalEntityManager.insert(MiUserProfile, {
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index dc53c8711d..aa16468ecb 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -15,7 +15,6 @@ import { LoggerService } from '@/core/LoggerService.js';
 import { HttpRequestService } from '@/core/HttpRequestService.js';
 import { bindThis } from '@/decorators.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { REMOTE_SERVER_CACHE_TTL } from '@/const.js';
 import type { DOMWindow } from 'jsdom';
 
 type NodeInfo = {
@@ -25,7 +24,6 @@ type NodeInfo = {
 		version?: unknown;
 	};
 	metadata?: {
-		httpMessageSignaturesImplementationLevel?: unknown,
 		name?: unknown;
 		nodeName?: unknown;
 		nodeDescription?: unknown;
@@ -41,7 +39,6 @@ type NodeInfo = {
 @Injectable()
 export class FetchInstanceMetadataService {
 	private logger: Logger;
-	private httpColon = 'https://';
 
 	constructor(
 		private httpRequestService: HttpRequestService,
@@ -51,7 +48,6 @@ export class FetchInstanceMetadataService {
 		private redisClient: Redis.Redis,
 	) {
 		this.logger = this.loggerService.getLogger('metadata', 'cyan');
-		this.httpColon = process.env.MISSKEY_USE_HTTP?.toLowerCase() === 'true' ? 'http://' : 'https://';
 	}
 
 	@bindThis
@@ -63,7 +59,7 @@ export class FetchInstanceMetadataService {
 		return await this.redisClient.set(
 			`fetchInstanceMetadata:mutex:v2:${host}`, '1',
 			'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
-			'GET', // 古い値を返す(なかったらnull)
+			'GET' // 古い値を返す(なかったらnull)
 		);
 	}
 
@@ -77,24 +73,23 @@ export class FetchInstanceMetadataService {
 	public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
 		const host = instance.host;
 
-		if (!force) {
-			// キャッシュ有効チェックはロック取得前に行う
-			const _instance = await this.federatedInstanceService.fetch(host);
-			const now = Date.now();
-			if (_instance && _instance.infoUpdatedAt != null && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) {
-				this.logger.debug(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`);
-				return;
-			}
-
-			// finallyでunlockされてしまうのでtry内でロックチェックをしない
-			// (returnであってもfinallyは実行される)
-			if (await this.tryLock(host) === '1') {
-				// 1が返ってきていたら他にロックされているという意味なので、何もしない
-				return;
-			}
+		// finallyでunlockされてしまうのでtry内でロックチェックをしない
+		// (returnであってもfinallyは実行される)
+		if (!force && await this.tryLock(host) === '1') {
+			// 1が返ってきていたらロックされているという意味なので、何もしない
+			return;
 		}
 
 		try {
+			if (!force) {
+				const _instance = await this.federatedInstanceService.fetch(host);
+				const now = Date.now();
+				if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
+					// unlock at the finally caluse
+					return;
+				}
+			}
+
 			this.logger.info(`Fetching metadata of ${instance.host} ...`);
 
 			const [info, dom, manifest] = await Promise.all([
@@ -123,14 +118,6 @@ export class FetchInstanceMetadataService {
 				updates.openRegistrations = info.openRegistrations;
 				updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
 				updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
-				if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel && (
-					info.metadata.httpMessageSignaturesImplementationLevel === '01' ||
-					info.metadata.httpMessageSignaturesImplementationLevel === '11'
-				)) {
-					updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel;
-				} else {
-					updates.httpMessageSignaturesImplementationLevel = '00';
-				}
 			}
 
 			if (name) updates.name = name;
@@ -142,12 +129,6 @@ export class FetchInstanceMetadataService {
 			await this.federatedInstanceService.update(instance.id, updates);
 
 			this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
-			this.logger.debug('Updated metadata:', {
-				info: !!info,
-				dom: !!dom,
-				manifest: !!manifest,
-				updates,
-			});
 		} catch (e) {
 			this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
 		} finally {
@@ -160,7 +141,7 @@ export class FetchInstanceMetadataService {
 		this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
 
 		try {
-			const wellknown = await this.httpRequestService.getJson(this.httpColon + instance.host + '/.well-known/nodeinfo')
+			const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
 				.catch(err => {
 					if (err.statusCode === 404) {
 						throw new Error('No nodeinfo provided');
@@ -203,7 +184,7 @@ export class FetchInstanceMetadataService {
 	private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> {
 		this.logger.info(`Fetching HTML of ${instance.host} ...`);
 
-		const url = this.httpColon + instance.host;
+		const url = 'https://' + instance.host;
 
 		const html = await this.httpRequestService.getHtml(url);
 
@@ -215,7 +196,7 @@ export class FetchInstanceMetadataService {
 
 	@bindThis
 	private async fetchManifest(instance: MiInstance): Promise<Record<string, unknown> | null> {
-		const url = this.httpColon + instance.host;
+		const url = 'https://' + instance.host;
 
 		const manifestUrl = url + '/manifest.json';
 
@@ -226,7 +207,7 @@ export class FetchInstanceMetadataService {
 
 	@bindThis
 	private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> {
-		const url = this.httpColon + instance.host;
+		const url = 'https://' + instance.host;
 
 		if (doc) {
 			// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
@@ -253,12 +234,12 @@ export class FetchInstanceMetadataService {
 	@bindThis
 	private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
 		if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
-			const url = this.httpColon + instance.host;
+			const url = 'https://' + instance.host;
 			return (new URL(manifest.icons[0].src, url)).href;
 		}
 
 		if (doc) {
-			const url = this.httpColon + instance.host;
+			const url = 'https://' + instance.host;
 
 			// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
 			const links = Array.from(doc.getElementsByTagName('link')).reverse();
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 312bcfb3b5..87aa70713e 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -249,7 +249,6 @@ 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']; };
 }
 
 type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts
index 4249c158d7..7f3cac7c58 100644
--- a/packages/backend/src/core/HttpRequestService.ts
+++ b/packages/backend/src/core/HttpRequestService.ts
@@ -70,7 +70,7 @@ export class HttpRequestService {
 			localAddress: config.outgoingAddress,
 		});
 
-		const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 16);
+		const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
 
 		this.httpAgent = config.proxy
 			? new HttpProxyAgent({
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index dd3f2182b4..80827a500b 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -13,6 +13,7 @@ import type { Config } from '@/config.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 import type {
 	DbJobData,
 	DeliverJobData,
@@ -32,7 +33,7 @@ import type {
 	UserWebhookDeliverQueue,
 	SystemWebhookDeliverQueue,
 } from './QueueModule.js';
-import { genRFC3230DigestHeader, type PrivateKeyWithPem, type ParsedSignature } from '@misskey-dev/node-http-message-signatures';
+import type httpSignature from '@peertube/http-signature';
 import type * as Bull from 'bullmq';
 
 @Injectable()
@@ -89,21 +90,21 @@ export class QueueService {
 	}
 
 	@bindThis
-	public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKeyWithPem) {
+	public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
 		if (content == null) return null;
 		if (to == null) return null;
 
 		const contentBody = JSON.stringify(content);
+		const digest = ApRequestCreator.createDigest(contentBody);
 
 		const data: DeliverJobData = {
 			user: {
 				id: user.id,
 			},
 			content: contentBody,
-			digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'),
+			digest,
 			to,
 			isSharedInbox,
-			privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem },
 		};
 
 		return this.deliverQueue.add(to, data, {
@@ -121,13 +122,13 @@ export class QueueService {
 	 * @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
 	 * @param content IActivity | null
 	 * @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
-	 * @param forceMainKey boolean | undefined, force to use main (rsa) key
 	 * @returns void
 	 */
 	@bindThis
-	public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>, privateKey?: PrivateKeyWithPem) {
+	public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
 		if (content == null) return null;
 		const contentBody = JSON.stringify(content);
+		const digest = ApRequestCreator.createDigest(contentBody);
 
 		const opts = {
 			attempts: this.config.deliverJobMaxAttempts ?? 12,
@@ -143,9 +144,9 @@ export class QueueService {
 			data: {
 				user,
 				content: contentBody,
+				digest,
 				to: d[0],
 				isSharedInbox: d[1],
-				privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem },
 			} as DeliverJobData,
 			opts,
 		})));
@@ -154,7 +155,7 @@ export class QueueService {
 	}
 
 	@bindThis
-	public inbox(activity: IActivity, signature: ParsedSignature | null) {
+	public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
 		const data = {
 			activity: activity,
 			signature,
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index ad01f98902..8dd3d64f5b 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -16,8 +16,6 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { DI } from '@/di-symbols.js';
 import { deepClone } from '@/misc/clone.js';
 import { bindThis } from '@/decorators.js';
-import { UserKeypairService } from './UserKeypairService.js';
-import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
 
 const ACTOR_USERNAME = 'relay.actor' as const;
 
@@ -36,7 +34,6 @@ export class RelayService {
 		private queueService: QueueService,
 		private createSystemUserService: CreateSystemUserService,
 		private apRendererService: ApRendererService,
-		private userKeypairService: UserKeypairService,
 	) {
 		this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
 	}
@@ -114,7 +111,7 @@ export class RelayService {
 	}
 
 	@bindThis
-	public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKeyWithPem): Promise<void> {
+	public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise<void> {
 		if (activity == null) return;
 
 		const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
@@ -124,9 +121,11 @@ export class RelayService {
 
 		const copy = deepClone(activity);
 		if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
-		privateKey = privateKey ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id);
-		const signed = await this.apRendererService.attachLdSignature(copy, privateKey);
 
-		this.queueService.deliverMany(user, signed, new Map(relays.map(({ inbox }) => [inbox, false])), privateKey);
+		const signed = await this.apRendererService.attachLdSignature(copy, user);
+
+		for (const relay of relays) {
+			this.queueService.deliver(user, signed, relay.inbox, false);
+		}
 	}
 }
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 54c6170062..5522ecd6cc 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { generateKeyPair } from 'node:crypto';
 import { Inject, Injectable } from '@nestjs/common';
 import bcrypt from 'bcryptjs';
 import { DataSource, IsNull } from 'typeorm';
@@ -20,7 +21,6 @@ import { bindThis } from '@/decorators.js';
 import UsersChart from '@/core/chart/charts/users.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { MetaService } from '@/core/MetaService.js';
-import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js';
 
 @Injectable()
 export class SignupService {
@@ -93,7 +93,22 @@ export class SignupService {
 			}
 		}
 
-		const keyPair = await genRSAAndEd25519KeyPair();
+		const keyPair = await new Promise<string[]>((res, rej) =>
+			generateKeyPair('rsa', {
+				modulusLength: 2048,
+				publicKeyEncoding: {
+					type: 'spki',
+					format: 'pem',
+				},
+				privateKeyEncoding: {
+					type: 'pkcs8',
+					format: 'pem',
+					cipher: undefined,
+					passphrase: undefined,
+				},
+			}, (err, publicKey, privateKey) =>
+				err ? rej(err) : res([publicKey, privateKey]),
+			));
 
 		let account!: MiUser;
 
@@ -116,8 +131,9 @@ export class SignupService {
 			}));
 
 			await transactionalEntityManager.save(new MiUserKeypair({
+				publicKey: keyPair[0],
+				privateKey: keyPair[1],
 				userId: account.id,
-				...keyPair,
 			}));
 
 			await transactionalEntityManager.save(new MiUserProfile({
diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts
index aa90f1e209..51ac99179a 100644
--- a/packages/backend/src/core/UserKeypairService.ts
+++ b/packages/backend/src/core/UserKeypairService.ts
@@ -5,184 +5,41 @@
 
 import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 import * as Redis from 'ioredis';
-import { genEd25519KeyPair, importPrivateKey, PrivateKey, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
 import type { MiUser } from '@/models/User.js';
 import type { UserKeypairsRepository } from '@/models/_.js';
-import { RedisKVCache, MemoryKVCache } from '@/misc/cache.js';
+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 { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { webcrypto } from 'node:crypto';
 
 @Injectable()
 export class UserKeypairService implements OnApplicationShutdown {
-	private keypairEntityCache: RedisKVCache<MiUserKeypair>;
-	private privateKeyObjectCache: MemoryKVCache<webcrypto.CryptoKey>;
+	private cache: RedisKVCache<MiUserKeypair>;
 
 	constructor(
 		@Inject(DI.redis)
 		private redisClient: Redis.Redis,
-		@Inject(DI.redisForSub)
-		private redisForSub: Redis.Redis,
+
 		@Inject(DI.userKeypairsRepository)
 		private userKeypairsRepository: UserKeypairsRepository,
-
-		private globalEventService: GlobalEventService,
-		private userEntityService: UserEntityService,
 	) {
-		this.keypairEntityCache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
+		this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
 			lifetime: 1000 * 60 * 60 * 24, // 24h
 			memoryCacheLifetime: Infinity,
 			fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
 			toRedisConverter: (value) => JSON.stringify(value),
 			fromRedisConverter: (value) => JSON.parse(value),
 		});
-		this.privateKeyObjectCache = new MemoryKVCache<webcrypto.CryptoKey>(1000 * 60 * 60 * 1);
-
-		this.redisForSub.on('message', this.onMessage);
 	}
 
 	@bindThis
 	public async getUserKeypair(userId: MiUser['id']): Promise<MiUserKeypair> {
-		return await this.keypairEntityCache.fetch(userId);
+		return await this.cache.fetch(userId);
 	}
 
-	/**
-	 * Get private key [Only PrivateKeyWithPem for queue data etc.]
-	 * @param userIdOrHint user id or MiUserKeypair
-	 * @param preferType
-	 *		If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists.
-	 *		Otherwise, main keypair will be returned.
-	 * @returns
-	 */
-	@bindThis
-	public async getLocalUserPrivateKeyPem(
-		userIdOrHint: MiUser['id'] | MiUserKeypair,
-		preferType?: string,
-	): Promise<PrivateKeyWithPem> {
-		const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint;
-		if (
-			preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase()) &&
-			keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null
-		) {
-			return {
-				keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`,
-				privateKeyPem: keypair.ed25519PrivateKey,
-			};
-		}
-		return {
-			keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`,
-			privateKeyPem: keypair.privateKey,
-		};
-	}
-
-	/**
-	 * Get private key [Only PrivateKey for ap request]
-	 * Using cache due to performance reasons of `crypto.subtle.importKey`
-	 * @param userIdOrHint user id, MiUserKeypair, or PrivateKeyWithPem
-	 * @param preferType
-	 * 		If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists.
-	 *		Otherwise, main keypair will be returned. (ignored if userIdOrHint is PrivateKeyWithPem)
-	 * @returns
-	 */
-	@bindThis
-	public async getLocalUserPrivateKey(
-		userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem,
-		preferType?: string,
-	): Promise<PrivateKey> {
-		if (typeof userIdOrHint === 'object' && 'privateKeyPem' in userIdOrHint) {
-			// userIdOrHint is PrivateKeyWithPem
-			return {
-				keyId: userIdOrHint.keyId,
-				privateKey: await this.privateKeyObjectCache.fetch(userIdOrHint.keyId, async () => {
-					return await importPrivateKey(userIdOrHint.privateKeyPem);
-				}),
-			};
-		}
-
-		const userId = typeof userIdOrHint === 'string' ? userIdOrHint : userIdOrHint.userId;
-		const getKeypair = () => typeof userIdOrHint === 'string' ? this.getUserKeypair(userId) : userIdOrHint;
-
-		if (preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase())) {
-			const keyId = `${this.userEntityService.genLocalUserUri(userId)}#ed25519-key`;
-			const fetched = await this.privateKeyObjectCache.fetchMaybe(keyId, async () => {
-				const keypair = await getKeypair();
-				if (keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) {
-					return await importPrivateKey(keypair.ed25519PrivateKey);
-				}
-				return;
-			});
-			if (fetched) {
-				return {
-					keyId,
-					privateKey: fetched,
-				};
-			}
-		}
-
-		const keyId = `${this.userEntityService.genLocalUserUri(userId)}#main-key`;
-		return {
-			keyId,
-			privateKey: await this.privateKeyObjectCache.fetch(keyId, async () => {
-				const keypair = await getKeypair();
-				return await importPrivateKey(keypair.privateKey);
-			}),
-		};
-	}
-
-	@bindThis
-	public async refresh(userId: MiUser['id']): Promise<void> {
-		return await this.keypairEntityCache.refresh(userId);
-	}
-
-	/**
-	 * If DB has ed25519 keypair, refresh cache and return it.
-	 * If not, create, save and return ed25519 keypair.
-	 * @param userId user id
-	 * @returns MiUserKeypair if keypair is created, void if keypair is already exists
-	 */
-	@bindThis
-	public async refreshAndPrepareEd25519KeyPair(userId: MiUser['id']): Promise<MiUserKeypair | void> {
-		await this.refresh(userId);
-		const keypair = await this.keypairEntityCache.fetch(userId);
-		if (keypair.ed25519PublicKey != null) {
-			return;
-		}
-
-		const ed25519 = await genEd25519KeyPair();
-		await this.userKeypairsRepository.update({ userId }, {
-			ed25519PublicKey: ed25519.publicKey,
-			ed25519PrivateKey: ed25519.privateKey,
-		});
-		this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId });
-		const result = {
-			...keypair,
-			ed25519PublicKey: ed25519.publicKey,
-			ed25519PrivateKey: ed25519.privateKey,
-		};
-		this.keypairEntityCache.set(userId, result);
-		return result;
-	}
-
-	@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.keypairEntityCache.dispose();
+		this.cache.dispose();
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts
index fc5a68c72e..d594a223f4 100644
--- a/packages/backend/src/core/UserSuspendService.ts
+++ b/packages/backend/src/core/UserSuspendService.ts
@@ -3,23 +3,27 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
+import { Not, IsNull } from 'typeorm';
+import type { FollowingsRepository } from '@/models/_.js';
 import type { MiUser } from '@/models/User.js';
+import { QueueService } from '@/core/QueueService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
 import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
-import { UserKeypairService } from './UserKeypairService.js';
-import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
 
 @Injectable()
 export class UserSuspendService {
 	constructor(
+		@Inject(DI.followingsRepository)
+		private followingsRepository: FollowingsRepository,
+
 		private userEntityService: UserEntityService,
+		private queueService: QueueService,
 		private globalEventService: GlobalEventService,
 		private apRendererService: ApRendererService,
-		private userKeypairService: UserKeypairService,
-		private apDeliverManagerService: ApDeliverManagerService,
 	) {
 	}
 
@@ -28,12 +32,28 @@ export class UserSuspendService {
 		this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
 
 		if (this.userEntityService.isLocalUser(user)) {
+			// 知り得る全SharedInboxにDelete配信
 			const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
-			const manager = this.apDeliverManagerService.createDeliverManager(user, content);
-			manager.addAllKnowingSharedInboxRecipe();
-			// process deliver時にはキーペアが消去されているはずなので、ここで挿入する
-			const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main');
-			manager.execute({ privateKey });
+
+			const queue: string[] = [];
+
+			const followings = await this.followingsRepository.find({
+				where: [
+					{ followerSharedInbox: Not(IsNull()) },
+					{ followeeSharedInbox: Not(IsNull()) },
+				],
+				select: ['followerSharedInbox', 'followeeSharedInbox'],
+			});
+
+			const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
+
+			for (const inbox of inboxes) {
+				if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+			}
+
+			for (const inbox of queue) {
+				this.queueService.deliver(user, content, inbox, true);
+			}
 		}
 	}
 
@@ -42,12 +62,28 @@ export class UserSuspendService {
 		this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
 
 		if (this.userEntityService.isLocalUser(user)) {
+			// 知り得る全SharedInboxにUndo Delete配信
 			const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
-			const manager = this.apDeliverManagerService.createDeliverManager(user, content);
-			manager.addAllKnowingSharedInboxRecipe();
-			// process deliver時にはキーペアが消去されているはずなので、ここで挿入する
-			const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main');
-			manager.execute({ privateKey });
+
+			const queue: string[] = [];
+
+			const followings = await this.followingsRepository.find({
+				where: [
+					{ followerSharedInbox: Not(IsNull()) },
+					{ followeeSharedInbox: Not(IsNull()) },
+				],
+				select: ['followerSharedInbox', 'followeeSharedInbox'],
+			});
+
+			const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
+
+			for (const inbox of inboxes) {
+				if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+			}
+
+			for (const inbox of queue) {
+				this.queueService.deliver(user as any, content, inbox, true);
+			}
 		}
 	}
 }
diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts
index aa1144778c..374536a741 100644
--- a/packages/backend/src/core/WebfingerService.ts
+++ b/packages/backend/src/core/WebfingerService.ts
@@ -46,7 +46,7 @@ export class WebfingerService {
 		const m = query.match(mRegex);
 		if (m) {
 			const hostname = m[2];
-			const useHttp = process.env.MISSKEY_USE_HTTP && process.env.MISSKEY_USE_HTTP.toLowerCase() === 'true';
+			const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
 			return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`;
 		}
 
diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts
index 973394683f..f6b70ead44 100644
--- a/packages/backend/src/core/activitypub/ApDbResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts
@@ -5,7 +5,7 @@
 
 import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { MiUser, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
+import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
 import type { Config } from '@/config.js';
 import { MemoryKVCache } from '@/misc/cache.js';
 import type { MiUserPublickey } from '@/models/UserPublickey.js';
@@ -13,12 +13,9 @@ import { CacheService } from '@/core/CacheService.js';
 import type { MiNote } from '@/models/Note.js';
 import { bindThis } from '@/decorators.js';
 import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
-import Logger from '@/logger.js';
 import { getApId } from './type.js';
 import { ApPersonService } from './models/ApPersonService.js';
-import { ApLoggerService } from './ApLoggerService.js';
 import type { IObject } from './type.js';
-import { UtilityService } from '../UtilityService.js';
 
 export type UriParseResult = {
 	/** wether the URI was generated by us */
@@ -38,8 +35,8 @@ export type UriParseResult = {
 
 @Injectable()
 export class ApDbResolverService implements OnApplicationShutdown {
-	private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey[] | null>;
-	private logger: Logger;
+	private publicKeyCache: MemoryKVCache<MiUserPublickey | null>;
+	private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey | null>;
 
 	constructor(
 		@Inject(DI.config)
@@ -56,17 +53,9 @@ export class ApDbResolverService implements OnApplicationShutdown {
 
 		private cacheService: CacheService,
 		private apPersonService: ApPersonService,
-		private apLoggerService: ApLoggerService,
-		private utilityService: UtilityService,
 	) {
-		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey[] | null>(Infinity);
-		this.logger = this.apLoggerService.logger.createSubLogger('db-resolver');
-	}
-
-	private punyHost(url: string): string {
-		const urlObj = new URL(url);
-		const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
-		return host;
+		this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
+		this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
 	}
 
 	@bindThis
@@ -127,141 +116,62 @@ export class ApDbResolverService implements OnApplicationShutdown {
 		}
 	}
 
+	/**
+	 * AP KeyId => Misskey User and Key
+	 */
 	@bindThis
-	private async refreshAndFindKey(userId: MiUser['id'], keyId: string): Promise<MiUserPublickey | null> {
-		this.refreshCacheByUserId(userId);
-		const keys = await this.getPublicKeyByUserId(userId);
-		if (keys == null || !Array.isArray(keys) || keys.length === 0) {
-			this.logger.warn(`No key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`);
-			return null;
-		}
-		const exactKey = keys.find(x => x.keyId === keyId);
-		if (exactKey) return exactKey;
-		this.logger.warn(`No exact key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`);
-		return null;
+	public async getAuthUserFromKeyId(keyId: string): Promise<{
+		user: MiRemoteUser;
+		key: MiUserPublickey;
+	} | null> {
+		const key = await this.publicKeyCache.fetch(keyId, async () => {
+			const key = await this.userPublickeysRepository.findOneBy({
+				keyId,
+			});
+
+			if (key == null) return null;
+
+			return key;
+		}, key => key != null);
+
+		if (key == null) return null;
+
+		const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null;
+		if (user == null) return null;
+		if (user.isDeleted) return null;
+
+		return {
+			user,
+			key,
+		};
 	}
 
 	/**
 	 * AP Actor id => Misskey User and Key
-	 * @param uri AP Actor id
-	 * @param keyId Key id to find. If not specified, main key will be selected.
-	 * @returns
-	 *	1. `null` if the user and key host do not match
-	 *	2. `{ user: null, key: null }` if the user is not found
-	 *	3. `{ user: MiRemoteUser, key: null }` if key is not found
-	 *  4. `{ user: MiRemoteUser, key: MiUserPublickey }` if both are found
 	 */
 	@bindThis
-	public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{
+	public async getAuthUserFromApId(uri: string): Promise<{
 		user: MiRemoteUser;
 		key: MiUserPublickey | null;
-	} | {
-		user: null;
-		key: null;
-	} |
-	null> {
-		if (keyId) {
-			if (this.punyHost(uri) !== this.punyHost(keyId)) {
-				/**
-				 * keyIdはURL形式かつkeyIdのホストはuriのホストと一致するはず
-				 * (ApPersonService.validateActorに由来)
-				 *
-				 * ただ、Mastodonはリプライ関連で他人のトゥートをHTTP Signature署名して送ってくることがある
-				 * そのような署名は有効性に疑問があるので無視することにする
-				 * ここではuriとkeyIdのホストが一致しない場合は無視する
-				 * ハッシュをなくしたkeyIdとuriの同一性を比べてみてもいいが、`uri#*-key`というkeyIdを設定するのが
-				 * 決まりごとというわけでもないため幅を持たせることにする
-				 *
-				 *
-				 * The keyId should be in URL format and its host should match the host of the uri
-				 * (derived from ApPersonService.validateActor)
-				 *
-				 * However, Mastodon sometimes sends toots from other users with HTTP Signature signing for reply-related purposes
-				 * Such signatures are of questionable validity, so we choose to ignore them
-				 * Here, we ignore cases where the hosts of uri and keyId do not match
-				 * We could also compare the equality of keyId without the hash and uri, but since setting a keyId like `uri#*-key`
-				 * is not a strict rule, we decide to allow for some flexibility
-				 */
-				this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`);
-				return null;
-			}
-		}
+	} | null> {
+		const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser;
+		if (user.isDeleted) return null;
 
-		const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser;
-		if (user.isDeleted) return { user: null, key: null };
-
-		const keys = await this.getPublicKeyByUserId(user.id);
-
-		if (keys == null || !Array.isArray(keys) || keys.length === 0) {
-			this.logger.warn(`No key found uri=${uri} userId=${user.id} keys=${JSON.stringify(keys)}`);
-			return { user, key: null };
-		}
-
-		if (!keyId) {
-			// Choose the main-like
-			const mainKey = keys.find(x => {
-				try {
-					const url = new URL(x.keyId);
-					const path = url.pathname.split('/').pop()?.toLowerCase();
-					if (url.hash) {
-						if (url.hash.toLowerCase().includes('main')) {
-							return true;
-						}
-					} else if (path?.includes('main') || path === 'publickey') {
-						return true;
-					}
-				} catch { /* noop */ }
-
-				return false;
-			});
-			return { user, key: mainKey ?? keys[0] };
-		}
-
-		const exactKey = keys.find(x => x.keyId === keyId);
-		if (exactKey) return { user, key: exactKey };
-
-		/**
-		 * keyIdで見つからない場合、まずはキャッシュを更新して再取得
-		 * If not found with keyId, update cache and reacquire
-		 */
-		const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id);
-		if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) {
-			const exactKey = await this.refreshAndFindKey(user.id, keyId);
-			if (exactKey) return { user, key: exactKey };
-		}
-
-		/**
-		 * lastFetchedAtでの更新制限を弱めて再取得
-		 * Reacquisition with weakened update limit at lastFetchedAt
-		 */
-		if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) {
-			this.logger.info(`Fetching user to find public key uri=${uri} userId=${user.id} keyId=${keyId}`);
-			const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0);
-			if (renewed == null || renewed.isDeleted) return null;
-
-			return { user, key: await this.refreshAndFindKey(user.id, keyId) };
-		}
-
-		this.logger.warn(`No key found uri=${uri} userId=${user.id} keyId=${keyId}`);
-		return { user, key: null };
-	}
-
-	@bindThis
-	public async getPublicKeyByUserId(userId: MiUser['id']): Promise<MiUserPublickey[] | null> {
-		return await this.publicKeyByUserIdCache.fetch(
-			userId,
-			() => this.userPublickeysRepository.find({ where: { userId } }),
+		const key = await this.publicKeyByUserIdCache.fetch(
+			user.id,
+			() => this.userPublickeysRepository.findOneBy({ userId: user.id }),
 			v => v != null,
 		);
-	}
 
-	@bindThis
-	public refreshCacheByUserId(userId: MiUser['id']): void {
-		this.publicKeyByUserIdCache.delete(userId);
+		return {
+			user,
+			key,
+		};
 	}
 
 	@bindThis
 	public dispose(): void {
+		this.publicKeyCache.dispose();
 		this.publicKeyByUserIdCache.dispose();
 	}
 
diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts
index db3302e6ff..5d07cd8e8f 100644
--- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts
+++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts
@@ -9,14 +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 { AccountUpdateService } from '@/core/AccountUpdateService.js';
-import type Logger from '@/logger.js';
-import { UserKeypairService } from '../UserKeypairService.js';
-import { ApLoggerService } from './ApLoggerService.js';
-import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
 
 interface IRecipe {
 	type: string;
@@ -31,19 +27,12 @@ interface IDirectRecipe extends IRecipe {
 	to: MiRemoteUser;
 }
 
-interface IAllKnowingSharedInboxRecipe extends IRecipe {
-	type: 'AllKnowingSharedInbox';
-}
-
 const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
 	recipe.type === 'Followers';
 
 const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
 	recipe.type === 'Direct';
 
-const isAllKnowingSharedInbox = (recipe: IRecipe): recipe is IAllKnowingSharedInboxRecipe =>
-	recipe.type === 'AllKnowingSharedInbox';
-
 class DeliverManager {
 	private actor: ThinUser;
 	private activity: IActivity | null;
@@ -51,18 +40,16 @@ class DeliverManager {
 
 	/**
 	 * Constructor
-	 * @param userKeypairService
+	 * @param userEntityService
 	 * @param followingsRepository
 	 * @param queueService
 	 * @param actor Actor
 	 * @param activity Activity to deliver
 	 */
 	constructor(
-		private userKeypairService: UserKeypairService,
+		private userEntityService: UserEntityService,
 		private followingsRepository: FollowingsRepository,
 		private queueService: QueueService,
-		private accountUpdateService: AccountUpdateService,
-		private logger: Logger,
 
 		actor: { id: MiUser['id']; host: null; },
 		activity: IActivity | null,
@@ -104,18 +91,6 @@ class DeliverManager {
 		this.addRecipe(recipe);
 	}
 
-	/**
-	 * Add recipe for all-knowing shared inbox deliver
-	 */
-	@bindThis
-	public addAllKnowingSharedInboxRecipe(): void {
-		const deliver: IAllKnowingSharedInboxRecipe = {
-			type: 'AllKnowingSharedInbox',
-		};
-
-		this.addRecipe(deliver);
-	}
-
 	/**
 	 * Add recipe
 	 * @param recipe Recipe
@@ -129,44 +104,11 @@ class DeliverManager {
 	 * Execute delivers
 	 */
 	@bindThis
-	public async execute(opts?: { privateKey?: PrivateKeyWithPem }): Promise<void> {
-		//#region MIGRATION
-		if (!opts?.privateKey) {
-			/**
-			 * ed25519の署名がなければ追加する
-			 */
-			const created = await this.userKeypairService.refreshAndPrepareEd25519KeyPair(this.actor.id);
-			if (created) {
-				// createdが存在するということは新規作成されたということなので、フォロワーに配信する
-				this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`);
-				// リモートに配信
-				const keyPair = await this.userKeypairService.getLocalUserPrivateKeyPem(created, 'main');
-				await this.accountUpdateService.publishToFollowers(this.actor.id, keyPair);
-			}
-		}
-		//#endregion
-
-		//#region collect inboxes by recipes
+	public async execute(): Promise<void> {
 		// The value flags whether it is shared or not.
 		// key: inbox URL, value: whether it is sharedInbox
 		const inboxes = new Map<string, boolean>();
 
-		if (this.recipes.some(r => isAllKnowingSharedInbox(r))) {
-			// all-knowing shared inbox
-			const followings = await this.followingsRepository.find({
-				where: [
-					{ followerSharedInbox: Not(IsNull()) },
-					{ followeeSharedInbox: Not(IsNull()) },
-				],
-				select: ['followerSharedInbox', 'followeeSharedInbox'],
-			});
-
-			for (const following of followings) {
-				if (following.followeeSharedInbox) inboxes.set(following.followeeSharedInbox, true);
-				if (following.followerSharedInbox) inboxes.set(following.followerSharedInbox, true);
-			}
-		}
-
 		// build inbox list
 		// Process follower recipes first to avoid duplication when processing direct recipes later.
 		if (this.recipes.some(r => isFollowers(r))) {
@@ -200,49 +142,39 @@ class DeliverManager {
 
 			inboxes.set(recipe.to.inbox, false);
 		}
-		//#endregion
 
 		// deliver
-		await this.queueService.deliverMany(this.actor, this.activity, inboxes, opts?.privateKey);
-		this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`);
+		await this.queueService.deliverMany(this.actor, this.activity, inboxes);
 	}
 }
 
 @Injectable()
 export class ApDeliverManagerService {
-	private logger: Logger;
-
 	constructor(
 		@Inject(DI.followingsRepository)
 		private followingsRepository: FollowingsRepository,
 
-		private userKeypairService: UserKeypairService,
+		private userEntityService: UserEntityService,
 		private queueService: QueueService,
-		private accountUpdateService: AccountUpdateService,
-		private apLoggerService: ApLoggerService,
 	) {
-		this.logger = this.apLoggerService.logger.createSubLogger('deliver-manager');
 	}
 
 	/**
 	 * Deliver activity to followers
 	 * @param actor
 	 * @param activity Activity
-	 * @param forceMainKey Force to use main (rsa) key
 	 */
 	@bindThis
-	public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKeyWithPem): Promise<void> {
+	public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
 		const manager = new DeliverManager(
-			this.userKeypairService,
+			this.userEntityService,
 			this.followingsRepository,
 			this.queueService,
-			this.accountUpdateService,
-			this.logger,
 			actor,
 			activity,
 		);
 		manager.addFollowersRecipe();
-		await manager.execute({ privateKey });
+		await manager.execute();
 	}
 
 	/**
@@ -254,11 +186,9 @@ export class ApDeliverManagerService {
 	@bindThis
 	public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise<void> {
 		const manager = new DeliverManager(
-			this.userKeypairService,
+			this.userEntityService,
 			this.followingsRepository,
 			this.queueService,
-			this.accountUpdateService,
-			this.logger,
 			actor,
 			activity,
 		);
@@ -269,11 +199,10 @@ export class ApDeliverManagerService {
 	@bindThis
 	public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
 		return new DeliverManager(
-			this.userKeypairService,
+			this.userEntityService,
 			this.followingsRepository,
 			this.queueService,
-			this.accountUpdateService,
-			this.logger,
+
 			actor,
 			activity,
 		);
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 1bef9fe071..e2164fec1d 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -114,8 +114,15 @@ export class ApInboxService {
 			result = await this.performOneActivity(actor, activity);
 		}
 
-		// ついでにリモートユーザーの情報が古かったら更新しておく?
-		// → No, この関数が呼び出される前に署名検証で更新されているはず
+		// ついでにリモートユーザーの情報が古かったら更新しておく
+		if (actor.uri) {
+			if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
+				setImmediate(() => {
+					this.apPersonService.updatePerson(actor.uri);
+				});
+			}
+		}
+		return result;
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 5d7419f934..98e944f347 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -22,6 +22,7 @@ import { UserKeypairService } from '@/core/UserKeypairService.js';
 import { MfmService } from '@/core/MfmService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import type { MiUserKeypair } from '@/models/UserKeypair.js';
 import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
 import { bindThis } from '@/decorators.js';
 import { CustomEmojiService } from '@/core/CustomEmojiService.js';
@@ -30,7 +31,6 @@ import { JsonLdService } from './JsonLdService.js';
 import { ApMfmService } from './ApMfmService.js';
 import { CONTEXT } from './misc/contexts.js';
 import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
-import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
 
 @Injectable()
 export class ApRendererService {
@@ -251,15 +251,15 @@ export class ApRendererService {
 	}
 
 	@bindThis
-	public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey {
+	public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
 		return {
-			id: `${this.userEntityService.genLocalUserUri(user.id)}${postfix ?? '/publickey'}`,
+			id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
 			type: 'Key',
 			owner: this.userEntityService.genLocalUserUri(user.id),
-			publicKeyPem: createPublicKey(publicKey).export({
+			publicKeyPem: createPublicKey(key.publicKey).export({
 				type: 'spki',
 				format: 'pem',
-			}) as string,
+			}),
 		};
 	}
 
@@ -499,10 +499,7 @@ export class ApRendererService {
 			tag,
 			manuallyApprovesFollowers: user.isLocked,
 			discoverable: user.isExplorable,
-			publicKey: this.renderKey(user, keypair.publicKey, '#main-key'),
-			additionalPublicKeys: [
-				...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key')] : []),
-			],
+			publicKey: this.renderKey(user, keypair, '#main-key'),
 			isCat: user.isCat,
 			attachment: attachment.length ? attachment : undefined,
 		};
@@ -625,10 +622,12 @@ export class ApRendererService {
 	}
 
 	@bindThis
-	public async attachLdSignature(activity: any, key: PrivateKeyWithPem): Promise<IActivity> {
+	public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
+		const keypair = await this.userKeypairService.getUserKeypair(user.id);
+
 		const jsonLd = this.jsonLdService.use();
 		jsonLd.debug = false;
-		activity = await jsonLd.signRsaSignature2017(activity, key.privateKeyPem, key.keyId);
+		activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
 
 		return activity;
 	}
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 0cae91316b..93ac8ce9a7 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -3,9 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import * as crypto from 'node:crypto';
 import { URL } from 'node:url';
 import { Inject, Injectable } from '@nestjs/common';
-import { genRFC3230DigestHeader, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import type { MiUser } from '@/models/User.js';
@@ -15,61 +15,122 @@ import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
 import type Logger from '@/logger.js';
 import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
-import type { PrivateKeyWithPem, PrivateKey } from '@misskey-dev/node-http-message-signatures';
 
-export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record<string, string> }) {
-	const u = new URL(args.url);
-	const request = {
-		url: u.href,
-		method: 'POST',
-		headers: {
-			'Date': new Date().toUTCString(),
-			'Host': u.host,
-			'Content-Type': 'application/activity+json',
-			...args.additionalHeaders,
-		} as Record<string, string>,
-	};
+type Request = {
+	url: string;
+	method: string;
+	headers: Record<string, string>;
+};
 
-	// TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする
-	const digestHeader = args.digest ?? await genRFC3230DigestHeader(args.body, 'SHA-256');
-	request.headers['Digest'] = digestHeader;
+type Signed = {
+	request: Request;
+	signingString: string;
+	signature: string;
+	signatureHeader: string;
+};
 
-	const result = await signAsDraftToRequest(
-		request,
-		args.key,
-		['(request-target)', 'date', 'host', 'digest'],
-	);
+type PrivateKey = {
+	privateKeyPem: string;
+	keyId: string;
+};
 
-	return {
-		request,
-		...result,
-	};
-}
+export class ApRequestCreator {
+	static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
+		const u = new URL(args.url);
+		const digestHeader = args.digest ?? this.createDigest(args.body);
 
-export async function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record<string, string> }) {
-	const u = new URL(args.url);
-	const request = {
-		url: u.href,
-		method: 'GET',
-		headers: {
-			'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
-			'Date': new Date().toUTCString(),
-			'Host': new URL(args.url).host,
-			...args.additionalHeaders,
-		} as Record<string, string>,
-	};
+		const request: Request = {
+			url: u.href,
+			method: 'POST',
+			headers: this.#objectAssignWithLcKey({
+				'Date': new Date().toUTCString(),
+				'Host': u.host,
+				'Content-Type': 'application/activity+json',
+				'Digest': digestHeader,
+			}, args.additionalHeaders),
+		};
 
-	// TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする
-	const result = await signAsDraftToRequest(
-		request,
-		args.key,
-		['(request-target)', 'date', 'host', 'accept'],
-	);
+		const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
 
-	return {
-		request,
-		...result,
-	};
+		return {
+			request,
+			signingString: result.signingString,
+			signature: result.signature,
+			signatureHeader: result.signatureHeader,
+		};
+	}
+
+	static createDigest(body: string) {
+		return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
+	}
+
+	static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
+		const u = new URL(args.url);
+
+		const request: Request = {
+			url: u.href,
+			method: 'GET',
+			headers: this.#objectAssignWithLcKey({
+				'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+				'Date': new Date().toUTCString(),
+				'Host': new URL(args.url).host,
+			}, args.additionalHeaders),
+		};
+
+		const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
+
+		return {
+			request,
+			signingString: result.signingString,
+			signature: result.signature,
+			signatureHeader: result.signatureHeader,
+		};
+	}
+
+	static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
+		const signingString = this.#genSigningString(request, includeHeaders);
+		const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
+		const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
+
+		request.headers = this.#objectAssignWithLcKey(request.headers, {
+			Signature: signatureHeader,
+		});
+		// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
+		delete request.headers['host'];
+
+		return {
+			request,
+			signingString,
+			signature,
+			signatureHeader,
+		};
+	}
+
+	static #genSigningString(request: Request, includeHeaders: string[]): string {
+		request.headers = this.#lcObjectKey(request.headers);
+
+		const results: string[] = [];
+
+		for (const key of includeHeaders.map(x => x.toLowerCase())) {
+			if (key === '(request-target)') {
+				results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
+			} else {
+				results.push(`${key}: ${request.headers[key]}`);
+			}
+		}
+
+		return results.join('\n');
+	}
+
+	static #lcObjectKey(src: Record<string, string>): Record<string, string> {
+		const dst: Record<string, string> = {};
+		for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
+		return dst;
+	}
+
+	static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
+		return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
+	}
 }
 
 @Injectable()
@@ -89,28 +150,21 @@ export class ApRequestService {
 	}
 
 	@bindThis
-	public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise<void> {
+	public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
 		const body = typeof object === 'string' ? object : JSON.stringify(object);
-		const keyFetched = await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level);
-		const req = await createSignedPost({
-			level,
-			key: keyFetched,
+
+		const keypair = await this.userKeypairService.getUserKeypair(user.id);
+
+		const req = ApRequestCreator.createSignedPost({
+			key: {
+				privateKeyPem: keypair.privateKey,
+				keyId: `${this.config.url}/users/${user.id}#main-key`,
+			},
 			url,
 			body,
-			additionalHeaders: {
-				'User-Agent': this.config.userAgent,
-			},
 			digest,
-		});
-
-		// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
-		delete req.request.headers['Host'];
-
-		this.logger.debug('create signed post', {
-			version: 'draft',
-			level,
-			url,
-			keyId: keyFetched.keyId,
+			additionalHeaders: {
+			},
 		});
 
 		await this.httpRequestService.send(url, {
@@ -126,27 +180,19 @@ export class ApRequestService {
 	 * @param url URL to fetch
 	 */
 	@bindThis
-	public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> {
-		const key = await this.userKeypairService.getLocalUserPrivateKey(user.id, level);
-		const req = await createSignedGet({
-			level,
-			key,
+	public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
+		const keypair = await this.userKeypairService.getUserKeypair(user.id);
+
+		const req = ApRequestCreator.createSignedGet({
+			key: {
+				privateKeyPem: keypair.privateKey,
+				keyId: `${this.config.url}/users/${user.id}#main-key`,
+			},
 			url,
 			additionalHeaders: {
-				'User-Agent': this.config.userAgent,
 			},
 		});
 
-		// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
-		delete req.request.headers['Host'];
-
-		this.logger.debug('create signed get', {
-			version: 'draft',
-			level,
-			url,
-			keyId: key.keyId,
-		});
-
 		const res = await this.httpRequestService.send(url, {
 			method: req.request.method,
 			headers: req.request.headers,
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index 727ff6f956..bb3c40f093 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -16,7 +16,6 @@ import { UtilityService } from '@/core/UtilityService.js';
 import { bindThis } from '@/decorators.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import type Logger from '@/logger.js';
-import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { isCollectionOrOrderedCollection } from './type.js';
 import { ApDbResolverService } from './ApDbResolverService.js';
 import { ApRendererService } from './ApRendererService.js';
@@ -42,7 +41,6 @@ export class Resolver {
 		private httpRequestService: HttpRequestService,
 		private apRendererService: ApRendererService,
 		private apDbResolverService: ApDbResolverService,
-		private federatedInstanceService: FederatedInstanceService,
 		private loggerService: LoggerService,
 		private recursionLimit = 100,
 	) {
@@ -105,10 +103,8 @@ export class Resolver {
 			this.user = await this.instanceActorService.getInstanceActor();
 		}
 
-		const server = await this.federatedInstanceService.fetch(host);
-
 		const object = (this.user
-			? await this.apRequestService.signedGet(value, this.user, server.httpMessageSignaturesImplementationLevel) as IObject
+			? await this.apRequestService.signedGet(value, this.user) as IObject
 			: await this.httpRequestService.getActivityJson(value)) as IObject;
 
 		if (
@@ -204,7 +200,6 @@ export class ApResolverService {
 		private httpRequestService: HttpRequestService,
 		private apRendererService: ApRendererService,
 		private apDbResolverService: ApDbResolverService,
-		private federatedInstanceService: FederatedInstanceService,
 		private loggerService: LoggerService,
 	) {
 	}
@@ -225,7 +220,6 @@ export class ApResolverService {
 			this.httpRequestService,
 			this.apRendererService,
 			this.apDbResolverService,
-			this.federatedInstanceService,
 			this.loggerService,
 		);
 	}
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index fc4e3e3bef..feb8c42c56 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -134,7 +134,6 @@ const security_v1 = {
 		'privateKey': { '@id': 'sec:privateKey', '@type': '@id' },
 		'privateKeyPem': 'sec:privateKeyPem',
 		'publicKey': { '@id': 'sec:publicKey', '@type': '@id' },
-		'additionalPublicKeys': { '@id': 'sec:publicKey', '@type': '@id' },
 		'publicKeyBase58': 'sec:publicKeyBase58',
 		'publicKeyPem': 'sec:publicKeyPem',
 		'publicKeyWif': 'sec:publicKeyWif',
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index c41fc713d5..457205e023 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -3,10 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { verify } from 'crypto';
 import { Inject, Injectable } from '@nestjs/common';
 import promiseLimit from 'promise-limit';
-import { DataSource, In, Not } from 'typeorm';
+import { DataSource } from 'typeorm';
 import { ModuleRef } from '@nestjs/core';
 import { DI } from '@/di-symbols.js';
 import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
@@ -40,7 +39,6 @@ import { MetaService } from '@/core/MetaService.js';
 import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 import type { AccountMoveService } from '@/core/AccountMoveService.js';
 import { checkHttps } from '@/misc/check-https.js';
-import { REMOTE_USER_CACHE_TTL, REMOTE_USER_MOVE_COOLDOWN } from '@/const.js';
 import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
 import { extractApHashtags } from './tag.js';
 import type { OnModuleInit } from '@nestjs/common';
@@ -50,7 +48,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js';
 import type { ApLoggerService } from '../ApLoggerService.js';
 // eslint-disable-next-line @typescript-eslint/consistent-type-imports
 import type { ApImageService } from './ApImageService.js';
-import type { IActor, IKey, IObject } from '../type.js';
+import type { IActor, IObject } from '../type.js';
 
 const nameLength = 128;
 const summaryLength = 2048;
@@ -187,38 +185,13 @@ export class ApPersonService implements OnModuleInit {
 		}
 
 		if (x.publicKey) {
-			const publicKeys = Array.isArray(x.publicKey) ? x.publicKey : [x.publicKey];
-
-			for (const publicKey of publicKeys) {
-				if (typeof publicKey.id !== 'string') {
-					throw new Error('invalid Actor: publicKey.id is not a string');
-				}
-
-				const publicKeyIdHost = this.punyHost(publicKey.id);
-				if (publicKeyIdHost !== expectHost) {
-					throw new Error('invalid Actor: publicKey.id has different host');
-				}
-			}
-		}
-
-		if (x.additionalPublicKeys) {
-			if (!x.publicKey) {
-				throw new Error('invalid Actor: additionalPublicKeys is set but publicKey is not');
+			if (typeof x.publicKey.id !== 'string') {
+				throw new Error('invalid Actor: publicKey.id is not a string');
 			}
 
-			if (!Array.isArray(x.additionalPublicKeys)) {
-				throw new Error('invalid Actor: additionalPublicKeys is not an array');
-			}
-
-			for (const key of x.additionalPublicKeys) {
-				if (typeof key.id !== 'string') {
-					throw new Error('invalid Actor: additionalPublicKeys.id is not a string');
-				}
-
-				const keyIdHost = this.punyHost(key.id);
-				if (keyIdHost !== expectHost) {
-					throw new Error('invalid Actor: additionalPublicKeys.id has different host');
-				}
+			const publicKeyIdHost = this.punyHost(x.publicKey.id);
+			if (publicKeyIdHost !== expectHost) {
+				throw new Error('invalid Actor: publicKey.id has different host');
 			}
 		}
 
@@ -255,33 +228,6 @@ export class ApPersonService implements OnModuleInit {
 		return null;
 	}
 
-	/**
-	 * uriからUser(Person)をフェッチします。
-	 *
-	 * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。
-	 * また、TTLが0でない場合、TTLを過ぎていた場合はupdatePersonを実行します。
-	 */
-	@bindThis
-	async fetchPersonWithRenewal(uri: string, TTL = REMOTE_USER_CACHE_TTL): Promise<MiLocalUser | MiRemoteUser | null> {
-		const exist = await this.fetchPerson(uri);
-		if (exist == null) return null;
-
-		if (this.userEntityService.isRemoteUser(exist)) {
-			if (TTL === 0 || exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > TTL) {
-				this.logger.debug('fetchPersonWithRenewal: renew', { uri, TTL, lastFetchedAt: exist.lastFetchedAt });
-				try {
-					await this.updatePerson(exist.uri);
-					return await this.fetchPerson(uri);
-				} catch (err) {
-					this.logger.error('error occurred while renewing user', { err });
-				}
-			}
-			this.logger.debug('fetchPersonWithRenewal: use cache', { uri, TTL, lastFetchedAt: exist.lastFetchedAt });
-		}
-
-		return exist;
-	}
-
 	private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>>> {
 		if (user == null) throw new Error('failed to create user: user is null');
 
@@ -417,15 +363,11 @@ export class ApPersonService implements OnModuleInit {
 				}));
 
 				if (person.publicKey) {
-					const publicKeys = new Map<string, IKey>();
-					(person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key));
-					(Array.isArray(person.publicKey) ? person.publicKey : [person.publicKey]).forEach(key => publicKeys.set(key.id, key));
-
-					await transactionalEntityManager.save(Array.from(publicKeys.values(), key => new MiUserPublickey({
-						keyId: key.id,
-						userId: user!.id,
-						keyPem: key.publicKeyPem,
-					})));
+					await transactionalEntityManager.save(new MiUserPublickey({
+						userId: user.id,
+						keyId: person.publicKey.id,
+						keyPem: person.publicKey.publicKeyPem,
+					}));
 				}
 			});
 		} catch (e) {
@@ -571,29 +513,11 @@ export class ApPersonService implements OnModuleInit {
 		// Update user
 		await this.usersRepository.update(exist.id, updates);
 
-		try {
-			// Deleteアクティビティ受信時にもここが走ってsaveがuserforeign key制約エラーを吐くことがある
-			// とりあえずtry-catchで囲っておく
-			const publicKeys = new Map<string, IKey>();
-			if (person.publicKey) {
-				(person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key));
-				(Array.isArray(person.publicKey) ? person.publicKey : [person.publicKey]).forEach(key => publicKeys.set(key.id, key));
-
-				await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({
-					keyId: key.id,
-					userId: exist.id,
-					keyPem: key.publicKeyPem,
-				})));
-			}
-
-			this.userPublickeysRepository.delete({
-				keyId: Not(In(Array.from(publicKeys.keys()))),
-				userId: exist.id,
-			}).catch(err => {
-				this.logger.error('something happened while deleting remote user public keys:', { userId: exist.id, err });
+		if (person.publicKey) {
+			await this.userPublickeysRepository.update({ userId: exist.id }, {
+				keyId: person.publicKey.id,
+				keyPem: person.publicKey.publicKeyPem,
 			});
-		} catch (err) {
-			this.logger.error('something happened while updating remote user public keys:', { userId: exist.id, err });
 		}
 
 		let _description: string | null = null;
@@ -635,7 +559,7 @@ export class ApPersonService implements OnModuleInit {
 			exist.movedAt == null ||
 			// 以前のmovingから14日以上経過した場合のみ移行処理を許可
 			// (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく)
-			exist.movedAt.getTime() + REMOTE_USER_MOVE_COOLDOWN < updated.movedAt.getTime()
+			exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime()
 		)) {
 			this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`);
 			return this.processRemoteMove(updated, movePreventUris)
@@ -658,9 +582,9 @@ export class ApPersonService implements OnModuleInit {
 	 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
 	 */
 	@bindThis
-	public async resolvePerson(uri: string, resolver?: Resolver, withRenewal = false): Promise<MiLocalUser | MiRemoteUser> {
+	public async resolvePerson(uri: string, resolver?: Resolver): Promise<MiLocalUser | MiRemoteUser> {
 		//#region このサーバーに既に登録されていたらそれを返す
-		const exist = withRenewal ? await this.fetchPersonWithRenewal(uri) : await this.fetchPerson(uri);
+		const exist = await this.fetchPerson(uri);
 		if (exist) return exist;
 		//#endregion
 
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 1d55971660..5b6c6c8ca6 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -55,7 +55,7 @@ export function getOneApId(value: ApObject): string {
 export function getApId(value: string | IObject): string {
 	if (typeof value === 'string') return value;
 	if (typeof value.id === 'string') return value.id;
-	throw new Error('cannot determine id');
+	throw new Error('cannot detemine id');
 }
 
 /**
@@ -169,8 +169,10 @@ export interface IActor extends IObject {
 	discoverable?: boolean;
 	inbox: string;
 	sharedInbox?: string;	// 後方互換性のため
-	publicKey?: IKey | IKey[];
-	additionalPublicKeys?: IKey[];
+	publicKey?: {
+		id: string;
+		publicKeyPem: string;
+	};
 	followers?: string | ICollection | IOrderedCollection;
 	following?: string | ICollection | IOrderedCollection;
 	featured?: string | IOrderedCollection;
@@ -234,9 +236,8 @@ export const isEmoji = (object: IObject): object is IApEmoji =>
 
 export interface IKey extends IObject {
 	type: 'Key';
-	id: string;
 	owner: string;
-	publicKeyPem: string;
+	publicKeyPem: string | Buffer;
 }
 
 export interface IApDocument extends IObject {
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index fd0f55c6ab..9117b13914 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -56,7 +56,6 @@ export class InstanceEntityService {
 			infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
 			latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
 			moderationNote: iAmModerator ? instance.moderationNote : null,
-			httpMessageSignaturesImplementationLevel: instance.httpMessageSignaturesImplementationLevel,
 		};
 	}
 
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index f498c110bf..bba64a06ef 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -195,9 +195,6 @@ export class MemoryKVCache<T> {
 	private lifetime: number;
 	private gcIntervalHandle: NodeJS.Timeout;
 
-	/**
-	 * @param lifetime キャッシュの生存期間 (ms)
-	 */
 	constructor(lifetime: MemoryKVCache<never>['lifetime']) {
 		this.cache = new Map();
 		this.lifetime = lifetime;
diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts
index 0b033ec33e..02a303dc0a 100644
--- a/packages/backend/src/misc/gen-key-pair.ts
+++ b/packages/backend/src/misc/gen-key-pair.ts
@@ -3,14 +3,39 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures';
+import * as crypto from 'node:crypto';
+import * as util from 'node:util';
 
-export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) {
-	const [rsa, ed25519] = await Promise.all([genRsaKeyPair(rsaModulusLength), genEd25519KeyPair()]);
-	return {
-		publicKey: rsa.publicKey,
-		privateKey: rsa.privateKey,
-		ed25519PublicKey: ed25519.publicKey,
-		ed25519PrivateKey: ed25519.privateKey,
-	};
+const generateKeyPair = util.promisify(crypto.generateKeyPair);
+
+export async function genRsaKeyPair(modulusLength = 2048) {
+	return await generateKeyPair('rsa', {
+		modulusLength,
+		publicKeyEncoding: {
+			type: 'spki',
+			format: 'pem',
+		},
+		privateKeyEncoding: {
+			type: 'pkcs8',
+			format: 'pem',
+			cipher: undefined,
+			passphrase: undefined,
+		},
+	});
+}
+
+export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') {
+	return await generateKeyPair('ec', {
+		namedCurve,
+		publicKeyEncoding: {
+			type: 'spki',
+			format: 'pem',
+		},
+		privateKeyEncoding: {
+			type: 'pkcs8',
+			format: 'pem',
+			cipher: undefined,
+			passphrase: undefined,
+		},
+	});
 }
diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts
index f2f2831cf1..17cd5c6665 100644
--- a/packages/backend/src/models/Instance.ts
+++ b/packages/backend/src/models/Instance.ts
@@ -158,9 +158,4 @@ export class MiInstance {
 		length: 16384, default: '',
 	})
 	public moderationNote: string;
-
-	@Column('varchar', {
-		length: 16, default: '00', nullable: false,
-	})
-	public httpMessageSignaturesImplementationLevel: string;
 }
diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts
index afa74ef11a..f5252d126c 100644
--- a/packages/backend/src/models/UserKeypair.ts
+++ b/packages/backend/src/models/UserKeypair.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm';
 import { id } from './util/id.js';
 import { MiUser } from './User.js';
 
@@ -12,42 +12,22 @@ export class MiUserKeypair {
 	@PrimaryColumn(id())
 	public userId: MiUser['id'];
 
-	@ManyToOne(type => MiUser, {
+	@OneToOne(type => MiUser, {
 		onDelete: 'CASCADE',
 	})
 	@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;
-
 	constructor(data: Partial<MiUserKeypair>) {
 		if (data == null) return;
 
diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts
index 0ecff2bcbe..6bcd785304 100644
--- a/packages/backend/src/models/UserPublickey.ts
+++ b/packages/backend/src/models/UserPublickey.ts
@@ -9,13 +9,7 @@ import { MiUser } from './User.js';
 
 @Entity('user_publickey')
 export class MiUserPublickey {
-	@PrimaryColumn('varchar', {
-		length: 256,
-	})
-	public keyId: string;
-
-	@Index()
-	@Column(id())
+	@PrimaryColumn(id())
 	public userId: MiUser['id'];
 
 	@OneToOne(type => MiUser, {
@@ -24,6 +18,12 @@ 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/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index c02e7f557a..ed40d405c6 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -116,9 +116,5 @@ export const packedFederationInstanceSchema = {
 			type: 'string',
 			optional: true, nullable: true,
 		},
-		httpMessageSignaturesImplementationLevel: {
-			type: 'string',
-			optional: false, nullable: false,
-		},
 	},
 } as const;
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 169b22c3f5..7bd74f3210 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -250,9 +250,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
 			}, {
 				...baseQueueOptions(this.config, QUEUE.DELIVER),
 				autorun: false,
-				concurrency: this.config.deliverJobConcurrency ?? 16,
+				concurrency: this.config.deliverJobConcurrency ?? 128,
 				limiter: {
-					max: this.config.deliverJobPerSec ?? 1024,
+					max: this.config.deliverJobPerSec ?? 128,
 					duration: 1000,
 				},
 				settings: {
@@ -290,9 +290,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
 			}, {
 				...baseQueueOptions(this.config, QUEUE.INBOX),
 				autorun: false,
-				concurrency: this.config.inboxJobConcurrency ?? 4,
+				concurrency: this.config.inboxJobConcurrency ?? 16,
 				limiter: {
-					max: this.config.inboxJobPerSec ?? 64,
+					max: this.config.inboxJobPerSec ?? 32,
 					duration: 1000,
 				},
 				settings: {
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 3bd9187e8b..d665945861 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -73,33 +73,25 @@ export class DeliverProcessorService {
 		}
 
 		try {
-			const _server = await this.federatedInstanceService.fetch(host);
-			await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {});
-			const server = await this.federatedInstanceService.fetch(host);
-
-			await this.apRequestService.signedPost(
-				job.data.user,
-				job.data.to,
-				job.data.content,
-				server.httpMessageSignaturesImplementationLevel,
-				job.data.digest,
-				job.data.privateKey,
-			);
+			await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
 
 			// Update stats
-			if (server.isNotResponding) {
-				this.federatedInstanceService.update(server.id, {
-					isNotResponding: false,
-					notRespondingSince: null,
-				});
-			}
+			this.federatedInstanceService.fetch(host).then(i => {
+				if (i.isNotResponding) {
+					this.federatedInstanceService.update(i.id, {
+						isNotResponding: false,
+						notRespondingSince: null,
+					});
+				}
 
-			this.apRequestChart.deliverSucc();
-			this.federationChart.deliverd(server.host, true);
+				this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+				this.apRequestChart.deliverSucc();
+				this.federationChart.deliverd(i.host, true);
 
-			if (meta.enableChartsForFederatedInstances) {
-				this.instanceChart.requestSent(server.host, true);
-			}
+				if (meta.enableChartsForFederatedInstances) {
+					this.instanceChart.requestSent(i.host, true);
+				}
+			});
 
 			return 'Success';
 		} catch (res) {
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 935c623df1..fa7009f8f5 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -5,8 +5,8 @@
 
 import { URL } from 'node:url';
 import { Injectable } from '@nestjs/common';
+import httpSignature from '@peertube/http-signature';
 import * as Bull from 'bullmq';
-import { verifyDraftSignature } from '@misskey-dev/node-http-message-signatures';
 import type Logger from '@/logger.js';
 import { MetaService } from '@/core/MetaService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -20,7 +20,6 @@ import type { MiRemoteUser } from '@/models/User.js';
 import type { MiUserPublickey } from '@/models/UserPublickey.js';
 import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
 import { StatusError } from '@/misc/status-error.js';
-import * as Acct from '@/misc/acct.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
 import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
@@ -53,15 +52,8 @@ export class InboxProcessorService {
 
 	@bindThis
 	public async process(job: Bull.Job<InboxJobData>): Promise<string> {
-		const signature = job.data.signature ?
-			'version' in job.data.signature ? job.data.signature.value : job.data.signature
-			: null;
-		if (Array.isArray(signature)) {
-			// RFC 9401はsignatureが配列になるが、とりあえずエラーにする
-			throw new Error('signature is array');
-		}
+		const signature = job.data.signature;	// HTTP-signature
 		let activity = job.data.activity;
-		let actorUri = getApId(activity.actor);
 
 		//#region Log
 		const info = Object.assign({}, activity);
@@ -69,7 +61,7 @@ export class InboxProcessorService {
 		this.logger.debug(JSON.stringify(info, null, 2));
 		//#endregion
 
-		const host = this.utilityService.toPuny(new URL(actorUri).hostname);
+		const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
 
 		// ブロックしてたら中断
 		const meta = await this.metaService.fetch();
@@ -77,76 +69,69 @@ export class InboxProcessorService {
 			return `Blocked request: ${host}`;
 		}
 
-		// HTTP-Signature keyIdを元にDBから取得
-		let authUser: Awaited<ReturnType<typeof this.apDbResolverService.getAuthUserFromApId>> = null;
-		let httpSignatureIsValid = null as boolean | null;
+		const keyIdLower = signature.keyId.toLowerCase();
+		if (keyIdLower.startsWith('acct:')) {
+			return `Old keyId is no longer supported. ${keyIdLower}`;
+		}
 
-		try {
-			authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, signature?.keyId);
-		} catch (err) {
-			// 対象が4xxならスキップ
-			if (err instanceof StatusError) {
-				if (!err.isRetryable) {
-					throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
+		// HTTP-Signature keyIdを元にDBから取得
+		let authUser: {
+			user: MiRemoteUser;
+			key: MiUserPublickey | null;
+		} | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId);
+
+		// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
+		if (authUser == null) {
+			try {
+				authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor));
+			} catch (err) {
+				// 対象が4xxならスキップ
+				if (err instanceof StatusError) {
+					if (!err.isRetryable) {
+						throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
+					}
+					throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
 				}
-				throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
 			}
 		}
 
-		// authUser.userがnullならスキップ
-		if (authUser != null && authUser.user == null) {
+		// それでもわからなければ終了
+		if (authUser == null) {
 			throw new Bull.UnrecoverableError('skip: failed to resolve user');
 		}
 
-		if (signature != null && authUser != null) {
-			if (signature.keyId.toLowerCase().startsWith('acct:')) {
-				this.logger.warn(`Old keyId is no longer supported. lowerKeyId=${signature.keyId.toLowerCase()}`);
-			} else if (authUser.key != null) {
-				// keyがなかったらLD Signatureで検証するべき
-				// HTTP-Signatureの検証
-				const errorLogger = (ms: any) => this.logger.error(ms);
-				httpSignatureIsValid = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger);
-				this.logger.debug('Inbox message validation: ', {
-					userId: authUser.user.id,
-					userAcct: Acct.toString(authUser.user),
-					parsedKeyId: signature.keyId,
-					foundKeyId: authUser.key.keyId,
-					httpSignatureValid: httpSignatureIsValid,
-				});
-			}
+		// publicKey がなくても終了
+		if (authUser.key == null) {
+			throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
 		}
 
-		if (
-			authUser == null ||
-			httpSignatureIsValid !== true ||
-			authUser.user.uri !== actorUri // 一応チェック
-		) {
+		// HTTP-Signatureの検証
+		const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
+
+		// また、signatureのsignerは、activity.actorと一致する必要がある
+		if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
 			// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
 			const ldSignature = activity.signature;
-
-			if (ldSignature && ldSignature.creator) {
+			if (ldSignature) {
 				if (ldSignature.type !== 'RsaSignature2017') {
 					throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
 				}
 
-				if (ldSignature.creator.toLowerCase().startsWith('acct:')) {
-					throw new Bull.UnrecoverableError(`old key not supported ${ldSignature.creator}`);
+				// ldSignature.creator: https://example.oom/users/user#main-key
+				// みたいになっててUserを引っ張れば公開キーも入ることを期待する
+				if (ldSignature.creator) {
+					const candicate = ldSignature.creator.replace(/#.*/, '');
+					await this.apPersonService.resolvePerson(candicate).catch(() => null);
 				}
 
-				authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, ldSignature.creator);
-
+				// keyIdからLD-Signatureのユーザーを取得
+				authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator);
 				if (authUser == null) {
-					throw new Bull.UnrecoverableError(`skip: LD-Signatureのactorとcreatorが一致しませんでした uri=${actorUri} creator=${ldSignature.creator}`);
-				}
-				if (authUser.user == null) {
-					throw new Bull.UnrecoverableError(`skip: LD-Signatureのユーザーが取得できませんでした uri=${actorUri} creator=${ldSignature.creator}`);
-				}
-				// 一応actorチェック
-				if (authUser.user.uri !== actorUri) {
-					throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${actorUri})`);
+					throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
 				}
+
 				if (authUser.key == null) {
-					throw new Bull.UnrecoverableError(`skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした uri=${actorUri} creator=${ldSignature.creator}`);
+					throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
 				}
 
 				const jsonLd = this.jsonLdService.use();
@@ -157,27 +142,13 @@ export class InboxProcessorService {
 					throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
 				}
 
-				// ブロックしてたら中断
-				const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
-				if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
-					throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
-				}
-
 				// アクティビティを正規化
-				// GHSA-2vxv-pv3m-3wvj
 				delete activity.signature;
 				try {
 					activity = await jsonLd.compact(activity) as IActivity;
 				} catch (e) {
 					throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
 				}
-
-				// actorが正規化前後で一致しているか確認
-				actorUri = getApId(activity.actor);
-				if (authUser.user.uri !== actorUri) {
-					throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity(after normalization).actor(${actorUri})`);
-				}
-
 				// TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
 				// https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
 				activity.signature = ldSignature;
@@ -187,8 +158,19 @@ export class InboxProcessorService {
 				delete compactedInfo['@context'];
 				this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
 				//#endregion
+
+				// もう一度actorチェック
+				if (authUser.user.uri !== activity.actor) {
+					throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
+				}
+
+				// ブロックしてたら中断
+				const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
+				if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
+					throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
+				}
 			} else {
-				throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. http_signature_keyId=${signature?.keyId}`);
+				throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
 			}
 		}
 
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index f2466f2e3d..a4077a0547 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -9,24 +9,7 @@ import type { MiNote } from '@/models/Note.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiWebhook } from '@/models/Webhook.js';
 import type { IActivity } from '@/core/activitypub/type.js';
-import type { ParsedSignature, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
-
-/**
- * @peertube/http-signature 時代の古いデータにも対応しておく
- * TODO: 2026年ぐらいには消す
- */
-export interface OldParsedSignature {
-	scheme: 'Signature';
-	params: {
-		keyId: string;
-		algorithm: string;
-		headers: string[];
-		signature: string;
-	};
-	signingString: string;
-	algorithm: string;
-	keyId: string;
-}
+import type httpSignature from '@peertube/http-signature';
 
 export type DeliverJobData = {
 	/** Actor */
@@ -39,13 +22,11 @@ export type DeliverJobData = {
 	to: string;
 	/** whether it is sharedInbox */
 	isSharedInbox: boolean;
-	/** force to use main (rsa) key */
-	privateKey?: PrivateKeyWithPem;
 };
 
 export type InboxJobData = {
 	activity: IActivity;
-	signature: ParsedSignature | OldParsedSignature | null;
+	signature: httpSignature.IParsedSignature;
 };
 
 export type RelationshipJobData = {
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 753eaad047..3255d64621 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -3,10 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import * as crypto from 'node:crypto';
 import { IncomingMessage } from 'node:http';
 import { Inject, Injectable } from '@nestjs/common';
 import fastifyAccepts from '@fastify/accepts';
-import { verifyDigestHeader, parseRequestSignature } from '@misskey-dev/node-http-message-signatures';
+import httpSignature from '@peertube/http-signature';
 import { Brackets, In, IsNull, LessThan, Not } from 'typeorm';
 import accepts from 'accepts';
 import vary from 'vary';
@@ -30,17 +31,12 @@ import { IActivity } from '@/core/activitypub/type.js';
 import { isQuote, isRenote } from '@/misc/is-renote.js';
 import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
 import type { FindOptionsWhere } from 'typeorm';
-import { LoggerService } from '@/core/LoggerService.js';
-import Logger from '@/logger.js';
 
 const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
 const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
 
 @Injectable()
 export class ActivityPubServerService {
-	private logger: Logger;
-	private inboxLogger: Logger;
-
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
@@ -75,11 +71,8 @@ export class ActivityPubServerService {
 		private queueService: QueueService,
 		private userKeypairService: UserKeypairService,
 		private queryService: QueryService,
-		private loggerService: LoggerService,
 	) {
 		//this.createServer = this.createServer.bind(this);
-		this.logger = this.loggerService.getLogger('server-ap', 'gray');
-		this.inboxLogger = this.logger.createSubLogger('inbox', 'gray');
 	}
 
 	@bindThis
@@ -107,44 +100,70 @@ export class ActivityPubServerService {
 	}
 
 	@bindThis
-	private async inbox(request: FastifyRequest, reply: FastifyReply) {
-		if (request.body == null) {
-			this.inboxLogger.warn('request body is empty');
-			reply.code(400);
-			return;
-		}
+	private inbox(request: FastifyRequest, reply: FastifyReply) {
+		let signature;
 
-		let signature: ReturnType<typeof parseRequestSignature>;
-
-		const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true);
-		if (verifyDigest !== true) {
-			this.inboxLogger.warn('digest verification failed');
+		try {
+			signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
+		} catch (e) {
 			reply.code(401);
 			return;
 		}
 
-		try {
-			signature = parseRequestSignature(request.raw, {
-				requiredInputs: {
-					draft: ['(request-target)', 'digest', 'host', 'date'],
-				},
-			});
-		} catch (err) {
-			this.inboxLogger.warn('signature header parsing failed', { err });
+		if (signature.params.headers.indexOf('host') === -1
+			|| request.headers.host !== this.config.host) {
+			// Host not specified or not match.
+			reply.code(401);
+			return;
+		}
 
-			if (typeof request.body === 'object' && 'signature' in request.body) {
-				// LD SignatureがあればOK
-				this.queueService.inbox(request.body as IActivity, null);
-				reply.code(202);
+		if (signature.params.headers.indexOf('digest') === -1) {
+			// Digest not found.
+			reply.code(401);
+		} else {
+			const digest = request.headers.digest;
+
+			if (typeof digest !== 'string') {
+				// Huh?
+				reply.code(401);
 				return;
 			}
 
-			this.inboxLogger.warn('signature header parsing failed and LD signature not found');
-			reply.code(401);
-			return;
+			const re = /^([a-zA-Z0-9\-]+)=(.+)$/;
+			const match = digest.match(re);
+
+			if (match == null) {
+				// Invalid digest
+				reply.code(401);
+				return;
+			}
+
+			const algo = match[1].toUpperCase();
+			const digestValue = match[2];
+
+			if (algo !== 'SHA-256') {
+				// Unsupported digest algorithm
+				reply.code(401);
+				return;
+			}
+
+			if (request.rawBody == null) {
+				// Bad request
+				reply.code(400);
+				return;
+			}
+
+			const hash = crypto.createHash('sha256').update(request.rawBody).digest('base64');
+
+			if (hash !== digestValue) {
+				// Invalid digest
+				reply.code(401);
+				return;
+			}
 		}
 
 		this.queueService.inbox(request.body as IActivity, signature);
+
 		reply.code(202);
 	}
 
@@ -621,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.publicKey)));
+				return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair)));
 			} else {
 				reply.code(400);
 				return;
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index c0f8084768..cc18997fdc 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -94,13 +94,6 @@ export class NodeinfoServerService {
 					localComments: 0,
 				},
 				metadata: {
-					/**
-					 * '00': Draft, RSA only
-					 * '01': Draft, Ed25519 suported
-					 * '11': RFC 9421, Ed25519 supported
-					 */
-					httpMessageSignaturesImplementationLevel: '01',
-
 					nodeName: meta.name,
 					nodeDescription: meta.description,
 					nodeAdmins: [{
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index bfe230da8d..305ae1af1d 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -56,8 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			const res = [] as [string, number][];
 
 			for (const job of jobs) {
-				const signature = job.data.signature ? 'version' in job.data.signature ? job.data.signature.value : job.data.signature : null;
-				const host = signature ? Array.isArray(signature) ? 'TODO' : new URL(signature.keyId).host : new URL(job.data.activity.actor).host;
+				const host = new URL(job.data.signature.keyId).host;
 				if (res.find(x => x[0] === host)) {
 					res.find(x => x[0] === host)![1]++;
 				} else {
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index fce1eacf00..540b866b28 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -378,7 +378,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 			assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
-		});
+		}, 1000 * 10);
 
 		test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
@@ -672,7 +672,7 @@ describe('Timelines', () => {
 
 			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
-		});
+		}, 1000 * 10);
 	});
 
 	describe('Social TL', () => {
@@ -812,7 +812,7 @@ describe('Timelines', () => {
 
 			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
-		});
+		}, 1000 * 10);
 	});
 
 	describe('User List TL', () => {
@@ -1025,7 +1025,7 @@ describe('Timelines', () => {
 
 			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
-		});
+		}, 1000 * 10);
 
 		test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
@@ -1184,7 +1184,7 @@ describe('Timelines', () => {
 
 			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
-		});
+		}, 1000 * 10);
 
 		test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts
index 485506ee64..3c7e796700 100644
--- a/packages/backend/test/misc/mock-resolver.ts
+++ b/packages/backend/test/misc/mock-resolver.ts
@@ -14,7 +14,6 @@ import type { InstanceActorService } from '@/core/InstanceActorService.js';
 import type { LoggerService } from '@/core/LoggerService.js';
 import type { MetaService } from '@/core/MetaService.js';
 import type { UtilityService } from '@/core/UtilityService.js';
-import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { bindThis } from '@/decorators.js';
 import type {
 	FollowRequestsRepository,
@@ -48,7 +47,6 @@ export class MockResolver extends Resolver {
 			{} as HttpRequestService,
 			{} as ApRendererService,
 			{} as ApDbResolverService,
-			{} as FederatedInstanceService,
 			loggerService,
 		);
 	}
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index 2e66b81fcd..bf8f3ab0e3 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -75,61 +75,62 @@ describe('FetchInstanceMetadataService', () => {
 	test('Lock and update', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date(now - 10 * 1000 * 60 * 60 * 24) } as any);
+		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
 
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
 		expect(httpRequestService.getJson).toHaveBeenCalled();
 	});
 
-	test('Don\'t lock and update if recently updated', async () => {
+	test('Lock and don\'t update', async () => {
 		redisClient.set = mockRedis();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date() } as any);
+		const now = Date.now();
+		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
 
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
+		expect(tryLockSpy).toHaveBeenCalledTimes(1);
+		expect(unlockSpy).toHaveBeenCalledTimes(1);
 		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
-		expect(tryLockSpy).toHaveBeenCalledTimes(0);
-		expect(unlockSpy).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
 
 	test('Do nothing when lock not acquired', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date(now - 10 * 1000 * 60 * 60 * 24) } as any);
+		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
 
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
 		expect(tryLockSpy).toHaveBeenCalledTimes(1);
 		expect(unlockSpy).toHaveBeenCalledTimes(0);
+		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
 	});
 
-	test('Do when forced', async () => {
+	test('Do when lock not acquired but forced', async () => {
 		redisClient.set = mockRedis();
 		const now = Date.now();
-		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date(now - 10 * 1000 * 60 * 60 * 24) } as any);
+		federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
 		httpRequestService.getJson.mockImplementation(() => { throw Error(); });
 		await fetchInstanceMetadataService.tryLock('example.com');
 		const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
 		const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
 
 		await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
-		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
 		expect(tryLockSpy).toHaveBeenCalledTimes(0);
 		expect(unlockSpy).toHaveBeenCalledTimes(1);
+		expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
 		expect(httpRequestService.getJson).toHaveBeenCalled();
 	});
 });
diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts
index 50894c8b81..d3d39240dc 100644
--- a/packages/backend/test/unit/ap-request.ts
+++ b/packages/backend/test/unit/ap-request.ts
@@ -4,8 +4,10 @@
  */
 
 import * as assert from 'assert';
-import { verifyDraftSignature, parseRequestSignature, genEd25519KeyPair, genRsaKeyPair, importPrivateKey } from '@misskey-dev/node-http-message-signatures';
-import { createSignedGet, createSignedPost } from '@/core/activitypub/ApRequestService.js';
+import httpSignature from '@peertube/http-signature';
+
+import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 
 export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
 	return {
@@ -22,68 +24,38 @@ export const buildParsedSignature = (signingString: string, signature: string, a
 	};
 };
 
-async function getKeyPair(level: string) {
-	if (level === '00') {
-		return await genRsaKeyPair();
-	} else if (level === '01') {
-		return await genEd25519KeyPair();
-	}
-	throw new Error('Invalid level');
-}
+describe('ap-request', () => {
+	test('createSignedPost with verify', async () => {
+		const keypair = await genRsaKeyPair();
+		const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
+		const url = 'https://example.com/inbox';
+		const activity = { a: 1 };
+		const body = JSON.stringify(activity);
+		const headers = {
+			'User-Agent': 'UA',
+		};
 
-describe('ap-request post', () => {
-	const url = 'https://example.com/inbox';
-	const activity = { a: 1 };
-	const body = JSON.stringify(activity);
-	const headers = {
-		'User-Agent': 'UA',
-	};
+		const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers });
 
-	describe.each(['00', '01'])('createSignedPost with verify', (level) => {
-		test('pem', async () => {
-			const keypair = await getKeyPair(level);
-			const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
+		const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
 
-			const req = await createSignedPost({ level, key, url, body, additionalHeaders: headers });
+		const result = httpSignature.verifySignature(parsed, keypair.publicKey);
+		assert.deepStrictEqual(result, true);
+	});
 
-			const parsed = parseRequestSignature(req.request);
-			expect(parsed.version).toBe('draft');
-			expect(Array.isArray(parsed.value)).toBe(false);
-			const verify = await verifyDraftSignature(parsed.value as any, keypair.publicKey);
-			assert.deepStrictEqual(verify, true);
-		});
-		test('imported', async () => {
-			const keypair = await getKeyPair(level);
-			const key = { keyId: 'x', 'privateKey': await importPrivateKey(keypair.privateKey) };
+	test('createSignedGet with verify', async () => {
+		const keypair = await genRsaKeyPair();
+		const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
+		const url = 'https://example.com/outbox';
+		const headers = {
+			'User-Agent': 'UA',
+		};
 
-			const req = await createSignedPost({ level, key, url, body, additionalHeaders: headers });
+		const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers });
 
-			const parsed = parseRequestSignature(req.request);
-			expect(parsed.version).toBe('draft');
-			expect(Array.isArray(parsed.value)).toBe(false);
-			const verify = await verifyDraftSignature(parsed.value as any, keypair.publicKey);
-			assert.deepStrictEqual(verify, true);
-		});
-	});
-});
-
-describe('ap-request get', () => {
-	describe.each(['00', '01'])('createSignedGet with verify', (level) => {
-		test('pass', async () => {
-			const keypair = await getKeyPair(level);
-			const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
-			const url = 'https://example.com/outbox';
-			const headers = {
-				'User-Agent': 'UA',
-			};
-
-			const req = await createSignedGet({ level, key, url, additionalHeaders: headers });
-
-			const parsed = parseRequestSignature(req.request);
-			expect(parsed.version).toBe('draft');
-			expect(Array.isArray(parsed.value)).toBe(false);
-			const verify = await verifyDraftSignature(parsed.value as any, keypair.publicKey);
-			assert.deepStrictEqual(verify, true);
-		});
+		const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
+
+		const result = httpSignature.verifySignature(parsed, keypair.publicKey);
+		assert.deepStrictEqual(result, true);
 	});
 });
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 2a7e5a323d..d3c857219b 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4608,7 +4608,6 @@ export type components = {
       /** Format: date-time */
       latestRequestReceivedAt: string | null;
       moderationNote?: string | null;
-      httpMessageSignaturesImplementationLevel: string;
     };
     GalleryPost: {
       /**
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2d426c2fa8..7d3fec8596 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -125,9 +125,6 @@ importers:
       '@fastify/view':
         specifier: 9.1.0
         version: 9.1.0
-      '@misskey-dev/node-http-message-signatures':
-        specifier: 0.0.10
-        version: 0.0.10
       '@misskey-dev/sharp-read-bmp':
         specifier: 1.2.0
         version: 1.2.0
@@ -146,6 +143,9 @@ importers:
       '@nestjs/testing':
         specifier: 10.3.10
         version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))
+      '@peertube/http-signature':
+        specifier: 1.7.0
+        version: 1.7.0
       '@sentry/node':
         specifier: 8.13.0
         version: 8.13.0
@@ -3228,11 +3228,6 @@ packages:
   '@kurkle/color@0.3.2':
     resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
 
-  '@lapo/asn1js@2.0.4':
-    resolution: {integrity: sha512-KJD3wQAZxozcraJdWp3utDU6DEZgAVBGp9INCdptUpZaXCEYkpwNb7h7wyYh5y6DxtpvIud8k0suhWJ/z2rKvw==}
-    engines: {node: '>=12.20.0'}
-    hasBin: true
-
   '@levischuck/tiny-cbor@0.2.2':
     resolution: {integrity: sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==}
 
@@ -3286,10 +3281,6 @@ packages:
       eslint-plugin-import: '>= 2'
       globals: '>= 15'
 
-  '@misskey-dev/node-http-message-signatures@0.0.10':
-    resolution: {integrity: sha512-HiAuc//tOU077KFUJhHYLAPWku9enTpOFIqQiK6l2i2mIizRvv7HhV7Y+yuav5quDOAz+WZGK/i5C9OR5fkKIg==}
-    engines: {node: '>=18.4.0'}
-
   '@misskey-dev/sharp-read-bmp@1.2.0':
     resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==}
 
@@ -3680,6 +3671,10 @@ packages:
   '@peculiar/asn1-x509@2.3.8':
     resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==}
 
+  '@peertube/http-signature@1.7.0':
+    resolution: {integrity: sha512-aGQIwo6/sWtyyqhVK4e1MtxYz4N1X8CNt6SOtCc+Wnczs5S5ONaLHDDR8LYaGn0MgOwvGgXyuZ5sJIfd7iyoUw==}
+    engines: {node: '>=0.10'}
+
   '@pkgjs/parseargs@0.11.0':
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
@@ -10529,9 +10524,6 @@ packages:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
 
-  rfc4648@1.5.3:
-    resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==}
-
   rfdc@1.3.0:
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
 
@@ -11083,10 +11075,6 @@ packages:
     resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==}
     engines: {node: '>=14.16'}
 
-  structured-headers@1.0.1:
-    resolution: {integrity: sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==}
-    engines: {node: '>= 14', npm: '>=6'}
-
   stylehacks@6.1.1:
     resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==}
     engines: {node: ^14 || ^16 || >=18.0}
@@ -14654,8 +14642,6 @@ snapshots:
 
   '@kurkle/color@0.3.2': {}
 
-  '@lapo/asn1js@2.0.4': {}
-
   '@levischuck/tiny-cbor@0.2.2': {}
 
   '@lukeed/csprng@1.0.1': {}
@@ -14736,12 +14722,6 @@ snapshots:
       eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.15.0(eslint@9.6.0)(typescript@5.5.3))(eslint@9.6.0)
       globals: 15.7.0
 
-  '@misskey-dev/node-http-message-signatures@0.0.10':
-    dependencies:
-      '@lapo/asn1js': 2.0.4
-      rfc4648: 1.5.3
-      structured-headers: 1.0.1
-
   '@misskey-dev/sharp-read-bmp@1.2.0':
     dependencies:
       decode-bmp: 0.2.1
@@ -15202,6 +15182,12 @@ snapshots:
       pvtsutils: 1.3.5
       tslib: 2.6.2
 
+  '@peertube/http-signature@1.7.0':
+    dependencies:
+      assert-plus: 1.0.0
+      jsprim: 1.4.2
+      sshpk: 1.17.0
+
   '@pkgjs/parseargs@0.11.0':
     optional: true
 
@@ -23893,8 +23879,6 @@ snapshots:
 
   reusify@1.0.4: {}
 
-  rfc4648@1.5.3: {}
-
   rfdc@1.3.0: {}
 
   rimraf@2.6.3:
@@ -24490,8 +24474,6 @@ snapshots:
       '@tokenizer/token': 0.3.0
       peek-readable: 5.0.0
 
-  structured-headers@1.0.1: {}
-
   stylehacks@6.1.1(postcss@8.4.38):
     dependencies:
       browserslist: 4.23.0

From 32651aba677c957bcd9c734eef9806375413dbc5 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 23 Jul 2024 18:39:59 +0900
Subject: [PATCH 9/9] Update about-misskey.vue

---
 packages/frontend/src/pages/about-misskey.vue | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index cc0394f401..8459f0f9d5 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -243,6 +243,21 @@ const patronsWithIcon = [{
 }, {
 	name: '越貝鯛丸',
 	icon: 'https://assets.misskey-hub.net/patrons/86c7374de37849b882d8ebbc833dc968.jpg',
+}, {
+	name: '☔あめ🍬(灬˘╰╯˘灬)',
+	icon: 'https://assets.misskey-hub.net/patrons/676eea72d4884d3f89aababbb62533fb.jpg',
+}, {
+	name: '貯水よび',
+	icon: 'https://assets.misskey-hub.net/patrons/2974506d53244bbe94a67707b27099e2.jpg',
+}, {
+	name: 'はるかさ',
+	icon: 'https://assets.misskey-hub.net/patrons/26ce2432739a400aa3aa0de0ef67a107.jpg',
+}, {
+	name: '天鈴のあ',
+	icon: 'https://assets.misskey-hub.net/patrons/995cdbb00bd6421184461a883adfe1d9.jpg',
+}, {
+	name: 'えとゔぁす',
+	icon: 'https://assets.misskey-hub.net/patrons/2578f441b82a44cfaa55ba83a318b26e.jpg',
 }];
 
 const patrons = [
@@ -347,6 +362,7 @@ const patrons = [
 	'SHO SEKIGUCHI',
 	'塩キャベツ',
 	'はとぽぷさん',
+	'100の人 (エスパー・イーシア)',
 ];
 
 const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));