mirror of
https://github.com/LemmyNet/activitypub-federation-rust.git
synced 2024-06-10 09:19:34 +00:00
First iteration of secure mode federation support
This commit is contained in:
parent
51613df9e3
commit
588c41efc9
|
@ -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?;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue