hyaenidae/src/apub.rs
2021-04-02 12:07:19 -05:00

267 lines
7.5 KiB
Rust

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<bool, Self::Error>>;
fn signature_verify(
&mut self,
algorithm: Option<Algorithm>,
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<bool, anyhow::Error>
})
.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<Uuid>,
sig_verified: Option<SignatureVerified>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
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<AnyBase>,
_: DigestVerified,
sig_verified: SignatureVerified,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
do_inbox(body.into_inner(), sig_verified.key_id().parse()?, &state).await
}
async fn shared_inbox(
body: web::Json<AnyBase>,
_: DigestVerified,
sig_verified: SignatureVerified,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
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<HttpResponse, Error> {
state.spawn.ingest(any_base, key_id);
Ok(HttpResponse::Created().finish())
}
impl Apub {
pub(super) fn build(base_url: Url, db: &sled::Db) -> Result<Self, sled::Error> {
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<Option<Url>, Error> {
Ok(self.uuid_url.get(uuid.as_bytes())?.and_then(url_from_ivec))
}
}
impl ApubIds for Apub {
fn gen_id(&self) -> Option<Url> {
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<Url> {
let mut url = id.to_owned();
url.set_fragment(Some("main-key"));
Some(url)
}
fn following(&self, id: &Url) -> Option<Url> {
Url::parse(&format!("{}/following", id)).ok()
}
fn followers(&self, id: &Url) -> Option<Url> {
Url::parse(&format!("{}/followers", id)).ok()
}
fn inbox(&self, id: &Url) -> Option<Url> {
Url::parse(&format!("{}/inbox", id)).ok()
}
fn outbox(&self, id: &Url) -> Option<Url> {
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<Url> {
String::from_utf8_lossy(&ivec).parse().ok()
}
fn url_key(url: &Url) -> &[u8] {
url.as_str().as_bytes()
}