feat: optionally fetch and verify relayed activity

relays usually Announce(Create), so the Create is not from them but the
announce is, and it gets processed properly. Lemmy does the correct
thing: it sends Announce(...activity...), so the "topmost" activity
effectively comes from the sending server and can be verified. however
aode relay sends activities as-is, without wrapping. so if we receive
activities from someone else, it won't match the http signature and
we thus can't be sure this wasn't falsified. added an option to directly
fetch such cases. it's probably not great, so defaults to OFF
This commit is contained in:
əlemi 2025-01-28 13:55:20 +01:00
parent d77197a325
commit 08fdc93d35
Signed by: alemi
GPG key ID: A4895B84D311642C
2 changed files with 14 additions and 3 deletions
core/src
routes/src/activitypub

View file

@ -139,6 +139,13 @@ pub struct CompatibilityConfig {
#[serde_inline_default(true)]
/// compatibility with lemmy: avoid showing images twice
pub skip_single_attachment_if_image_is_set: bool,
#[serde_inline_default(false)]
/// compatibility with most relays: since they send us other server's activities, we must fetch
/// them to verify that they aren't falsified by the relay itself. this is quite expensive, as
/// relays send a lot of activities and we effectively end up fetching again all these, so this
/// defaults to false
pub verify_relayed_activities_by_fetching: bool,
}
#[serde_inline_default::serde_inline_default]

View file

@ -1,7 +1,7 @@
use apb::{Activity, ActivityType, Base};
use axum::{extract::{Query, State}, http::StatusCode, Json};
use sea_orm::{sea_query::IntoCondition, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
use upub::{model::job::JobType, selector::{RichActivity, RichFillable}, Context};
use upub::{model::job::JobType, selector::{RichActivity, RichFillable}, traits::Fetcher, Context};
use crate::{AuthIdentity, Identity, builders::JsonLD};
@ -41,7 +41,7 @@ pub async fn page(
pub async fn post(
State(ctx): State<Context>,
AuthIdentity(auth): AuthIdentity,
Json(activity): Json<serde_json::Value>
Json(mut activity): Json<serde_json::Value>
) -> crate::ApiResult<StatusCode> {
let Identity::Remote { domain, user: uid, .. } = auth else {
if matches!(activity.activity_type(), Ok(ActivityType::Delete)) {
@ -72,7 +72,11 @@ pub async fn post(
let server = upub::Context::server(&aid);
if activity.actor().id()? != uid {
return Err(crate::ApiError::forbidden());
if ctx.cfg().compat.verify_relayed_activities_by_fetching {
activity = ctx.pull(&activity.id()?).await?.activity()?;
} else {
return Err(crate::ApiError::forbidden());
}
}
if let Some(_internal) = upub::model::activity::Entity::ap_to_internal(&aid, ctx.db()).await? {