First iteration of secure mode federation support

This commit is contained in:
Alex Auvolat 2023-05-30 19:45:34 +02:00
parent 51613df9e3
commit 588c41efc9
6 changed files with 211 additions and 11 deletions

View file

@ -4,7 +4,7 @@ use crate::{
config::Data,
error::Error,
fetch::object_id::ObjectId,
http_signatures::{verify_inbox_hash, verify_signature},
http_signatures::{verify_body_hash, verify_signature},
traits::{ActivityHandler, Actor, Object},
};
use actix_web::{web::Bytes, HttpRequest, HttpResponse};
@ -30,7 +30,7 @@ where
<ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
Datatype: Clone,
{
verify_inbox_hash(request.headers().get("Digest"), &body)?;
verify_body_hash(request.headers().get("Digest"), &body)?;
let activity: Activity = serde_json::from_slice(&body)?;
data.config.verify_url_and_domain(&activity).await?;

View file

@ -3,3 +3,29 @@
pub mod inbox;
#[doc(hidden)]
pub mod middleware;
use crate::{
config::Data,
error::Error,
http_signatures::{self, verify_body_hash},
traits::{Actor, Object},
};
use actix_web::{web::Bytes, HttpRequest};
use serde::Deserialize;
/// Checks whether the request is signed by an actor of type A, and returns
/// the actor in question if a valid signature is found.
pub async fn signing_actor<A>(
request: HttpRequest,
body: Option<Bytes>,
data: &Data<<A as Object>::DataType>,
) -> Result<A, <A as Object>::Error>
where
A: Object + Actor,
<A as Object>::Error: From<Error> + From<anyhow::Error>,
for<'de2> <A as Object>::Kind: Deserialize<'de2>,
{
verify_body_hash(request.headers().get("Digest"), &body.unwrap_or_default())?;
http_signatures::signing_actor(request.headers(), request.method(), request.uri(), data).await
}

View file

@ -6,7 +6,7 @@ use crate::{
config::Data,
error::Error,
fetch::object_id::ObjectId,
http_signatures::{verify_inbox_hash, verify_signature},
http_signatures::{verify_body_hash, verify_signature},
traits::{ActivityHandler, Actor, Object},
};
use axum::{
@ -36,7 +36,7 @@ where
<ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
Datatype: Clone,
{
verify_inbox_hash(activity_data.headers.get("Digest"), &activity_data.body)?;
verify_body_hash(activity_data.headers.get("Digest"), &activity_data.body)?;
let activity: Activity = serde_json::from_slice(&activity_data.body)?;
data.config.verify_url_and_domain(&activity).await?;

View file

@ -2,7 +2,15 @@
//!
#![doc = include_str!("../../docs/07_fetching_data.md")]
use crate::{config::Data, error::Error, reqwest_shim::ResponseExt, FEDERATION_CONTENT_TYPE};
use crate::{
config::Data,
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;
@ -56,3 +64,49 @@ pub async fn fetch_object_http<T: Clone, Kind: DeserializeOwned>(
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)?;
if res.status() == StatusCode::GONE {
return Err(Error::ObjectDeleted);
}
res.json_limited().await
}

View file

@ -1,4 +1,9 @@
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Object};
use crate::{
config::Data,
error::Error,
fetch::{fetch_object_http, fetch_object_http_signed},
traits::{Actor, Object},
};
use anyhow::anyhow;
use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
@ -119,6 +124,44 @@ 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(
@ -163,6 +206,31 @@ 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

@ -6,8 +6,11 @@
//! [receive_activity (axum)](crate::axum::inbox::receive_activity).
use crate::{
config::Data,
error::{Error, Error::ActivitySignatureInvalid},
fetch::object_id::ObjectId,
protocol::public_key::main_key_id,
traits::{Actor, Object},
};
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri};
@ -21,6 +24,7 @@ use openssl::{
};
use reqwest::Request;
use reqwest_middleware::RequestBuilder;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind};
use tracing::debug;
@ -107,6 +111,54 @@ where
header_map.insert(name.to_string(), value.to_string());
}
}
verify_signature_inner(header_map, method, uri, public_key)
}
pub(crate) async fn signing_actor<'a, A, H>(
headers: H,
method: &Method,
uri: &Uri,
data: &Data<<A as Object>::DataType>,
) -> Result<A, <A as Object>::Error>
where
A: Object + Actor,
<A as Object>::Error: From<Error> + From<anyhow::Error>,
for<'de2> <A as Object>::Kind: Deserialize<'de2>,
H: IntoIterator<Item = (&'a HeaderName, &'a HeaderValue)>,
{
let mut header_map = BTreeMap::<String, String>::new();
for (name, value) in headers {
if let Ok(value) = value.to_str() {
header_map.insert(name.to_string(), value.to_string());
}
}
let signature = header_map
.get("signature")
.ok_or(Error::ActivitySignatureInvalid)?;
let actor_id_re = regex::Regex::new("keyId=\"([^\"]+)#([^\"]+)\"").unwrap();
let actor_id = match actor_id_re.captures(signature) {
None => return Err(Error::ActivitySignatureInvalid.into()),
Some(caps) => caps.get(1).unwrap().as_str(),
};
let actor_url = Url::parse(actor_id).map_err(|_| Error::ActivitySignatureInvalid)?;
let actor_id: ObjectId<A> = actor_url.into();
let actor = actor_id.dereference(data).await?;
let public_key = actor.public_key_pem();
verify_signature_inner(header_map, method, uri, public_key)?;
Ok(actor)
}
fn verify_signature_inner(
header_map: BTreeMap<String, String>,
method: &Method,
uri: &Uri,
public_key: &str,
) -> Result<(), Error> {
let path_and_query = uri.path_and_query().map(PathAndQuery::as_str).unwrap_or("");
let verified = CONFIG2
@ -166,7 +218,7 @@ impl DigestPart {
}
/// Verify body of an inbox request against the hash provided in `Digest` header.
pub(crate) fn verify_inbox_hash(
pub(crate) fn verify_body_hash(
digest_header: Option<&HeaderValue>,
body: &[u8],
) -> Result<(), Error> {
@ -266,21 +318,21 @@ pub mod test {
}
#[test]
fn test_verify_inbox_hash_valid() {
fn test_verify_body_hash_valid() {
let digest_header =
HeaderValue::from_static("SHA-256=lzFT+G7C2hdI5j8M+FuJg1tC+O6AGMVJhooTCKGfbKM=");
let body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
let valid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
let valid = verify_body_hash(Some(&digest_header), body.as_bytes());
println!("{:?}", &valid);
assert!(valid.is_ok());
}
#[test]
fn test_verify_inbox_hash_not_valid() {
fn test_verify_body_hash_not_valid() {
let digest_header =
HeaderValue::from_static("SHA-256=Z9h7DJfYWjffXw2XftmWCnpEaK/yqOHKvzCIzIaqgbU=");
let body = "lorem ipsum";
let invalid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
let invalid = verify_body_hash(Some(&digest_header), body.as_bytes());
assert_eq!(invalid, Err(Error::ActivityBodyDigestInvalid));
}