Implement secure mode fetch as a global config parameter

This commit is contained in:
Alex Auvolat 2023-05-31 15:31:06 +02:00
parent f068545df8
commit 216458eb82
4 changed files with 38 additions and 121 deletions

View file

@ -18,7 +18,7 @@ use crate::{
activity_queue::create_activity_queue,
error::Error,
protocol::verification::verify_domains_match,
traits::ActivityHandler,
traits::{ActivityHandler, Actor},
};
use async_trait::async_trait;
use background_jobs::Manager;
@ -75,6 +75,11 @@ pub struct FederationConfig<T: Clone> {
/// <https://git.pleroma.social/pleroma/pleroma/-/issues/2939>
#[builder(default = "false")]
pub(crate) http_signature_compat: bool,
/// Actor Id and private key to use to sign all federated fetch requests.
/// This can be used to implement secure mode federation.
/// <https://docs.joinmastodon.org/spec/activitypub/#secure-mode>
#[builder(default = "None", setter(custom))]
pub(crate) signed_fetch_actor: Option<Arc<(Url, String)>>,
/// Queue for sending outgoing activities. Only optional to make builder work, its always
/// present once constructed.
#[builder(setter(skip))]
@ -170,6 +175,13 @@ impl<T: Clone> FederationConfig<T> {
}
impl<T: Clone> FederationConfigBuilder<T> {
/// Sets an actor to use to sign all federated fetch requests
pub fn signed_fetch_actor<A: Actor>(&mut self, actor: &A) -> &mut Self {
let private_key_pem = actor.private_key_pem().unwrap();
self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key_pem))));
self
}
/// Constructs a new config instance with the values supplied to builder.
///
/// Values which are not explicitly specified use the defaults. Also initializes the

View file

@ -7,10 +7,8 @@ use crate::{
error::Error,
http_signatures::sign_request,
reqwest_shim::ResponseExt,
traits::Actor,
FEDERATION_CONTENT_TYPE,
};
use anyhow::anyhow;
use http::StatusCode;
use serde::de::DeserializeOwned;
use std::sync::atomic::Ordering;
@ -49,60 +47,25 @@ pub async fn fetch_object_http<T: Clone, Kind: DeserializeOwned>(
return Err(Error::RequestLimit);
}
let res = config
.client
.get(url.as_str())
.header("Accept", FEDERATION_CONTENT_TYPE)
.timeout(config.request_timeout)
.send()
.await
.map_err(Error::other)?;
if res.status() == StatusCode::GONE {
return Err(Error::ObjectDeleted);
}
res.json_limited().await
}
/// Signed version of `fetch_object_http_signed`. This will sign the GET request
/// using the private key of the given actor, which allows to implement secure
/// federation mode.
pub async fn fetch_object_http_signed<T: Clone, Kind: DeserializeOwned, A: Actor>(
url: &Url,
data: &Data<T>,
actor: &A,
) -> Result<Kind, Error> {
let config = &data.config;
// dont fetch local objects this way
debug_assert!(url.domain() != Some(&config.domain));
config.verify_url_valid(url).await?;
info!("Fetching remote object {}", url.to_string());
let counter = data.request_counter.fetch_add(1, Ordering::SeqCst);
if counter > config.http_fetch_limit {
return Err(Error::RequestLimit);
}
let req = config
.client
.get(url.as_str())
.header("Accept", FEDERATION_CONTENT_TYPE)
.timeout(config.request_timeout);
let private_key_pem = actor
.private_key_pem()
.ok_or(anyhow!("Actor does not have a private key to sign with"))?;
let req = sign_request(
req,
actor.id(),
String::new(),
private_key_pem,
data.config.http_signature_compat,
)
.await?;
let res = config.client.execute(req).await.map_err(Error::other)?;
let res = if let Some((actor_id, private_key_pem)) = config.signed_fetch_actor.as_deref() {
let req = sign_request(
req,
actor_id.clone(),
String::new(),
private_key_pem.clone(),
data.config.http_signature_compat,
)
.await?;
config.client.execute(req).await.map_err(Error::other)?
} else {
req.send().await.map_err(Error::other)?
};
if res.status() == StatusCode::GONE {
return Err(Error::ObjectDeleted);

View file

@ -1,9 +1,4 @@
use crate::{
config::Data,
error::Error,
fetch::{fetch_object_http, fetch_object_http_signed},
traits::{Actor, Object},
};
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Object};
use anyhow::anyhow;
use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
@ -124,44 +119,6 @@ where
}
}
/// Fetches an activitypub object, either from local database (if possible), or over http.
pub async fn dereference_signed<A>(
&self,
data: &Data<<Kind as Object>::DataType>,
actor: &A,
) -> Result<Kind, <Kind as Object>::Error>
where
<Kind as Object>::Error: From<Error> + From<anyhow::Error>,
A: Actor,
{
let db_object = self.dereference_from_db(data).await?;
// if its a local object, only fetch it from the database and not over http
if data.config.is_local_url(&self.0) {
return match db_object {
None => Err(Error::NotFound.into()),
Some(o) => Ok(o),
};
}
// object found in database
if let Some(object) = db_object {
// object is old and should be refetched
if let Some(last_refreshed_at) = object.last_refreshed_at() {
if should_refetch_object(last_refreshed_at) {
return self
.dereference_from_http_signed(data, Some(object), actor)
.await;
}
}
Ok(object)
}
// object not found, need to fetch over http
else {
self.dereference_from_http_signed(data, None, actor).await
}
}
/// Fetch an object from the local db. Instead of falling back to http, this throws an error if
/// the object is not found in the database.
pub async fn dereference_local(
@ -206,31 +163,6 @@ where
Kind::verify(&res2, self.inner(), data).await?;
Kind::from_json(res2, data).await
}
async fn dereference_from_http_signed<A>(
&self,
data: &Data<<Kind as Object>::DataType>,
db_object: Option<Kind>,
actor: &A,
) -> Result<Kind, <Kind as Object>::Error>
where
<Kind as Object>::Error: From<Error> + From<anyhow::Error>,
A: Actor,
{
let res = fetch_object_http_signed(&self.0, data, actor).await;
if let Err(Error::ObjectDeleted) = &res {
if let Some(db_object) = db_object {
db_object.delete(data).await?;
}
return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
}
let res2 = res?;
Kind::verify(&res2, self.inner(), data).await?;
Kind::from_json(res2, data).await
}
}
/// Need to implement clone manually, to avoid requiring Kind to be Clone

View file

@ -95,7 +95,11 @@ pub(crate) async fn sign_request(
static CONFIG2: Lazy<http_signature_normalization::Config> =
Lazy::new(http_signature_normalization::Config::new);
/// Verifies the HTTP signature on an incoming inbox request.
/// Verifies the HTTP signature on an incoming federation request
/// for a given actor's public key.
///
/// Internally, this just converts the headers to a BTreeMap and passes to
/// `verify_signature_inner` for actual signature verification.
pub(crate) fn verify_signature<'a, H>(
headers: H,
method: &Method,
@ -115,6 +119,10 @@ where
verify_signature_inner(header_map, method, uri, public_key)
}
/// Checks whether the given federation request has a valid signature,
/// from any actor of type A, and returns that actor if a valid signature is found.
/// This function will return an `Err` variant when no signature is found
/// or if the signature could not be verified.
pub(crate) async fn signing_actor<'a, A, H>(
headers: H,
method: &Method,
@ -153,6 +161,8 @@ where
Ok(actor)
}
/// Verifies that the signature present in the request is valid for
/// the specified actor's public key.
fn verify_signature_inner(
header_map: BTreeMap<String, String>,
method: &Method,