From cf5b42a160ae8a4d94bf3dcea04ce12935ca4f76 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Fri, 9 Feb 2024 04:25:57 +0900 Subject: [PATCH] feat: accept more signature algorithms like ECDSA/Ed25519 cf. 3272b908c63b24f056d01c180e546f124009a6d1 Co-authored-by: naskya --- .../src/remote/activitypub/check-fetch.ts | 19 ----- packages/backend/src/server/activitypub.ts | 77 ++++++++++++++++++- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index ab46d97f85..a170e3eb3a 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -10,8 +10,6 @@ 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(); @@ -158,20 +156,3 @@ export function verifySignature( 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 3af3d3c7ac..660db98e48 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -23,7 +23,6 @@ import { getUserKeypair } from "@/misc/keypair-store.js"; import { checkFetch, getSignatureUser, - verifyDigest, } from "@/remote/activitypub/check-fetch.js"; import { getInstanceActor } from "@/services/instance-actor.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; @@ -35,6 +34,9 @@ import Outbox, { packActivity } from "./activitypub/outbox.js"; import { serverLogger } from "./index.js"; import config from "@/config/index.js"; import Koa from "koa"; +import * as crypto from "node:crypto"; +import { inspect } from "node:util"; +import type { IActivity } from "@/remote/activitypub/type.js"; // Init router const router = new Router(); @@ -43,27 +45,94 @@ const router = new Router(); function inbox(ctx: Router.RouterContext) { if (ctx.req.headers.host !== config.host) { + serverLogger.warn("inbox: Invalid Host"); ctx.status = 400; + ctx.message = "Invalid Host"; return; } - let signature; + let signature: httpSignature.IParsedSignature; try { signature = httpSignature.parseRequest(ctx.req, { headers: ["(request-target)", "digest", "host", "date"], }); } catch (e) { + serverLogger.warn(`inbox: signature parse error: ${inspect(e)}`); ctx.status = 401; + + if (e instanceof Error) { + if (e.name === "ExpiredRequestError") + ctx.message = "Expired Request Error"; + if (e.name === "MissingHeaderError") + ctx.message = "Missing Required Header"; + } + return; } - if (!verifyDigest(ctx.request.rawBody, ctx.headers.digest)) { + // Validate signature algorithm + if ( + !signature.algorithm + .toLowerCase() + .match(/^((dsa|rsa|ecdsa)-(sha256|sha384|sha512)|ed25519-sha512|hs2019)$/) + ) { + serverLogger.warn( + `inbox: invalid signature algorithm ${signature.algorithm}`, + ); ctx.status = 401; + ctx.message = "Invalid Signature Algorithm"; + return; + + // hs2019 + // keyType=ED25519 => ed25519-sha512 + // keyType=other => (keyType)-sha256 + } + + // Validate digest header + const digest = ctx.req.headers.digest; + + if (typeof digest !== "string") { + serverLogger.warn( + "inbox: zero or more than one digest header(s) are present", + ); + ctx.status = 401; + ctx.message = "Invalid Digest Header"; return; } - processInbox(ctx.request.body, signature); + const match = digest.match(/^([0-9A-Za-z-]+)=(.+)$/); + + if (match == null) { + serverLogger.warn("inbox: unrecognized digest header"); + ctx.status = 401; + ctx.message = "Invalid Digest Header"; + return; + } + + const digestAlgo = match[1]; + const expectedDigest = match[2]; + + if (digestAlgo.toUpperCase() !== "SHA-256") { + serverLogger.warn("inbox: unsupported digest algorithm"); + ctx.status = 401; + ctx.message = "Unsupported Digest Algorithm"; + return; + } + + const actualDigest = crypto + .createHash("sha256") + .update(ctx.request.rawBody) + .digest("base64"); + + if (expectedDigest !== actualDigest) { + serverLogger.warn("inbox: Digest Mismatch"); + ctx.status = 401; + ctx.message = "Digest Missmatch"; + return; + } + + processInbox(ctx.request.body as IActivity, signature); ctx.status = 202; }