HTTP Signatureがなかったり使えなかったりしそうな場合にLD Signatureを活用するように

This commit is contained in:
tamaina 2024-03-09 19:06:59 +00:00
parent da4a44b337
commit 8104963e1d
5 changed files with 101 additions and 48 deletions

View file

@ -135,7 +135,7 @@ export class QueueService {
} }
@bindThis @bindThis
public inbox(activity: IActivity, signature: ParsedSignature) { public inbox(activity: IActivity, signature: ParsedSignature | null) {
const data = { const data = {
activity: activity, activity: activity,
signature, signature,

View file

@ -137,14 +137,41 @@ export class ApDbResolverService implements OnApplicationShutdown {
* AP Actor id => Misskey User and Key * AP Actor id => Misskey User and Key
* @param uri AP Actor id * @param uri AP Actor id
* @param keyId Key id to find. If not specified, main key will be selected. * @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 @bindThis
public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{
user: MiRemoteUser; user: MiRemoteUser;
key: MiUserPublickey | null; 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; 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); const keys = await this.getPublicKeyByUserId(user.id);

View file

@ -52,12 +52,15 @@ export class InboxProcessorService {
@bindThis @bindThis
public async process(job: Bull.Job<InboxJobData>): Promise<string> { public async process(job: Bull.Job<InboxJobData>): Promise<string> {
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)) { if (Array.isArray(signature)) {
// RFC 9401はsignatureが配列になるが、とりあえずエラーにする // RFC 9401はsignatureが配列になるが、とりあえずエラーにする
throw new Error('signature is array'); throw new Error('signature is array');
} }
const activity = job.data.activity; const activity = job.data.activity;
const actorUri = getApId(activity.actor);
//#region Log //#region Log
const info = Object.assign({}, activity); const info = Object.assign({}, activity);
@ -65,7 +68,7 @@ export class InboxProcessorService {
this.logger.debug(JSON.stringify(info, null, 2)); this.logger.debug(JSON.stringify(info, null, 2));
//#endregion //#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(); const meta = await this.metaService.fetch();
@ -73,19 +76,12 @@ export class InboxProcessorService {
return `Blocked request: ${host}`; 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から取得 // HTTP-Signature keyIdを元にDBから取得
let authUser: { let authUser: Awaited<ReturnType<typeof this.apDbResolverService.getAuthUserFromApId>> = null;
user: MiRemoteUser; let httpSignatureIsValid = null as boolean | null;
key: MiUserPublickey | null;
} | null = null;
try { try {
authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId); authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, signature?.keyId);
} catch (err) { } catch (err) {
// 対象が4xxならスキップ // 対象が4xxならスキップ
if (err instanceof StatusError) { if (err instanceof StatusError) {
@ -96,45 +92,58 @@ export class InboxProcessorService {
} }
} }
// それでもわからなければ終了 // authUser.userがnullならスキップ
if (authUser == null) { if (authUser != null && authUser.user == null) {
throw new Bull.UnrecoverableError('skip: failed to resolve user'); throw new Bull.UnrecoverableError('skip: failed to resolve user');
} }
// publicKey がなくても終了 if (signature != null && authUser != null) {
if (authUser.key == null) { if (signature.keyId.toLowerCase().startsWith('acct:')) {
// publicKeyがないのはpublicKeyの変更主にmain→ed25519 this.logger.warn(`Old keyId is no longer supported. lowerKeyId=${signature.keyId.toLowerCase()}`);
// 対応しきれていない場合があるためリトライする } else if (authUser.key != null) {
throw new Error(`skip: failed to resolve user publicKey: keyId=${signature.keyId}`); // keyがなかったらLD Signatureで検証するべき
}
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const errorLogger = (ms: any) => this.logger.error(ms); const errorLogger = (ms: any) => this.logger.error(ms);
const httpSignatureValidated = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger); httpSignatureIsValid = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger);
this.logger.debug('Inbox message validation: ', { this.logger.debug('Inbox message validation: ', {
userId: authUser.user.id, userId: authUser.user.id,
userAcct: Acct.toString(authUser.user), userAcct: Acct.toString(authUser.user),
parsedKeyId: signature.keyId, parsedKeyId: signature.keyId,
foundKeyId: authUser.key.keyId, foundKeyId: authUser.key.keyId,
httpSignatureValidated, httpSignatureValid: httpSignatureIsValid,
}); });
}
}
// また、signatureのsignerは、activity.actorと一致する必要がある if (
if (httpSignatureValidated !== true || authUser.user.uri !== activity.actor) { authUser == null ||
httpSignatureIsValid !== true ||
authUser.user.uri !== actorUri // 一応チェック
) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る // 一致しなくても、でもLD-Signatureがありそうならそっちも見る
if (activity.signature?.creator) { if (activity.signature?.creator) {
if (activity.signature.type !== 'RsaSignature2017') { if (activity.signature.type !== 'RsaSignature2017') {
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`);
} }
authUser = await this.apDbResolverService.getAuthUserFromApId(activity.signature.creator.replace(/#.*/, '')); if (activity.signature.creator.toLowerCase().startsWith('acct:')) {
throw new Bull.UnrecoverableError(`old key not supported ${activity.signature.creator}`);
if (authUser == null) {
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
} }
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) { 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検証 // LD-Signature検証
@ -144,18 +153,13 @@ export class InboxProcessorService {
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); 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); const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
} }
} else { } 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}`);
} }
} }

View file

@ -44,7 +44,7 @@ export type DeliverJobData = {
export type InboxJobData = { export type InboxJobData = {
activity: IActivity; activity: IActivity;
signature: ParsedSignature | OldParsedSignature; signature: ParsedSignature | OldParsedSignature | null;
}; };
export type RelationshipJobData = { export type RelationshipJobData = {

View file

@ -30,12 +30,17 @@ import { IActivity } from '@/core/activitypub/type.js';
import { isPureRenote } from '@/misc/is-pure-renote.js'; import { isPureRenote } from '@/misc/is-pure-renote.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; 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 ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@Injectable() @Injectable()
export class ActivityPubServerService { export class ActivityPubServerService {
private logger: Logger;
private inboxLogger: Logger;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@ -70,8 +75,11 @@ export class ActivityPubServerService {
private queueService: QueueService, private queueService: QueueService,
private userKeypairService: UserKeypairService, private userKeypairService: UserKeypairService,
private queryService: QueryService, private queryService: QueryService,
private loggerService: LoggerService,
) { ) {
//this.createServer = this.createServer.bind(this); //this.createServer = this.createServer.bind(this);
this.logger = this.loggerService.getLogger('server-ap', 'gray', false);
this.inboxLogger = this.logger.createSubLogger('inbox', 'gray', false);
} }
@bindThis @bindThis
@ -100,10 +108,17 @@ export class ActivityPubServerService {
@bindThis @bindThis
private async inbox(request: FastifyRequest, reply: FastifyReply) { 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<typeof parseRequestSignature>; let signature: ReturnType<typeof parseRequestSignature>;
const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true); const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true);
if (verifyDigest !== true) { if (verifyDigest !== true) {
this.inboxLogger.warn('digest verification failed');
reply.code(401); reply.code(401);
return; return;
} }
@ -115,12 +130,19 @@ export class ActivityPubServerService {
}, },
}); });
} catch (e) { } 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); reply.code(401);
return; return;
} }
this.queueService.inbox(request.body as IActivity, signature); this.queueService.inbox(request.body as IActivity, signature);
reply.code(202); reply.code(202);
} }