2019-08-17 19:29:20 +00:00
|
|
|
import crypto from "crypto";
|
2019-02-20 23:07:29 +00:00
|
|
|
import { Request, Response, NextFunction } from "express";
|
2019-02-24 15:21:14 +00:00
|
|
|
import { getActor } from "../federate";
|
2019-02-20 23:07:29 +00:00
|
|
|
|
|
|
|
export = async (req: Request, res: Response, next: NextFunction) => {
|
|
|
|
if (req.method !== "POST") {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
2019-08-17 18:50:18 +00:00
|
|
|
const actor = await getActor(req.body.actor as string);
|
2019-09-18 12:28:17 +00:00
|
|
|
if (actor && actor.publicKey && validate(req, actor.publicKey.publicKeyPem)) {
|
2019-02-20 23:07:29 +00:00
|
|
|
next();
|
|
|
|
} else {
|
2019-02-24 15:21:14 +00:00
|
|
|
// if the first check fails, force re-fetch the actor and try again
|
2019-08-17 18:50:18 +00:00
|
|
|
const actor = await getActor(req.body.actor as string, true);
|
2019-06-30 18:59:39 +00:00
|
|
|
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");
|
|
|
|
}
|
2020-04-15 16:55:16 +00:00
|
|
|
} else if (!actor.publicKey || !actor.publicKey.publicKeyPem || !validate(req, actor.publicKey.publicKeyPem)) {
|
2019-02-24 15:21:14 +00:00
|
|
|
console.log(`Could not validate HTTP signature for ${req.body.actor}`);
|
|
|
|
res.status(401).end("Could not validate HTTP signature");
|
2019-06-30 18:59:39 +00:00
|
|
|
} else {
|
|
|
|
next();
|
2019-02-24 15:21:14 +00:00
|
|
|
}
|
2019-02-20 23:07:29 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
2019-08-17 18:50:18 +00:00
|
|
|
}
|