HTTP Signatureがなかったり使えなかったりしそうな場合にLD Signatureを活用するように
This commit is contained in:
parent
da4a44b337
commit
8104963e1d
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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の検証
|
||||||
|
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の検証
|
if (
|
||||||
const errorLogger = (ms: any) => this.logger.error(ms);
|
authUser == null ||
|
||||||
const httpSignatureValidated = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger);
|
httpSignatureIsValid !== true ||
|
||||||
this.logger.debug('Inbox message validation: ', {
|
authUser.user.uri !== actorUri // 一応チェック
|
||||||
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) {
|
|
||||||
// 一致しなくても、でも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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue