Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notary server endpoints #5

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import typia, { tags } from "typia";

// // https://spec.matrix.org/v1.9/server-server-api/#get_matrixkeyv2server

interface Response {
export interface Response {
old_verify_keys: Record<
string,
{
Expand Down
6 changes: 6 additions & 0 deletions packages/homeserver/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ export const KeysDTO = t.Object(
},
);

export const NotaryServerKeysDTO = t.Object(
{
server_keys: t.Array(KeysDTO),
},
);

export const StrippedStateDTO = t.Object(
{
content: t.Object(
Expand Down
1 change: 1 addition & 0 deletions packages/homeserver/src/helpers/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isTruthy = <T>(x: T | null | undefined | 0 | false | ''): x is T => Boolean(x);
58 changes: 25 additions & 33 deletions packages/homeserver/src/plugins/mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import type { EventBase } from "@hs/core/src/events/eventBase";
export interface Server {
_id: string;
name: string;
url: string;
keys: {
[key: `${string}:${string}`]: {
old_verify_keys: Record<
string,
{
expired_ts: number;
key: string;
validUntil: number;
};
};
}
>;
server_name: string;
signatures: Record<string, Record<string, string>>;
valid_until_ts: number;
verify_keys: Record<
string,
{
key: string;
}
>;

// signatures: {
// from: string;
Expand Down Expand Up @@ -113,49 +122,32 @@ export const routerWithMongodb = (db: Db) =>
.toArray();
};

const getValidPublicKeyFromLocal = async (
const getValidServerKeysFromLocal = async (
origin: string,
key: string,
): Promise<string | undefined> => {
const server = await serversCollection.findOne({
) => {
return serversCollection.findOne({
name: origin,
valid_until_ts: { $gte: Date.now() },
});
if (!server) {
return;
}
const [, publicKey] =
Object.entries(server.keys).find(
([protocolAndVersion, value]) => protocolAndVersion === key && value.validUntil > Date.now(),
) ?? [];
return publicKey?.key;
};

const storePublicKey = async (
const storeServerKeys = async (
origin: string,
key: string,
value: string,
validUntil: number,
serverKeys: Omit<Server, '_id' | 'name'>,
) => {
await serversCollection.findOneAndUpdate(
{ name: origin },
{ name: origin, valid_until_ts: { $gte: Date.now() }, },
{
$set: {
keys: {
[key]: {
key: value,
validUntil,
},
}
}
$set: serverKeys,
},
{ upsert: true },
);
};

return {
serversCollection,
getValidPublicKeyFromLocal,
storePublicKey,
getValidServerKeysFromLocal,
storeServerKeys,

eventsCollection,
getDeepEarliestAndLatestEvents,
Expand Down
38 changes: 30 additions & 8 deletions packages/homeserver/src/plugins/validateHeaderSignature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import Elysia from "elysia";
import { type SigningKey, generateKeyPairsFromString } from "../keys";
import { authorizationHeaders } from "../authentication";
import { toUnpaddedBase64 } from "../binaryData";
import { encodeCanonicalJson, signJson } from "../signJson";
import { signJson } from "../signJson";

describe("validateHeaderSignature getting public key from local", () => {
let app: Elysia<any, any, any, any, any, any>;
let signature: SigningKey;
const key = 'ed25519:a_yNbw';

beforeAll(async () => {
signature = await generateKeyPairsFromString(
Expand All @@ -31,10 +32,10 @@ describe("validateHeaderSignature getting public key from local", () => {
version: "org.matrix.msc3757.10",
})
.decorate("mongo", {
getValidPublicKeyFromLocal: async () => {
return toUnpaddedBase64(signature.publicKey);
getValidServerKeysFromLocal: async () => {
return { verify_keys: { [key]: { key: toUnpaddedBase64(signature.publicKey) } } };
},
storePublicKey: async () => {
storeServerKeys: async () => {
return;
},
eventsCollection: {
Expand Down Expand Up @@ -90,7 +91,6 @@ describe("validateHeaderSignature getting public key from local", () => {
"GET",
"/",
);

const resp = await app.handle(
new Request("http://localhost/", {
headers: {
Expand Down Expand Up @@ -183,10 +183,10 @@ describe("validateHeaderSignature getting public key from remote", () => {
version: "org.matrix.msc3757.10",
})
.decorate("mongo", {
getValidPublicKeyFromLocal: async () => {
getValidServerKeysFromLocal: async () => {
return;
},
storePublicKey: async () => {
storeServerKeys: async () => {
return;
},
eventsCollection: {
Expand Down Expand Up @@ -226,7 +226,7 @@ describe("validateHeaderSignature getting public key from remote", () => {
"synapse1",
);

mock("https://synapse1/_matrix/key/v2/server", { data: result });
mock("https://synapse1:8448/_matrix/key/v2/server", { data: result });

const authorizationHeader = await authorizationHeaders(
"synapse1",
Expand Down Expand Up @@ -283,4 +283,26 @@ describe("validateHeaderSignature getting public key from remote", () => {

expect(resp.status).toBe(401);
});

it("Should reject if there are no keys in the cache and the target server is unreachable", async () => {
const authorizationHeader = await authorizationHeaders(
"synapse1",
signature,
"synapse2",
"GET",
"/",
);

mock("https://synapse1:8448/_matrix/key/v2/server", { throw: new Error("Not found") });

const resp = await app.handle(
new Request("http://localhost/", {
headers: {
authorization: authorizationHeader,
},
}),
);

expect(resp.status).toBe(401);
});
});
40 changes: 26 additions & 14 deletions packages/homeserver/src/plugins/validateHeaderSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
} from "../signJson";
import { isConfigContext } from "./isConfigContext";
import { isMongodbContext } from "./isMongodbContext";
import { makeGetPublicKeyFromServerProcedure } from "../procedures/getPublicKeyFromServer";
import { makeGetServerKeysFromServerProcedure } from "../procedures/getServerKeysFromRemote";
import { makeRequest } from "../makeRequest";
import { ForbiddenError, UnknownTokenError } from "../errors";
import { extractURIfromURL } from "../helpers/url";
import type { Server } from "./mongodb";

export interface OriginOptions {
/**
Expand Down Expand Up @@ -42,6 +43,19 @@ export interface OriginOptions {
};
}

const extractKeyFromServerKeys = (verifyKeys: Server['verify_keys'], key: string) => {
const [, publickey] =
Object.entries(verifyKeys).find(
([keyFromServer]) => keyFromServer === key,
) ?? [];

if (!publickey) {
throw new Error("Public key not found");
}

return publickey;
}

export const validateHeaderSignature = async ({
headers: { authorization },
request,
Expand All @@ -66,8 +80,8 @@ export const validateHeaderSignature = async ({
throw new Error("Invalid destination");
}

const getPublicKeyFromServer = makeGetPublicKeyFromServerProcedure(
context.mongo.getValidPublicKeyFromLocal,
const getPublicKeyFromServer = makeGetServerKeysFromServerProcedure(
context.mongo.getValidServerKeysFromLocal,
async () => {
const result = await makeRequest({
method: "GET",
Expand All @@ -84,10 +98,7 @@ export const validateHeaderSignature = async ({
origin.origin,
);

const [, publickey] =
Object.entries(result.verify_keys).find(
([key]) => key === origin.key,
) ?? [];
const publickey = extractKeyFromServerKeys(result.verify_keys, origin.key);

if (!publickey) {
throw new Error("Public key not found");
Expand Down Expand Up @@ -116,20 +127,21 @@ export const validateHeaderSignature = async ({
throw new Error("Invalid algorithm");
}

return {
key: publickey.key,
validUntil: result.valid_until_ts,
};
return result;
},
context.mongo.storePublicKey,
context.mongo.storeServerKeys,
);

const publickey = await getPublicKeyFromServer(origin.origin, origin.key);
const serverKeys = await getPublicKeyFromServer(origin.origin);
if (!serverKeys) {
throw new Error('Could not retrieve the server keys to verify');
}
const publickey = extractKeyFromServerKeys(serverKeys.verify_keys, origin.key);
const url = new URL(request.url);
if (
!(await validateAuthorizationHeader(
origin.origin,
publickey,
publickey.key,
origin.destination,
request.method,
extractURIfromURL(url),
Expand Down
20 changes: 0 additions & 20 deletions packages/homeserver/src/procedures/getPublicKeyFromServer.ts

This file was deleted.

28 changes: 28 additions & 0 deletions packages/homeserver/src/procedures/getServerKeysFromRemote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Response as ServerKeysResponse } from '@hs/core/src/server';
import type { Server } from "../plugins/mongodb";
import type { WithId } from 'mongodb';

export const makeGetServerKeysFromServerProcedure = (
getFromLocal: (origin: string) => Promise<WithId<Server> | null>,
getFromOrigin: (origin: string) => Promise<ServerKeysResponse>,
store: (origin: string, serverKeys: Omit<Server, '_id' | 'name'>) => Promise<void>,
) => {
return async (origin: string) => {
try {
const localServerKeys = await getFromLocal(origin);
if (localServerKeys) {
return localServerKeys;
}

const result = await getFromOrigin(origin);
if (result) {
await store(origin, result);
return result;
}
} catch {
return;
}

throw new Error("Keys not found");
};
};
7 changes: 4 additions & 3 deletions packages/homeserver/src/routes/key/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Elysia } from "elysia";

import { getServerKeyRoute } from "./getServerKey";
import { notaryServerRoutes } from "./notaryServer";

export const keyV2Endpoints = new Elysia({ prefix: "/_matrix/key/v2" }).use(
getServerKeyRoute,
);
export const keyV2Endpoints = new Elysia({ prefix: "/_matrix/key/v2" })
.use(getServerKeyRoute)
.use(notaryServerRoutes);
Loading
Loading