Support blind key rotation (#399)

* Allow receiving objects with new unknown key

* Rotate key after sending Delete activity

* Do the right check
This commit is contained in:
fdb-hiroshima 2019-01-05 22:30:28 +01:00 committed by Igor Galić
parent aa72334dc6
commit c4a4ea5b6c
5 changed files with 68 additions and 12 deletions

View file

@ -364,6 +364,10 @@ impl User {
.followers_string()?), .followers_string()?),
users::avatar_id.eq(avatar.map(|a| a.id)), users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()), users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json
.custom_props
.public_key_publickey()?
.public_key_pem_string()?),
)) ))
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
@ -638,6 +642,30 @@ impl User {
).map_err(Error::from) ).map_err(Error::from)
} }
pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> {
if self.private_key.is_none() {
return Err(Error::InvalidValue)
}
if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 {
//rotated recently
self.get_keypair()
} else {
let (public_key, private_key) = gen_keypair();
let public_key = String::from_utf8(public_key).expect("NewUser::new_local: public key error");
let private_key = String::from_utf8(private_key).expect("NewUser::new_local: private key error");
let res = PKey::from_rsa(
Rsa::private_key_from_pem(private_key.as_ref())?
)?;
diesel::update(self)
.set((users::public_key.eq(public_key),
users::private_key.eq(Some(private_key)),
users::last_fetched_date.eq(Utc::now().naive_utc())))
.execute(conn)
.map_err(Error::from)
.map(|_| res)
}
}
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
let mut actor = Person::default(); let mut actor = Person::default();
actor actor

View file

@ -7,6 +7,8 @@ use rocket_i18n::I18n;
use validator::Validate; use validator::Validate;
use template_utils::Ructe; use template_utils::Ructe;
use std::time::Duration;
use plume_common::{utils, activity_pub::{broadcast, ApRequest, use plume_common::{utils, activity_pub::{broadcast, ApRequest,
ActivityStream, inbox::Deletable}}; ActivityStream, inbox::Deletable}};
use plume_models::{ use plume_models::{
@ -104,7 +106,9 @@ pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, wor
if comment.author_id == user.id { if comment.author_id == user.id {
let dest = User::one_by_instance(&*conn)?; let dest = User::one_by_instance(&*conn)?;
let delete_activity = comment.delete(&*conn)?; let delete_activity = comment.delete(&*conn)?;
worker.execute(move || broadcast(&user, delete_activity, dest)); let user_c = user.clone();
worker.execute(move || broadcast(&user_c, delete_activity, dest));
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");});
} }
} }
Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))

View file

@ -10,6 +10,7 @@ use plume_models::{
admin::Admin, admin::Admin,
comments::Comment, comments::Comment,
db_conn::DbConn, db_conn::DbConn,
Error,
headers::Headers, headers::Headers,
posts::Post, posts::Post,
users::User, users::User,
@ -180,16 +181,26 @@ pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<R
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> { pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers: Headers, searcher: Searcher) -> Result<String, status::BadRequest<&'static str>> {
let act = data.1.into_inner(); let act = data.1.into_inner();
let sig = data.0;
let activity = act.clone(); let activity = act.clone();
let actor_id = activity["actor"].as_str() let actor_id = activity["actor"].as_str()
.or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?; .or_else(|| activity["actor"]["id"].as_str()).ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error"); let actor = User::from_url(&conn, actor_id).expect("instance::shared_inbox: user error");
if !verify_http_headers(&actor, &headers.0, &data.0).is_secure() && if !verify_http_headers(&actor, &headers.0, &sig).is_secure() &&
!act.clone().verify(&actor) { !act.clone().verify(&actor) {
// maybe we just know an old key?
actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id))
.and_then(|u| if verify_http_headers(&u, &headers.0, &sig).is_secure() ||
act.clone().verify(&u) {
Ok(())
} else {
Err(Error::Signature)
})
.map_err(|_| {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
return Err(status::BadRequest(Some("Invalid signature"))); status::BadRequest(Some("Invalid signature"))})?;
} }
if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? { if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? {

View file

@ -3,7 +3,10 @@ use heck::{CamelCase, KebabCase};
use rocket::request::LenientForm; use rocket::request::LenientForm;
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use std::{collections::{HashMap, HashSet}, borrow::Cow}; use std::{
collections::{HashMap, HashSet},
borrow::Cow, time::Duration,
};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, inbox::Deletable}; use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest, inbox::Deletable};
@ -397,7 +400,9 @@ pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker:
} else { } else {
let dest = User::one_by_instance(&*conn)?; let dest = User::one_by_instance(&*conn)?;
let delete_activity = post.delete(&(&conn, &searcher))?; let delete_activity = post.delete(&(&conn, &searcher))?;
worker.execute(move || broadcast(&user, delete_activity, dest)); let user_c = user.clone();
worker.execute(move || broadcast(&user_c, delete_activity, dest));
worker.execute_after(Duration::from_secs(10*60), move || {user.rotate_keypair(&conn).expect("Failed to rotate keypair");});
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
} }

View file

@ -370,6 +370,7 @@ pub fn inbox(
) -> Result<String, Option<status::BadRequest<&'static str>>> { ) -> Result<String, Option<status::BadRequest<&'static str>>> {
let user = User::find_local(&*conn, &name).map_err(|_| None)?; let user = User::find_local(&*conn, &name).map_err(|_| None)?;
let act = data.1.into_inner(); let act = data.1.into_inner();
let sig = data.0;
let activity = act.clone(); let activity = act.clone();
let actor_id = activity["actor"] let actor_id = activity["actor"]
@ -380,14 +381,21 @@ pub fn inbox(
))))?; ))))?;
let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error"); let actor = User::from_url(&conn, actor_id).expect("user::inbox: user error");
if !verify_http_headers(&actor, &headers.0, &data.0).is_secure() if !verify_http_headers(&actor, &headers.0, &sig).is_secure()
&& !act.clone().verify(&actor) && !act.clone().verify(&actor)
{ {
println!( // maybe we just know an old key?
"Rejected invalid activity supposedly from {}, with headers {:?}", actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id))
actor.username, headers.0 .and_then(|actor| if verify_http_headers(&actor, &headers.0, &sig).is_secure()
); || act.clone().verify(&actor)
return Err(Some(status::BadRequest(Some("Invalid signature")))); {
Ok(())
} else {
Err(Error::Signature)
})
.map_err(|_| {
println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0);
status::BadRequest(Some("Invalid signature"))})?;
} }
if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? { if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {