fix: 🔒 Fix HTTP signature validation
Co-authored-by: perillamint <perillamint@silicon.moe> Co-authored-by: yunochi <yuno@yunochi.com>
This commit is contained in:
parent
dadb45f609
commit
3272b908c6
3 changed files with 65 additions and 6 deletions
|
@ -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がありそうならそっちも見る
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -108,6 +113,8 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
|
|||
if (!httpSignatureValidated) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
return verifySignature(signature, authUser.key) ? 200 : 401;
|
||||
}
|
||||
return 200;
|
||||
}
|
||||
|
@ -130,3 +137,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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
@ -23,6 +23,7 @@ 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";
|
||||
|
@ -32,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();
|
||||
|
@ -39,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;
|
||||
|
@ -72,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) => {
|
||||
|
|
Loading…
Reference in a new issue