Skip to content

Commit

Permalink
chore: make basic pdu validations and unit tests (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggazzo authored Dec 17, 2024
1 parent b6498ba commit b8d1e4a
Show file tree
Hide file tree
Showing 8 changed files with 571 additions and 92 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/events/m.room.member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare module "./eventBase" {
age_ts: number;
};
content: {
join_authorised_via_users_server?: string;
membership: Membership;
};
};
Expand All @@ -25,6 +26,7 @@ export interface RoomMemberEvent extends EventBase {
type: "m.room.member";
content: {
membership: Membership;
join_authorised_via_users_server?: string;
};
state_key: string;
unsigned: {
Expand Down
21 changes: 18 additions & 3 deletions packages/homeserver/src/plugins/mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { InferContext } from "elysia";
import { type Db, MongoClient } from "mongodb";

import type { EventBase } from "@hs/core/src/events/eventBase";
import { generateId } from "../authentication";

export interface Server {
_id: string;
Expand Down Expand Up @@ -125,7 +126,8 @@ export const routerWithMongodb = (db: Db) =>
}
const [, publicKey] =
Object.entries(server.keys).find(
([protocolAndVersion, value]) => protocolAndVersion === key && value.validUntil > Date.now(),
([protocolAndVersion, value]) =>
protocolAndVersion === key && value.validUntil > Date.now(),
) ?? [];
return publicKey?.key;
};
Expand All @@ -145,13 +147,24 @@ export const routerWithMongodb = (db: Db) =>
key: value,
validUntil,
},
}
}
},
},
},
{ upsert: true },
);
};

const createStagingEvent = async (event: EventBase) => {
const id = generateId(event);
await eventsCollection.insertOne({
_id: id,
event,
staged: true,
});

return id;
};

return {
serversCollection,
getValidPublicKeyFromLocal,
Expand All @@ -162,6 +175,7 @@ export const routerWithMongodb = (db: Db) =>
getMissingEventsByDeep,
getLastEvent,
getAuthEvents,
createStagingEvent,
};
})(),
);
Expand All @@ -171,4 +185,5 @@ export type Context = InferContext<ReturnType<typeof routerWithMongodb>>;
export type EventStore = {
_id: string;
event: EventBase;
staged?: true;
};
61 changes: 9 additions & 52 deletions packages/homeserver/src/plugins/validateHeaderSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
} from "../signJson";
import { isConfigContext } from "./isConfigContext";
import { isMongodbContext } from "./isMongodbContext";
import { makeGetPublicKeyFromServerProcedure } from "../procedures/getPublicKeyFromServer";
import {
getPublicKeyFromRemoteServer,
makeGetPublicKeyFromServerProcedure,
} from "../procedures/getPublicKeyFromServer";
import { makeRequest } from "../makeRequest";
import { ForbiddenError, UnknownTokenError } from "../errors";
import { extractURIfromURL } from "../helpers/url";
Expand Down Expand Up @@ -68,59 +71,13 @@ export const validateHeaderSignature = async ({

const getPublicKeyFromServer = makeGetPublicKeyFromServerProcedure(
context.mongo.getValidPublicKeyFromLocal,
async () => {
const result = await makeRequest({
method: "GET",
domain: origin.origin,
uri: "/_matrix/key/v2/server",
signingName: context.config.name,
});
if (result.valid_until_ts < Date.now()) {
throw new Error("Expired remote public key");
}

const [signature] = await getSignaturesFromRemote(
result,
() =>
getPublicKeyFromRemoteServer(
origin.origin,
);

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

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

if (!signature) {
throw new Error(`Signatures not found for ${origin.origin}`);
}

if (
!(await verifyJsonSignature(
result,
origin.origin,
Uint8Array.from(atob(signature.signature), (c) => c.charCodeAt(0)),
Uint8Array.from(atob(publickey.key), (c) => c.charCodeAt(0)),
signature.algorithm,
signature.version,
))
) {
throw new Error("Invalid signature");
}

const [algorithm, version] = origin.key.split(":");

if (!isValidAlgorithm(algorithm)) {
throw new Error("Invalid algorithm");
}
origin.destination,
origin.key,
),

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

Expand Down
73 changes: 71 additions & 2 deletions packages/homeserver/src/procedures/getPublicKeyFromServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { makeRequest } from "../makeRequest";
import {
getSignaturesFromRemote,
isValidAlgorithm,
verifyJsonSignature,
} from "../signJson";

export const makeGetPublicKeyFromServerProcedure = (
getFromLocal: (origin: string, key: string) => Promise<string | undefined>,
getFromOrigin: (origin: string) => Promise<{ key: string; validUntil: number }>,
store: (origin: string, key: string, value: string, validUntil: number) => Promise<void>,
getFromOrigin: (
origin: string,
) => Promise<{ key: string; validUntil: number }>,
store: (
origin: string,
key: string,
value: string,
validUntil: number,
) => Promise<void>,
) => {
return async (origin: string, key: string) => {
const localPublicKey = await getFromLocal(origin, key);
Expand All @@ -18,3 +32,58 @@ export const makeGetPublicKeyFromServerProcedure = (
throw new Error("Public key not found");
};
};

export const getPublicKeyFromRemoteServer = async (
domain: string,
signingName: string,
algorithmAndVersion: string,
) => {
const result = await makeRequest({
method: "GET",
domain,
uri: "/_matrix/key/v2/server",
signingName,
});
if (result.valid_until_ts < Date.now()) {
throw new Error("Expired remote public key");
}

const [signature] = await getSignaturesFromRemote(result, domain);

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

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

if (!signature) {
throw new Error(`Signatures not found for ${domain}`);
}

if (
!(await verifyJsonSignature(
result,
domain,
Uint8Array.from(atob(signature.signature), (c) => c.charCodeAt(0)),
Uint8Array.from(atob(publickey.key), (c) => c.charCodeAt(0)),
signature.algorithm,
signature.version,
))
) {
throw new Error("Invalid signature");
}

const [algorithm, version] = algorithmAndVersion.split(":");

if (!isValidAlgorithm(algorithm)) {
throw new Error("Invalid algorithm");
}

return {
key: publickey.key,
validUntil: result.valid_until_ts,
};
};
23 changes: 15 additions & 8 deletions packages/homeserver/src/routes/federation/sendInviteV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,21 @@ export const sendInviteV2Route = new Elysia().put(
event: responseBody.event,
});
}
if (responseBody.state?.length) {
await eventsCollection.insertMany(
responseBody.state.map((event) => ({
_id: generateId(event),
event,
})),
);
}
await Promise.all(
responseBody.state?.map((event) => {
const promise = eventsCollection
.insertOne({
_id: generateId(event),
event,
})
.catch((e) => {
// TODO events failing because of duplicate key
// the reason is that we are saving the event on invite event
console.error("error saving event", e, event);
});
return promise;
}) ?? [],
);
}, 1000);

return { event: body.event };
Expand Down
Loading

0 comments on commit b8d1e4a

Please sign in to comment.