forked from shadowfacts/shadowfacts.net
44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
|
import crypto, { createVerify } from "crypto";
|
||
|
import { Request, Response, NextFunction } from "express";
|
||
|
import { fetchActor } from "../federate";
|
||
|
import { IncomingHttpHeaders } from "http";
|
||
|
|
||
|
export = async (req: Request, res: Response, next: NextFunction) => {
|
||
|
if (req.method !== "POST") {
|
||
|
next();
|
||
|
return;
|
||
|
}
|
||
|
const actor = await fetchActor(req.body.actor as string);
|
||
|
if (validate(req, actor.publicKey.publicKeyPem)) {
|
||
|
next();
|
||
|
} else {
|
||
|
console.log(`Could not validate HTTP signature for ${req.body.actor}`);
|
||
|
res.status(401).end("Could not validate HTTP signature");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
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;
|
||
|
}
|