Validate HTTP signatures
This commit is contained in:
parent
2adb93395e
commit
9191a9d987
|
@ -7,7 +7,7 @@
|
|||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Generate",
|
||||
"name": "Run",
|
||||
"program": "${workspaceFolder}/lib/index.ts",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": [
|
||||
|
|
|
@ -37,4 +37,7 @@ export interface Note extends Activity {
|
|||
export interface Actor {
|
||||
id: string;
|
||||
inbox: string;
|
||||
publicKey: {
|
||||
publicKeyPem: string;
|
||||
};
|
||||
}
|
|
@ -3,9 +3,7 @@ import { promises as fs } from "fs";
|
|||
|
||||
const domain = process.env.DOMAIN;
|
||||
|
||||
export default async function actor(): Promise<Router> {
|
||||
const router = Router();
|
||||
|
||||
export default async function actor(router: Router) {
|
||||
const pubKeyPem = (await fs.readFile(process.env.PUB_KEY_PEM!)).toString();
|
||||
const actorObj = {
|
||||
"@context": [
|
||||
|
@ -39,6 +37,4 @@ export default async function actor(): Promise<Router> {
|
|||
res.redirect("/");
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
|
@ -54,9 +54,7 @@ export async function toFederate(db: Database): Promise<[string, Article][]> {
|
|||
});
|
||||
}
|
||||
|
||||
export function router(): Router {
|
||||
const router = Router();
|
||||
|
||||
export function route(router: Router) {
|
||||
router.use("/:category/:year/:slug/", (req, res, next) => {
|
||||
if (req.accepts("text/html")) {
|
||||
next();
|
||||
|
@ -74,6 +72,4 @@ export function router(): Router {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
|
@ -49,9 +49,8 @@ export async function signAndSend(activity: Activity, inbox: string) {
|
|||
const stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${date.toUTCString()}`;
|
||||
signer.update(stringToSign);
|
||||
signer.end();
|
||||
const signature = signer.sign(privKey);
|
||||
const base64Signature = signature.toString("base64");
|
||||
const header = `keyId="https://${domain}/ap/actor#main-key",headers="(request-target) host date",signature="${base64Signature}"`;
|
||||
const signature = signer.sign(privKey, "base64");
|
||||
const header = `keyId="https://${domain}/ap/actor#main-key",headers="(request-target) host date",signature="${signature}"`;
|
||||
console.log("Sending:", activity);
|
||||
console.log("stringToSign:", stringToSign);
|
||||
console.log("Signature: " + header);
|
||||
|
|
|
@ -3,9 +3,7 @@ import { Database } from "sqlite3";
|
|||
|
||||
const domain = process.env.DOMAIN;
|
||||
|
||||
export default function followers(): Router {
|
||||
const router = Router();
|
||||
|
||||
export default function followers(router: Router) {
|
||||
router.get("/ap/actor/followers", (req, res) => {
|
||||
const db = <Database>req.app.get("db");
|
||||
|
||||
|
@ -22,6 +20,4 @@ export default function followers(): Router {
|
|||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
|
@ -7,13 +7,9 @@ import { URL } from "url";
|
|||
|
||||
const domain = process.env.DOMAIN;
|
||||
|
||||
export default function inbox(): Router {
|
||||
const router = Router();
|
||||
|
||||
export default function inbox(router: Router) {
|
||||
router.post("/ap/inbox", handleInbox);
|
||||
router.post("/inbox", handleInbox);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function handleInbox(req: Request, res: Response) {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
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;
|
||||
}
|
|
@ -2,9 +2,7 @@ import express, { Router } from "express";
|
|||
|
||||
const domain = process.env.DOMAIN;
|
||||
|
||||
export default function webfinger(): Router {
|
||||
const router = Router();
|
||||
|
||||
export default function webfinger(router: Router) {
|
||||
router.get("/.well-known/webfinger", (req, res) => {
|
||||
res.json({
|
||||
"subject": `acct:shadowfacts@${domain}`,
|
||||
|
@ -18,6 +16,4 @@ export default function webfinger(): Router {
|
|||
});
|
||||
res.end();
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
17
lib/index.ts
17
lib/index.ts
|
@ -1,10 +1,11 @@
|
|||
import { Page } from "./metadata";
|
||||
import generators from "./generate";
|
||||
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
import morgan from "morgan";
|
||||
import bodyParser from "body-parser";
|
||||
import activitypub from "./activitypub";
|
||||
import validateHttpSig from "./activitypub/middleware/http-signature";
|
||||
|
||||
import sqlite3 from "sqlite3";
|
||||
|
||||
|
@ -43,12 +44,14 @@ app.use(bodyParser.json({ type: "application/activity+json" }));
|
|||
await activitypub.articles.setup(posts, db);
|
||||
const toFederate = await activitypub.articles.toFederate(db);
|
||||
|
||||
|
||||
app.use(await activitypub.actor());
|
||||
app.use(activitypub.followers());
|
||||
app.use(activitypub.inbox());
|
||||
app.use(activitypub.webfinger());
|
||||
app.use(activitypub.articles.router());
|
||||
const apRouter = Router();
|
||||
apRouter.use(validateHttpSig);
|
||||
await activitypub.actor(apRouter);
|
||||
activitypub.followers(apRouter);
|
||||
activitypub.inbox(apRouter);
|
||||
activitypub.webfinger(apRouter);
|
||||
activitypub.articles.route(apRouter);
|
||||
app.use(apRouter);
|
||||
app.use(express.static("out"));
|
||||
|
||||
const port = process.env.PORT || 8083;
|
||||
|
|
Loading…
Reference in New Issue