|
|
|
@@ -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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|