From dae6e9437bb3ce6b70c1cf0b73aff7c1477f95e2 Mon Sep 17 00:00:00 2001 From: silverpill Date: Tue, 8 Nov 2022 21:59:45 +0000 Subject: [PATCH] Use general Did type intsead of DidPkh in identity proofs --- src/activitypub/actors/attachments.rs | 7 ++- src/activitypub/authentication.rs | 8 ++- src/identity/did.rs | 79 +++++++++++++++++++++++++++ src/identity/did_pkh.rs | 29 +--------- src/identity/mod.rs | 1 + src/mastodon_api/accounts/types.rs | 6 +- src/mastodon_api/accounts/views.rs | 18 +++--- src/mastodon_api/search/helpers.rs | 6 +- src/models/profiles/queries.rs | 23 +++++--- src/models/profiles/types.rs | 9 +-- src/models/users/queries.rs | 7 ++- src/models/users/types.rs | 6 +- 12 files changed, 135 insertions(+), 64 deletions(-) create mode 100644 src/identity/did.rs diff --git a/src/activitypub/actors/attachments.rs b/src/activitypub/actors/attachments.rs index 001f421..b7e5b72 100644 --- a/src/activitypub/actors/attachments.rs +++ b/src/activitypub/actors/attachments.rs @@ -11,7 +11,7 @@ use crate::ethereum::identity::{ verify_eip191_identity_proof, }; use crate::frontend::get_subscription_page_url; -use crate::identity::did_pkh::DidPkh; +use crate::identity::did::Did; use crate::models::profiles::types::{ ExtraField, IdentityProof, @@ -45,13 +45,14 @@ pub fn parse_identity_proof( if proof_type != ETHEREUM_EIP191_PROOF { return Err(ValidationError("unknown proof type")); }; - let did = attachment.name.parse::() + let did = attachment.name.parse::() .map_err(|_| ValidationError("invalid did"))?; + let Did::Pkh(ref did_pkh) = did; let signature = attachment.signature_value.as_ref() .ok_or(ValidationError("missing signature"))?; verify_eip191_identity_proof( actor_id, - &did, + did_pkh, signature, ).map_err(|_| ValidationError("invalid identity proof"))?; let proof = IdentityProof { diff --git a/src/activitypub/authentication.rs b/src/activitypub/authentication.rs index 8597dcc..6910f90 100644 --- a/src/activitypub/authentication.rs +++ b/src/activitypub/authentication.rs @@ -9,6 +9,7 @@ use crate::http_signatures::verify::{ verify_http_signature, HttpSignatureVerificationError as HttpSignatureError, }; +use crate::identity::did::Did; use crate::json_signatures::verify::{ get_json_signature, verify_jcs_rsa_signature, @@ -136,8 +137,9 @@ pub async fn verify_signed_activity( verify_jcs_rsa_signature(&signature_data, &public_key)?; actor_profile }, - JsonSigner::DidPkh(ref signer) => { - let mut profiles: Vec<_> = search_profiles_by_did_only(db_client, signer) + JsonSigner::DidPkh(did_pkh) => { + let did = Did::Pkh(did_pkh.clone()); + let mut profiles: Vec<_> = search_profiles_by_did_only(db_client, &did) .await?.into_iter() // Exclude local profiles .filter(|profile| !profile.is_local()) @@ -150,7 +152,7 @@ pub async fn verify_signed_activity( }; if let Some(profile) = profiles.pop() { verify_jcs_eip191_signature( - signer, + &did_pkh, &signature_data.message, &signature_data.signature, )?; diff --git a/src/identity/did.rs b/src/identity/did.rs new file mode 100644 index 0000000..4e14a0f --- /dev/null +++ b/src/identity/did.rs @@ -0,0 +1,79 @@ +/// https://www.w3.org/TR/did-core/ +use std::fmt; +use std::str::FromStr; + +use regex::Regex; +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::Error as DeserializerError, +}; + +use super::did_pkh::DidPkh; + +const DID_RE: &str = r"did:(?P\w+):.+"; + +#[derive(Clone, Debug, PartialEq)] +pub enum Did { + Pkh(DidPkh), +} + +#[derive(thiserror::Error, Debug)] +#[error("DID parse error")] +pub struct DidParseError; + +impl FromStr for Did { + type Err = DidParseError; + + fn from_str(value: &str) -> Result { + let did_re = Regex::new(DID_RE).unwrap(); + let caps = did_re.captures(value).ok_or(DidParseError)?; + let did = match &caps["method"] { + "pkh" => { + let did_pkh = DidPkh::from_str(value)?; + Self::Pkh(did_pkh) + }, + _ => return Err(DidParseError), + }; + Ok(did) + } +} + +impl fmt::Display for Did { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let did_str = match self { + Self::Pkh(did_pkh) => did_pkh.to_string(), + }; + write!(formatter, "{}", did_str) + } +} + +impl<'de> Deserialize<'de> for Did { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> + { + let did_str: String = Deserialize::deserialize(deserializer)?; + did_str.parse().map_err(DeserializerError::custom) + } +} + +impl Serialize for Did { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + let did_str = self.to_string(); + serializer.serialize_str(&did_str) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_did_string_conversion() { + let did_str = "did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"; + let did: Did = did_str.parse().unwrap(); + assert!(matches!(did, Did::Pkh(_))); + assert_eq!(did.to_string(), did_str); + } +} diff --git a/src/identity/did_pkh.rs b/src/identity/did_pkh.rs index 5f1cc15..f3e42c7 100644 --- a/src/identity/did_pkh.rs +++ b/src/identity/did_pkh.rs @@ -3,13 +3,10 @@ use std::convert::TryInto; use std::str::FromStr; use regex::Regex; -use serde::{ - Deserialize, Deserializer, Serialize, Serializer, - de::Error as DeserializerError, -}; use crate::utils::caip2::ChainId; use crate::utils::currencies::Currency; +use super::did::DidParseError; #[derive(Clone, Debug, PartialEq)] pub struct DidPkh { @@ -43,10 +40,6 @@ impl ToString for DidPkh { } } -#[derive(thiserror::Error, Debug)] -#[error("DID parse error")] -pub struct DidParseError; - // https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md#syntax const DID_PKH_RE: &str = r"did:pkh:(?P[-a-z0-9]{3,8}):(?P[-a-zA-Z0-9]{1,32}):(?P
[a-zA-Z0-9]{1,64})"; @@ -67,30 +60,12 @@ impl FromStr for DidPkh { } } -impl Serialize for DidPkh { - fn serialize(&self, serializer: S) -> Result - where S: Serializer - { - let did_str = self.to_string(); - serializer.serialize_str(&did_str) - } -} - -impl<'de> Deserialize<'de> for DidPkh { - fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> - { - let did_str: String = Deserialize::deserialize(deserializer)?; - did_str.parse().map_err(DeserializerError::custom) - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn test_did_string_conversion() { + fn test_did_pkh_string_conversion() { let address = "0xB9C5714089478a327F09197987f16f9E5d936E8a"; let ethereum = Currency::Ethereum; let did = DidPkh::from_address(ðereum, address); diff --git a/src/identity/mod.rs b/src/identity/mod.rs index 2073aa9..71660b9 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -1 +1,2 @@ +pub mod did; pub mod did_pkh; diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index 18ebdb5..4292cf9 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::errors::ValidationError; +use crate::identity::did::Did; use crate::mastodon_api::pagination::PageSize; use crate::mastodon_api::uploads::{UploadError, save_validated_b64_file}; use crate::models::profiles::types::{ @@ -78,12 +79,13 @@ impl Account { let mut identity_proofs = vec![]; for proof in profile.identity_proofs.clone().into_inner() { + let Did::Pkh(did_pkh) = proof.issuer; // Skip proof if it doesn't map to field name - if let Some(currency) = proof.issuer.currency() { + if let Some(currency) = did_pkh.currency() { let field_name = currency.field_name(); let field = AccountField { name: field_name, - value: proof.issuer.address, + value: did_pkh.address, // Use current time because DID proofs are always valid verified_at: Some(Utc::now()), }; diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 0b32274..7245915 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -25,7 +25,7 @@ use crate::ethereum::identity::{ create_identity_claim, verify_eip191_identity_proof, }; -use crate::identity::did_pkh::DidPkh; +use crate::identity::did::Did; use crate::json_signatures::{ canonicalization::canonicalize_object, create::{add_integrity_proof, IntegrityProof}, @@ -249,7 +249,7 @@ async fn send_signed_update( ) -> 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::() + let signer = data.signer.parse::() .map_err(|_| ValidationError("invalid DID"))?; if !current_user.profile.identity_proofs.any(&signer) { return Err(ValidationError("unknown signer").into()); @@ -261,6 +261,7 @@ async fn send_signed_update( ).map_err(|_| HttpError::InternalError)?; let canonical_json = canonicalize_object(&activity) .map_err(|_| HttpError::InternalError)?; + let Did::Pkh(signer) = signer; verify_jcs_eip191_signature(&signer, &canonical_json, &data.signature) .map_err(|_| ValidationError("invalid signature"))?; let proof = IntegrityProof::jcs_eip191(&signer, &data.signature); @@ -290,7 +291,7 @@ async fn get_identity_claim( let db_client = &**get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; let actor_id = current_user.profile.actor_id(&config.instance_url()); - let did = query_params.did.parse::() + let Did::Pkh(did) = query_params.did.parse::() .map_err(|_| ValidationError("invalid DID"))?; let claim = create_identity_claim(&actor_id, &did) .map_err(|_| HttpError::InternalError)?; @@ -308,9 +309,10 @@ async fn create_identity_proof( let db_client = &**get_database_client(&db_pool).await?; let mut current_user = get_current_user(db_client, auth.token()).await?; let actor_id = current_user.profile.actor_id(&config.instance_url()); - let did = proof_data.did.parse::() + let did = proof_data.did.parse::() .map_err(|_| ValidationError("invalid DID"))?; - if did.chain_id != ChainId::ethereum_mainnet() { + let Did::Pkh(ref did_pkh) = did; + if did_pkh.chain_id != ChainId::ethereum_mainnet() { // DID must point to Ethereum Mainnet because it is a valid // identifier on any Ethereum chain return Err(ValidationError("unsupported chain ID").into()); @@ -319,7 +321,7 @@ async fn create_identity_proof( 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 { + if did_pkh.address != address { return Err(ValidationError("DID doesn't match current identity").into()); }; }; @@ -336,7 +338,7 @@ async fn create_identity_proof( }; verify_eip191_identity_proof( &actor_id, - &did, + did_pkh, &proof_data.signature, )?; let proof = IdentityProof { @@ -399,7 +401,7 @@ async fn search_by_did( query_params: web::Query, ) -> Result { let db_client = &**get_database_client(&db_pool).await?; - let did: DidPkh = query_params.did.parse() + let did: Did = query_params.did.parse() .map_err(|_| ValidationError("invalid DID"))?; let profiles = search_profiles_by_did(db_client, &did, false).await?; let accounts: Vec = profiles.into_iter() diff --git a/src/mastodon_api/search/helpers.rs b/src/mastodon_api/search/helpers.rs index ab24e9b..364878e 100644 --- a/src/mastodon_api/search/helpers.rs +++ b/src/mastodon_api/search/helpers.rs @@ -11,7 +11,7 @@ use crate::activitypub::fetcher::helpers::{ }; use crate::config::Config; use crate::errors::{ValidationError, HttpError}; -use crate::identity::did_pkh::DidPkh; +use crate::identity::did::Did; use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::statuses::helpers::build_status_list; use crate::mastodon_api::statuses::types::Tag; @@ -33,7 +33,7 @@ enum SearchQuery { TagQuery(String), Url(String), WalletAddress(String), - Did(DidPkh), + Did(Did), Unknown, } @@ -66,7 +66,7 @@ fn parse_tag_query(query: &str) -> Result { fn parse_search_query(search_query: &str) -> SearchQuery { let search_query = search_query.trim(); // DID is a valid URI so it should be tried before Url::parse - if let Ok(did) = DidPkh::from_str(search_query) { + if let Ok(did) = Did::from_str(search_query) { return SearchQuery::Did(did); }; if Url::parse(search_query).is_ok() { diff --git a/src/models/profiles/queries.rs b/src/models/profiles/queries.rs index 2a87e53..f30acad 100644 --- a/src/models/profiles/queries.rs +++ b/src/models/profiles/queries.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use crate::database::catch_unique_violation; use crate::database::query_macro::query; use crate::errors::DatabaseError; -use crate::identity::did_pkh::DidPkh; +use crate::identity::{did::Did, did_pkh::DidPkh}; use crate::models::cleanup::{ find_orphaned_files, find_orphaned_ipfs_objects, @@ -402,7 +402,7 @@ pub async fn search_profiles( pub async fn search_profiles_by_did_only( db_client: &impl GenericClient, - did: &DidPkh, + did: &Did, ) -> Result, DatabaseError> { let rows = db_client.query( " @@ -425,12 +425,17 @@ pub async fn search_profiles_by_did_only( pub async fn search_profiles_by_did( db_client: &impl GenericClient, - did: &DidPkh, + did: &Did, prefer_verified: bool, ) -> Result, DatabaseError> { - let did_str = did.to_string(); let verified = search_profiles_by_did_only(db_client, did).await?; - let unverified = if let Some(currency) = did.currency() { + let maybe_currency_address = match did { + Did::Pkh(did_pkh) => { + did_pkh.currency() + .map(|currency| (currency, did_pkh.address.clone())) + }, + }; + let unverified = if let Some((currency, address)) = maybe_currency_address { // If currency is Ethereum, // search over extra fields must be case insensitive. let value_op = match currency { @@ -457,9 +462,8 @@ pub async fn search_profiles_by_did( let field_name = currency.field_name(); let query = query!( &statement, - did=did_str, field_name=field_name, - field_value=did.address, + field_value=address, )?; let rows = db_client.query(query.sql(), query.parameters()).await?; let unverified = rows.iter() @@ -487,7 +491,8 @@ pub async fn search_profiles_by_wallet_address( wallet_address: &str, prefer_verified: bool, ) -> Result, DatabaseError> { - let did = DidPkh::from_address(currency, wallet_address); + let did_pkh = DidPkh::from_address(currency, wallet_address); + let did = Did::Pkh(did_pkh); search_profiles_by_did(db_client, &did, prefer_verified).await } @@ -723,7 +728,7 @@ mod tests { async fn test_search_profiles_by_wallet_address_identity_proof() { let db_client = &mut create_test_database().await; let identity_proof = IdentityProof { - issuer: DidPkh::from_address(ÐEREUM, "0x1234abcd"), + issuer: Did::Pkh(DidPkh::from_address(ÐEREUM, "0x1234abcd")), proof_type: "ethereum".to_string(), value: "13590013185bdea963".to_string(), }; diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index 33f5c66..5913994 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -14,7 +14,7 @@ use crate::activitypub::actors::types::{Actor, ActorAddress}; use crate::activitypub::identifiers::local_actor_id; use crate::database::json_macro::{json_from_sql, json_to_sql}; use crate::errors::{ConversionError, ValidationError}; -use crate::identity::did_pkh::DidPkh; +use crate::identity::did::Did; use crate::utils::caip2::ChainId; use super::validators::{ validate_username, @@ -25,7 +25,7 @@ use super::validators::{ #[derive(Clone, Debug, Deserialize, Serialize)] pub struct IdentityProof { - pub issuer: DidPkh, + pub issuer: Did, pub proof_type: String, pub value: String, } @@ -41,7 +41,7 @@ impl IdentityProofs { /// Returns true if identity proof list contains at least one proof /// created by a given DID. - pub fn any(&self, issuer: &DidPkh) -> bool { + pub fn any(&self, issuer: &Did) -> bool { let Self(identity_proofs) = self; identity_proofs.iter().any(|proof| proof.issuer == *issuer) } @@ -430,7 +430,8 @@ mod tests { fn test_identity_proof_serialization() { let json_data = r#"{"issuer":"did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a","proof_type":"ethereum-eip191-00","value":"dbfe"}"#; let proof: IdentityProof = serde_json::from_str(json_data).unwrap(); - assert_eq!(proof.issuer.address, "0xb9c5714089478a327f09197987f16f9e5d936e8a"); + let Did::Pkh(ref did_pkh) = proof.issuer; + assert_eq!(did_pkh.address, "0xb9c5714089478a327f09197987f16f9e5d936e8a"); let serialized = serde_json::to_string(&proof).unwrap(); assert_eq!(serialized, json_data); } diff --git a/src/models/users/queries.rs b/src/models/users/queries.rs index eac3e27..61d6663 100644 --- a/src/models/users/queries.rs +++ b/src/models/users/queries.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use crate::database::catch_unique_violation; use crate::errors::DatabaseError; -use crate::identity::did_pkh::DidPkh; +use crate::identity::{did::Did, did_pkh::DidPkh}; use crate::models::profiles::queries::create_profile; use crate::models::profiles::types::{DbActorProfile, ProfileCreateData}; use crate::utils::currencies::Currency; @@ -184,7 +184,7 @@ pub async fn get_user_by_login_address( pub async fn get_user_by_did( db_client: &impl GenericClient, - did: &DidPkh, + did: &Did, ) -> Result { // DIDs must be locally unique let maybe_row = db_client.query_opt( @@ -212,7 +212,8 @@ pub async fn get_user_by_public_wallet_address( currency: &Currency, wallet_address: &str, ) -> Result { - let did = DidPkh::from_address(currency, wallet_address); + let did_pkh = DidPkh::from_address(currency, wallet_address); + let did = Did::Pkh(did_pkh); get_user_by_did(db_client, &did).await } diff --git a/src/models/users/types.rs b/src/models/users/types.rs index a5eacd1..0d316f8 100644 --- a/src/models/users/types.rs +++ b/src/models/users/types.rs @@ -4,6 +4,7 @@ use regex::Regex; use uuid::Uuid; use crate::errors::ValidationError; +use crate::identity::did::Did; use crate::models::profiles::types::DbActorProfile; use crate::utils::currencies::Currency; @@ -48,11 +49,12 @@ impl User { /// Returns wallet address if it is verified pub fn public_wallet_address(&self, currency: &Currency) -> Option { for proof in self.profile.identity_proofs.clone().into_inner() { + let Did::Pkh(did_pkh) = proof.issuer; // Return the first matching address, because only // one proof per currency is allowed. - if let Some(ref address_currency) = proof.issuer.currency() { + if let Some(ref address_currency) = did_pkh.currency() { if address_currency == currency { - return Some(proof.issuer.address); + return Some(did_pkh.address); }; }; };