From eafe1ed4900a5676ee000822842ffc33d856b753 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Fri, 28 Sep 2018 22:02:15 +0200 Subject: [PATCH 1/8] Add content digest to signed http headers Sign the Digest HTTP header --- plume-common/src/activity_pub/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index fbaf3c1a..612964ea 100644 --- a/plume-common/src/activity_pub/mod.rs +++ b/plume-common/src/activity_pub/mod.rs @@ -104,11 +104,12 @@ 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())); let res = Client::new() .post(&inbox[..]) - .headers(request::headers()) - .header(request::signature(sender, request::headers())) - .header(request::digest(signed.to_string())) + .headers(headers.clone()) + .header(request::signature(sender, headers)) .body(signed.to_string()) .send(); match res { From d610ed164117e08c5946496628ef0622953285d9 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Fri, 28 Sep 2018 23:18:01 +0200 Subject: [PATCH 2/8] Add verify() to the Signer trait And implement it for Blog and User --- plume-common/src/activity_pub/sign.rs | 2 ++ plume-models/src/blogs.rs | 9 ++++++++- plume-models/src/users.rs | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs index eb82a326..9d2f39ce 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -20,6 +20,8 @@ pub trait Signer { /// Sign some data with the signer keypair fn sign(&self, to_sign: String) -> Vec; + /// Verify if the signature is valid + fn verify(&self, data: String, signature: Vec) -> bool; } pub trait Signable { diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs index c825f6aa..3018d090 100644 --- a/plume-models/src/blogs.rs +++ b/plume-models/src/blogs.rs @@ -12,7 +12,7 @@ use openssl::{ hash::MessageDigest, pkey::{PKey, Private}, rsa::Rsa, - sign::Signer + sign::{Signer,Verifier} }; use webfinger::*; @@ -309,6 +309,13 @@ impl sign::Signer for Blog { signer.update(to_sign.as_bytes()).unwrap(); signer.sign_to_vec().unwrap() } + + 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 = Verifier::new(MessageDigest::sha256(), &key).unwrap(); + verifier.update(data.as_bytes()).unwrap(); + verifier.verify(&signature).unwrap() + } } impl NewBlog { diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index d8addb19..58d01b55 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -604,6 +604,13 @@ impl Signer for User { signer.update(to_sign.as_bytes()).unwrap(); signer.sign_to_vec().unwrap() } + + 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(); + verifier.update(data.as_bytes()).unwrap(); + verifier.verify(&signature).unwrap() + } } impl NewUser { From 0a5d435249f9f4b4df7f42fc40578af302a91954 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Wed, 3 Oct 2018 09:31:38 +0200 Subject: [PATCH 3/8] Verify http signatures --- plume-common/src/activity_pub/mod.rs | 58 +++++++++++++++++++++++- plume-common/src/activity_pub/request.rs | 50 +++++++++++++++++--- plume-models/src/users.rs | 2 +- src/routes/instance.rs | 36 +++++++++++++-- 4 files changed, 133 insertions(+), 13 deletions(-) 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(); } From 62c94ed463dd01f9df5c1fdb0eea9f991acb3996 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Wed, 3 Oct 2018 20:48:25 +0200 Subject: [PATCH 4/8] Refactor and verify http signature on personnal inbox Verify signature on personnal inbox Reduce code duplication Put Headers in plume-models --- plume-common/src/activity_pub/mod.rs | 9 ++++++++ plume-models/src/headers.rs | 17 +++++++++++++++ plume-models/src/lib.rs | 1 + src/routes/instance.rs | 32 +++++----------------------- src/routes/user.rs | 12 +++++++++-- 5 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 plume-models/src/headers.rs diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index 9b614f13..c0ad233a 100644 --- a/plume-common/src/activity_pub/mod.rs +++ b/plume-common/src/activity_pub/mod.rs @@ -128,6 +128,15 @@ pub enum SignatureValidity { 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; diff --git a/plume-models/src/headers.rs b/plume-models/src/headers.rs new file mode 100644 index 00000000..d94b86a5 --- /dev/null +++ b/plume-models/src/headers.rs @@ -0,0 +1,17 @@ +use rocket::request::{self, FromRequest, Request}; +use rocket::{http::HeaderMap, Outcome}; + + +pub struct Headers<'r>(pub 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)) + } +} diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index 71c4f4e5..0afdd738 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -110,6 +110,7 @@ pub mod blogs; pub mod comments; pub mod db_conn; pub mod follows; +pub mod headers; pub mod instance; pub mod likes; pub mod medias; diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 16672caf..8f592ed9 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -1,16 +1,15 @@ use gettextrs::gettext; -use rocket::{http::HeaderMap, Outcome, - request::{self, FromRequest, LenientForm, Request}, - response::Redirect}; +use rocket::{request::LenientForm, response::Redirect}; use rocket_contrib::{Json, Template}; use serde_json; use validator::{Validate}; -use plume_common::activity_pub::{verify_http_headers, SignatureValidity}; +use plume_common::activity_pub::verify_http_headers; use plume_models::{ admin::Admin, comments::Comment, db_conn::DbConn, + headers::Headers, posts::Post, users::User, safe_string::SafeString, @@ -191,22 +190,6 @@ 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, headers: Headers) -> String { let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); @@ -215,14 +198,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 = match verify_http_headers(&User::from_url(&conn, actor_id.to_owned()).unwrap(), headers.headers, data) { - SignatureValidity::Valid => true, - _ => { - // TODO verify json signature - false - } - }; + 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 return "invalid signature".to_owned(); } diff --git a/src/routes/user.rs b/src/routes/user.rs index 42ac6230..c206e51b 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -16,13 +16,14 @@ use workerpool::thunk::*; use plume_common::activity_pub::{ ActivityStream, broadcast, Id, IntoId, ApRequest, - inbox::{FromActivity, Notify, Deletable} + verify_http_headers, inbox::{FromActivity, Notify, Deletable} }; use plume_common::utils; use plume_models::{ blogs::Blog, db_conn::DbConn, follows, + headers::Headers, instance::Instance, posts::Post, reshares::Reshare, @@ -295,13 +296,20 @@ fn outbox(name: String, conn: DbConn) -> ActivityStream { } #[post("/@//inbox", data = "")] -fn inbox(name: String, conn: DbConn, data: String) -> String { +fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> String { let user = User::find_local(&*conn, name).unwrap(); 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("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 + return "invalid signature".to_owned(); + } + if Instance::is_blocked(&*conn, actor_id.to_string()) { return String::new(); } From 3466e55548a26ad8d6e50827be21218ab6707299 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Sat, 6 Oct 2018 10:06:06 +0200 Subject: [PATCH 5/8] Implement JSON-ld signature verification Implement JSON-ld signature verification Move signature verification functions to the proper file --- plume-common/src/activity_pub/mod.rs | 66 +-------------------- plume-common/src/activity_pub/sign.rs | 84 +++++++++++++++++++++++++++ src/routes/instance.rs | 9 +-- src/routes/user.rs | 9 +-- 4 files changed, 95 insertions(+), 73 deletions(-) diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index c0ad233a..5949c867 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 9d2f39ce..fa9252e0 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 8f592ed9..901bf713 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 c206e51b..8d655f3b 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(); } From 0d6a2af851b43d9798a80812bdf807f17800407e Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Sat, 6 Oct 2018 10:57:37 +0200 Subject: [PATCH 6/8] Code style improvement --- plume-common/src/activity_pub/sign.rs | 79 ++++++++++++++------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs index fa9252e0..427f50f2 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -97,46 +97,47 @@ impl SignatureValidity { } 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 + let sig_header = all_headers.get_one("Signature"); + if sig_header.is_none() { + return SignatureValidity::Absent + } + let sig_header = sig_header.unwrap(); + + 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_none() || headers.is_none() {//missing part of the header + return SignatureValidity::Invalid + } + 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())) { + return SignatureValidity::Invalid + } + if !headers.contains(&"digest") {// signature is valid, but body content is not verified + return SignatureValidity::ValidNoDigest + } + 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) {// signature was valid, but body content does not match its digest + SignatureValidity::Invalid } else { - SignatureValidity::Absent + SignatureValidity::Valid// all check passed } } From ba4695f490077cdb00a1a65ab6850defa266202a Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Wed, 10 Oct 2018 21:10:43 +0200 Subject: [PATCH 7/8] Add support for signature verification on pseudo header Add support for pseudo-header '(request-target)' Add some logging for denied request --- plume-models/src/headers.rs | 12 +++++++++++- src/routes/instance.rs | 3 ++- src/routes/user.rs | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/plume-models/src/headers.rs b/plume-models/src/headers.rs index d94b86a5..a3b046bd 100644 --- a/plume-models/src/headers.rs +++ b/plume-models/src/headers.rs @@ -1,5 +1,5 @@ use rocket::request::{self, FromRequest, Request}; -use rocket::{http::HeaderMap, Outcome}; +use rocket::{http::{Header, HeaderMap}, Outcome}; pub struct Headers<'r>(pub HeaderMap<'r>); @@ -12,6 +12,16 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { for header in request.headers().clone().into_iter() { headers.add(header); } + let ori = request.uri(); + let uri = if let Some(query) = ori.query() { + format!("{}?{}", ori.path(), query) + } else { + ori.path().to_owned() + }; + headers.add(Header::new("(request-target)", + format!("{} {}", + request.method().as_str().to_lowercase(), + uri.to_lowercase()))); Outcome::Success(Headers(headers)) } } diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 901bf713..d1e45cfa 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -200,8 +200,9 @@ fn shared_inbox(conn: DbConn, data: String, headers: Headers) -> String { .unwrap_or_else(|| activity["actor"]["id"].as_str().expect("No actor ID for incoming activity, blocks by panicking")); let actor = User::from_url(&conn, actor_id.to_owned()).unwrap(); - if !verify_http_headers(&actor, headers.0, data).is_secure() && + if !verify_http_headers(&actor, headers.0.clone(), data).is_secure() && !act.clone().verify(&actor) { + println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); return "invalid signature".to_owned(); } diff --git a/src/routes/user.rs b/src/routes/user.rs index 8d655f3b..90c8986f 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -306,8 +306,9 @@ fn inbox(name: String, conn: DbConn, data: String, headers: Headers) -> String { .unwrap_or_else(|| activity["actor"]["id"].as_str().expect("User: No actor ID for incoming activity, blocks by panicking")); let actor = User::from_url(&conn, actor_id.to_owned()).unwrap(); - if !verify_http_headers(&actor, headers.0, data).is_secure() && + if !verify_http_headers(&actor, headers.0.clone(), data).is_secure() && !act.clone().verify(&actor) { + println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); return "invalid signature".to_owned(); } From 09e7226c237b4e950975a962305d8b369160cf67 Mon Sep 17 00:00:00 2001 From: Baptiste Gelez Date: Wed, 10 Oct 2018 20:18:10 +0000 Subject: [PATCH 8/8] Don't lowercase URL for requests signatures --- plume-models/src/headers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plume-models/src/headers.rs b/plume-models/src/headers.rs index a3b046bd..57030deb 100644 --- a/plume-models/src/headers.rs +++ b/plume-models/src/headers.rs @@ -21,7 +21,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { headers.add(Header::new("(request-target)", format!("{} {}", request.method().as_str().to_lowercase(), - uri.to_lowercase()))); + uri))); Outcome::Success(Headers(headers)) } }