From 32872181ddf14cfbf006dc93d04eeacac5eaf7a0 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Thu, 25 Jul 2024 10:37:23 -0400 Subject: [PATCH] feat: implement `attachLdSignatureForRelays` to control signing of Relayed activities --- .config/ci.yml | 8 +- .config/docker_example.yml | 4 +- .config/example.yml | 4 +- chart/files/default.yml | 4 +- packages/backend/src/config.ts | 11 ++- .../src/core/activitypub/ApRendererService.ts | 97 +++++++++++++------ 6 files changed, 89 insertions(+), 39 deletions(-) diff --git a/.config/ci.yml b/.config/ci.yml index c381d21d92..02081e5971 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -106,7 +106,7 @@ redis: # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global +# You can set scope to local (default value) or global # (include notes from remote). #meilisearch: @@ -198,13 +198,15 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". +# but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/.config/docker_example.yml b/.config/docker_example.yml index c22bd83c2e..375753e79f 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -270,8 +270,10 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/.config/example.yml b/.config/example.yml index ae55b983bb..4b6aaae63b 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -285,8 +285,10 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/chart/files/default.yml b/chart/files/default.yml index 2e1381ec57..7c94bcbea3 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -208,8 +208,10 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 58c4d028aa..10a63f8ae2 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -4,12 +4,12 @@ */ import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {dirname, resolve} from 'node:path'; import * as yaml from 'js-yaml'; -import { globSync } from 'glob'; +import {globSync} from 'glob'; import * as Sentry from '@sentry/node'; -import type { RedisOptions } from 'ioredis'; +import type {RedisOptions} from 'ioredis'; type RedisOptionsSource = Partial & { host: string; @@ -95,6 +95,7 @@ type Source = { customMOTD?: string[]; signToActivityPubGet?: boolean; + attachLdSignatureForRelays?: boolean; checkActivityPubGetSignature?: boolean; perChannelMaxNoteCacheCount?: number; @@ -161,6 +162,7 @@ export type Config = { proxyRemoteFiles: boolean | undefined; customMOTD: string[] | undefined; signToActivityPubGet: boolean; + attachLdSignatureForRelays: boolean; checkActivityPubGetSignature: boolean | undefined; version: string; @@ -291,6 +293,7 @@ export function loadConfig(): Config { proxyRemoteFiles: config.proxyRemoteFiles, customMOTD: config.customMOTD, signToActivityPubGet: config.signToActivityPubGet ?? true, + attachLdSignatureForRelays: config.attachLdSignatureForRelays ?? true, checkActivityPubGetSignature: config.checkActivityPubGetSignature, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 90784fdc1d..28c5dcf150 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -3,36 +3,69 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createPublicKey, randomUUID } from 'node:crypto'; -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import {createPublicKey, randomUUID} from 'node:crypto'; +import {Inject, Injectable} from '@nestjs/common'; +import {In} from 'typeorm'; import * as mfm from '@transfem-org/sfm-js'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; -import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { IMentionedRemoteUsers, MiNote } from '@/models/Note.js'; -import type { MiBlocking } from '@/models/Blocking.js'; -import type { MiRelay } from '@/models/Relay.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNoteReaction } from '@/models/NoteReaction.js'; -import type { MiEmoji } from '@/models/Emoji.js'; -import type { MiPoll } from '@/models/Poll.js'; -import type { MiPollVote } from '@/models/PollVote.js'; -import { UserKeypairService } from '@/core/UserKeypairService.js'; -import { MfmService } from '@/core/MfmService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import type { MiUserKeypair } from '@/models/UserKeypair.js'; -import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, InstancesRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { isNotNull } from '@/misc/is-not-null.js'; -import { IdService } from '@/core/IdService.js'; -import { MetaService } from '../MetaService.js'; -import { JsonLdService } from './JsonLdService.js'; -import { ApMfmService } from './ApMfmService.js'; -import { CONTEXT } from './misc/contexts.js'; -import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; +import {DI} from '@/di-symbols.js'; +import type {Config} from '@/config.js'; +import type {MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser} from '@/models/User.js'; +import type {IMentionedRemoteUsers, MiNote} from '@/models/Note.js'; +import type {MiBlocking} from '@/models/Blocking.js'; +import type {MiRelay} from '@/models/Relay.js'; +import type {MiDriveFile} from '@/models/DriveFile.js'; +import type {MiNoteReaction} from '@/models/NoteReaction.js'; +import type {MiEmoji} from '@/models/Emoji.js'; +import type {MiPoll} from '@/models/Poll.js'; +import type {MiPollVote} from '@/models/PollVote.js'; +import {UserKeypairService} from '@/core/UserKeypairService.js'; +import {MfmService} from '@/core/MfmService.js'; +import {UserEntityService} from '@/core/entities/UserEntityService.js'; +import {DriveFileEntityService} from '@/core/entities/DriveFileEntityService.js'; +import type {MiUserKeypair} from '@/models/UserKeypair.js'; +import type { + DriveFilesRepository, + InstancesRepository, + NotesRepository, + PollsRepository, + UserProfilesRepository, + UsersRepository +} from '@/models/_.js'; +import {bindThis} from '@/decorators.js'; +import {CustomEmojiService} from '@/core/CustomEmojiService.js'; +import {isNotNull} from '@/misc/is-not-null.js'; +import {IdService} from '@/core/IdService.js'; +import {MetaService} from '../MetaService.js'; +import {JsonLdService} from './JsonLdService.js'; +import {ApMfmService} from './ApMfmService.js'; +import {CONTEXT} from './misc/contexts.js'; +import type { + IAccept, + IActivity, + IAdd, + IAnnounce, + IApDocument, + IApEmoji, + IApHashtag, + IApImage, + IApMention, + IBlock, + ICreate, + IDelete, + IFlag, + IFollow, + IKey, + ILike, + IMove, + IObject, + IPost, + IQuestion, + IReject, + IRemove, + ITombstone, + IUndo, + IUpdate +} from './type.js'; @Injectable() export class ApRendererService { @@ -793,6 +826,12 @@ export class ApRendererService { @bindThis public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise { + // When using authorized fetch, Linked Data signatures are often undesired (as it can allow blocked instances to bypass the check). + // We allow admins to disable LD signatures for increased privacy, at the expense of increased incoming fetch (GET) requests. + if (!this.config.attachLdSignatureForRelays) { + return activity; + } + const keypair = await this.userKeypairService.getUserKeypair(user.id); const jsonLd = this.jsonLdService.use();