Verify http signatures

This commit is contained in:
Trinity Pointard 2018-10-03 09:31:38 +02:00
parent d610ed1641
commit 0a5d435249
4 changed files with 133 additions and 13 deletions

View File

@ -1,9 +1,10 @@
use activitypub::{Activity, Actor, Object, Link}; use activitypub::{Activity, Actor, Object, Link};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use base64;
use reqwest::Client; use reqwest::Client;
use rocket::{ use rocket::{
Outcome, Outcome,
http::Status, http::{Status,HeaderMap},
response::{Response, Responder}, response::{Response, Responder},
request::{FromRequest, Request} request::{FromRequest, Request}
}; };
@ -105,7 +106,7 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(send
for inbox in boxes { for inbox in boxes {
// TODO: run it in Sidekiq or something like that // TODO: run it in Sidekiq or something like that
let mut headers = request::headers(); let mut headers = request::headers();
headers.set(request::digest(signed.to_string())); headers.set(request::Digest::digest(signed.to_string()));
let res = Client::new() let res = Client::new()
.post(&inbox[..]) .post(&inbox[..])
.headers(headers.clone()) .headers(headers.clone())
@ -119,6 +120,59 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(send
} }
} }
#[derive(Debug)]
pub enum SignatureValidity {
Invalid,
ValidNoDigest,
Valid,
Absent,
}
pub fn verify_http_headers<S: sign::Signer+::std::fmt::Debug>(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::<Vec<_>>();
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::<Vec<_>>().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)] #[derive(Clone, Serialize, Deserialize)]
pub struct Id(String); pub struct Id(String);

View File

@ -4,6 +4,7 @@ use reqwest::{
mime::Mime, mime::Mime,
header::{Accept, Date, Headers, UserAgent, qitem} header::{Accept, Date, Headers, UserAgent, qitem}
}; };
use std::ops::Deref;
use std::time::SystemTime; use std::time::SystemTime;
use activity_pub::ap_accept_header; use activity_pub::ap_accept_header;
@ -19,6 +20,48 @@ header! {
(Digest, "Digest") => [String] (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<u8> {
let pos = self.0.find('=').unwrap()+1;
base64::decode(&self.0[pos..]).unwrap()
}
pub fn from_header(dig: &str) -> Result<Self, ()> {
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 { pub fn headers() -> Headers {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(UserAgent::new(USER_AGENT)); headers.set(UserAgent::new(USER_AGENT));
@ -41,10 +84,3 @@ pub fn signature<S: Signer>(signer: &S, headers: Headers) -> Signature {
signature = sign 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))
}

View File

@ -607,7 +607,7 @@ impl Signer for User {
fn verify(&self, data: String, signature: Vec<u8>) -> bool { fn verify(&self, data: String, signature: Vec<u8>) -> bool {
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()).unwrap(); 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.update(data.as_bytes()).unwrap();
verifier.verify(&signature).unwrap() verifier.verify(&signature).unwrap()
} }

View File

@ -1,9 +1,12 @@
use gettextrs::gettext; 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 rocket_contrib::{Json, Template};
use serde_json; use serde_json;
use validator::{Validate}; use validator::{Validate};
use plume_common::activity_pub::{verify_http_headers, SignatureValidity};
use plume_models::{ use plume_models::{
admin::Admin, admin::Admin,
comments::Comment, comments::Comment,
@ -12,7 +15,6 @@ use plume_models::{
users::User, users::User,
safe_string::SafeString, safe_string::SafeString,
instance::* instance::*
}; };
use inbox::Inbox; use inbox::Inbox;
use routes::Page; use routes::Page;
@ -189,13 +191,41 @@ fn ban(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
Redirect::to(uri!(admin_users)) 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<Self, ()> {
let mut headers = HeaderMap::new();
for header in request.headers().clone().into_iter() {
headers.add(header);
}
Outcome::Success(Headers{headers})
}
}
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<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 act: serde_json::Value = serde_json::from_str(&data[..]).unwrap();
let activity = act.clone(); let activity = act.clone();
let actor_id = activity["actor"].as_str() 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")); .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()) { if Instance::is_blocked(&*conn, actor_id.to_string()) {
return String::new(); return String::new();
} }