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",
"request": "launch",
"name": "Generate",
"name": "Run",
"program": "${workspaceFolder}/lib/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [

View File

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

View File

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

View File

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

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()}`;
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);

View File

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

View File

@ -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) {

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

View File

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