diff --git a/README.md b/README.md index 12c52c06f..5bcb33684 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ ## 細かい変更点 +- 署名アルゴリズムとして ECDSA や Ed25519 なども受け入れる([github.com/mei23/misskey-v12](https://github.com/mei23/misskey-v12) から取り込み) - Pleroma のチャットに対応(Catodon から取り込み) - 翻訳機能にて、投稿言語が指定されていない場合にのみ言語の自動検出を用いるように変更 - アップデート時に更新内容を確認できる機能を追加 diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index ab46d97f8..a170e3eb3 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 c9c05adac..68ccec48c 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; }