[backend] Fix HTTP signature validation

Co-authored-by: perillamint <perillamint@silicon.moe>
Co-authored-by: yunochi <yuno@yunochi.com>
This commit is contained in:
Laura Hausmann 2023-11-26 17:31:51 +01:00 committed by Laura Hausmann
parent b814ebcdfb
commit 8890902675
3 changed files with 65 additions and 7 deletions

View file

@ -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<InboxJobData>): Promise<string> => {
);
}
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がありそうならそっちも見る

View file

@ -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<string> {
const meta = await fetchMeta();
@ -28,10 +31,12 @@ export async function hasSignature(req: IncomingMessage): Promise<string> {
export async function checkFetch(req: IncomingMessage): Promise<number> {
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<number> {
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);
}

View file

@ -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) => {