Skip to content

Commit

Permalink
feat(federation): ✨ Add cryptography
Browse files Browse the repository at this point in the history
  • Loading branch information
CPlusPatch committed May 14, 2024
1 parent 967ceb8 commit b86933e
Show file tree
Hide file tree
Showing 5 changed files with 516 additions and 6 deletions.
8 changes: 7 additions & 1 deletion federation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ This library is built for JavaScript runtimes with the support for:

- [**ES Modules**](https://nodejs.org/api/esm.html)
- [**ECMAScript 2020**](https://www.ecma-international.org/ecma-262/11.0/index.html)
- (only required for cryptography) [**Ed25519**](https://en.wikipedia.org/wiki/EdDSA) cryptography in the [**WebCrypto API**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)

#### Runtimes

- **Node.js**: 14.0+ is the minimum, but only Node.js 20.0+ (LTS) is officially supported.
- **Node.js**: 14.0+ is the minimum (18.0+ for cryptography), but only Node.js 20.0+ (LTS) is officially supported.
- **Deno**: Support is unknown. 1.0+ is expected to work.
- **Bun**: Bun 1.1.8 is the minimum-supported version. As Bun is rapidly evolving, this may change. Previous versions may also work.

Expand All @@ -95,6 +96,11 @@ Consequently, this library is compatible without any bundling in the following b
- **Opera**: 67+
- **Internet Explorer**: None

Cryptography functions are supported in the following browsers:

- **Safari**: 17.0+
- **Chrome**: 113.0+ with `#enable-experimental-web-platform-features` enabled

If you are targeting older browsers, please don't, you are doing yourself a disservice.

Transpilation to non-ES Module environments is not officially supported, but should be simple with the use of a bundler like [**Parcel**](https://parceljs.org) or [**Rollup**](https://rollupjs.org).
Expand Down
184 changes: 184 additions & 0 deletions federation/cryptography/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { describe, beforeAll, test, expect, beforeEach } from "bun:test";
import { SignatureValidator, SignatureConstructor } from "./index";

describe("SignatureValidator", () => {
let validator: SignatureValidator;
let privateKey: CryptoKey;
let publicKey: CryptoKey;
let body: string;
let signature: string;
let date: string;

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("Signature") ?? "";
date = headers.get("Date") ?? "";
});

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: {
Signature: signature,
Date: date,
},
body: body,
});
const isValid = await validator.validate(request);
expect(isValid).toBe(true);
});

test("should throw with an invalid signature", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
Signature: "invalid",
Date: date,
},
body: body,
});

expect(() => validator.validate(request)).toThrow(TypeError);
});

test("should throw with missing headers", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
Signature: signature,
},
body: body,
});
expect(() => validator.validate(request)).toThrow(TypeError);
});

test("should throw with missing date", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
Signature: signature,
},
body: body,
});
expect(() => validator.validate(request)).toThrow(TypeError);
});

test("should not verify a valid signature with a different body", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
Signature: signature,
Date: date,
},
body: "different",
});

const isValid = await validator.validate(request);
expect(isValid).toBe(false);
});

test("should not verify a signature with a wrong key", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
Signature:
'keyId="badbbadwrong",algorithm="ed25519",headers="(request-target) host date digest",signature="ohno"',
Date: date,
},
body: body,
});

const isValid = await validator.validate(request);
expect(isValid).toBe(false);
});
});
});

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);
expect(headers.get("Signature")).toBeDefined();
expect(headers.get("Date")).toBeDefined();

// Check structure of Signature
const signature = headers.get("Signature") ?? "";
const parts = signature.split(",");
expect(parts).toHaveLength(4);

expect(parts[0].split("=")[0]).toBe("keyId");
expect(parts[1].split("=")[0]).toBe("algorithm");
expect(parts[2].split("=")[0]).toBe("headers");
expect(parts[3].split("=")[0]).toBe("signature");

expect(parts[0].split("=")[1]).toBe(
'"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51"',
);
expect(parts[1].split("=")[1]).toBe('"ed25519"');
expect(parts[2].split("=")[1]).toBe(
'"(request-target) host date digest"',
);
expect(parts[3].split("=")[1]).toBeString();
});
});
});
Loading

0 comments on commit b86933e

Please sign in to comment.