diff --git a/docs/openapi.yaml b/docs/openapi.yaml index f9a6026..19aaaac 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -98,13 +98,61 @@ paths: $ref: '#/components/schemas/Account' 400: description: Invalid user data + /api/v1/accounts/signed_update: + get: + summary: Build Update(Person) activity for signing (experimental). + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: object + properties: + internal_activity_id: + description: Internal activity ID. + type: string + format: uuid + activity: + description: Canonical representation of activity. + type: string + example: '{"type":"Update"}' + post: + summary: Send signed activity (experimental). + requestBody: + content: + application/json: + schema: + type: object + properties: + internal_activity_id: + description: Internal activity ID. + type: string + format: uuid + signer: + description: Signer's identifier (DID) + type: string + example: 'did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a' + signature: + description: Signature value. + type: string + example: '3312dacd...' + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + 400: + description: Invalid signature data. /api/v1/accounts/identity_proof: get: summary: Get unsigned data for identity proof. parameters: - name: did in: query - description: Identifier (DID) + description: Identifier (DID). required: true schema: type: string @@ -132,7 +180,7 @@ paths: type: object properties: did: - description: Identifier (DID) + description: Signer (DID). type: string example: 'did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a' signature: diff --git a/src/activitypub/builders/update_person.rs b/src/activitypub/builders/update_person.rs index 3eb713f..d6cca02 100644 --- a/src/activitypub/builders/update_person.rs +++ b/src/activitypub/builders/update_person.rs @@ -15,13 +15,16 @@ use crate::models::relationships::queries::get_followers; use crate::models::users::types::User; use crate::utils::id::new_uuid; -fn build_update_person( +pub fn build_update_person( instance_url: &str, user: &User, + maybe_internal_activity_id: Option, ) -> Result { let actor = get_local_actor(user, instance_url)?; // Update(Person) is idempotent so its ID can be random - let activity_id = local_object_id(instance_url, &new_uuid()); + let internal_activity_id = + maybe_internal_activity_id.unwrap_or(new_uuid()); + let activity_id = local_object_id(instance_url, &internal_activity_id); let activity = create_activity( instance_url, &user.profile.username, @@ -56,7 +59,7 @@ pub async fn prepare_update_person( instance: &Instance, user: &User, ) -> Result, DatabaseError> { - let activity = build_update_person(&instance.url(), user) + let activity = build_update_person(&instance.url(), user, None) .map_err(|_| ConversionError)?; let recipients = get_update_person_recipients(db_client, &user.id).await?; Ok(OutgoingActivity { diff --git a/src/json_signatures/mod.rs b/src/json_signatures/mod.rs index bec976e..b8af087 100644 --- a/src/json_signatures/mod.rs +++ b/src/json_signatures/mod.rs @@ -1,3 +1,3 @@ -mod canonicalization; +pub mod canonicalization; pub mod create; pub mod verify; diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index 3a449ee..18ebdb5 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -254,6 +254,19 @@ impl AccountUpdateData { } } +#[derive(Serialize)] +pub struct UnsignedUpdate { + pub internal_activity_id: Uuid, + pub activity: String, // canonical representation +} + +#[derive(Deserialize)] +pub struct SignedUpdate { + pub internal_activity_id: Uuid, + pub signer: String, + pub signature: String, +} + #[derive(Deserialize)] pub struct IdentityClaimQueryParams { pub did: String, diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index acfb5fe..b748c16 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -8,7 +8,10 @@ use uuid::Uuid; use crate::activitypub::builders::{ follow::prepare_follow, undo_follow::prepare_undo_follow, - update_person::prepare_update_person, + update_person::{ + build_update_person, + prepare_update_person, + }, }; use crate::config::Config; use crate::database::{Pool, get_database_client}; @@ -22,6 +25,7 @@ use crate::ethereum::identity::{ create_identity_claim, verify_identity_proof, }; +use crate::json_signatures::canonicalization::canonicalize_object; use crate::mastodon_api::oauth::auth::get_current_user; use crate::mastodon_api::pagination::get_paginated_response; use crate::mastodon_api::search::helpers::search_profiles_only; @@ -62,11 +66,13 @@ use crate::utils::crypto::{ serialize_private_key, }; use crate::utils::currencies::Currency; +use crate::utils::id::new_uuid; use super::helpers::get_relationship; use super::types::{ Account, AccountCreateData, AccountUpdateData, + ApiSubscription, FollowData, FollowListQueryParams, IdentityClaim, @@ -75,8 +81,9 @@ use super::types::{ RelationshipQueryParams, SearchAcctQueryParams, SearchDidQueryParams, + SignedUpdate, StatusListQueryParams, - ApiSubscription, + UnsignedUpdate, }; #[post("")] @@ -205,6 +212,52 @@ async fn update_credentials( Ok(HttpResponse::Ok().json(account)) } +#[get("/signed_update")] +async fn get_unsigned_update( + auth: BearerAuth, + config: web::Data, + db_pool: web::Data, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, auth.token()).await?; + let internal_activity_id = new_uuid(); + let activity = build_update_person( + &config.instance_url(), + ¤t_user, + Some(internal_activity_id), + ).map_err(|_| HttpError::InternalError)?; + let canonical_json = canonicalize_object(&activity) + .map_err(|_| HttpError::InternalError)?; + let data = UnsignedUpdate { + internal_activity_id, + activity: canonical_json, + }; + Ok(HttpResponse::Ok().json(data)) +} + +#[post("/signed_update")] +async fn send_signed_update( + auth: BearerAuth, + config: web::Data, + db_pool: web::Data, + data: web::Json, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, auth.token()).await?; + let signer = data.signer.parse::() + .map_err(|_| ValidationError("invalid DID"))?; + if !current_user.profile.identity_proofs.any(&signer) { + return Err(ValidationError("unknown signer").into()); + }; + let _activity = build_update_person( + &config.instance_url(), + ¤t_user, + Some(data.internal_activity_id), + ).map_err(|_| HttpError::InternalError)?; + let account = Account::from_user(current_user, &config.instance_url()); + Ok(HttpResponse::Ok().json(account)) +} + #[get("/identity_proof")] async fn get_identity_claim( auth: BearerAuth, @@ -243,10 +296,13 @@ async fn create_identity_proof( let maybe_public_address = current_user.public_wallet_address(&Currency::Ethereum); if let Some(address) = maybe_public_address { + // Do not allow to add more than one address proof if did.address != address { return Err(ValidationError("DID doesn't match current identity").into()); }; }; + // Reject proof if there's another local user with the same DID. + // This is needed for matching ethereum subscriptions match get_user_by_did(db_client, &did).await { Ok(user) => { if user.id != current_user.id { @@ -575,6 +631,8 @@ pub fn account_api_scope() -> Scope { .service(create_account) .service(verify_credentials) .service(update_credentials) + .service(get_unsigned_update) + .service(send_signed_update) .service(get_identity_claim) .service(create_identity_proof) .service(get_relationships_view) diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index f4202db..0765955 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -38,6 +38,13 @@ impl IdentityProofs { let Self(identity_proofs) = self; identity_proofs } + + /// Returns true if identity proof list contains at least one proof + /// created by a given DID. + pub fn any(&self, issuer: &DidPkh) -> bool { + let Self(identity_proofs) = self; + identity_proofs.iter().any(|proof| proof.issuer == *issuer) + } } json_from_sql!(IdentityProofs);