From c4b0e414622c89bfd6f9bf7e23659f62692a9fa3 Mon Sep 17 00:00:00 2001 From: Marcos Defendi Date: Fri, 13 Dec 2024 13:34:30 -0300 Subject: [PATCH 1/2] refactor: make mongodb servers collection save all the keys data + refactor procedure to support it --- packages/homeserver/src/plugins/mongodb.ts | 58 ++++++++----------- .../plugins/validateHeaderSignature.spec.ts | 38 +++++++++--- .../src/plugins/validateHeaderSignature.ts | 40 ++++++++----- .../src/procedures/getPublicKeyFromServer.ts | 20 ------- .../src/procedures/getServerKeysFromRemote.ts | 28 +++++++++ 5 files changed, 109 insertions(+), 75 deletions(-) delete mode 100644 packages/homeserver/src/procedures/getPublicKeyFromServer.ts create mode 100644 packages/homeserver/src/procedures/getServerKeysFromRemote.ts diff --git a/packages/homeserver/src/plugins/mongodb.ts b/packages/homeserver/src/plugins/mongodb.ts index 82e641c9..10c09b58 100644 --- a/packages/homeserver/src/plugins/mongodb.ts +++ b/packages/homeserver/src/plugins/mongodb.ts @@ -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>; + valid_until_ts: number; + verify_keys: Record< + string, + { + key: string; + } + >; // signatures: { // from: string; @@ -113,40 +122,23 @@ export const routerWithMongodb = (db: Db) => .toArray(); }; - const getValidPublicKeyFromLocal = async ( + const getValidServerKeysFromLocal = async ( origin: string, - key: string, - ): Promise => { - 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, ) => { await serversCollection.findOneAndUpdate( - { name: origin }, + { name: origin, valid_until_ts: { $gte: Date.now() }, }, { - $set: { - keys: { - [key]: { - key: value, - validUntil, - }, - } - } + $set: serverKeys, }, { upsert: true }, ); @@ -154,8 +146,8 @@ export const routerWithMongodb = (db: Db) => return { serversCollection, - getValidPublicKeyFromLocal, - storePublicKey, + getValidServerKeysFromLocal, + storeServerKeys, eventsCollection, getDeepEarliestAndLatestEvents, diff --git a/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts b/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts index 010c0ce5..eef34061 100644 --- a/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts +++ b/packages/homeserver/src/plugins/validateHeaderSignature.spec.ts @@ -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; let signature: SigningKey; + const key = 'ed25519:a_yNbw'; beforeAll(async () => { signature = await generateKeyPairsFromString( @@ -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: { @@ -90,7 +91,6 @@ describe("validateHeaderSignature getting public key from local", () => { "GET", "/", ); - const resp = await app.handle( new Request("http://localhost/", { headers: { @@ -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: { @@ -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", @@ -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); + }); }); diff --git a/packages/homeserver/src/plugins/validateHeaderSignature.ts b/packages/homeserver/src/plugins/validateHeaderSignature.ts index d2e8822d..454e6d44 100644 --- a/packages/homeserver/src/plugins/validateHeaderSignature.ts +++ b/packages/homeserver/src/plugins/validateHeaderSignature.ts @@ -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 { /** @@ -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, @@ -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", @@ -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"); @@ -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), diff --git a/packages/homeserver/src/procedures/getPublicKeyFromServer.ts b/packages/homeserver/src/procedures/getPublicKeyFromServer.ts deleted file mode 100644 index b2332252..00000000 --- a/packages/homeserver/src/procedures/getPublicKeyFromServer.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const makeGetPublicKeyFromServerProcedure = ( - getFromLocal: (origin: string, key: string) => Promise, - getFromOrigin: (origin: string) => Promise<{ key: string; validUntil: number }>, - store: (origin: string, key: string, value: string, validUntil: number) => Promise, -) => { - return async (origin: string, key: string) => { - const localPublicKey = await getFromLocal(origin, key); - if (localPublicKey) { - return localPublicKey; - } - - const { key: remotePublicKey, validUntil } = await getFromOrigin(origin); - if (remotePublicKey) { - await store(origin, key, remotePublicKey, validUntil); - return remotePublicKey; - } - - throw new Error("Public key not found"); - }; -}; diff --git a/packages/homeserver/src/procedures/getServerKeysFromRemote.ts b/packages/homeserver/src/procedures/getServerKeysFromRemote.ts new file mode 100644 index 00000000..6ee7cf42 --- /dev/null +++ b/packages/homeserver/src/procedures/getServerKeysFromRemote.ts @@ -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 | null>, + getFromOrigin: (origin: string) => Promise, + store: (origin: string, serverKeys: Omit) => Promise, +) => { + 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"); + }; +}; From e51469efb15960a5c381e1c72b6ac0dcaf17f09b Mon Sep 17 00:00:00 2001 From: Marcos Defendi Date: Fri, 13 Dec 2024 13:35:36 -0300 Subject: [PATCH 2/2] feat: add notary server endpoints --- packages/core/src/server.ts | 2 +- packages/homeserver/src/dto.ts | 6 + packages/homeserver/src/helpers/array.ts | 1 + packages/homeserver/src/routes/key/index.ts | 7 +- .../src/routes/key/notaryServer.spec.ts | 501 ++++++++++++++++++ .../homeserver/src/routes/key/notaryServer.ts | 203 +++++++ 6 files changed, 716 insertions(+), 4 deletions(-) create mode 100644 packages/homeserver/src/helpers/array.ts create mode 100644 packages/homeserver/src/routes/key/notaryServer.spec.ts create mode 100644 packages/homeserver/src/routes/key/notaryServer.ts diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index f186e0f6..d2ff436f 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -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, { diff --git a/packages/homeserver/src/dto.ts b/packages/homeserver/src/dto.ts index 0eaf5657..8848f0d2 100644 --- a/packages/homeserver/src/dto.ts +++ b/packages/homeserver/src/dto.ts @@ -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( diff --git a/packages/homeserver/src/helpers/array.ts b/packages/homeserver/src/helpers/array.ts new file mode 100644 index 00000000..67e641b2 --- /dev/null +++ b/packages/homeserver/src/helpers/array.ts @@ -0,0 +1 @@ +export const isTruthy = (x: T | null | undefined | 0 | false | ''): x is T => Boolean(x); diff --git a/packages/homeserver/src/routes/key/index.ts b/packages/homeserver/src/routes/key/index.ts index b240449b..789e04af 100644 --- a/packages/homeserver/src/routes/key/index.ts +++ b/packages/homeserver/src/routes/key/index.ts @@ -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); diff --git a/packages/homeserver/src/routes/key/notaryServer.spec.ts b/packages/homeserver/src/routes/key/notaryServer.spec.ts new file mode 100644 index 00000000..1e121ff1 --- /dev/null +++ b/packages/homeserver/src/routes/key/notaryServer.spec.ts @@ -0,0 +1,501 @@ +import { afterEach, beforeAll, describe, expect, it } from "bun:test"; +import { generateKeyPairsFromString } from "../../keys"; +import Elysia from "elysia"; +import { mock, clearMocks } from "bun-bagel"; +import { signJson } from "../../signJson"; +import { filterServerResponse } from "./notaryServer"; +import { keyV2Endpoints } from "."; + +describe('Notary Server Requests', async () => { + let app: Elysia; + const remoteServerName = "hs1"; + const remoteSignature = await generateKeyPairsFromString( + "ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw", + ); + const localServerName = "rc1"; + const localSignature = await generateKeyPairsFromString( + "ed25519 0 44Ve8dmV9y53MteX2bHSfJYXDJP/v3Pdjz/chZkOLyE", + ); + const responseWithoutSignature = { + "old_verify_keys": {}, + "server_name": "hs1", + "valid_until_ts": 1734184604097, + "verify_keys": { + "ed25519:a_HDhg": { + "key": "7dhilbxkbkcIAYMVc2yqRQ8mqfeVhuY9dU0hLvYpQFM" + } + } + } + + afterEach(() => { + clearMocks(); + }); + + beforeAll(async () => { + app = new Elysia() + .decorate("config", { + path: "./config.json", + signingKeyPath: "./keys/ed25519.signing.key", + port: 8080, + signingKey: [ + localSignature + ], + name: localServerName, + version: "org.matrix.msc3757.10", + }) + .decorate("mongo", { + getValidServerKeysFromLocal: async () => { + return; + }, + storeServerKeys: async () => { + return; + }, + serversCollection: { + findOne: async () => { + return; + }, + } as any, + }) + .use(keyV2Endpoints) + }); + + describe('GET /_matrix/key/v2/query/:serverName', () => { + + it("should return the correct response with the correct signatures from both local and remote servers", async () => { + const remoteSigned = await signJson(responseWithoutSignature, remoteSignature, remoteServerName); + const localSigned = await signJson(responseWithoutSignature, localSignature, localServerName); + + mock("https://hs1:8448/_matrix/key/v2/server", { data: remoteSigned }); + + const resp = await app.handle( + new Request( + 'http://localhost/_matrix/key/v2/query/hs1', + ), + ); + + expect(resp.status).toBe(200); + const content = await resp.json(); + + expect(content).toEqual({ + server_keys: [ + { + ...localSigned, + signatures: { + ...localSigned.signatures, + ...remoteSigned.signatures, + } + } + ] + }) + }); + + it("should return an empty response when keys were not being retrieved succesfully from remote", async () => { + mock("https://hs1:8448/_matrix/key/v2/server", { throw: new Error("Error") }); + const resp = await app.handle( + new Request( + 'http://localhost/_matrix/key/v2/query/hs1', + ), + ); + + expect(resp.status).toBe(200); + const content = await resp.json(); + + expect(content).toEqual({ + server_keys: [] + }) + }); + }); + + describe('POST /_matrix/key/v2/query', async () => { + it("should return the correct response with the correct signatures from both local and remote servers", async () => { + const remoteSigned = await signJson(responseWithoutSignature, remoteSignature, remoteServerName); + const localSigned = await signJson(responseWithoutSignature, localSignature, localServerName); + + mock("https://hs1:8448/_matrix/key/v2/server", { data: remoteSigned }); + + const resp = await app.handle( + new Request('http://localhost/_matrix/key/v2/query', { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + server_keys: { + 'hs1': { + 'ed25519:a_HDhg': { + minimum_valid_until_ts: 1734184604097 - 1000, + } + } + } + }), + } + ), + ); + + expect(resp.status).toBe(200); + const content = await resp.json(); + + expect(content).toEqual({ + server_keys: [ + { + ...localSigned, + signatures: { + ...localSigned.signatures, + ...remoteSigned.signatures, + } + } + ] + }) + }); + + it("should return an empty response when there is no filter", async () => { + const resp = await app.handle( + new Request('http://localhost/_matrix/key/v2/query', { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + server_keys: { + } + }), + } + ), + ); + + expect(resp.status).toBe(200); + const content = await resp.json(); + + expect(content).toEqual({ + server_keys: [] + }) + }); + + it("should return an empty response when it was not possible to retrieve the keys from remote server", async () => { + mock("https://hs1:8448/_matrix/key/v2/server", { throw: new Error("Error") }); + + const resp = await app.handle( + new Request('http://localhost/_matrix/key/v2/query', { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + server_keys: { + } + }), + } + ), + ); + + expect(resp.status).toBe(200); + const content = await resp.json(); + + expect(content).toEqual({ + server_keys: [] + }) + }); + + }); + + describe('#filterServerResponse()', () => { + it('should return all the server keys when there is a filter for a specific key and its a valid ts', () => { + const serverKeys = [ + { + "server_name": "example.org", + "verify_keys": { + "ed25519:abc123": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9hZA" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example.org": { + "ed25519:abc123": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1cmU" + } + }, + "valid_until_ts": 1652262000000 + } + ]; + + const filter = { + "example.org": { + "ed25519:abc123": { + minimum_valid_until_ts: 1652262000000 - 1000, + }, + }, + }; + + const result = filterServerResponse(serverKeys, filter); + + expect(result).toEqual(serverKeys); + }); + + it('should return an empty array if the provided filter key does not exists in the response', () => { + const serverKeys = [ + { + "server_name": "example.org", + "verify_keys": { + "ed25519:abc123": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9hZA" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example.org": { + "ed25519:abc123": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1cmU" + } + }, + "valid_until_ts": 1652262000000 + } + ]; + + const filter = { + "example.org": { + "ed25519:0": { + minimum_valid_until_ts: 1652262000000 - 1000, + }, + }, + }; + + const result = filterServerResponse(serverKeys, filter); + + expect(result).toEqual([]); + }); + + it('should return all the server keys when there is no filter applied for a server', () => { + const serverKeys = [ + { + "server_name": "example.org", + "verify_keys": { + "ed25519:abc123": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9hZA" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example.org": { + "ed25519:abc123": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1cmU" + } + }, + "valid_until_ts": 1652262000000 + } + ]; + + const filter = { + "example.org": { + }, + }; + + const result = filterServerResponse(serverKeys, filter); + + expect(result).toEqual(serverKeys); + }); + + it('should return the first server keys only when the filter wipe out the other element (by having ts invalid)', () => { + const serverKeys = [ + { + "server_name": "example.org", + "verify_keys": { + "ed25519:abc123": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9hZA" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example.org": { + "ed25519:abc123": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1cmU" + } + }, + "valid_until_ts": 1652262000000 + }, + { + "server_name": "example2.org", + "verify_keys": { + "ed25519:auto": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9Lia" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example2.org": { + "ed25519:auto": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1354" + } + }, + "valid_until_ts": 1652262000001 + } + ] as any; + + const filter = { + "example.org": { + "ed25519:abc123": { + minimum_valid_until_ts: 1652262000000 - 1000, + }, + }, + "example2.org": { + "ed25519:auto": { + minimum_valid_until_ts: 1652262000001 + 1000, + }, + }, + }; + + const result = filterServerResponse(serverKeys, filter); + + expect(result).toEqual([serverKeys[0]]); + }); + + it('should return the all the server keys when the first item is valid and theres no filter for the second', () => { + const serverKeys = [ + { + "server_name": "example.org", + "verify_keys": { + "ed25519:abc123": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9hZA" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example.org": { + "ed25519:abc123": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1cmU" + } + }, + "valid_until_ts": 1652262000000 + }, + { + "server_name": "example2.org", + "verify_keys": { + "ed25519:auto": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9Lia" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example2.org": { + "ed25519:auto": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1354" + } + }, + "valid_until_ts": 1652262000001 + } + ] as any; + + const filter = { + "example.org": { + "ed25519:abc123": { + minimum_valid_until_ts: 1652262000000 - 1000, + }, + }, + "example2.org": { + }, + }; + + const result = filterServerResponse(serverKeys, filter); + + expect(result).toEqual(serverKeys); + }); + + + it('should return the all the server keys when the first item is valid and all (multiple) fitler for the second are valid', () => { + const serverKeys = [ + { + "server_name": "example.org", + "verify_keys": { + "ed25519:abc123": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9hZA" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example.org": { + "ed25519:abc123": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1cmU" + } + }, + "valid_until_ts": 1652262000000 + }, + { + "server_name": "example2.org", + "verify_keys": { + "ed25519:auto": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9Lia" + }, + "ed25519:auto2": { + "key": "VGhpcyBzaG91bGQgYmUgYSByZWFsIGVkMjU1MTkgcGF5bG9Lia" + } + }, + "old_verify_keys": { + "ed25519:0ldk3y": { + "expired_ts": 1532645052628, + "key": "VGhpcyBzaG91bGQgYmUgeW91ciBvbGQga2V5J3MgZWQyNTUxOSBwYXlsb2FkLg" + } + }, + "signatures": { + "example2.org": { + "ed25519:auto": "VGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgYSBzaWduYXR1354" + } + }, + "valid_until_ts": 1652262000001 + } + ] as any; + + const filter = { + "example.org": { + "ed25519:abc123": { + minimum_valid_until_ts: 1652262000000 - 1000, + }, + }, + "example2.org": { + "ed25519:auto": { + minimum_valid_until_ts: 1652262000001 - 6000, + }, + "ed25519:auto2": { + minimum_valid_until_ts: 1652262000001 - 5000, + }, + }, + }; + + const result = filterServerResponse(serverKeys, filter); + + expect(result).toEqual(serverKeys); + }); + + }); +}); \ No newline at end of file diff --git a/packages/homeserver/src/routes/key/notaryServer.ts b/packages/homeserver/src/routes/key/notaryServer.ts new file mode 100644 index 00000000..90a7a4ae --- /dev/null +++ b/packages/homeserver/src/routes/key/notaryServer.ts @@ -0,0 +1,203 @@ +import { Elysia, t } from "elysia"; +import { NotaryServerKeysDTO } from "../../dto"; +import { getSignaturesFromRemote, signJson } from "../../signJson"; +import { isConfigContext } from "../../plugins/isConfigContext"; +import { makeRequest } from "../../makeRequest"; +import type { Config } from "../../plugins/config"; +import { makeGetServerKeysFromServerProcedure } from "../../procedures/getServerKeysFromRemote"; +import { isMongodbContext } from "../../plugins/isMongodbContext"; +import type { Response as ServerKeysResponse } from "@hs/core/src/server"; +import { isTruthy } from "../../helpers/array"; + +const parseNotaryResult = async (serverKeys: ServerKeysResponse, config: Config): Promise => { + const { signatures, ...rest } = serverKeys; + const signed = await signJson(rest, config.signingKey[0], config.name); + + return { + ...signed, + signatures: { + ...signed.signatures, + ...signatures + } + } +} + +export const filterServerResponse = (serverKeys: ServerKeysResponse[], filter: { + [x: string]: { + [x: string]: { + minimum_valid_until_ts: number; + }; + }; +}): ServerKeysResponse[] => { + const servers = Object.keys(filter); + if (servers.length === 0) { + return []; + } + + return serverKeys.filter((serverKey) => { + const { server_name, valid_until_ts } = serverKey; + if (!filter[server_name]) { + return false; + } + + const filterKeys = Object.keys(filter[server_name]); + if (filterKeys.length === 0) { + return true; + } + + const verifyKeys = Object.keys(serverKey.verify_keys); + const filteredVerifyKeys = filterKeys.filter((key) => verifyKeys.includes(key)); + if (filteredVerifyKeys.length === 0) { + return false; + } + + const greatestTSInFilter = Math.max(...filteredVerifyKeys.map((key) => filter[server_name][key].minimum_valid_until_ts)); + + return valid_until_ts > greatestTSInFilter; + }); +} + +const getServerKeysFromRemote = async (serverName: string, config: Config) => { + const result = await makeRequest({ + method: "GET", + domain: serverName, + uri: "/_matrix/key/v2/server", + signingName: config.name, + }); + + const [signature] = await getSignaturesFromRemote(result, serverName); + + if (!signature) { + throw new Error(`Signatures not found for ${serverName}`); + } + + return result; +} + +export const notaryServerRoutes = new Elysia() + .get( + "/query/:serverName", + async ({ params, body, ...context }) => { + if (!isConfigContext(context)) { + throw new Error("No config context"); + } + if (!isMongodbContext(context)) { + throw new Error("No mongodb context"); + } + + const { config, mongo } = context; + const getPublicKeyFromServer = makeGetServerKeysFromServerProcedure( + mongo.getValidServerKeysFromLocal, + async () => getServerKeysFromRemote(params.serverName, config), + mongo.storeServerKeys, + ); + + const serverKeys = await getPublicKeyFromServer(params.serverName); + if (!serverKeys) { + return { server_keys: [] }; + } + + return { + server_keys: [await parseNotaryResult(serverKeys, config)], + }; + }, + { + params: t.Object( + { + serverName: t.String({ + description: "The server name to query for keys.", + }), + }, + { + examples: [ + { + serverName: "matrix.org", + }, + ], + }, + ), + response: NotaryServerKeysDTO, + detail: { + description: + "Query for another server’s keys. The receiving (notary) server must sign the keys returned by the queried server.", + operationId: "getServerKeysThroughNotaryServerRequest", + }, + }, + ) + .post('/query', async ({ params, body, ...context }) => { + if (!isConfigContext(context)) { + throw new Error("No config context"); + } + if (!isMongodbContext(context)) { + throw new Error("No mongodb context"); + } + + const { config, mongo } = context; + const servers = Object.keys(body.server_keys); + + if (servers.length === 0) { + return { server_keys: [] }; + } + + const getPublicKeyFromServer = makeGetServerKeysFromServerProcedure( + mongo.getValidServerKeysFromLocal, + async (serverName: string) => getServerKeysFromRemote(serverName, config), + mongo.storeServerKeys, + ); + + const response = ( + await Promise.all( + servers + .map(async (serverName) => { + const serverKeys = await getPublicKeyFromServer(serverName); + if (serverKeys) { + return parseNotaryResult(serverKeys, config); + } + }))) + .filter(isTruthy); + + + return { + server_keys: filterServerResponse(response, body.server_keys), + } + }, + { + body: t.Object({ + server_keys: t.Record( + t.String(), + t.Record( + t.String(), + t.Object( + { + minimum_valid_until_ts: t.Integer({ + format: "int64", + description: + "A millisecond POSIX timestamp in milliseconds indicating when the returned certificates will need to be valid until to be useful to the requesting server.", + examples: [1532645052628], + }), + }, + ) + ), + ), + }, + { + examples: [ + { + "server_keys": { + "hs1": { + "ed25519:0": { + "minimum_valid_until_ts": 1234567890 + } + } + } + } + ], + },), + response: NotaryServerKeysDTO, + detail: { + description: + "Query for keys from multiple servers in a batch format. The receiving (notary) server must sign the keys returned by the queried servers.", + operationId: "getServerKeysThroughNotaryServerBatchRequest", + }, + } + );