From 0c9c04f89d710b89c79ec08e0037a240c41b4aca Mon Sep 17 00:00:00 2001 From: Erin Shepherd Date: Mon, 16 Oct 2023 23:37:09 +0200 Subject: [PATCH] [backend] Refetch user keys when HTTP Signature validation fails If a user has had a key rotation, and nobody on this server follows that user, we will not receive the Update activity with the new key Therefore, when we encounter key validation errors we should check for an up-to-date key. References (other implementations): * [Mastodon](https://github.com/mastodon/mastodon/blob/fc9ab61448af94513524f14f5fd287d2a26ceeab/app/controllers/concerns/signature_verification.rb#L96) * [Akkoma](https://akkoma.dev/AkkomaGang/http_signatures/src/branch/main/lib/http_signatures/http_signatures.ex#L46) --- packages/backend/src/queue/processors/inbox.ts | 16 +++++++++++++++- .../src/remote/activitypub/check-fetch.ts | 16 +++++++++++++++- .../src/remote/activitypub/db-resolver.ts | 13 +++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 51652357f..033f3413b 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -95,11 +95,25 @@ export default async (job: Bull.Job): Promise => { } // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature( + let httpSignatureValidated = httpSignature.verifySignature( signature, authUser.key.keyPem, ); + // If signature validation failed, try refetching the actor + if (!httpSignatureValidated) { + authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user); + + if (authUser.key == null) { + return "skip: failed to re-resolve user publicKey"; + } + + httpSignatureValidated = httpSignature.verifySignature( + signature, + authUser.key.keyPem, + ); + } + // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index e7d011d90..b583a4d7e 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -87,11 +87,25 @@ export async function checkFetch(req: IncomingMessage): Promise { } // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature( + let httpSignatureValidated = httpSignature.verifySignature( signature, authUser.key.keyPem, ); + // If signature validation failed, try refetching the actor + if (!httpSignatureValidated) { + authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user); + + if (authUser.key == null) { + return 403; + } + + httpSignatureValidated = httpSignature.verifySignature( + signature, + authUser.key.keyPem, + ); + } + if (!httpSignatureValidated) { return 403; } diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 52626f399..d8aa3b706 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -17,7 +17,7 @@ import { Cache } from "@/misc/cache.js"; import { uriPersonCache, userByIdCache } from "@/services/user-cache.js"; import type { IObject } from "./type.js"; import { getApId } from "./type.js"; -import { resolvePerson } from "./models/person.js"; +import { resolvePerson, updatePerson } from "./models/person.js"; import {redisClient, subscriber} from "@/db/redis.js"; const publicKeyCache = new Cache("publicKey", 60 * 30); @@ -152,7 +152,7 @@ export default class DbResolver { */ public async getAuthUserFromKeyId(keyId: string): Promise<{ user: CacheableRemoteUser; - key: UserPublickey; + key: UserPublickey | null; } | null> { const key = await publicKeyCache.fetch( keyId, @@ -204,6 +204,15 @@ export default class DbResolver { key, }; } + + public async refetchPublicKeyForApId(user: CacheableRemoteUser): Promise { + await updatePerson(user.uri!, undefined, undefined, user); + let key = await UserPublickeys.findOneBy({ userId: user.id }); + if (key != null) { + await publicKeyByUserIdCache.set(user.id, key); + } + return key; + } } subscriber.on("message", async (_, data) => {