[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](fc9ab61448/app/controllers/concerns/signature_verification.rb (L96))
 * [Akkoma](https://akkoma.dev/AkkomaGang/http_signatures/src/branch/main/lib/http_signatures/http_signatures.ex#L46)
This commit is contained in:
Erin Shepherd 2023-10-16 23:37:09 +02:00 committed by Laura Hausmann
parent 092462d3a9
commit 0c9c04f89d
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 41 additions and 4 deletions

View file

@ -95,11 +95,25 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature( let httpSignatureValidated = httpSignature.verifySignature(
signature, signature,
authUser.key.keyPem, 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と一致する必要がある // また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る // 一致しなくても、でもLD-Signatureがありそうならそっちも見る

View file

@ -87,11 +87,25 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature( let httpSignatureValidated = httpSignature.verifySignature(
signature, signature,
authUser.key.keyPem, 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) { if (!httpSignatureValidated) {
return 403; return 403;
} }

View file

@ -17,7 +17,7 @@ import { Cache } from "@/misc/cache.js";
import { uriPersonCache, userByIdCache } from "@/services/user-cache.js"; import { uriPersonCache, userByIdCache } from "@/services/user-cache.js";
import type { IObject } from "./type.js"; import type { IObject } from "./type.js";
import { getApId } 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"; import {redisClient, subscriber} from "@/db/redis.js";
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30); const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
@ -152,7 +152,7 @@ export default class DbResolver {
*/ */
public async getAuthUserFromKeyId(keyId: string): Promise<{ public async getAuthUserFromKeyId(keyId: string): Promise<{
user: CacheableRemoteUser; user: CacheableRemoteUser;
key: UserPublickey; key: UserPublickey | null;
} | null> { } | null> {
const key = await publicKeyCache.fetch( const key = await publicKeyCache.fetch(
keyId, keyId,
@ -204,6 +204,15 @@ export default class DbResolver {
key, key,
}; };
} }
public async refetchPublicKeyForApId(user: CacheableRemoteUser): Promise<UserPublickey | null> {
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) => { subscriber.on("message", async (_, data) => {