diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index 612964ea..9b614f13 100644 --- a/plume-common/src/activity_pub/mod.rs +++ b/plume-common/src/activity_pub/mod.rs @@ -1,9 +1,10 @@ use activitypub::{Activity, Actor, Object, Link}; use array_tool::vec::Uniq; +use base64; use reqwest::Client; use rocket::{ Outcome, - http::Status, + http::{Status,HeaderMap}, response::{Response, Responder}, request::{FromRequest, Request} }; @@ -105,7 +106,7 @@ pub fn broadcast(send for inbox in boxes { // TODO: run it in Sidekiq or something like that let mut headers = request::headers(); - headers.set(request::digest(signed.to_string())); + headers.set(request::Digest::digest(signed.to_string())); let res = Client::new() .post(&inbox[..]) .headers(headers.clone()) @@ -119,6 +120,59 @@ pub fn broadcast(send } } +#[derive(Debug)] +pub enum SignatureValidity { + Invalid, + ValidNoDigest, + Valid, + Absent, +} + +pub fn verify_http_headers(sender: &S, all_headers: HeaderMap, data: String) -> SignatureValidity{ + if let Some(sig_header) = all_headers.get_one("Signature") { + let mut _key_id = None; + let mut _algorithm = None; + let mut headers = None; + let mut signature = None; + for part in sig_header.split(',') { + match part { + part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len()-1]), + part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len()-1]), + part if part.starts_with("headers=") => headers = Some(&part[9..part.len()-1]), + part if part.starts_with("signature=") => signature = Some(&part[11..part.len()-1]), + _ => {}, + } + } + if signature.is_some() && headers.is_some() { + let headers = headers.unwrap().split_whitespace().collect::>(); + let signature = signature.unwrap(); + let h = headers.iter() + .map(|header| (header,all_headers.get_one(header))) + .map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or(""))) + .collect::>().join("\n"); + if sender.verify(h, base64::decode(signature).unwrap_or(Vec::new())) { + if headers.contains(&"digest") { + let digest = all_headers.get_one("digest").unwrap_or(""); + let digest = request::Digest::from_header(digest); + if digest.map(|d| d.verify(data)).unwrap_or(false) { + SignatureValidity::Valid + } else { + SignatureValidity::Invalid + } + } else { + SignatureValidity::ValidNoDigest + } + } else { + SignatureValidity::Invalid + } + } else { + SignatureValidity::Invalid + } + } else { + SignatureValidity::Absent + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct Id(String); diff --git a/plume-common/src/activity_pub/request.rs b/plume-common/src/activity_pub/request.rs index fa9313f9..da01760c 100644 --- a/plume-common/src/activity_pub/request.rs +++ b/plume-common/src/activity_pub/request.rs @@ -4,6 +4,7 @@ use reqwest::{ mime::Mime, header::{Accept, Date, Headers, UserAgent, qitem} }; +use std::ops::Deref; use std::time::SystemTime; use activity_pub::ap_accept_header; @@ -19,6 +20,48 @@ header! { (Digest, "Digest") => [String] } +impl Digest { + pub fn digest(body: String) -> Self { + let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap(); + hasher.update(&body.into_bytes()[..]).unwrap(); + let res = base64::encode(&hasher.finish().unwrap()); + Digest(format!("SHA-256={}", res)) + } + + pub fn verify(&self, body: String) -> bool { + if self.algorithm()=="SHA-256" { + let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap(); + hasher.update(&body.into_bytes()).unwrap(); + self.value().deref()==hasher.finish().unwrap().deref() + } else { + false //algorithm not supported + } + } + + pub fn algorithm(&self) -> &str { + let pos = self.0.find('=').unwrap(); + &self.0[..pos] + } + + pub fn value(&self) -> Vec { + let pos = self.0.find('=').unwrap()+1; + base64::decode(&self.0[pos..]).unwrap() + } + + pub fn from_header(dig: &str) -> Result { + if let Some(pos) = dig.find('=') { + let pos = pos+1; + if let Ok(_) = base64::decode(&dig[pos..]) { + Ok(Digest(dig.to_owned())) + } else { + Err(()) + } + } else { + Err(()) + } + } +} + pub fn headers() -> Headers { let mut headers = Headers::new(); headers.set(UserAgent::new(USER_AGENT)); @@ -41,10 +84,3 @@ pub fn signature(signer: &S, headers: Headers) -> Signature { signature = sign )) } - -pub fn digest(body: String) -> Digest { - let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap(); - hasher.update(&body.into_bytes()[..]).unwrap(); - let res = base64::encode(&hasher.finish().unwrap()); - Digest(format!("SHA-256={}", res)) -} diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 58d01b55..3016e97b 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -607,7 +607,7 @@ impl Signer for User { fn verify(&self, data: String, signature: Vec) -> bool { let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()).unwrap(); - let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); + let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); verifier.update(data.as_bytes()).unwrap(); verifier.verify(&signature).unwrap() } diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 0db72f20..16672caf 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -1,9 +1,12 @@ use gettextrs::gettext; -use rocket::{request::LenientForm, response::Redirect}; +use rocket::{http::HeaderMap, Outcome, + request::{self, FromRequest, LenientForm, Request}, + response::Redirect}; use rocket_contrib::{Json, Template}; use serde_json; use validator::{Validate}; +use plume_common::activity_pub::{verify_http_headers, SignatureValidity}; use plume_models::{ admin::Admin, comments::Comment, @@ -12,7 +15,6 @@ use plume_models::{ users::User, safe_string::SafeString, instance::* - }; use inbox::Inbox; use routes::Page; @@ -189,13 +191,41 @@ fn ban(_admin: Admin, conn: DbConn, id: i32) -> Redirect { Redirect::to(uri!(admin_users)) } +struct Headers<'r> { + headers: HeaderMap<'r>, +} + +impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { + type Error = (); + + fn from_request(request: &'a Request<'r>) ->request::Outcome { + let mut headers = HeaderMap::new(); + for header in request.headers().clone().into_iter() { + headers.add(header); + } + Outcome::Success(Headers{headers}) + } +} + #[post("/inbox", data = "")] -fn shared_inbox(conn: DbConn, data: String) -> String { +fn shared_inbox(conn: DbConn, data: String, headers: Headers) -> String { let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); let activity = act.clone(); let actor_id = activity["actor"].as_str() .unwrap_or_else(|| activity["actor"]["id"].as_str().expect("No actor ID for incoming activity, blocks by panicking")); + + let sig = match verify_http_headers(&User::from_url(&conn, actor_id.to_owned()).unwrap(), headers.headers, data) { + SignatureValidity::Valid => true, + _ => { + // TODO verify json signature + false + } + }; + if !sig { + return "invalid signature".to_owned(); + } + if Instance::is_blocked(&*conn, actor_id.to_string()) { return String::new(); }