refactor(federation): ♻️ Allow ActivityPub bridge requests to omit all signature headers, including x-signed-by

This commit is contained in:
Jesse Wierzbinski 2024-11-24 16:40:23 +01:00
parent 80b5184d6a
commit b55237cdc8
No known key found for this signature in database
2 changed files with 39 additions and 6 deletions

View file

@ -25,7 +25,11 @@ export const schemas = {
header: z.object({ header: z.object({
"x-signature": z.string().optional(), "x-signature": z.string().optional(),
"x-nonce": z.string().optional(), "x-nonce": z.string().optional(),
"x-signed-by": z.string().url().or(z.string().startsWith("instance ")), "x-signed-by": z
.string()
.url()
.or(z.string().startsWith("instance "))
.optional(),
authorization: z.string().optional(), authorization: z.string().optional(),
}), }),
body: z.any(), body: z.any(),
@ -111,6 +115,32 @@ export default apiRoute((app) =>
const logger = getLogger(["federation", "inbox"]); const logger = getLogger(["federation", "inbox"]);
const body: Entity = await context.req.valid("json"); const body: Entity = await context.req.valid("json");
if (authorization) {
const processor = new InboxProcessor(
context,
body,
null,
{
signature,
nonce,
authorization,
},
logger,
);
return await processor.process();
}
// If not potentially from bridge, check for required headers
if (!(signature && nonce && signedBy)) {
return context.json(
{
error: "Missing required headers: x-signature, x-nonce, or x-signed-by",
},
400,
);
}
const sender = await User.resolve(signedBy); const sender = await User.resolve(signedBy);
if (!(sender || signedBy.startsWith("instance "))) { if (!(sender || signedBy.startsWith("instance "))) {

View file

@ -70,7 +70,7 @@ export class InboxProcessor {
* *
* @param context Hono request context. * @param context Hono request context.
* @param body Entity JSON body. * @param body Entity JSON body.
* @param senderInstance Sender of the request's instance (from X-Signed-By header). * @param senderInstance Sender of the request's instance (from X-Signed-By header). Null if request is from a bridge.
* @param headers Various request headers. * @param headers Various request headers.
* @param logger LogTape logger instance. * @param logger LogTape logger instance.
* @param requestIp Request IP address. Grabs it from the Hono context if not provided. * @param requestIp Request IP address. Grabs it from the Hono context if not provided.
@ -78,7 +78,7 @@ export class InboxProcessor {
public constructor( public constructor(
private context: Context, private context: Context,
private body: Entity, private body: Entity,
private senderInstance: Instance, private senderInstance: Instance | null,
private headers: { private headers: {
signature?: string; signature?: string;
nonce?: string; nonce?: string;
@ -94,9 +94,9 @@ export class InboxProcessor {
* @returns {Promise<boolean>} - Whether the signature is valid. * @returns {Promise<boolean>} - Whether the signature is valid.
*/ */
private async isSignatureValid(): Promise<boolean> { private async isSignatureValid(): Promise<boolean> {
if (!this.senderInstance.data.publicKey?.key) { if (!this.senderInstance?.data.publicKey?.key) {
throw new Error( throw new Error(
`Instance ${this.senderInstance.data.baseUrl} has no public key stored in database`, `Instance ${this.senderInstance?.data.baseUrl} has no public key stored in database`,
); );
} }
@ -196,7 +196,10 @@ export class InboxProcessor {
public async process(): Promise< public async process(): Promise<
(Response & TypedResponse<{ error: string }, 500, "json">) | Response (Response & TypedResponse<{ error: string }, 500, "json">) | Response
> { > {
if (isDefederated(this.senderInstance.data.baseUrl)) { if (
this.senderInstance &&
isDefederated(this.senderInstance.data.baseUrl)
) {
// Return 201 to avoid // Return 201 to avoid
// 1. Leaking defederated instance information // 1. Leaking defederated instance information
// 2. Preventing the sender from thinking the message was not delivered and retrying // 2. Preventing the sender from thinking the message was not delivered and retrying