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()?), | ||||
|                     users::avatar_id.eq(avatar.map(|a| a.id)), | ||||
|                     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) | ||||
|                 .map(|_| ()) | ||||
| @ -638,6 +642,30 @@ impl User { | ||||
|         ).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> { | ||||
|         let mut actor = Person::default(); | ||||
|         actor | ||||
|  | ||||
| @ -7,6 +7,8 @@ use rocket_i18n::I18n; | ||||
| use validator::Validate; | ||||
| use template_utils::Ructe; | ||||
| 
 | ||||
| use std::time::Duration; | ||||
| 
 | ||||
| use plume_common::{utils, activity_pub::{broadcast, ApRequest, 
 | ||||
|     ActivityStream, inbox::Deletable}}; | ||||
| 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 { | ||||
|             let dest = User::one_by_instance(&*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 = _))) | ||||
|  | ||||
| @ -10,6 +10,7 @@ use plume_models::{ | ||||
|     admin::Admin, | ||||
|     comments::Comment, | ||||
|     db_conn::DbConn, | ||||
|     Error, | ||||
|     headers::Headers, | ||||
|     posts::Post, | ||||
|     users::User, | ||||
| @ -180,16 +181,26 @@ pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<R | ||||
| #[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>> { | ||||
|     let act = data.1.into_inner(); | ||||
|     let sig = data.0; | ||||
| 
 | ||||
|     let activity = act.clone(); | ||||
|     let actor_id = activity["actor"].as_str() | ||||
|         .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"); | ||||
|     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) { | ||||
|         println!("Rejected invalid activity supposedly from {}, with headers {:?}", actor.username, headers.0); | ||||
|         return Err(status::BadRequest(Some("Invalid signature"))); | ||||
|         // 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); | ||||
|                 status::BadRequest(Some("Invalid signature"))})?; | ||||
|     } | ||||
| 
 | ||||
|     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::response::{Redirect, Flash}; | ||||
| 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 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 { | ||||
|             let dest = User::one_by_instance(&*conn)?; | ||||
|             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 = _))) | ||||
|         } | ||||
|  | ||||
| @ -370,6 +370,7 @@ pub fn inbox( | ||||
| ) -> Result<String, Option<status::BadRequest<&'static str>>> { | ||||
|     let user = User::find_local(&*conn, &name).map_err(|_| None)?; | ||||
|     let act = data.1.into_inner(); | ||||
|     let sig = data.0; | ||||
| 
 | ||||
|     let activity = act.clone(); | ||||
|     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"); | ||||
|     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) | ||||
|     { | ||||
|         println!( | ||||
|             "Rejected invalid activity supposedly from {}, with headers {:?}", | ||||
|             actor.username, headers.0 | ||||
|         ); | ||||
|         return Err(Some(status::BadRequest(Some("Invalid signature")))); | ||||
|         // maybe we just know an old key?
 | ||||
|         actor.refetch(&conn).and_then(|_| User::get(&conn, actor.id)) | ||||
|             .and_then(|actor| if verify_http_headers(&actor, &headers.0, &sig).is_secure() | ||||
|                       || act.clone().verify(&actor) | ||||
|                     { | ||||
|                         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)? { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user