From d9fc2c061d2008e2a132f9e58a378d1204696c9a Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 28 Jun 2023 01:02:28 +0200 Subject: [PATCH 1/5] Add signature to remote note lookup --- packages/backend/src/remote/activitypub/request.ts | 2 ++ packages/backend/src/remote/activitypub/resolver.ts | 8 ++++++++ packages/backend/src/server/api/endpoints/ap/show.ts | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index ffb3e25a33..7360f95449 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -3,6 +3,7 @@ import { getUserKeypair } from "@/misc/keypair-store.js"; import type { User } from "@/models/entities/user.js"; import { getResponse } from "../../misc/fetch.js"; import { createSignedPost, createSignedGet } from "./ap-request.js"; +import { apLogger } from "@/remote/activitypub/logger.js"; export default async (user: { id: User["id"] }, url: string, object: any) => { const body = JSON.stringify(object); @@ -35,6 +36,7 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { * @param url URL to fetch */ export async function signedGet(url: string, user: { id: User["id"] }) { + apLogger.debug("running signedGet on url: " + url); const keypair = await getUserKeypair(user.id); const req = createSignedGet({ diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 0547927609..e6bbf9fafc 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -23,6 +23,7 @@ import renderCreate from "@/remote/activitypub/renderer/create.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js"; +import { apLogger } from "@/remote/activitypub/logger.js"; export default class Resolver { private history: Set; @@ -34,6 +35,10 @@ export default class Resolver { this.recursionLimit = recursionLimit; } + public setUser(user) { + this.user = user; + } + public getHistory(): string[] { return Array.from(this.history); } @@ -102,6 +107,9 @@ export default class Resolver { this.user = await getInstanceActor(); } + apLogger.debug("getting object from remote, authenticated as user:"); + apLogger.debug(JSON.stringify(this.user, null, 2)); + const object = ( this.user ? await signedGet(value, this.user) diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 0bd3414ee3..2fdf24075d 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -127,6 +127,7 @@ async function fetchAny( // fetching Object once from remote const resolver = new Resolver(); + resolver.setUser(me); const object = await resolver.resolve(uri); // /@user If a URI other than the id is specified, @@ -144,8 +145,8 @@ async function fetchAny( return await mergePack( me, - isActor(object) ? await createPerson(getApId(object)) : null, - isPost(object) ? await createNote(getApId(object), undefined, true) : null, + isActor(object) ? await createPerson(getApId(object), resolver) : null, + isPost(object) ? await createNote(getApId(object), resolver, true) : null, ); } From b20298ceb12491b0bb87dd9b98f7c2bd6c9aa535 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 28 Jun 2023 02:01:55 +0200 Subject: [PATCH 2/5] Fix AP resolver history on reuse for authorized fetch --- packages/backend/src/remote/activitypub/request.ts | 2 +- packages/backend/src/remote/activitypub/resolver.ts | 12 +++++++++++- packages/backend/src/server/api/endpoints/ap/show.ts | 8 ++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 7360f95449..69c97a445d 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -36,7 +36,7 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { * @param url URL to fetch */ export async function signedGet(url: string, user: { id: User["id"] }) { - apLogger.debug("running signedGet on url: " + url); + apLogger.debug(`Running signedGet on url: ${url}`); const keypair = await getUserKeypair(user.id); const req = createSignedGet({ diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index e6bbf9fafc..608ca3e935 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -39,6 +39,11 @@ export default class Resolver { this.user = user; } + public reset(): Resolver { + this.history = new Set(); + return this; + } + public getHistory(): string[] { return Array.from(this.history); } @@ -61,15 +66,20 @@ export default class Resolver { } if (typeof value !== "string") { + apLogger.debug("Object to resolve is not a string"); if (typeof value.id !== "undefined") { const host = extractDbHost(getApId(value)); if (await shouldBlockInstance(host)) { throw new Error("instance is blocked"); } } + apLogger.debug("Returning existing object:"); + apLogger.debug(JSON.stringify(value, null, 2)); return value; } + apLogger.debug(`Resolving: ${value}`); + if (value.includes("#")) { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). @@ -107,7 +117,7 @@ export default class Resolver { this.user = await getInstanceActor(); } - apLogger.debug("getting object from remote, authenticated as user:"); + apLogger.debug("Getting object from remote, authenticated as user:"); apLogger.debug(JSON.stringify(this.user, null, 2)); const object = ( diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 2fdf24075d..3dd168d718 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -145,8 +145,12 @@ async function fetchAny( return await mergePack( me, - isActor(object) ? await createPerson(getApId(object), resolver) : null, - isPost(object) ? await createNote(getApId(object), resolver, true) : null, + isActor(object) + ? await createPerson(getApId(object), resolver.reset()) + : null, + isPost(object) + ? await createNote(getApId(object), resolver.reset(), true) + : null, ); } From 516e0f8ecf35c55a41209059b9ba32eda22c9225 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 28 Jun 2023 03:43:32 +0200 Subject: [PATCH 3/5] Allow follower-only notes to be fetched by properly authorized remote users --- .../src/remote/activitypub/check-fetch.ts | 76 +++++++++++++++++++ packages/backend/src/server/activitypub.ts | 34 ++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index a8bbe61b84..3e52575a94 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -95,3 +95,79 @@ export async function checkFetch(req: IncomingMessage): Promise { } return 200; } + +export async function getSignatureUser( + req: IncomingMessage, +): Promise { + let authUser; + const meta = await fetchMeta(); + if (meta.secureMode || meta.privateMode) { + let signature; + + try { + signature = httpSignature.parseRequest(req, { headers: [] }); + } catch (e) { + return null; + } + + const keyId = new URL(signature.keyId); + const host = toPuny(keyId.hostname); + + if (await shouldBlockInstance(host, meta)) { + return 403; + } + + if ( + meta.privateMode && + host !== config.host && + !meta.allowedHosts.includes(host) + ) { + return null; + } + + const keyIdLower = signature.keyId.toLowerCase(); + if (keyIdLower.startsWith("acct:")) { + // Old keyId is no longer supported. + return null; + } + + const dbResolver = new DbResolver(); + + // HTTP-Signature keyIdを元にDBから取得 + authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); + + // keyIdでわからなければ、resolveしてみる + if (authUser == null) { + try { + keyId.hash = ""; + authUser = await dbResolver.getAuthUserFromApId( + getApId(keyId.toString()), + ); + } catch (e) { + // できなければ駄目 + return null; + } + } + + // publicKey がなくても終了 + if (authUser?.key == null) { + return null; + } + + // もう一回チェック + if (authUser.user.host !== host) { + return null; + } + + // HTTP-Signatureの検証 + const httpSignatureValidated = httpSignature.verifySignature( + signature, + authUser.key.keyPem, + ); + + if (!httpSignatureValidated) { + return null; + } + } + return authUser; +} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 042ab446c7..548aafdd2f 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -20,7 +20,11 @@ import { import type { ILocalUser, User } from "@/models/entities/user.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { getUserKeypair } from "@/misc/keypair-store.js"; -import { checkFetch, hasSignature } from "@/remote/activitypub/check-fetch.js"; +import { + checkFetch, + hasSignature, + getSignatureUser, +} from "@/remote/activitypub/check-fetch.js"; import { getInstanceActor } from "@/services/instance-actor.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; @@ -28,6 +32,7 @@ import Featured from "./activitypub/featured.js"; import Following from "./activitypub/following.js"; import Followers from "./activitypub/followers.js"; import Outbox, { packActivity } from "./activitypub/outbox.js"; +import { serverLogger } from "./index.js"; // Init router const router = new Router(); @@ -84,7 +89,7 @@ router.get("/notes/:note", async (ctx, next) => { const note = await Notes.findOneBy({ id: ctx.params.note, - visibility: In(["public" as const, "home" as const]), + visibility: In(["public" as const, "home" as const, "followers" as const]), localOnly: false, }); @@ -103,6 +108,31 @@ router.get("/notes/:note", async (ctx, next) => { return; } + if (note.visibility == "followers") { + serverLogger.debug( + "Responding to request for follower-only note, validating access...", + ); + let remoteUser = await getSignatureUser(ctx.req); + serverLogger.debug("Local note author user:"); + serverLogger.debug(JSON.stringify(note, null, 2)); + serverLogger.debug("Authenticated remote user:"); + serverLogger.debug(JSON.stringify(remoteUser, null, 2)); + + let relation = await Users.getRelation(remoteUser.user.id, note.userId); + serverLogger.debug("Relation:"); + serverLogger.debug(JSON.stringify(relation, null, 2)); + + if (!relation.isFollowing || relation.isBlocked) { + serverLogger.debug( + "Rejecting: authenticated user is not following us or was blocked by us", + ); + ctx.status = 403; + return; + } + + serverLogger.debug("Accepting: access criteria met"); + } + ctx.body = renderActivity(await renderNote(note, false)); const meta = await fetchMeta(); From 487e7ba43cddac3d1dd3c31042f5bc1bb70b79be Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Tue, 27 Jun 2023 22:20:52 -0700 Subject: [PATCH 4/5] fixes --- .../backend/src/remote/activitypub/check-fetch.ts | 15 ++++++++++----- packages/backend/src/server/activitypub.ts | 12 +++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index 3e52575a94..a3e241c254 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -7,6 +7,8 @@ import DbResolver from "@/remote/activitypub/db-resolver.js"; import { getApId } from "@/remote/activitypub/type.js"; import { shouldBlockInstance } from "@/misc/should-block-instance.js"; import type { IncomingMessage } from "http"; +import type { CacheableRemoteUser } from "@/models/entities/user.js"; +import type { UserPublickey } from "@/models/entities/user-publickey.js"; export async function hasSignature(req: IncomingMessage): Promise { const meta = await fetchMeta(); @@ -98,7 +100,10 @@ export async function checkFetch(req: IncomingMessage): Promise { export async function getSignatureUser( req: IncomingMessage, -): Promise { +): Promise<{ + user: CacheableRemoteUser; + key: UserPublickey | null; +} | null> { let authUser; const meta = await fetchMeta(); if (meta.secureMode || meta.privateMode) { @@ -114,7 +119,7 @@ export async function getSignatureUser( const host = toPuny(keyId.hostname); if (await shouldBlockInstance(host, meta)) { - return 403; + return null; } if ( @@ -137,20 +142,20 @@ export async function getSignatureUser( authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); // keyIdでわからなければ、resolveしてみる - if (authUser == null) { + if (!authUser) { try { keyId.hash = ""; authUser = await dbResolver.getAuthUserFromApId( getApId(keyId.toString()), ); - } catch (e) { + } catch { // できなければ駄目 return null; } } // publicKey がなくても終了 - if (authUser?.key == null) { + if (!authUser?.key) { return null; } diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 548aafdd2f..f9d5eb99c3 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -108,17 +108,23 @@ router.get("/notes/:note", async (ctx, next) => { return; } - if (note.visibility == "followers") { + if (note.visibility === "followers") { serverLogger.debug( "Responding to request for follower-only note, validating access...", ); - let remoteUser = await getSignatureUser(ctx.req); + const remoteUser = await getSignatureUser(ctx.req); serverLogger.debug("Local note author user:"); serverLogger.debug(JSON.stringify(note, null, 2)); serverLogger.debug("Authenticated remote user:"); serverLogger.debug(JSON.stringify(remoteUser, null, 2)); - let relation = await Users.getRelation(remoteUser.user.id, note.userId); + if (remoteUser == null) { + serverLogger.debug("Rejecting: no user"); + ctx.status = 401; + return; + } + + const relation = await Users.getRelation(remoteUser.user.id, note.userId); serverLogger.debug("Relation:"); serverLogger.debug(JSON.stringify(relation, null, 2)); From 89e4e3ea5bf8c631f366f027b29ff51dccb2a232 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Tue, 27 Jun 2023 22:44:16 -0700 Subject: [PATCH 5/5] refactor: simplify getSignatureUser --- .../src/remote/activitypub/check-fetch.ts | 84 +++---------------- 1 file changed, 12 insertions(+), 72 deletions(-) diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts index a3e241c254..c885b4a199 100644 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ b/packages/backend/src/remote/activitypub/check-fetch.ts @@ -98,81 +98,21 @@ export async function checkFetch(req: IncomingMessage): Promise { return 200; } -export async function getSignatureUser( - req: IncomingMessage, -): Promise<{ +export async function getSignatureUser(req: IncomingMessage): Promise<{ user: CacheableRemoteUser; key: UserPublickey | null; } | null> { - let authUser; - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - let signature; + const signature = httpSignature.parseRequest(req, { headers: [] }); + const keyId = new URL(signature.keyId); + const dbResolver = new DbResolver(); - try { - signature = httpSignature.parseRequest(req, { headers: [] }); - } catch (e) { - return null; - } - - const keyId = new URL(signature.keyId); - const host = toPuny(keyId.hostname); - - if (await shouldBlockInstance(host, meta)) { - return null; - } - - if ( - meta.privateMode && - host !== config.host && - !meta.allowedHosts.includes(host) - ) { - return null; - } - - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith("acct:")) { - // Old keyId is no longer supported. - return null; - } - - const dbResolver = new DbResolver(); - - // HTTP-Signature keyIdを元にDBから取得 - authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); - - // keyIdでわからなければ、resolveしてみる - if (!authUser) { - try { - keyId.hash = ""; - authUser = await dbResolver.getAuthUserFromApId( - getApId(keyId.toString()), - ); - } catch { - // できなければ駄目 - return null; - } - } - - // publicKey がなくても終了 - if (!authUser?.key) { - return null; - } - - // もう一回チェック - if (authUser.user.host !== host) { - return null; - } - - // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature( - signature, - authUser.key.keyPem, - ); - - if (!httpSignatureValidated) { - return null; - } + // Retrieve from DB by HTTP-Signature keyId + const authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); + if (authUser) { + return authUser; } - return authUser; + + // Resolve if failed to retrieve by keyId + keyId.hash = ""; + return await dbResolver.getAuthUserFromApId(getApId(keyId.toString())); }