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:
parent
aa72334dc6
commit
c4a4ea5b6c
|
@ -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
|
||||||
|
|
|
@ -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 = _)))
|
||||||
|
|
|
@ -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")))? {
|
||||||
|
|
|
@ -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 = _)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)? {
|
||||||
|
|
Loading…
Reference in New Issue