From 22cb286f86c6eee86a46a4b51fa54355a17c241c Mon Sep 17 00:00:00 2001 From: Bat Date: Thu, 3 May 2018 20:11:04 +0100 Subject: [PATCH] Signing activities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I hope it works correctly… Fixes #6 --- .../2018-05-03-182555_blogs_add_keys/down.sql | 3 ++ .../2018-05-03-182555_blogs_add_keys/up.sql | 3 ++ src/activity_pub/actor.rs | 20 ++++++++--- src/activity_pub/inbox.rs | 13 +++++-- src/activity_pub/outbox.rs | 5 +-- src/activity_pub/sign.rs | 13 +++++-- src/models/blogs.rs | 35 +++++++++++++++++-- src/models/users.rs | 27 +++++++------- src/routes/posts.rs | 2 +- src/routes/user.rs | 2 +- src/schema.rs | 2 ++ 11 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 migrations/2018-05-03-182555_blogs_add_keys/down.sql create mode 100644 migrations/2018-05-03-182555_blogs_add_keys/up.sql diff --git a/migrations/2018-05-03-182555_blogs_add_keys/down.sql b/migrations/2018-05-03-182555_blogs_add_keys/down.sql new file mode 100644 index 00000000..3646c18b --- /dev/null +++ b/migrations/2018-05-03-182555_blogs_add_keys/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN private_key; +ALTER TABLE blogs DROP COLUMN public_key; diff --git a/migrations/2018-05-03-182555_blogs_add_keys/up.sql b/migrations/2018-05-03-182555_blogs_add_keys/up.sql new file mode 100644 index 00000000..cbd45305 --- /dev/null +++ b/migrations/2018-05-03-182555_blogs_add_keys/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN private_key TEXT; +ALTER TABLE blogs ADD COLUMN public_key TEXT NOT NULL DEFAULT ''; diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs index f0f0595e..25f4effc 100644 --- a/src/activity_pub/actor.rs +++ b/src/activity_pub/actor.rs @@ -1,9 +1,11 @@ use diesel::PgConnection; use reqwest::Client; +use serde_json; use BASE_URL; use activity_pub::{activity_pub, ActivityPub, context, ap_url}; use activity_pub::activity::Activity; +use activity_pub::sign::*; use models::instance::Instance; pub enum ActorType { @@ -33,8 +35,12 @@ pub trait Actor: Sized { fn get_actor_type() -> ActorType; + fn custom_props(&self, _conn: &PgConnection) -> serde_json::Map { + serde_json::Map::new() + } + fn as_activity_pub (&self, conn: &PgConnection) -> ActivityPub { - activity_pub(json!({ + let mut repr = json!({ "@context": context(), "id": self.compute_id(conn), "type": Self::get_actor_type().to_string(), @@ -47,7 +53,11 @@ pub trait Actor: Sized { "endpoints": { "sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str())) } - })) + }); + + self.custom_props(conn).iter().for_each(|p| repr[p.0] = p.1.clone()); + + activity_pub(repr) } fn compute_outbox(&self, conn: &PgConnection) -> String { @@ -71,10 +81,12 @@ pub trait Actor: Sized { )) } - fn send_to_inbox(&self, conn: &PgConnection, act: A) { + fn send_to_inbox(&self, conn: &PgConnection, sender: &S, act: A) { + let mut act = act.serialize(); + let signed = act.sign(sender, conn); let res = Client::new() .post(&self.compute_inbox(conn)[..]) - .body(act.serialize().to_string()) + .body(signed.to_string()) .send(); match res { Ok(_) => println!("Successfully sent activity to inbox"), diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index e1971372..e00dbf7a 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -3,6 +3,7 @@ use serde_json; use activity_pub::activity; use activity_pub::actor::Actor; +use activity_pub::sign::*; use models::blogs::Blog; use models::follows::{Follow, NewFollow}; use models::posts::{Post, NewPost}; @@ -45,13 +46,21 @@ pub trait Inbox: Actor + Sized { } } - fn accept_follow(&self, conn: &PgConnection, from: &A, target: &B, follow: &T, from_id: i32, target_id: i32) { + fn accept_follow( + &self, + conn: &PgConnection, + from: &A, + target: &B, + follow: &T, + from_id: i32, + target_id: i32 + ) { Follow::insert(conn, NewFollow { follower_id: from_id, following_id: target_id }); let accept = activity::Accept::new(target, follow, conn); - from.send_to_inbox(conn, accept) + from.send_to_inbox(conn, target, accept) } } diff --git a/src/activity_pub/outbox.rs b/src/activity_pub/outbox.rs index 2902007c..3c991fdb 100644 --- a/src/activity_pub/outbox.rs +++ b/src/activity_pub/outbox.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use activity_pub::{activity_pub, ActivityPub, context}; use activity_pub::activity::Activity; use activity_pub::actor::Actor; +use activity_pub::sign::Signer; use models::users::User; pub struct Outbox { @@ -41,8 +42,8 @@ impl<'r> Responder<'r> for Outbox { } } -pub fn broadcast(conn: &PgConnection, act: A, to: Vec) { +pub fn broadcast(conn: &PgConnection, sender: &S, act: A, to: Vec) { for user in to { - user.send_to_inbox(conn, act.clone()); // TODO: run it in Sidekiq or something like that + user.send_to_inbox(conn, sender, act.clone()); // TODO: run it in Sidekiq or something like that } } diff --git a/src/activity_pub/sign.rs b/src/activity_pub/sign.rs index 48d470b0..bc0197fb 100644 --- a/src/activity_pub/sign.rs +++ b/src/activity_pub/sign.rs @@ -2,9 +2,18 @@ use base64; use diesel::PgConnection; use hex; use chrono::Utc; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; use openssl::sha::sha256; use serde_json; +/// Returns (public key, private key) +pub fn gen_keypair() -> (Vec, Vec) { + let keypair = Rsa::generate(2048).unwrap(); + let keypair = PKey::from_rsa(keypair).unwrap(); + (keypair.public_key_to_pem().unwrap(), keypair.private_key_to_pem_pkcs8().unwrap()) +} + pub trait Signer { fn get_key_id(&self, conn: &PgConnection) -> String; @@ -13,7 +22,7 @@ pub trait Signer { } pub trait Signable { - fn sign(&mut self, creator: T, conn: &PgConnection) -> &mut Self where T: Signer; + fn sign(&mut self, creator: &T, conn: &PgConnection) -> &mut Self where T: Signer; fn hash(data: String) -> String { let bytes = data.into_bytes(); @@ -22,7 +31,7 @@ pub trait Signable { } impl Signable for serde_json::Value { - fn sign(&mut self, creator: T, conn: &PgConnection) -> &mut serde_json::Value { + fn sign(&mut self, creator: &T, conn: &PgConnection) -> &mut serde_json::Value { let creation_date = Utc::now().to_rfc3339(); let mut options = json!({ "type": "RsaSignature2017", diff --git a/src/models/blogs.rs b/src/models/blogs.rs index d2bceb94..8f0825e6 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -1,10 +1,15 @@ use chrono::NaiveDateTime; use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection}; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; +use openssl::rsa::Rsa; +use openssl::sign::Signer; use std::sync::Arc; use activity_pub::activity::Activity; use activity_pub::actor::{Actor, ActorType}; use activity_pub::outbox::Outbox; +use activity_pub::sign; use activity_pub::webfinger::*; use models::instance::Instance; use schema::blogs; @@ -20,7 +25,9 @@ pub struct Blog { pub inbox_url: String, pub instance_id: i32, pub creation_date: NaiveDateTime, - pub ap_url: String + pub ap_url: String, + pub private_key: Option, + pub public_key: String } #[derive(Insertable)] @@ -32,7 +39,9 @@ pub struct NewBlog { pub outbox_url: String, pub inbox_url: String, pub instance_id: i32, - pub ap_url: String + pub ap_url: String, + pub private_key: Option, + pub public_key: String } impl Blog { @@ -86,6 +95,10 @@ impl Blog { fn get_activities(&self, _conn: &PgConnection) -> Vec> { vec![] } + + pub fn get_keypair(&self) -> PKey { + PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() + } } impl Actor for Blog { @@ -149,6 +162,19 @@ impl Webfinger for Blog { } } +impl sign::Signer for Blog { + fn get_key_id(&self, conn: &PgConnection) -> String { + format!("{}#main-key", self.compute_id(conn)) + } + + fn sign(&self, to_sign: String) -> Vec { + let key = self.get_keypair(); + let mut signer = Signer::new(MessageDigest::sha256(), &key).unwrap(); + signer.update(to_sign.as_bytes()).unwrap(); + signer.sign_to_vec().unwrap() + } +} + impl NewBlog { pub fn new_local( actor_id: String, @@ -156,6 +182,7 @@ impl NewBlog { summary: String, instance_id: i32 ) -> NewBlog { + let (pub_key, priv_key) = sign::gen_keypair(); NewBlog { actor_id: actor_id, title: title, @@ -163,7 +190,9 @@ impl NewBlog { outbox_url: String::from(""), inbox_url: String::from(""), instance_id: instance_id, - ap_url: String::from("") + ap_url: String::from(""), + public_key: String::from_utf8(pub_key).unwrap(), + private_key: Some(String::from_utf8(priv_key).unwrap()) } } } diff --git a/src/models/users.rs b/src/models/users.rs index 7f3a6dcd..95688792 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -5,7 +5,7 @@ use diesel::dsl::any; use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; -use openssl::sign::Signer; +use openssl::sign; use reqwest::Client; use reqwest::header::{Accept, qitem}; use reqwest::mime::Mime; @@ -20,7 +20,7 @@ use activity_pub::activity::{Create, Activity}; use activity_pub::actor::{ActorType, Actor}; use activity_pub::inbox::Inbox; use activity_pub::outbox::Outbox; -use activity_pub::sign; +use activity_pub::sign::{Signer, gen_keypair}; use activity_pub::webfinger::{Webfinger, resolve}; use db_conn::DbConn; use models::follows::Follow; @@ -263,6 +263,16 @@ impl Actor for User { ActorType::Person } + fn custom_props(&self, conn: &PgConnection) -> serde_json::Map { + let mut res = serde_json::Map::new(); + res.insert("publicKey".to_string(), json!({ + "id": self.get_key_id(conn), + "owner": self.compute_id(conn), + "publicKeyPem": self.public_key + })); + res + } + fn from_url(conn: &PgConnection, url: String) -> Option { let in_db = users::table.filter(users::ap_url.eq(url.clone())) .limit(1) @@ -318,14 +328,14 @@ impl Webfinger for User { } } -impl sign::Signer for User { +impl Signer for User { fn get_key_id(&self, conn: &PgConnection) -> String { format!("{}#main-key", self.compute_id(conn)) } fn sign(&self, to_sign: String) -> Vec { let key = self.get_keypair(); - let mut signer = Signer::new(MessageDigest::sha256(), &key).unwrap(); + let mut signer = sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); signer.update(to_sign.as_bytes()).unwrap(); signer.sign_to_vec().unwrap() } @@ -342,7 +352,7 @@ impl NewUser { password: String, instance_id: i32 ) -> NewUser { - let (pub_key, priv_key) = NewUser::gen_keypair(); + let (pub_key, priv_key) = gen_keypair(); NewUser { username: username, display_name: display_name, @@ -358,11 +368,4 @@ impl NewUser { private_key: Some(String::from_utf8(priv_key).unwrap()) } } - - // Returns (public key, private key) - fn gen_keypair() -> (Vec, Vec) { - let keypair = Rsa::generate(2048).unwrap(); - let keypair = PKey::from_rsa(keypair).unwrap(); - (keypair.public_key_to_pem().unwrap(), keypair.private_key_to_pem_pkcs8().unwrap()) - } } diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 9c2ad4d8..5dcb8482 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -56,7 +56,7 @@ fn create(blog_name: String, data: Form, user: User, conn: DbConn) }); let act = Create::new(&user, &post, &*conn); - broadcast(&*conn, act, user.get_followers(&*conn)); + broadcast(&*conn, &user, act, user.get_followers(&*conn)); Redirect::to(format!("/~/{}/{}", blog_name, slug).as_str()) } diff --git a/src/routes/user.rs b/src/routes/user.rs index 08cf2fca..13b5f9f4 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -34,7 +34,7 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect { follower_id: user.id, following_id: target.id }); - target.send_to_inbox(&*conn, activity::Follow::new(&user, &target, &*conn)); + target.send_to_inbox(&*conn, &user, activity::Follow::new(&user, &target, &*conn)); Redirect::to(format!("/@/{}", name).as_ref()) } diff --git a/src/schema.rs b/src/schema.rs index 30a65d8d..f44dfdb3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -18,6 +18,8 @@ table! { instance_id -> Int4, creation_date -> Timestamp, ap_url -> Text, + private_key -> Nullable, + public_key -> Text, } }