From 26e449a72a4dd44bc2271548adfac5f8a13800dc Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 26 Nov 2023 17:31:51 +0100 Subject: [PATCH] [backend] Fix HTTP signature validation Co-authored-by: perillamint Co-authored-by: yunochi --- .../backend/src/queue/processors/inbox.ts | 5 +++ .../src/remote/activitypub/check-fetch.ts | 30 ++++++++++++++- packages/backend/src/server/activitypub.ts | 37 ++++++++++++++++--- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 033f3413b..1985edaac 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -22,6 +22,7 @@ import { StatusError } from "@/misc/fetch.js"; import type { CacheableRemoteUser } from "@/models/entities/user.js"; import type { UserPublickey } from "@/models/entities/user-publickey.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js"; +import { verifySignature } from "@/remote/activitypub/check-fetch.js"; const logger = new Logger("inbox"); @@ -114,6 +115,10 @@ export default async (job: Bull.Job): Promise => { ); } + if (httpSignatureValidated) { + if (!verifySignature(signature, authUser.key)) return `skip: Invalid HTTP signature`; + } + // また、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 96bc21495..b7a4b4742 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -1,5 +1,5 @@ import { URL } from "url"; -import httpSignature from "@peertube/http-signature"; +import httpSignature, { IParsedSignature } from "@peertube/http-signature"; import config from "@/config/index.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { toPuny } from "@/misc/convert-host.js"; @@ -9,6 +9,9 @@ import { shouldBlockInstance } from "@/misc/should-block-instance.js"; import type { IncomingMessage } from "http"; import type { CacheableRemoteUser } from "@/models/entities/user.js"; import type { UserPublickey } from "@/models/entities/user-publickey.js"; +import { verify } from "node:crypto"; +import { toSingle } from "@/prelude/array.js"; +import { createHash } from "node:crypto"; export async function hasSignature(req: IncomingMessage): Promise { const meta = await fetchMeta(); @@ -28,10 +31,12 @@ export async function hasSignature(req: IncomingMessage): Promise { export async function checkFetch(req: IncomingMessage): Promise { const meta = await fetchMeta(); if (meta.secureMode || meta.privateMode) { + if (req.headers.host !== config.host) return 400; + let signature; try { - signature = httpSignature.parseRequest(req, { headers: [] }); + signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"] }); } catch (e) { return 401; } @@ -114,6 +119,8 @@ export async function checkFetch(req: IncomingMessage): Promise { if (!httpSignatureValidated) { return 403; } + + return verifySignature(signature, authUser.key) ? 200 : 401; } return 200; } @@ -136,3 +143,22 @@ export async function getSignatureUser(req: IncomingMessage): Promise<{ keyId.hash = ""; return await dbResolver.getAuthUserFromApId(getApId(keyId.toString())); } + +export function verifySignature(sig: IParsedSignature, key: UserPublickey): boolean { + if (!['hs2019', 'rsa-sha256'].includes(sig.algorithm.toLowerCase())) return false; + try { + return verify('rsa-sha256', Buffer.from(sig.signingString, 'utf8'), key.keyPem, Buffer.from(sig.params.signature, 'base64')); + } + catch { + // Algo not supported + return false; + } +} + +export function verifyDigest(body: string, digest: string | string[] | undefined): boolean { + digest = toSingle(digest); + if (body == null || digest == null || !digest.toLowerCase().startsWith('sha-256=')) + return false; + + return createHash('sha256').update(body).digest('base64') === digest.substring(8); +} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index f9d5eb99c..9df6a0c7d 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -1,5 +1,5 @@ import Router from "@koa/router"; -import json from "koa-json-body"; +import bodyParser from "koa-bodyparser"; import httpSignature from "@peertube/http-signature"; import { In, IsNull, Not } from "typeorm"; @@ -22,8 +22,8 @@ import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { getUserKeypair } from "@/misc/keypair-store.js"; import { checkFetch, - hasSignature, getSignatureUser, + verifyDigest, } from "@/remote/activitypub/check-fetch.js"; import { getInstanceActor } from "@/services/instance-actor.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; @@ -33,6 +33,8 @@ import Following from "./activitypub/following.js"; import Followers from "./activitypub/followers.js"; import Outbox, { packActivity } from "./activitypub/outbox.js"; import { serverLogger } from "./index.js"; +import config from "@/config/index.js"; +import Koa from "koa"; // Init router const router = new Router(); @@ -40,15 +42,25 @@ const router = new Router(); //#region Routing function inbox(ctx: Router.RouterContext) { + if (ctx.req.headers.host !== config.host) { + ctx.status = 400; + return; + } + let signature; try { - signature = httpSignature.parseRequest(ctx.req, { headers: [] }); + signature = httpSignature.parseRequest(ctx.req, { headers: ['(request-target)', 'digest', 'host', 'date'] }); } catch (e) { ctx.status = 401; return; } + if (!verifyDigest(ctx.request.rawBody, ctx.headers.digest)) { + ctx.status = 401; + return; + } + processInbox(ctx.request.body, signature); ctx.status = 202; @@ -73,9 +85,24 @@ export function setResponseType(ctx: Router.RouterContext) { } } +async function parseJsonBodyOrFail(ctx: Router.RouterContext, next: Koa.Next) { + const koaBodyParser = bodyParser({ + enableTypes: ["json"], + detectJSON: () => true, + }); + + try { + await koaBodyParser(ctx, next); + } + catch { + ctx.status = 400; + return; + } +} + // inbox -router.post("/inbox", json(), inbox); -router.post("/users/:user/inbox", json(), inbox); +router.post("/inbox", parseJsonBodyOrFail, inbox); +router.post("/users/:user/inbox", parseJsonBodyOrFail, inbox); // note router.get("/notes/:note", async (ctx, next) => {