Federate identity proofs as actor attachments

https://codeberg.org/silverpill/mitra/issues/7
This commit is contained in:
silverpill 2022-04-16 18:48:00 +00:00
parent 83fbbefaab
commit 7a47c28034
6 changed files with 155 additions and 47 deletions

View file

@ -2,7 +2,13 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::config::Instance; use crate::config::Instance;
use crate::models::profiles::types::ExtraField; use crate::errors::ValidationError;
use crate::ethereum::identity::{
ETHEREUM_EIP191_PROOF,
DidPkh,
verify_identity_proof,
};
use crate::models::profiles::types::{ExtraField, IdentityProof};
use crate::models::users::types::User; use crate::models::users::types::User;
use crate::utils::crypto::{deserialize_private_key, get_public_key_pem}; use crate::utils::crypto::{deserialize_private_key, get_public_key_pem};
use crate::utils::files::get_file_url; use crate::utils::files::get_file_url;
@ -14,7 +20,7 @@ use super::views::{
get_followers_url, get_followers_url,
get_following_url, get_following_url,
}; };
use super::vocabulary::{IMAGE, PERSON, PROPERTY_VALUE, SERVICE}; use super::vocabulary::{IDENTITY_PROOF, IMAGE, PERSON, PROPERTY_VALUE, SERVICE};
const W3ID_CONTEXT: &str = "https://w3id.org/security/v1"; const W3ID_CONTEXT: &str = "https://w3id.org/security/v1";
@ -43,11 +49,21 @@ pub struct ActorCapabilities {
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ActorProperty { #[serde(rename_all = "camelCase")]
pub struct ActorAttachment {
name: String, name: String,
#[serde(rename = "type")] #[serde(rename = "type")]
object_type: String, object_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>, value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
signature_algorithm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
signature_value: Option<String>,
} }
// Clone and Debug traits are required by FromSql // Clone and Debug traits are required by FromSql
@ -90,36 +106,99 @@ pub struct Actor {
pub summary: Option<String>, pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub attachment: Option<Vec<ActorProperty>>, pub attachment: Option<Vec<ActorAttachment>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>, pub url: Option<String>,
} }
impl Actor { fn parse_identity_proof(
/// Parse 'attachment' into ExtraField vector actor_id: &str,
pub fn extra_fields(&self) -> Vec<ExtraField> { attachment: &ActorAttachment,
let mut extra_fields = vec![]; ) -> Result<IdentityProof, ValidationError> {
if let Some(properties) = &self.attachment { if attachment.object_type != IDENTITY_PROOF {
for property in properties { return Err(ValidationError("invalid attachment type"));
if property.object_type != PROPERTY_VALUE {
log::warn!(
"ignoring actor property of type {}",
property.object_type,
);
continue;
}; };
if let Some(property_value) = &property.value { let proof_type = attachment.signature_algorithm.as_ref()
.ok_or(ValidationError("missing proof type"))?;
if proof_type != ETHEREUM_EIP191_PROOF {
return Err(ValidationError("unknown proof type"));
};
let did = attachment.name.parse::<DidPkh>()
.map_err(|_| ValidationError("invalid did"))?;
let signature = attachment.signature_value.as_ref()
.ok_or(ValidationError("missing signature"))?;
verify_identity_proof(
actor_id,
&did,
signature,
).map_err(|_| ValidationError("invalid identity proof"))?;
let proof = IdentityProof {
issuer: did,
proof_type: proof_type.to_string(),
value: signature.to_string(),
};
Ok(proof)
}
fn parse_extra_field(
attachment: &ActorAttachment,
) -> Result<ExtraField, ValidationError> {
if attachment.object_type != PROPERTY_VALUE {
return Err(ValidationError("invalid attachment type"));
};
let property_value = attachment.value.as_ref()
.ok_or(ValidationError("missing property value"))?;
let field = ExtraField { let field = ExtraField {
name: property.name.clone(), name: attachment.name.clone(),
value: property_value.clone(), value: property_value.to_string(),
value_source: None, value_source: None,
}; };
extra_fields.push(field); Ok(field)
}
impl Actor {
pub fn parse_attachments(&self) -> (Vec<IdentityProof>, Vec<ExtraField>) {
let mut identity_proofs = vec![];
let mut extra_fields = vec![];
if let Some(attachments) = &self.attachment {
for attachment in attachments {
match attachment.object_type.as_str() {
IDENTITY_PROOF => {
match parse_identity_proof(&self.id, attachment) {
Ok(proof) => identity_proofs.push(proof),
Err(error) => {
log::warn!(
"ignoring actor attachment of type {}: {}",
attachment.object_type,
error,
);
},
};
},
PROPERTY_VALUE => {
match parse_extra_field(attachment) {
Ok(field) => extra_fields.push(field),
Err(error) => {
log::warn!(
"ignoring actor attachment of type {}: {}",
attachment.object_type,
error,
);
},
};
},
_ => {
log::warn!(
"ignoring actor attachment of type {}",
attachment.object_type,
);
},
}; };
}; };
}; };
extra_fields (identity_proofs, extra_fields)
} }
} }
@ -183,15 +262,27 @@ pub fn get_local_actor(
}, },
None => None, None => None,
}; };
let properties = user.profile.extra_fields.clone() let mut attachments = vec![];
.into_inner().into_iter() for proof in user.profile.identity_proofs.clone().into_inner() {
.map(|field| { let attachment = ActorAttachment {
ActorProperty { object_type: IDENTITY_PROOF.to_string(),
name: proof.issuer.to_string(),
value: None,
signature_algorithm: Some(proof.proof_type),
signature_value: Some(proof.value),
};
attachments.push(attachment);
};
for field in user.profile.extra_fields.clone().into_inner() {
let attachment = ActorAttachment {
object_type: PROPERTY_VALUE.to_string(), object_type: PROPERTY_VALUE.to_string(),
name: field.name, name: field.name,
value: Some(field.value), value: Some(field.value),
} signature_algorithm: None,
}).collect(); signature_value: None,
};
attachments.push(attachment);
};
let actor = Actor { let actor = Actor {
context: Some(json!([ context: Some(json!([
AP_CONTEXT.to_string(), AP_CONTEXT.to_string(),
@ -210,7 +301,7 @@ pub fn get_local_actor(
icon: avatar, icon: avatar,
image: banner, image: banner,
summary: None, summary: None,
attachment: Some(properties), attachment: Some(attachments),
url: Some(actor_id), url: Some(actor_id),
}; };
Ok(actor) Ok(actor)
@ -279,5 +370,6 @@ mod tests {
let actor = get_local_actor(&user, INSTANCE_URL).unwrap(); let actor = get_local_actor(&user, INSTANCE_URL).unwrap();
assert_eq!(actor.id, "https://example.com/users/testuser"); assert_eq!(actor.id, "https://example.com/users/testuser");
assert_eq!(actor.preferred_username, user.profile.username); assert_eq!(actor.preferred_username, user.profile.username);
assert_eq!(actor.attachment.unwrap().len(), 0);
} }
} }

View file

@ -146,7 +146,7 @@ pub async fn fetch_profile_by_actor_id(
let actor_json = send_request(instance, actor_url, &[]).await?; let actor_json = send_request(instance, actor_url, &[]).await?;
let actor: Actor = serde_json::from_str(&actor_json)?; let actor: Actor = serde_json::from_str(&actor_json)?;
let (avatar, banner) = fetch_avatar_and_banner(&actor, media_dir).await?; let (avatar, banner) = fetch_avatar_and_banner(&actor, media_dir).await?;
let extra_fields = actor.extra_fields(); let (identity_proofs, extra_fields) = actor.parse_attachments();
let actor_address = format!( let actor_address = format!(
"{}@{}", "{}@{}",
actor.preferred_username, actor.preferred_username,
@ -159,7 +159,7 @@ pub async fn fetch_profile_by_actor_id(
bio: actor.summary.clone(), bio: actor.summary.clone(),
avatar, avatar,
banner, banner,
identity_proofs: vec![], identity_proofs,
extra_fields, extra_fields,
actor_json: Some(actor), actor_json: Some(actor),
}; };

View file

@ -698,7 +698,7 @@ pub async fn receive_activity(
let profile = get_profile_by_actor_id(db_client, &actor.id).await?; let profile = get_profile_by_actor_id(db_client, &actor.id).await?;
let (avatar, banner) = fetch_avatar_and_banner(&actor, &config.media_dir()).await let (avatar, banner) = fetch_avatar_and_banner(&actor, &config.media_dir()).await
.map_err(|_| ValidationError("failed to fetch image"))?; .map_err(|_| ValidationError("failed to fetch image"))?;
let extra_fields = actor.extra_fields(); let (identity_proofs, extra_fields) = actor.parse_attachments();
let actor_old = profile.actor_json.unwrap(); let actor_old = profile.actor_json.unwrap();
if actor_old.id != actor.id { if actor_old.id != actor.id {
log::warn!( log::warn!(
@ -720,7 +720,7 @@ pub async fn receive_activity(
bio_source: actor.summary.clone(), bio_source: actor.summary.clone(),
avatar, avatar,
banner, banner,
identity_proofs: vec![], identity_proofs,
extra_fields, extra_fields,
actor_json: Some(actor), actor_json: Some(actor),
}; };

View file

@ -29,4 +29,5 @@ pub const ORDERED_COLLECTION_PAGE: &str = "OrderedCollectionPage";
// Misc // Misc
pub const HASHTAG: &str = "Hashtag"; pub const HASHTAG: &str = "Hashtag";
pub const IDENTITY_PROOF: &str = "IdentityProof";
pub const PROPERTY_VALUE: &str = "PropertyValue"; pub const PROPERTY_VALUE: &str = "PropertyValue";

View file

@ -1,11 +1,26 @@
use tokio_postgres::GenericClient; use tokio_postgres::GenericClient;
use uuid::Uuid; use uuid::Uuid;
use crate::activitypub::actor::Actor;
use crate::errors::DatabaseError; use crate::errors::DatabaseError;
use crate::models::relationships::queries::get_relationships; use crate::models::relationships::queries::{get_followers, get_relationships};
use crate::models::relationships::types::RelationshipType; use crate::models::relationships::types::RelationshipType;
use super::types::RelationshipMap; use super::types::RelationshipMap;
pub async fn get_profile_update_recipients(
db_client: &impl GenericClient,
current_user_id: &Uuid,
) -> Result<Vec<Actor>, DatabaseError> {
let followers = get_followers(db_client, current_user_id, None, None).await?;
let mut recipients: Vec<Actor> = Vec::new();
for profile in followers {
if let Some(remote_actor) = profile.actor_json {
recipients.push(remote_actor);
};
};
Ok(recipients)
}
pub async fn get_relationship( pub async fn get_relationship(
db_client: &impl GenericClient, db_client: &impl GenericClient,
source_id: &Uuid, source_id: &Uuid,

View file

@ -7,7 +7,6 @@ use crate::activitypub::activity::{
create_activity_undo_follow, create_activity_undo_follow,
create_activity_update_person, create_activity_update_person,
}; };
use crate::activitypub::actor::Actor;
use crate::activitypub::deliverer::deliver_activity; use crate::activitypub::deliverer::deliver_activity;
use crate::config::Config; use crate::config::Config;
use crate::database::{Pool, get_database_client}; use crate::database::{Pool, get_database_client};
@ -57,7 +56,7 @@ use crate::utils::crypto::{
serialize_private_key, serialize_private_key,
}; };
use crate::utils::files::FileError; use crate::utils::files::FileError;
use super::helpers::get_relationship; use super::helpers::{get_profile_update_recipients, get_relationship};
use super::types::{ use super::types::{
Account, Account,
AccountCreateData, AccountCreateData,
@ -210,13 +209,7 @@ async fn update_credentials(
// Federate // Federate
let activity = create_activity_update_person(&current_user, &config.instance_url()) let activity = create_activity_update_person(&current_user, &config.instance_url())
.map_err(|_| HttpError::InternalError)?; .map_err(|_| HttpError::InternalError)?;
let followers = get_followers(db_client, &current_user.id, None, None).await?; let recipients = get_profile_update_recipients(db_client, &current_user.id).await?;
let mut recipients: Vec<Actor> = Vec::new();
for follower in followers {
if let Some(remote_actor) = follower.actor_json {
recipients.push(remote_actor);
};
};
deliver_activity(&config, &current_user, activity, recipients); deliver_activity(&config, &current_user, activity, recipients);
let account = Account::from_user(current_user, &config.instance_url()); let account = Account::from_user(current_user, &config.instance_url());
@ -282,6 +275,13 @@ async fn create_identity_proof(
&current_user.id, &current_user.id,
profile_data, profile_data,
).await?; ).await?;
// Federate
let activity = create_activity_update_person(&current_user, &config.instance_url())
.map_err(|_| HttpError::InternalError)?;
let recipients = get_profile_update_recipients(db_client, &current_user.id).await?;
deliver_activity(&config, &current_user, activity, recipients);
let account = Account::from_user(current_user, &config.instance_url()); let account = Account::from_user(current_user, &config.instance_url());
Ok(HttpResponse::Ok().json(account)) Ok(HttpResponse::Ok().json(account))
} }