2021-01-09 04:35:35 +00:00
|
|
|
use crate::{error::Error, State};
|
2021-01-06 08:21:37 +00:00
|
|
|
use activitystreams::base::AnyBase;
|
|
|
|
use actix_web::{web, HttpResponse, Scope};
|
2021-01-17 07:29:59 +00:00
|
|
|
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};
|
2021-01-06 08:21:37 +00:00
|
|
|
use sled::Tree;
|
|
|
|
use url::Url;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub(super) struct Apub {
|
|
|
|
base_url: Url,
|
|
|
|
uuid_url: Tree,
|
|
|
|
url_uuid: Tree,
|
|
|
|
}
|
|
|
|
|
2021-01-17 07:29:59 +00:00
|
|
|
pub(super) fn scope(state: State) -> Scope {
|
2021-01-06 08:21:37 +00:00
|
|
|
web::scope("/apub")
|
|
|
|
.service(
|
|
|
|
web::scope("/objects/{id}")
|
2021-01-17 07:29:59 +00:00
|
|
|
.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)),
|
|
|
|
),
|
2021-01-06 08:21:37 +00:00
|
|
|
)
|
2021-01-17 07:29:59 +00:00
|
|
|
.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)) {
|
2021-01-17 21:08:34 +00:00
|
|
|
log::error!("Bad Algorithm");
|
2021-01-17 07:29:59 +00:00
|
|
|
return Err(VerifyError);
|
|
|
|
}
|
|
|
|
|
|
|
|
web::block(move || {
|
|
|
|
let public_key = if let Some(key) = apub.public_key_for_id(&key_id)? {
|
|
|
|
key
|
|
|
|
} else {
|
2021-01-17 21:08:34 +00:00
|
|
|
log::warn!("No public key for ID {}", key_id);
|
2021-01-17 07:29:59 +00:00
|
|
|
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
|
|
|
|
.map_err(|e| {
|
|
|
|
log::error!("Failed to verify signature: {}", e);
|
|
|
|
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()
|
2021-01-06 08:21:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn serve_object(
|
|
|
|
path: web::Path<Uuid>,
|
2021-01-17 07:29:59 +00:00
|
|
|
sig_verified: Option<SignatureVerified>,
|
2021-01-06 08:21:37 +00:00
|
|
|
state: web::Data<State>,
|
2021-01-09 04:35:35 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-01-17 07:29:59 +00:00
|
|
|
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? {
|
2021-01-06 08:21:37 +00:00
|
|
|
Some(url) => url,
|
|
|
|
None => return Ok(crate::to_404()),
|
|
|
|
};
|
|
|
|
|
2021-01-17 07:29:59 +00:00
|
|
|
let object = match state.profiles.serve_object(url, key_id).await? {
|
2021-01-06 08:21:37 +00:00
|
|
|
Some(object) => object,
|
|
|
|
None => return Ok(crate::to_404()),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type("application/activity+json")
|
|
|
|
.json(&object))
|
|
|
|
}
|
|
|
|
|
2021-01-17 07:29:59 +00:00
|
|
|
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
|
2021-01-06 08:21:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn shared_inbox(
|
|
|
|
body: web::Json<AnyBase>,
|
2021-01-17 07:29:59 +00:00
|
|
|
_: DigestVerified,
|
|
|
|
sig_verified: SignatureVerified,
|
2021-01-06 08:21:37 +00:00
|
|
|
state: web::Data<State>,
|
2021-01-09 04:35:35 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-01-17 07:29:59 +00:00
|
|
|
do_inbox(body.into_inner(), sig_verified.key_id().parse()?, &state).await
|
2021-01-06 08:21:37 +00:00
|
|
|
}
|
|
|
|
|
2021-01-17 07:29:59 +00:00
|
|
|
async fn do_inbox(any_base: AnyBase, key_id: Url, state: &State) -> Result<HttpResponse, Error> {
|
|
|
|
state.spawn.ingest(any_base, key_id);
|
2021-01-06 08:21:37 +00:00
|
|
|
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)))
|
2021-01-10 01:49:33 +00:00
|
|
|
.map_err(|e| log::error!("Error generating ID: {}", e))
|
2021-01-06 08:21:37 +00:00
|
|
|
.ok()?
|
|
|
|
.is_err()
|
|
|
|
} {}
|
2021-01-10 01:49:33 +00:00
|
|
|
self.url_uuid
|
|
|
|
.insert(url_key(&url), uuid.as_bytes())
|
|
|
|
.map_err(|e| log::error!("Failed to insert url -> uuid: {}", e))
|
|
|
|
.ok()?;
|
2021-01-06 08:21:37 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|