shadowfacts.net/lib/activitypub/middleware/http-signature.ts

62 lines
2.3 KiB
TypeScript

import crypto from "crypto";
import { Request, Response, NextFunction } from "express";
import { getActor } from "../federate";
export = async (req: Request, res: Response, next: NextFunction) => {
if (req.method !== "POST") {
next();
return;
}
const actor = await getActor(req.body.actor as string);
if (actor && actor.publicKey && validate(req, actor.publicKey.publicKeyPem)) {
next();
} else {
// if the first check fails, force re-fetch the actor and try again
const actor = await getActor(req.body.actor as string, true);
if (!actor) {
// probably caused by Delete activity for an actor
if (req.body.type === "Delete") {
// if we don't have a cached copy of the key, we have had no interaction with actor
// so the Delete can be safely ignored
// we still send a 200 OK status, so that the originating instances knows it has successfully
// delivered the Delete (we just can't act on it)
res.status(200).end();
} else {
console.log(`Could not retrieve actor ${req.body.actor} to validate HTTP signature for`, req.body);
res.status(401).end("Could not retrieve actor to validate HTTP signature");
}
} else if (!validate(req, actor.publicKey.publicKeyPem)) {
console.log(`Could not validate HTTP signature for ${req.body.actor}`);
res.status(401).end("Could not validate HTTP signature");
} else {
next();
}
}
};
function validate(req: Request, publicKeyPem: string): boolean {
const signature = parseSignature(req.header("signature")!);
const usedHeaders = signature.get("headers")!.split(/\s/);
const signingString = usedHeaders.map(header => {
const value = header === "(request-target)" ? `${req.method} ${req.path}`.toLowerCase() : req.header(header);
return `${header}: ${value}`;
}).join("\n");
const verifier = crypto.createVerify("sha256");
verifier.update(signingString);
verifier.end();
return verifier.verify(publicKeyPem, signature.get("signature")!, "base64");
}
function parseSignature(signature: string): Map<string, string> {
const map = new Map<string, string>();
map.set("headers", "date");
for (const part of signature.split(",")) {
const index = part.indexOf("=");
const key = part.substring(0, index);
const value = part.substring(index + 1);
const unquoted = value.replace(/^"+|"+$/g, ""); // strip leading and trailing quotes
map.set(key, unquoted);
}
return map;
}