diff --git a/src/actors.rs b/src/actors.rs index 5e5f3c4..d6098e2 100644 --- a/src/actors.rs +++ b/src/actors.rs @@ -133,21 +133,36 @@ pub async fn get_local_actor( user_id: uuid::Uuid, data: &Data, ) -> Result { - let user = data - .user_repo - .find_by_id(user_id) - .await - .map_err(Error::from)? - .ok_or_else(|| Error::not_found(anyhow::anyhow!("user not found: {}", user_id)))?; + build_local_actor( + user_id, + &data.base_url, + data.user_repo.as_ref(), + data.actor_repo.as_ref(), + ) + .await + .map_err(|e| Error::not_found(anyhow::anyhow!("{e}"))) +} - let (public_key, private_key) = match data.actor_repo.get_local_actor_keypair(user_id).await? { +/// Build a local actor's `DbActor` from repository data. Generates a keypair +/// if one doesn't exist yet. Usable outside of a `FederationData` context +/// (e.g. during service construction). +pub async fn build_local_actor( + user_id: uuid::Uuid, + base_url: &str, + user_repo: &dyn crate::user::ApUserRepository, + actor_repo: &dyn crate::repository::ActorRepository, +) -> anyhow::Result { + let user = user_repo + .find_by_id(user_id) + .await? + .ok_or_else(|| anyhow::anyhow!("user not found: {}", user_id))?; + + let (public_key, private_key) = match actor_repo.get_local_actor_keypair(user_id).await? { Some(kp) => kp, None => { let kp = generate_actor_keypair()?; - // Zeroize the private key after storing it so the plaintext doesn't - // linger in memory beyond this scope. let private_zeroized = Zeroizing::new(kp.private_key.clone()); - data.actor_repo + actor_repo .save_local_actor_keypair( user_id, kp.public_key.clone(), @@ -166,7 +181,7 @@ pub async fn get_local_actor( outbox_url, followers_url, following_url, - } = ActorUrls::build(&data.base_url, user_id); + } = ActorUrls::build(base_url, user_id); Ok(DbActor { user_id, diff --git a/src/federation.rs b/src/federation.rs index c4f8f4e..4dd94df 100644 --- a/src/federation.rs +++ b/src/federation.rs @@ -2,6 +2,7 @@ use activitypub_federation::config::{Data, FederationConfig, FederationMiddlewar use activitypub_federation::error::Error as FedError; use url::Url; +use crate::actors::DbActor; use crate::data::FederationData; #[derive(Clone)] @@ -27,7 +28,15 @@ impl ApFederationConfig { /// and accepts any URL. **Never use in production.** /// /// Outbound signing always uses Mastodon compat mode regardless of this flag. - pub async fn new(data: FederationData, debug: bool) -> anyhow::Result { + /// + /// When `signing_actor` is provided, all outgoing fetch requests (GETs) are + /// signed with that actor's keypair — required for instances with + /// authorized-fetch / Secure Mode enabled. + pub async fn new( + data: FederationData, + debug: bool, + signing_actor: Option<&DbActor>, + ) -> anyhow::Result { let config = if debug { FederationConfig::builder() .domain(&data.domain) @@ -38,12 +47,12 @@ impl ApFederationConfig { .build() .await? } else { - FederationConfig::builder() - .domain(&data.domain) - .app_data(data) - .debug(false) - .build() - .await? + let mut builder = FederationConfig::builder(); + builder.domain(&data.domain).app_data(data).debug(false); + if let Some(actor) = signing_actor { + builder.signed_fetch_actor(actor); + } + builder.build().await? }; Ok(Self(config)) } diff --git a/src/service/fetch.rs b/src/service/fetch.rs new file mode 100644 index 0000000..a60ce9a --- /dev/null +++ b/src/service/fetch.rs @@ -0,0 +1,20 @@ +use url::Url; + +use super::ActivityPubService; + +impl ActivityPubService { + /// Fetch a remote ActivityPub resource with HTTP Signatures. + /// + /// Requires `signed_fetch_actor_id` to have been set on the builder. + /// Returns the raw JSON value of the remote resource. + pub async fn signed_fetch(&self, url: &Url) -> anyhow::Result { + let data = self.federation_config.to_request_data(); + let res = activitypub_federation::fetch::fetch_object_http::< + crate::data::FederationData, + serde_json::Value, + >(url, &data) + .await + .map_err(|e| anyhow::anyhow!("{e}"))?; + Ok(res.object) + } +} diff --git a/src/service/mod.rs b/src/service/mod.rs index c75b4be..2a944fc 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -23,6 +23,7 @@ use crate::{ mod backfill; pub(crate) mod broadcast; pub(super) mod delivery; +mod fetch; mod follow; /// Default max delivery retries per inbox (used as the builder default). @@ -57,6 +58,7 @@ pub struct ActivityPubServiceBuilder { event_publisher: Option>, delivery_max_attempts: u32, delivery_initial_delay_secs: u64, + signed_fetch_actor_id: Option, } impl ActivityPubServiceBuilder { @@ -113,6 +115,14 @@ impl ActivityPubServiceBuilder { self } + /// Set a local actor whose keypair signs all outgoing fetch requests + /// (HTTP Signature on GETs). Required for federating with instances + /// that enforce authorized-fetch / Secure Mode. + pub fn signed_fetch_actor_id(mut self, v: uuid::Uuid) -> Self { + self.signed_fetch_actor_id = Some(v); + self + } + pub async fn build(self) -> anyhow::Result { let activity_repo = self .activity_repo @@ -138,9 +148,9 @@ impl ActivityPubServiceBuilder { let data = FederationData::new( activity_repo, follow_repo, - actor_repo, + actor_repo.clone(), blocklist_repo, - user_repo, + user_repo.clone(), content_reader, object_handler, self.base_url.clone(), @@ -148,7 +158,20 @@ impl ActivityPubServiceBuilder { self.software_name, self.event_publisher, ); - let federation_config = ApFederationConfig::new(data, self.debug).await?; + let signing_actor = if let Some(uid) = self.signed_fetch_actor_id { + let actor = crate::actors::build_local_actor( + uid, + &self.base_url, + user_repo.as_ref(), + actor_repo.as_ref(), + ) + .await?; + Some(actor) + } else { + None + }; + let federation_config = + ApFederationConfig::new(data, self.debug, signing_actor.as_ref()).await?; Ok(ActivityPubService { federation_config, base_url: self.base_url, @@ -175,6 +198,7 @@ impl ActivityPubService { event_publisher: None, delivery_max_attempts: DELIVERY_MAX_ATTEMPTS, delivery_initial_delay_secs: DELIVERY_INITIAL_DELAY_SECS, + signed_fetch_actor_id: None, } }