Validate HTTP signatures

This commit is contained in:
Shadowfacts 2019-02-20 18:07:29 -05:00
parent 2adb93395e
commit 9191a9d987
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
10 changed files with 65 additions and 36 deletions

2
.vscode/launch.json vendored
View File

@ -7,7 +7,7 @@
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Generate", "name": "Run",
"program": "${workspaceFolder}/lib/index.ts", "program": "${workspaceFolder}/lib/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json", "preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [ "outFiles": [

View File

@ -37,4 +37,7 @@ export interface Note extends Activity {
export interface Actor { export interface Actor {
id: string; id: string;
inbox: string; inbox: string;
publicKey: {
publicKeyPem: string;
};
} }

View File

@ -3,9 +3,7 @@ import { promises as fs } from "fs";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
export default async function actor(): Promise<Router> { export default async function actor(router: Router) {
const router = Router();
const pubKeyPem = (await fs.readFile(process.env.PUB_KEY_PEM!)).toString(); const pubKeyPem = (await fs.readFile(process.env.PUB_KEY_PEM!)).toString();
const actorObj = { const actorObj = {
"@context": [ "@context": [
@ -39,6 +37,4 @@ export default async function actor(): Promise<Router> {
res.redirect("/"); res.redirect("/");
} }
}); });
return router;
} }

View File

@ -54,9 +54,7 @@ export async function toFederate(db: Database): Promise<[string, Article][]> {
}); });
} }
export function router(): Router { export function route(router: Router) {
const router = Router();
router.use("/:category/:year/:slug/", (req, res, next) => { router.use("/:category/:year/:slug/", (req, res, next) => {
if (req.accepts("text/html")) { if (req.accepts("text/html")) {
next(); next();
@ -74,6 +72,4 @@ export function router(): Router {
}); });
} }
}); });
return router;
} }

View File

@ -49,9 +49,8 @@ export async function signAndSend(activity: Activity, inbox: string) {
const stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${date.toUTCString()}`; const stringToSign = `(request-target): post ${inboxFragment}\nhost: ${targetDomain}\ndate: ${date.toUTCString()}`;
signer.update(stringToSign); signer.update(stringToSign);
signer.end(); signer.end();
const signature = signer.sign(privKey); const signature = signer.sign(privKey, "base64");
const base64Signature = signature.toString("base64"); const header = `keyId="https://${domain}/ap/actor#main-key",headers="(request-target) host date",signature="${signature}"`;
const header = `keyId="https://${domain}/ap/actor#main-key",headers="(request-target) host date",signature="${base64Signature}"`;
console.log("Sending:", activity); console.log("Sending:", activity);
console.log("stringToSign:", stringToSign); console.log("stringToSign:", stringToSign);
console.log("Signature: " + header); console.log("Signature: " + header);

View File

@ -3,9 +3,7 @@ import { Database } from "sqlite3";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
export default function followers(): Router { export default function followers(router: Router) {
const router = Router();
router.get("/ap/actor/followers", (req, res) => { router.get("/ap/actor/followers", (req, res) => {
const db = <Database>req.app.get("db"); const db = <Database>req.app.get("db");
@ -22,6 +20,4 @@ export default function followers(): Router {
res.end(); res.end();
}); });
}); });
return router;
} }

View File

@ -7,13 +7,9 @@ import { URL } from "url";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
export default function inbox(): Router { export default function inbox(router: Router) {
const router = Router();
router.post("/ap/inbox", handleInbox); router.post("/ap/inbox", handleInbox);
router.post("/inbox", handleInbox); router.post("/inbox", handleInbox);
return router;
} }
async function handleInbox(req: Request, res: Response) { async function handleInbox(req: Request, res: Response) {

View File

@ -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;
}

View File

@ -2,9 +2,7 @@ import express, { Router } from "express";
const domain = process.env.DOMAIN; const domain = process.env.DOMAIN;
export default function webfinger(): Router { export default function webfinger(router: Router) {
const router = Router();
router.get("/.well-known/webfinger", (req, res) => { router.get("/.well-known/webfinger", (req, res) => {
res.json({ res.json({
"subject": `acct:shadowfacts@${domain}`, "subject": `acct:shadowfacts@${domain}`,
@ -18,6 +16,4 @@ export default function webfinger(): Router {
}); });
res.end(); res.end();
}); });
return router;
} }

View File

@ -1,10 +1,11 @@
import { Page } from "./metadata"; import { Page } from "./metadata";
import generators from "./generate"; import generators from "./generate";
import express from "express"; import express, { Router } from "express";
import morgan from "morgan"; import morgan from "morgan";
import bodyParser from "body-parser"; import bodyParser from "body-parser";
import activitypub from "./activitypub"; import activitypub from "./activitypub";
import validateHttpSig from "./activitypub/middleware/http-signature";
import sqlite3 from "sqlite3"; import sqlite3 from "sqlite3";
@ -43,12 +44,14 @@ app.use(bodyParser.json({ type: "application/activity+json" }));
await activitypub.articles.setup(posts, db); await activitypub.articles.setup(posts, db);
const toFederate = await activitypub.articles.toFederate(db); const toFederate = await activitypub.articles.toFederate(db);
const apRouter = Router();
app.use(await activitypub.actor()); apRouter.use(validateHttpSig);
app.use(activitypub.followers()); await activitypub.actor(apRouter);
app.use(activitypub.inbox()); activitypub.followers(apRouter);
app.use(activitypub.webfinger()); activitypub.inbox(apRouter);
app.use(activitypub.articles.router()); activitypub.webfinger(apRouter);
activitypub.articles.route(apRouter);
app.use(apRouter);
app.use(express.static("out")); app.use(express.static("out"));
const port = process.env.PORT || 8083; const port = process.env.PORT || 8083;