use crate::{error::Error, State}; use activitystreams::base::AnyBase; use actix_web::{web, HttpResponse, Scope}; use futures::future::LocalBoxFuture; use http_signature_normalization_actix::prelude::*; use hyaenidae_profiles::{apub::ApubIds, Spawner}; use rsa::{PublicKey, RSAPublicKey}; use rsa_pem::KeyExt; use sha2::{Digest, Sha256}; use sled::Tree; use url::Url; use uuid::Uuid; #[derive(Clone)] pub(super) struct Apub { base_url: Url, uuid_url: Tree, url_uuid: Tree, } pub(super) fn scope(state: State) -> Scope { web::scope("/apub") .service( web::scope("/objects/{id}") .wrap( VerifySignature::new(RsaVerifier::new(state.clone()), signature_config()) .optional(), ) .service(web::resource("").route(web::get().to(serve_object))) .service( web::resource("/inbox") .wrap(VerifyDigest::new(Sha256::new())) .route(web::post().to(inbox)), ), ) .service( web::resource("/inbox") .wrap(VerifySignature::new( RsaVerifier::new(state), signature_config(), )) .wrap(VerifyDigest::new(Sha256::new())) .route(web::post().to(shared_inbox)), ) } #[derive(Clone, Debug)] struct RsaVerifier { state: State, } impl RsaVerifier { fn new(state: State) -> Self { RsaVerifier { state } } } impl SignatureVerify for RsaVerifier { type Error = VerifyError; type Future = LocalBoxFuture<'static, Result>; fn signature_verify( &mut self, algorithm: Option, key_id: String, signature: String, signing_string: String, ) -> Self::Future { let apub = self.state.profiles.apub.clone(); let spawner = self.state.spawn.clone(); Box::pin(async move { let key_id: Url = key_id.parse().map_err(|e| { log::error!("key id parse: {}", e); VerifyError })?; if !matches!(algorithm, Some(Algorithm::Hs2019)) { log::error!("Bad Algorithm"); return Err(VerifyError); } let res = web::block(move || { let public_key = if let Some(key) = apub.public_key_for_id(&key_id)? { key } else { log::warn!("No public key for ID {}", key_id); spawner.download_apub(hyaenidae_profiles::OnBehalfOf::Server, key_id, vec![]); return Err(VerifyError.into()); }; let public_key = RSAPublicKey::from_pem_pkcs8(&public_key)?; let signature_bytes = base64::decode(&signature)?; let hashed = Sha256::digest(signing_string.as_bytes()); public_key.verify( rsa::PaddingScheme::PKCS1v15Sign { hash: Some(rsa::Hash::SHA2_256), }, &hashed, &signature_bytes, )?; Ok(true) as Result }) .await; match res { Ok(Ok(item)) => Ok(item), Ok(Err(e)) => { log::error!("Failed to verify signature: {}", e); Err(VerifyError) } Err(_) => { log::error!("Panic while verifying signature"); Err(VerifyError) } } }) } } #[derive(Debug, thiserror::Error)] #[error("Error verifying digest")] struct VerifyError; impl actix_web::ResponseError for VerifyError { fn status_code(&self) -> actix_web::http::StatusCode { actix_web::http::StatusCode::UNAUTHORIZED } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()).finish() } } fn signature_config() -> Config { Config::new() } async fn serve_object( path: web::Path, sig_verified: Option, state: web::Data, ) -> Result { let key_id = if let Some(sig_verified) = sig_verified { let key_id: Url = sig_verified.key_id().parse()?; Some(key_id) } else { None }; let object_id = path.into_inner(); let apub = state.apub.clone(); let url = match web::block(move || apub.id_for_uuid(object_id)).await?? { Some(url) => url, None => return Ok(crate::to_404()), }; let object = match state.profiles.serve_object(url, key_id).await? { Some(object) => object, None => return Ok(crate::to_404()), }; Ok(HttpResponse::Ok() .content_type("application/activity+json") .json(&object)) } async fn inbox( body: web::Json, _: DigestVerified, sig_verified: SignatureVerified, state: web::Data, ) -> Result { do_inbox(body.into_inner(), sig_verified.key_id().parse()?, &state).await } async fn shared_inbox( body: web::Json, _: DigestVerified, sig_verified: SignatureVerified, state: web::Data, ) -> Result { do_inbox(body.into_inner(), sig_verified.key_id().parse()?, &state).await } async fn do_inbox(any_base: AnyBase, key_id: Url, state: &State) -> Result { state.spawn.ingest(any_base, key_id); Ok(HttpResponse::Created().finish()) } impl Apub { pub(super) fn build(base_url: Url, db: &sled::Db) -> Result { Ok(Apub { base_url, uuid_url: db.open_tree("/main/apub/uuid_url")?, url_uuid: db.open_tree("/main/apub/url_uuid")?, }) } fn id_for_uuid(&self, uuid: Uuid) -> Result, Error> { Ok(self.uuid_url.get(uuid.as_bytes())?.and_then(url_from_ivec)) } } impl ApubIds for Apub { fn gen_id(&self) -> Option { let mut url = self.base_url.clone(); let mut uuid; while { uuid = Uuid::new_v4(); url.set_path(&format!("/apub/objects/{}", uuid)); self.uuid_url .compare_and_swap(uuid.as_bytes(), None as Option<&[u8]>, Some(url_key(&url))) .map_err(|e| log::error!("Error generating ID: {}", e)) .ok()? .is_err() } {} self.url_uuid .insert(url_key(&url), uuid.as_bytes()) .map_err(|e| log::error!("Failed to insert url -> uuid: {}", e)) .ok()?; Some(url) } fn public_key(&self, id: &Url) -> Option { let mut url = id.to_owned(); url.set_fragment(Some("main-key")); Some(url) } fn following(&self, id: &Url) -> Option { Url::parse(&format!("{}/following", id)).ok() } fn followers(&self, id: &Url) -> Option { Url::parse(&format!("{}/followers", id)).ok() } fn inbox(&self, id: &Url) -> Option { Url::parse(&format!("{}/inbox", id)).ok() } fn outbox(&self, id: &Url) -> Option { Url::parse(&format!("{}/outbox", id)).ok() } fn shared_inbox(&self) -> Url { let mut url = self.base_url.clone(); url.set_path("/apub/inbox"); url } } fn url_from_ivec(ivec: sled::IVec) -> Option { String::from_utf8_lossy(&ivec).parse().ok() } fn url_key(url: &Url) -> &[u8] { url.as_str().as_bytes() }