api/federation/cryptography/index.test.ts
2025-02-13 17:39:16 +01:00

184 lines
6.3 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, test } from "bun:test";
import { SignatureConstructor, SignatureValidator } from "./index.ts";
describe("SignatureValidator", () => {
let validator: SignatureValidator;
let privateKey: CryptoKey;
let publicKey: CryptoKey;
let body: string;
let signature: string;
let timestamp: Date;
beforeAll(async () => {
const keys = await crypto.subtle.generateKey("Ed25519", true, [
"sign",
"verify",
]);
publicKey = keys.publicKey;
privateKey = keys.privateKey;
body = JSON.stringify({ key: "value" });
const { headers } = await new SignatureConstructor(
privateKey,
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
).sign("GET", new URL("https://example.com"), body);
signature = headers.get("Versia-Signature") ?? "";
timestamp = new Date(Number(headers.get("Versia-Signed-At")) * 1000);
});
test("fromStringKey", async () => {
const base64PublicKey = Buffer.from(
await crypto.subtle.exportKey("spki", publicKey),
).toString("base64");
validator = await SignatureValidator.fromStringKey(base64PublicKey);
expect(validator).toBeInstanceOf(SignatureValidator);
});
describe("Validator", async () => {
beforeEach(() => {
validator = new SignatureValidator(publicKey);
});
test("should verify a valid signature", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"Versia-Signature": signature,
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: body,
});
const isValid = await validator.validate(request);
expect(isValid).toBe(true);
});
test("should return false with an invalid signature", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"Versia-Signature": "invalid",
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: body,
});
const isValid = await validator.validate(request);
expect(isValid).toBe(false);
});
test("should throw with missing timestamp", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"Versia-Signature": signature,
},
body: body,
});
expect(() => validator.validate(request)).toThrow(
"Headers are missing in request: Versia-Signed-At",
);
});
test("should not verify a valid signature with a different body", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"Versia-Signature": signature,
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: "different",
});
const isValid = await validator.validate(request);
expect(isValid).toBe(false);
});
test("should throw if signature is not base64", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"Versia-Signature": "thisIsNotbase64OhNo$^ù",
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: body,
});
expect(() => validator.validate(request)).toThrow(
"Signature is not valid base64",
);
});
});
});
describe("SignatureConstructor", () => {
let ctor: SignatureConstructor;
let privateKey: CryptoKey;
let body: string;
let headers: Headers;
beforeAll(async () => {
const keys = await crypto.subtle.generateKey("Ed25519", true, [
"sign",
"verify",
]);
privateKey = keys.privateKey;
body = JSON.stringify({ key: "value" });
});
beforeEach(() => {
ctor = new SignatureConstructor(
privateKey,
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
);
});
test("fromStringKey", async () => {
const base64PrivateKey = Buffer.from(
await crypto.subtle.exportKey("pkcs8", privateKey),
).toString("base64");
const constructorFromString = await SignatureConstructor.fromStringKey(
base64PrivateKey,
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
);
expect(constructorFromString).toBeInstanceOf(SignatureConstructor);
});
describe("Signing", () => {
test("should correctly sign ", async () => {
const url = new URL("https://example.com");
headers = (await ctor.sign("GET", url, body)).headers;
expect(headers.get("Versia-Signature")).toBeDefined();
expect(headers.get("Versia-Signed-At")).toBeDefined();
expect(headers.get("Versia-Signed-At")?.length).toBeGreaterThan(10);
expect(headers.get("Versia-Signature")?.length).toBeGreaterThan(10);
});
test("should correctly sign a Request", async () => {
const url = new URL("https://example.com");
const request = new Request(url.toString(), {
method: "GET",
body: body,
});
const { request: newRequest } = await ctor.sign(request);
headers = newRequest.headers;
expect(headers.get("Versia-Signature")).toBeDefined();
expect(headers.get("Versia-Signed-At")).toBeDefined();
expect(await newRequest.text()).toBe(body);
});
test("signing should also output a signed string", async () => {
const url = new URL("https://example.com");
const { signedString } = await ctor.sign("GET", url, body);
expect(signedString).toBeString();
expect(signedString.length).toBeGreaterThan(10);
});
});
});