diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index a0ec9987a7..6488687867 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -135,7 +135,7 @@ export class QueueService { } @bindThis - public inbox(activity: IActivity, signature: ParsedSignature) { + public inbox(activity: IActivity, signature: ParsedSignature | null) { const data = { activity: activity, signature, diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 8509f04c09..16804a240d 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -137,14 +137,41 @@ export class ApDbResolverService implements OnApplicationShutdown { * 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. + * keyIdがURLライクの場合、ハッシュを削除したkeyIdはuriと同一であることが期待される + * @returns + * 1. uriとkeyIdが一致しない場合`null` + * 2. userが見つからない場合`{ user: null, key: null }` + * 3. keyが見つからない場合`{ user, key: null }` */ @bindThis public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ user: MiRemoteUser; key: MiUserPublickey | null; - } | null> { + } | { + user: null; + key: null; + } | + null> { + if (keyId) { + try { + const actorUrl = new URL(uri); + const keyUrl = new URL(keyId); + actorUrl.hash = ''; + keyUrl.hash = ''; + if (actorUrl.href !== keyUrl.href) { + // uriとkeyId(のhashなし)が一致しない場合、actorと鍵の所有者が一致していないということである + // その場合、そもそも署名は有効といえないのでキーの検索は無意味 + this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`); + return null; + } + } catch (err) { + // キーがURLっぽくない場合はエラーになるはず。そういった場合はとりあえずキー検索してみる + this.logger.warn(`maybe actor uri or keyId are not url like: uri=${uri} keyId=${keyId}`, { err }); + } + } + const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser; - if (user.isDeleted) return null; + if (user.isDeleted) return { user: null, key: null }; const keys = await this.getPublicKeyByUserId(user.id); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index dcaa24e8b7..07f36f4d7a 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -52,12 +52,15 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { - const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; + 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 activity = job.data.activity; + const actorUri = getApId(activity.actor); //#region Log const info = Object.assign({}, activity); @@ -65,7 +68,7 @@ export class InboxProcessorService { this.logger.debug(JSON.stringify(info, null, 2)); //#endregion - const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); + const host = this.utilityService.toPuny(new URL(activity.actor).hostname); // ブロックしてたら中断 const meta = await this.metaService.fetch(); @@ -73,19 +76,12 @@ export class InboxProcessorService { return `Blocked request: ${host}`; } - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith('acct:')) { - return `Old keyId is no longer supported. ${keyIdLower}`; - } - // HTTP-Signature keyIdを元にDBから取得 - let authUser: { - user: MiRemoteUser; - key: MiUserPublickey | null; - } | null = null; + let authUser: Awaited> = null; + let httpSignatureIsValid = null as boolean | null; try { - authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId); + authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, signature?.keyId); } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { @@ -96,45 +92,58 @@ export class InboxProcessorService { } } - // それでもわからなければ終了 - if (authUser == null) { + // authUser.userがnullならスキップ + if (authUser != null && authUser.user == null) { throw new Bull.UnrecoverableError('skip: failed to resolve user'); } - // publicKey がなくても終了 - if (authUser.key == null) { - // publicKeyがないのはpublicKeyの変更(主にmain→ed25519)に - // 対応しきれていない場合があるためリトライする - throw new Error(`skip: failed to resolve user publicKey: keyId=${signature.keyId}`); + 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, + }); + } } - // HTTP-Signatureの検証 - const errorLogger = (ms: any) => this.logger.error(ms); - const httpSignatureValidated = 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, - httpSignatureValidated, - }); - - // また、signatureのsignerは、activity.actorと一致する必要がある - if (httpSignatureValidated !== true || authUser.user.uri !== activity.actor) { + if ( + authUser == null || + httpSignatureIsValid !== true || + authUser.user.uri !== actorUri // 一応チェック + ) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る if (activity.signature?.creator) { if (activity.signature.type !== 'RsaSignature2017') { throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } - authUser = await this.apDbResolverService.getAuthUserFromApId(activity.signature.creator.replace(/#.*/, '')); - - if (authUser == null) { - throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); + if (activity.signature.creator.toLowerCase().startsWith('acct:')) { + throw new Bull.UnrecoverableError(`old key not supported ${activity.signature.creator}`); } + authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, activity.signature.creator); + + if (authUser == null) { + throw new Bull.UnrecoverableError(`skip: LD-Signatureのactorとcreatorが一致しませんでした uri=${actorUri} creator=${activity.signature.creator}`); + } + if (authUser.user == null) { + throw new Bull.UnrecoverableError(`skip: LD-Signatureのユーザーが取得できませんでした uri=${actorUri} creator=${activity.signature.creator}`); + } + // 一応actorチェック + if (authUser.user.uri !== actorUri) { + throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${actorUri})`); + } if (authUser.key == null) { - throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); + throw new Bull.UnrecoverableError(`skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした uri=${actorUri} creator=${activity.signature.creator}`); } // LD-Signature検証 @@ -144,18 +153,13 @@ export class InboxProcessorService { throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } - // もう一度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. keyId=${signature.keyId}`); + throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. http_signature_keyId=${signature?.keyId}`); } } diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 85b1c4924b..eaa836d965 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -44,7 +44,7 @@ export type DeliverJobData = { export type InboxJobData = { activity: IActivity; - signature: ParsedSignature | OldParsedSignature; + signature: ParsedSignature | OldParsedSignature | null; }; export type RelationshipJobData = { diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 78b89e8684..4a24ddf53c 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -30,12 +30,17 @@ import { IActivity } from '@/core/activitypub/type.js'; import { isPureRenote } from '@/misc/is-pure-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, @@ -70,8 +75,11 @@ 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', false); + this.inboxLogger = this.logger.createSubLogger('inbox', 'gray', false); } @bindThis @@ -100,10 +108,17 @@ 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; + } + let signature: ReturnType; const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true); if (verifyDigest !== true) { + this.inboxLogger.warn('digest verification failed'); reply.code(401); return; } @@ -115,12 +130,19 @@ export class ActivityPubServerService { }, }); } catch (e) { + if (typeof request.body === 'object' && 'signature' in request.body) { + // LD SignatureがあればOK + this.queueService.inbox(request.body as IActivity, null); + reply.code(202); + return; + } + + this.inboxLogger.warn('signature header parsing failed and LD signature not found'); reply.code(401); return; } this.queueService.inbox(request.body as IActivity, signature); - reply.code(202); }