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 { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||||
import type { UserPublickey } from "@/models/entities/user-publickey.js";
|
import type { UserPublickey } from "@/models/entities/user-publickey.js";
|
||||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||||
|
import { verifySignature } from "@/remote/activitypub/check-fetch.js";
|
||||||
|
|
||||||
const logger = new Logger("inbox");
|
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と一致する必要がある
|
// また、signatureのsignerは、activity.actorと一致する必要がある
|
||||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import httpSignature from "@peertube/http-signature";
|
import httpSignature, { IParsedSignature } from "@peertube/http-signature";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
import { toPuny } from "@/misc/convert-host.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 { IncomingMessage } from "http";
|
||||||
import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||||
import type { UserPublickey } from "@/models/entities/user-publickey.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> {
|
export async function hasSignature(req: IncomingMessage): Promise<string> {
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
|
@ -28,10 +31,12 @@ export async function hasSignature(req: IncomingMessage): Promise<string> {
|
||||||
export async function checkFetch(req: IncomingMessage): Promise<number> {
|
export async function checkFetch(req: IncomingMessage): Promise<number> {
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
if (meta.secureMode || meta.privateMode) {
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
if (req.headers.host !== config.host) return 400;
|
||||||
|
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(req, { headers: [] });
|
signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 401;
|
return 401;
|
||||||
}
|
}
|
||||||
|
@ -108,6 +113,8 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
|
||||||
if (!httpSignatureValidated) {
|
if (!httpSignatureValidated) {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return verifySignature(signature, authUser.key) ? 200 : 401;
|
||||||
}
|
}
|
||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
@ -130,3 +137,22 @@ export async function getSignatureUser(req: IncomingMessage): Promise<{
|
||||||
keyId.hash = "";
|
keyId.hash = "";
|
||||||
return await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
|
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 Router from "@koa/router";
|
||||||
import json from "koa-json-body";
|
import bodyParser from "koa-bodyparser";
|
||||||
import httpSignature from "@peertube/http-signature";
|
import httpSignature from "@peertube/http-signature";
|
||||||
|
|
||||||
import { In, IsNull, Not } from "typeorm";
|
import { In, IsNull, Not } from "typeorm";
|
||||||
|
@ -23,6 +23,7 @@ import { getUserKeypair } from "@/misc/keypair-store.js";
|
||||||
import {
|
import {
|
||||||
checkFetch,
|
checkFetch,
|
||||||
getSignatureUser,
|
getSignatureUser,
|
||||||
|
verifyDigest,
|
||||||
} from "@/remote/activitypub/check-fetch.js";
|
} from "@/remote/activitypub/check-fetch.js";
|
||||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.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 Followers from "./activitypub/followers.js";
|
||||||
import Outbox, { packActivity } from "./activitypub/outbox.js";
|
import Outbox, { packActivity } from "./activitypub/outbox.js";
|
||||||
import { serverLogger } from "./index.js";
|
import { serverLogger } from "./index.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
import Koa from "koa";
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
@ -39,15 +42,25 @@ const router = new Router();
|
||||||
//#region Routing
|
//#region Routing
|
||||||
|
|
||||||
function inbox(ctx: Router.RouterContext) {
|
function inbox(ctx: Router.RouterContext) {
|
||||||
|
if (ctx.req.headers.host !== config.host) {
|
||||||
|
ctx.status = 400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(ctx.req, { headers: [] });
|
signature = httpSignature.parseRequest(ctx.req, { headers: ['(request-target)', 'digest', 'host', 'date'] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!verifyDigest(ctx.request.rawBody, ctx.headers.digest)) {
|
||||||
|
ctx.status = 401;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
processInbox(ctx.request.body, signature);
|
processInbox(ctx.request.body, signature);
|
||||||
|
|
||||||
ctx.status = 202;
|
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
|
// inbox
|
||||||
router.post("/inbox", json(), inbox);
|
router.post("/inbox", parseJsonBodyOrFail, inbox);
|
||||||
router.post("/users/:user/inbox", json(), inbox);
|
router.post("/users/:user/inbox", parseJsonBodyOrFail, inbox);
|
||||||
|
|
||||||
// note
|
// note
|
||||||
router.get("/notes/:note", async (ctx, next) => {
|
router.get("/notes/:note", async (ctx, next) => {
|
||||||
|
|
Loading…
Reference in a new issue