Merge pull request #256 from Plume-org/verify-signature
Verify activity's signature
This commit is contained in:
commit
8fdb55a501
|
@ -2,8 +2,7 @@ use activitypub::{Activity, Actor, Object, Link};
|
||||||
use array_tool::vec::Uniq;
|
use array_tool::vec::Uniq;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Outcome,
|
Outcome, http::Status,
|
||||||
http::Status,
|
|
||||||
response::{Response, Responder},
|
response::{Response, Responder},
|
||||||
request::{FromRequest, Request}
|
request::{FromRequest, Request}
|
||||||
};
|
};
|
||||||
|
@ -104,11 +103,12 @@ 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();
|
||||||
|
headers.set(request::Digest::digest(signed.to_string()));
|
||||||
let res = Client::new()
|
let res = Client::new()
|
||||||
.post(&inbox[..])
|
.post(&inbox[..])
|
||||||
.headers(request::headers())
|
.headers(headers.clone())
|
||||||
.header(request::signature(sender, request::headers()))
|
.header(request::signature(sender, headers))
|
||||||
.header(request::digest(signed.to_string()))
|
|
||||||
.body(signed.to_string())
|
.body(signed.to_string())
|
||||||
.send();
|
.send();
|
||||||
match res {
|
match res {
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ use openssl::{
|
||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sha::sha256
|
sha::sha256
|
||||||
};
|
};
|
||||||
|
use super::request;
|
||||||
|
use rocket::http::HeaderMap;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
/// Returns (public key, private key)
|
/// Returns (public key, private key)
|
||||||
|
@ -20,10 +22,13 @@ pub trait Signer {
|
||||||
|
|
||||||
/// Sign some data with the signer keypair
|
/// Sign some data with the signer keypair
|
||||||
fn sign(&self, to_sign: String) -> Vec<u8>;
|
fn sign(&self, to_sign: String) -> Vec<u8>;
|
||||||
|
/// Verify if the signature is valid
|
||||||
|
fn verify(&self, data: String, signature: Vec<u8>) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Signable {
|
pub trait Signable {
|
||||||
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
|
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
|
||||||
|
fn verify<T>(self, creator: &T) -> bool where T: Signer;
|
||||||
|
|
||||||
fn hash(data: String) -> String {
|
fn hash(data: String) -> String {
|
||||||
let bytes = data.into_bytes();
|
let bytes = data.into_bytes();
|
||||||
|
@ -53,4 +58,86 @@ impl Signable for serde_json::Value {
|
||||||
self["signature"] = options;
|
self["signature"] = options;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify<T: Signer>(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<S: Signer+::std::fmt::Debug>(sender: &S, all_headers: HeaderMap, data: String) -> SignatureValidity{
|
||||||
|
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::<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())) {
|
||||||
|
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::Valid// all check passed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
pkey::{PKey, Private},
|
pkey::{PKey, Private},
|
||||||
rsa::Rsa,
|
rsa::Rsa,
|
||||||
sign::Signer
|
sign::{Signer,Verifier}
|
||||||
};
|
};
|
||||||
use webfinger::*;
|
use webfinger::*;
|
||||||
|
|
||||||
|
@ -309,6 +309,13 @@ impl sign::Signer for Blog {
|
||||||
signer.update(to_sign.as_bytes()).unwrap();
|
signer.update(to_sign.as_bytes()).unwrap();
|
||||||
signer.sign_to_vec().unwrap()
|
signer.sign_to_vec().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 mut verifier = Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||||
|
verifier.update(data.as_bytes()).unwrap();
|
||||||
|
verifier.verify(&signature).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewBlog {
|
impl NewBlog {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
use rocket::request::{self, FromRequest, Request};
|
||||||
|
use rocket::{http::{Header, 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<Self, ()> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
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)));
|
||||||
|
Outcome::Success(Headers(headers))
|
||||||
|
}
|
||||||
|
}
|
|
@ -219,6 +219,7 @@ pub mod blogs;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod db_conn;
|
pub mod db_conn;
|
||||||
pub mod follows;
|
pub mod follows;
|
||||||
|
pub mod headers;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod likes;
|
pub mod likes;
|
||||||
pub mod medias;
|
pub mod medias;
|
||||||
|
|
|
@ -624,6 +624,13 @@ impl Signer for User {
|
||||||
signer.update(to_sign.as_bytes()).unwrap();
|
signer.update(to_sign.as_bytes()).unwrap();
|
||||||
signer.sign_to_vec().unwrap()
|
signer.sign_to_vec().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
|
||||||
|
verifier.update(data.as_bytes()).unwrap();
|
||||||
|
verifier.verify(&signature).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewUser {
|
impl NewUser {
|
||||||
|
|
|
@ -4,15 +4,17 @@ use rocket_contrib::{Json, Template};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use validator::{Validate};
|
use validator::{Validate};
|
||||||
|
|
||||||
|
use plume_common::activity_pub::sign::{Signable,
|
||||||
|
verify_http_headers};
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
admin::Admin,
|
admin::Admin,
|
||||||
comments::Comment,
|
comments::Comment,
|
||||||
db_conn::DbConn,
|
db_conn::DbConn,
|
||||||
|
headers::Headers,
|
||||||
posts::Post,
|
posts::Post,
|
||||||
users::User,
|
users::User,
|
||||||
safe_string::SafeString,
|
safe_string::SafeString,
|
||||||
instance::*
|
instance::*
|
||||||
|
|
||||||
};
|
};
|
||||||
use inbox::Inbox;
|
use inbox::Inbox;
|
||||||
use routes::Page;
|
use routes::Page;
|
||||||
|
@ -194,12 +196,20 @@ fn ban(_admin: Admin, conn: DbConn, id: i32) -> Redirect {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 actor = User::from_url(&conn, actor_id.to_owned()).unwrap();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
if Instance::is_blocked(&*conn, actor_id.to_string()) {
|
if Instance::is_blocked(&*conn, actor_id.to_string()) {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,15 @@ use workerpool::thunk::*;
|
||||||
|
|
||||||
use plume_common::activity_pub::{
|
use plume_common::activity_pub::{
|
||||||
ActivityStream, broadcast, Id, IntoId, ApRequest,
|
ActivityStream, broadcast, Id, IntoId, ApRequest,
|
||||||
inbox::{FromActivity, Notify, Deletable}
|
inbox::{FromActivity, Notify, Deletable},
|
||||||
|
sign::{Signable, verify_http_headers}
|
||||||
};
|
};
|
||||||
use plume_common::utils;
|
use plume_common::utils;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
blogs::Blog,
|
blogs::Blog,
|
||||||
db_conn::DbConn,
|
db_conn::DbConn,
|
||||||
follows,
|
follows,
|
||||||
|
headers::Headers,
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
posts::Post,
|
posts::Post,
|
||||||
reshares::Reshare,
|
reshares::Reshare,
|
||||||
|
@ -295,13 +297,21 @@ fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/@/<name>/inbox", data = "<data>")]
|
#[post("/@/<name>/inbox", data = "<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 user = User::find_local(&*conn, name).unwrap();
|
||||||
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("User: No actor ID for incoming activity, blocks by panicking"));
|
.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.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();
|
||||||
|
}
|
||||||
|
|
||||||
if Instance::is_blocked(&*conn, actor_id.to_string()) {
|
if Instance::is_blocked(&*conn, actor_id.to_string()) {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue