diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index c0ad233..5949c86 100644 --- a/plume-common/src/activity_pub/mod.rs +++ b/plume-common/src/activity_pub/mod.rs @@ -1,10 +1,8 @@ use activitypub::{Activity, Actor, Object, Link}; use array_tool::vec::Uniq; -use base64; use reqwest::Client; use rocket::{ - Outcome, - http::{Status,HeaderMap}, + Outcome, http::Status, response::{Response, Responder}, request::{FromRequest, Request} }; @@ -120,68 +118,6 @@ pub fn broadcast(send } } -#[derive(Debug)] -pub enum SignatureValidity { - Invalid, - ValidNoDigest, - Valid, - Absent, -} - -impl SignatureValidity { - pub fn is_secure(&self) -> bool { - match self { - SignatureValidity::Valid => true, - _ => false, - } - } -} - -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/sign.rs b/plume-common/src/activity_pub/sign.rs index 9d2f39c..fa9252e 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -6,6 +6,8 @@ use openssl::{ rsa::Rsa, sha::sha256 }; +use super::request; +use rocket::http::HeaderMap; use serde_json; /// Returns (public key, private key) @@ -26,6 +28,7 @@ pub trait Signer { pub trait Signable { fn sign(&mut self, creator: &T) -> &mut Self where T: Signer; + fn verify(self, creator: &T) -> bool where T: Signer; fn hash(data: String) -> String { let bytes = data.into_bytes(); @@ -55,4 +58,85 @@ impl Signable for serde_json::Value { self["signature"] = options; self } + + fn verify(mut self, creator: &T) -> bool { + let signature_obj = if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) { + sig + } else { + //signature not present + return false + }; + let signature = if let Ok(sig) = base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) { + sig + } else { + return false + }; + let creation_date = &signature_obj["created"]; + let options_hash = Self::hash(json!({ + "@context": "https://w3id.org/identity/v1", + "created": creation_date + }).to_string()); + let document_hash = Self::hash(self.to_string()); + let to_be_signed = options_hash + &document_hash; + creator.verify(to_be_signed, signature) + } +} + +#[derive(Debug,Copy,Clone,PartialEq)] +pub enum SignatureValidity { + Invalid, + ValidNoDigest, + Valid, + Absent, +} + +impl SignatureValidity { + pub fn is_secure(&self) -> bool { + self==&SignatureValidity::Valid + } +} + +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 + } } diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 8f592ed..901bf71 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -4,7 +4,8 @@ use rocket_contrib::{Json, Template}; use serde_json; use validator::{Validate}; -use plume_common::activity_pub::verify_http_headers; +use plume_common::activity_pub::sign::{Signable, + verify_http_headers}; use plume_models::{ admin::Admin, comments::Comment, @@ -198,9 +199,9 @@ fn shared_inbox(conn: DbConn, data: String, headers: Headers) -> String { 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 = verify_http_headers(&User::from_url(&conn, actor_id.to_owned()).unwrap(), headers.0, data).is_secure(); - if !sig { - // TODO check for valid json-ld signature + let actor = User::from_url(&conn, actor_id.to_owned()).unwrap(); + if !verify_http_headers(&actor, headers.0, data).is_secure() && + !act.clone().verify(&actor) { return "invalid signature".to_owned(); } diff --git a/src/routes/user.rs b/src/routes/user.rs index c206e51..8d655f3 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -16,7 +16,8 @@ use workerpool::thunk::*; use plume_common::activity_pub::{ ActivityStream, broadcast, Id, IntoId, ApRequest, - verify_http_headers, inbox::{FromActivity, Notify, Deletable} + inbox::{FromActivity, Notify, Deletable}, + sign::{Signable, verify_http_headers} }; use plume_common::utils; use plume_models::{ @@ -304,9 +305,9 @@ fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> String { let actor_id = activity["actor"].as_str() .unwrap_or_else(|| activity["actor"]["id"].as_str().expect("User: No actor ID for incoming activity, blocks by panicking")); - let sig = verify_http_headers(&User::from_url(&conn, actor_id.to_owned()).unwrap(), headers.0, data).is_secure(); - if !sig { - // TODO check for json-ld signature + let actor = User::from_url(&conn, actor_id.to_owned()).unwrap(); + if !verify_http_headers(&actor, headers.0, data).is_secure() && + !act.clone().verify(&actor) { return "invalid signature".to_owned(); }