Validate HTTP signatures
This commit is contained in:
parent
2adb93395e
commit
9191a9d987
|
@ -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": [
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
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;
|
|
||||||
}
|
}
|
17
lib/index.ts
17
lib/index.ts
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue